diff --git a/js/src/common.scss b/js/src/common.scss index 30160e383..b9c39647d 100644 --- a/js/src/common.scss +++ b/js/src/common.scss @@ -157,3 +157,10 @@ p { background-color: whitesmoke; color: #0a0a0a; } + +/** + * Bulma/Buefy fixes + */ +.icon { + vertical-align: middle; +} diff --git a/js/src/components/Event/DateCalendarIcon.vue b/js/src/components/Event/DateCalendarIcon.vue index bd87863d1..59a55e3fc 100644 --- a/js/src/components/Event/DateCalendarIcon.vue +++ b/js/src/components/Event/DateCalendarIcon.vue @@ -12,18 +12,17 @@ diff --git a/js/src/components/Event/MultiEventMinimalistCard.vue b/js/src/components/Event/MultiEventMinimalistCard.vue new file mode 100644 index 000000000..ea99e4917 --- /dev/null +++ b/js/src/components/Event/MultiEventMinimalistCard.vue @@ -0,0 +1,38 @@ + + + diff --git a/js/src/components/Footer.vue b/js/src/components/Footer.vue index a38792dbd..d4c99d937 100644 --- a/js/src/components/Footer.vue +++ b/js/src/components/Footer.vue @@ -47,6 +47,7 @@
  • @@ -62,13 +63,16 @@ tag="span" path="Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}." > - {{ + {{ $t("Mobilizon") }} {{ new Date().getFullYear() }} - {{ - $t("more than 1360 contributors") - }} + {{ $t("more than 1360 contributors") }} diff --git a/js/src/components/Group/GroupMemberCard.vue b/js/src/components/Group/GroupMemberCard.vue index bf63e0670..77002a359 100644 --- a/js/src/components/Group/GroupMemberCard.vue +++ b/js/src/components/Group/GroupMemberCard.vue @@ -4,6 +4,7 @@
    + {{ displayNameAndUsername(member.actor) }}
    @@ -47,7 +48,7 @@
    -

    {{ member.parent.summary }}

    +

    @@ -110,7 +111,8 @@ export default class GroupMemberCard extends Vue { display: flex; padding: 5px; - figure { + figure, + span.icon { padding-right: 3px; } } diff --git a/js/src/components/Image/LazyImage.vue b/js/src/components/Image/LazyImage.vue index 22674bd2b..5af065b92 100644 --- a/js/src/components/Image/LazyImage.vue +++ b/js/src/components/Image/LazyImage.vue @@ -16,7 +16,7 @@ :width="width" :height="height" class="absolute top-0 left-0 transition-opacity duration-500" - :class="isLoaded ? 'opacity-100' : 'opacity-0'" + :class="{ isLoaded: isLoaded ? 'opacity-100' : 'opacity-0', rounded }" alt="" />
    @@ -37,6 +37,7 @@ export default class LazyImage extends Vue { @Prop({ type: String, required: false, default: null }) blurhash!: string; @Prop({ type: Number, default: 1 }) width!: number; @Prop({ type: Number, default: 1 }) height!: number; + @Prop({ type: Boolean, default: false }) rounded!: boolean; inheritAttrs = false; isLoaded = false; @@ -115,5 +116,8 @@ img { height: 100%; object-fit: cover; object-position: 50% 50%; + &.rounded { + border-radius: 8px; + } } diff --git a/js/src/components/Image/LazyImageWrapper.vue b/js/src/components/Image/LazyImageWrapper.vue index 1b3e1313a..038207644 100644 --- a/js/src/components/Image/LazyImageWrapper.vue +++ b/js/src/components/Image/LazyImageWrapper.vue @@ -5,6 +5,7 @@ :width="pictureOrDefault.metadata.width" :height="pictureOrDefault.metadata.height" :blurhash="pictureOrDefault.metadata.blurhash" + :rounded="rounded" /> + diff --git a/js/src/components/Post/PostElementItem.vue b/js/src/components/Post/PostElementItem.vue deleted file mode 100644 index 045ffbdab..000000000 --- a/js/src/components/Post/PostElementItem.vue +++ /dev/null @@ -1,134 +0,0 @@ - - - diff --git a/js/src/components/Post/PostListItem.vue b/js/src/components/Post/PostListItem.vue index daa1024a3..c9d37ff1c 100644 --- a/js/src/components/Post/PostListItem.vue +++ b/js/src/components/Post/PostListItem.vue @@ -3,53 +3,116 @@ class="post-minimalist-card-wrapper" :to="{ name: RouteName.POST, params: { slug: post.slug } }" > -
    + +

    {{ post.title }}

    - {{ - formatDistanceToNow(new Date(post.publishAt || post.insertedAt), { - locale: $dateFnsLocale, - addSuffix: true, - }) - }} +

    + + {{ + publishedAt | formatDateTimeString(undefined, false, "short") + }} + {{ + formatDistanceToNow(publishedAt, { + locale: $dateFnsLocale, + addSuffix: true, + }) + }} +

    + + + {{ tag.title }} + +

    + + + {{ + displayName(post.author) + }} + +

    diff --git a/js/src/components/Resource/FolderItem.vue b/js/src/components/Resource/FolderItem.vue index 5bf4ae112..32771dc84 100644 --- a/js/src/components/Resource/FolderItem.vue +++ b/js/src/components/Resource/FolderItem.vue @@ -61,7 +61,7 @@ export default class FolderItem extends Mixins(ResourceMixin) { list = []; groupObject: Record = { - name: `folder-${this.resource.title}`, + name: `folder-${this.resource?.title}`, pull: false, put: ["resources"], }; diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index 427690f01..aa7f0d372 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -198,67 +198,6 @@ export const UPDATE_CURRENT_ACTOR_CLIENT = gql` } `; -export const LOGGED_USER_PARTICIPATIONS = gql` - query LoggedUserParticipations( - $afterDateTime: DateTime - $beforeDateTime: DateTime - $page: Int - $limit: Int - ) { - loggedUser { - id - participations( - afterDatetime: $afterDateTime - beforeDatetime: $beforeDateTime - page: $page - limit: $limit - ) { - total - elements { - event { - id - uuid - title - picture { - id - url - alt - } - beginsOn - visibility - organizerActor { - ...ActorFragment - } - attributedTo { - ...ActorFragment - } - participantStats { - going - notApproved - participant - } - options { - maximumAttendeeCapacity - remainingAttendeeCapacity - } - tags { - id - slug - title - } - } - id - role - actor { - ...ActorFragment - } - } - } - } - } - ${ACTOR_FRAGMENT} -`; - export const LOGGED_USER_DRAFTS = gql` query LoggedUserDrafts($page: Int, $limit: Int) { loggedUser { @@ -267,6 +206,7 @@ export const LOGGED_USER_DRAFTS = gql` id uuid title + draft picture { id url diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 10d8aefba..2385e0db9 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -1,66 +1,13 @@ import gql from "graphql-tag"; import { ACTOR_FRAGMENT } from "./actor"; import { ADDRESS_FRAGMENT } from "./address"; +import { EVENT_OPTIONS_FRAGMENT } from "./event_options"; +import { + PARTICIPANTS_QUERY_FRAGMENT, + PARTICIPANT_QUERY_FRAGMENT, +} from "./participant"; import { TAG_FRAGMENT } from "./tags"; -const PARTICIPANT_QUERY_FRAGMENT = gql` - fragment ParticipantQuery on Participant { - role - id - actor { - ...ActorFragment - } - event { - id - uuid - } - metadata { - cancellationToken - message - } - insertedAt - } - ${ACTOR_FRAGMENT} -`; - -const PARTICIPANTS_QUERY_FRAGMENT = gql` - fragment ParticipantsQuery on PaginatedParticipantList { - total - elements { - ...ParticipantQuery - } - } - ${PARTICIPANT_QUERY_FRAGMENT} -`; - -export const EVENT_OPTIONS_FRAGMENT = gql` - fragment EventOptions on EventOptions { - maximumAttendeeCapacity - remainingAttendeeCapacity - showRemainingAttendeeCapacity - anonymousParticipation - showStartTime - showEndTime - timezone - offers { - price - priceCurrency - url - } - participationConditions { - title - content - url - } - attendees - program - commentModeration - showParticipationPrice - hideOrganizerWhenGroupEvent - isOnline - } -`; - const FULL_EVENT_FRAGMENT = gql` fragment FullEvent on Event { id @@ -473,7 +420,7 @@ export const FETCH_GROUP_EVENTS = gql` $afterDateTime: DateTime $beforeDateTime: DateTime $organisedEventsPage: Int - $organisedEventslimit: Int + $organisedEventsLimit: Int ) { group(preferredUsername: $name) { ...ActorFragment @@ -481,7 +428,7 @@ export const FETCH_GROUP_EVENTS = gql` afterDatetime: $afterDateTime beforeDatetime: $beforeDateTime page: $organisedEventsPage - limit: $organisedEventslimit + limit: $organisedEventsLimit ) { elements { id @@ -490,7 +437,7 @@ export const FETCH_GROUP_EVENTS = gql` beginsOn draft options { - maximumAttendeeCapacity + ...EventOptions } participantStats { participant @@ -502,12 +449,21 @@ export const FETCH_GROUP_EVENTS = gql` organizerActor { ...ActorFragment } + physicalAddress { + ...AdressFragment + } + picture { + url + id + } } total } } } + ${EVENT_OPTIONS_FRAGMENT} ${ACTOR_FRAGMENT} + ${ADDRESS_FRAGMENT} `; export const CLOSE_EVENTS = gql` diff --git a/js/src/graphql/event_options.ts b/js/src/graphql/event_options.ts new file mode 100644 index 000000000..ca521506b --- /dev/null +++ b/js/src/graphql/event_options.ts @@ -0,0 +1,29 @@ +import gql from "graphql-tag"; + +export const EVENT_OPTIONS_FRAGMENT = gql` + fragment EventOptions on EventOptions { + maximumAttendeeCapacity + remainingAttendeeCapacity + showRemainingAttendeeCapacity + anonymousParticipation + showStartTime + showEndTime + timezone + offers { + price + priceCurrency + url + } + participationConditions { + title + content + url + } + attendees + program + commentModeration + showParticipationPrice + hideOrganizerWhenGroupEvent + isOnline + } +`; diff --git a/js/src/graphql/group.ts b/js/src/graphql/group.ts index 09a702326..481f15286 100644 --- a/js/src/graphql/group.ts +++ b/js/src/graphql/group.ts @@ -3,6 +3,9 @@ import { DISCUSSION_BASIC_FIELDS_FRAGMENT } from "./discussion"; import { RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT } from "./resources"; import { POST_BASIC_FIELDS } from "./post"; import { ACTOR_FRAGMENT } from "./actor"; +import { ADDRESS_FRAGMENT } from "./address"; +import { TAG_FRAGMENT } from "./tags"; +import { EVENT_OPTIONS_FRAGMENT } from "./event_options"; export const LIST_GROUPS = gql` query ListGroups( @@ -68,6 +71,7 @@ export const GROUP_FIELDS_FRAGMENTS = gql` type id originId + url } avatar { id @@ -93,7 +97,7 @@ export const GROUP_FIELDS_FRAGMENTS = gql` afterDatetime: $afterDateTime beforeDatetime: $beforeDateTime page: $organisedEventsPage - limit: $organisedEventslimit + limit: $organisedEventsLimit ) { elements { id @@ -114,6 +118,19 @@ export const GROUP_FIELDS_FRAGMENTS = gql` organizerActor { ...ActorFragment } + picture { + id + url + } + physicalAddress { + ...AdressFragment + } + options { + ...EventOptions + } + tags { + ...TagFragment + } } total } @@ -177,6 +194,9 @@ export const GROUP_FIELDS_FRAGMENTS = gql` } } ${ACTOR_FRAGMENT} + ${ADDRESS_FRAGMENT} + ${EVENT_OPTIONS_FRAGMENT} + ${TAG_FRAGMENT} `; export const FETCH_GROUP = gql` @@ -185,7 +205,7 @@ export const FETCH_GROUP = gql` $afterDateTime: DateTime $beforeDateTime: DateTime $organisedEventsPage: Int - $organisedEventslimit: Int + $organisedEventsLimit: Int $postsPage: Int $postsLimit: Int $membersPage: Int @@ -209,7 +229,7 @@ export const GET_GROUP = gql` $afterDateTime: DateTime $beforeDateTime: DateTime $organisedEventsPage: Int - $organisedEventslimit: Int + $organisedEventsLimit: Int $postsPage: Int $postsLimit: Int $membersPage: Int diff --git a/js/src/graphql/home.ts b/js/src/graphql/home.ts index e5a059ec0..0b6ff5c11 100644 --- a/js/src/graphql/home.ts +++ b/js/src/graphql/home.ts @@ -1,7 +1,7 @@ import gql from "graphql-tag"; import { ACTOR_FRAGMENT } from "./actor"; import { ADDRESS_FRAGMENT } from "./address"; -import { EVENT_OPTIONS_FRAGMENT } from "./event"; +import { EVENT_OPTIONS_FRAGMENT } from "./event_options"; import { TAG_FRAGMENT } from "./tags"; import { USER_SETTINGS_FRAGMENT } from "./user"; diff --git a/js/src/graphql/participant.ts b/js/src/graphql/participant.ts new file mode 100644 index 000000000..b3772833e --- /dev/null +++ b/js/src/graphql/participant.ts @@ -0,0 +1,210 @@ +import gql from "graphql-tag"; +import { ACTOR_FRAGMENT } from "./actor"; +import { ADDRESS_FRAGMENT } from "./address"; +import { EVENT_OPTIONS_FRAGMENT } from "./event_options"; +import { TAG_FRAGMENT } from "./tags"; + +export const LOGGED_USER_PARTICIPATIONS = gql` + query LoggedUserParticipations( + $afterDateTime: DateTime + $beforeDateTime: DateTime + $page: Int + $limit: Int + ) { + loggedUser { + id + participations( + afterDatetime: $afterDateTime + beforeDatetime: $beforeDateTime + page: $page + limit: $limit + ) { + total + elements { + event { + id + uuid + url + title + picture { + id + url + alt + } + beginsOn + visibility + organizerActor { + ...ActorFragment + } + attributedTo { + ...ActorFragment + } + participantStats { + going + notApproved + participant + } + options { + ...EventOptions + } + tags { + id + slug + title + } + physicalAddress { + ...AdressFragment + } + } + id + role + actor { + ...ActorFragment + } + } + } + } + } + ${ACTOR_FRAGMENT} + ${ADDRESS_FRAGMENT} + ${EVENT_OPTIONS_FRAGMENT} +`; + +export const LOGGED_USER_UPCOMING_EVENTS = gql` + query LoggedUserUpcomingEvents( + $afterDateTime: DateTime + $beforeDateTime: DateTime + $page: Int + $limit: Int + ) { + loggedUser { + id + participations( + afterDatetime: $afterDateTime + beforeDatetime: $beforeDateTime + page: $page + limit: $limit + ) { + total + elements { + event { + id + uuid + url + title + picture { + id + url + alt + } + beginsOn + visibility + organizerActor { + ...ActorFragment + } + attributedTo { + ...ActorFragment + } + participantStats { + going + notApproved + rejected + participant + } + options { + ...EventOptions + } + tags { + id + slug + title + } + physicalAddress { + ...AdressFragment + } + } + id + role + actor { + ...ActorFragment + } + } + } + followedGroupEvents(afterDatetime: $afterDateTime) { + total + elements { + profile { + id + } + group { + ...ActorFragment + } + event { + id + uuid + title + beginsOn + picture { + url + } + attributedTo { + ...ActorFragment + } + organizerActor { + ...ActorFragment + } + options { + ...EventOptions + } + physicalAddress { + ...AdressFragment + } + tags { + ...TagFragment + } + participantStats { + going + notApproved + rejected + participant + } + } + } + } + } + } + ${ACTOR_FRAGMENT} + ${ADDRESS_FRAGMENT} + ${EVENT_OPTIONS_FRAGMENT} + ${TAG_FRAGMENT} +`; + +export const PARTICIPANT_QUERY_FRAGMENT = gql` + fragment ParticipantQuery on Participant { + role + id + actor { + ...ActorFragment + } + event { + id + uuid + } + metadata { + cancellationToken + message + } + insertedAt + } + ${ACTOR_FRAGMENT} +`; + +export const PARTICIPANTS_QUERY_FRAGMENT = gql` + fragment ParticipantsQuery on PaginatedParticipantList { + total + elements { + ...ParticipantQuery + } + } + ${PARTICIPANT_QUERY_FRAGMENT} +`; diff --git a/js/src/graphql/post.ts b/js/src/graphql/post.ts index 54ec058b0..b7ccc7bd4 100644 --- a/js/src/graphql/post.ts +++ b/js/src/graphql/post.ts @@ -61,8 +61,12 @@ export const POST_BASIC_FIELDS = gql` url name } + tags { + ...TagFragment + } } ${ACTOR_FRAGMENT} + ${TAG_FRAGMENT} `; export const FETCH_GROUP_POSTS = gql` diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 29d566f6a..2fff97df0 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -1021,7 +1021,6 @@ "A member has been updated": "A member has been updated", "User settings": "User settings", "You changed your email or password": "You changed your email or password", - "Organized by you": "Organized by you", "Move resource to the root folder": "Move resource to the root folder", "Share this group": "Share this group", "This group is accessible only through it's link. Be careful where you post this link.": "This group is accessible only through it's link. Be careful where you post this link.", @@ -1208,7 +1207,24 @@ "Unfollow": "Unfollow", "your notification settings": "your notification settings", "You will receive notifications about this group's public activity depending on %{notification_settings}.": "You will receive notifications about this group's public activity depending on %{notification_settings}.", - "Recent events from your groups": "Recent events from your groups", "Online": "Online", - "That you follow or of which you are a member": "That you follow or of which you are a member" + "That you follow or of which you are a member": "That you follow or of which you are a member", + "{number} seats left": "{number} seats left", + "Published by {name}": "Published by {name}", + "Share this post": "Share this post", + "This post is accessible only through it's link. Be careful where you post this link.": "This post is accessible only through it's link. Be careful where you post this link.", + "Post URL": "Post URL", + "Are you sure you want to delete this post? This action cannot be reverted.": "Are you sure you want to delete this post? This action cannot be reverted.", + "Attending": "Attending", + "From my groups": "From my groups", + "You don't have any upcoming events. Maybe try another filter?": "You don't have any upcoming events. Maybe try another filter?", + "Leave group": "Leave group", + "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.": "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.", + "Upcoming events from your groups": "Upcoming events from your groups", + "Accessible only by link": "Accessible only by link", + "Report this post": "Report this post", + "Post {eventTitle} reported": "Post {eventTitle} reported", + "You have attended {count} events in the past.": "You have not attended any events in the past.|You have attended one event in the past.|You have attended {count} events in the past.", + "Showing events starting on": "Showing events starting on", + "Showing events before": "Showing events before" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index fb97c197c..960689693 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -1267,8 +1267,6 @@ "Time in your timezone ({timezone})": "Heure dans votre fuseau horaire ({timezone})", "Export": "Export", "Times in your timezone ({timezone})": "Heures dans votre fuseau horaire ({timezone})", - "has loaded": "a chargé", - "Skip to main": "", "Navigated to {pageTitle}": "Navigué vers {pageTitle}", "Comment body": "Corps du commentaire", "Confirm participation": "Confirmer la participation", @@ -1314,7 +1312,23 @@ "Unfollow": "Ne plus suivre", "your notification settings": "vos paramètres de notification", "You will receive notifications about this group's public activity depending on %{notification_settings}.": "Vous recevrez des notifications à propos de l'activité publique de ce groupe en fonction de %{notification_settings}.", - "Recent events from your groups": "Événements récents de vos groupes", "Online": "En ligne", - "That you follow or of which you are a member": "Que vous suivez ou dont vous êtes membre" + "That you follow or of which you are a member": "Que vous suivez ou dont vous êtes membre", + "{number} seats left": "{number} places restantes", + "Published by {name}": "Publié par {name}", + "Share this post": "Partager ce billet", + "This post is accessible only through it's link. Be careful where you post this link.": "Ce billet est accessible uniquement à travers son lien. Faites attention où vous le diffusez.", + "Post URL": "URL du billet", + "Are you sure you want to delete this post? This action cannot be reverted.": "Voulez-vous vraiment supprimer ce billet ? Cette action ne peut pas être annulée.", + "Attending": "Participant⋅e", + "From my groups": "De mes groupes", + "You don't have any upcoming events. Maybe try another filter?": "Vous n'avez pas d'événements à venir. Essayez peut-être un autre filtre ?", + "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.": "Êtes-vous certain⋅e de vouloir quitter le groupe {groupName}? Vous perdrez accès au contenu privé de ce groupe. Cette action ne peut être annulée.", + "Upcoming events from your groups": "Événements de vos groupes à venir", + "Accessible only by link": "Accessible uniquement par lien", + "Report this post": "Signaler ce billet", + "Post {eventTitle} reported": "Billet {eventTitle} signalé", + "You have attended {count} events in the past.": "Vous n'avez participé à aucun événement par le passé.|Vous avez participé à un événement par le passé.|Vous avez participé à {count} événements par le passé.", + "Showing events starting on": "Afficher les événements à partir de", + "Showing events before": "Afficher les événements avant" } diff --git a/js/src/mixins/group.ts b/js/src/mixins/group.ts index 01eb13cda..752c84c9d 100644 --- a/js/src/mixins/group.ts +++ b/js/src/mixins/group.ts @@ -3,7 +3,7 @@ import { GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED, PERSON_STATUS_GROUP, } from "@/graphql/actor"; -import { FETCH_GROUP } from "@/graphql/group"; +import { DELETE_GROUP, FETCH_GROUP } from "@/graphql/group"; import RouteName from "@/router/name"; import { IActor, @@ -14,6 +14,7 @@ import { } from "@/types/actor"; import { MemberRole } from "@/types/enums"; import { Component, Vue } from "vue-property-decorator"; +import { Route } from "vue-router"; const now = new Date(); @@ -135,4 +136,28 @@ export default class GroupMixin extends Vue { this.$router.replace({ name: RouteName.PAGE_NOT_FOUND }); } } + + confirmDeleteGroup(): void { + this.$buefy.dialog.confirm({ + title: this.$t("Delete group") as string, + message: this.$t( + "Are you sure you want to completely delete this group? All members - including remote ones - will be notified and removed from the group, and all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed." + ) as string, + confirmText: this.$t("Delete group") as string, + cancelText: this.$t("Cancel") as string, + type: "is-danger", + hasIcon: true, + onConfirm: () => this.deleteGroup(), + }); + } + + async deleteGroup(): Promise { + await this.$apollo.mutate<{ deleteGroup: IGroup }>({ + mutation: DELETE_GROUP, + variables: { + groupId: this.group.id, + }, + }); + return this.$router.push({ name: RouteName.MY_GROUPS }); + } } diff --git a/js/src/mixins/post.ts b/js/src/mixins/post.ts new file mode 100644 index 000000000..cfd7968e1 --- /dev/null +++ b/js/src/mixins/post.ts @@ -0,0 +1,64 @@ +import { DELETE_POST, FETCH_POST } from "@/graphql/post"; +import { usernameWithDomain } from "@/types/actor"; +import { IPost } from "@/types/post.model"; +import { Component, Vue } from "vue-property-decorator"; +import RouteName from "../router/name"; + +@Component({ + apollo: { + post: { + query: FETCH_POST, + fetchPolicy: "cache-and-network", + variables() { + return { + slug: this.slug, + }; + }, + skip() { + return !this.slug; + }, + error({ graphQLErrors }) { + this.handleErrors(graphQLErrors); + }, + }, + }, +}) +export default class PostMixin extends Vue { + post!: IPost; + + RouteName = RouteName; + + protected async openDeletePostModal(): Promise { + this.$buefy.dialog.confirm({ + type: "is-danger", + title: this.$t("Delete post") as string, + message: this.$t( + "Are you sure you want to delete this post? This action cannot be reverted." + ) as string, + onConfirm: () => this.deletePost(), + }); + } + + async deletePost(): Promise { + const { data } = await this.$apollo.mutate({ + mutation: DELETE_POST, + variables: { + id: this.post.id, + }, + }); + if (data && this.post.attributedTo) { + this.$router.push({ + name: RouteName.POSTS, + params: { + preferredUsername: usernameWithDomain(this.post.attributedTo), + }, + }); + } + } + + handleErrors(errors: any[]): void { + if (errors.some((error) => error.status_code === 404)) { + this.$router.replace({ name: RouteName.PAGE_NOT_FOUND }); + } + } +} diff --git a/js/src/router/event.ts b/js/src/router/event.ts index 633f5698b..1851b9e0d 100644 --- a/js/src/router/event.ts +++ b/js/src/router/event.ts @@ -52,6 +52,7 @@ export const eventRoutes: RouteConfig[] = [ path: "/events/me", name: EventRouteName.MY_EVENTS, component: myEvents, + props: true, meta: { requiredAuth: true, announcer: { message: (): string => i18n.t("My events") as string }, diff --git a/js/src/styles/_event-card.scss b/js/src/styles/_event-card.scss new file mode 100644 index 000000000..f657bc47d --- /dev/null +++ b/js/src/styles/_event-card.scss @@ -0,0 +1,18 @@ +.event-organizer { + display: flex; + align-items: center; + + .organizer-name { + padding-left: 5px; + font-weight: 600; + } +} + +.event-subtitle { + display: flex; + align-items: center; + + & > span:not(.icon) { + padding-left: 5px; + } +} diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index e96c2601a..6f66e085b 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -259,7 +259,7 @@ export function toEditJSON(event: IEditableEvent): IEventEditJSON { } export function organizer(event: IEvent): IActor | null { - if (event.attributedTo) { + if (event.attributedTo?.id) { return event.attributedTo; } if (event.organizerActor) { diff --git a/js/src/variables.scss b/js/src/variables.scss index 02942a8c1..46c03bcbb 100644 --- a/js/src/variables.scss +++ b/js/src/variables.scss @@ -53,7 +53,7 @@ $success: #0d8758; $success-invert: findColorInvert($success); $info: #36bcd4; $info-invert: findColorInvert($info); -$danger: #ff2e54; +$danger: #cd2026; $danger-invert: findColorInvert($danger); $link: $primary; $link-invert: $primary-invert; diff --git a/js/src/views/Event/Edit.vue b/js/src/views/Event/Edit.vue index ddf96e3a8..341d5212c 100644 --- a/js/src/views/Event/Edit.vue +++ b/js/src/views/Event/Edit.vue @@ -606,7 +606,6 @@ import { CURRENT_ACTOR_CLIENT, IDENTITIES, LOGGED_USER_DRAFTS, - LOGGED_USER_PARTICIPATIONS, PERSON_STATUS_GROUP, } from "../../graphql/actor"; import { @@ -635,6 +634,7 @@ import { IEventOptions } from "@/types/event-options.model"; import { USER_SETTINGS } from "@/graphql/user"; import { IUser } from "@/types/current-user.model"; import { IAddress } from "@/types/address.model"; +import { LOGGED_USER_PARTICIPATIONS } from "@/graphql/participant"; const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10; diff --git a/js/src/views/Event/GroupEvents.vue b/js/src/views/Event/GroupEvents.vue index 0ca6d74ab..5c3567bed 100644 --- a/js/src/views/Event/GroupEvents.vue +++ b/js/src/views/Event/GroupEvents.vue @@ -52,14 +52,10 @@ {{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }} {{ $t("Past events") }} - - - +
    -
    - - {{ $t("Upcoming") }} - - -
    - {{ month[0] }} - -
    -
    -
    - {{ $t("Load more") }} +
    + + + {{ + showUpcoming ? $t("Upcoming events") : $t("Past events") + }} + + + {{ $t("Drafts") }} + + + {{ + $t("Attending") + }} + + + {{ + $t("From my groups") + }} + +

    + {{ + $tc( + "You have attended {count} events in the past.", + pastParticipations.total, + { + count: pastParticipations.total, + } + ) + }} +

    + + + + +
    +
    +
    +
    -
    -
    -
    - - {{ $t("Drafts") }} - -
    - -
    -
    -
    - - {{ $t("Past events") }} - - -
    - {{ month[0] }} - -
    -
    -
    - +
    +
    + +
    + {{ month[0] }} +
    + + +
    +
    +
    +
    + {{ $t("Load more") }} +
    +
    +
    {{ $t("Load more") }} - -
    -
    -
    -

    - {{ $t("You didn't create or join any event yet.") }} - - {{ $t("create an event") }} - {{ $t("explore the events") }} - + {{ + $t( + "You don't have any upcoming events. Maybe try another filter?" + ) + }}

    + + {{ $t("create an event") }} + {{ $t("explore the events") }} +
    -
    +
    +
    + +
    + {{ month[0] }} + +
    +
    +
    + {{ $t("Load more") }} +
    +
    - + @@ -133,35 +198,47 @@ import { ParticipantRole } from "@/types/enums"; import RouteName from "@/router/name"; import { supportsWebPFormat } from "@/utils/support"; import { IParticipant, Participant } from "../../types/participant.model"; +import { LOGGED_USER_DRAFTS } from "../../graphql/actor"; +import { EventModel, IEvent } from "../../types/event.model"; +import EventParticipationCard from "../../components/Event/EventParticipationCard.vue"; +import MultiEventMinimalistCard from "../../components/Event/MultiEventMinimalistCard.vue"; +import EventMinimalistCard from "../../components/Event/EventMinimalistCard.vue"; +import Subtitle from "../../components/Utils/Subtitle.vue"; import { LOGGED_USER_PARTICIPATIONS, - LOGGED_USER_DRAFTS, -} from "../../graphql/actor"; -import { EventModel, IEvent } from "../../types/event.model"; -import EventListCard from "../../components/Event/EventListCard.vue"; -import EventCard from "../../components/Event/EventCard.vue"; -import Subtitle from "../../components/Utils/Subtitle.vue"; + LOGGED_USER_UPCOMING_EVENTS, +} from "@/graphql/participant"; +import { Paginate } from "@/types/paginate"; + +type Eventable = IParticipant | IEvent; @Component({ components: { Subtitle, - EventCard, - EventListCard, + MultiEventMinimalistCard, + EventParticipationCard, + EventMinimalistCard, }, apollo: { config: CONFIG, - futureParticipations: { - query: LOGGED_USER_PARTICIPATIONS, + userUpcomingEvents: { + query: LOGGED_USER_UPCOMING_EVENTS, fetchPolicy: "cache-and-network", - variables: { - page: 1, - limit: 10, - afterDateTime: new Date().toISOString(), + variables() { + return { + page: 1, + limit: 10, + afterDateTime: this.dateFilter, + }; }, - update: (data) => - data.loggedUser.participations.elements.map( + update(data) { + this.futureParticipations = data.loggedUser.participations.elements.map( (participation: IParticipant) => new Participant(participation) - ), + ); + this.groupEvents = data.loggedUser.followedGroupEvents.elements.map( + ({ event }: { event: IEvent }) => event + ); + }, }, drafts: { query: LOGGED_USER_DRAFTS, @@ -176,15 +253,14 @@ import Subtitle from "../../components/Utils/Subtitle.vue"; pastParticipations: { query: LOGGED_USER_PARTICIPATIONS, fetchPolicy: "cache-and-network", - variables: { - page: 1, - limit: 10, - beforeDateTime: new Date().toISOString(), + variables() { + return { + page: 1, + limit: 10, + beforeDateTime: this.dateFilter, + }; }, - update: (data) => - data.loggedUser.participations.elements.map( - (participation: IParticipant) => new Participant(participation) - ), + update: (data) => data.loggedUser.participations, }, }, metaInfo() { @@ -200,13 +276,89 @@ export default class MyEvents extends Vue { limit = 10; + get showUpcoming(): boolean { + return ((this.$route.query.showUpcoming as string) || "true") === "true"; + } + + set showUpcoming(showUpcoming: boolean) { + this.$router.push({ + name: RouteName.MY_EVENTS, + query: { ...this.$route.query, showUpcoming: showUpcoming.toString() }, + }); + } + + get showDrafts(): boolean { + return ((this.$route.query.showDrafts as string) || "true") === "true"; + } + + set showDrafts(showDrafts: boolean) { + this.$router.push({ + name: RouteName.MY_EVENTS, + query: { ...this.$route.query, showDrafts: showDrafts.toString() }, + }); + } + + get showAttending(): boolean { + return ((this.$route.query.showAttending as string) || "true") === "true"; + } + + set showAttending(showAttending: boolean) { + this.$router.push({ + name: RouteName.MY_EVENTS, + query: { ...this.$route.query, showAttending: showAttending.toString() }, + }); + } + + get showMyGroups(): boolean { + return ((this.$route.query.showMyGroups as string) || "false") === "true"; + } + + set showMyGroups(showMyGroups: boolean) { + this.$router.push({ + name: RouteName.MY_EVENTS, + query: { ...this.$route.query, showMyGroups: showMyGroups.toString() }, + }); + } + + get dateFilter(): Date { + const query = this.$route.query.dateFilter as string; + if (query && /(\d{4}-\d{2}-\d{2})/.test(query)) { + return new Date(`${query}T00:00:00Z`); + } + return new Date(); + } + + set dateFilter(date: Date) { + const pad = (number: number) => { + if (number < 10) { + return "0" + number; + } + return number; + }; + const stringifiedDate = `${date.getFullYear()}-${pad( + date.getMonth() + 1 + )}-${pad(date.getDate())}`; + + if (this.$route.query.dateFilter !== stringifiedDate) { + this.$router.push({ + name: RouteName.MY_EVENTS, + query: { + ...this.$route.query, + dateFilter: stringifiedDate, + }, + }); + } + } + config!: IConfig; futureParticipations: IParticipant[] = []; + groupEvents: IEvent[] = []; + hasMoreFutureParticipations = true; - pastParticipations: IParticipant[] = []; + pastParticipations: Paginate = { elements: [], total: 0 }; hasMorePastParticipations = true; @@ -216,49 +368,68 @@ export default class MyEvents extends Vue { supportsWebPFormat = supportsWebPFormat; - static monthlyParticipations( - participations: IParticipant[], + static monthlyEvents( + elements: Eventable[], revertSort = false - ): Map { - const res = participations.filter( - ({ event, role }) => - event.beginsOn != null && role !== ParticipantRole.REJECTED - ); - if (revertSort) { - res.sort( - (a: IParticipant, b: IParticipant) => - b.event.beginsOn.getTime() - a.event.beginsOn.getTime() - ); - } else { - res.sort( - (a: IParticipant, b: IParticipant) => - a.event.beginsOn.getTime() - b.event.beginsOn.getTime() - ); - } - return res.reduce( - (acc: Map, participation: IParticipant) => { - const month = new Date(participation.event.beginsOn).toLocaleDateString( - undefined, - { - year: "numeric", - month: "long", - } + ): Map { + const res = elements.filter((element: Eventable) => { + if ("role" in element) { + return ( + element.event.beginsOn != null && + element.role !== ParticipantRole.REJECTED ); - const filteredParticipations: IParticipant[] = acc.get(month) || []; - filteredParticipations.push(participation); - acc.set(month, filteredParticipations); - return acc; - }, - new Map() - ); + } + return element.beginsOn != null; + }); + if (revertSort) { + res.sort((a: Eventable, b: Eventable) => { + const aTime = "role" in a ? a.event.beginsOn : a.beginsOn; + const bTime = "role" in b ? b.event.beginsOn : b.beginsOn; + return new Date(bTime).getTime() - new Date(aTime).getTime(); + }); + } else { + res.sort((a: Eventable, b: Eventable) => { + const aTime = "role" in a ? a.event.beginsOn : a.beginsOn; + const bTime = "role" in b ? b.event.beginsOn : b.beginsOn; + return new Date(aTime).getTime() - new Date(bTime).getTime(); + }); + } + return res.reduce((acc: Map, element: Eventable) => { + const month = new Date( + "role" in element ? element.event.beginsOn : element.beginsOn + ).toLocaleDateString(undefined, { + year: "numeric", + month: "long", + }); + const filteredElements: Eventable[] = acc.get(month) || []; + filteredElements.push(element); + acc.set(month, filteredElements); + return acc; + }, new Map()); } - get monthlyFutureParticipations(): Map { - return MyEvents.monthlyParticipations(this.futureParticipations); + get monthlyFutureEvents(): Map { + let eventable = [] as Eventable[]; + if (this.showAttending) { + eventable = [...eventable, ...this.futureParticipations]; + } + if (this.showMyGroups) { + eventable = [...eventable, ...this.groupEvents]; + } + return MyEvents.monthlyEvents(eventable); } - get monthlyPastParticipations(): Map { - return MyEvents.monthlyParticipations(this.pastParticipations, true); + get monthlyPastParticipations(): Map { + return MyEvents.monthlyEvents(this.pastParticipations.elements, true); + } + + monthParticipationsIds(elements: Eventable[]): string[] { + let res = elements.filter((element: Eventable) => { + return "role" in element; + }) as IParticipant[]; + return res.map(({ event }: { event: IEvent }) => { + return event.id as string; + }); } loadMoreFutureParticipations(): void { @@ -287,9 +458,12 @@ export default class MyEvents extends Vue { this.futureParticipations = this.futureParticipations.filter( (participation) => participation.event.id !== eventid ); - this.pastParticipations = this.pastParticipations.filter( - (participation) => participation.event.id !== eventid - ); + this.pastParticipations = { + elements: this.pastParticipations.elements.filter( + (participation) => participation.event.id !== eventid + ), + total: this.pastParticipations.total - 1, + }; } get hideCreateEventButton(): boolean { @@ -300,6 +474,8 @@ export default class MyEvents extends Vue { diff --git a/js/src/views/Group/Group.vue b/js/src/views/Group/Group.vue index a3f6944a2..8e5033df3 100644 --- a/js/src/views/Group/Group.vue +++ b/js/src/views/Group/Group.vue @@ -149,6 +149,7 @@ currentActor.id " @click="joinGroup" + @keyup.enter="joinGroup" type="is-primary" :disabled="previewPublic" >{{ $t("Join group") }}{{ $t("Follow") }}{{ $t("Cancel follow request") }} {{ $t("Share") }} @@ -246,6 +251,7 @@ v-if="!previewPublic && isCurrentActorAGroupMember" aria-role="menuitem" @click="triggerShare()" + @keyup.enter="triggerShare()" > @@ -280,6 +286,7 @@ v-if="ableToReport" aria-role="menuitem" @click="isReportModalActive = true" + @keyup.enter="isReportModalActive = true" > @@ -289,7 +296,8 @@ @@ -401,8 +409,8 @@ class="organized-events-wrapper" v-if="group && group.organizedEvents.total > 0" > - @@ -122,8 +204,6 @@ import { PERSON_MEMBERSHIPS, PERSON_STATUS_GROUP, } from "../../graphql/actor"; -import { FETCH_POST } from "../../graphql/post"; -import { IPost } from "../../types/post.model"; import { usernameWithDomain } from "../../types/actor"; import RouteName from "../../router/name"; import Tag from "../../components/Tag.vue"; @@ -132,9 +212,17 @@ import ActorInline from "../../components/Account/ActorInline.vue"; import { formatDistanceToNowStrict } from "date-fns"; import { CURRENT_USER_CLIENT } from "@/graphql/user"; import { ICurrentUser } from "@/types/current-user.model"; +import { CONFIG } from "@/graphql/config"; +import { IConfig } from "@/types/config.model"; +import SharePostModal from "../../components/Post/SharePostModal.vue"; +import { IReport } from "@/types/report.model"; +import { CREATE_REPORT } from "@/graphql/report"; +import ReportModal from "../../components/Report/ReportModal.vue"; +import PostMixin from "../../mixins/post"; @Component({ apollo: { + config: CONFIG, currentUser: CURRENT_USER_CLIENT, currentActor: CURRENT_ACTOR_CLIENT, memberships: { @@ -150,21 +238,6 @@ import { ICurrentUser } from "@/types/current-user.model"; return !this.currentActor || !this.currentActor.id; }, }, - post: { - query: FETCH_POST, - fetchPolicy: "cache-and-network", - variables() { - return { - slug: this.slug, - }; - }, - skip() { - return !this.slug; - }, - error({ graphQLErrors }) { - this.handleErrors(graphQLErrors); - }, - }, person: { query: PERSON_STATUS_GROUP, fetchPolicy: "cache-and-network", @@ -187,6 +260,8 @@ import { ICurrentUser } from "@/types/current-user.model"; Tag, LazyImageWrapper, ActorInline, + SharePostModal, + ReportModal, }, metaInfo() { return { @@ -200,13 +275,13 @@ import { ICurrentUser } from "@/types/current-user.model"; }; }, }) -export default class Post extends mixins(GroupMixin) { +export default class Post extends mixins(GroupMixin, PostMixin) { @Prop({ required: true, type: String }) slug!: string; - post!: IPost; - memberships!: IMember[]; + config!: IConfig; + RouteName = RouteName; currentUser!: ICurrentUser; @@ -217,11 +292,9 @@ export default class Post extends mixins(GroupMixin) { PostVisibility = PostVisibility; - handleErrors(errors: any[]): void { - if (errors.some((error) => error.status_code === 404)) { - this.$router.replace({ name: RouteName.PAGE_NOT_FOUND }); - } - } + isShareModalActive = false; + + isReportModalActive = false; get isCurrentActorMember(): boolean { if (!this.post.attributedTo || !this.memberships) return false; @@ -236,6 +309,62 @@ export default class Post extends mixins(GroupMixin) { ICurrentUserRole.MODERATOR, ].includes(this.currentUser.role); } + + get ableToReport(): boolean { + return ( + this.config && + (this.currentActor.id != null || this.config.anonymous.reports.allowed) + ); + } + + triggerShare(): void { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-start + if (navigator.share) { + navigator + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + .share({ + title: this.post.title, + url: this.post.url, + }) + .then(() => console.log("Successful share")) + .catch((error: any) => console.log("Error sharing", error)); + } else { + this.isShareModalActive = true; + // send popup + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-end + } + + async reportPost(content: string, forward: boolean): Promise { + this.isReportModalActive = false; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.$refs.reportModal.close(); + const postTitle = this.post.title; + + try { + await this.$apollo.mutate({ + mutation: CREATE_REPORT, + variables: { + postId: this.post.id, + reportedId: this.post.attributedTo?.id, + content, + forward, + }, + }); + this.$notifier.success( + this.$t("Post {eventTitle} reported", { postTitle }) as string + ); + } catch (error) { + console.error(error); + } + } + get groupDomain(): string | undefined | null { + return this.post.attributedTo?.domain; + } } diff --git a/js/src/views/Resources/ResourceFolder.vue b/js/src/views/Resources/ResourceFolder.vue index c830327b4..e8a6a90b9 100644 --- a/js/src/views/Resources/ResourceFolder.vue +++ b/js/src/views/Resources/ResourceFolder.vue @@ -60,11 +60,11 @@ @@ -418,6 +418,10 @@ export default class Resources extends Mixins(ResourceMixin) { return this.filteredPath.slice(-1)[0]; } + get resourceProviders(): IProvider[] { + return this.config?.resourceProviders || []; + } + async createResource(): Promise { if (!this.resource.actor) return; this.modalError = ""; diff --git a/js/tests/unit/specs/components/Post/PostElementItem.spec.ts b/js/tests/unit/specs/components/Post/PostElementItem.spec.ts deleted file mode 100644 index 0ce89f5a9..000000000 --- a/js/tests/unit/specs/components/Post/PostElementItem.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { config, createLocalVue, mount } from "@vue/test-utils"; -import PostElementItem from "@/components/Post/PostElementItem.vue"; -import { formatDateTimeString } from "@/filters/datetime"; -import Buefy from "buefy"; -import VueRouter from "vue-router"; -import { routes } from "@/router"; -import { PostVisibility } from "@/types/enums"; - -const localVue = createLocalVue(); -localVue.use(Buefy); -localVue.use(VueRouter); -const router = new VueRouter({ routes, mode: "history" }); -localVue.filter("formatDateTimeString", formatDateTimeString); -config.mocks.$t = (key: string): string => key; - -const postData = { - id: "1", - slug: "my-blog-post-some-uuid", - title: "My Blog Post", - body: "My content", - insertedAt: "2020-12-02T09:01:20.873Z", - visibility: PostVisibility.PUBLIC, - author: { - preferredUsername: "author", - domain: "remote-domain.tld", - name: "Author", - }, - attributedTo: { - preferredUsername: "my-awesome-group", - domain: null, - name: "My Awesome Group", - }, -}; - -const generateWrapper = ( - customPostData: Record = {}, - isCurrentActorMember = false -) => { - return mount(PostElementItem, { - localVue, - router, - propsData: { - post: { ...postData, ...customPostData }, - isCurrentActorMember, - }, - }); -}; - -describe("PostElementItem", () => { - it("renders post with basic informations", () => { - const wrapper = generateWrapper(); - expect(wrapper.html()).toMatchSnapshot(); - - expect( - wrapper.find("a.post-minimalist-card-wrapper").attributes("href") - ).toBe(`/p/${postData.slug}`); - - expect(wrapper.find(".post-minimalist-title").text()).toContain( - postData.title - ); - expect(wrapper.find(".metadata").text()).toContain( - formatDateTimeString(postData.insertedAt, undefined, false) - ); - - expect(wrapper.find(".metadata small").text()).not.toContain("Public"); - }); - - it("shows the author if actor is a group member", () => { - const wrapper = generateWrapper({}, true); - expect(wrapper.html()).toMatchSnapshot(); - - expect(wrapper.find(".metadata").text()).toContain(`Created by {username}`); - }); - - it("shows the draft tag if post is a draft", () => { - const wrapper = generateWrapper({ draft: true }); - expect(wrapper.html()).toMatchSnapshot(); - - expect(wrapper.findComponent({ name: "b-tag" }).exists()).toBe(true); - }); - - it("tells if the post is public when the actor is a group member", () => { - const wrapper = generateWrapper({}, true); - expect(wrapper.html()).toMatchSnapshot(); - - expect(wrapper.find(".metadata small").text()).toContain("Public"); - }); - - it("tells if the post is accessible only through link", () => { - const wrapper = generateWrapper({ visibility: PostVisibility.UNLISTED }); - expect(wrapper.html()).toMatchSnapshot(); - - expect(wrapper.find(".metadata small").text()).toContain( - "Accessible through link" - ); - }); - - it("tells if the post is accessible only to members", () => { - const wrapper = generateWrapper({ visibility: PostVisibility.PRIVATE }); - expect(wrapper.html()).toMatchSnapshot(); - - expect(wrapper.find(".metadata small").text()).toContain( - "Accessible only to members" - ); - }); -}); diff --git a/js/tests/unit/specs/components/Post/PostListItem.spec.ts b/js/tests/unit/specs/components/Post/PostListItem.spec.ts index cf52cb295..f34b29a87 100644 --- a/js/tests/unit/specs/components/Post/PostListItem.spec.ts +++ b/js/tests/unit/specs/components/Post/PostListItem.spec.ts @@ -4,6 +4,8 @@ import Buefy from "buefy"; import VueRouter from "vue-router"; import { routes } from "@/router"; import { enUS } from "date-fns/locale"; +import { formatDateTimeString } from "@/filters/datetime"; +import { i18n } from "@/utils/i18n"; const localVue = createLocalVue(); localVue.use(Buefy); @@ -20,14 +22,23 @@ const postData = { title: "My Blog Post", body: "My content", insertedAt: "2020-12-02T09:01:20.873Z", + tags: [], }; -const generateWrapper = (customPostData: Record = {}) => { +const generateWrapper = ( + customPostData: Record = {}, + customProps: Record = {} +) => { return mount(PostListItem, { localVue, router, + i18n, propsData: { post: { ...postData, ...customPostData }, + ...customProps, + }, + filters: { + formatDateTimeString, }, }); }; @@ -36,14 +47,40 @@ describe("PostListItem", () => { it("renders post list item with basic informations", () => { const wrapper = generateWrapper(); - // can't use the snapshot feature because of `ago` + expect(wrapper.html()).toMatchSnapshot(); expect( wrapper.find("a.post-minimalist-card-wrapper").attributes("href") ).toBe(`/p/${postData.slug}`); - expect(wrapper.find(".post-minimalist-title").text()).toContain( - postData.title + expect(wrapper.find(".post-minimalist-title").text()).toBe(postData.title); + + expect(wrapper.find(".post-publication-date").text()).toBe("Dec 2, 2020"); + + expect(wrapper.find(".post-publisher").exists()).toBeFalsy(); + }); + + it("renders post list item with tags", () => { + const wrapper = generateWrapper({ + tags: [{ slug: "a-tag", title: "A tag" }], + }); + + expect(wrapper.html()).toMatchSnapshot(); + + expect(wrapper.find(".tags").text()).toContain("A tag"); + + expect(wrapper.find(".post-publisher").exists()).toBeFalsy(); + }); + + it("renders post list item with publisher name", () => { + const wrapper = generateWrapper( + { author: { name: "An author" } }, + { isCurrentActorMember: true } ); + + expect(wrapper.html()).toMatchSnapshot(); + + expect(wrapper.find(".post-publisher").exists()).toBeTruthy(); + expect(wrapper.find(".post-publisher").text()).toContain("An author"); }); }); diff --git a/js/tests/unit/specs/components/Post/__snapshots__/PostElementItem.spec.ts.snap b/js/tests/unit/specs/components/Post/__snapshots__/PostElementItem.spec.ts.snap deleted file mode 100644 index a8d299b71..000000000 --- a/js/tests/unit/specs/components/Post/__snapshots__/PostElementItem.spec.ts.snap +++ /dev/null @@ -1,103 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PostElementItem renders post with basic informations 1`] = ` - -
    -
    -
    -
    -

    My Blog Post

    - -
    -
    -
    -
    -`; - -exports[`PostElementItem shows the author if actor is a group member 1`] = ` - -
    -
    -
    -
    -

    My Blog Post

    - -
    -
    -
    -
    -`; - -exports[`PostElementItem shows the draft tag if post is a draft 1`] = ` - -
    -
    -
    -
    -

    My Blog Post

    - -
    -
    -
    -
    -`; - -exports[`PostElementItem tells if the post is accessible only through link 1`] = ` - -
    -
    -
    -
    -

    My Blog Post

    - -
    -
    -
    -
    -`; - -exports[`PostElementItem tells if the post is accessible only to members 1`] = ` - -
    -
    -
    -
    -

    My Blog Post

    - -
    -
    -
    -
    -`; - -exports[`PostElementItem tells if the post is public when the actor is a group member 1`] = ` - -
    -
    -
    -
    -

    My Blog Post

    - -
    -
    -
    -
    -`; diff --git a/js/tests/unit/specs/components/Post/__snapshots__/PostListItem.spec.ts.snap b/js/tests/unit/specs/components/Post/__snapshots__/PostListItem.spec.ts.snap new file mode 100644 index 000000000..9b86db3f5 --- /dev/null +++ b/js/tests/unit/specs/components/Post/__snapshots__/PostListItem.spec.ts.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PostListItem renders post list item with basic informations 1`] = ` + + +
    +

    My Blog Post

    +

    Dec 2, 2020

    + + +
    +
    +`; + +exports[`PostListItem renders post list item with publisher name 1`] = ` + + +
    +

    My Blog Post

    +

    Dec 2, 2020

    + +

    Published by An author

    +
    +
    +`; + +exports[`PostListItem renders post list item with tags 1`] = ` + + +
    +

    My Blog Post

    +

    Dec 2, 2020

    +
    A tag + +
    + +
    +
    +`; diff --git a/js/vue.config.js b/js/vue.config.js index d55ed6fcd..2298e3d43 100644 --- a/js/vue.config.js +++ b/js/vue.config.js @@ -21,7 +21,9 @@ module.exports = { css: { loaderOptions: { scss: { - additionalData: `@import "@/variables.scss";`, + additionalData: ` + @use "@/variables.scss" as *; + `, sassOptions: { quietDeps: true, },