Improvements to group page

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-09-02 17:42:17 +02:00
parent 69e2a36d03
commit b0e8a32d2a
19 changed files with 298 additions and 87 deletions

View File

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

View File

@@ -40,6 +40,7 @@
</button>
<button
v-if="!isBasicMode"
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 1 }) }"
@click="commands.heading({ level: 1 })"
@@ -49,6 +50,7 @@
</button>
<button
v-if="!isBasicMode"
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 2 }) }"
@click="commands.heading({ level: 2 })"
@@ -58,6 +60,7 @@
</button>
<button
v-if="!isBasicMode"
class="menubar__button"
:class="{ 'is-active': isActive.heading({ level: 3 }) }"
@click="commands.heading({ level: 3 })"
@@ -75,12 +78,18 @@
<b-icon icon="link" />
</button>
<button class="menubar__button" @click="showImagePrompt(commands.image)" type="button">
<button
class="menubar__button"
v-if="!isBasicMode"
@click="showImagePrompt(commands.image)"
type="button"
>
<b-icon icon="image" />
</button>
<button
class="menubar__button"
v-if="!isBasicMode"
:class="{ 'is-active': isActive.bullet_list() }"
@click="commands.bullet_list"
type="button"
@@ -89,6 +98,7 @@
</button>
<button
v-if="!isBasicMode"
class="menubar__button"
:class="{ 'is-active': isActive.ordered_list() }"
@click="commands.ordered_list"
@@ -98,6 +108,7 @@
</button>
<button
v-if="!isBasicMode"
class="menubar__button"
:class="{ 'is-active': isActive.blockquote() }"
@click="commands.blockquote"
@@ -106,11 +117,11 @@
<b-icon icon="format-quote-close" />
</button>
<button class="menubar__button" @click="commands.undo" type="button">
<button v-if="!isBasicMode" class="menubar__button" @click="commands.undo" type="button">
<b-icon icon="undo" />
</button>
<button class="menubar__button" @click="commands.redo" type="button">
<button v-if="!isBasicMode" class="menubar__button" @click="commands.redo" type="button">
<b-icon icon="redo" />
</button>
</div>
@@ -229,26 +240,30 @@ export default class EditorComponent extends Vue {
filteredActors: IActor[] = [];
suggestionRange!: object | null;
suggestionRange!: Record<string, unknown> | null;
navigatedActorIndex = 0;
popup!: Instance[] | null;
get isDescriptionMode() {
return this.mode === "description";
get isDescriptionMode(): boolean {
return this.mode === "description" || this.isBasicMode;
}
get isCommentMode() {
get isCommentMode(): boolean {
return this.mode === "comment";
}
get hasResults() {
return this.filteredActors.length;
get hasResults(): boolean {
return this.filteredActors.length > 0;
}
get showSuggestions() {
return this.query || this.hasResults;
get showSuggestions(): boolean {
return (this.query || this.hasResults) as boolean;
}
get isBasicMode(): boolean {
return this.mode === "basic";
}
// eslint-disable-next-line
@@ -258,7 +273,7 @@ export default class EditorComponent extends Vue {
observer!: MutationObserver | null;
mounted() {
mounted(): void {
this.editor = new Editor({
extensions: [
new Blockquote(),

View File

@@ -16,7 +16,7 @@
</span>
<span>
<span>
{{ $t("Organized by {name}", { name: event.organizerActor.displayName() }) }}
{{ $t("Organized by {name}", { name: usernameWithDomain(event.organizerActor) }) }}
</span>
</span>
</div>
@@ -53,7 +53,7 @@
import { ParticipantRole, EventVisibility, IEventCardOptions, IEvent } from "@/types/event.model";
import { Component, Prop } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { IPerson } from "@/types/actor";
import { IPerson, usernameWithDomain } from "@/types/actor";
import { mixins } from "vue-class-component";
import ActorMixin from "@/mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
@@ -96,6 +96,8 @@ export default class EventListViewCard extends mixins(ActorMixin, EventMixin) {
EventVisibility = EventVisibility;
RouteName = RouteName;
usernameWithDomain = usernameWithDomain;
}
</script>

View File

@@ -9,7 +9,46 @@
<p v-if="event.physicalAddress" class="has-text-grey">
{{ event.physicalAddress.description }}
</p>
<p v-else>3 demandes de participation à traiter</p>
<p v-else>
<span v-if="event.options.maximumAttendeeCapacity !== 0">
{{
$tc(
"{available}/{capacity} available places",
event.options.maximumAttendeeCapacity - event.participantStats.participant,
{
available:
event.options.maximumAttendeeCapacity - event.participantStats.participant,
capacity: event.options.maximumAttendeeCapacity,
}
)
}}
</span>
<span v-else>
{{
$tc("{count} participants", event.participantStats.participant, {
count: event.participantStats.participant,
})
}}
</span>
<span v-if="event.participantStats.notApproved > 0">
<b-button
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
query: { role: ParticipantRole.NOT_APPROVED },
params: { eventId: event.uuid },
})
"
>
{{
$tc("{count} requests waiting", event.participantStats.notApproved, {
count: event.participantStats.notApproved,
})
}}
</b-button>
</span>
</p>
</div>
</router-link>
</template>

View File

@@ -78,12 +78,30 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
banner {
url
}
organizedEvents {
organizedEvents(
afterDatetime: $afterDateTime
beforeDatetime: $beforeDateTime
page: $organisedEventsPage
limit: $organisedEventslimit
) {
elements {
id
uuid
title
beginsOn
options {
maximumAttendeeCapacity
}
participantStats {
participant
notApproved
}
organizerActor {
id
preferredUsername
name
domain
}
}
total
}
@@ -154,7 +172,13 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
`;
export const FETCH_GROUP = gql`
query($name: String!) {
query(
$name: String!
$afterDateTime: DateTime
$beforeDateTime: DateTime
$organisedEventsPage: Int
$organisedEventslimit: Int
) {
group(preferredUsername: $name) {
...GroupFullFields
}
@@ -166,7 +190,13 @@ export const FETCH_GROUP = gql`
`;
export const GET_GROUP = gql`
query($id: ID!) {
query(
$id: ID!
$afterDateTime: DateTime
$beforeDateTime: DateTime
$organisedEventsPage: Int
$organisedEventslimit: Int
) {
getGroup(id: $id) {
...GroupFullFields
}

View File

@@ -34,20 +34,25 @@
}}
</p>
<b-loading :active.sync="$apollo.loading"></b-loading>
<section v-if="group && group.organizedEvents.total > 0">
<section v-if="group">
<subtitle>
{{ $t("Past events") }}
{{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }}
</subtitle>
<b-switch v-model="showPassedEvents">{{ $t("Past events") }}</b-switch>
<transition-group name="list" tag="p">
<EventListViewCard v-for="event in group.organizedEvents.elements" :key="event.id" />
<EventListViewCard
v-for="event in group.organizedEvents.elements"
:key="event.id"
:event="event"
/>
</transition-group>
<b-message
v-if="group.organizedEvents.elements.length === 0 && $apollo.loading === false"
type="is-danger"
>
{{ $t("No events found") }}
</b-message>
</section>
<b-message
v-if="group.organizedEvents.elements.length === 0 && $apollo.loading === false"
type="is-danger"
>
{{ $t("No events found") }}
</b-message>
</section>
</div>
</template>
@@ -55,6 +60,8 @@
import { Component, Vue } from "vue-property-decorator";
import { FETCH_GROUP } from "@/graphql/group";
import RouteName from "@/router/name";
import Subtitle from "@/components/Utils/Subtitle.vue";
import EventListViewCard from "@/components/Event/EventListViewCard.vue";
import { IGroup, usernameWithDomain } from "../../types/actor";
@Component({
@@ -64,10 +71,16 @@ import { IGroup, usernameWithDomain } from "../../types/actor";
variables() {
return {
name: this.$route.params.preferredUsername,
beforeDateTime: this.showPassedEvents ? new Date() : null,
afterDateTime: this.showPassedEvents ? null : new Date(),
};
},
},
},
components: {
Subtitle,
EventListViewCard,
},
})
export default class GroupEvents extends Vue {
group!: IGroup;
@@ -75,5 +88,7 @@ export default class GroupEvents extends Vue {
usernameWithDomain = usernameWithDomain;
RouteName = RouteName;
showPassedEvents = false;
}
</script>

View File

@@ -302,6 +302,10 @@
{{ $t("No group found") }}
</b-message>
<div v-else class="public-container">
<section>
<subtitle>{{ $t("About") }}</subtitle>
<div v-html="group.summary" />
</section>
<section>
<subtitle>{{ $t("Upcoming events") }}</subtitle>
<div class="organized-events-wrapper" v-if="group && group.organizedEvents.total > 0">
@@ -318,16 +322,12 @@
</section>
<section>
<subtitle>{{ $t("Latest posts") }}</subtitle>
<div v-if="group && group.posts.total > 0">
<router-link
v-for="post in group.posts.elements"
:key="post.id"
:to="{ name: RouteName.POST, params: { slug: post.slug } }"
>
{{ post.title }}
</router-link>
<div v-if="group.posts.total > 0" class="posts-wrapper">
<post-list-item v-for="post in group.posts.elements" :key="post.id" :post="post" />
</div>
<div v-else-if="group" class="content has-text-grey has-text-centered">
<p>{{ $t("No posts yet") }}</p>
</div>
<span v-else-if="group">{{ $t("No public posts") }}</span>
<b-skeleton animated v-else></b-skeleton>
</section>
<b-modal v-if="physicalAddress && physicalAddress.geom" :active.sync="showMap">
@@ -369,6 +369,7 @@ import FolderItem from "@/components/Resource/FolderItem.vue";
import { Address } from "@/types/address.model";
import Invitations from "@/components/Group/Invitations.vue";
import addMinutes from "date-fns/addMinutes";
import { Route } from "vue-router";
import GroupSection from "../../components/Group/GroupSection.vue";
import RouteName from "../../router/name";
@@ -413,11 +414,13 @@ import RouteName from "../../router/name";
metaInfo() {
return {
// if no subcomponents specify a metaInfo.title, this title will be used
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
title: this.groupTitle,
// all titles will be injected into this template
titleTemplate: "%s | Mobilizon",
meta: [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
{ name: "description", content: this.groupSummary },
],
@@ -442,14 +445,14 @@ export default class Group extends Vue {
showMap = false;
@Watch("currentActor")
watchCurrentActor(currentActor: IActor, oldActor: IActor) {
watchCurrentActor(currentActor: IActor, oldActor: IActor): void {
if (currentActor.id && oldActor && currentActor.id !== oldActor.id) {
this.$apollo.queries.group.refetch();
}
}
async leaveGroup() {
const { data } = await this.$apollo.mutate({
async leaveGroup(): Promise<Route> {
await this.$apollo.mutate({
mutation: LEAVE_GROUP,
variables: {
groupId: this.group.id,
@@ -458,9 +461,10 @@ export default class Group extends Vue {
return this.$router.push({ name: RouteName.MY_GROUPS });
}
acceptInvitation() {
acceptInvitation(): void {
if (this.groupMember) {
const index = this.person.memberships.elements.findIndex(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
({ id }: IMember) => id === this.groupMember.id
);
@@ -471,12 +475,12 @@ export default class Group extends Vue {
}
}
get groupTitle() {
get groupTitle(): undefined | string {
if (!this.group) return undefined;
return this.group.preferredUsername;
}
get groupSummary() {
get groupSummary(): undefined | string {
if (!this.group) return undefined;
return this.group.summary;
}
@@ -486,8 +490,8 @@ export default class Group extends Vue {
return this.person.memberships.elements.find(({ parent: { id } }) => id === this.group.id);
}
get groupMemberships() {
if (!this.person || !this.person.id) return undefined;
get groupMemberships(): (string | undefined)[] {
if (!this.person || !this.person.id) return [];
return this.person.memberships.elements
.filter(
(membership: IMember) =>
@@ -499,7 +503,7 @@ export default class Group extends Vue {
}
get isCurrentActorAGroupMember(): boolean {
return this.groupMemberships != undefined && this.groupMemberships.includes(this.group.id);
return this.groupMemberships !== undefined && this.groupMemberships.includes(this.group.id);
}
get isCurrentActorARejectedGroupMember(): boolean {
@@ -532,7 +536,8 @@ export default class Group extends Vue {
}
/**
* New members, if on a different server, can take a while to refresh the group and fetch all private data
* New members, if on a different server,
* can take a while to refresh the group and fetch all private data
*/
get isCurrentActorARecentMember(): boolean {
return (
@@ -673,5 +678,11 @@ div.container {
}
}
}
.public-container {
section {
margin-top: 2rem;
}
}
}
</style>

View File

@@ -37,7 +37,7 @@
<b-input v-model="group.name" />
</b-field>
<b-field :label="$t('Group short description')">
<b-input type="textarea" v-model="group.summary"
<editor mode="basic" v-model="group.summary"
/></b-field>
<p class="label">{{ $t("Group visibility") }}</p>
<div class="field">
@@ -105,12 +105,12 @@
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
import { Route } from "vue-router";
import RouteName from "../../router/name";
import { FETCH_GROUP, UPDATE_GROUP, DELETE_GROUP } from "../../graphql/group";
import { IGroup, usernameWithDomain } from "../../types/actor";
import { Address, IAddress } from "../../types/address.model";
import { IMember, Group } from "../../types/actor/group.model";
import { Paginate } from "../../types/paginate";
import { Group } from "../../types/actor/group.model";
@Component({
apollo: {
@@ -129,6 +129,7 @@ import { Paginate } from "../../types/paginate";
},
components: {
FullAddressAutoComplete,
editor: () => import("../../components/Editor.vue"),
},
})
export default class GroupSettings extends Vue {
@@ -149,7 +150,7 @@ export default class GroupSettings extends Vue {
showCopiedTooltip = false;
async updateGroup() {
async updateGroup(): Promise<void> {
const variables = { ...this.group };
// eslint-disable-next-line
// @ts-ignore
@@ -165,7 +166,7 @@ export default class GroupSettings extends Vue {
});
}
confirmDeleteGroup() {
confirmDeleteGroup(): void {
this.$buefy.dialog.confirm({
title: this.$t("Delete group") as string,
message: this.$t(
@@ -179,7 +180,7 @@ export default class GroupSettings extends Vue {
});
}
async deleteGroup() {
async deleteGroup(): Promise<Route> {
await this.$apollo.mutate<{ deleteGroup: IGroup }>({
mutation: DELETE_GROUP,
variables: {
@@ -189,7 +190,7 @@ export default class GroupSettings extends Vue {
return this.$router.push({ name: RouteName.MY_GROUPS });
}
async copyURL() {
async copyURL(): Promise<void> {
await window.navigator.clipboard.writeText(this.group.url);
this.showCopiedTooltip = true;
setTimeout(() => {
@@ -197,6 +198,7 @@ export default class GroupSettings extends Vue {
}, 2000);
}
// eslint-disable-next-line class-methods-use-this
get canShowCopyButton(): boolean {
return window.isSecureContext;
}