Merge branch 'improvements' into 'master'

Upgrade vue-apollo to @vue/apollo-option

See merge request framasoft/mobilizon!937
This commit is contained in:
Thomas Citharel 2021-06-11 16:59:54 +00:00
commit 3fa6001eae
37 changed files with 457 additions and 347 deletions

View File

@ -29,6 +29,7 @@
"@tiptap/extension-underline": "^2.0.0-beta.7",
"@tiptap/starter-kit": "^2.0.0-beta.37",
"@tiptap/vue-2": "^2.0.0-beta.21",
"@vue/apollo-option": "^4.0.0-alpha.11",
"apollo-absinthe-upload-link": "^1.5.0",
"blurhash": "^1.1.3",
"buefy": "^0.9.0",
@ -50,7 +51,6 @@
"unfetch": "^4.2.0",
"v-tooltip": "^2.1.3",
"vue": "^2.6.11",
"vue-apollo": "^3.0.3",
"vue-class-component": "^7.2.3",
"vue-i18n": "^8.14.0",
"vue-meta": "^2.3.1",

View File

@ -37,7 +37,7 @@ export const possibleTypes = types.reduce((acc, type) => {
export const typePolicies: TypePolicies = {
Discussion: {
fields: {
comments: paginatedLimitPagination(),
comments: paginatedLimitPagination<IComment>(),
},
},
Group: {

View File

@ -32,7 +32,6 @@ $color-black: #000;
.mention {
background: rgba($color-black, 0.1);
color: rgba($color-black, 0.6);
font-size: 0.9rem;
font-weight: bold;
border-radius: 5px;

View File

@ -12,7 +12,7 @@
<p>
{{ actor.name || `@${usernameWithDomain(actor)}` }}
</p>
<p class="has-text-grey" v-if="actor.name">
<p class="has-text-grey-dark" v-if="actor.name">
@{{ usernameWithDomain(actor) }}
</p>
<div

View File

@ -147,12 +147,7 @@ import RelayMixin from "../../mixins/relay";
import { RELAY_FOLLOWINGS } from "@/graphql/admin";
import { Paginate } from "@/types/paginate";
import RouteName from "@/router/name";
import {
ApolloCache,
FetchResult,
InMemoryCache,
Reference,
} from "@apollo/client/core";
import { ApolloCache, FetchResult, Reference } from "@apollo/client/core";
import gql from "graphql-tag";
const FOLLOWINGS_PER_PAGE = 10;
@ -221,7 +216,10 @@ export default class Followings extends Mixins(RelayMixin) {
variables: {
address: this.newRelayAddress.trim(), // trim to fix copy and paste domain name spaces and tabs
},
update(cache: ApolloCache<InMemoryCache>, { data }: FetchResult) {
update(
cache: ApolloCache<{ relayFollowings: Paginate<IFollower> }>,
{ data }: FetchResult
) {
cache.modify({
fields: {
relayFollowings(
@ -274,12 +272,12 @@ export default class Followings extends Mixins(RelayMixin) {
async removeRelay(follower: IFollower): Promise<void> {
const address = `${follower.targetActor.preferredUsername}@${follower.targetActor.domain}`;
try {
await this.$apollo.mutate({
await this.$apollo.mutate<{ removeRelay: IFollower }>({
mutation: REMOVE_RELAY,
variables: {
address,
},
update(cache: ApolloCache<InMemoryCache>) {
update(cache: ApolloCache<{ removeRelay: IFollower }>) {
cache.modify({
fields: {
relayFollowings(existingFollowingRefs, { readField }) {

View File

@ -67,7 +67,8 @@
<div v-else>{{ $t("[This comment has been deleted]") }}</div>
<div class="load-replies" v-if="comment.totalReplies">
<p v-if="!showReplies" @click="fetchReplies">
<b-icon icon="chevron-down" /><span>{{
<b-icon icon="chevron-down" class="reply-btn" />
<span class="reply-btn">{{
$tc("View a reply", comment.totalReplies, {
totalReplies: comment.totalReplies,
})
@ -77,8 +78,8 @@
v-else-if="comment.totalReplies && showReplies"
@click="showReplies = false"
>
<b-icon icon="chevron-up" />
<span>{{ $t("Hide replies") }}</span>
<b-icon icon="chevron-up" class="reply-btn" />
<span class="reply-btn">{{ $t("Hide replies") }}</span>
</p>
</div>
</div>
@ -465,7 +466,7 @@ a.comment-link {
& > p > span {
font-weight: bold;
color: $primary;
color: $violet-2;
}
}

View File

@ -107,11 +107,7 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
eventUUID: this.event.uuid,
};
},
update(data) {
return data.event.comments.map(
(comment: IComment) => new CommentModel(comment)
);
},
update: (data) => data.event.comments,
skip() {
return !this.event.uuid;
},

View File

@ -57,7 +57,12 @@
<div v-if="!editMode && !comment.deletedAt" class="text-wrapper">
<div class="description-content" v-html="comment.text"></div>
<p
v-if="comment.insertedAt.getTime() !== comment.updatedAt.getTime()"
v-if="
comment.insertedAt &&
comment.updatedAt &&
new Date(comment.insertedAt).getTime() !==
new Date(comment.updatedAt).getTime()
"
:title="comment.updatedAt | formatDateTimeString"
>
{{

View File

@ -13,206 +13,208 @@
:small="true"
/>
</div>
<div class="content">
<div class="title-wrapper">
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<h3 class="title">{{ participation.event.title }}</h3>
</router-link>
</div>
<div class="participation-actor">
<span>
<b-icon
icon="earth"
v-if="participation.event.visibility === EventVisibility.PUBLIC"
/>
<b-icon
icon="link"
v-else-if="
participation.event.visibility === EventVisibility.UNLISTED
"
/>
<b-icon
icon="lock"
v-else-if="
participation.event.visibility === EventVisibility.PRIVATE
"
/>
</span>
<span
v-if="
participation.event.physicalAddress &&
participation.event.physicalAddress.locality
"
>{{ participation.event.physicalAddress.locality }} -</span
>
<i18n
tag="span"
path="Organized by {name}"
v-if="organizerActor.id !== currentActor.id"
>
<popover-actor-card
slot="name"
:actor="organizerActor"
:inline="true"
>
{{ organizerActor.displayName() }}
</popover-actor-card>
</i18n>
<span v-else>{{ $t("Organized by you") }}</span>
</div>
<div>
<span
class="participant-stats"
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<span
v-if="participation.event.options.maximumAttendeeCapacity !== 0"
>
{{
$tc(
"{available}/{capacity} available places",
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
{
available:
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
capacity:
participation.event.options.maximumAttendeeCapacity,
}
)
}}
</span>
<span v-else>
{{
$tc(
"{count} participants",
participation.event.participantStats.participant,
{
count: participation.event.participantStats.participant,
}
)
}}
</span>
<span v-if="participation.event.participantStats.notApproved > 0">
<b-button
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
query: { role: ParticipantRole.NOT_APPROVED },
params: { eventId: participation.event.uuid },
})
"
>
{{
$tc(
"{count} requests waiting",
participation.event.participantStats.notApproved,
{
count: participation.event.participantStats.notApproved,
}
)
}}
</b-button>
</span>
</span>
</div>
</div>
<div class="actions">
<b-dropdown aria-role="list" position="is-bottom-left">
<b-button slot="trigger" role="button" icon-right="dots-horizontal">
{{ $t("Actions") }}
</b-button>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.EDIT_EVENT,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="pencil" />
{{ $t("Edit") }}
</b-dropdown-item>
<b-dropdown-item
v-if="participation.role === ParticipantRole.CREATOR"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.DUPLICATE_EVENT,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="content-duplicate" />
{{ $t("Duplicate") }}
</b-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="openDeleteEventModalWrapper"
>
<b-icon icon="delete" />
{{ $t("Delete") }}
</b-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="account-multiple-plus" />
{{ $t("Manage participations") }}
</b-dropdown-item>
<b-dropdown-item aria-role="listitem" has-link>
<div class="content-and-actions">
<div class="list-card-content">
<div class="title-wrapper">
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<b-icon icon="view-compact" />
{{ $t("View event page") }}
<h3 class="title">{{ participation.event.title }}</h3>
</router-link>
</b-dropdown-item>
</b-dropdown>
</div>
<div class="participation-actor">
<span>
<b-icon
icon="earth"
v-if="participation.event.visibility === EventVisibility.PUBLIC"
/>
<b-icon
icon="link"
v-else-if="
participation.event.visibility === EventVisibility.UNLISTED
"
/>
<b-icon
icon="lock"
v-else-if="
participation.event.visibility === EventVisibility.PRIVATE
"
/>
</span>
<span
v-if="
participation.event.physicalAddress &&
participation.event.physicalAddress.locality
"
>{{ participation.event.physicalAddress.locality }} -</span
>
<i18n
tag="span"
path="Organized by {name}"
v-if="organizerActor.id !== currentActor.id"
>
<popover-actor-card
slot="name"
:actor="organizerActor"
:inline="true"
>
{{ organizerActor.displayName() }}
</popover-actor-card>
</i18n>
<span v-else>{{ $t("Organized by you") }}</span>
</div>
<div>
<span
class="participant-stats"
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<span
v-if="participation.event.options.maximumAttendeeCapacity !== 0"
>
{{
$tc(
"{available}/{capacity} available places",
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
{
available:
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
capacity:
participation.event.options.maximumAttendeeCapacity,
}
)
}}
</span>
<span v-else>
{{
$tc(
"{count} participants",
participation.event.participantStats.participant,
{
count: participation.event.participantStats.participant,
}
)
}}
</span>
<span v-if="participation.event.participantStats.notApproved > 0">
<b-button
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
query: { role: ParticipantRole.NOT_APPROVED },
params: { eventId: participation.event.uuid },
})
"
>
{{
$tc(
"{count} requests waiting",
participation.event.participantStats.notApproved,
{
count: participation.event.participantStats.notApproved,
}
)
}}
</b-button>
</span>
</span>
</div>
</div>
<div class="actions">
<b-dropdown aria-role="list" position="is-bottom-left">
<b-button slot="trigger" role="button" icon-right="dots-horizontal">
{{ $t("Actions") }}
</b-button>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.EDIT_EVENT,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="pencil" />
{{ $t("Edit") }}
</b-dropdown-item>
<b-dropdown-item
v-if="participation.role === ParticipantRole.CREATOR"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.DUPLICATE_EVENT,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="content-duplicate" />
{{ $t("Duplicate") }}
</b-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="openDeleteEventModalWrapper"
>
<b-icon icon="delete" />
{{ $t("Delete") }}
</b-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="account-multiple-plus" />
{{ $t("Manage participations") }}
</b-dropdown-item>
<b-dropdown-item aria-role="listitem" has-link>
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<b-icon icon="view-compact" />
{{ $t("View event page") }}
</router-link>
</b-dropdown-item>
</b-dropdown>
</div>
</div>
</div>
</article>
@ -351,51 +353,72 @@ article.box {
.list-card {
display: flex;
align-items: center;
padding: 0 6px;
position: relative;
flex-direction: column;
.actions {
padding-right: 7.5px;
cursor: pointer;
div.date-component {
align-self: flex-start;
padding: 5px;
position: absolute;
top: 0;
left: 0;
margin-top: 1px;
height: 0;
display: flex;
align-items: flex-end;
margin-bottom: 15px;
margin-left: 0rem;
}
div.content {
flex: 1;
padding: 5px;
.content-and-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
padding-bottom: 1rem;
.participation-actor span,
.participant-stats span {
padding: 0 5px;
button {
height: auto;
padding-top: 0;
}
.actions {
padding-right: 7.5px;
cursor: pointer;
}
div.title-wrapper {
display: flex;
align-items: center;
div.list-card-content {
flex: 1;
padding: 5px;
min-width: 350px;
div.date-component {
flex: 0;
margin-right: 16px;
.participation-actor span,
.participant-stats span {
padding: 0 5px;
button {
height: auto;
padding-top: 0;
}
}
a {
text-decoration: none;
}
div.title-wrapper {
display: flex;
align-items: center;
padding-top: 5px;
.title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 400;
line-height: 1em;
font-size: 1.6em;
padding-bottom: 5px;
margin: auto 0;
a {
text-decoration: none;
padding-bottom: 5px;
}
.title {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 400;
line-height: 1em;
font-size: 1.4em;
padding-bottom: 5px;
margin: auto 0;
}
}
}
}
@ -405,6 +428,7 @@ article.box {
background: $yellow-2;
display: flex;
padding: 5px;
padding-left: calc(48px + 15px);
figure {
padding-right: 3px;

View File

@ -97,6 +97,10 @@ export default class GroupMemberCard extends Vue {
& > div:last-child {
cursor: pointer;
}
.media-content {
overflow: hidden;
}
}
.identity-header {

View File

@ -56,6 +56,7 @@
tag="a"
href="https://mediation.koena.net/framasoft/mobilizon/"
target="_blank"
rel="noopener"
>
<img
src="/img/koena-a11y.svg"
@ -69,20 +70,34 @@
<search-field @navbar-search="mobileNavbarActive = false" />
</b-navbar-item>
<b-navbar-dropdown v-if="currentActor.id && currentUser.isLoggedIn" right>
<b-navbar-dropdown
v-if="currentActor.id && currentUser.isLoggedIn"
right
collapsible
>
<template
slot="label"
v-if="currentActor"
class="navbar-dropdown-profile"
>
<figure class="image is-32x32" v-if="currentActor.avatar">
<img
class="is-rounded"
alt="avatarUrl"
:src="currentActor.avatar.url"
/>
</figure>
<b-icon v-else icon="account-circle" />
<div class="identity-wrapper">
<div>
<figure class="image is-32x32" v-if="currentActor.avatar">
<img
class="is-rounded"
alt="avatarUrl"
:src="currentActor.avatar.url"
/>
</figure>
<b-icon v-else icon="account-circle" />
</div>
<div class="media-content is-hidden-desktop">
<span>{{ displayName(currentActor) }}</span>
<span class="has-text-grey-dark" v-if="currentActor.name"
>@{{ currentActor.preferredUsername }}</span
>
</div>
</div>
</template>
<!-- No identities dropdown if no identities -->
@ -103,8 +118,8 @@
</div>
<div class="media-content">
<span>{{ identity.displayName() }}</span>
<span class="has-text-grey" v-if="identity.name"
<span>{{ displayName(identity) }}</span>
<span class="has-text-grey-dark" v-if="identity.name"
>@{{ identity.preferredUsername }}</span
>
</div>
@ -169,7 +184,7 @@ import {
IDENTITIES,
UPDATE_DEFAULT_ACTOR,
} from "../graphql/actor";
import { IPerson, Person } from "../types/actor";
import { displayName, IPerson, Person } from "../types/actor";
import { CONFIG } from "../graphql/config";
import { IConfig } from "../types/config.model";
import { ICurrentUser, IUser } from "../types/current-user.model";
@ -227,6 +242,8 @@ export default class NavBar extends Vue {
mobileNavbarActive = false;
displayName = displayName;
@Watch("currentActor")
async initializeListOfIdentities(): Promise<void> {
if (!this.currentUser.isLoggedIn) return;
@ -309,7 +326,7 @@ nav {
cursor: pointer;
span {
display: inherit;
display: flex;
}
&.is-active {
@ -343,5 +360,14 @@ nav {
padding-top: 0.2rem;
}
}
.identity-wrapper {
display: flex;
.media-content span {
display: flex;
color: $violet-2;
}
}
}
</style>

View File

@ -134,7 +134,7 @@ import { addLocalUnconfirmedAnonymousParticipation } from "@/services/AnonymousP
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
import RouteName from "@/router/name";
import { IParticipant } from "../../types/participant.model";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import { ApolloCache, FetchResult } from "@apollo/client/core";
@Component({
apollo: {
@ -197,7 +197,7 @@ export default class ParticipationWithoutAccount extends Vue {
locale: this.$i18n.locale,
},
update: (
store: ApolloCache<InMemoryCache>,
store: ApolloCache<{ joinEvent: IParticipant }>,
{ data: updateData }: FetchResult
) => {
if (updateData == null) {

View File

@ -188,7 +188,7 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
`;
export const FETCH_GROUP = gql`
query (
query FetchGroup(
$name: String!
$afterDateTime: DateTime
$beforeDateTime: DateTime
@ -212,7 +212,7 @@ export const FETCH_GROUP = gql`
`;
export const GET_GROUP = gql`
query (
query GetGroup(
$id: ID!
$afterDateTime: DateTime
$beforeDateTime: DateTime

View File

@ -31,7 +31,12 @@ export default class EventMixin extends mixins(Vue) {
actorId,
token,
},
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
update: (
store: ApolloCache<{
leaveEvent: IParticipant;
}>,
{ data }: FetchResult
) => {
if (data == null) return;
let participation;

View File

@ -15,6 +15,8 @@ import {
import { MemberRole } from "@/types/enums";
import { Component, Vue } from "vue-property-decorator";
const now = new Date();
@Component({
apollo: {
group: {
@ -24,7 +26,7 @@ import { Component, Vue } from "vue-property-decorator";
return {
name: this.$route.params.preferredUsername,
beforeDateTime: null,
afterDateTime: new Date(),
afterDateTime: now,
};
},
skip() {

View File

@ -128,7 +128,7 @@ import { MOBILIZON_INSTANCE_HOST } from "../../api/_entrypoint";
import RouteName from "../../router/name";
import { changeIdentity } from "../../utils/auth";
import identityEditionMixin from "../../mixins/identityEdition";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import { ApolloCache, FetchResult } from "@apollo/client/core";
@Component({
apollo: {
@ -171,7 +171,7 @@ export default class Register extends mixins(identityEditionMixin) {
mutation: REGISTER_PERSON,
variables: { email: this.email, ...this.identity },
update: (
store: ApolloCache<InMemoryCache>,
store: ApolloCache<{ registerPerson: IPerson }>,
{ data: localData }: FetchResult
) => {
if (this.userAlreadyActivated) {

View File

@ -289,7 +289,7 @@ import { usernameWithDomain, IActor } from "../../types/actor/actor.model";
import RouteName from "../../router/name";
import ActorCard from "../../components/Account/ActorCard.vue";
import EmptyContent from "../../components/Utils/EmptyContent.vue";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import { ApolloCache, FetchResult } from "@apollo/client/core";
import VueRouter from "vue-router";
const { isNavigationFailure, NavigationFailureType } = VueRouter;
@ -427,7 +427,10 @@ export default class AdminGroupProfile extends Vue {
variables: {
id: this.id,
},
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
update: (
store: ApolloCache<{ suspendProfile: { id: string } }>,
{ data }: FetchResult
) => {
if (data == null) return;
const profileId = this.id;

View File

@ -272,7 +272,7 @@ import { usernameWithDomain } from "../../types/actor/actor.model";
import RouteName from "../../router/name";
import ActorCard from "../../components/Account/ActorCard.vue";
import EmptyContent from "../../components/Utils/EmptyContent.vue";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import { ApolloCache, FetchResult } from "@apollo/client/core";
import VueRouter from "vue-router";
import { MemberRole } from "@/types/enums";
const { isNavigationFailure, NavigationFailureType } = VueRouter;
@ -389,7 +389,10 @@ export default class AdminProfile extends Vue {
variables: {
id: this.id,
},
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
update: (
store: ApolloCache<{ suspendProfile: { id: string } }>,
{ data }: FetchResult
) => {
if (data == null) return;
const profileId = this.id;

View File

@ -127,7 +127,6 @@
</template>
<script lang="ts">
import { Component, Prop } from "vue-property-decorator";
import { mixins } from "vue-class-component";
import {
GET_DISCUSSION,
REPLY_TO_DISCUSSION,
@ -135,21 +134,22 @@ import {
DELETE_DISCUSSION,
DISCUSSION_COMMENT_CHANGED,
} from "@/graphql/discussion";
import { IDiscussion, Discussion } from "@/types/discussions";
import { IDiscussion } from "@/types/discussions";
import { Discussion as DiscussionModel } from "@/types/discussions";
import { usernameWithDomain } from "@/types/actor";
import DiscussionComment from "@/components/Discussion/DiscussionComment.vue";
import { GraphQLError } from "graphql";
import { DELETE_COMMENT, UPDATE_COMMENT } from "@/graphql/comment";
import GroupMixin from "@/mixins/group";
import RouteName from "../../router/name";
import { IComment } from "../../types/comment.model";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import { ApolloCache, FetchResult } from "@apollo/client/core";
import { mixins } from "vue-class-component";
import GroupMixin from "@/mixins/group";
@Component({
apollo: {
discussion: {
query: GET_DISCUSSION,
fetchPolicy: "cache-and-network",
variables() {
return {
slug: this.slug,
@ -163,12 +163,11 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
error({ graphQLErrors }) {
this.handleErrors(graphQLErrors);
},
update: (data) => new Discussion(data.discussion),
subscribeToMore: {
document: DISCUSSION_COMMENT_CHANGED,
variables() {
return {
slug: this.slug,
slug: this.$route.params.slug,
page: this.page,
limit: this.COMMENTS_PER_PAGE,
};
@ -180,10 +179,10 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
const previousDiscussion = previousResult.discussion;
const lastComment =
subscriptionData.data.discussionCommentChanged.lastComment;
const canLoadMore = !previousDiscussion.comments.elements.find(
this.hasMoreComments = !previousDiscussion.comments.elements.some(
(comment: IComment) => comment.id === lastComment.id
);
if (canLoadMore) {
if (this.hasMoreComments) {
return {
discussion: {
...previousDiscussion,
@ -219,10 +218,10 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
};
},
})
export default class discussion extends mixins(GroupMixin) {
export default class Discussion extends mixins(GroupMixin) {
@Prop({ type: String, required: true }) slug!: string;
discussion: IDiscussion = new Discussion();
discussion: IDiscussion = new DiscussionModel();
newComment = "";
@ -261,7 +260,10 @@ export default class discussion extends mixins(GroupMixin) {
commentId: comment.id,
text: comment.text,
},
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
update: (
store: ApolloCache<{ deleteComment: IComment }>,
{ data }: FetchResult
) => {
if (!data || !data.deleteComment) return;
const discussionData = store.readQuery<{
discussion: IDiscussion;
@ -296,7 +298,10 @@ export default class discussion extends mixins(GroupMixin) {
variables: {
commentId: comment.id,
},
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
update: (
store: ApolloCache<{ deleteComment: IComment }>,
{ data }: FetchResult
) => {
if (!data || !data.deleteComment) return;
const discussionData = store.readQuery<{
discussion: IDiscussion;
@ -343,7 +348,7 @@ export default class discussion extends mixins(GroupMixin) {
async loadMoreComments(): Promise<void> {
if (!this.hasMoreComments) return;
this.page += 1;
this.page++;
try {
await this.$apollo.queries.discussion.fetchMore({
// New variables
@ -352,21 +357,38 @@ export default class discussion extends mixins(GroupMixin) {
page: this.page,
limit: this.COMMENTS_PER_PAGE,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
return {
discussion: {
...previousResult.discussion,
comments: {
...fetchMoreResult.discussion.comments,
elements: [
...previousResult.discussion.comments.elements,
...fetchMoreResult.discussion.comments.elements,
],
},
},
};
},
});
this.hasMoreComments = !this.discussion.comments.elements
.map(({ id }) => id)
.includes(this.discussion?.lastComment?.id);
} catch (e) {
console.error(e);
}
}
async updateDiscussion(): Promise<void> {
await this.$apollo.mutate({
await this.$apollo.mutate<{ updateDiscussion: IDiscussion }>({
mutation: UPDATE_DISCUSSION,
variables: {
discussionId: this.discussion.id,
title: this.newTitle,
},
update: (
store: ApolloCache<InMemoryCache>,
store: ApolloCache<{ updateDiscussion: IDiscussion }>,
{ data }: FetchResult<{ updateDiscussion: IDiscussion }>
) => {
const discussionData = store.readQuery<{

View File

@ -485,7 +485,7 @@ import RouteName from "../../router/name";
import "intersection-observer";
import { CONFIG } from "../../graphql/config";
import { IConfig } from "../../types/config.model";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import { ApolloCache, FetchResult } from "@apollo/client/core";
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
@ -684,11 +684,11 @@ export default class EditEvent extends Vue {
const variables = await this.buildVariables();
try {
const { data } = await this.$apollo.mutate({
const { data } = await this.$apollo.mutate<{ createEvent: IEvent }>({
mutation: CREATE_EVENT,
variables,
update: (
store: ApolloCache<InMemoryCache>,
store: ApolloCache<{ createEvent: IEvent }>,
{ data: updatedData }: FetchResult
) => this.postCreateOrUpdate(store, updatedData?.createEvent),
refetchQueries: ({ data: updatedData }: FetchResult) =>
@ -703,10 +703,12 @@ export default class EditEvent extends Vue {
position: "is-bottom-right",
duration: 5000,
});
await this.$router.push({
name: "Event",
params: { uuid: data.createEvent.uuid },
});
if (data?.createEvent) {
await this.$router.push({
name: "Event",
params: { uuid: data.createEvent.uuid },
});
}
} catch (err) {
this.saving = false;
console.error(err);
@ -719,11 +721,11 @@ export default class EditEvent extends Vue {
const variables = await this.buildVariables();
try {
await this.$apollo.mutate({
await this.$apollo.mutate<{ updateEvent: IEvent }>({
mutation: EDIT_EVENT,
variables,
update: (
store: ApolloCache<InMemoryCache>,
store: ApolloCache<{ updateEvent: IEvent }>,
{ data: updatedData }: FetchResult
) => this.postCreateOrUpdate(store, updatedData?.updateEvent),
refetchQueries: ({ data }: FetchResult) =>

View File

@ -315,7 +315,9 @@
>
{{ physicalAddress.poiInfos.name }}
</p>
<p>{{ physicalAddress.poiInfos.alternativeName }}</p>
<p class="has-text-grey-dark">
{{ physicalAddress.poiInfos.alternativeName }}
</p>
</address>
</div>
<span
@ -663,7 +665,7 @@ import EventBanner from "../../components/Event/EventBanner.vue";
import ActorCard from "../../components/Account/ActorCard.vue";
import PopoverActorCard from "../../components/Account/PopoverActorCard.vue";
import { IParticipant } from "../../types/participant.model";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import { ApolloCache, FetchResult } from "@apollo/client/core";
// noinspection TypeScriptValidateTypes
@Component({
@ -1001,7 +1003,12 @@ export default class Event extends EventMixin {
actorId: identity.id,
message,
},
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
update: (
store: ApolloCache<{
joinEvent: IParticipant;
}>,
{ data }: FetchResult
) => {
if (data == null) return;
const participationCachedData = store.readQuery<{ person: IPerson }>({
@ -1419,7 +1426,6 @@ div.sidebar {
}
:not(.addressDescription) {
color: rgba(46, 62, 72, 0.6);
flex: 1;
min-width: 100%;
}

View File

@ -41,6 +41,7 @@
v-if="isCurrentActorAGroupModerator"
:to="{
name: RouteName.CREATE_EVENT,
query: { actorId: group.id },
}"
class="button is-primary"
>{{ $t("+ Create an event") }}</router-link
@ -88,17 +89,16 @@ import { mixins } from "vue-class-component";
import RouteName from "@/router/name";
import Subtitle from "@/components/Utils/Subtitle.vue";
import EventListViewCard from "@/components/Event/EventListViewCard.vue";
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import GroupMixin from "@/mixins/group";
import { IMember } from "@/types/actor/member.model";
import { FETCH_GROUP_EVENTS } from "@/graphql/event";
import { IGroup, IPerson, usernameWithDomain } from "../../types/actor";
import { usernameWithDomain } from "../../types/actor";
const EVENTS_PAGE_LIMIT = 10;
@Component({
apollo: {
currentActor: CURRENT_ACTOR_CLIENT,
memberships: {
query: PERSON_MEMBERSHIPS,
fetchPolicy: "cache-and-network",
@ -141,12 +141,8 @@ const EVENTS_PAGE_LIMIT = 10;
},
})
export default class GroupEvents extends mixins(GroupMixin) {
group!: IGroup;
memberships!: IMember[];
currentActor!: IPerson;
eventsPage = 1;
usernameWithDomain = usernameWithDomain;

View File

@ -67,7 +67,7 @@
</subtitle>
<transition-group name="list" tag="p">
<div v-for="month in monthlyPastParticipations" :key="month[0]">
<span>{{ month[0] }}</span>
<span class="past-month">{{ month[0] }}</span>
<EventListCard
v-for="participation in month[1]"
:key="participation.id"
@ -304,8 +304,24 @@ main > .container {
}
section {
.upcoming-month {
.upcoming-month,
.past-month {
text-transform: capitalize;
display: inline-block;
position: relative;
font-size: 1.3rem;
&::after {
background: $orange-3;
position: absolute;
left: 0;
right: 0;
top: 100%;
content: "";
width: calc(100% + 30px);
height: 3px;
max-width: 150px;
}
}
}

View File

@ -608,7 +608,7 @@ export default class Home extends Vue {
main > div > .container {
background: $white;
padding: 1rem 1.5rem 3rem;
padding: 1rem 0.5rem 3rem;
}
.search-autocomplete {

View File

@ -302,7 +302,7 @@ import { IComment } from "@/types/comment.model";
import { ActorType, ReportStatusEnum } from "@/types/enums";
import RouteName from "../../router/name";
import { GraphQLError } from "graphql";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import { ApolloCache, FetchResult } from "@apollo/client/core";
@Component({
apollo: {
@ -360,7 +360,10 @@ export default class Report extends Vue {
reportId: this.report.id,
content: this.noteContent,
},
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
update: (
store: ApolloCache<{ createReportNote: IReportNote }>,
{ data }: FetchResult
) => {
if (data == null) return;
const cachedData = store.readQuery<{ report: IReport }>({
query: REPORT,
@ -460,13 +463,16 @@ export default class Report extends Vue {
async updateReport(status: ReportStatusEnum): Promise<void> {
try {
await this.$apollo.mutate({
await this.$apollo.mutate<{ updateReportStatus: IReport }>({
mutation: UPDATE_REPORT,
variables: {
reportId: this.report.id,
status,
},
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
update: (
store: ApolloCache<{ updateReportStatus: IReport }>,
{ data }: FetchResult
) => {
if (data == null) return;
const reportCachedData = store.readQuery<{ report: IReport }>({
query: REPORT,

View File

@ -15,12 +15,12 @@
>{{ post.attributedTo.name }}</router-link
>
</i18n>
<p class="published" v-if="!post.draft">
<p class="published has-text-grey-dark" v-if="!post.draft">
{{ post.publishAt | formatDateTimeString }}
</p>
<small
v-if="post.visibility === PostVisibility.PRIVATE"
class="has-text-grey"
class="has-text-grey-dark"
>
<b-icon icon="lock" size="is-small" />
{{
@ -68,7 +68,7 @@ import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "../../graphql/actor";
import { FETCH_POST } from "../../graphql/post";
import { IPost } from "../../types/post.model";
import { IPerson, usernameWithDomain } from "../../types/actor";
import { usernameWithDomain } from "../../types/actor";
import RouteName from "../../router/name";
import Tag from "../../components/Tag.vue";
@ -126,8 +126,6 @@ export default class Post extends mixins(GroupMixin) {
memberships!: IMember[];
currentActor!: IPerson;
RouteName = RouteName;
usernameWithDomain = usernameWithDomain;
@ -171,7 +169,6 @@ article {
.published {
margin-top: 1rem;
color: rgba(0, 0, 0, 0.5);
}
&::after {

View File

@ -249,7 +249,7 @@ import { CONFIG } from "../../graphql/config";
import { IConfig } from "../../types/config.model";
import ResourceMixin from "../../mixins/resource";
import ResourceSelector from "../../components/Resource/ResourceSelector.vue";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import { ApolloCache, FetchResult } from "@apollo/client/core";
@Component({
components: { FolderItem, ResourceItem, Draggable, ResourceSelector },
@ -560,7 +560,10 @@ export default class Resources extends Mixins(ResourceMixin) {
path: resource.path,
},
refetchQueries: () => this.postRefreshQueries(),
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
update: (
store: ApolloCache<{ updateResource: IResource }>,
{ data }: FetchResult
) => {
if (!data || data.updateResource == null || parentPath == null)
return;
if (!this.resource.actor) return;

View File

@ -1,5 +1,5 @@
import Vue from "vue";
import VueApollo from "vue-apollo";
import VueApollo from "@vue/apollo-option";
import { onError } from "@apollo/client/link/error";
import { createLink } from "apollo-absinthe-upload-link";
import {

View File

@ -6,7 +6,7 @@ import {
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import VueApollo from "vue-apollo";
import VueApollo from "@vue/apollo-option";
import {
COMMENTS_THREADS_WITH_REPLIES,
CREATE_COMMENT_FROM_EVENT,

View File

@ -10,7 +10,7 @@ import {
RequestHandler,
} from "mock-apollo-client";
import { CONFIG } from "@/graphql/config";
import VueApollo from "vue-apollo";
import VueApollo from "@vue/apollo-option";
import { configMock } from "../../mocks/config";
import { InMemoryCache } from "@apollo/client/cache";
import { defaultResolvers } from "../../common";

View File

@ -14,7 +14,7 @@ import {
RequestHandler,
} from "mock-apollo-client";
import { CONFIG } from "@/graphql/config";
import VueApollo from "vue-apollo";
import VueApollo from "@vue/apollo-option";
import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event";
import { IEvent } from "@/types/event.model";
import { i18n } from "@/utils/i18n";

View File

@ -3,7 +3,7 @@ import PasswordReset from "@/views/User/PasswordReset.vue";
import Buefy from "buefy";
import { createMockClient, RequestHandler } from "mock-apollo-client";
import { RESET_PASSWORD } from "@/graphql/auth";
import VueApollo from "vue-apollo";
import VueApollo from "@vue/apollo-option";
import { resetPasswordResponseMock } from "../../mocks/auth";
import RouteName from "@/router/name";
import flushPromises from "flush-promises";

View File

@ -6,7 +6,7 @@ import {
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import VueApollo from "vue-apollo";
import VueApollo from "@vue/apollo-option";
import buildCurrentUserResolver from "@/apollo/user";
import { configMock } from "../../mocks/config";
import { i18n } from "@/utils/i18n";

View File

@ -5,7 +5,7 @@ import {
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import VueApollo from "vue-apollo";
import VueApollo from "@vue/apollo-option";
import { CONFIG } from "@/graphql/config";
import { USER_SETTINGS } from "@/graphql/user";
import buildCurrentUserResolver from "@/apollo/user";

View File

@ -2313,6 +2313,13 @@
"@typescript-eslint/types" "4.26.0"
eslint-visitor-keys "^2.0.0"
"@vue/apollo-option@^4.0.0-alpha.11":
version "4.0.0-alpha.11"
resolved "https://registry.yarnpkg.com/@vue/apollo-option/-/apollo-option-4.0.0-alpha.11.tgz#b4ecac2d1ac40271cb7f20683fb8e4c85974329a"
integrity sha512-IU458Y2cH/eo3RYXyoEoozyRxEgUz/BLC96DQAQKiEc7hFkE6I0g/VZu4tVIP0Od1/ivezdGaAPEVPA2AxE1ug==
dependencies:
throttle-debounce "^2.3.0"
"@vue/babel-helper-vue-jsx-merge-props@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81"
@ -10642,7 +10649,7 @@ throat@^5.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
throttle-debounce@^2.1.0:
throttle-debounce@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
@ -11131,15 +11138,6 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
vue-apollo@^3.0.3:
version "3.0.7"
resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.7.tgz#97a031d45641faa4888a6d5a7f71c40834359704"
integrity sha512-EUfIn4cJmoflnDJiSNP8gH4fofIEzd0I2AWnd9nhHB8mddmzIfgSNjIRihDcRB10wypYG1OG0GcU335CFgZRfA==
dependencies:
chalk "^2.4.2"
serialize-javascript "^4.0.0"
throttle-debounce "^2.1.0"
vue-class-component@^7.2.3:
version "7.2.6"
resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.6.tgz#8471e037b8e4762f5a464686e19e5afc708502e4"

View File

@ -162,9 +162,7 @@ defmodule Mobilizon.Mixfile do
{:sweet_xml, "~> 0.6.6"},
{:web_push_encryption,
git: "https://github.com/tcitworld/elixir-web-push-encryption", branch: "otp-24"},
{:eblurhash,
git: "https://github.com/zotonic/eblurhash",
ref: "04a0b76eadf4de1be17726f39b6313b88708fd12"},
{:eblurhash, "~> 1.2"},
# Dev and test dependencies
{:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
{:ex_machina, "~> 2.3", only: [:dev, :test]},

View File

@ -25,7 +25,7 @@
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"earmark": {:hex, :earmark, "1.4.15", "2c7f924bf495ec1f65bd144b355d0949a05a254d0ec561740308a54946a67888", [:mix], [{:earmark_parser, ">= 1.4.13", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "3b1209b85bc9f3586f370f7c363f6533788fb4e51db23aa79565875e7f9999ee"},
"earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"},
"eblurhash": {:git, "https://github.com/zotonic/eblurhash", "04a0b76eadf4de1be17726f39b6313b88708fd12", [ref: "04a0b76eadf4de1be17726f39b6313b88708fd12"]},
"eblurhash": {:hex, :eblurhash, "1.2.0", "ff461979542fcb1bfd428aaba6ee56e544975f6d657aa3dfcf3e314b1d1c2517", [:rebar3], [], "hexpm", "8fa6b740f1630adc0a3e425dbbb4ff92d036e62eb973e7a06676226137a10aa7"},
"ecto": {:hex, :ecto, "3.6.2", "efdf52acfc4ce29249bab5417415bd50abd62db7b0603b8bab0d7b996548c2bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "efad6dfb04e6f986b8a3047822b0f826d9affe8e4ebdd2aeedbfcb14fd48884e"},
"ecto_autoslug_field": {:hex, :ecto_autoslug_field, "2.0.1", "2177c1c253f6dd3efd4b56d1cb76104d0a6ef044c6b9a7a0ad6d32665c4111e5", [:mix], [{:ecto, ">= 2.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:slugger, ">= 0.2.0", [hex: :slugger, repo: "hexpm", optional: false]}], "hexpm", "a3cc73211f2e75b89a03332183812ebe1ac08be2e25a1df5aa3d1422f92c45c3"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},