@@ -106,22 +106,29 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
import { ABOUT } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import RouteName from "../router/name";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import { useCurrentUserClient } from "@/composition/apollo/user";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
const { currentUser } = useCurrentUserClient();
|
||||
|
||||
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
|
||||
const { result: configResult } = useQuery<{ config: IConfig }>(ABOUT);
|
||||
|
||||
const config = computed(() => configResult.value?.config);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() =>
|
||||
t("About {instance}", { instance: config.value?.name })
|
||||
),
|
||||
});
|
||||
|
||||
// metaInfo() {
|
||||
// return {
|
||||
// title: this.t("About {instance}", {
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<o-button
|
||||
v-if="isUpdate"
|
||||
@click="openDeleteIdentityConfirmation()"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
>
|
||||
{{ $t("Delete this identity") }}
|
||||
</o-button>
|
||||
@@ -593,7 +593,7 @@ const dialog = inject<Dialog>("dialog");
|
||||
|
||||
const openRegenerateFeedTokensConfirmation = (): void => {
|
||||
dialog?.confirm({
|
||||
type: "is-warning",
|
||||
variant: "warning",
|
||||
title: t("Regenerate new links") as string,
|
||||
message: t(
|
||||
"You'll need to change the URLs where there were previously entered."
|
||||
@@ -606,7 +606,7 @@ const openRegenerateFeedTokensConfirmation = (): void => {
|
||||
|
||||
const openDeleteIdentityConfirmation = (): void => {
|
||||
dialog?.prompt({
|
||||
type: "danger",
|
||||
variant: "danger",
|
||||
title: t("Delete your identity") as string,
|
||||
message: `${t(
|
||||
"This will delete / anonymize all content (events, comments, messages, participations…) created from this identity."
|
||||
|
||||
@@ -112,33 +112,27 @@
|
||||
:label="t('Member')"
|
||||
v-slot="props"
|
||||
>
|
||||
<article class="media">
|
||||
<figure
|
||||
class="media-left image is-48x48"
|
||||
v-if="props.row.actor.avatar"
|
||||
>
|
||||
<img
|
||||
class="is-rounded"
|
||||
:src="props.row.actor.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
</figure>
|
||||
<o-icon
|
||||
class="media-left"
|
||||
v-else
|
||||
size="large"
|
||||
icon="account-circle"
|
||||
/>
|
||||
<div class="media-content">
|
||||
<article class="flex gap-1">
|
||||
<div class="flex-none">
|
||||
<figure v-if="props.row.actor.avatar">
|
||||
<img
|
||||
class="rounded"
|
||||
:src="props.row.actor.avatar.url"
|
||||
alt=""
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
</figure>
|
||||
<AccountCircle :size="48" v-else />
|
||||
</div>
|
||||
<div>
|
||||
<div class="prose dark:prose-invert">
|
||||
<span v-if="props.row.actor.name">{{
|
||||
props.row.actor.name
|
||||
}}</span
|
||||
><span v-else>@{{ usernameWithDomain(props.row.actor) }}</span
|
||||
><br />
|
||||
<span
|
||||
v-if="props.row.actor.name"
|
||||
class="is-size-7 has-text-grey"
|
||||
<span v-if="props.row.actor.name"
|
||||
>@{{ usernameWithDomain(props.row.actor) }}</span
|
||||
>
|
||||
</div>
|
||||
@@ -146,39 +140,39 @@
|
||||
</article>
|
||||
</o-table-column>
|
||||
<o-table-column field="role" :label="t('Role')" v-slot="props">
|
||||
<b-tag
|
||||
<tag
|
||||
variant="primary"
|
||||
v-if="props.row.role === MemberRole.ADMINISTRATOR"
|
||||
>
|
||||
{{ t("Administrator") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="primary"
|
||||
v-else-if="props.row.role === MemberRole.MODERATOR"
|
||||
>
|
||||
{{ t("Moderator") }}
|
||||
</b-tag>
|
||||
<b-tag v-else-if="props.row.role === MemberRole.MEMBER">
|
||||
</tag>
|
||||
<tag v-else-if="props.row.role === MemberRole.MEMBER">
|
||||
{{ t("Member") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="warning"
|
||||
v-else-if="props.row.role === MemberRole.NOT_APPROVED"
|
||||
>
|
||||
{{ t("Not approved") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === MemberRole.REJECTED"
|
||||
>
|
||||
{{ t("Rejected") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === MemberRole.INVITED"
|
||||
>
|
||||
{{ t("Invited") }}
|
||||
</b-tag>
|
||||
</tag>
|
||||
</o-table-column>
|
||||
<o-table-column field="insertedAt" :label="t('Date')" v-slot="props">
|
||||
<span class="has-text-centered">
|
||||
@@ -225,9 +219,7 @@
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: props.row.uuid } }"
|
||||
>
|
||||
{{ props.row.title }}
|
||||
<b-tag variant="info" v-if="props.row.draft">{{
|
||||
t("Draft")
|
||||
}}</b-tag>
|
||||
<tag variant="info" v-if="props.row.draft">{{ t("Draft") }}</tag>
|
||||
</router-link>
|
||||
</o-table-column>
|
||||
<o-table-column field="beginsOn" :label="t('Begins on')" v-slot="props">
|
||||
@@ -271,9 +263,7 @@
|
||||
:to="{ name: RouteName.POST, params: { slug: props.row.slug } }"
|
||||
>
|
||||
{{ props.row.title }}
|
||||
<b-tag variant="info" v-if="props.row.draft">{{
|
||||
t("Draft")
|
||||
}}</b-tag>
|
||||
<tag variant="info" v-if="props.row.draft">{{ t("Draft") }}</tag>
|
||||
</router-link>
|
||||
</o-table-column>
|
||||
<o-table-column
|
||||
@@ -295,7 +285,7 @@
|
||||
{{ t("This group was not found") }}
|
||||
<template #desc>
|
||||
<o-button
|
||||
type="is-text"
|
||||
variant="text"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.ADMIN_GROUPS }"
|
||||
>{{ t("Back to group list") }}</o-button
|
||||
@@ -330,6 +320,8 @@ import {
|
||||
} from "@/filters/datetime";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
|
||||
const EVENTS_PER_PAGE = 10;
|
||||
const POSTS_PER_PAGE = 10;
|
||||
@@ -396,23 +388,21 @@ const dialog = inject<Dialog>("dialog");
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
const confirmSuspendProfile = (): void => {
|
||||
const message = (
|
||||
group.value.domain
|
||||
? t(
|
||||
"Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.",
|
||||
{ instance: group.value.domain }
|
||||
)
|
||||
: t(
|
||||
"Are you sure you want to <b>suspend</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>."
|
||||
)
|
||||
) as string;
|
||||
const message = group.value.domain
|
||||
? t(
|
||||
"Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.",
|
||||
{ instance: group.value.domain }
|
||||
)
|
||||
: t(
|
||||
"Are you sure you want to <b>suspend</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>."
|
||||
);
|
||||
|
||||
dialog?.confirm({
|
||||
title: t("Suspend group") as string,
|
||||
title: t("Suspend group"),
|
||||
message,
|
||||
confirmText: t("Suspend group") as string,
|
||||
cancelText: t("Cancel") as string,
|
||||
type: "danger",
|
||||
confirmText: t("Suspend group"),
|
||||
cancelText: t("Cancel"),
|
||||
variant: "danger",
|
||||
hasIcon: true,
|
||||
onConfirm: () =>
|
||||
suspendProfile({
|
||||
|
||||
@@ -34,14 +34,14 @@
|
||||
<tr
|
||||
v-for="{ key, value, link } in metadata"
|
||||
:key="key"
|
||||
class="odd:bg-white even:bg-gray-50 border-b"
|
||||
class="odd:bg-white dark:odd:bg-zinc-800 even:bg-gray-50 dark:even:bg-zinc-700 border-b"
|
||||
>
|
||||
<td class="py-4 px-2 whitespace-nowrap">
|
||||
{{ key }}
|
||||
</td>
|
||||
<td
|
||||
v-if="link"
|
||||
class="py-4 px-2 text-sm text-gray-500 whitespace-nowrap"
|
||||
class="py-4 px-2 text-sm text-gray-500 dark:text-gray-200 whitespace-nowrap"
|
||||
>
|
||||
<router-link :to="link">
|
||||
{{ value }}
|
||||
@@ -49,7 +49,7 @@
|
||||
</td>
|
||||
<td
|
||||
v-else
|
||||
class="py-4 px-2 text-sm text-gray-500 whitespace-nowrap"
|
||||
class="py-4 px-2 text-sm text-gray-500 dark:text-gray-200 whitespace-nowrap"
|
||||
>
|
||||
{{ value }}
|
||||
</td>
|
||||
@@ -102,7 +102,7 @@
|
||||
<section class="mt-4 mb-3">
|
||||
<h2 class="">{{ $t("Organized events") }}</h2>
|
||||
<o-table
|
||||
:data="person.organizedEvents.elements"
|
||||
:data="person.organizedEvents?.elements"
|
||||
:loading="loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
@@ -111,7 +111,7 @@
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:total="person.organizedEvents.total"
|
||||
:total="person.organizedEvents?.total"
|
||||
:per-page="EVENTS_PER_PAGE"
|
||||
@page-change="onOrganizedEventsPageChange"
|
||||
>
|
||||
@@ -140,7 +140,7 @@
|
||||
<h2 class="">{{ $t("Participations") }}</h2>
|
||||
<o-table
|
||||
:data="
|
||||
person.participations.elements.map(
|
||||
person.participations?.elements.map(
|
||||
(participation) => participation.event
|
||||
)
|
||||
"
|
||||
@@ -152,7 +152,7 @@
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:total="person.participations.total"
|
||||
:total="person.participations?.total"
|
||||
:per-page="EVENTS_PER_PAGE"
|
||||
@page-change="onParticipationsPageChange"
|
||||
>
|
||||
@@ -180,7 +180,7 @@
|
||||
<section class="mt-4 mb-3">
|
||||
<h2 class="">{{ $t("Memberships") }}</h2>
|
||||
<o-table
|
||||
:data="person.memberships.elements"
|
||||
:data="person.memberships?.elements"
|
||||
:loading="loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
@@ -189,7 +189,7 @@
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:total="person.memberships.total"
|
||||
:total="person.memberships?.total"
|
||||
:per-page="EVENTS_PER_PAGE"
|
||||
@page-change="onMembershipsPageChange"
|
||||
>
|
||||
@@ -215,47 +215,45 @@
|
||||
props.row.parent.name
|
||||
}}</span
|
||||
><br />
|
||||
<span class="is-size-7 has-text-grey"
|
||||
>@{{ usernameWithDomain(props.row.parent) }}</span
|
||||
>
|
||||
<span>@{{ usernameWithDomain(props.row.parent) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</o-table-column>
|
||||
<o-table-column field="role" :label="$t('Role')" v-slot="props">
|
||||
<b-tag
|
||||
<tag
|
||||
variant="primary"
|
||||
v-if="props.row.role === MemberRole.ADMINISTRATOR"
|
||||
>
|
||||
{{ $t("Administrator") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="primary"
|
||||
v-else-if="props.row.role === MemberRole.MODERATOR"
|
||||
>
|
||||
{{ $t("Moderator") }}
|
||||
</b-tag>
|
||||
<b-tag v-else-if="props.row.role === MemberRole.MEMBER">
|
||||
</tag>
|
||||
<tag v-else-if="props.row.role === MemberRole.MEMBER">
|
||||
{{ $t("Member") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="warning"
|
||||
v-else-if="props.row.role === MemberRole.NOT_APPROVED"
|
||||
>
|
||||
{{ $t("Not approved") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === MemberRole.REJECTED"
|
||||
>
|
||||
{{ $t("Rejected") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === MemberRole.INVITED"
|
||||
>
|
||||
{{ $t("Invited") }}
|
||||
</b-tag>
|
||||
</tag>
|
||||
</o-table-column>
|
||||
<o-table-column field="insertedAt" :label="$t('Date')" v-slot="props">
|
||||
<span class="has-text-centered">
|
||||
@@ -276,7 +274,7 @@
|
||||
{{ $t("This profile was not found") }}
|
||||
<template #desc>
|
||||
<o-button
|
||||
type="is-text"
|
||||
variant="text"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.PROFILES }"
|
||||
>{{ $t("Back to profile list") }}</o-button
|
||||
@@ -310,6 +308,7 @@ import {
|
||||
formatDateTimeString,
|
||||
} from "@/filters/datetime";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
|
||||
const EVENTS_PER_PAGE = 10;
|
||||
const PARTICIPATIONS_PER_PAGE = 10;
|
||||
@@ -423,7 +422,7 @@ const { mutate: suspendProfile } = useMutation<
|
||||
});
|
||||
|
||||
if (!profileData) return;
|
||||
const { person } = profileData;
|
||||
const { person: cachedPerson } = profileData;
|
||||
store.writeQuery({
|
||||
query: GET_PERSON,
|
||||
variables: {
|
||||
@@ -431,7 +430,7 @@ const { mutate: suspendProfile } = useMutation<
|
||||
},
|
||||
data: {
|
||||
person: {
|
||||
...cloneDeep(person),
|
||||
...cloneDeep(cachedPerson),
|
||||
participations: { total: 0, elements: [] },
|
||||
suspended: true,
|
||||
avatar: null,
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<table v-if="metadata.length > 0" class="min-w-full">
|
||||
<tbody>
|
||||
<tr
|
||||
class="odd:bg-white even:bg-gray-50 border-b"
|
||||
class="border-b"
|
||||
v-for="{ key, value, type } in metadata"
|
||||
:key="key"
|
||||
>
|
||||
@@ -67,7 +67,7 @@
|
||||
size="small"
|
||||
v-if="!user.disabled"
|
||||
@click="isEmailChangeModalActive = true"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
icon-left="pencil"
|
||||
>{{ t("Change email") }}</o-button
|
||||
>
|
||||
@@ -78,7 +78,7 @@
|
||||
query: { emailFilter: `@${userEmailDomain}` },
|
||||
}"
|
||||
size="small"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
icon-left="magnify"
|
||||
>{{
|
||||
t("Other users with the same email domain")
|
||||
@@ -93,7 +93,7 @@
|
||||
size="small"
|
||||
v-if="!user.confirmedAt || user.disabled"
|
||||
@click="isConfirmationModalActive = true"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
icon-left="check"
|
||||
>{{ t("Confirm user") }}</o-button
|
||||
>
|
||||
@@ -106,7 +106,7 @@
|
||||
size="small"
|
||||
v-if="!user.disabled"
|
||||
@click="isRoleChangeModalActive = true"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
icon-left="chevron-double-up"
|
||||
>{{ t("Change role") }}</o-button
|
||||
>
|
||||
@@ -122,7 +122,7 @@
|
||||
query: { ipFilter: user.currentSignInIp },
|
||||
}"
|
||||
size="small"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
icon-left="web"
|
||||
>{{
|
||||
t("Other users with the same IP address")
|
||||
@@ -192,7 +192,7 @@
|
||||
</header>
|
||||
<section class="">
|
||||
<o-field :label="t('Previous email')">
|
||||
<o-input type="email" :value="user.email" disabled> </o-input>
|
||||
<o-input type="email" v-model="user.email" disabled />
|
||||
</o-field>
|
||||
<o-field :label="t('New email')">
|
||||
<o-input
|
||||
@@ -208,7 +208,7 @@
|
||||
}}</o-checkbox>
|
||||
</section>
|
||||
<footer class="mt-2 flex gap-2">
|
||||
<o-button @click="isEmailChangeModalActive = false">{{
|
||||
<o-button outlined @click="isEmailChangeModalActive = false">{{
|
||||
t("Close")
|
||||
}}</o-button>
|
||||
<o-button native-type="submit" variant="primary">{{
|
||||
@@ -309,7 +309,7 @@
|
||||
{{ t("This user was not found") }}
|
||||
<template #desc>
|
||||
<o-button
|
||||
type="is-text"
|
||||
variant="text"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.USERS }"
|
||||
>{{ t("Back to user list") }}</o-button
|
||||
@@ -459,7 +459,7 @@ const suspendAccount = async (): Promise<void> => {
|
||||
),
|
||||
confirmText: t("Suspend the account"),
|
||||
cancelText: t("Cancel"),
|
||||
type: "is-danger",
|
||||
variant: "danger",
|
||||
onConfirm: async () => {
|
||||
suspendUser({
|
||||
userId: props.id,
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<div>
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{ name: RouteName.MODERATION, text: $t('Moderation') },
|
||||
{ name: RouteName.MODERATION, text: t('Moderation') },
|
||||
{
|
||||
name: RouteName.ADMIN_GROUPS,
|
||||
text: $t('Groups'),
|
||||
text: t('Groups'),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
@@ -13,13 +13,13 @@
|
||||
<router-link
|
||||
class="button is-primary"
|
||||
:to="{ name: RouteName.CREATE_GROUP }"
|
||||
>{{ $t("Create group") }}</router-link
|
||||
>{{ t("Create group") }}</router-link
|
||||
>
|
||||
</div>
|
||||
<div v-if="groups">
|
||||
<div class="flex gap-2">
|
||||
<o-switch v-model="local">{{ $t("Local") }}</o-switch>
|
||||
<o-switch v-model="suspended">{{ $t("Suspended") }}</o-switch>
|
||||
<o-switch v-model="local">{{ t("Local") }}</o-switch>
|
||||
<o-switch v-model="suspended">{{ t("Suspended") }}</o-switch>
|
||||
</div>
|
||||
<o-table
|
||||
:data="groups.elements"
|
||||
@@ -29,10 +29,10 @@
|
||||
backend-filtering
|
||||
:debounce-search="200"
|
||||
v-model:current-page="page"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
:total="groups.total"
|
||||
:per-page="PROFILES_PER_PAGE"
|
||||
@page-change="onPageChange"
|
||||
@@ -40,14 +40,14 @@
|
||||
>
|
||||
<o-table-column
|
||||
field="preferredUsername"
|
||||
:label="$t('Username')"
|
||||
:label="t('Username')"
|
||||
searchable
|
||||
>
|
||||
<template #searchable="props">
|
||||
<o-input
|
||||
:aria-label="$t('Filter')"
|
||||
:aria-label="t('Filter')"
|
||||
v-model="props.filters.preferredUsername"
|
||||
:placeholder="$t('Filter')"
|
||||
:placeholder="t('Filter')"
|
||||
icon="magnify"
|
||||
/>
|
||||
</template>
|
||||
@@ -82,12 +82,12 @@
|
||||
</template>
|
||||
</o-table-column>
|
||||
|
||||
<o-table-column field="domain" :label="$t('Domain')" searchable>
|
||||
<o-table-column field="domain" :label="t('Domain')" searchable>
|
||||
<template #searchable="props">
|
||||
<o-input
|
||||
:aria-label="$t('Filter')"
|
||||
:aria-label="t('Filter')"
|
||||
v-model="props.filters.domain"
|
||||
:placeholder="$t('Filter')"
|
||||
:placeholder="t('Filter')"
|
||||
icon="magnify"
|
||||
/>
|
||||
</template>
|
||||
@@ -97,7 +97,7 @@
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content icon="account-group" :inline="true">
|
||||
{{ $t("No group matches the filters") }}
|
||||
{{ t("No group matches the filters") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
</o-table>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div
|
||||
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
|
||||
>
|
||||
<div class="bg-gray-50 rounded-xl p-8">
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILES,
|
||||
@@ -24,7 +24,7 @@
|
||||
<span class="text-sm block">{{ $t("Profiles") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-8">
|
||||
<div class="bg-gray-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_GROUPS,
|
||||
@@ -37,19 +37,19 @@
|
||||
<span class="text-sm block">{{ $t("Groups") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-8">
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followingsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Followings") }}</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-8">
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followersCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Followers") }}</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-8">
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{ name: RouteName.REPORTS, query: { domain: instance.domain } }"
|
||||
>
|
||||
@@ -59,7 +59,7 @@
|
||||
<span class="text-sm block">{{ $t("Reports") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-8">
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 font-semibold block">{{
|
||||
formatBytes(instance.mediaSize)
|
||||
}}</span>
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
<div class="mt-3 grid xl:grid-cols-2 gap-4">
|
||||
<div
|
||||
class="border bg-white p-6 shadow-md rounded-md"
|
||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md"
|
||||
v-if="instance.hasRelay"
|
||||
>
|
||||
<button
|
||||
@@ -104,7 +104,9 @@
|
||||
<div v-else class="md:h-48 py-16 text-center opacity-50">
|
||||
{{ $t("Only Mobilizon instances can be followed") }}
|
||||
</div>
|
||||
<div class="border bg-white p-6 shadow-md rounded-md flex flex-col gap-2">
|
||||
<div
|
||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2"
|
||||
>
|
||||
<button
|
||||
@click="
|
||||
acceptInstance({
|
||||
@@ -140,6 +142,7 @@ import {
|
||||
ADD_INSTANCE,
|
||||
INSTANCE,
|
||||
REJECT_RELAY,
|
||||
REMOVE_RELAY,
|
||||
} from "@/graphql/admin";
|
||||
import { formatBytes } from "@/utils/datetime";
|
||||
import RouteName from "@/router/name";
|
||||
@@ -215,7 +218,7 @@ onRejectInstanceError((error) => {
|
||||
});
|
||||
|
||||
const { mutate: followInstanceMutation, onError: onFollowInstanceError } =
|
||||
useMutation(ADD_INSTANCE);
|
||||
useMutation<{ addInstance: IInstance }>(ADD_INSTANCE);
|
||||
|
||||
onFollowInstanceError((error) => {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
@@ -232,10 +235,10 @@ const followInstance = async (e: Event): Promise<void> => {
|
||||
* Stop following instance
|
||||
*/
|
||||
const { mutate: removeInstanceFollow, onError: onRemoveInstanceFollowError } =
|
||||
useMutation(REJECT_RELAY, () => ({
|
||||
useMutation(REMOVE_RELAY, () => ({
|
||||
update(cache: ApolloCache<any>) {
|
||||
cache.writeFragment({
|
||||
id: cache.identify(instance as unknown as Reference),
|
||||
id: cache.identify(instance.value as unknown as Reference),
|
||||
fragment: gql`
|
||||
fragment InstanceFollowedStatus on Instance {
|
||||
followedStatus
|
||||
@@ -2,24 +2,29 @@
|
||||
<div>
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{ name: RouteName.ADMIN, text: $t('Admin') },
|
||||
{ text: $t('Instances') },
|
||||
{ name: RouteName.ADMIN, text: t('Admin') },
|
||||
{ text: t('Instances') },
|
||||
]"
|
||||
/>
|
||||
<section>
|
||||
<h1 class="title">{{ $t("Instances") }}</h1>
|
||||
<h1 class="title">{{ t("Instances") }}</h1>
|
||||
<form @submit="followInstance" class="my-4">
|
||||
<o-field :label="$t('Follow a new instance')" horizontal>
|
||||
<o-field
|
||||
:label="t('Follow a new instance')"
|
||||
horizontal
|
||||
label-for="newRelayAddress"
|
||||
>
|
||||
<o-field grouped group-multiline expanded size="large">
|
||||
<p class="control">
|
||||
<o-input
|
||||
id="newRelayAddress"
|
||||
v-model="newRelayAddress"
|
||||
:placeholder="$t('Ex: mobilizon.fr')"
|
||||
:placeholder="t('Ex: mobilizon.fr')"
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<o-button variant="primary" native-type="submit">{{
|
||||
$t("Add an instance")
|
||||
t("Add an instance")
|
||||
}}</o-button>
|
||||
<o-loading
|
||||
:is-full-page="true"
|
||||
@@ -31,31 +36,31 @@
|
||||
</o-field>
|
||||
</form>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<o-field :label="$t('Follow status')">
|
||||
<o-field :label="t('Follow status')">
|
||||
<o-radio
|
||||
v-model="followStatus"
|
||||
:native-value="InstanceFilterFollowStatus.ALL"
|
||||
>{{ $t("All") }}</o-radio
|
||||
>{{ t("All") }}</o-radio
|
||||
>
|
||||
<o-radio
|
||||
v-model="followStatus"
|
||||
:native-value="InstanceFilterFollowStatus.FOLLOWING"
|
||||
>{{ $t("Following") }}</o-radio
|
||||
>{{ t("Following") }}</o-radio
|
||||
>
|
||||
<o-radio
|
||||
v-model="followStatus"
|
||||
:native-value="InstanceFilterFollowStatus.FOLLOWED"
|
||||
>{{ $t("Followed") }}</o-radio
|
||||
>{{ t("Followed") }}</o-radio
|
||||
>
|
||||
</o-field>
|
||||
<o-field
|
||||
:label="$t('Domain')"
|
||||
:label="t('Domain')"
|
||||
label-for="domain-filter"
|
||||
class="flex-auto"
|
||||
>
|
||||
<o-input
|
||||
id="domain-filter"
|
||||
:placeholder="$t('mobilizon-instance.tld')"
|
||||
:placeholder="t('mobilizon-instance.tld')"
|
||||
:value="filterDomain"
|
||||
@input="debouncedUpdateDomainFilter"
|
||||
/>
|
||||
@@ -67,7 +72,7 @@
|
||||
name: RouteName.INSTANCE,
|
||||
params: { domain: instance.domain },
|
||||
}"
|
||||
class="flex items-center mb-2 rounded bg-secondary p-4 flex-wrap justify-center gap-x-2 gap-y-3"
|
||||
class="flex items-center mb-2 rounded bg-mbz-yellow-alt-300 dark:bg-mbz-purple-400 p-4 flex-wrap justify-center gap-x-2 gap-y-3"
|
||||
v-for="instance in instances.elements"
|
||||
:key="instance.domain"
|
||||
>
|
||||
@@ -75,19 +80,19 @@
|
||||
<img
|
||||
class="w-12"
|
||||
v-if="instance.hasRelay"
|
||||
src="../../assets/logo.svg"
|
||||
src="/img/logo.svg"
|
||||
alt=""
|
||||
/>
|
||||
<CloudQuestion v-else :size="36" />
|
||||
|
||||
<div class="">
|
||||
<h4 class="text-lg truncate">{{ instance.domain }}</h4>
|
||||
<h3 class="text-lg truncate">{{ instance.domain }}</h3>
|
||||
<span
|
||||
class="text-sm"
|
||||
v-if="instance.followedStatus === InstanceFollowStatus.APPROVED"
|
||||
>
|
||||
<o-icon icon="inbox-arrow-down" />
|
||||
{{ $t("Followed") }}</span
|
||||
{{ t("Followed") }}</span
|
||||
>
|
||||
<span
|
||||
class="text-sm"
|
||||
@@ -96,32 +101,32 @@
|
||||
"
|
||||
>
|
||||
<o-icon icon="inbox-arrow-down" />
|
||||
{{ $t("Followed, pending response") }}</span
|
||||
{{ t("Followed, pending response") }}</span
|
||||
>
|
||||
<span
|
||||
class="text-sm"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.APPROVED"
|
||||
>
|
||||
<o-icon icon="inbox-arrow-up" />
|
||||
{{ $t("Follows us") }}</span
|
||||
{{ t("Follows us") }}</span
|
||||
>
|
||||
<span
|
||||
class="text-sm"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
|
||||
>
|
||||
<o-icon icon="inbox-arrow-up" />
|
||||
{{ $t("Follows us, pending approval") }}</span
|
||||
{{ t("Follows us, pending approval") }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none flex gap-3 ltr:ml-3 rtl:mr-3">
|
||||
<p class="flex flex-col text-center">
|
||||
<span class="text-xl">{{ instance.eventCount }}</span
|
||||
><span class="text-sm">{{ $t("Events") }}</span>
|
||||
><span class="text-sm">{{ t("Events") }}</span>
|
||||
</p>
|
||||
<p class="flex flex-col text-center">
|
||||
<span class="text-xl">{{ instance.personCount }}</span
|
||||
><span class="text-sm">{{ $t("Profiles") }}</span>
|
||||
><span class="text-sm">{{ t("Profiles") }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</router-link>
|
||||
@@ -130,26 +135,26 @@
|
||||
:total="instances.total"
|
||||
v-model="instancePage"
|
||||
:per-page="INSTANCES_PAGE_LIMIT"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
>
|
||||
</o-pagination>
|
||||
</div>
|
||||
<div v-else-if="instances && instances.elements.length == 0">
|
||||
<empty-content icon="lan-disconnect" :inline="true">
|
||||
{{ $t("No instance found.") }}
|
||||
{{ t("No instance found.") }}
|
||||
<template #desc>
|
||||
<span v-if="hasFilter">
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"No instances match this filter. Try resetting filter fields?"
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t("You haven't interacted with other instances yet.") }}
|
||||
{{ t("You haven't interacted with other instances yet.") }}
|
||||
</span>
|
||||
</template>
|
||||
</empty-content>
|
||||
@@ -176,10 +181,11 @@ import {
|
||||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
const INSTANCES_PAGE_LIMIT = 10;
|
||||
|
||||
@@ -243,6 +249,8 @@ onDone(({ data }) => {
|
||||
});
|
||||
});
|
||||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
onError((error) => {
|
||||
if (error.message) {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
@@ -2,16 +2,18 @@
|
||||
<div>
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{ name: RouteName.MODERATION, text: $t('Moderation') },
|
||||
{ name: RouteName.MODERATION, text: t('Moderation') },
|
||||
{
|
||||
name: RouteName.PROFILES,
|
||||
text: $t('Profiles'),
|
||||
text: t('Profiles'),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div v-if="persons">
|
||||
<o-switch v-model="local">{{ $t("Local") }}</o-switch>
|
||||
<o-switch v-model="suspended">{{ $t("Suspended") }}</o-switch>
|
||||
<div class="flex gap-2">
|
||||
<o-switch v-model="local">{{ t("Local") }}</o-switch>
|
||||
<o-switch v-model="suspended">{{ t("Suspended") }}</o-switch>
|
||||
</div>
|
||||
<o-table
|
||||
:data="persons.elements"
|
||||
:loading="loading"
|
||||
@@ -20,25 +22,24 @@
|
||||
backend-filtering
|
||||
:debounce-search="200"
|
||||
v-model:current-page="page"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
:total="persons.total"
|
||||
:per-page="PROFILES_PER_PAGE"
|
||||
@page-change="onPageChange"
|
||||
@filters-change="onFiltersChange"
|
||||
>
|
||||
<o-table-column
|
||||
field="preferredUsername"
|
||||
:label="$t('Username')"
|
||||
:label="t('Username')"
|
||||
searchable
|
||||
>
|
||||
<template #searchable="props">
|
||||
<o-input
|
||||
v-model="props.filters.preferredUsername"
|
||||
:aria-label="$t('Filter')"
|
||||
:placeholder="$t('Filter')"
|
||||
:aria-label="t('Filter')"
|
||||
:placeholder="t('Filter')"
|
||||
icon="magnify"
|
||||
/>
|
||||
</template>
|
||||
@@ -57,6 +58,7 @@
|
||||
:alt="props.row.avatar.alt || ''"
|
||||
width="48"
|
||||
height="48"
|
||||
class="rounded-full"
|
||||
/>
|
||||
</figure>
|
||||
<Account v-else :size="48" />
|
||||
@@ -72,12 +74,12 @@
|
||||
</template>
|
||||
</o-table-column>
|
||||
|
||||
<o-table-column field="domain" :label="$t('Domain')" searchable>
|
||||
<o-table-column field="domain" :label="t('Domain')" searchable>
|
||||
<template #searchable="props">
|
||||
<o-input
|
||||
v-model="props.filters.domain"
|
||||
:aria-label="$t('Filter')"
|
||||
:placeholder="$t('Filter')"
|
||||
:aria-label="t('Filter')"
|
||||
:placeholder="t('Filter')"
|
||||
icon="magnify"
|
||||
/>
|
||||
</template>
|
||||
@@ -87,7 +89,7 @@
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content icon="account" :inline="true">
|
||||
{{ $t("No profile matches the filters") }}
|
||||
{{ t("No profile matches the filters") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
</o-table>
|
||||
@@ -142,10 +144,6 @@ useHead({
|
||||
title: computed(() => t("Profiles")),
|
||||
});
|
||||
|
||||
const onPageChange = async (): Promise<void> => {
|
||||
await fetchMore();
|
||||
};
|
||||
|
||||
const onFiltersChange = ({
|
||||
preferredUsername: newPreferredUsername,
|
||||
domain: newDomain,
|
||||
@@ -155,7 +153,7 @@ const onFiltersChange = ({
|
||||
}): void => {
|
||||
preferredUsername.value = newPreferredUsername;
|
||||
domain.value = newDomain;
|
||||
fetchMore();
|
||||
fetchMore({});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@@ -104,20 +104,15 @@ import {
|
||||
CategoryPictureLicencing,
|
||||
CategoryPictureLicencingElement,
|
||||
} from "@/components/Categories/constants";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useEventCategories } from "@/composition/apollo/config";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
|
||||
|
||||
const config = computed(() => configResult.value?.config);
|
||||
|
||||
const eventCategories = computed(() => config.value?.eventCategories ?? []);
|
||||
const { eventCategories } = useEventCategories();
|
||||
|
||||
const eventCategoryLabel = (categoryId: string): string | undefined => {
|
||||
return eventCategories.value.find(({ id }) => categoryId == id)?.label;
|
||||
return eventCategories.value?.find(({ id }) => categoryId == id)?.label;
|
||||
};
|
||||
|
||||
const { result: categoryStatsResult } = useQuery<{
|
||||
@@ -132,7 +127,7 @@ const promotedCategories = computed((): CategoryStatsModel[] => {
|
||||
.map(({ key, number }) => ({
|
||||
key,
|
||||
number,
|
||||
label: eventCategoryLabel(key),
|
||||
label: eventCategoryLabel(key) as string,
|
||||
}))
|
||||
.filter(
|
||||
({ key, number, label }) =>
|
||||
|
||||
@@ -72,7 +72,9 @@ import { useHead } from "@vueuse/head";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { AbsintheGraphQLError } from "@/types/errors.model";
|
||||
|
||||
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue"));
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("@/components/TextEditor.vue")
|
||||
);
|
||||
|
||||
const props = defineProps<{ preferredUsername: string }>();
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<discussion-comment
|
||||
class="border rounded-md p-2 mt-4"
|
||||
v-for="comment in discussion.comments.elements"
|
||||
:key="comment.id"
|
||||
:model-value="comment"
|
||||
@@ -237,7 +238,9 @@ const discussion = computed(() => discussionResult.value?.discussion);
|
||||
|
||||
const { group } = useGroup(usernameWithDomain(discussion.value?.actor));
|
||||
|
||||
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue"));
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("@/components/TextEditor.vue")
|
||||
);
|
||||
|
||||
useHead({
|
||||
title: computed(() => discussion.value?.title ?? ""),
|
||||
@@ -362,7 +365,7 @@ const dialog = inject<Dialog>("dialog");
|
||||
|
||||
const openDeleteDiscussionConfirmation = (): void => {
|
||||
dialog?.confirm({
|
||||
type: "is-danger",
|
||||
variant: "danger",
|
||||
title: t("Delete this discussion"),
|
||||
message: t("Are you sure you want to delete this entire discussion?"),
|
||||
confirmText: t("Delete discussion"),
|
||||
@@ -41,6 +41,7 @@
|
||||
:key="discussion.id"
|
||||
/>
|
||||
<o-pagination
|
||||
v-show="group.discussions.total > DISCUSSIONS_PER_PAGE"
|
||||
class="discussion-pagination"
|
||||
:total="group.discussions.total"
|
||||
v-model="page"
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<div class="container mx-auto" v-if="hasCurrentActorPermissionsToEdit">
|
||||
<h1 class="" v-if="isUpdate === true">
|
||||
{{ $t("Update event {name}", { name: event.title }) }}
|
||||
{{ t("Update event {name}", { name: event.title }) }}
|
||||
</h1>
|
||||
<h1 class="" v-else>
|
||||
{{ $t("Create a new event") }}
|
||||
{{ t("Create a new event") }}
|
||||
</h1>
|
||||
|
||||
<form ref="form">
|
||||
<h2>{{ $t("General information") }}</h2>
|
||||
<h2>{{ t("General information") }}</h2>
|
||||
<picture-upload
|
||||
v-if="pictureFile"
|
||||
v-model:pictureFile="pictureFile"
|
||||
:textFallback="$t('Headline picture')"
|
||||
v-model:modelValue="pictureFile"
|
||||
:textFallback="t('Headline picture')"
|
||||
:defaultImage="event.picture"
|
||||
/>
|
||||
|
||||
<o-field
|
||||
:label="$t('Title')"
|
||||
:label="t('Title')"
|
||||
label-for="title"
|
||||
:type="checkTitleLength[0]"
|
||||
:message="checkTitleLength[1]"
|
||||
@@ -35,12 +35,12 @@
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<o-field
|
||||
v-if="eventCategories"
|
||||
:label="$t('Category')"
|
||||
:label="t('Category')"
|
||||
label-for="category"
|
||||
class="w-full md:max-w-fit"
|
||||
>
|
||||
<o-select
|
||||
:placeholder="$t('Select a category')"
|
||||
:placeholder="t('Select a category')"
|
||||
v-model="event.category"
|
||||
expanded
|
||||
>
|
||||
@@ -62,13 +62,13 @@
|
||||
|
||||
<o-field
|
||||
horizontal
|
||||
:label="$t('Starts on…')"
|
||||
:label="t('Starts on…')"
|
||||
class="begins-on-field"
|
||||
label-for="begins-on-field"
|
||||
>
|
||||
<o-datetimepicker
|
||||
class="datepicker starts-on"
|
||||
:placeholder="$t('Type or select a date…')"
|
||||
:placeholder="t('Type or select a date…')"
|
||||
icon="calendar-today"
|
||||
:locale="$i18n.locale"
|
||||
v-model="beginsOn"
|
||||
@@ -78,17 +78,17 @@
|
||||
:first-day-of-week="firstDayOfWeek"
|
||||
:datepicker="{
|
||||
id: 'begins-on-field',
|
||||
'aria-next-label': $t('Next month'),
|
||||
'aria-previous-label': $t('Previous month'),
|
||||
'aria-next-label': t('Next month'),
|
||||
'aria-previous-label': t('Previous month'),
|
||||
}"
|
||||
>
|
||||
</o-datetimepicker>
|
||||
</o-field>
|
||||
|
||||
<o-field horizontal :label="$t('Ends on…')" label-for="ends-on-field">
|
||||
<o-field horizontal :label="t('Ends on…')" label-for="ends-on-field">
|
||||
<o-datetimepicker
|
||||
class="datepicker ends-on"
|
||||
:placeholder="$t('Type or select a date…')"
|
||||
:placeholder="t('Type or select a date…')"
|
||||
icon="calendar-today"
|
||||
:locale="$i18n.locale"
|
||||
v-model="endsOn"
|
||||
@@ -99,41 +99,40 @@
|
||||
:first-day-of-week="firstDayOfWeek"
|
||||
:datepicker="{
|
||||
id: 'ends-on-field',
|
||||
'aria-next-label': $t('Next month'),
|
||||
'aria-previous-label': $t('Previous month'),
|
||||
'aria-next-label': t('Next month'),
|
||||
'aria-previous-label': t('Previous month'),
|
||||
}"
|
||||
>
|
||||
</o-datetimepicker>
|
||||
</o-field>
|
||||
|
||||
<o-button type="is-text" @click="dateSettingsIsOpen = true">
|
||||
{{ $t("Date parameters") }}
|
||||
<o-button class="block" variant="text" @click="dateSettingsIsOpen = true">
|
||||
{{ t("Date parameters") }}
|
||||
</o-button>
|
||||
|
||||
<div class="address">
|
||||
<div class="my-6">
|
||||
<full-address-auto-complete
|
||||
v-model="eventPhysicalAddress"
|
||||
:user-timezone="userActualTimezone"
|
||||
:disabled="isOnline"
|
||||
:disabled="event.options.isOnline"
|
||||
:hideSelected="true"
|
||||
/>
|
||||
<o-switch class="is-online" v-model="isOnline">{{
|
||||
$t("The event is fully online")
|
||||
<o-switch class="my-4" v-model="isOnline">{{
|
||||
t("The event is fully online")
|
||||
}}</o-switch>
|
||||
</div>
|
||||
|
||||
<div class="o-field field">
|
||||
<label class="o-field__label field-label">{{
|
||||
$t("Description")
|
||||
}}</label>
|
||||
<label class="o-field__label field-label">{{ t("Description") }}</label>
|
||||
<editor-component
|
||||
v-if="currentActor"
|
||||
:current-actor="(currentActor as IPerson)"
|
||||
v-model="event.description"
|
||||
:aria-label="$t('Event description body')"
|
||||
:aria-label="t('Event description body')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<o-field :label="$t('Website / URL')" label-for="website-url">
|
||||
<o-field :label="t('Website / URL')" label-for="website-url">
|
||||
<o-input
|
||||
icon="link"
|
||||
type="url"
|
||||
@@ -144,7 +143,7 @@
|
||||
</o-field>
|
||||
|
||||
<section class="my-4">
|
||||
<h2>{{ $t("Organizers") }}</h2>
|
||||
<h2>{{ t("Organizers") }}</h2>
|
||||
|
||||
<div v-if="features?.groups && organizerActor?.id">
|
||||
<o-field>
|
||||
@@ -155,21 +154,21 @@
|
||||
</o-field>
|
||||
<p v-if="!attributedToAGroup && organizerActorEqualToCurrentActor">
|
||||
{{
|
||||
$t("The event will show as attributed to your personal profile.")
|
||||
t("The event will show as attributed to your personal profile.")
|
||||
}}
|
||||
</p>
|
||||
<p v-else-if="!attributedToAGroup">
|
||||
{{ $t("The event will show as attributed to this profile.") }}
|
||||
{{ t("The event will show as attributed to this profile.") }}
|
||||
</p>
|
||||
<p v-else>
|
||||
<span>{{
|
||||
$t("The event will show as attributed to this group.")
|
||||
t("The event will show as attributed to this group.")
|
||||
}}</span>
|
||||
<span
|
||||
v-if="event.contacts && event.contacts.length"
|
||||
v-html="
|
||||
' ' +
|
||||
$t(
|
||||
t(
|
||||
'<b>{contact}</b> will be displayed as contact.',
|
||||
|
||||
{
|
||||
@@ -184,16 +183,16 @@
|
||||
"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ $t("You may show some members as contacts.") }}
|
||||
{{ t("You may show some members as contacts.") }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="my-4">
|
||||
<h2>{{ $t("Event metadata") }}</h2>
|
||||
<h2>{{ t("Event metadata") }}</h2>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"Integrate this event with 3rd-party tools and show metadata for the event."
|
||||
)
|
||||
}}
|
||||
@@ -202,12 +201,12 @@
|
||||
</section>
|
||||
<section class="my-4">
|
||||
<h2>
|
||||
{{ $t("Who can view this event and participate") }}
|
||||
{{ t("Who can view this event and participate") }}
|
||||
</h2>
|
||||
<fieldset>
|
||||
<legend>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"When the event is private, you'll need to share the link around."
|
||||
)
|
||||
}}
|
||||
@@ -217,7 +216,7 @@
|
||||
v-model="event.visibility"
|
||||
name="eventVisibility"
|
||||
:native-value="EventVisibility.PUBLIC"
|
||||
>{{ $t("Visible everywhere on the web (public)") }}</o-radio
|
||||
>{{ t("Visible everywhere on the web (public)") }}</o-radio
|
||||
>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -225,7 +224,7 @@
|
||||
v-model="event.visibility"
|
||||
name="eventVisibility"
|
||||
:native-value="EventVisibility.UNLISTED"
|
||||
>{{ $t("Only accessible through link (private)") }}</o-radio
|
||||
>{{ t("Only accessible through link (private)") }}</o-radio
|
||||
>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -233,7 +232,7 @@
|
||||
<o-radio v-model="event.visibility"
|
||||
name="eventVisibility"
|
||||
:native-value="EventVisibility.PRIVATE">
|
||||
{{ $t('Page limited to my group (asks for auth)') }}
|
||||
{{ t('Page limited to my group (asks for auth)') }}
|
||||
</o-radio>
|
||||
</div>-->
|
||||
|
||||
@@ -242,9 +241,7 @@
|
||||
:label="t('Anonymous participations')"
|
||||
>
|
||||
<o-switch v-model="eventOptions.anonymousParticipation">
|
||||
{{
|
||||
$t("I want to allow people to participate without an account.")
|
||||
}}
|
||||
{{ t("I want to allow people to participate without an account.") }}
|
||||
<small
|
||||
v-if="
|
||||
anonymousParticipationConfig?.validation.email
|
||||
@@ -253,7 +250,7 @@
|
||||
>
|
||||
<br />
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail."
|
||||
)
|
||||
}}
|
||||
@@ -263,23 +260,23 @@
|
||||
|
||||
<o-field :label="t('Participation approval')">
|
||||
<o-switch v-model="needsApproval">{{
|
||||
$t("I want to approve every participation request")
|
||||
t("I want to approve every participation request")
|
||||
}}</o-switch>
|
||||
</o-field>
|
||||
|
||||
<o-field :label="t('Number of places')">
|
||||
<o-switch v-model="limitedPlaces">{{
|
||||
$t("Limited number of places")
|
||||
t("Limited number of places")
|
||||
}}</o-switch>
|
||||
</o-field>
|
||||
|
||||
<div class="" v-if="limitedPlaces">
|
||||
<o-field :label="$t('Number of places')" label-for="number-of-places">
|
||||
<o-field :label="t('Number of places')" label-for="number-of-places">
|
||||
<o-input
|
||||
type="number"
|
||||
controls-position="compact"
|
||||
:aria-minus-label="$t('Decrease')"
|
||||
:aria-plus-label="$t('Increase')"
|
||||
:aria-minus-label="t('Decrease')"
|
||||
:aria-plus-label="t('Increase')"
|
||||
min="1"
|
||||
v-model="eventOptions.maximumAttendeeCapacity"
|
||||
id="number-of-places"
|
||||
@@ -288,28 +285,28 @@
|
||||
<!--
|
||||
<o-field>
|
||||
<o-switch v-model="eventOptions.showRemainingAttendeeCapacity">
|
||||
{{ $t('Show remaining number of places') }}
|
||||
{{ t('Show remaining number of places') }}
|
||||
</o-switch>
|
||||
</o-field>
|
||||
|
||||
<o-field>
|
||||
<o-switch v-model="eventOptions.showParticipationPrice">
|
||||
{{ $t('Display participation price') }}
|
||||
{{ t('Display participation price') }}
|
||||
</o-switch>
|
||||
</o-field>-->
|
||||
</div>
|
||||
</section>
|
||||
<section class="my-4">
|
||||
<h2>{{ $t("Public comment moderation") }}</h2>
|
||||
<h2>{{ t("Public comment moderation") }}</h2>
|
||||
|
||||
<fieldset>
|
||||
<legend>{{ $t("Who can post a comment?") }}</legend>
|
||||
<legend>{{ t("Who can post a comment?") }}</legend>
|
||||
<o-field>
|
||||
<o-radio
|
||||
v-model="eventOptions.commentModeration"
|
||||
name="commentModeration"
|
||||
:native-value="CommentModeration.ALLOW_ALL"
|
||||
>{{ $t("Allow all comments from users with accounts") }}</o-radio
|
||||
>{{ t("Allow all comments from users with accounts") }}</o-radio
|
||||
>
|
||||
</o-field>
|
||||
|
||||
@@ -317,7 +314,7 @@
|
||||
<!-- <o-radio v-model="eventOptions.commentModeration"-->
|
||||
<!-- name="commentModeration"-->
|
||||
<!-- :native-value="CommentModeration.MODERATED">-->
|
||||
<!-- {{ $t('Moderated comments (shown after approval)') }}-->
|
||||
<!-- {{ t('Moderated comments (shown after approval)') }}-->
|
||||
<!-- </o-radio>-->
|
||||
<!-- </div>-->
|
||||
|
||||
@@ -326,18 +323,18 @@
|
||||
v-model="eventOptions.commentModeration"
|
||||
name="commentModeration"
|
||||
:native-value="CommentModeration.CLOSED"
|
||||
>{{ $t("Close comments for all (except for admins)") }}</o-radio
|
||||
>{{ t("Close comments for all (except for admins)") }}</o-radio
|
||||
>
|
||||
</o-field>
|
||||
</fieldset>
|
||||
</section>
|
||||
<section class="my-4">
|
||||
<h2>{{ $t("Status") }}</h2>
|
||||
<h2>{{ t("Status") }}</h2>
|
||||
|
||||
<fieldset>
|
||||
<legend>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"Does the event needs to be confirmed later or is it cancelled?"
|
||||
)
|
||||
}}
|
||||
@@ -350,7 +347,7 @@
|
||||
:native-value="EventStatus.TENTATIVE"
|
||||
>
|
||||
<o-icon icon="calendar-question" />
|
||||
{{ $t("Tentative: Will be confirmed later") }}
|
||||
{{ t("Tentative: Will be confirmed later") }}
|
||||
</o-radio>
|
||||
<o-radio
|
||||
v-model="event.status"
|
||||
@@ -359,7 +356,7 @@
|
||||
:native-value="EventStatus.CONFIRMED"
|
||||
>
|
||||
<o-icon icon="calendar-check" />
|
||||
{{ $t("Confirmed: Will happen") }}
|
||||
{{ t("Confirmed: Will happen") }}
|
||||
</o-radio>
|
||||
<o-radio
|
||||
v-model="event.status"
|
||||
@@ -368,7 +365,7 @@
|
||||
:native-value="EventStatus.CANCELLED"
|
||||
>
|
||||
<o-icon icon="calendar-remove" />
|
||||
{{ $t("Cancelled: Won't happen") }}
|
||||
{{ t("Cancelled: Won't happen") }}
|
||||
</o-radio>
|
||||
</o-field>
|
||||
</fieldset>
|
||||
@@ -377,30 +374,30 @@
|
||||
</div>
|
||||
<div class="container mx-auto" v-else>
|
||||
<o-notification variant="danger">
|
||||
{{ $t("Only group moderators can create, edit and delete events.") }}
|
||||
{{ t("Only group moderators can create, edit and delete events.") }}
|
||||
</o-notification>
|
||||
</div>
|
||||
<o-modal
|
||||
v-model:active="dateSettingsIsOpen"
|
||||
has-modal-card
|
||||
trap-focus
|
||||
:close-button-aria-label="$t('Close')"
|
||||
:close-button-aria-label="t('Close')"
|
||||
>
|
||||
<form class="p-3">
|
||||
<header class="">
|
||||
<h2 class="">{{ $t("Date and time settings") }}</h2>
|
||||
<h2 class="">{{ t("Date and time settings") }}</h2>
|
||||
</header>
|
||||
<section class="">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"Event timezone will default to the timezone of the event's address if there is one, or to your own timezone setting."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<o-field :label="$t('Timezone')" label-for="timezone" expanded>
|
||||
<o-field :label="t('Timezone')" label-for="timezone" expanded>
|
||||
<o-select
|
||||
:placeholder="$t('Select a timezone')"
|
||||
:placeholder="t('Select a timezone')"
|
||||
:loading="timezoneLoading"
|
||||
v-model="timezone"
|
||||
id="timezone"
|
||||
@@ -424,23 +421,23 @@
|
||||
@click="timezone = null"
|
||||
class="reset-area"
|
||||
icon-left="close"
|
||||
:title="$t('Clear timezone field')"
|
||||
:title="t('Clear timezone field')"
|
||||
/>
|
||||
</o-field>
|
||||
<o-field :label="$t('Event page settings')">
|
||||
<o-field :label="t('Event page settings')">
|
||||
<o-switch v-model="eventOptions.showStartTime">{{
|
||||
$t("Show the time when the event begins")
|
||||
t("Show the time when the event begins")
|
||||
}}</o-switch>
|
||||
</o-field>
|
||||
<o-field>
|
||||
<o-switch v-model="eventOptions.showEndTime">{{
|
||||
$t("Show the time when the event ends")
|
||||
t("Show the time when the event ends")
|
||||
}}</o-switch>
|
||||
</o-field>
|
||||
</section>
|
||||
<footer class="mt-2">
|
||||
<o-button @click="dateSettingsIsOpen = false">
|
||||
{{ $t("OK") }}
|
||||
{{ t("OK") }}
|
||||
</o-button>
|
||||
</footer>
|
||||
</form>
|
||||
@@ -449,23 +446,22 @@
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="main navigation"
|
||||
class="bg-secondary/70"
|
||||
class="bg-mbz-yellow-alt-200 py-3"
|
||||
:class="{ 'is-fixed-bottom': showFixedNavbar }"
|
||||
v-if="hasCurrentActorPermissionsToEdit"
|
||||
>
|
||||
<div class="container mx-auto">
|
||||
<div class="flex justify-between">
|
||||
<div class="">
|
||||
<span class="dark:text-gray-900" v-if="isEventModified">{{
|
||||
$t("Unsaved changes")
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="dark:text-gray-900" v-if="isEventModified">
|
||||
{{ t("Unsaved changes") }}
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-3 items-center">
|
||||
<span class="">
|
||||
<o-button type="is-text" @click="confirmGoBack">{{
|
||||
$t("Cancel")
|
||||
}}</o-button>
|
||||
</span>
|
||||
<o-button
|
||||
variant="text"
|
||||
@click="confirmGoBack"
|
||||
class="dark:!text-black"
|
||||
>{{ t("Cancel") }}</o-button
|
||||
>
|
||||
<!-- If an event has been published we can't make it draft anymore -->
|
||||
<span class="" v-if="event.draft === true">
|
||||
<o-button
|
||||
@@ -473,7 +469,7 @@
|
||||
outlined
|
||||
@click="createOrUpdateDraft"
|
||||
:disabled="saving"
|
||||
>{{ $t("Save draft") }}</o-button
|
||||
>{{ t("Save draft") }}</o-button
|
||||
>
|
||||
</span>
|
||||
<span class="">
|
||||
@@ -483,9 +479,9 @@
|
||||
@click="createOrUpdatePublish"
|
||||
@keyup.enter="createOrUpdatePublish"
|
||||
>
|
||||
<span v-if="isUpdate === false">{{ $t("Create my event") }}</span>
|
||||
<span v-else-if="event.draft === true">{{ $t("Publish") }}</span>
|
||||
<span v-else>{{ $t("Update my event") }}</span>
|
||||
<span v-if="isUpdate === false">{{ t("Create my event") }}</span>
|
||||
<span v-else-if="event.draft === true">{{ t("Publish") }}</span>
|
||||
<span v-else>{{ t("Update my event") }}</span>
|
||||
</o-button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -596,7 +592,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { getTimezoneOffset } from "date-fns-tz";
|
||||
import PictureUpload from "@/components/PictureUpload.vue";
|
||||
import EditorComponent from "@/components/Editor.vue";
|
||||
import EditorComponent from "@/components/TextEditor.vue";
|
||||
import TagInput from "@/components/Event/TagInput.vue";
|
||||
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
|
||||
import EventMetadataList from "@/components/Event/EventMetadataList.vue";
|
||||
@@ -617,27 +613,33 @@ import {
|
||||
MemberRole,
|
||||
ParticipantRole,
|
||||
} from "@/types/enums";
|
||||
import OrganizerPickerWrapper from "../../components/Event/OrganizerPickerWrapper.vue";
|
||||
import OrganizerPickerWrapper from "@/components/Event/OrganizerPickerWrapper.vue";
|
||||
import {
|
||||
CREATE_EVENT,
|
||||
EDIT_EVENT,
|
||||
EVENT_PERSON_PARTICIPATION,
|
||||
} from "../../graphql/event";
|
||||
} from "@/graphql/event";
|
||||
import {
|
||||
EventModel,
|
||||
IEditableEvent,
|
||||
IEvent,
|
||||
removeTypeName,
|
||||
toEditJSON,
|
||||
} from "../../types/event.model";
|
||||
import { LOGGED_USER_DRAFTS } from "../../graphql/actor";
|
||||
import { IActor, IGroup, IPerson, usernameWithDomain } from "../../types/actor";
|
||||
} from "@/types/event.model";
|
||||
import { LOGGED_USER_DRAFTS } from "@/graphql/actor";
|
||||
import {
|
||||
IActor,
|
||||
IGroup,
|
||||
IPerson,
|
||||
usernameWithDomain,
|
||||
displayNameAndUsername,
|
||||
} from "@/types/actor";
|
||||
import {
|
||||
buildFileFromIMedia,
|
||||
buildFileVariable,
|
||||
readFileAsync,
|
||||
} from "../../utils/image";
|
||||
import RouteName from "../../router/name";
|
||||
} from "@/utils/image";
|
||||
import RouteName from "@/router/name";
|
||||
import "intersection-observer";
|
||||
import {
|
||||
ApolloCache,
|
||||
@@ -759,7 +761,7 @@ const unmodifiedEvent = ref<IEditableEvent>(new EventModel());
|
||||
|
||||
const pictureFile = ref<File | null>(null);
|
||||
|
||||
const canPromote = ref(true);
|
||||
// const canPromote = ref(true);
|
||||
const limitedPlaces = ref(false);
|
||||
const showFixedNavbar = ref(true);
|
||||
|
||||
@@ -905,7 +907,7 @@ onCreateEventMutationDone(async ({ data }) => {
|
||||
message: (event.value.draft
|
||||
? t("The event has been created as a draft")
|
||||
: t("The event has been published")) as string,
|
||||
variant: "is-success",
|
||||
variant: "success",
|
||||
position: "bottom-right",
|
||||
duration: 5000,
|
||||
});
|
||||
@@ -964,6 +966,7 @@ onEditEventMutationError((err) => {
|
||||
const updateEvent = async (): Promise<void> => {
|
||||
saving.value = true;
|
||||
const variables = await buildVariables();
|
||||
console.debug("update event", variables);
|
||||
editEventMutation(variables);
|
||||
};
|
||||
|
||||
@@ -1016,7 +1019,6 @@ const handleError = (err: any) => {
|
||||
*/
|
||||
const postCreateOrUpdate = (store: any, updatedEvent: IEvent) => {
|
||||
const resultEvent: IEvent = { ...updatedEvent };
|
||||
console.debug("resultEvent", resultEvent);
|
||||
if (!updatedEvent.draft) {
|
||||
store.writeQuery({
|
||||
query: EVENT_PERSON_PARTICIPATION,
|
||||
@@ -1056,7 +1058,6 @@ const postCreateOrUpdate = (store: any, updatedEvent: IEvent) => {
|
||||
/**
|
||||
* Refresh drafts or participation cache depending if the event is still draft or not
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
const postRefetchQueries = (
|
||||
updatedEvent: IEvent
|
||||
): InternalRefetchQueriesInclude => {
|
||||
@@ -1093,11 +1094,11 @@ const buildVariables = async () => {
|
||||
options: eventOptions.value,
|
||||
};
|
||||
|
||||
// const organizerActor = event.value?.organizerActor?.id
|
||||
// ? event.value.organizerActor
|
||||
// : organizerActor.value;
|
||||
const localOrganizerActor = event.value?.organizerActor?.id
|
||||
? event.value.organizerActor
|
||||
: organizerActor.value;
|
||||
if (organizerActor.value) {
|
||||
res = { ...res, organizerActorId: organizerActor.value?.id };
|
||||
res = { ...res, organizerActorId: localOrganizerActor?.id };
|
||||
}
|
||||
const attributedToId = event.value?.attributedTo?.id
|
||||
? event.value?.attributedTo.id
|
||||
@@ -1155,7 +1156,7 @@ const needsApproval = computed({
|
||||
|
||||
const checkTitleLength = computed((): Array<string | undefined> => {
|
||||
return event.value.title.length > 80
|
||||
? ["is-info", t("The event title will be ellipsed.") as string]
|
||||
? ["info", t("The event title will be ellipsed.")]
|
||||
: [undefined, undefined];
|
||||
});
|
||||
|
||||
@@ -1189,7 +1190,7 @@ const confirmGoElsewhere = (): Promise<boolean> => {
|
||||
message,
|
||||
confirmText: t("Abandon editing") as string,
|
||||
cancelText: t("Continue editing") as string,
|
||||
type: "is-warning",
|
||||
variant: "warning",
|
||||
hasIcon: true,
|
||||
onConfirm: () => resolve(true),
|
||||
onCancel: () => resolve(false),
|
||||
@@ -1356,6 +1357,13 @@ const isOnline = computed({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
watch(isOnline, (newIsOnline) => {
|
||||
if (newIsOnline === true) {
|
||||
eventPhysicalAddress.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
const dateFnsLocale = inject<Locale>("dateFnsLocale");
|
||||
|
||||
const firstDayOfWeek = computed((): number => {
|
||||
@@ -1366,6 +1374,15 @@ const { event: fetchedEvent, onResult: onFetchEventResult } = useFetchEvent(
|
||||
eventId.value
|
||||
);
|
||||
|
||||
watch(
|
||||
fetchedEvent,
|
||||
() => {
|
||||
if (!fetchedEvent.value) return;
|
||||
event.value = { ...fetchedEvent.value };
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onFetchEventResult((result) => {
|
||||
if (!result.loading && result.data?.event) {
|
||||
event.value = { ...result.data?.event };
|
||||
@@ -65,36 +65,35 @@
|
||||
</popover-actor-card>
|
||||
</span>
|
||||
</div>
|
||||
<p class="flex gap-1 items-center" dir="auto">
|
||||
<tag v-if="eventCategory" class="category" capitalize>{{
|
||||
eventCategory
|
||||
}}</tag>
|
||||
<router-link
|
||||
v-for="tag in event?.tags ?? []"
|
||||
:key="tag.title"
|
||||
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
||||
>
|
||||
<tag>{{ tag.title }}</tag>
|
||||
</router-link>
|
||||
</p>
|
||||
<tag variant="warning" size="is-medium" v-if="event?.draft"
|
||||
>{{ t("Draft") }}
|
||||
</tag>
|
||||
<span
|
||||
class="event-status"
|
||||
v-if="event?.status !== EventStatus.CONFIRMED"
|
||||
>
|
||||
<tag
|
||||
variant="warning"
|
||||
v-if="event?.status === EventStatus.TENTATIVE"
|
||||
>{{ t("Event to be confirmed") }}</tag
|
||||
>
|
||||
<tag
|
||||
variant="danger"
|
||||
v-if="event?.status === EventStatus.CANCELLED"
|
||||
>{{ t("Event cancelled") }}</tag
|
||||
>
|
||||
</span>
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<p v-if="event?.status !== EventStatus.CONFIRMED">
|
||||
<tag
|
||||
variant="warning"
|
||||
v-if="event?.status === EventStatus.TENTATIVE"
|
||||
>{{ t("Event to be confirmed") }}</tag
|
||||
>
|
||||
<tag
|
||||
variant="danger"
|
||||
v-if="event?.status === EventStatus.CANCELLED"
|
||||
>{{ t("Event cancelled") }}</tag
|
||||
>
|
||||
</p>
|
||||
<p class="flex gap-1 items-center" dir="auto">
|
||||
<tag v-if="eventCategory" class="category" capitalize>{{
|
||||
eventCategory
|
||||
}}</tag>
|
||||
<router-link
|
||||
v-for="tag in event?.tags ?? []"
|
||||
:key="tag.title"
|
||||
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
||||
>
|
||||
<tag>{{ tag.title }}</tag>
|
||||
</router-link>
|
||||
</p>
|
||||
<tag variant="warning" size="medium" v-if="event?.draft"
|
||||
>{{ t("Draft") }}
|
||||
</tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
@@ -375,7 +374,7 @@
|
||||
ref="reportModal"
|
||||
:close-button-aria-label="t('Close')"
|
||||
>
|
||||
<report-modal
|
||||
<ReportModal
|
||||
:on-confirm="reportEvent"
|
||||
:title="t('Report this event')"
|
||||
:outside-domain="organizerDomain"
|
||||
@@ -456,7 +455,7 @@
|
||||
<o-field :label="t('Message')">
|
||||
<o-input
|
||||
type="textarea"
|
||||
size="is-medium"
|
||||
size="medium"
|
||||
v-model="messageForConfirmation"
|
||||
minlength="10"
|
||||
></o-input>
|
||||
@@ -522,35 +521,28 @@ import {
|
||||
FETCH_EVENT,
|
||||
JOIN_EVENT,
|
||||
} from "@/graphql/event";
|
||||
import { CURRENT_ACTOR_CLIENT, PERSON_STATUS_GROUP } from "@/graphql/actor";
|
||||
import { EventModel, IEvent } from "@/types/event.model";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import {
|
||||
displayName,
|
||||
IActor,
|
||||
IPerson,
|
||||
Person,
|
||||
usernameWithDomain,
|
||||
} from "@/types/actor";
|
||||
import { GRAPHQL_API_ENDPOINT } from "@/api/_entrypoint";
|
||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||
import MultiCard from "@/components/Event/MultiCard.vue";
|
||||
import ReportModal from "@/components/Report/ReportModal.vue";
|
||||
import { IReport } from "@/types/report.model";
|
||||
import { CREATE_REPORT } from "@/graphql/report";
|
||||
import EventMixin from "@/mixins/event";
|
||||
import IdentityPicker from "../Account/IdentityPicker.vue";
|
||||
import IdentityPicker from "@/views/Account/IdentityPicker.vue";
|
||||
import ParticipationSection from "@/components/Participation/ParticipationSection.vue";
|
||||
import RouteName from "@/router/name";
|
||||
import CommentTree from "@/components/Comment/CommentTree.vue";
|
||||
import "intersection-observer";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
import {
|
||||
AnonymousParticipationNotFoundError,
|
||||
getLeaveTokenForParticipation,
|
||||
isParticipatingInThisEvent,
|
||||
removeAnonymousParticipation,
|
||||
} from "@/services/AnonymousParticipationStorage";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
import EventMetadataSidebar from "@/components/Event/EventMetadataSidebar.vue";
|
||||
import EventBanner from "@/components/Event/EventBanner.vue";
|
||||
@@ -560,12 +552,9 @@ import { IParticipant } from "@/types/participant.model";
|
||||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||
import { IEventMetadataDescription } from "@/types/event-metadata";
|
||||
import { eventMetaDataList } from "@/services/EventMetadata";
|
||||
import { USER_SETTINGS } from "@/graphql/user";
|
||||
import { IUser } from "@/types/current-user.model";
|
||||
import { useDeleteEvent, useFetchEvent } from "@/composition/apollo/event";
|
||||
import {
|
||||
computed,
|
||||
handleError,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
@@ -601,24 +590,26 @@ import { useI18n } from "vue-i18n";
|
||||
import { useProgrammatic } from "@oruga-ui/oruga-next";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { AbsintheGraphQLErrors } from "@/types/errors.model";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
const ShareEventModal = defineAsyncComponent(
|
||||
() => import("@/components/Event/ShareEventModal.vue")
|
||||
);
|
||||
const IntegrationTwitch = defineAsyncComponent(
|
||||
() => import("@/components/Event/Integrations/Twitch.vue")
|
||||
() => import("@/components/Event/Integrations/TwitchIntegration.vue")
|
||||
);
|
||||
const IntegrationPeertube = defineAsyncComponent(
|
||||
() => import("@/components/Event/Integrations/PeerTube.vue")
|
||||
() => import("@/components/Event/Integrations/PeerTubeIntegration.vue")
|
||||
);
|
||||
const IntegrationYoutube = defineAsyncComponent(
|
||||
() => import("@/components/Event/Integrations/YouTube.vue")
|
||||
() => import("@/components/Event/Integrations/YouTubeIntegration.vue")
|
||||
);
|
||||
const IntegrationJitsiMeet = defineAsyncComponent(
|
||||
() => import("@/components/Event/Integrations/JitsiMeet.vue")
|
||||
() => import("@/components/Event/Integrations/JitsiMeetIntegration.vue")
|
||||
);
|
||||
const IntegrationEtherpad = defineAsyncComponent(
|
||||
() => import("@/components/Event/Integrations/Etherpad.vue")
|
||||
() => import("@/components/Event/Integrations/EtherpadIntegration.vue")
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -1057,8 +1048,8 @@ const triggerShare = (): void => {
|
||||
title: event.value?.title,
|
||||
url: event.value?.url,
|
||||
})
|
||||
.then(() => console.log("Successful share"))
|
||||
.catch((error: any) => console.log("Error sharing", error));
|
||||
.then(() => console.debug("Successful share"))
|
||||
.catch((error: any) => console.debug("Error sharing", error));
|
||||
} else {
|
||||
isShareModalActive.value = true;
|
||||
// send popup
|
||||
@@ -1067,7 +1058,7 @@ const triggerShare = (): void => {
|
||||
// @ts-ignore-end
|
||||
};
|
||||
|
||||
const handleErrors = (errors: any[]): void => {
|
||||
const handleErrors = (errors: AbsintheGraphQLErrors): void => {
|
||||
if (
|
||||
errors.some((error) => error.status_code === 404) ||
|
||||
errors.some(({ message }) => message.includes("has invalid value $uuid"))
|
||||
@@ -1076,7 +1067,9 @@ const handleErrors = (errors: any[]): void => {
|
||||
}
|
||||
};
|
||||
|
||||
onFetchEventError(({ graphQlErrors }) => handleErrors(graphQLErrors));
|
||||
onFetchEventError(({ graphQLErrors }) =>
|
||||
handleErrors(graphQLErrors as AbsintheGraphQLErrors)
|
||||
);
|
||||
|
||||
const actorIsParticipant = computed((): boolean => {
|
||||
if (actorIsOrganizer.value) return true;
|
||||
@@ -1108,7 +1101,7 @@ const canManageEvent = computed((): boolean => {
|
||||
return actorIsOrganizer.value || hasGroupPrivileges.value;
|
||||
});
|
||||
|
||||
const endDate = computed((): Date | undefined => {
|
||||
const endDate = computed((): string | undefined => {
|
||||
return event.value?.endsOn && event.value.endsOn > event.value.beginsOn
|
||||
? event.value.endsOn
|
||||
: event.value?.beginsOn;
|
||||
@@ -1211,6 +1204,11 @@ const eventCategory = computed((): string | undefined => {
|
||||
return eventCategory.id === event.value?.category;
|
||||
})?.label as string;
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: computed(() => eventTitle.value ?? ""),
|
||||
meta: [{ name: "description", content: eventDescription.value }],
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/_mixins" as *;
|
||||
@@ -1380,10 +1378,6 @@ a.participations-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.event-status .tag {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.no-border {
|
||||
border: 0;
|
||||
cursor: auto;
|
||||
@@ -70,7 +70,7 @@
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<o-button type="is-text" tag="a" :href="group.url">
|
||||
<o-button variant="text" tag="a" :href="group.url">
|
||||
{{ $t("View the group profile on the original instance") }}
|
||||
</o-button>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
</div>
|
||||
<!-- <o-loading v-model:active="$apollo.loading"></o-loading> -->
|
||||
<div class="wrapper flex flex-wrap gap-4 items-start">
|
||||
<div class="event-filter text-violet-1 flex-auto md:flex-none">
|
||||
<div
|
||||
class="event-filter rounded p-3 flex-auto md:flex-none bg-zinc-300 dark:bg-zinc-700"
|
||||
>
|
||||
<o-field>
|
||||
<o-switch v-model="showUpcoming">{{
|
||||
showUpcoming ? t("Upcoming events") : t("Past events")
|
||||
@@ -56,10 +58,12 @@
|
||||
? t('Showing events starting on')
|
||||
: t('Showing events before')
|
||||
"
|
||||
labelFor="events-start-datepicker"
|
||||
>
|
||||
<o-datepicker
|
||||
v-model="dateFilter"
|
||||
:first-day-of-week="firstDayOfWeek"
|
||||
id="events-start-datepicker"
|
||||
/>
|
||||
<o-button
|
||||
@click="dateFilter = new Date()"
|
||||
@@ -469,9 +473,6 @@ section {
|
||||
|
||||
.event-filter {
|
||||
grid-area: filter;
|
||||
background: lightgray;
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem 1.25rem 0.25rem;
|
||||
|
||||
// @include desktop {
|
||||
// padding: 2rem 1.25rem;
|
||||
@@ -55,16 +55,22 @@
|
||||
:key="format"
|
||||
aria-role="listitem"
|
||||
@click="
|
||||
exportParticipants({
|
||||
eventId: event?.id,
|
||||
format,
|
||||
})
|
||||
exportParticipants(
|
||||
{
|
||||
eventId: event?.id,
|
||||
format,
|
||||
},
|
||||
{ context: { type: format } }
|
||||
)
|
||||
"
|
||||
@keyup.enter="
|
||||
exportParticipants({
|
||||
eventId: event.value?.id,
|
||||
format,
|
||||
})
|
||||
exportParticipants(
|
||||
{
|
||||
eventId: event?.id,
|
||||
format,
|
||||
},
|
||||
{ context: { type: format } }
|
||||
)
|
||||
"
|
||||
>
|
||||
<button class="dropdown-button">
|
||||
@@ -81,9 +87,9 @@
|
||||
ref="queueTable"
|
||||
detailed
|
||||
detail-key="id"
|
||||
:checked-rows.sync="checkedRows"
|
||||
v-model:checked-rows="checkedRows"
|
||||
checkable
|
||||
:is-row-checkable="(row) => row.role !== ParticipantRole.CREATOR"
|
||||
:is-row-checkable="(row: IParticipant) => row.role !== ParticipantRole.CREATOR"
|
||||
checkbox-position="left"
|
||||
:show-detail-icon="false"
|
||||
:loading="participantsLoading"
|
||||
@@ -100,37 +106,38 @@
|
||||
backend-sorting
|
||||
:default-sort-direction="'desc'"
|
||||
:default-sort="['insertedAt', 'desc']"
|
||||
@page-change="(newPage) => (page = newPage)"
|
||||
@sort="(field, order) => emit('sort', field, order)"
|
||||
@page-change="(newPage: number) => (page = newPage)"
|
||||
@sort="(field: string, order: string) => emit('sort', field, order)"
|
||||
>
|
||||
<o-table-column
|
||||
field="actor.preferredUsername"
|
||||
:label="t('Participant')"
|
||||
v-slot="props"
|
||||
>
|
||||
<article class="media">
|
||||
<figure
|
||||
class="media-left image is-48x48"
|
||||
v-if="props.row.actor.avatar"
|
||||
>
|
||||
<img class="is-rounded" :src="props.row.actor.avatar.url" alt="" />
|
||||
<article>
|
||||
<figure v-if="props.row.actor.avatar">
|
||||
<img
|
||||
class="rounded"
|
||||
:src="props.row.actor.avatar.url"
|
||||
alt=""
|
||||
height="48"
|
||||
width="48"
|
||||
/>
|
||||
</figure>
|
||||
<Incognito
|
||||
v-else-if="props.row.actor.preferredUsername === 'anonymous'"
|
||||
:size="48"
|
||||
/>
|
||||
<AccountCircle v-else :size="48" />
|
||||
<div class="media-content">
|
||||
<div>
|
||||
<div class="prose dark:prose-invert">
|
||||
<span v-if="props.row.actor.preferredUsername !== 'anonymous'">
|
||||
<span v-if="props.row.actor.name">{{
|
||||
props.row.actor.name
|
||||
}}</span
|
||||
><br />
|
||||
<span class="is-size-7 has-text-grey-dark"
|
||||
>@{{ usernameWithDomain(props.row.actor) }}</span
|
||||
>
|
||||
</span>
|
||||
>@{{ usernameWithDomain(props.row.actor) }}</span
|
||||
>
|
||||
<span v-else>
|
||||
{{ t("Anonymous participant") }}
|
||||
</span>
|
||||
@@ -139,30 +146,30 @@
|
||||
</article>
|
||||
</o-table-column>
|
||||
<o-table-column field="role" :label="t('Role')" v-slot="props">
|
||||
<b-tag
|
||||
<tag
|
||||
variant="primary"
|
||||
v-if="props.row.role === ParticipantRole.CREATOR"
|
||||
>
|
||||
{{ t("Organizer") }}
|
||||
</b-tag>
|
||||
<b-tag v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
|
||||
</tag>
|
||||
<tag v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
|
||||
{{ t("Participant") }}
|
||||
</b-tag>
|
||||
<b-tag v-else-if="props.row.role === ParticipantRole.NOT_CONFIRMED">
|
||||
</tag>
|
||||
<tag v-else-if="props.row.role === ParticipantRole.NOT_CONFIRMED">
|
||||
{{ t("Not confirmed") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="warning"
|
||||
v-else-if="props.row.role === ParticipantRole.NOT_APPROVED"
|
||||
>
|
||||
{{ t("Not approved") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === ParticipantRole.REJECTED"
|
||||
>
|
||||
{{ t("Rejected") }}
|
||||
</b-tag>
|
||||
</tag>
|
||||
</o-table-column>
|
||||
<o-table-column
|
||||
field="metadata.message"
|
||||
@@ -250,17 +257,17 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ParticipantRole } from "@/types/enums";
|
||||
import { IParticipant } from "../../types/participant.model";
|
||||
import { IEvent, IEventParticipantStats } from "../../types/event.model";
|
||||
import { IParticipant } from "@/types/participant.model";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import {
|
||||
EXPORT_EVENT_PARTICIPATIONS,
|
||||
PARTICIPANTS,
|
||||
UPDATE_PARTICIPANT,
|
||||
} from "../../graphql/event";
|
||||
import { usernameWithDomain } from "../../types/actor";
|
||||
import { nl2br } from "../../utils/html";
|
||||
import { asyncForEach } from "../../utils/asyncForEach";
|
||||
import RouteName from "../../router/name";
|
||||
} from "@/graphql/event";
|
||||
import { usernameWithDomain } from "@/types/actor";
|
||||
import { nl2br } from "@/utils/html";
|
||||
import { asyncForEach } from "@/utils/asyncForEach";
|
||||
import RouteName from "@/router/name";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useParticipantsExportFormats } from "@/composition/config";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
@@ -276,6 +283,7 @@ import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Incognito from "vue-material-design-icons/Incognito.vue";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
|
||||
const PARTICIPANTS_PER_PAGE = 10;
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
@@ -286,6 +294,8 @@ const props = defineProps<{
|
||||
eventId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["sort"]);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
@@ -307,11 +317,9 @@ const role = useRouteQuery(
|
||||
enumTransformer(ParticipantRole)
|
||||
);
|
||||
|
||||
const limit = ref(PARTICIPANTS_PER_PAGE);
|
||||
|
||||
const checkedRows = ref<IParticipant[]>([]);
|
||||
|
||||
// const queueTable = ref(null);
|
||||
const queueTable = ref();
|
||||
|
||||
const { result: participantsResult, loading: participantsLoading } = useQuery<{
|
||||
event: IEvent;
|
||||
@@ -333,10 +341,10 @@ const { result: participantsResult, loading: participantsLoading } = useQuery<{
|
||||
|
||||
const event = computed(() => participantsResult.value?.event);
|
||||
|
||||
const participantStats = computed((): IEventParticipantStats | null => {
|
||||
if (!event.value) return null;
|
||||
return event.value.participantStats;
|
||||
});
|
||||
// const participantStats = computed((): IEventParticipantStats | null => {
|
||||
// if (!event.value) return null;
|
||||
// return event.value.participantStats;
|
||||
// });
|
||||
|
||||
const { mutate: updateParticipant, onError: onUpdateParticipantError } =
|
||||
useMutation(UPDATE_PARTICIPANT);
|
||||
@@ -373,14 +381,14 @@ const {
|
||||
onError: onExportParticipantsMutationError,
|
||||
} = useMutation(EXPORT_EVENT_PARTICIPATIONS);
|
||||
|
||||
onExportParticipantsMutationDone(({ data }) => {
|
||||
onExportParticipantsMutationDone(({ data, context }) => {
|
||||
const link =
|
||||
window.origin +
|
||||
"/exports/" +
|
||||
type.toLowerCase() +
|
||||
context?.type.toLowerCase() +
|
||||
"/" +
|
||||
exportEventParticipants;
|
||||
console.log(link);
|
||||
data?.exportEventParticipants;
|
||||
console.debug(link);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
@@ -449,7 +457,7 @@ const toggleQueueDetails = (row: IParticipant): void => {
|
||||
}
|
||||
};
|
||||
|
||||
const openDetailedRows = <Record<string, boolean>>{};
|
||||
const openDetailedRows = ref<Record<string, boolean>>({});
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
@@ -485,21 +493,4 @@ nav.breadcrumb {
|
||||
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>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="container mx-auto">
|
||||
<h1>{{ $t("Create a new group") }}</h1>
|
||||
<h1>{{ t("Create a new group") }}</h1>
|
||||
|
||||
<o-notification
|
||||
variant="danger"
|
||||
@@ -11,7 +11,7 @@
|
||||
</o-notification>
|
||||
|
||||
<form @submit.prevent="createGroup">
|
||||
<o-field :label="$t('Group display name')" label-for="group-display-name">
|
||||
<o-field :label="t('Group display name')" label-for="group-display-name">
|
||||
<o-input
|
||||
aria-required="true"
|
||||
required
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="group-preferred-username">{{
|
||||
$t("Federated Group Name")
|
||||
t("Federated Group Name")
|
||||
}}</label>
|
||||
<div class="field-body">
|
||||
<o-field
|
||||
@@ -40,7 +40,7 @@
|
||||
:useHtml5Validation="true"
|
||||
:validation-message="
|
||||
group.preferredUsername
|
||||
? $t(
|
||||
? t(
|
||||
'Only alphanumeric lowercased characters and underscores are supported.'
|
||||
)
|
||||
: null
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
|
||||
<o-field
|
||||
:label="$t('Description')"
|
||||
:label="t('Description')"
|
||||
label-for="group-summary"
|
||||
:message="summaryErrors[0]"
|
||||
:type="summaryErrors[1]"
|
||||
@@ -73,26 +73,26 @@
|
||||
</o-field>
|
||||
|
||||
<div>
|
||||
<b>{{ $t("Avatar") }}</b>
|
||||
<b>{{ t("Avatar") }}</b>
|
||||
<picture-upload
|
||||
:textFallback="$t('Avatar')"
|
||||
:textFallback="t('Avatar')"
|
||||
v-model="avatarFile"
|
||||
:maxSize="avatarMaxSize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b>{{ $t("Banner") }}</b>
|
||||
<b>{{ t("Banner") }}</b>
|
||||
<picture-upload
|
||||
:textFallback="$t('Banner')"
|
||||
:textFallback="t('Banner')"
|
||||
v-model="bannerFile"
|
||||
:maxSize="bannerMaxSize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="button is-primary" native-type="submit">
|
||||
{{ $t("Create my group") }}
|
||||
</button>
|
||||
<o-button variant="primary" native-type="submit">
|
||||
{{ t("Create my group") }}
|
||||
</o-button>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
@@ -105,7 +105,6 @@ import PictureUpload from "../../components/PictureUpload.vue";
|
||||
import { ErrorResponse } from "@/types/errors.model";
|
||||
import { ServerParseError } from "@apollo/client/link/http";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useUploadLimits } from "@/composition/apollo/config";
|
||||
import { computed, inject, reactive, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
@@ -116,9 +115,9 @@ import {
|
||||
useHost,
|
||||
} from "@/composition/config";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
const { uploadLimits } = useUploadLimits();
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
@@ -146,34 +145,14 @@ const bannerMaxSize = useBannerMaxSize();
|
||||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
const createGroup = async (): Promise<void> => {
|
||||
errors.value = [];
|
||||
fieldErrors.preferred_username = undefined;
|
||||
fieldErrors.summary = undefined;
|
||||
const variables = buildVariables();
|
||||
const { onDone, onError } = useCreateGroup(variables);
|
||||
watch(
|
||||
() => group.value.name,
|
||||
(newGroupName) => {
|
||||
group.value.preferredUsername = convertToUsername(newGroupName);
|
||||
}
|
||||
);
|
||||
|
||||
onDone(() => {
|
||||
notifier?.success(
|
||||
t("Group {displayName} created", {
|
||||
displayName: displayName(group),
|
||||
})
|
||||
);
|
||||
|
||||
router.push({
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group.value) },
|
||||
});
|
||||
});
|
||||
|
||||
onError((err) => handleError(err as unknown as ErrorResponse));
|
||||
};
|
||||
|
||||
watch(group, (newGroup) => {
|
||||
group.value.preferredUsername = convertToUsername(newGroup.name);
|
||||
});
|
||||
|
||||
const buildVariables = () => {
|
||||
const buildVariables = computed(() => {
|
||||
let avatarObj = {};
|
||||
let bannerObj = {};
|
||||
|
||||
@@ -212,7 +191,7 @@ const buildVariables = () => {
|
||||
...avatarObj,
|
||||
...bannerObj,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
const handleError = (err: ErrorResponse) => {
|
||||
if (err?.networkError?.name === "ServerParseError") {
|
||||
@@ -241,7 +220,7 @@ const handleError = (err: ErrorResponse) => {
|
||||
|
||||
const summaryErrors = computed(() => {
|
||||
const message = fieldErrors.summary ? fieldErrors.summary : undefined;
|
||||
const type = fieldErrors.summary ? "is-danger" : undefined;
|
||||
const type = fieldErrors.summary ? "danger" : undefined;
|
||||
return [message, type];
|
||||
});
|
||||
|
||||
@@ -251,9 +230,33 @@ const preferredUsernameErrors = computed(() => {
|
||||
: t(
|
||||
"Only alphanumeric lowercased characters and underscores are supported."
|
||||
);
|
||||
const type = fieldErrors.preferred_username ? "is-danger" : undefined;
|
||||
const type = fieldErrors.preferred_username ? "danger" : undefined;
|
||||
return [message, type];
|
||||
});
|
||||
|
||||
const { onDone, onError, mutate } = useCreateGroup();
|
||||
|
||||
onDone(() => {
|
||||
notifier?.success(
|
||||
t("Group {displayName} created", {
|
||||
displayName: displayName(group.value),
|
||||
})
|
||||
);
|
||||
|
||||
router.push({
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group.value) },
|
||||
});
|
||||
});
|
||||
|
||||
onError((err) => handleError(err as unknown as ErrorResponse));
|
||||
|
||||
const createGroup = async (): Promise<void> => {
|
||||
errors.value = [];
|
||||
fieldErrors.preferred_username = undefined;
|
||||
fieldErrors.summary = undefined;
|
||||
mutate(buildVariables.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -25,6 +25,7 @@
|
||||
class="container mx-auto section"
|
||||
v-if="group && isCurrentActorAGroupAdmin"
|
||||
>
|
||||
<h1>{{ t("Group Members") }} ({{ group.members.total }})</h1>
|
||||
<form @submit.prevent="inviteMember">
|
||||
<o-field
|
||||
:label="t('Invite a new member')"
|
||||
@@ -54,14 +55,14 @@
|
||||
</o-field>
|
||||
</o-field>
|
||||
</form>
|
||||
<h1>{{ t("Group Members") }} ({{ group.members.total }})</h1>
|
||||
<o-field
|
||||
class="my-2"
|
||||
:label="t('Status')"
|
||||
horizontal
|
||||
label-for="group-members-status-filter"
|
||||
>
|
||||
<o-select v-model="roles" id="group-members-status-filter">
|
||||
<option value="">
|
||||
<option :value="undefined">
|
||||
{{ t("Everything") }}
|
||||
</option>
|
||||
<option :value="MemberRole.ADMINISTRATOR">
|
||||
@@ -103,7 +104,7 @@
|
||||
:default-sort-direction="'desc'"
|
||||
:default-sort="['insertedAt', 'desc']"
|
||||
@page-change="loadMoreMembers"
|
||||
@sort="(field, order) => $emit('sort', field, order)"
|
||||
@sort="(field: string, order: string) => emit('sort', field, order)"
|
||||
>
|
||||
<o-table-column
|
||||
field="actor.preferredUsername"
|
||||
@@ -123,7 +124,7 @@
|
||||
<AccountCircle v-else :size="48" />
|
||||
|
||||
<div class="">
|
||||
<div class="">
|
||||
<div class="text-start">
|
||||
<span v-if="props.row.actor.name">{{
|
||||
props.row.actor.name
|
||||
}}</span
|
||||
@@ -134,39 +135,39 @@
|
||||
</article>
|
||||
</o-table-column>
|
||||
<o-table-column field="role" :label="t('Role')" v-slot="props">
|
||||
<b-tag
|
||||
<tag
|
||||
variant="info"
|
||||
v-if="props.row.role === MemberRole.ADMINISTRATOR"
|
||||
>
|
||||
{{ t("Administrator") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="info"
|
||||
v-else-if="props.row.role === MemberRole.MODERATOR"
|
||||
>
|
||||
{{ t("Moderator") }}
|
||||
</b-tag>
|
||||
<b-tag v-else-if="props.row.role === MemberRole.MEMBER">
|
||||
</tag>
|
||||
<tag v-else-if="props.row.role === MemberRole.MEMBER">
|
||||
{{ t("Member") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="warning"
|
||||
v-else-if="props.row.role === MemberRole.NOT_APPROVED"
|
||||
>
|
||||
{{ t("Not approved") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === MemberRole.REJECTED"
|
||||
>
|
||||
{{ t("Rejected") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="warning"
|
||||
v-else-if="props.row.role === MemberRole.INVITED"
|
||||
>
|
||||
{{ t("Invited") }}
|
||||
</b-tag>
|
||||
</tag>
|
||||
</o-table-column>
|
||||
<o-table-column field="insertedAt" :label="t('Date')" v-slot="props">
|
||||
<span class="has-text-centered">
|
||||
@@ -253,13 +254,12 @@ import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject, ref, watch } from "vue";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import {
|
||||
enumTransformer,
|
||||
integerTransformer,
|
||||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
useCurrentActorClient,
|
||||
usePersonStatusGroup,
|
||||
@@ -267,6 +267,7 @@ import {
|
||||
import { formatTimeString, formatDateString } from "@/filters/datetime";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
@@ -276,9 +277,10 @@ useHead({
|
||||
|
||||
const props = defineProps<{ preferredUsername: string }>();
|
||||
|
||||
const emit = defineEmits(["sort"]);
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const loading = ref(true);
|
||||
const newMemberUsername = ref("");
|
||||
const inviteError = ref("");
|
||||
const page = useRouteQuery("page", 1, integerTransformer);
|
||||
@@ -339,9 +341,6 @@ const inviteMember = async (): Promise<void> => {
|
||||
});
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const loadMoreMembers = async (): Promise<void> => {
|
||||
await fetchMoreGroupMembers({
|
||||
// New variables
|
||||
@@ -368,13 +367,13 @@ const {
|
||||
],
|
||||
}));
|
||||
|
||||
onRemoveMemberDone(() => {
|
||||
onRemoveMemberDone(({ context }) => {
|
||||
let message = t("The member was removed from the group {group}", {
|
||||
group: displayName(group.value),
|
||||
}) as string;
|
||||
if (oldMember.role === MemberRole.NOT_APPROVED) {
|
||||
if (context?.oldMember.role === MemberRole.NOT_APPROVED) {
|
||||
message = t("The membership request from {profile} was rejected", {
|
||||
group: displayName(oldMember.actor),
|
||||
group: displayName(context?.oldMember.actor),
|
||||
}) as string;
|
||||
}
|
||||
notifier?.success(message);
|
||||
@@ -388,10 +387,15 @@ onRemoveMemberError((error) => {
|
||||
});
|
||||
|
||||
const removeMember = (oldMember: IMember) => {
|
||||
mutateRemoveMember({
|
||||
groupId: group.value?.id,
|
||||
memberId: oldMember.id,
|
||||
});
|
||||
mutateRemoveMember(
|
||||
{
|
||||
groupId: group.value?.id,
|
||||
memberId: oldMember.id,
|
||||
},
|
||||
{
|
||||
context: { oldMember },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const promoteMember = (member: IMember): void => {
|
||||
@@ -465,7 +469,7 @@ onUpdateMutationDone(({ data, context }) => {
|
||||
successMessage = "The member role was updated to administrator";
|
||||
break;
|
||||
case MemberRole.MEMBER:
|
||||
if (oldMember.role === MemberRole.NOT_APPROVED) {
|
||||
if (context?.oldMember.role === MemberRole.NOT_APPROVED) {
|
||||
successMessage = "The member was approved";
|
||||
} else {
|
||||
successMessage = "The member role was updated to simple member";
|
||||
@@ -488,11 +492,14 @@ const updateMember = async (
|
||||
oldMember: IMember,
|
||||
role: MemberRole
|
||||
): Promise<void> => {
|
||||
updateMemberMutation({
|
||||
memberId: oldMember.id as string,
|
||||
role,
|
||||
oldRole: oldMember.role,
|
||||
});
|
||||
updateMemberMutation(
|
||||
{
|
||||
memberId: oldMember.id as string,
|
||||
role,
|
||||
oldRole: oldMember.role,
|
||||
},
|
||||
{ context: { oldMember } }
|
||||
);
|
||||
};
|
||||
|
||||
const isCurrentActorAGroupAdmin = computed((): boolean => {
|
||||
@@ -500,10 +507,10 @@ const isCurrentActorAGroupAdmin = computed((): boolean => {
|
||||
});
|
||||
|
||||
const hasCurrentActorThisRole = (givenRole: string | string[]): boolean => {
|
||||
const roles = Array.isArray(givenRole) ? givenRole : [givenRole];
|
||||
const rolesToConsider = Array.isArray(givenRole) ? givenRole : [givenRole];
|
||||
return (
|
||||
personMemberships.value?.total > 0 &&
|
||||
roles.includes(personMemberships.value?.elements[0].role)
|
||||
rolesToConsider.includes(personMemberships.value?.elements[0].role)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,60 +11,60 @@
|
||||
{
|
||||
name: RouteName.GROUP_SETTINGS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
text: $t('Settings'),
|
||||
text: t('Settings'),
|
||||
},
|
||||
{
|
||||
name: RouteName.GROUP_PUBLIC_SETTINGS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
text: $t('Group settings'),
|
||||
text: t('Group settings'),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<o-loading :active="loading" />
|
||||
<section
|
||||
class="container mx-auto section"
|
||||
class="container mx-auto mb-6"
|
||||
v-if="group && isCurrentActorAGroupAdmin"
|
||||
>
|
||||
<form @submit.prevent="updateGroup">
|
||||
<o-field :label="$t('Group name')" label-for="group-settings-name">
|
||||
<form @submit.prevent="updateGroup(buildVariables)" v-if="editableGroup">
|
||||
<o-field :label="t('Group name')" label-for="group-settings-name">
|
||||
<o-input v-model="editableGroup.name" id="group-settings-name" />
|
||||
</o-field>
|
||||
<o-field :label="$t('Group short description')">
|
||||
<o-field :label="t('Group short description')">
|
||||
<Editor
|
||||
mode="basic"
|
||||
v-model="editableGroup.summary"
|
||||
:maxSize="500"
|
||||
:aria-label="$t('Group description body')"
|
||||
:aria-label="t('Group description body')"
|
||||
v-if="currentActor"
|
||||
:currentActor="currentActor"
|
||||
/></o-field>
|
||||
<o-field :label="$t('Avatar')">
|
||||
<o-field :label="t('Avatar')">
|
||||
<picture-upload
|
||||
:textFallback="$t('Avatar')"
|
||||
:textFallback="t('Avatar')"
|
||||
v-model="avatarFile"
|
||||
:defaultImage="group.avatar"
|
||||
:maxSize="avatarMaxSize"
|
||||
/>
|
||||
</o-field>
|
||||
|
||||
<o-field :label="$t('Banner')">
|
||||
<o-field :label="t('Banner')">
|
||||
<picture-upload
|
||||
:textFallback="$t('Banner')"
|
||||
:textFallback="t('Banner')"
|
||||
v-model="bannerFile"
|
||||
:defaultImage="group.banner"
|
||||
:maxSize="bannerMaxSize"
|
||||
/>
|
||||
</o-field>
|
||||
<p class="label">{{ $t("Group visibility") }}</p>
|
||||
<p class="label">{{ t("Group visibility") }}</p>
|
||||
<div class="field">
|
||||
<o-radio
|
||||
v-model="editableGroup.visibility"
|
||||
name="groupVisibility"
|
||||
:native-value="GroupVisibility.PUBLIC"
|
||||
>
|
||||
{{ $t("Visible everywhere on the web") }}<br />
|
||||
{{ t("Visible everywhere on the web") }}<br />
|
||||
<small>{{
|
||||
$t(
|
||||
t(
|
||||
"The group will be publicly listed in search results and may be suggested in the explore section. Only public informations will be shown on it's page."
|
||||
)
|
||||
}}</small>
|
||||
@@ -75,9 +75,9 @@
|
||||
v-model="editableGroup.visibility"
|
||||
name="groupVisibility"
|
||||
:native-value="GroupVisibility.UNLISTED"
|
||||
>{{ $t("Only accessible through link") }}<br />
|
||||
>{{ t("Only accessible through link") }}<br />
|
||||
<small>{{
|
||||
$t(
|
||||
t(
|
||||
"You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines."
|
||||
)
|
||||
}}</small>
|
||||
@@ -86,7 +86,7 @@
|
||||
<code>{{ group.url }}</code>
|
||||
<o-tooltip
|
||||
v-if="canShowCopyButton"
|
||||
:label="$t('URL copied to clipboard')"
|
||||
:label="t('URL copied to clipboard')"
|
||||
:active="showCopiedTooltip"
|
||||
always
|
||||
variant="success"
|
||||
@@ -103,16 +103,16 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="label">{{ $t("New members") }}</p>
|
||||
<p class="label">{{ t("New members") }}</p>
|
||||
<div class="field">
|
||||
<o-radio
|
||||
v-model="editableGroup.openness"
|
||||
name="groupOpenness"
|
||||
:native-value="Openness.OPEN"
|
||||
>
|
||||
{{ $t("Anyone can join freely") }}<br />
|
||||
{{ t("Anyone can join freely") }}<br />
|
||||
<small>{{
|
||||
$t(
|
||||
t(
|
||||
"Anyone wanting to be a member from your group will be able to from your group page."
|
||||
)
|
||||
}}</small>
|
||||
@@ -123,9 +123,9 @@
|
||||
v-model="editableGroup.openness"
|
||||
name="groupOpenness"
|
||||
:native-value="Openness.MODERATED"
|
||||
>{{ $t("Moderate new members") }}<br />
|
||||
>{{ t("Moderate new members") }}<br />
|
||||
<small>{{
|
||||
$t(
|
||||
t(
|
||||
"Anyone can request being a member, but an administrator needs to approve the membership."
|
||||
)
|
||||
}}</small>
|
||||
@@ -136,9 +136,9 @@
|
||||
v-model="editableGroup.openness"
|
||||
name="groupOpenness"
|
||||
:native-value="Openness.INVITE_ONLY"
|
||||
>{{ $t("Manually invite new members") }}<br />
|
||||
>{{ t("Manually invite new members") }}<br />
|
||||
<small>{{
|
||||
$t(
|
||||
t(
|
||||
"The only way for your group to get new members is if an admininistrator invites them."
|
||||
)
|
||||
}}</small>
|
||||
@@ -146,26 +146,26 @@
|
||||
</div>
|
||||
|
||||
<o-field
|
||||
:label="$t('Followers')"
|
||||
:message="$t('Followers will receive new public events and posts.')"
|
||||
:label="t('Followers')"
|
||||
:message="t('Followers will receive new public events and posts.')"
|
||||
>
|
||||
<o-checkbox v-model="editableGroup.manuallyApprovesFollowers">
|
||||
{{ $t("Manually approve new followers") }}
|
||||
{{ t("Manually approve new followers") }}
|
||||
</o-checkbox>
|
||||
</o-field>
|
||||
|
||||
<full-address-auto-complete
|
||||
:label="$t('Group address')"
|
||||
:label="t('Group address')"
|
||||
v-model="currentAddress"
|
||||
:hideMap="true"
|
||||
/>
|
||||
|
||||
<div class="flex flex-wrap gap-2 my-2">
|
||||
<o-button native-type="submit" variant="primary">{{
|
||||
$t("Update group")
|
||||
t("Update group")
|
||||
}}</o-button>
|
||||
<o-button @click="confirmDeleteGroup" variant="danger">{{
|
||||
$t("Delete group")
|
||||
t("Delete group")
|
||||
}}</o-button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -178,7 +178,7 @@
|
||||
</o-notification>
|
||||
</section>
|
||||
<o-notification v-else-if="!loading">
|
||||
{{ $t("You are not an administrator for this group.") }}
|
||||
{{ t("You are not an administrator for this group.") }}
|
||||
</o-notification>
|
||||
</div>
|
||||
</template>
|
||||
@@ -187,7 +187,7 @@
|
||||
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
|
||||
import PictureUpload from "@/components/PictureUpload.vue";
|
||||
import { GroupVisibility, MemberRole, Openness } from "@/types/enums";
|
||||
import { Group, IGroup, usernameWithDomain, displayName } from "@/types/actor";
|
||||
import { IGroup, usernameWithDomain, displayName } from "@/types/actor";
|
||||
import { Address, IAddress } from "@/types/address.model";
|
||||
import { ServerParseError } from "@apollo/client/link/http";
|
||||
import { ErrorResponse } from "@apollo/client/link/error";
|
||||
@@ -208,13 +208,19 @@ import { Dialog } from "@/plugins/dialog";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue"));
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("@/components/TextEditor.vue")
|
||||
);
|
||||
|
||||
const props = defineProps<{ preferredUsername: string }>();
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const { group, loading } = useGroup(props.preferredUsername);
|
||||
const {
|
||||
group,
|
||||
loading,
|
||||
onResult: onGroupResult,
|
||||
} = useGroup(props.preferredUsername);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
@@ -231,20 +237,17 @@ const errors = ref<string[]>([]);
|
||||
|
||||
const showCopiedTooltip = ref(false);
|
||||
|
||||
const editableGroup = ref<IGroup>(new Group());
|
||||
const editableGroup = ref<IGroup>();
|
||||
|
||||
const updateGroup = async (): Promise<void> => {
|
||||
const variables = buildVariables();
|
||||
const { onDone, onError } = useUpdateGroup(variables);
|
||||
const { onDone, onError, mutate: updateGroup } = useUpdateGroup();
|
||||
|
||||
onDone(() => {
|
||||
notifier?.success(t("Group settings saved") as string);
|
||||
});
|
||||
onDone(() => {
|
||||
notifier?.success(t("Group settings saved"));
|
||||
});
|
||||
|
||||
onError((err) => {
|
||||
handleError(err as unknown as ErrorResponse);
|
||||
});
|
||||
};
|
||||
onError((err) => {
|
||||
handleError(err as unknown as ErrorResponse);
|
||||
});
|
||||
|
||||
const copyURL = async (): Promise<void> => {
|
||||
await window.navigator.clipboard.writeText(group.value?.url ?? "");
|
||||
@@ -254,28 +257,40 @@ const copyURL = async (): Promise<void> => {
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
watch(group, async (oldGroup: IGroup, newGroup: IGroup) => {
|
||||
try {
|
||||
if (
|
||||
oldGroup?.avatar !== undefined &&
|
||||
oldGroup?.avatar !== newGroup?.avatar
|
||||
) {
|
||||
avatarFile.value = await buildFileFromIMedia(group.value?.avatar);
|
||||
}
|
||||
if (
|
||||
oldGroup?.banner !== undefined &&
|
||||
oldGroup?.banner !== newGroup?.banner
|
||||
) {
|
||||
bannerFile.value = await buildFileFromIMedia(group.value?.banner);
|
||||
}
|
||||
} catch (e) {
|
||||
// Catch errors while building media
|
||||
console.error(e);
|
||||
}
|
||||
editableGroup.value = { ...group.value };
|
||||
onGroupResult(({ data }) => {
|
||||
editableGroup.value = data.group;
|
||||
});
|
||||
|
||||
const buildVariables = () => {
|
||||
watch(
|
||||
group,
|
||||
async (newGroup: IGroup, oldGroup: IGroup) => {
|
||||
console.debug("watching group");
|
||||
if (!newGroup) return;
|
||||
try {
|
||||
if (
|
||||
oldGroup?.avatar !== undefined &&
|
||||
oldGroup?.avatar !== newGroup?.avatar
|
||||
) {
|
||||
avatarFile.value = await buildFileFromIMedia(newGroup?.avatar);
|
||||
}
|
||||
if (
|
||||
oldGroup?.banner !== undefined &&
|
||||
oldGroup?.banner !== newGroup?.banner
|
||||
) {
|
||||
bannerFile.value = await buildFileFromIMedia(newGroup?.banner);
|
||||
}
|
||||
} catch (e) {
|
||||
// Catch errors while building media
|
||||
console.error(e);
|
||||
}
|
||||
editableGroup.value = { ...newGroup };
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const buildVariables = computed(() => {
|
||||
let avatarObj = {};
|
||||
let bannerObj = {};
|
||||
const variables = { ...editableGroup.value };
|
||||
@@ -309,7 +324,7 @@ const buildVariables = () => {
|
||||
media: {
|
||||
name: avatarFile.value?.name,
|
||||
alt: `${editableGroup.value?.preferredUsername}'s avatar`,
|
||||
file: avatarFile,
|
||||
file: avatarFile.value,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -321,13 +336,13 @@ const buildVariables = () => {
|
||||
media: {
|
||||
name: bannerFile.value?.name,
|
||||
alt: `${editableGroup.value?.preferredUsername}'s banner`,
|
||||
file: bannerFile,
|
||||
file: bannerFile.value,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: group.value?.id,
|
||||
id: group.value?.id ?? "",
|
||||
name: editableGroup.value?.name,
|
||||
summary: editableGroup.value?.summary,
|
||||
visibility: editableGroup.value?.visibility,
|
||||
@@ -337,7 +352,7 @@ const buildVariables = () => {
|
||||
...avatarObj,
|
||||
...bannerObj,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
const canShowCopyButton = computed((): boolean => {
|
||||
return window.isSecureContext;
|
||||
@@ -348,7 +363,9 @@ const currentAddress = computed({
|
||||
return new Address(editableGroup.value?.physicalAddress);
|
||||
},
|
||||
set(address: IAddress) {
|
||||
editableGroup.value.physicalAddress = address;
|
||||
if (editableGroup.value) {
|
||||
editableGroup.value.physicalAddress = address;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="flex self-center h-0 mt-4 items-end">
|
||||
<figure class="" v-if="group.avatar">
|
||||
<img
|
||||
class="rounded-full border"
|
||||
class="rounded-full border h-32 w-32"
|
||||
:src="group.avatar.url"
|
||||
alt=""
|
||||
width="128"
|
||||
@@ -382,7 +382,7 @@
|
||||
</header>
|
||||
</div>
|
||||
<div
|
||||
v-if="isCurrentActorAGroupMember && !previewPublic"
|
||||
v-if="isCurrentActorAGroupMember && !previewPublic && group"
|
||||
class="block-container flex gap-2 flex-wrap mt-3"
|
||||
>
|
||||
<!-- Private things -->
|
||||
@@ -419,14 +419,20 @@
|
||||
<aside class="group-metadata">
|
||||
<div class="sticky">
|
||||
<o-notification v-if="group.domain && !isCurrentActorAGroupMember">
|
||||
{{
|
||||
t(
|
||||
"This profile is from another instance, the informations shown here may be incomplete."
|
||||
)
|
||||
}}
|
||||
<a :href="group.url" rel="noopener noreferrer external">{{
|
||||
t("View full profile")
|
||||
}}</a>
|
||||
<p>
|
||||
{{
|
||||
t(
|
||||
"This profile is from another instance, the informations shown here may be incomplete."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<o-button
|
||||
variant="text"
|
||||
tag="a"
|
||||
:href="group.url"
|
||||
rel="noopener noreferrer external"
|
||||
>{{ t("View full profile") }}</o-button
|
||||
>
|
||||
</o-notification>
|
||||
<event-metadata-block
|
||||
:title="t('About')"
|
||||
@@ -480,7 +486,7 @@
|
||||
</div>
|
||||
<o-button
|
||||
class="map-show-button"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
@click="showMap = !showMap"
|
||||
@keyup.enter="showMap = !showMap"
|
||||
v-if="physicalAddress.geom"
|
||||
@@ -528,7 +534,7 @@
|
||||
<o-button
|
||||
tag="router-link"
|
||||
class="my-2 self-center"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
:to="{
|
||||
name: RouteName.GROUP_EVENTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
@@ -543,7 +549,7 @@
|
||||
<o-button
|
||||
tag="router-link"
|
||||
class="my-4"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
v-if="organizedEvents.total > 0"
|
||||
:to="{
|
||||
name: RouteName.GROUP_EVENTS,
|
||||
@@ -579,7 +585,7 @@
|
||||
class="self-center my-2"
|
||||
v-if="posts.total > 0"
|
||||
tag="router-link"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
:to="{
|
||||
name: RouteName.POSTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
@@ -606,6 +612,7 @@
|
||||
</div>
|
||||
<o-modal v-if="group" v-model:active="isReportModalActive">
|
||||
<report-modal
|
||||
ref="reportModalRef"
|
||||
:on-confirm="reportGroup"
|
||||
:title="t('Report this group')"
|
||||
:outside-domain="group.domain"
|
||||
@@ -637,7 +644,7 @@ import { JOIN_GROUP } from "@/graphql/member";
|
||||
import { MemberRole, Openness, PostVisibility } from "@/types/enums";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import RouteName from "../../router/name";
|
||||
import ReportModal from "../../components/Report/ReportModal.vue";
|
||||
import ReportModal from "@/components/Report/ReportModal.vue";
|
||||
import {
|
||||
GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED,
|
||||
PERSON_STATUS_GROUP,
|
||||
@@ -648,11 +655,7 @@ import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { IPost } from "@/types/post.model";
|
||||
import {
|
||||
FOLLOW_GROUP,
|
||||
UNFOLLOW_GROUP,
|
||||
UPDATE_GROUP_FOLLOW,
|
||||
} from "@/graphql/followers";
|
||||
import { FOLLOW_GROUP, UPDATE_GROUP_FOLLOW } from "@/graphql/followers";
|
||||
import { useAnonymousReportsConfig } from "../../composition/apollo/config";
|
||||
import { computed, defineAsyncComponent, inject, ref, watch } from "vue";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
@@ -670,10 +673,10 @@ import AccountMultiplePlus from "vue-material-design-icons/AccountMultiplePlus.v
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useCreateReport } from "@/composition/apollo/report";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import Discussions from "@/components/Group/Sections/Discussions.vue";
|
||||
import Resources from "@/components/Group/Sections/Resources.vue";
|
||||
import Posts from "@/components/Group/Sections/Posts.vue";
|
||||
import Events from "@/components/Group/Sections/Events.vue";
|
||||
import Discussions from "@/components/Group/Sections/DiscussionsSection.vue";
|
||||
import Resources from "@/components/Group/Sections/ResourcesSection.vue";
|
||||
import Posts from "@/components/Group/Sections/PostsSection.vue";
|
||||
import Events from "@/components/Group/Sections/EventsSection.vue";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
@@ -718,28 +721,32 @@ subscribeToMore<{ actorId: string; group: string }>({
|
||||
const person = computed(() => result.value?.person);
|
||||
|
||||
const MapLeaflet = defineAsyncComponent(
|
||||
() => import("../../components/Map.vue")
|
||||
() => import("@/components/LeafletMap.vue")
|
||||
);
|
||||
const ShareGroupModal = defineAsyncComponent(
|
||||
() => import("../../components/Group/ShareGroupModal.vue")
|
||||
() => import("@/components/Group/ShareGroupModal.vue")
|
||||
);
|
||||
|
||||
const showMap = ref(false);
|
||||
const isReportModalActive = ref(false);
|
||||
const reportModalRef = ref();
|
||||
const isShareModalActive = ref(false);
|
||||
const previewPublic = ref(false);
|
||||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
watch(currentActor, (watchedCurrentActor: IActor, oldActor: IActor) => {
|
||||
if (
|
||||
watchedCurrentActor.id &&
|
||||
oldActor &&
|
||||
watchedCurrentActor.id !== oldActor.id
|
||||
) {
|
||||
refetchGroup();
|
||||
watch(
|
||||
currentActor,
|
||||
(watchedCurrentActor: IActor | undefined, oldActor: IActor | undefined) => {
|
||||
if (
|
||||
watchedCurrentActor?.id &&
|
||||
oldActor &&
|
||||
watchedCurrentActor?.id !== oldActor.id
|
||||
) {
|
||||
refetchGroup();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const { mutate: joinGroupMutation, onError: onJoinGroupError } =
|
||||
useMutation(JOIN_GROUP);
|
||||
@@ -785,7 +792,7 @@ const dialog = inject<Dialog>("dialog");
|
||||
|
||||
const openLeaveGroupModal = async (): Promise<void> => {
|
||||
dialog?.confirm({
|
||||
type: "danger",
|
||||
variant: "danger",
|
||||
title: t("Leave group"),
|
||||
message: t(
|
||||
"Are you sure you want to leave the group {groupName}? You'll loose access to this group's private content. This action cannot be undone.",
|
||||
@@ -902,7 +909,7 @@ const toggleFollowNotify = () => {
|
||||
|
||||
const reportGroup = async (content: string, forward: boolean) => {
|
||||
isReportModalActive.value = false;
|
||||
reportModal.value.close();
|
||||
reportModalRef.value.close();
|
||||
|
||||
const {
|
||||
mutate: createReportMutation,
|
||||
@@ -937,8 +944,8 @@ const triggerShare = (): void => {
|
||||
title: displayName(group.value),
|
||||
url: group.value?.url,
|
||||
})
|
||||
.then(() => console.log("Successful share"))
|
||||
.catch((error: any) => console.log("Error sharing", error));
|
||||
.then(() => console.debug("Successful share"))
|
||||
.catch((error: any) => console.debug("Error sharing", error));
|
||||
} else {
|
||||
isShareModalActive.value = true;
|
||||
// send popup
|
||||
@@ -1100,7 +1107,7 @@ const isCurrentActorAPendingGroupMember = computed((): boolean => {
|
||||
});
|
||||
|
||||
const currentActorFollow = computed((): IFollower | undefined => {
|
||||
if (person?.value && person?.value?.follows?.total > 0) {
|
||||
if (person?.value?.follows?.total && person?.value?.follows?.total > 0) {
|
||||
return person?.value?.follows?.elements[0];
|
||||
}
|
||||
return undefined;
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="container mx-auto">
|
||||
<h1 class="">{{ t("Settings") }}</h1>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<aside class="max-w-xs flex-1">
|
||||
<aside class="sm:max-w-xs flex-1 min-w-[320px]">
|
||||
<ul>
|
||||
<SettingMenuSection
|
||||
:title="t('Settings')"
|
||||
@@ -11,7 +11,7 @@
|
||||
{
|
||||
name: RouteName.TIMELINE,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
text: $t('Activity'),
|
||||
text: t('Activity'),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
@@ -20,51 +20,51 @@
|
||||
<o-field>
|
||||
<o-radio v-model="activityType" :native-value="undefined">
|
||||
<TimelineText />
|
||||
{{ $t("All activities") }}</o-radio
|
||||
{{ t("All activities") }}</o-radio
|
||||
>
|
||||
<o-radio v-model="activityType" :native-value="ActivityType.MEMBER">
|
||||
<o-icon icon="account-multiple-plus"></o-icon>
|
||||
{{ $t("Members") }}</o-radio
|
||||
{{ t("Members") }}</o-radio
|
||||
>
|
||||
<o-radio v-model="activityType" :native-value="ActivityType.GROUP">
|
||||
<o-icon icon="cog"></o-icon>
|
||||
{{ $t("Settings") }}</o-radio
|
||||
{{ t("Settings") }}</o-radio
|
||||
>
|
||||
<o-radio v-model="activityType" :native-value="ActivityType.EVENT">
|
||||
<o-icon icon="calendar"></o-icon>
|
||||
{{ $t("Events") }}</o-radio
|
||||
{{ t("Events") }}</o-radio
|
||||
>
|
||||
<o-radio v-model="activityType" :native-value="ActivityType.POST">
|
||||
<o-icon icon="bullhorn"></o-icon>
|
||||
{{ $t("Posts") }}</o-radio
|
||||
{{ t("Posts") }}</o-radio
|
||||
>
|
||||
<o-radio v-model="activityType" :native-value="ActivityType.DISCUSSION">
|
||||
<o-icon icon="chat"></o-icon>
|
||||
{{ $t("Discussions") }}</o-radio
|
||||
{{ t("Discussions") }}</o-radio
|
||||
>
|
||||
<o-radio v-model="activityType" :native-value="ActivityType.RESOURCE">
|
||||
<o-icon icon="link"></o-icon>
|
||||
{{ $t("Resources") }}</o-radio
|
||||
{{ t("Resources") }}</o-radio
|
||||
>
|
||||
</o-field>
|
||||
<o-field>
|
||||
<o-radio v-model="activityAuthor" :native-value="undefined">
|
||||
<TimelineText />
|
||||
{{ $t("All activities") }}</o-radio
|
||||
{{ t("All activities") }}</o-radio
|
||||
>
|
||||
<o-radio
|
||||
v-model="activityAuthor"
|
||||
:native-value="ActivityAuthorFilter.SELF"
|
||||
>
|
||||
<o-icon icon="account"></o-icon>
|
||||
{{ $t("From yourself") }}</o-radio
|
||||
{{ t("From yourself") }}</o-radio
|
||||
>
|
||||
<o-radio
|
||||
v-model="activityAuthor"
|
||||
:native-value="ActivityAuthorFilter.BY"
|
||||
>
|
||||
<o-icon icon="account-multiple"></o-icon>
|
||||
{{ $t("By others") }}</o-radio
|
||||
{{ t("By others") }}</o-radio
|
||||
>
|
||||
</o-field>
|
||||
<transition-group name="timeline-list" tag="div">
|
||||
@@ -78,25 +78,20 @@
|
||||
width="300px"
|
||||
height="48px"
|
||||
/>
|
||||
<h2 class="is-size-3 has-text-weight-bold" v-else-if="isToday(date)">
|
||||
<h2 v-else-if="isToday(date)">
|
||||
<span v-tooltip="formatDateString(date)">
|
||||
{{ $t("Today") }}
|
||||
{{ t("Today") }}
|
||||
</span>
|
||||
</h2>
|
||||
<h2
|
||||
class="is-size-3 has-text-weight-bold"
|
||||
v-else-if="isYesterday(date)"
|
||||
>
|
||||
<span v-tooltip="formatDateString(date)">{{
|
||||
$t("Yesterday")
|
||||
}}</span>
|
||||
<h2 v-else-if="isYesterday(date)">
|
||||
<span v-tooltip="formatDateString(date)">{{ t("Yesterday") }}</span>
|
||||
</h2>
|
||||
<h2 v-else class="is-size-3 has-text-weight-bold">
|
||||
<h2 v-else>
|
||||
{{ formatDateString(date) }}
|
||||
</h2>
|
||||
<ul>
|
||||
<li v-for="activityItem in activityItems" :key="activityItem.id">
|
||||
<skeleton-activity-item v-if="activityItem.skeleton" />
|
||||
<skeleton-activity-item v-if="activityItem.type === 'skeleton'" />
|
||||
<component
|
||||
v-else
|
||||
:is="component(activityItem.type)"
|
||||
@@ -113,14 +108,14 @@
|
||||
activity.elements.length >= activity.total
|
||||
"
|
||||
>
|
||||
{{ $t("No more activity to display.") }}
|
||||
{{ t("No more activity to display.") }}
|
||||
</empty-content>
|
||||
<empty-content
|
||||
v-if="!loading && activity.total === 0"
|
||||
icon="timeline-text"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"There is no activity yet. Start doing some things to see activity appear here."
|
||||
)
|
||||
}}
|
||||
@@ -129,7 +124,7 @@
|
||||
<o-button
|
||||
v-if="activity.elements.length < activity.total"
|
||||
@click="loadMore"
|
||||
>{{ $t("Load more activities") }}</o-button
|
||||
>{{ t("Load more activities") }}</o-button
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
@@ -154,14 +149,16 @@ import { formatDateString } from "@/filters/datetime";
|
||||
const PAGINATION_LIMIT = 25;
|
||||
const SKELETON_DAY_ITEMS = 2;
|
||||
const SKELETON_ITEMS_PER_DAY = 5;
|
||||
type IActivitySkeleton = IActivity | { skeleton: string };
|
||||
type IActivitySkeleton =
|
||||
| IActivity
|
||||
| { skeleton: string; id: string; type: "skeleton" };
|
||||
|
||||
enum ActivityAuthorFilter {
|
||||
SELF = "SELF",
|
||||
BY = "BY",
|
||||
}
|
||||
|
||||
type ActivityFilter = ActivityType | ActivityAuthorFilter | null;
|
||||
// type ActivityFilter = ActivityType | ActivityAuthorFilter | null;
|
||||
|
||||
const props = defineProps<{ preferredUsername: string }>();
|
||||
|
||||
@@ -230,12 +227,12 @@ const activity = computed((): Paginate<IActivitySkeleton> => {
|
||||
total: 0,
|
||||
elements: skeletons.value.map((skeleton) => ({
|
||||
skeleton,
|
||||
id: skeleton,
|
||||
type: "skeleton",
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const limit = PAGINATION_LIMIT;
|
||||
|
||||
const component = (type: ActivityType): any | undefined => {
|
||||
switch (type) {
|
||||
case ActivityType.EVENT:
|
||||
@@ -309,11 +306,11 @@ const activities = computed((): Record<string, IActivitySkeleton[]> => {
|
||||
const isIActivity = (object: IActivitySkeleton): object is IActivity => {
|
||||
return !("skeleton" in object);
|
||||
};
|
||||
const getRandomInt = (min: number, max: number): number => {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min) + min);
|
||||
};
|
||||
// const getRandomInt = (min: number, max: number): number => {
|
||||
// min = Math.ceil(min);
|
||||
// max = Math.floor(max);
|
||||
// return Math.floor(Math.random() * (max - min) + min);
|
||||
// };
|
||||
|
||||
const isToday = (dateString: string): boolean => {
|
||||
const now = new Date();
|
||||
@@ -130,7 +130,8 @@
|
||||
<!-- Recent events -->
|
||||
<CloseEvents @doGeoLoc="performGeoLocation()" :userLocation="userLocation" />
|
||||
<CloseGroups :userLocation="userLocation" @doGeoLoc="performGeoLocation()" />
|
||||
<LastEvents v-if="config" :instanceName="config.name" />
|
||||
<OnlineEvents />
|
||||
<LastEvents v-if="instanceName" :instanceName="instanceName" />
|
||||
<!-- Unlogged content section -->
|
||||
<picture v-if="!currentUser?.isLoggedIn">
|
||||
<source
|
||||
@@ -169,30 +170,23 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { EventSortField, ParticipantRole, SortDirection } from "@/types/enums";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { ParticipantRole } from "@/types/enums";
|
||||
import { IParticipant } from "../types/participant.model";
|
||||
import { FETCH_EVENTS } from "../graphql/event";
|
||||
import EventParticipationCard from "../components/Event/EventParticipationCard.vue";
|
||||
import MultiCard from "../components/Event/MultiCard.vue";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../graphql/actor";
|
||||
import { IPerson, displayName } from "../types/actor";
|
||||
import {
|
||||
ICurrentUser,
|
||||
IUser,
|
||||
IUserSettings,
|
||||
} from "../types/current-user.model";
|
||||
import { ICurrentUser, IUser } from "../types/current-user.model";
|
||||
import { CURRENT_USER_CLIENT } from "../graphql/user";
|
||||
import { HOME_USER_QUERIES } from "../graphql/home";
|
||||
import RouteName from "../router/name";
|
||||
import { IEvent } from "../types/event.model";
|
||||
import { CONFIG } from "../graphql/config";
|
||||
import { IConfig } from "../types/config.model";
|
||||
// import { IFollowedGroupEvent } from "../types/followedGroupEvent.model";
|
||||
import CloseEvents from "@/components/Local/CloseEvents.vue";
|
||||
import CloseGroups from "@/components/Local/CloseGroups.vue";
|
||||
import LastEvents from "@/components/Local/LastEvents.vue";
|
||||
import { computed, onMounted, reactive, watch } from "vue";
|
||||
import OnlineEvents from "@/components/Local/OnlineEvents.vue";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useRouter } from "vue-router";
|
||||
import { REVERSE_GEOCODE } from "@/graphql/address";
|
||||
@@ -208,17 +202,18 @@ import UnloggedIntroduction from "@/components/Home/UnloggedIntroduction.vue";
|
||||
import SearchFields from "@/components/Home/SearchFields.vue";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { geoHashToCoords } from "@/utils/location";
|
||||
import { useServerProvidedLocation } from "@/composition/apollo/config";
|
||||
import { ABOUT } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
|
||||
const { result: resultEvents } = useQuery<{ events: Paginate<IEvent> }>(
|
||||
FETCH_EVENTS,
|
||||
{
|
||||
orderBy: EventSortField.INSERTED_AT,
|
||||
direction: SortDirection.DESC,
|
||||
}
|
||||
);
|
||||
const events = computed(
|
||||
() => resultEvents.value?.events || { total: 0, elements: [] }
|
||||
);
|
||||
const { result: aboutConfigResult } = useQuery<{
|
||||
config: Pick<
|
||||
IConfig,
|
||||
"name" | "description" | "slogan" | "registrationsOpen"
|
||||
>;
|
||||
}>(ABOUT);
|
||||
|
||||
const config = computed(() => aboutConfigResult.value?.config);
|
||||
|
||||
const { result: currentActorResult } = useQuery<{ currentActor: IPerson }>(
|
||||
CURRENT_ACTOR_CLIENT
|
||||
@@ -233,9 +228,7 @@ const { result: currentUserResult } = useQuery<{
|
||||
|
||||
const currentUser = computed(() => currentUserResult.value?.currentUser);
|
||||
|
||||
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
|
||||
|
||||
const config = computed<IConfig | undefined>(() => configResult.value?.config);
|
||||
const instanceName = computed(() => config.value?.name);
|
||||
|
||||
const { result: userResult } = useQuery<{ loggedUser: IUser }>(
|
||||
HOME_USER_QUERIES,
|
||||
@@ -254,14 +247,8 @@ const currentUserParticipations = computed(
|
||||
() => loggedUser.value?.participations.elements
|
||||
);
|
||||
|
||||
const instanceName = computed((): string | undefined => config.value?.name);
|
||||
const welcomeBack = computed<boolean>(
|
||||
() => window.localStorage.getItem("welcome-back") === "yes"
|
||||
);
|
||||
|
||||
const newRegisteredUser = computed<boolean>(
|
||||
() => window.localStorage.getItem("new-registered-user") === "yes"
|
||||
);
|
||||
const location = ref(null);
|
||||
const search = ref("");
|
||||
|
||||
const isToday = (date: string): boolean => {
|
||||
return new Date(date).toDateString() === new Date().toDateString();
|
||||
@@ -338,10 +325,6 @@ const goingToEvents = computed<Map<string, Map<string, IParticipant>>>(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const loggedUserSettings = computed<IUserSettings | undefined>(() => {
|
||||
return loggedUser.value?.settings;
|
||||
});
|
||||
|
||||
const canShowMyUpcomingEvents = computed<boolean>(() => {
|
||||
return currentActor.value?.id != undefined && goingToEvents.value.size > 0;
|
||||
});
|
||||
@@ -362,11 +345,16 @@ const filteredFollowedGroupsEvents = computed<IEvent[]>(() => {
|
||||
.slice(0, 4);
|
||||
});
|
||||
|
||||
const welcomeBack = ref(false);
|
||||
const newRegisteredUser = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
if (window.localStorage.getItem("welcome-back")) {
|
||||
welcomeBack.value = true;
|
||||
window.localStorage.removeItem("welcome-back");
|
||||
}
|
||||
if (window.localStorage.getItem("new-registered-user")) {
|
||||
newRegisteredUser.value = true;
|
||||
window.localStorage.removeItem("new-registered-user");
|
||||
}
|
||||
});
|
||||
@@ -393,7 +381,7 @@ const userSettingsLocationGeoHash = computed(
|
||||
);
|
||||
|
||||
// The location provided by the server
|
||||
const serverLocation = computed(() => config.value?.location);
|
||||
const { location: serverLocation } = useServerProvidedLocation();
|
||||
|
||||
// The coords from the user location or the server provided location
|
||||
const coords = computed(() => {
|
||||
@@ -496,7 +484,8 @@ onReverseGeocodeResult((result) => {
|
||||
|
||||
const fetchAndSaveCurrentLocationName = async ({
|
||||
coords: { latitude, longitude, accuracy },
|
||||
}: GeolocationPosition) => {
|
||||
}: // eslint-disable-next-line no-undef
|
||||
GeolocationPosition) => {
|
||||
reverseGeoCodeInformation.latitude = latitude;
|
||||
reverseGeoCodeInformation.longitude = longitude;
|
||||
reverseGeoCodeInformation.accuracy = accuracy;
|
||||
|
||||
@@ -35,13 +35,12 @@ import RouteName from "../router/name";
|
||||
import { GraphQLError } from "graphql";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed, reactive } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouteQuery } from "vue-use-route-query";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const uri = useRouteQuery("uri", "");
|
||||
@@ -49,9 +48,9 @@ const uri = useRouteQuery("uri", "");
|
||||
const isURI = computed((): boolean => {
|
||||
try {
|
||||
const url = new URL(uri.value);
|
||||
return !(url instanceof URL);
|
||||
return url instanceof URL;
|
||||
} catch (e) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -65,7 +64,7 @@ const { onResult, onError, loading } = useQuery<{
|
||||
uri: uri.value,
|
||||
}),
|
||||
() => ({
|
||||
enabled: isURI.value !== false,
|
||||
enabled: isURI.value === true,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,526 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<breadcrumbs-nav
|
||||
v-if="report"
|
||||
:links="[
|
||||
{
|
||||
name: RouteName.MODERATION,
|
||||
text: t('Moderation'),
|
||||
},
|
||||
{
|
||||
name: RouteName.REPORTS,
|
||||
text: t('Reports'),
|
||||
},
|
||||
{
|
||||
name: RouteName.REPORT,
|
||||
params: { id: report.id },
|
||||
text: t('Report #{reportNumber}', { reportNumber: report.id }),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<section>
|
||||
<o-notification
|
||||
title="Error"
|
||||
variant="danger"
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
>
|
||||
{{ error }}
|
||||
</o-notification>
|
||||
<div class="container mx-auto" v-if="report">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<o-button
|
||||
v-if="report.status !== ReportStatusEnum.RESOLVED"
|
||||
@click="updateReport(ReportStatusEnum.RESOLVED)"
|
||||
variant="primary"
|
||||
>{{ t("Mark as resolved") }}</o-button
|
||||
>
|
||||
<o-button
|
||||
v-if="report.status !== ReportStatusEnum.OPEN"
|
||||
@click="updateReport(ReportStatusEnum.OPEN)"
|
||||
variant="success"
|
||||
>{{ t("Reopen") }}</o-button
|
||||
>
|
||||
<o-button
|
||||
v-if="report.status !== ReportStatusEnum.CLOSED"
|
||||
@click="updateReport(ReportStatusEnum.CLOSED)"
|
||||
variant="danger"
|
||||
>{{ t("Close") }}</o-button
|
||||
>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<table class="table w-full">
|
||||
<tbody>
|
||||
<tr v-if="report.reported.__typename === 'Group'">
|
||||
<td>{{ t("Reported group") }}</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_GROUP_PROFILE,
|
||||
params: { id: report.reported.id },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="report.reported.avatar"
|
||||
class="image"
|
||||
:src="report.reported.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
{{ displayNameAndUsername(report.reported) }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
{{ t("Reported identity") }}
|
||||
</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_PROFILE,
|
||||
params: { id: report.reported.id },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="report.reported.avatar"
|
||||
class="image"
|
||||
:src="report.reported.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
{{ displayNameAndUsername(report.reported) }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ t("Reported by") }}</td>
|
||||
<td v-if="report.reporter.type === ActorType.APPLICATION">
|
||||
{{ report.reporter.domain }}
|
||||
</td>
|
||||
<td v-else>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_PROFILE,
|
||||
params: { id: report.reporter.id },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="report.reporter.avatar"
|
||||
class="image"
|
||||
:src="report.reporter.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
{{ displayNameAndUsername(report.reporter) }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ t("Reported") }}</td>
|
||||
<td>{{ formatDateTimeString(report.insertedAt) }}</td>
|
||||
</tr>
|
||||
<tr v-if="report.updatedAt !== report.insertedAt">
|
||||
<td>{{ t("Updated") }}</td>
|
||||
<td>{{ formatDateTimeString(report.updatedAt) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ t("Status") }}</td>
|
||||
<td>
|
||||
<span v-if="report.status === ReportStatusEnum.OPEN">{{
|
||||
t("Open")
|
||||
}}</span>
|
||||
<span v-else-if="report.status === ReportStatusEnum.CLOSED">
|
||||
{{ t("Closed") }}
|
||||
</span>
|
||||
<span v-else-if="report.status === ReportStatusEnum.RESOLVED">
|
||||
{{ t("Resolved") }}
|
||||
</span>
|
||||
<span v-else>{{ t("Unknown") }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="report.event && report.comments.length > 0">
|
||||
<td>{{ t("Event") }}</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: report.event.uuid },
|
||||
}"
|
||||
>
|
||||
{{ report.event.title }}
|
||||
</router-link>
|
||||
<span class="is-pulled-right">
|
||||
<!-- <o-button-->
|
||||
<!-- tag="router-link"-->
|
||||
<!-- variant="primary"-->
|
||||
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
|
||||
<!-- icon-left="pencil"-->
|
||||
<!-- size="small">{{ t('Edit') }}</o-button>-->
|
||||
<o-button
|
||||
variant="danger"
|
||||
@click="confirmEventDelete()"
|
||||
icon-left="delete"
|
||||
size="small"
|
||||
>{{ t("Delete") }}</o-button
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<p v-if="report.content" v-html="nl2br(report.content)" />
|
||||
<p v-else>{{ t("No comment") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="" v-if="report.event && report.comments.length === 0">
|
||||
<router-link
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid } }"
|
||||
>
|
||||
<h3 class="title">{{ report.event.title }}</h3>
|
||||
<p v-html="report.event.description" />
|
||||
</router-link>
|
||||
<!-- <o-button-->
|
||||
<!-- tag="router-link"-->
|
||||
<!-- variant="primary"-->
|
||||
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
|
||||
<!-- icon-left="pencil"-->
|
||||
<!-- size="small">{{ t('Edit') }}</o-button>-->
|
||||
<o-button
|
||||
variant="danger"
|
||||
@click="confirmEventDelete()"
|
||||
icon-left="delete"
|
||||
size="small"
|
||||
>{{ t("Delete") }}</o-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="report.comments.length > 0">
|
||||
<ul v-for="comment in report.comments" :key="comment.id">
|
||||
<li>
|
||||
<div class="" v-if="comment">
|
||||
<article class="flex gap-1">
|
||||
<div class="">
|
||||
<figure
|
||||
class=""
|
||||
v-if="comment.actor && comment.actor.avatar"
|
||||
>
|
||||
<img
|
||||
:src="comment.actor.avatar.url"
|
||||
alt=""
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
</figure>
|
||||
<AccountCircle :size="48" v-else />
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="prose dark:prose-invert">
|
||||
<span v-if="comment.actor">
|
||||
<strong>{{ comment.actor.name }}</strong>
|
||||
<small>@{{ comment.actor.preferredUsername }}</small>
|
||||
</span>
|
||||
<span v-else>{{ t("Unknown actor") }}</span>
|
||||
<br />
|
||||
<p v-html="comment.text" />
|
||||
</div>
|
||||
<o-button
|
||||
variant="danger"
|
||||
@click="confirmCommentDelete(comment)"
|
||||
icon-left="delete"
|
||||
size="small"
|
||||
>{{ t("Delete") }}</o-button
|
||||
>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 v-if="report.notes.length > 0">{{ t("Notes") }}</h2>
|
||||
<div
|
||||
class="box note"
|
||||
v-for="note in report.notes"
|
||||
:id="`note-${note.id}`"
|
||||
:key="note.id"
|
||||
>
|
||||
<p>{{ note.content }}</p>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_PROFILE,
|
||||
params: { id: note.moderator.id },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="rounded-full"
|
||||
:src="note.moderator.avatar.url"
|
||||
v-if="note.moderator.avatar"
|
||||
/>
|
||||
@{{ note.moderator.preferredUsername }}
|
||||
</router-link>
|
||||
<br />
|
||||
<small>
|
||||
<a :href="`#note-${note.id}`" v-if="note.insertedAt">
|
||||
{{ formatDateTimeString(note.insertedAt) }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<form
|
||||
@submit="
|
||||
createReportNoteMutation({
|
||||
reportId: report?.id,
|
||||
content: noteContent,
|
||||
})
|
||||
"
|
||||
>
|
||||
<o-field :label="t('New note')" label-for="newNoteInput">
|
||||
<o-input
|
||||
type="textarea"
|
||||
v-model="noteContent"
|
||||
id="newNoteInput"
|
||||
></o-input>
|
||||
</o-field>
|
||||
<o-button class="mt-2" type="submit">{{ t("Add a note") }}</o-button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { CREATE_REPORT_NOTE, REPORT, UPDATE_REPORT } from "@/graphql/report";
|
||||
import { IReport, IReportNote } from "@/types/report.model";
|
||||
import { displayNameAndUsername } from "@/types/actor";
|
||||
import { DELETE_EVENT } from "@/graphql/event";
|
||||
import uniq from "lodash/uniq";
|
||||
import { nl2br } from "@/utils/html";
|
||||
import { DELETE_COMMENT } from "@/graphql/comment";
|
||||
import { IComment } from "@/types/comment.model";
|
||||
import { ActorType, ReportStatusEnum } from "@/types/enums";
|
||||
import RouteName from "@/router/name";
|
||||
import { GraphQLError } from "graphql";
|
||||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ref, computed, inject } from "vue";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{ reportId: string }>();
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const { result: reportResult, onError: onReportQueryError } = useQuery<{
|
||||
report: IReport;
|
||||
}>(REPORT, () => ({
|
||||
id: props.reportId,
|
||||
}));
|
||||
|
||||
const report = computed(() => reportResult.value?.report);
|
||||
|
||||
onReportQueryError(({ graphQLErrors }) => {
|
||||
errors.value = uniq(
|
||||
graphQLErrors.map(({ message }: GraphQLError) => message)
|
||||
);
|
||||
});
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() => t("Report")),
|
||||
});
|
||||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
const errors = ref<string[]>([]);
|
||||
|
||||
const noteContent = ref("");
|
||||
|
||||
const {
|
||||
mutate: createReportNoteMutation,
|
||||
onDone: createReportNoteMutationDone,
|
||||
onError: createReportNoteMutationError,
|
||||
} = useMutation<{
|
||||
createReportNote: IReportNote;
|
||||
}>(CREATE_REPORT_NOTE, () => ({
|
||||
update: (
|
||||
store: ApolloCache<{ createReportNote: IReportNote }>,
|
||||
{ data }: FetchResult
|
||||
) => {
|
||||
if (data == null) return;
|
||||
const cachedData = store.readQuery<{ report: IReport }>({
|
||||
query: REPORT,
|
||||
variables: { id: report.value.id },
|
||||
});
|
||||
if (cachedData == null) return;
|
||||
const { report } = cachedData;
|
||||
if (report === null) {
|
||||
console.error("Cannot update event notes cache, because of null value.");
|
||||
return;
|
||||
}
|
||||
const note = data.createReportNote;
|
||||
note.moderator = currentActor.value;
|
||||
|
||||
report.notes = report.notes.concat([note]);
|
||||
|
||||
store.writeQuery({
|
||||
query: REPORT,
|
||||
variables: { id: report.value.id },
|
||||
data: { report },
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
createReportNoteMutationDone(() => {
|
||||
noteContent.value = "";
|
||||
});
|
||||
|
||||
createReportNoteMutationError((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const dialog = inject<Dialog>("dialog");
|
||||
|
||||
const confirmEventDelete = (): void => {
|
||||
dialog?.confirm({
|
||||
title: t("Deleting event"),
|
||||
message: t(
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead."
|
||||
),
|
||||
confirmText: t("Delete Event"),
|
||||
type: "danger",
|
||||
hasIcon: true,
|
||||
onConfirm: () => deleteEvent(),
|
||||
});
|
||||
};
|
||||
|
||||
const confirmCommentDelete = (comment: IComment): void => {
|
||||
dialog?.confirm({
|
||||
title: t("Deleting comment"),
|
||||
message: t(
|
||||
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone."
|
||||
),
|
||||
confirmText: t("Delete Comment"),
|
||||
type: "danger",
|
||||
hasIcon: true,
|
||||
onConfirm: () => deleteCommentMutation({ commentId: comment.id }),
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: deleteEventMutation,
|
||||
onDone: deleteEventMutationDone,
|
||||
onError: deleteEventMutationError,
|
||||
} = useMutation<{ deleteEvent: { id: string } }>(DELETE_EVENT);
|
||||
|
||||
deleteEventMutationDone(() => {
|
||||
const eventTitle = report.value?.event?.title;
|
||||
notifier?.success(
|
||||
t("Event {eventTitle} deleted", {
|
||||
eventTitle,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
deleteEventMutationError((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const deleteEvent = async (): Promise<void> => {
|
||||
if (!report.value?.event?.id) return;
|
||||
|
||||
deleteEventMutation({ eventId: report.value.event.id });
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: deleteCommentMutation,
|
||||
onDone: deleteCommentMutationDone,
|
||||
onError: deleteCommentMutationError,
|
||||
} = useMutation<{ deleteComment: { id: string } }>(DELETE_COMMENT);
|
||||
|
||||
deleteCommentMutationDone(() => {
|
||||
notifier?.success(t("Comment deleted") as string);
|
||||
});
|
||||
|
||||
deleteCommentMutationError((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateReportMutation,
|
||||
onDone: onUpdateReportMutation,
|
||||
onError: onUpdateReportError,
|
||||
} = useMutation(UPDATE_REPORT, () => ({
|
||||
update: (
|
||||
store: ApolloCache<{ updateReportStatus: IReport }>,
|
||||
{ data }: FetchResult
|
||||
) => {
|
||||
if (data == null) return;
|
||||
const reportCachedData = store.readQuery<{ report: IReport }>({
|
||||
query: REPORT,
|
||||
variables: { id: report.value.id },
|
||||
});
|
||||
if (reportCachedData == null) return;
|
||||
const { report } = reportCachedData;
|
||||
if (report === null) {
|
||||
console.error("Cannot update event notes cache, because of null value.");
|
||||
return;
|
||||
}
|
||||
const updatedReport = {
|
||||
...report,
|
||||
status: data.updateReportStatus.status,
|
||||
};
|
||||
|
||||
store.writeQuery({
|
||||
query: REPORT,
|
||||
variables: { id: report.value.id },
|
||||
data: { report: updatedReport },
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
onUpdateReportMutation(() => {
|
||||
router.push({ name: RouteName.REPORTS });
|
||||
});
|
||||
|
||||
onUpdateReportError((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const updateReport = async (status: ReportStatusEnum): Promise<void> => {
|
||||
updateReportMutation({
|
||||
reportId: report.value?.id,
|
||||
status,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
tbody td img.image,
|
||||
.note img.image {
|
||||
display: inline;
|
||||
height: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.dialog .modal-card-foot {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.box a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
td > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -4,35 +4,35 @@
|
||||
:links="[
|
||||
{
|
||||
name: RouteName.MODERATION,
|
||||
text: $t('Moderation'),
|
||||
text: t('Moderation'),
|
||||
},
|
||||
{
|
||||
name: RouteName.REPORTS,
|
||||
text: $t('Reports'),
|
||||
text: t('Reports'),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<section>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<o-field :label="$t('Report status')">
|
||||
<o-field :label="t('Report status')">
|
||||
<o-radio v-model="status" :native-value="ReportStatusEnum.OPEN">{{
|
||||
$t("Open")
|
||||
t("Open")
|
||||
}}</o-radio>
|
||||
<o-radio v-model="status" :native-value="ReportStatusEnum.RESOLVED">{{
|
||||
$t("Resolved")
|
||||
t("Resolved")
|
||||
}}</o-radio>
|
||||
<o-radio v-model="status" :native-value="ReportStatusEnum.CLOSED">{{
|
||||
$t("Closed")
|
||||
t("Closed")
|
||||
}}</o-radio>
|
||||
</o-field>
|
||||
<o-field
|
||||
:label="$t('Domain')"
|
||||
:label="t('Domain')"
|
||||
label-for="domain-filter"
|
||||
class="flex-auto"
|
||||
>
|
||||
<o-input
|
||||
id="domain-filter"
|
||||
:placeholder="$t('mobilizon-instance.tld')"
|
||||
:placeholder="t('mobilizon-instance.tld')"
|
||||
:value="filterDomain"
|
||||
@input="debouncedUpdateDomainFilter"
|
||||
/>
|
||||
@@ -53,21 +53,21 @@
|
||||
inline
|
||||
v-if="status === ReportStatusEnum.OPEN"
|
||||
>
|
||||
{{ $t("No open reports yet") }}
|
||||
{{ t("No open reports yet") }}
|
||||
</empty-content>
|
||||
<empty-content
|
||||
icon="chat-alert"
|
||||
inline
|
||||
v-if="status === ReportStatusEnum.RESOLVED"
|
||||
>
|
||||
{{ $t("No resolved reports yet") }}
|
||||
{{ t("No resolved reports yet") }}
|
||||
</empty-content>
|
||||
<empty-content
|
||||
icon="chat-alert"
|
||||
inline
|
||||
v-if="status === ReportStatusEnum.CLOSED"
|
||||
>
|
||||
{{ $t("No closed reports yet") }}
|
||||
{{ t("No closed reports yet") }}
|
||||
</empty-content>
|
||||
</div>
|
||||
<o-pagination
|
||||
@@ -75,10 +75,10 @@
|
||||
v-model="page"
|
||||
:simple="true"
|
||||
:per-page="REPORT_PAGE_LIMIT"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
>
|
||||
</o-pagination>
|
||||
</section>
|
||||
@@ -96,7 +96,7 @@ import debounce from "lodash/debounce";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { computed, ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
enumTransformer,
|
||||
integerTransformer,
|
||||
@@ -132,7 +132,7 @@ useHead({
|
||||
title: computed(() => t("Reports")),
|
||||
});
|
||||
|
||||
const filterReports = ref<ReportStatusEnum>(ReportStatusEnum.OPEN);
|
||||
// const filterReports = ref<ReportStatusEnum>(ReportStatusEnum.OPEN);
|
||||
|
||||
const updateDomainFilter = (event: InputEvent) => {
|
||||
filterDomain.value = event.target?.value;
|
||||
543
js/src/views/Moderation/ReportView.vue
Normal file
543
js/src/views/Moderation/ReportView.vue
Normal file
@@ -0,0 +1,543 @@
|
||||
<template>
|
||||
<breadcrumbs-nav
|
||||
v-if="report"
|
||||
:links="[
|
||||
{
|
||||
name: RouteName.MODERATION,
|
||||
text: t('Moderation'),
|
||||
},
|
||||
{
|
||||
name: RouteName.REPORTS,
|
||||
text: t('Reports'),
|
||||
},
|
||||
{
|
||||
name: RouteName.REPORT,
|
||||
params: { id: report.id },
|
||||
text: t('Report #{reportNumber}', { reportNumber: report.id }),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<o-notification
|
||||
title="Error"
|
||||
variant="danger"
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
>
|
||||
{{ error }}
|
||||
</o-notification>
|
||||
<div class="container mx-auto" v-if="report">
|
||||
<div class="flex flex-wrap gap-2 my-2">
|
||||
<o-button
|
||||
v-if="report.status !== ReportStatusEnum.RESOLVED"
|
||||
@click="updateReport(ReportStatusEnum.RESOLVED)"
|
||||
variant="primary"
|
||||
>{{ t("Mark as resolved") }}</o-button
|
||||
>
|
||||
<o-button
|
||||
v-if="report.status !== ReportStatusEnum.OPEN"
|
||||
@click="updateReport(ReportStatusEnum.OPEN)"
|
||||
variant="success"
|
||||
>{{ t("Reopen") }}</o-button
|
||||
>
|
||||
<o-button
|
||||
v-if="report.status !== ReportStatusEnum.CLOSED"
|
||||
@click="updateReport(ReportStatusEnum.CLOSED)"
|
||||
variant="danger"
|
||||
>{{ t("Close") }}</o-button
|
||||
>
|
||||
</div>
|
||||
<section class="w-full">
|
||||
<table class="table w-full">
|
||||
<tbody>
|
||||
<tr v-if="report.reported.type === ActorType.GROUP">
|
||||
<td>{{ t("Reported group") }}</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_GROUP_PROFILE,
|
||||
params: { id: report.reported.id },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="report.reported.avatar"
|
||||
class="image"
|
||||
:src="report.reported.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
{{ displayNameAndUsername(report.reported) }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
{{ t("Reported identity") }}
|
||||
</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_PROFILE,
|
||||
params: { id: report.reported.id },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="report.reported.avatar"
|
||||
class="image"
|
||||
:src="report.reported.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
{{ displayNameAndUsername(report.reported) }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ t("Reported by") }}</td>
|
||||
<td v-if="report.reporter.type === ActorType.APPLICATION">
|
||||
{{ report.reporter.domain }}
|
||||
</td>
|
||||
<td v-else>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_PROFILE,
|
||||
params: { id: report.reporter.id },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="report.reporter.avatar"
|
||||
class="image"
|
||||
:src="report.reporter.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
{{ displayNameAndUsername(report.reporter) }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ t("Reported") }}</td>
|
||||
<td>{{ formatDateTimeString(report.insertedAt) }}</td>
|
||||
</tr>
|
||||
<tr v-if="report.updatedAt !== report.insertedAt">
|
||||
<td>{{ t("Updated") }}</td>
|
||||
<td>{{ formatDateTimeString(report.updatedAt) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ t("Status") }}</td>
|
||||
<td>
|
||||
<span v-if="report.status === ReportStatusEnum.OPEN">{{
|
||||
t("Open")
|
||||
}}</span>
|
||||
<span v-else-if="report.status === ReportStatusEnum.CLOSED">
|
||||
{{ t("Closed") }}
|
||||
</span>
|
||||
<span v-else-if="report.status === ReportStatusEnum.RESOLVED">
|
||||
{{ t("Resolved") }}
|
||||
</span>
|
||||
<span v-else>{{ t("Unknown") }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="report.event && report.comments.length > 0">
|
||||
<td>{{ t("Event") }}</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: report.event.uuid },
|
||||
}"
|
||||
>
|
||||
{{ report.event.title }}
|
||||
</router-link>
|
||||
<span>
|
||||
<!-- <o-button-->
|
||||
<!-- tag="router-link"-->
|
||||
<!-- variant="primary"-->
|
||||
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
|
||||
<!-- icon-left="pencil"-->
|
||||
<!-- size="small">{{ t('Edit') }}</o-button>-->
|
||||
<o-button
|
||||
variant="danger"
|
||||
@click="confirmEventDelete()"
|
||||
icon-left="delete"
|
||||
size="small"
|
||||
>{{ t("Delete") }}</o-button
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>{{ t("Report reason") }}</h2>
|
||||
<div class="dark:bg-zinc-700 p-2 rounded my-2">
|
||||
<div class="flex gap-1">
|
||||
<figure class="" v-if="report.reported.avatar">
|
||||
<img
|
||||
alt=""
|
||||
:src="report.reported.avatar.url"
|
||||
class="rounded-full"
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
</figure>
|
||||
<AccountCircle v-else :size="48" />
|
||||
<div class="">
|
||||
<p class="" v-if="report.reported.name">
|
||||
{{ report.reported.name }}
|
||||
</p>
|
||||
<p class="">@{{ usernameWithDomain(report.reported) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="prose dark:prose-invert"
|
||||
v-if="report.content"
|
||||
v-html="nl2br(report.content)"
|
||||
/>
|
||||
<p v-else>{{ t("No comment") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="" v-if="report.event && report.comments.length === 0">
|
||||
<h2>{{ t("Reported content") }}</h2>
|
||||
<EventCard :event="report.event" mode="row" class="my-2" />
|
||||
<!-- <o-button-->
|
||||
<!-- tag="router-link"-->
|
||||
<!-- variant="primary"-->
|
||||
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
|
||||
<!-- icon-left="pencil"-->
|
||||
<!-- size="small">{{ t('Edit') }}</o-button>-->
|
||||
<o-button
|
||||
variant="danger"
|
||||
@click="confirmEventDelete()"
|
||||
icon-left="delete"
|
||||
size="small"
|
||||
>{{ t("Delete") }}</o-button
|
||||
>
|
||||
</section>
|
||||
|
||||
<div v-if="report.comments.length > 0">
|
||||
<ul v-for="comment in report.comments" :key="comment.id">
|
||||
<li>
|
||||
<div class="" v-if="comment">
|
||||
<article class="flex gap-1">
|
||||
<div class="">
|
||||
<figure class="" v-if="comment.actor && comment.actor.avatar">
|
||||
<img
|
||||
:src="comment.actor.avatar.url"
|
||||
alt=""
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
</figure>
|
||||
<AccountCircle :size="48" v-else />
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="prose dark:prose-invert">
|
||||
<span v-if="comment.actor">
|
||||
<strong>{{ comment.actor.name }}</strong>
|
||||
<small>@{{ comment.actor.preferredUsername }}</small>
|
||||
</span>
|
||||
<span v-else>{{ t("Unknown actor") }}</span>
|
||||
<br />
|
||||
<p v-html="comment.text" />
|
||||
</div>
|
||||
<o-button
|
||||
variant="danger"
|
||||
@click="confirmCommentDelete(comment)"
|
||||
icon-left="delete"
|
||||
size="small"
|
||||
>{{ t("Delete") }}</o-button
|
||||
>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>{{ t("Notes") }}</h2>
|
||||
<div
|
||||
class="box note"
|
||||
v-for="note in report.notes"
|
||||
:id="`note-${note.id}`"
|
||||
:key="note.id"
|
||||
>
|
||||
<p>{{ note.content }}</p>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_PROFILE,
|
||||
params: { id: note.moderator.id },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="rounded-full"
|
||||
:src="note.moderator.avatar.url"
|
||||
v-if="note.moderator.avatar"
|
||||
/>
|
||||
@{{ note.moderator.preferredUsername }}
|
||||
</router-link>
|
||||
<br />
|
||||
<small>
|
||||
<a :href="`#note-${note.id}`" v-if="note.insertedAt">
|
||||
{{ formatDateTimeString(note.insertedAt) }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<form
|
||||
@submit="
|
||||
createReportNoteMutation({
|
||||
reportId: report?.id,
|
||||
content: noteContent,
|
||||
})
|
||||
"
|
||||
>
|
||||
<o-field :label="t('New note')" label-for="newNoteInput">
|
||||
<o-input
|
||||
type="textarea"
|
||||
v-model="noteContent"
|
||||
id="newNoteInput"
|
||||
></o-input>
|
||||
</o-field>
|
||||
<o-button class="mt-2" type="submit">{{ t("Add a note") }}</o-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { CREATE_REPORT_NOTE, REPORT, UPDATE_REPORT } from "@/graphql/report";
|
||||
import { IReport, IReportNote } from "@/types/report.model";
|
||||
import { displayNameAndUsername, usernameWithDomain } from "@/types/actor";
|
||||
import { DELETE_EVENT } from "@/graphql/event";
|
||||
import uniq from "lodash/uniq";
|
||||
import { nl2br } from "@/utils/html";
|
||||
import { DELETE_COMMENT } from "@/graphql/comment";
|
||||
import { IComment } from "@/types/comment.model";
|
||||
import { ActorType, ReportStatusEnum } from "@/types/enums";
|
||||
import RouteName from "@/router/name";
|
||||
import { GraphQLError } from "graphql";
|
||||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ref, computed, inject } from "vue";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{ reportId: string }>();
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const { result: reportResult, onError: onReportQueryError } = useQuery<{
|
||||
report: IReport;
|
||||
}>(REPORT, () => ({
|
||||
id: props.reportId,
|
||||
}));
|
||||
|
||||
const report = computed(() => reportResult.value?.report);
|
||||
|
||||
onReportQueryError(({ graphQLErrors }) => {
|
||||
errors.value = uniq(
|
||||
graphQLErrors.map(({ message }: GraphQLError) => message)
|
||||
);
|
||||
});
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() => t("Report")),
|
||||
});
|
||||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
const errors = ref<string[]>([]);
|
||||
|
||||
const noteContent = ref("");
|
||||
|
||||
const {
|
||||
mutate: createReportNoteMutation,
|
||||
onDone: createReportNoteMutationDone,
|
||||
onError: createReportNoteMutationError,
|
||||
} = useMutation<{
|
||||
createReportNote: IReportNote;
|
||||
}>(CREATE_REPORT_NOTE, () => ({
|
||||
update: (
|
||||
store: ApolloCache<{ createReportNote: IReportNote }>,
|
||||
{ data }: FetchResult
|
||||
) => {
|
||||
if (data == null) return;
|
||||
const cachedData = store.readQuery<{ report: IReport }>({
|
||||
query: REPORT,
|
||||
variables: { id: report.value?.id },
|
||||
});
|
||||
if (cachedData == null) return;
|
||||
const { report: cachedReport } = cachedData;
|
||||
if (cachedReport === null) {
|
||||
console.error("Cannot update event notes cache, because of null value.");
|
||||
return;
|
||||
}
|
||||
const note = data.createReportNote;
|
||||
note.moderator = currentActor.value;
|
||||
|
||||
cachedReport.notes = cachedReport.notes.concat([note]);
|
||||
|
||||
store.writeQuery({
|
||||
query: REPORT,
|
||||
variables: { id: report.value?.id },
|
||||
data: { report },
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
createReportNoteMutationDone(() => {
|
||||
noteContent.value = "";
|
||||
});
|
||||
|
||||
createReportNoteMutationError((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const dialog = inject<Dialog>("dialog");
|
||||
|
||||
const confirmEventDelete = (): void => {
|
||||
dialog?.confirm({
|
||||
title: t("Deleting event"),
|
||||
message: t(
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead."
|
||||
),
|
||||
confirmText: t("Delete Event"),
|
||||
variant: "danger",
|
||||
hasIcon: true,
|
||||
onConfirm: () => deleteEvent(),
|
||||
});
|
||||
};
|
||||
|
||||
const confirmCommentDelete = (comment: IComment): void => {
|
||||
dialog?.confirm({
|
||||
title: t("Deleting comment"),
|
||||
message: t(
|
||||
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone."
|
||||
),
|
||||
confirmText: t("Delete Comment"),
|
||||
variant: "danger",
|
||||
hasIcon: true,
|
||||
onConfirm: () => deleteCommentMutation({ commentId: comment.id }),
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: deleteEventMutation,
|
||||
onDone: deleteEventMutationDone,
|
||||
onError: deleteEventMutationError,
|
||||
} = useMutation<{ deleteEvent: { id: string } }>(DELETE_EVENT);
|
||||
|
||||
deleteEventMutationDone(() => {
|
||||
const eventTitle = report.value?.event?.title;
|
||||
notifier?.success(
|
||||
t("Event {eventTitle} deleted", {
|
||||
eventTitle,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
deleteEventMutationError((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const deleteEvent = async (): Promise<void> => {
|
||||
if (!report.value?.event?.id) return;
|
||||
|
||||
deleteEventMutation({ eventId: report.value.event.id });
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: deleteCommentMutation,
|
||||
onDone: deleteCommentMutationDone,
|
||||
onError: deleteCommentMutationError,
|
||||
} = useMutation<{ deleteComment: { id: string } }>(DELETE_COMMENT);
|
||||
|
||||
deleteCommentMutationDone(() => {
|
||||
notifier?.success(t("Comment deleted") as string);
|
||||
});
|
||||
|
||||
deleteCommentMutationError((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateReportMutation,
|
||||
onDone: onUpdateReportMutation,
|
||||
onError: onUpdateReportError,
|
||||
} = useMutation(UPDATE_REPORT, () => ({
|
||||
update: (
|
||||
store: ApolloCache<{ updateReportStatus: IReport }>,
|
||||
{ data }: FetchResult
|
||||
) => {
|
||||
if (data == null) return;
|
||||
const reportCachedData = store.readQuery<{ report: IReport }>({
|
||||
query: REPORT,
|
||||
variables: { id: report.value?.id },
|
||||
});
|
||||
if (reportCachedData == null) return;
|
||||
const { report: cachedReport } = reportCachedData;
|
||||
if (cachedReport === null) {
|
||||
console.error("Cannot update event notes cache, because of null value.");
|
||||
return;
|
||||
}
|
||||
const updatedReport = {
|
||||
...cachedReport,
|
||||
status: data.updateReportStatus.status,
|
||||
};
|
||||
|
||||
store.writeQuery({
|
||||
query: REPORT,
|
||||
variables: { id: report.value?.id },
|
||||
data: { report: updatedReport },
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
onUpdateReportMutation(() => {
|
||||
router.push({ name: RouteName.REPORTS });
|
||||
});
|
||||
|
||||
onUpdateReportError((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const updateReport = async (status: ReportStatusEnum): Promise<void> => {
|
||||
updateReportMutation({
|
||||
reportId: report.value?.id,
|
||||
status,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
tbody td img.image,
|
||||
.note img.image {
|
||||
display: inline;
|
||||
height: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.dialog .modal-card-foot {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.box a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
td > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -83,12 +83,13 @@
|
||||
<div class="container mx-auto">
|
||||
<div class="navbar-menu flex flex-wrap py-2">
|
||||
<div class="flex flex-wrap justify-end ml-auto gap-1">
|
||||
<o-button type="is-text" @click="$router.go(-1)">{{
|
||||
<o-button variant="text" @click="$router.go(-1)">{{
|
||||
$t("Cancel")
|
||||
}}</o-button>
|
||||
<o-button
|
||||
v-if="isUpdate"
|
||||
type="is-danger is-outlined"
|
||||
variant="danger"
|
||||
outlined
|
||||
@click="openDeletePostModal"
|
||||
>{{ $t("Delete post") }}</o-button
|
||||
>
|
||||
@@ -140,7 +141,7 @@ import {
|
||||
} from "../../graphql/post";
|
||||
|
||||
import { IPost } from "../../types/post.model";
|
||||
import Editor from "../../components/Editor.vue";
|
||||
import Editor from "../../components/TextEditor.vue";
|
||||
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
||||
import TagInput from "../../components/Event/TagInput.vue";
|
||||
import RouteName from "../../router/name";
|
||||
@@ -191,11 +192,13 @@ onMounted(async () => {
|
||||
pictureFile.value = await buildFileFromIMedia(post.value?.picture);
|
||||
});
|
||||
|
||||
watch(post, async (newPost: IPost, oldPost: IPost) => {
|
||||
if (oldPost?.picture !== newPost.picture) {
|
||||
watch(post, async (newPost: IPost | undefined, oldPost: IPost | undefined) => {
|
||||
if (oldPost?.picture !== newPost?.picture) {
|
||||
pictureFile.value = await buildFileFromIMedia(post.value?.picture);
|
||||
}
|
||||
editablePost.value = { ...post.value };
|
||||
if (newPost) {
|
||||
editablePost.value = { ...newPost };
|
||||
}
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
@@ -370,7 +373,7 @@ const dialog = inject<Dialog>("dialog");
|
||||
|
||||
const openDeletePostModal = async (): Promise<void> => {
|
||||
dialog?.confirm({
|
||||
type: "danger",
|
||||
variant: "danger",
|
||||
title: t("Delete post"),
|
||||
message: t(
|
||||
"Are you sure you want to delete this post? This action cannot be reverted."
|
||||
@@ -382,11 +385,8 @@ const openDeletePostModal = async (): Promise<void> => {
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: deletePost,
|
||||
onDone: onDeletePostDone,
|
||||
onError: onDeletePostError,
|
||||
} = useMutation(DELETE_POST);
|
||||
const { mutate: deletePost, onDone: onDeletePostDone } =
|
||||
useMutation(DELETE_POST);
|
||||
|
||||
onDeletePostDone(({ data }) => {
|
||||
if (data && post.value?.attributedTo) {
|
||||
@@ -405,27 +405,3 @@ useHead({
|
||||
),
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/_mixins" as *;
|
||||
|
||||
form {
|
||||
nav.navbar {
|
||||
// min-height: 2rem !important;
|
||||
|
||||
.container {
|
||||
// min-height: 2rem;
|
||||
|
||||
.navbar-menu,
|
||||
.navbar-end {
|
||||
// display: flex !important;
|
||||
// flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.navbar-end {
|
||||
// justify-content: flex-end;
|
||||
// @include margin-left(auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,12 +11,12 @@
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="inline">
|
||||
<b-tag
|
||||
<tag
|
||||
class="mr-2"
|
||||
variant="warning"
|
||||
size="is-medium"
|
||||
size="medium"
|
||||
v-if="post.draft"
|
||||
>{{ $t("Draft") }}</b-tag
|
||||
>{{ $t("Draft") }}</tag
|
||||
>
|
||||
<h1 class="inline" :lang="post.language">
|
||||
{{ post.title }}
|
||||
@@ -191,7 +191,7 @@
|
||||
has-modal-card
|
||||
ref="reportModal"
|
||||
>
|
||||
<report-modal
|
||||
<ReportModal
|
||||
:on-confirm="reportPost"
|
||||
:title="$t('Report this post')"
|
||||
:outside-domain="groupDomain"
|
||||
@@ -224,7 +224,6 @@ import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
|
||||
import ActorInline from "@/components/Account/ActorInline.vue";
|
||||
import { formatDistanceToNowStrict, Locale } from "date-fns";
|
||||
import SharePostModal from "@/components/Post/SharePostModal.vue";
|
||||
import { CREATE_REPORT } from "@/graphql/report";
|
||||
import ReportModal from "@/components/Report/ReportModal.vue";
|
||||
import { useAnonymousReportsConfig } from "@/composition/apollo/config";
|
||||
import {
|
||||
@@ -283,13 +282,6 @@ const isShareModalActive = ref(false);
|
||||
const isReportModalActive = ref(false);
|
||||
const reportModal = ref();
|
||||
|
||||
const isCurrentActorMember = computed((): boolean => {
|
||||
if (!post.value?.attributedTo || !memberships.value) return false;
|
||||
return memberships.value.elements
|
||||
.map(({ parent: { id } }) => id)
|
||||
.includes(post.value?.attributedTo.id);
|
||||
});
|
||||
|
||||
const isInstanceModerator = computed((): boolean => {
|
||||
return (
|
||||
currentUser.value?.role !== undefined &&
|
||||
@@ -313,8 +305,8 @@ const triggerShare = (): void => {
|
||||
title: post.value?.title,
|
||||
url: post.value?.url,
|
||||
})
|
||||
.then(() => console.log("Successful share"))
|
||||
.catch((error: any) => console.log("Error sharing", error));
|
||||
.then(() => console.debug("Successful share"))
|
||||
.catch((error: any) => console.debug("Error sharing", error));
|
||||
} else {
|
||||
isShareModalActive.value = true;
|
||||
// send popup
|
||||
@@ -382,7 +374,7 @@ const dialog = inject<Dialog>("dialog");
|
||||
|
||||
const openDeletePostModal = async (): Promise<void> => {
|
||||
dialog?.confirm({
|
||||
type: "danger",
|
||||
variant: "danger",
|
||||
title: t("Delete post"),
|
||||
message: t(
|
||||
"Are you sure you want to delete this post? This action cannot be reverted."
|
||||
@@ -396,11 +388,8 @@ const openDeletePostModal = async (): Promise<void> => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
mutate: deletePost,
|
||||
onDone: onDeletePostDone,
|
||||
onError: onDeletePostError,
|
||||
} = useMutation(DELETE_POST);
|
||||
const { mutate: deletePost, onDone: onDeletePostDone } =
|
||||
useMutation(DELETE_POST);
|
||||
|
||||
onDeletePostDone(({ data }) => {
|
||||
if (data && post.value?.attributedTo) {
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
<o-dropdown-item aria-role="listitem" @click="createFolderModal">
|
||||
<Folder />
|
||||
{{ $t("New folder") }}
|
||||
{{ t("New folder") }}
|
||||
</o-dropdown-item>
|
||||
<o-dropdown-item aria-role="listitem" @click="createLinkModal">
|
||||
<Link />
|
||||
{{ $t("New link") }}
|
||||
{{ t("New link") }}
|
||||
</o-dropdown-item>
|
||||
<hr
|
||||
role="presentation"
|
||||
@@ -48,21 +48,21 @@
|
||||
:total="resource.children.total"
|
||||
v-model="page"
|
||||
:per-page="RESOURCES_PER_PAGE"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
>
|
||||
</o-pagination>
|
||||
<o-modal
|
||||
v-model:active="renameModal"
|
||||
has-modal-card
|
||||
:close-button-aria-label="$t('Close')"
|
||||
:close-button-aria-label="t('Close')"
|
||||
>
|
||||
<div class="w-full md:w-[640px]">
|
||||
<section>
|
||||
<form @submit.prevent="renameResource">
|
||||
<o-field :label="$t('Title')">
|
||||
<o-field :label="t('Title')">
|
||||
<o-input
|
||||
ref="resourceRenameInput"
|
||||
aria-required="true"
|
||||
@@ -70,9 +70,7 @@
|
||||
/>
|
||||
</o-field>
|
||||
|
||||
<o-button native-type="submit">{{
|
||||
$t("Rename resource")
|
||||
}}</o-button>
|
||||
<o-button native-type="submit">{{ t("Rename resource") }}</o-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
@@ -80,7 +78,7 @@
|
||||
<o-modal
|
||||
v-model:active="moveModal"
|
||||
has-modal-card
|
||||
:close-button-aria-label="$t('Close')"
|
||||
:close-button-aria-label="t('Close')"
|
||||
>
|
||||
<div class="w-full md:w-[640px]">
|
||||
<section>
|
||||
@@ -96,36 +94,41 @@
|
||||
<o-modal
|
||||
v-model:active="createResourceModal"
|
||||
has-modal-card
|
||||
:close-button-aria-label="$t('Close')"
|
||||
:close-button-aria-label="t('Close')"
|
||||
trap-focus
|
||||
>
|
||||
<div class="w-full md:w-[640px]">
|
||||
<section>
|
||||
<o-notification variant="danger" v-if="modalError">
|
||||
{{ modalError }}
|
||||
</o-notification>
|
||||
<form @submit.prevent="createResource">
|
||||
<o-field :label="$t('Title')" label-for="new-resource-title">
|
||||
<o-input
|
||||
ref="modalNewResourceInput"
|
||||
aria-required="true"
|
||||
v-model="newResource.title"
|
||||
id="new-resource-title"
|
||||
/>
|
||||
</o-field>
|
||||
<section class="w-full md:w-[640px]">
|
||||
<o-notification variant="danger" v-if="modalError">
|
||||
{{ modalError }}
|
||||
</o-notification>
|
||||
<form @submit.prevent="createResource">
|
||||
<p v-if="newResource.type !== 'folder'">
|
||||
{{
|
||||
t("The pad will be created on {service}", {
|
||||
service: newResourceHost,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<o-field :label="t('Title')" label-for="new-resource-title">
|
||||
<o-input
|
||||
ref="modalNewResourceInput"
|
||||
aria-required="true"
|
||||
v-model="newResource.title"
|
||||
id="new-resource-title"
|
||||
/>
|
||||
</o-field>
|
||||
|
||||
<o-button native-type="submit">{{
|
||||
createResourceButtonLabel
|
||||
}}</o-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<o-button class="mt-2" native-type="submit">{{
|
||||
createResourceButtonLabel
|
||||
}}</o-button>
|
||||
</form>
|
||||
</section>
|
||||
</o-modal>
|
||||
<o-modal
|
||||
v-model:active="createLinkResourceModal"
|
||||
has-modal-card
|
||||
aria-modal
|
||||
:close-button-aria-label="$t('Close')"
|
||||
:close-button-aria-label="t('Close')"
|
||||
trap-focus
|
||||
:width="640"
|
||||
>
|
||||
@@ -135,7 +138,7 @@
|
||||
{{ modalError }}
|
||||
</o-notification>
|
||||
<form @submit.prevent="createResource">
|
||||
<o-field expanded :label="$t('URL')" label-for="new-resource-url">
|
||||
<o-field expanded :label="t('URL')" label-for="new-resource-url">
|
||||
<o-input
|
||||
id="new-resource-url"
|
||||
type="url"
|
||||
@@ -150,7 +153,7 @@
|
||||
<resource-item :resource="newResource" :preview="true" />
|
||||
</div>
|
||||
|
||||
<o-field :label="$t('Title')" label-for="new-resource-link-title">
|
||||
<o-field :label="t('Title')" label-for="new-resource-link-title">
|
||||
<o-input
|
||||
aria-required="true"
|
||||
v-model="newResource.title"
|
||||
@@ -158,10 +161,7 @@
|
||||
/>
|
||||
</o-field>
|
||||
|
||||
<o-field
|
||||
:label="$t('Description')"
|
||||
label-for="new-resource-summary"
|
||||
>
|
||||
<o-field :label="t('Description')" label-for="new-resource-summary">
|
||||
<o-input
|
||||
type="textarea"
|
||||
v-model="newResource.summary"
|
||||
@@ -170,7 +170,7 @@
|
||||
</o-field>
|
||||
|
||||
<o-button native-type="submit" class="mt-2">{{
|
||||
$t("Create resource")
|
||||
t("Create resource")
|
||||
}}</o-button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -246,8 +246,6 @@ onGetResourceError(({ graphQLErrors }) => {
|
||||
handleErrors(graphQLErrors);
|
||||
});
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const { resourceProviders } = useResourceProviders();
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
@@ -489,7 +487,7 @@ const { mutate: updateResourceMutation } = useMutation<{
|
||||
if (!data || data.updateResource == null || parentPath == null) return;
|
||||
if (!resource.value?.actor) return;
|
||||
|
||||
console.log("Removing ressource from old parent");
|
||||
console.debug("Removing ressource from old parent");
|
||||
const oldParentCachedData = store.readQuery<{ resource: IResource }>({
|
||||
query: GET_RESOURCE,
|
||||
variables: {
|
||||
@@ -525,11 +523,11 @@ const { mutate: updateResourceMutation } = useMutation<{
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log("Finished removing ressource from old parent");
|
||||
console.debug("Finished removing ressource from old parent");
|
||||
|
||||
console.log("Adding resource to new parent");
|
||||
console.debug("Adding resource to new parent");
|
||||
if (!updatedResource.parent || !updatedResource.parent.path) {
|
||||
console.log("No cache found for new parent");
|
||||
console.debug("No cache found for new parent");
|
||||
return;
|
||||
}
|
||||
const newParentCachedData = store.readQuery<{ resource: IResource }>({
|
||||
@@ -562,7 +560,7 @@ const { mutate: updateResourceMutation } = useMutation<{
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log("Finished adding resource to new parent");
|
||||
console.debug("Finished adding resource to new parent");
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -633,12 +631,17 @@ const breadcrumbLinks = computed(() => {
|
||||
return links;
|
||||
});
|
||||
|
||||
const newResourceHost = computed(() => {
|
||||
if (!newResource.resourceUrl) return;
|
||||
return new URL(newResource.resourceUrl).host;
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: computed(() =>
|
||||
isRoot.value
|
||||
? t("Resources")
|
||||
: t("{folder} - Resources", {
|
||||
folder: lastFragment,
|
||||
folder: lastFragment.value,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<template>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<SearchFields
|
||||
class="md:ml-10 mr-2"
|
||||
v-model:search="searchDebounced"
|
||||
v-model:location="location"
|
||||
:locationDefaultText="locationName"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="container mx-auto md:py-3 md:px-4 flex flex-col lg:flex-row gap-x-5 gap-y-1"
|
||||
>
|
||||
<aside
|
||||
class="flex-none lg:block lg:sticky top-8 rounded-md px-2 pt-2 w-full lg:w-80 flex-col justify-between mt-2 lg:pb-10 lg:px-8 overflow-y-auto dark:text-slate-100 bg-white dark:bg-mbz-purple"
|
||||
class="flex-none lg:block lg:sticky top-8 rounded-md w-full lg:w-80 flex-col justify-between mt-2 lg:pb-10 lg:px-8 overflow-y-auto dark:text-slate-100 bg-white dark:bg-mbz-purple"
|
||||
>
|
||||
<o-button
|
||||
@click="toggleFilters"
|
||||
@@ -16,7 +24,7 @@
|
||||
<form
|
||||
@submit.prevent="doNewSearch"
|
||||
:class="{ hidden: filtersPanelOpened }"
|
||||
class="lg:block mt-2"
|
||||
class="lg:block mt-4 px-2"
|
||||
>
|
||||
<p class="sr-only">{{ t("Type") }}</p>
|
||||
<ul
|
||||
@@ -53,6 +61,46 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div
|
||||
class="py-4 border-b border-gray-200 dark:border-gray-500"
|
||||
v-show="globalSearchEnabled"
|
||||
>
|
||||
<fieldset class="flex flex-col">
|
||||
<legend class="sr-only">{{ t("Search target") }}</legend>
|
||||
|
||||
<div>
|
||||
<input
|
||||
id="internalTarget"
|
||||
v-model="searchTarget"
|
||||
type="radio"
|
||||
name="searchTarget"
|
||||
:value="SearchTargets.INTERNAL"
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
for="internalTarget"
|
||||
class="ml-3 font-medium text-gray-900 dark:text-gray-300"
|
||||
>{{ t("In this instance's network") }}</label
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
id="globalTarget"
|
||||
v-model="searchTarget"
|
||||
type="radio"
|
||||
name="searchTarget"
|
||||
:value="SearchTargets.GLOBAL"
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
for="globalTarget"
|
||||
class="ml-3 font-medium text-gray-900 dark:text-gray-300"
|
||||
>{{ t("On the Fediverse") }}</label
|
||||
>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="py-4 border-b border-gray-200 dark:border-gray-500"
|
||||
v-show="contentType !== 'GROUPS'"
|
||||
@@ -374,81 +422,48 @@
|
||||
</form>
|
||||
</aside>
|
||||
<div class="flex-1 px-2">
|
||||
<o-tabs type="boxed" v-if="contentType == ContentType.ALL">
|
||||
<o-tab-item>
|
||||
<template #header>
|
||||
<Calendar />
|
||||
|
||||
<span>
|
||||
{{ t("Events") }}
|
||||
<b-tag rounded>{{ searchEvents?.total }}</b-tag>
|
||||
</span>
|
||||
</template>
|
||||
<div v-if="searchEvents && searchEvents.total > 0">
|
||||
<multi-card class="my-4" :events="searchEvents?.elements" />
|
||||
<div
|
||||
class="pagination"
|
||||
v-if="searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT"
|
||||
>
|
||||
<o-pagination
|
||||
:total="searchEvents.total"
|
||||
v-model:current="eventPage"
|
||||
:per-page="EVENT_PAGE_LIMIT"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
>
|
||||
</o-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<o-notification v-else-if="searchLoading === false" variant="info">
|
||||
<p>{{ t("No events found") }}</p>
|
||||
<p v-if="searchIsUrl && !currentUser?.id">
|
||||
{{
|
||||
t(
|
||||
"Only registered users may fetch remote events from their URL."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</o-notification>
|
||||
</o-tab-item>
|
||||
|
||||
<o-tab-item>
|
||||
<template #header>
|
||||
<AccountMultiple />
|
||||
<span>
|
||||
{{ t("Groups") }} <b-tag rounded>{{ searchGroups?.total }}</b-tag>
|
||||
</span>
|
||||
</template>
|
||||
<o-notification v-if="features && !features.groups" variant="danger">
|
||||
{{ t("Groups are not enabled on this instance.") }}
|
||||
</o-notification>
|
||||
<div v-else-if="searchGroups && searchGroups?.total > 0">
|
||||
<multi-group-card class="my-4" :groups="searchGroups?.elements" />
|
||||
<div class="pagination">
|
||||
<o-pagination
|
||||
:total="searchGroups?.total"
|
||||
v-model:current="groupPage"
|
||||
:per-page="GROUP_PAGE_LIMIT"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
>
|
||||
</o-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<o-notification v-else-if="searchLoading === false" variant="danger">
|
||||
{{ t("No groups found") }}
|
||||
</o-notification>
|
||||
</o-tab-item>
|
||||
</o-tabs>
|
||||
<div v-else-if="contentType === ContentType.EVENTS">
|
||||
<div v-if="searchEvents && searchEvents.total > 0">
|
||||
<multi-card class="my-4" :events="searchEvents?.elements" />
|
||||
<template v-if="contentType === ContentType.ALL">
|
||||
<o-notification v-if="features && !features.groups" variant="danger">
|
||||
{{ t("Groups are not enabled on this instance.") }}
|
||||
</o-notification>
|
||||
<div v-else-if="searchGroups && searchGroups?.total > 0">
|
||||
<GroupCard
|
||||
v-for="group in searchGroups?.elements"
|
||||
:group="group"
|
||||
:key="group.id"
|
||||
:isRemoteGroup="group.__typename === 'GroupResult'"
|
||||
:isLoggedIn="currentUser?.isLoggedIn"
|
||||
mode="row"
|
||||
/>
|
||||
<o-pagination
|
||||
v-show="searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT"
|
||||
v-if="searchGroups && searchGroups?.total > GROUP_PAGE_LIMIT"
|
||||
:total="searchGroups?.total"
|
||||
v-model:current="groupPage"
|
||||
:per-page="GROUP_PAGE_LIMIT"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
>
|
||||
</o-pagination>
|
||||
</div>
|
||||
<o-notification v-else-if="searchLoading === false" variant="danger">
|
||||
{{ t("No groups found") }}
|
||||
</o-notification>
|
||||
<div v-if="searchEvents && searchEvents.total > 0">
|
||||
<event-card
|
||||
mode="row"
|
||||
v-for="event in searchEvents?.elements"
|
||||
:event="event"
|
||||
:key="event.uuid"
|
||||
:options="{
|
||||
isRemoteEvent: event.__typename === 'EventResult',
|
||||
isLoggedIn: currentUser?.isLoggedIn,
|
||||
}"
|
||||
class="my-4"
|
||||
/>
|
||||
<o-pagination
|
||||
v-if="searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT"
|
||||
:total="searchEvents.total"
|
||||
v-model:current="eventPage"
|
||||
:per-page="EVENT_PAGE_LIMIT"
|
||||
@@ -467,13 +482,55 @@
|
||||
}}
|
||||
</p>
|
||||
</o-notification>
|
||||
</div>
|
||||
<div v-else-if="contentType === ContentType.GROUPS">
|
||||
</template>
|
||||
<template v-else-if="contentType === ContentType.EVENTS">
|
||||
<template v-if="searchEvents && searchEvents.total > 0">
|
||||
<event-card
|
||||
mode="row"
|
||||
v-for="event in searchEvents?.elements"
|
||||
:event="event"
|
||||
:key="event.uuid"
|
||||
:options="{
|
||||
isRemoteEvent: event.__typename === 'EventResult',
|
||||
isLoggedIn: currentUser?.isLoggedIn,
|
||||
}"
|
||||
class="my-4"
|
||||
/>
|
||||
<o-pagination
|
||||
v-show="searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT"
|
||||
:total="searchEvents.total"
|
||||
v-model:current="eventPage"
|
||||
:per-page="EVENT_PAGE_LIMIT"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
>
|
||||
</o-pagination>
|
||||
</template>
|
||||
<o-notification v-else-if="searchLoading === false" variant="info">
|
||||
<p>{{ t("No events found") }}</p>
|
||||
<p v-if="searchIsUrl && !currentUser?.id">
|
||||
{{
|
||||
t("Only registered users may fetch remote events from their URL.")
|
||||
}}
|
||||
</p>
|
||||
</o-notification>
|
||||
</template>
|
||||
<template v-else-if="contentType === ContentType.GROUPS">
|
||||
<o-notification v-if="features && !features.groups" variant="danger">
|
||||
{{ t("Groups are not enabled on this instance.") }}
|
||||
</o-notification>
|
||||
<div v-else-if="searchGroups && searchGroups?.total > 0">
|
||||
<multi-group-card class="my-4" :groups="searchGroups?.elements" />
|
||||
|
||||
<template v-else-if="searchGroups && searchGroups?.total > 0">
|
||||
<GroupCard
|
||||
v-for="group in searchGroups?.elements"
|
||||
:group="group"
|
||||
:key="group.id"
|
||||
:isRemoteGroup="group.__typename === 'GroupResult'"
|
||||
:isLoggedIn="currentUser?.isLoggedIn"
|
||||
mode="row"
|
||||
/>
|
||||
<o-pagination
|
||||
v-show="searchGroups && searchGroups?.total > GROUP_PAGE_LIMIT"
|
||||
:total="searchGroups?.total"
|
||||
@@ -485,11 +542,11 @@
|
||||
:aria-current-label="t('Current page')"
|
||||
>
|
||||
</o-pagination>
|
||||
</div>
|
||||
</template>
|
||||
<o-notification v-else-if="searchLoading === false" variant="danger">
|
||||
{{ t("No groups found") }}
|
||||
</o-notification>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -508,18 +565,17 @@ import {
|
||||
startOfMonth,
|
||||
eachWeekendOfInterval,
|
||||
} from "date-fns";
|
||||
import { ContentType, EventStatus } from "@/types/enums";
|
||||
import MultiCard from "../components/Event/MultiCard.vue";
|
||||
import { IEvent } from "../types/event.model";
|
||||
import FullAddressAutoComplete from "../components/Event/FullAddressAutoComplete.vue";
|
||||
import { SEARCH_EVENTS_AND_GROUPS } from "../graphql/search";
|
||||
import { Paginate } from "../types/paginate";
|
||||
import { IGroup } from "../types/actor";
|
||||
import MultiGroupCard from "../components/Group/MultiGroupCard.vue";
|
||||
import { ContentType, EventStatus, SearchTargets } from "@/types/enums";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { SEARCH_EVENTS_AND_GROUPS } from "@/graphql/search";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IGroup } from "@/types/actor";
|
||||
import GroupCard from "@/components/Group/GroupCard.vue";
|
||||
import { CURRENT_USER_CLIENT } from "@/graphql/user";
|
||||
import { ICurrentUser } from "@/types/current-user.model";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { computed, inject, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
floatTransformer,
|
||||
@@ -538,11 +594,35 @@ import type { Locale } from "date-fns";
|
||||
import FilterSection from "@/components/Search/filters/FilterSection.vue";
|
||||
import { listShortDisjunctionFormatter } from "@/utils/listFormat";
|
||||
import langs from "@/i18n/langs.json";
|
||||
import { useEventCategories, useFeatures } from "@/composition/apollo/config";
|
||||
import geohash from "ngeohash";
|
||||
import {
|
||||
useEventCategories,
|
||||
useFeatures,
|
||||
useSearchConfig,
|
||||
} from "@/composition/apollo/config";
|
||||
import { coordsToGeoHash } from "@/utils/location";
|
||||
import SearchFields from "@/components/Home/SearchFields.vue";
|
||||
import { refDebounced } from "@vueuse/core";
|
||||
import { IAddress } from "@/types/address.model";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
|
||||
const search = useRouteQuery("search", "");
|
||||
const searchDebounced = refDebounced(search, 1000);
|
||||
const locationName = useRouteQuery("locationName", null);
|
||||
const location = ref<IAddress | null>(null);
|
||||
|
||||
watch(location, (newLocation) => {
|
||||
console.debug("location change");
|
||||
if (newLocation?.geom) {
|
||||
latitude.value = parseFloat(newLocation?.geom.split(";")[1]);
|
||||
longitude.value = parseFloat(newLocation?.geom.split(";")[0]);
|
||||
locationName.value = newLocation?.description;
|
||||
} else {
|
||||
console.debug("location emptied");
|
||||
latitude.value = undefined;
|
||||
longitude.value = undefined;
|
||||
locationName.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
interface ISearchTimeOption {
|
||||
label: string;
|
||||
@@ -580,6 +660,11 @@ const statusOneOf = useRouteQuery(
|
||||
arrayTransformer
|
||||
);
|
||||
const languageOneOf = useRouteQuery("languageOneOf", [], arrayTransformer);
|
||||
const searchTarget = useRouteQuery(
|
||||
"target",
|
||||
SearchTargets.INTERNAL,
|
||||
enumTransformer(SearchTargets)
|
||||
);
|
||||
|
||||
const EVENT_PAGE_LIMIT = 16;
|
||||
|
||||
@@ -667,8 +752,8 @@ const dateOptions: Record<string, ISearchTimeOption> = {
|
||||
},
|
||||
any: {
|
||||
label: t("Any day") as string,
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
start: new Date().toISOString(),
|
||||
end: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -688,9 +773,9 @@ const end = computed((): string | undefined | null => {
|
||||
|
||||
const searchIsUrl = computed((): boolean => {
|
||||
let url;
|
||||
if (!search.value) return false;
|
||||
if (!searchDebounced.value) return false;
|
||||
try {
|
||||
url = new URL(search.value);
|
||||
url = new URL(searchDebounced.value);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
@@ -820,21 +905,48 @@ const geoHashLocation = computed(() =>
|
||||
|
||||
const radius = computed(() => Number.parseInt(distance.value.slice(0, -3)));
|
||||
|
||||
const { searchConfig, onResult: onSearchConfigResult } = useSearchConfig();
|
||||
|
||||
onSearchConfigResult(({ data }) =>
|
||||
handleSearchConfigChanged(data?.config?.search)
|
||||
);
|
||||
|
||||
const handleSearchConfigChanged = (
|
||||
searchConfigChanged: IConfig["search"] | undefined
|
||||
) => {
|
||||
if (
|
||||
searchConfigChanged?.global?.isEnabled &&
|
||||
searchConfigChanged?.global?.isDefault
|
||||
) {
|
||||
searchTarget.value = SearchTargets.GLOBAL;
|
||||
}
|
||||
};
|
||||
|
||||
watch(searchConfig, (newSearchConfig) =>
|
||||
handleSearchConfigChanged(newSearchConfig)
|
||||
);
|
||||
|
||||
const globalSearchEnabled = computed(
|
||||
() => searchConfig.value?.global?.isEnabled
|
||||
);
|
||||
|
||||
const { result: searchElementsResult, loading: searchLoading } = useQuery<{
|
||||
searchEvents: Paginate<IEvent>;
|
||||
searchGroups: Paginate<IGroup>;
|
||||
}>(SEARCH_EVENTS_AND_GROUPS, () => ({
|
||||
term: search.value,
|
||||
term: searchDebounced.value,
|
||||
tags: props.tag,
|
||||
location: geoHashLocation.value,
|
||||
beginsOn: start.value,
|
||||
endsOn: end.value,
|
||||
radius: radius.value,
|
||||
radius: geoHashLocation.value ? radius.value : undefined,
|
||||
eventPage: eventPage.value,
|
||||
groupPage: groupPage.value,
|
||||
limit: EVENT_PAGE_LIMIT,
|
||||
type: isOnline.value ? "ONLINE" : "IN_PERSON",
|
||||
type: isOnline.value ? "ONLINE" : undefined,
|
||||
categoryOneOf: categoryOneOf.value,
|
||||
statusOneOf: statusOneOf.value,
|
||||
languageOneOf: languageOneOf.value,
|
||||
searchTarget: searchTarget.value,
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
</o-tooltip>
|
||||
<o-button
|
||||
icon-left="refresh"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
@click="openRegenerateFeedTokensConfirmation"
|
||||
@keyup.enter="openRegenerateFeedTokensConfirmation"
|
||||
>{{ $t("Regenerate new links") }}</o-button
|
||||
@@ -291,7 +291,7 @@
|
||||
<div v-else>
|
||||
<o-button
|
||||
icon-left="refresh"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
@click="generateFeedTokens"
|
||||
@keyup.enter="generateFeedTokens"
|
||||
>{{ $t("Create new links") }}</o-button
|
||||
@@ -739,7 +739,7 @@ const {
|
||||
} = useMutation(UNREGISTER_PUSH_MUTATION);
|
||||
|
||||
onUnregisterPushMutationDone(({ data }) => {
|
||||
console.log(data);
|
||||
console.debug(data);
|
||||
subscribed.value = false;
|
||||
});
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ const locale = computed({
|
||||
locale: newLocale,
|
||||
});
|
||||
saveLocaleData(newLocale);
|
||||
console.log("changing locale", i18nLocale, newLocale);
|
||||
console.debug("changing locale", i18nLocale, newLocale);
|
||||
i18nLocale.value = newLocale;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<router-link
|
||||
:to="{ name: RouteName.TODO_LIST, params: { id: todoList.id } }"
|
||||
>
|
||||
<h3 class="is-size-3">
|
||||
<h3>
|
||||
{{
|
||||
$t(
|
||||
"{title} ({count} todos)",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
name: RouteName.SEND_PASSWORD_RESET,
|
||||
params: { email: credentials.email },
|
||||
}"
|
||||
>{{ t("Forgot your password ?") }}</o-button
|
||||
>{{ t("Forgot your password?") }}</o-button
|
||||
>
|
||||
<o-button
|
||||
tag="router-link"
|
||||
@@ -145,6 +145,7 @@ import AuthProviders from "@/components/User/AuthProviders.vue";
|
||||
import RouteName from "@/router/name";
|
||||
import { LoginError, LoginErrorCode } from "@/types/enums";
|
||||
import { useCurrentUserClient } from "@/composition/apollo/user";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -198,7 +199,9 @@ onLoginMutationDone(async (result) => {
|
||||
router.push(redirect.value);
|
||||
return;
|
||||
}
|
||||
console.debug("No redirect, going to homepage");
|
||||
if (window.localStorage) {
|
||||
console.debug("Has localstorage, setting welcome back");
|
||||
window.localStorage.setItem("welcome-back", "yes");
|
||||
}
|
||||
router.replace({ name: RouteName.HOME });
|
||||
@@ -235,7 +238,6 @@ const { onDone: onCurrentUserMutationDone, mutate: updateCurrentUserMutation } =
|
||||
useMutation(UPDATE_CURRENT_USER_CLIENT);
|
||||
|
||||
onCurrentUserMutationDone(async () => {
|
||||
console.debug("saved current user client, now for actor client");
|
||||
try {
|
||||
await initializeCurrentActor();
|
||||
} catch (err: any) {
|
||||
@@ -252,7 +254,6 @@ onCurrentUserMutationDone(async () => {
|
||||
});
|
||||
|
||||
const setupClientUserAndActors = async (login: ILogin): Promise<void> => {
|
||||
console.debug("setuping client user and actors after login", login);
|
||||
updateCurrentUserMutation({
|
||||
id: login.user.id,
|
||||
email: credentials.email,
|
||||
@@ -276,7 +277,7 @@ const caseWarningText = computed<string | undefined>(() => {
|
||||
|
||||
const caseWarningType = computed<string | undefined>(() => {
|
||||
if (hasCaseWarning.value) {
|
||||
return "is-warning";
|
||||
return "warning";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
@@ -306,4 +307,8 @@ onMounted(() => {
|
||||
router.push("/");
|
||||
}
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: computed(() => t("Login")),
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -209,14 +209,14 @@ import RouteName from "../../router/name";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import AuthProviders from "../../components/User/AuthProviders.vue";
|
||||
import { AbsintheGraphQLError } from "../../types/apollo";
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { AbsintheGraphQLErrors } from "@/types/errors.model";
|
||||
|
||||
type errorType = "is-danger" | "is-warning";
|
||||
type errorType = "danger" | "warning";
|
||||
type errorMessage = { type: errorType; message: string };
|
||||
type credentialsType = { email: string; password: string; locale: string };
|
||||
|
||||
@@ -271,24 +271,25 @@ onDone(() => {
|
||||
});
|
||||
|
||||
onError((error) => {
|
||||
// @ts-ignore
|
||||
error.graphQLErrors.forEach(({ field, message }: AbsintheGraphQLError) => {
|
||||
switch (field) {
|
||||
case "email":
|
||||
emailErrors.value.push({
|
||||
type: "is-danger" as errorType,
|
||||
message: message[0] as string,
|
||||
});
|
||||
break;
|
||||
case "password":
|
||||
passwordErrors.value.push({
|
||||
type: "is-danger" as errorType,
|
||||
message: message[0] as string,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
(error.graphQLErrors as AbsintheGraphQLErrors).forEach(
|
||||
({ field, message }) => {
|
||||
switch (field) {
|
||||
case "email":
|
||||
emailErrors.value.push({
|
||||
type: "danger" as errorType,
|
||||
message: message[0] as string,
|
||||
});
|
||||
break;
|
||||
case "password":
|
||||
passwordErrors.value.push({
|
||||
type: "danger" as errorType,
|
||||
message: message[0] as string,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
sendingForm.value = false;
|
||||
});
|
||||
|
||||
@@ -310,10 +311,10 @@ const submit = async (): Promise<void> => {
|
||||
watch(credentials, () => {
|
||||
if (credentials.email !== credentials.email.toLowerCase()) {
|
||||
const error = {
|
||||
type: "is-warning" as errorType,
|
||||
type: "warning" as errorType,
|
||||
message: t(
|
||||
"Emails usually don't contain capitals, make sure you haven't made a typo."
|
||||
) as string,
|
||||
),
|
||||
};
|
||||
emailErrors.value = [error];
|
||||
}
|
||||
@@ -322,9 +323,9 @@ watch(credentials, () => {
|
||||
const maxErrorType = (errors: errorMessage[]): errorType | undefined => {
|
||||
if (!errors || errors.length === 0) return undefined;
|
||||
return errors.reduce<errorType>((acc, error) => {
|
||||
if (error.type === "is-danger" || acc === "is-danger") return "is-danger";
|
||||
return "is-warning";
|
||||
}, "is-warning");
|
||||
if (error.type === "danger" || acc === "danger") return "danger";
|
||||
return "warning";
|
||||
}, "warning");
|
||||
};
|
||||
|
||||
const errorEmailType = computed((): errorType | undefined => {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<o-button
|
||||
v-if="stepIndex >= 2"
|
||||
variant="success"
|
||||
size="is-big"
|
||||
size="big"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.HOME }"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user