Merge branch 'fix-group-events-past' into 'master'

Fix group events past

Closes #492

See merge request framasoft/mobilizon!750
This commit is contained in:
Thomas Citharel 2020-12-09 10:24:45 +01:00
commit a0db8aedfb
11 changed files with 244 additions and 26 deletions

View file

@ -384,7 +384,7 @@ export default class EditorComponent extends Vue {
searchText: query, searchText: query,
}, },
}); });
// TODO: TipTap doesn't handle async for onFilter, hence the following line. // TipTap doesn't handle async for onFilter, hence the following line.
this.filteredActors = result.data.searchPersons.elements; this.filteredActors = result.data.searchPersons.elements;
return this.filteredActors; return this.filteredActors;
}, },

View file

@ -72,7 +72,6 @@
<a :href="emailShareUrl" target="_blank" rel="nofollow noopener" <a :href="emailShareUrl" target="_blank" rel="nofollow noopener"
><b-icon icon="email" size="is-large" type="is-primary" ><b-icon icon="email" size="is-large" type="is-primary"
/></a> /></a>
<!-- TODO: mailto: links are not used anymore, we should provide a popup to redact a message instead -->
</div> </div>
</div> </div>
</section> </section>

View file

@ -1,4 +1,5 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import { GROUP_FIELDS_FRAGMENTS } from "./group";
const participantQuery = ` const participantQuery = `
role, role,
@ -622,3 +623,54 @@ export const GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED = gql`
} }
} }
`; `;
export const FETCH_GROUP_EVENTS = gql`
query(
$name: String!
$afterDateTime: DateTime
$beforeDateTime: DateTime
$organisedEventsPage: Int
$organisedEventslimit: Int
) {
group(preferredUsername: $name) {
id
preferredUsername
domain
name
organizedEvents(
afterDatetime: $afterDateTime
beforeDatetime: $beforeDateTime
page: $organisedEventsPage
limit: $organisedEventslimit
) {
elements {
id
uuid
title
beginsOn
draft
options {
maximumAttendeeCapacity
}
participantStats {
participant
notApproved
}
attributedTo {
id
preferredUsername
name
domain
}
organizerActor {
id
preferredUsername
name
domain
}
}
total
}
}
}
`;

View file

@ -442,7 +442,6 @@
"Actor": "Actor", "Actor": "Actor",
"Text": "Text", "Text": "Text",
"Upcoming events": "Upcoming events", "Upcoming events": "Upcoming events",
"View all upcoming events": "View all upcoming events",
"Resources": "Resources", "Resources": "Resources",
"Public page": "Public page", "Public page": "Public page",
"Discussions": "Discussions", "Discussions": "Discussions",
@ -810,5 +809,6 @@
"Your participation will be validated once you click the confirmation link into the email.": "Your participation will be validated once you click the confirmation link into the email.", "Your participation will be validated once you click the confirmation link into the email.": "Your participation will be validated once you click the confirmation link into the email.",
"Unable to load event for participation. The error details are provided below:": "Unable to load event for participation. The error details are provided below:", "Unable to load event for participation. The error details are provided below:": "Unable to load event for participation. The error details are provided below:",
"Unable to save your participation in this browser.": "Unable to save your participation in this browser.", "Unable to save your participation in this browser.": "Unable to save your participation in this browser.",
"return to the event's page": "return to the event's page" "return to the event's page": "return to the event's page",
"View all events": "View all events"
} }

View file

@ -903,5 +903,6 @@
"Unable to load event for participation. The error details are provided below:": "Impossible de charger l'événement pour la participation. Les détails de l'erreur sont disponibles ci-dessous :", "Unable to load event for participation. The error details are provided below:": "Impossible de charger l'événement pour la participation. Les détails de l'erreur sont disponibles ci-dessous :",
"Unable to save your participation in this browser.": "Échec de la sauvegarde de votre participation dans ce navigateur.", "Unable to save your participation in this browser.": "Échec de la sauvegarde de votre participation dans ce navigateur.",
"return to the event's page": "retourner sur la page de l'événement", "return to the event's page": "retourner sur la page de l'événement",
"You may now close this window, or {return_to_event}.": "Vous pouvez maintenant fermer cette fenêtre, ou bien {return_to_event}." "You may now close this window, or {return_to_event}.": "Vous pouvez maintenant fermer cette fenêtre, ou bien {return_to_event}.",
"View all events": "Voir tous les événements"
} }

View file

@ -14,6 +14,8 @@ import { Component, Vue } from "vue-property-decorator";
variables() { variables() {
return { return {
name: this.$route.params.preferredUsername, name: this.$route.params.preferredUsername,
beforeDateTime: null,
afterDateTime: new Date(),
}; };
}, },
skip() { skip() {

View file

@ -51,7 +51,7 @@
{{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }} {{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }}
</subtitle> </subtitle>
<b-switch v-model="showPassedEvents">{{ $t("Past events") }}</b-switch> <b-switch v-model="showPassedEvents">{{ $t("Past events") }}</b-switch>
<transition-group name="list" tag="p"> <transition-group name="list" tag="div" class="event-list">
<EventListViewCard <EventListViewCard
v-for="event in group.organizedEvents.elements" v-for="event in group.organizedEvents.elements"
:key="event.id" :key="event.id"
@ -68,6 +68,16 @@
> >
{{ $t("No events found") }} {{ $t("No events found") }}
</b-message> </b-message>
<b-pagination
:total="group.organizedEvents.total"
v-model="eventsPage"
:per-page="EVENTS_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')"
>
</b-pagination>
</section> </section>
</section> </section>
</div> </div>
@ -75,15 +85,17 @@
<script lang="ts"> <script lang="ts">
import { Component } from "vue-property-decorator"; import { Component } from "vue-property-decorator";
import { mixins } from "vue-class-component"; import { mixins } from "vue-class-component";
import { FETCH_GROUP } from "@/graphql/group";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import Subtitle from "@/components/Utils/Subtitle.vue"; import Subtitle from "@/components/Utils/Subtitle.vue";
import EventListViewCard from "@/components/Event/EventListViewCard.vue"; import EventListViewCard from "@/components/Event/EventListViewCard.vue";
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor"; import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
import GroupMixin from "@/mixins/group"; import GroupMixin from "@/mixins/group";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import { FETCH_GROUP_EVENTS } from "@/graphql/event";
import { IGroup, IPerson, usernameWithDomain } from "../../types/actor"; import { IGroup, IPerson, usernameWithDomain } from "../../types/actor";
const EVENTS_PAGE_LIMIT = 10;
@Component({ @Component({
apollo: { apollo: {
currentActor: CURRENT_ACTOR_CLIENT, currentActor: CURRENT_ACTOR_CLIENT,
@ -101,12 +113,14 @@ import { IGroup, IPerson, usernameWithDomain } from "../../types/actor";
}, },
}, },
group: { group: {
query: FETCH_GROUP, query: FETCH_GROUP_EVENTS,
variables() { variables() {
return { return {
name: this.$route.params.preferredUsername, name: this.$route.params.preferredUsername,
beforeDateTime: this.showPassedEvents ? new Date() : null, beforeDateTime: this.showPassedEvents ? new Date() : null,
afterDateTime: this.showPassedEvents ? null : new Date(), afterDateTime: this.showPassedEvents ? null : new Date(),
organisedEventsPage: this.eventsPage,
organisedEventslimit: EVENTS_PAGE_LIMIT,
}; };
}, },
}, },
@ -123,11 +137,13 @@ export default class GroupEvents extends mixins(GroupMixin) {
currentActor!: IPerson; currentActor!: IPerson;
eventsPage = 1;
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
RouteName = RouteName; RouteName = RouteName;
showPassedEvents = false; EVENTS_PAGE_LIMIT = EVENTS_PAGE_LIMIT;
get isCurrentActorMember(): boolean { get isCurrentActorMember(): boolean {
if (!this.group || !this.memberships) return false; if (!this.group || !this.memberships) return false;
@ -135,10 +151,25 @@ export default class GroupEvents extends mixins(GroupMixin) {
.map(({ parent: { id } }) => id) .map(({ parent: { id } }) => id)
.includes(this.group.id); .includes(this.group.id);
} }
get showPassedEvents(): boolean {
return (
this.$route.query.future !== undefined &&
this.$route.query.future.toString() === "false"
);
}
set showPassedEvents(value: boolean) {
this.$router.push({ query: { future: this.showPassedEvents.toString() } });
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.container.section { .container.section {
background: $white; background: $white;
} }
div.event-list {
margin-bottom: 1rem;
}
</style> </style>

View file

@ -362,18 +362,26 @@
:key="event.uuid" :key="event.uuid"
class="organized-event" class="organized-event"
/> />
<router-link </div>
:to="{ <div
name: RouteName.GROUP_EVENTS, v-else-if="group && group.organizedEvents.elements.length == 0"
params: { preferredUsername: usernameWithDomain(group) }, class="content has-text-grey has-text-centered"
}" >
>{{ $t("View all upcoming events") }}</router-link <p>{{ $t("No public upcoming events") }}</p>
>
</div> </div>
<div v-else-if="group" class="content has-text-grey has-text-centered"> <div v-else-if="group" class="content has-text-grey has-text-centered">
<p>{{ $t("No public upcoming events") }}</p> <p>{{ $t("No public upcoming events") }}</p>
</div> </div>
<b-skeleton animated v-else></b-skeleton> <b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
<router-link
v-if="group.organizedEvents.total > 0"
:to="{
name: RouteName.GROUP_EVENTS,
params: { preferredUsername: usernameWithDomain(group) },
query: { future: group.organizedEvents.elements.length > 0 },
}"
>{{ $t("View all events") }}</router-link
>
</section> </section>
<section> <section>
<subtitle>{{ $t("Latest posts") }}</subtitle> <subtitle>{{ $t("Latest posts") }}</subtitle>
@ -383,18 +391,19 @@
:key="post.id" :key="post.id"
:post="post" :post="post"
/> />
<router-link
:to="{
name: RouteName.POSTS,
params: { preferredUsername: usernameWithDomain(group) },
}"
>{{ $t("View all posts") }}</router-link
>
</div> </div>
<div v-else-if="group" class="content has-text-grey has-text-centered"> <div v-else-if="group" class="content has-text-grey has-text-centered">
<p>{{ $t("No posts yet") }}</p> <p>{{ $t("No posts yet") }}</p>
</div> </div>
<b-skeleton animated v-else></b-skeleton> <b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
<router-link
v-if="group.posts.total > 0"
:to="{
name: RouteName.POSTS,
params: { preferredUsername: usernameWithDomain(group) },
}"
>{{ $t("View all posts") }}</router-link
>
</section> </section>
<b-modal <b-modal
v-if="physicalAddress && physicalAddress.geom" v-if="physicalAddress && physicalAddress.geom"
@ -607,7 +616,6 @@ export default class Group extends mixins(GroupMixin) {
@Watch("isCurrentActorAGroupMember") @Watch("isCurrentActorAGroupMember")
refetchGroupData(): void { refetchGroupData(): void {
console.log("refetchGroupData");
this.$apollo.queries.group.refetch(); this.$apollo.queries.group.refetch();
} }

View file

@ -0,0 +1,95 @@
import { config, createLocalVue, mount } from "@vue/test-utils";
import GroupSection from "@/components/Group/GroupSection.vue";
import Buefy from "buefy";
import VueRouter, { Location } from "vue-router";
import RouteName from "@/router/name";
import { routes } from "@/router";
const localVue = createLocalVue();
localVue.use(Buefy);
config.mocks.$t = (key: string): string => key;
localVue.use(VueRouter);
const router = new VueRouter({ routes, mode: "history" });
const groupPreferredUsername = "my_group";
const groupDomain = "remotedomain.net";
const groupUsername = `${groupPreferredUsername}@${groupDomain}`;
const defaultSlotText = "A list of elements";
const createSlotButtonText = "+ Post a public message";
type Props = {
title?: string;
icon?: string;
privateSection?: boolean;
route?: Location;
};
const baseProps: Props = {
title: "My group section",
icon: "bullhorn",
route: {
name: RouteName.POSTS,
params: {
preferredUsername: groupUsername,
},
},
};
const generateWrapper = (customProps: Props = {}) => {
return mount(GroupSection, {
localVue,
router,
propsData: { ...baseProps, ...customProps },
slots: {
default: `<div>${defaultSlotText}</div>`,
create: `<router-link :to="{
name: 'POST_CREATE',
params: { preferredUsername: '${groupUsername}' },
}"
class="button is-primary"
>{{ $t("${createSlotButtonText}") }}</router-link
>`,
},
});
};
describe("GroupSection", () => {
it("renders group section with basic informations", () => {
const wrapper = generateWrapper({});
expect(
wrapper
.find(".group-section-title h2 span.icon i")
.classes(`mdi-${baseProps.icon}`)
).toBe(true);
expect(wrapper.find(".group-section-title h2 span:last-child").text()).toBe(
baseProps.title
);
expect(wrapper.find(".group-section-title a").attributes("href")).toBe(
`/@${groupUsername}/p`
);
expect(wrapper.find(".group-section-title").classes("privateSection")).toBe(
true
);
expect(wrapper.find(".main-slot div").text()).toBe(defaultSlotText);
expect(wrapper.find(".create-slot a").text()).toBe(createSlotButtonText);
expect(wrapper.find(".create-slot a").attributes("href")).toBe(
`/@${groupUsername}/p/new`
);
expect(wrapper.html()).toMatchSnapshot();
});
it("renders public group section", () => {
const wrapper = generateWrapper({ privateSection: false });
expect(wrapper.find(".group-section-title").classes("privateSection")).toBe(
false
);
expect(wrapper.html()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GroupSection renders group section with basic informations 1`] = `
<section>
<div class="group-section-title privateSection">
<h2><span class="icon"><i class="mdi mdi-bullhorn mdi-24px"></i></span> <span>My group section</span></h2> <a href="/@my_group@remotedomain.net/p" class="">View all</a>
</div>
<div class="main-slot">
<div>A list of elements</div>
</div>
<div class="create-slot"><a href="/@my_group@remotedomain.net/p/new" class="button is-primary">+ Post a public message</a></div>
</section>
`;
exports[`GroupSection renders public group section 1`] = `
<section>
<div class="group-section-title">
<h2><span class="icon"><i class="mdi mdi-bullhorn mdi-24px"></i></span> <span>My group section</span></h2> <a href="/@my_group@remotedomain.net/p" class="">View all</a>
</div>
<div class="main-slot">
<div>A list of elements</div>
</div>
<div class="create-slot"><a href="/@my_group@remotedomain.net/p/new" class="button is-primary">+ Post a public message</a></div>
</section>
`;

5
vetur.config.js Normal file
View file

@ -0,0 +1,5 @@
// vetur.config.js
/** @type {import('vls').VeturConfig} */
module.exports = {
projects: ["./js"],
};