Introduce group basic federation, event new page and notifications

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-02-18 08:57:00 +01:00
parent 300ef8f245
commit 4144e9ffd0
416 changed files with 32220 additions and 16750 deletions

View File

@@ -1,133 +1,155 @@
<template>
<div class="address-autocomplete">
<b-field expanded>
<template slot="label">
{{ $t('Find an address') }}
<b-button v-if="!gettingLocation" size="is-small" icon-right="map-marker" @click="locateMe" />
<span v-else>{{ $t('Getting location') }}</span>
</template>
<b-autocomplete
:data="addressData"
v-model="queryText"
:placeholder="$t('e.g. 10 Rue Jangot')"
field="fullName"
:loading="isFetching"
@typing="fetchAsyncData"
icon="map-marker"
expanded
@select="updateSelected">
<template slot-scope="{option}">
<b-icon :icon="option.poiInfos.poiIcon.icon" />
<b>{{ option.poiInfos.name }}</b><br />
<small>{{ option.poiInfos.alternativeName }}</small>
</template>
<template slot="empty">
<span v-if="isFetching">{{ $t('Searching') }}</span>
<div v-else-if="queryText.length >= 3" class="is-enabled">
<span>{{ $t('No results for "{queryText}"') }}</span>
<span>{{ $t('You can try another search term or drag and drop the marker on the map', { queryText }) }}</span>
<!-- <p class="control" @click="openNewAddressModal">-->
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
<!-- </p>-->
</div>
</template>
</b-autocomplete>
</b-field>
<div class="map" v-if="selected && selected.geom">
<map-leaflet
:coords="selected.geom"
:marker="{ text: [selected.poiInfos.name, selected.poiInfos.alternativeName], icon: selected.poiInfos.poiIcon.icon}"
:updateDraggableMarkerCallback="reverseGeoCode"
:options="{ zoom: mapDefaultZoom }"
:readOnly="false"
/>
</div>
<!-- <b-modal v-if="selected" :active.sync="addressModalActive" :width="640" has-modal-card scroll="keep">-->
<!-- <div class="modal-card" style="width: auto">-->
<!-- <header class="modal-card-head">-->
<!-- <p class="modal-card-title">{{ $t('Add an address') }}</p>-->
<!-- </header>-->
<!-- <section class="modal-card-body">-->
<!-- <form>-->
<!-- <b-field :label="$t('Name')">-->
<!-- <b-input aria-required="true" required v-model="selected.description" />-->
<!-- </b-field>-->
<!-- <b-field :label="$t('Street')">-->
<!-- <b-input v-model="selected.street" />-->
<!-- </b-field>-->
<!-- <b-field grouped>-->
<!-- <b-field :label="$t('Postal Code')">-->
<!-- <b-input v-model="selected.postalCode" />-->
<!-- </b-field>-->
<!-- <b-field :label="$t('Locality')">-->
<!-- <b-input v-model="selected.locality" />-->
<!-- </b-field>-->
<!-- </b-field>-->
<!-- <b-field grouped>-->
<!-- <b-field :label="$t('Region')">-->
<!-- <b-input v-model="selected.region" />-->
<!-- </b-field>-->
<!-- <b-field :label="$t('Country')">-->
<!-- <b-input v-model="selected.country" />-->
<!-- </b-field>-->
<!-- </b-field>-->
<!-- </form>-->
<!-- </section>-->
<!-- <footer class="modal-card-foot">-->
<!-- <button class="button" type="button" @click="resetPopup()">{{ $t('Clear') }}</button>-->
<!-- </footer>-->
<!-- </div>-->
<!-- </b-modal>-->
<div class="address-autocomplete">
<b-field expanded>
<template slot="label">
{{ $t("Find an address") }}
<b-button
v-if="!gettingLocation"
size="is-small"
icon-right="map-marker"
@click="locateMe"
/>
<span v-else>{{ $t("Getting location") }}</span>
</template>
<b-autocomplete
:data="addressData"
v-model="queryText"
:placeholder="$t('e.g. 10 Rue Jangot')"
field="fullName"
:loading="isFetching"
@typing="fetchAsyncData"
icon="map-marker"
expanded
@select="updateSelected"
>
<template slot-scope="{ option }">
<b-icon :icon="option.poiInfos.poiIcon.icon" />
<b>{{ option.poiInfos.name }}</b
><br />
<small>{{ option.poiInfos.alternativeName }}</small>
</template>
<template slot="empty">
<span v-if="isFetching">{{ $t("Searching") }}</span>
<div v-else-if="queryText.length >= 3" class="is-enabled">
<span>{{ $t('No results for "{queryText}"') }}</span>
<span>{{
$t("You can try another search term or drag and drop the marker on the map", {
queryText,
})
}}</span>
<!-- <p class="control" @click="openNewAddressModal">-->
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
<!-- </p>-->
</div>
</template>
</b-autocomplete>
</b-field>
<div class="map" v-if="selected && selected.geom">
<map-leaflet
:coords="selected.geom"
:marker="{
text: [selected.poiInfos.name, selected.poiInfos.alternativeName],
icon: selected.poiInfos.poiIcon.icon,
}"
:updateDraggableMarkerCallback="reverseGeoCode"
:options="{ zoom: mapDefaultZoom }"
:readOnly="false"
/>
</div>
<!-- <b-modal v-if="selected" :active.sync="addressModalActive" :width="640" has-modal-card scroll="keep">-->
<!-- <div class="modal-card" style="width: auto">-->
<!-- <header class="modal-card-head">-->
<!-- <p class="modal-card-title">{{ $t('Add an address') }}</p>-->
<!-- </header>-->
<!-- <section class="modal-card-body">-->
<!-- <form>-->
<!-- <b-field :label="$t('Name')">-->
<!-- <b-input aria-required="true" required v-model="selected.description" />-->
<!-- </b-field>-->
<!-- <b-field :label="$t('Street')">-->
<!-- <b-input v-model="selected.street" />-->
<!-- </b-field>-->
<!-- <b-field grouped>-->
<!-- <b-field :label="$t('Postal Code')">-->
<!-- <b-input v-model="selected.postalCode" />-->
<!-- </b-field>-->
<!-- <b-field :label="$t('Locality')">-->
<!-- <b-input v-model="selected.locality" />-->
<!-- </b-field>-->
<!-- </b-field>-->
<!-- <b-field grouped>-->
<!-- <b-field :label="$t('Region')">-->
<!-- <b-input v-model="selected.region" />-->
<!-- </b-field>-->
<!-- <b-field :label="$t('Country')">-->
<!-- <b-input v-model="selected.country" />-->
<!-- </b-field>-->
<!-- </b-field>-->
<!-- </form>-->
<!-- </section>-->
<!-- <footer class="modal-card-foot">-->
<!-- <button class="button" type="button" @click="resetPopup()">{{ $t('Clear') }}</button>-->
<!-- </footer>-->
<!-- </div>-->
<!-- </b-modal>-->
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { Address, IAddress } from '@/types/address.model';
import { ADDRESS, REVERSE_GEOCODE } from '@/graphql/address';
import { Modal } from 'buefy/dist/components/dialog';
import { LatLng } from 'leaflet';
import { debounce } from 'lodash';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { LatLng } from "leaflet";
import { debounce } from "lodash";
import { Address, IAddress } from "../../types/address.model";
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
import { CONFIG } from "../../graphql/config";
import { IConfig } from "../../types/config.model";
@Component({
components: {
'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'),
Modal,
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
},
apollo: {
config: CONFIG,
},
})
export default class AddressAutoComplete extends Vue {
@Prop({ required: true }) value!: IAddress;
addressData: IAddress[] = [];
selected: IAddress = new Address();
isFetching: boolean = false;
queryText: string = (this.value && (new Address(this.value)).fullName) || '';
addressModalActive: boolean = false;
private gettingLocation: boolean = false;
isFetching = false;
queryText: string = (this.value && new Address(this.value).fullName) || "";
addressModalActive = false;
private gettingLocation = false;
private location!: Position;
private gettingLocationError: any;
private mapDefaultZoom: number = 15;
private mapDefaultZoom = 15;
config!: IConfig;
// We put this in data because of issues like https://github.com/vuejs/vue-class-component/issues/263
fetchAsyncData!: Function;
// We put this in data because of issues like
// https://github.com/vuejs/vue-class-component/issues/263
data() {
return {
fetchAsyncData: debounce(this.asyncData, 200),
};
}
async asyncData(query: String) {
async asyncData(query: string) {
if (!query.length) {
this.addressData = [];
this.selected = new Address();
@@ -142,27 +164,27 @@ export default class AddressAutoComplete extends Vue {
this.isFetching = true;
const result = await this.$apollo.query({
query: ADDRESS,
fetchPolicy: 'network-only',
fetchPolicy: "network-only",
variables: {
query,
locale: this.$i18n.locale,
},
});
this.addressData = result.data.searchAddress.map(address => new Address(address));
this.addressData = result.data.searchAddress.map((address: IAddress) => new Address(address));
this.isFetching = false;
}
@Watch('config')
watchConfig(config: IConfig) {
@Watch("config")
watchConfig(config: IConfig) {
if (!config.geocoding.autocomplete) {
// If autocomplete is disabled, we put a larger debounce value so that we don't request with incomplete address
// @ts-ignore
// If autocomplete is disabled, we put a larger debounce value
// so that we don't request with incomplete address
this.fetchAsyncData = debounce(this.asyncData, 2000);
}
}
@Watch('value')
@Watch("value")
updateEditing() {
if (!(this.value && this.value.id)) return;
this.selected = this.value;
@@ -170,10 +192,10 @@ export default class AddressAutoComplete extends Vue {
this.queryText = `${address.poiInfos.name} ${address.poiInfos.alternativeName}`;
}
updateSelected(option) {
updateSelected(option: IAddress) {
if (option == null) return;
this.selected = option;
this.$emit('input', this.selected);
this.$emit("input", this.selected);
}
resetPopup() {
@@ -185,8 +207,8 @@ export default class AddressAutoComplete extends Vue {
this.addressModalActive = true;
}
async reverseGeoCode(e: LatLng, zoom: Number) {
// If the position has been updated through autocomplete selection, no need to geocode it !
async reverseGeoCode(e: LatLng, zoom: number) {
// If the position has been updated through autocomplete selection, no need to geocode it!
if (this.checkCurrentPosition(e)) return;
const result = await this.$apollo.query({
query: REVERSE_GEOCODE,
@@ -198,74 +220,77 @@ export default class AddressAutoComplete extends Vue {
},
});
this.addressData = result.data.reverseGeocode.map(address => new Address(address));
this.addressData = result.data.reverseGeocode.map((address: IAddress) => new Address(address));
const defaultAddress = new Address(this.addressData[0]);
this.selected = defaultAddress;
this.$emit('input', this.selected);
this.$emit("input", this.selected);
this.queryText = `${defaultAddress.poiInfos.name} ${defaultAddress.poiInfos.alternativeName}`;
}
checkCurrentPosition(e: LatLng) {
if (!this.selected || !this.selected.geom) return false;
const lat = parseFloat(this.selected.geom.split(';')[1]);
const lon = parseFloat(this.selected.geom.split(';')[0]);
const lat = parseFloat(this.selected.geom.split(";")[1]);
const lon = parseFloat(this.selected.geom.split(";")[0]);
return e.lat === lat && e.lng === lon;
}
async locateMe(): Promise<void> {
this.gettingLocation = true;
try {
this.gettingLocation = false;
this.location = await this.getLocation();
this.location = await AddressAutoComplete.getLocation();
this.mapDefaultZoom = 12;
this.reverseGeoCode(new LatLng(this.location.coords.latitude, this.location.coords.longitude), 12);
this.reverseGeoCode(
new LatLng(this.location.coords.latitude, this.location.coords.longitude),
12
);
} catch (e) {
this.gettingLocation = false;
this.gettingLocationError = e.message;
}
}
async getLocation(): Promise<Position> {
static async getLocation(): Promise<Position> {
return new Promise((resolve, reject) => {
if (!('geolocation' in navigator)) {
reject(new Error('Geolocation is not available.'));
if (!("geolocation" in navigator)) {
reject(new Error("Geolocation is not available."));
}
navigator.geolocation.getCurrentPosition(pos => {
resolve(pos);
}, err => {
reject(err);
});
navigator.geolocation.getCurrentPosition(
(pos) => {
resolve(pos);
},
(err) => {
reject(err);
}
);
});
}
}
</script>
<style lang="scss">
.address-autocomplete {
margin-bottom: 0.75rem;
}
.address-autocomplete {
margin-bottom: 0.75rem;
}
.autocomplete {
.dropdown-menu {
z-index: 2000;
}
.autocomplete {
.dropdown-menu {
z-index: 2000;
}
.dropdown-item.is-disabled {
opacity: 1 !important;
cursor: auto;
}
}
.dropdown-item.is-disabled {
opacity: 1 !important;
cursor: auto;
}
}
.read-only {
cursor: pointer;
}
.read-only {
cursor: pointer;
}
.map {
height: 400px;
width: 100%;
}
.map {
height: 400px;
width: 100%;
}
</style>

View File

@@ -12,13 +12,13 @@
</docs>
<template>
<time class="datetime-container" :datetime="dateObj.getUTCSeconds()">
<span class="month">{{ month }}</span>
<span class="day">{{ day }}</span>
</time>
<time class="datetime-container" :datetime="dateObj.getUTCSeconds()">
<span class="month">{{ month }}</span>
<span class="day">{{ day }}</span>
</time>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class DateCalendarIcon extends Vue {
@@ -32,44 +32,44 @@ export default class DateCalendarIcon extends Vue {
}
get month() {
return this.dateObj.toLocaleString(undefined, { month: 'short' });
return this.dateObj.toLocaleString(undefined, { month: "short" });
}
get day() {
return this.dateObj.toLocaleString(undefined, { day: 'numeric' });
return this.dateObj.toLocaleString(undefined, { day: "numeric" });
}
}
</script>
<style lang="scss" scoped>
time.datetime-container {
background: #f6f7f8;
border: 1px solid rgba(46,62,72,.12);
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
/*height: 50px;*/
width: 50px;
padding: 8px;
text-align: center;
time.datetime-container {
background: #f6f7f8;
border: 1px solid rgba(46, 62, 72, 0.12);
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
/*height: 50px;*/
width: 50px;
padding: 8px;
text-align: center;
span {
display: block;
font-weight: 600;
span {
display: block;
font-weight: 600;
&.month {
color: #fa3e3e;
padding: 2px 0;
font-size: 12px;
line-height: 12px;
text-transform: uppercase;
}
&.month {
color: #fa3e3e;
padding: 2px 0;
font-size: 12px;
line-height: 12px;
text-transform: uppercase;
}
&.day {
font-size: 20px;
line-height: 20px;
}
&.day {
font-size: 20px;
line-height: 20px;
}
}
}
</style>

View File

@@ -12,40 +12,41 @@
```
</docs>
<template>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">{{ label }}</label>
</div>
<div class="field-body">
<div class="field is-narrow is-grouped calendar-picker">
<b-datepicker
:day-names="localeShortWeekDayNamesProxy"
:month-names="localeMonthNamesProxy"
:first-day-of-week="parseInt($t('firstDayOfWeek'), 10)"
:min-date="minDatetime"
:max-date="maxDatetime"
v-model="dateWithTime"
:placeholder="$t('Click to select')"
:years-range="[-2,10]"
icon="calendar"
class="is-narrow"
/>
<b-timepicker
placeholder="Type or select a time..."
icon="clock"
v-model="dateWithTime"
:min-time="minDatetime"
:max-time="maxDatetime"
size="is-small"
inline>
</b-timepicker>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">{{ label }}</label>
</div>
<div class="field-body">
<div class="field is-narrow is-grouped calendar-picker">
<b-datepicker
:day-names="localeShortWeekDayNamesProxy"
:month-names="localeMonthNamesProxy"
:first-day-of-week="parseInt($t('firstDayOfWeek'), 10)"
:min-date="minDatetime"
:max-date="maxDatetime"
v-model="dateWithTime"
:placeholder="$t('Click to select')"
:years-range="[-2, 10]"
icon="calendar"
class="is-narrow"
/>
<b-timepicker
placeholder="Type or select a time..."
icon="clock"
v-model="dateWithTime"
:min-time="minDatetime"
:max-time="maxDatetime"
size="is-small"
inline
>
</b-timepicker>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { localeMonthNames, localeShortWeekDayNames } from '@/utils/datetime';
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { localeMonthNames, localeShortWeekDayNames } from "@/utils/datetime";
@Component
export default class DateTimePicker extends Vue {
@@ -58,34 +59,35 @@ export default class DateTimePicker extends Vue {
/**
* What's shown besides the picker
*/
@Prop({ required: false, type: String, default: 'Datetime' }) label!: string;
@Prop({ required: false, type: String, default: "Datetime" }) label!: string;
/**
* The step for the time input
*/
@Prop({ required: false, type: Number, default: 1 }) step!: number;
/**
* Earliest date available for selection
*/
/**
* Earliest date available for selection
*/
@Prop({ required: false, type: Date, default: null }) minDatetime!: Date;
/**
* Latest date available for selection
*/
/**
* Latest date available for selection
*/
@Prop({ required: false, type: Date, default: null }) maxDatetime!: Date;
dateWithTime: Date = this.value;
localeShortWeekDayNamesProxy = localeShortWeekDayNames();
localeMonthNamesProxy = localeMonthNames();
@Watch('value')
@Watch("value")
updateValue() {
this.dateWithTime = this.value;
}
@Watch('dateWithTime')
@Watch("dateWithTime")
updateDateWithTimeWatcher() {
this.updateDateTime();
}
@@ -96,21 +98,21 @@ export default class DateTimePicker extends Vue {
*
* @type {Date}
*/
this.$emit('input', this.dateWithTime);
this.$emit("input", this.dateWithTime);
}
}
</script>
<style lang="scss" scoped>
.timepicker {
/deep/ .dropdown-content {
padding: 0;
}
}
.timepicker {
/deep/ .dropdown-content {
padding: 0;
}
}
.calendar-picker {
/deep/ .dropdown-menu {
z-index: 200;
}
}
.calendar-picker {
/deep/ .dropdown-menu {
z-index: 200;
}
}
</style>

View File

@@ -5,7 +5,7 @@ A simple card for an event
```vue
<EventCard
:event="{
:event="{
title: 'Vue Styleguidist first meetup: learn the basics!',
beginsOn: new Date(),
tags: [
@@ -29,9 +29,16 @@ A simple card for an event
<template>
<router-link class="card" :to="{ name: 'Event', params: { uuid: event.uuid } }">
<div class="card-image">
<figure class="image is-16by9" :style="`background-image: url('${event.picture ? event.picture.url : '/img/mobilizon_default_card.png'}')`">
<figure
class="image is-16by9"
:style="`background-image: url('${
event.picture ? event.picture.url : '/img/mobilizon_default_card.png'
}')`"
>
<div class="tag-container" v-if="event.tags">
<b-tag v-for="tag in event.tags.slice(0, 3)" :key="tag.slug" type="is-light">{{ tag.title }}</b-tag>
<b-tag v-for="tag in event.tags.slice(0, 3)" :key="tag.slug" type="is-light">{{
tag.title
}}</b-tag>
</div>
</figure>
</div>
@@ -43,57 +50,57 @@ A simple card for an event
<div class="media-content">
<p class="event-title">{{ event.title }}</p>
<div class="event-subtitle" v-if="event.physicalAddress">
<!-- <p>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</p>-->
<!-- <p>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</p>-->
<span>
{{ event.physicalAddress.description }}, {{ event.physicalAddress.locality }}
{{ event.physicalAddress.description }}, {{ event.physicalAddress.locality }}
</span>
</div>
</div>
</div>
</div>
<!-- <div class="date-and-title">-->
<!-- <div class="date-component">-->
<!-- <date-calendar-icon v-if="!mergedOptions.hideDate" :date="event.beginsOn" />-->
<!-- </div>-->
<!-- <div class="title-wrapper">-->
<!-- <h4>{{ event.title }}</h4>-->
<!-- <div class="organizer-place-wrapper has-text-grey">-->
<!-- <span>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</span>-->
<!-- ·-->
<!-- <span v-if="event.physicalAddress">-->
<!-- {{ event.physicalAddress.description }}, {{ event.physicalAddress.locality }}-->
<!-- </span>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div v-if="!mergedOptions.hideDetails" class="details">-->
<!-- <div v-if="event.participants.length > 0 &&-->
<!-- mergedOptions.loggedPerson &&-->
<!-- event.participants[0].actor.id === mergedOptions.loggedPerson.id">-->
<!-- <b-tag type="is-info"><translate>Organizer</translate></b-tag>-->
<!-- </div>-->
<!-- <div v-else-if="event.participants.length === 1">-->
<!-- <translate-->
<!-- :translate-params="{name: event.participants[0].actor.preferredUsername}"-->
<!-- >{name} organizes this event</translate>-->
<!-- </div>-->
<!-- <div v-else>-->
<!-- <span v-for="participant in event.participants" :key="participant.actor.uuid">-->
<!-- {{ participant.actor.preferredUsername }}-->
<!-- <span v-if="participant.role === ParticipantRole.CREATOR">(organizer)</span>,-->
<!-- &lt;!&ndash; <translate-->
<!-- :translate-params="{name: participant.actor.preferredUsername}"-->
<!-- >&nbsp;{name} is in,</translate>&ndash;&gt;-->
<!-- </span>-->
<!-- </div>-->
</router-link>
<!-- <div class="date-and-title">-->
<!-- <div class="date-component">-->
<!-- <date-calendar-icon v-if="!mergedOptions.hideDate" :date="event.beginsOn" />-->
<!-- </div>-->
<!-- <div class="title-wrapper">-->
<!-- <h4>{{ event.title }}</h4>-->
<!-- <div class="organizer-place-wrapper has-text-grey">-->
<!-- <span>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</span>-->
<!-- ·-->
<!-- <span v-if="event.physicalAddress">-->
<!-- {{ event.physicalAddress.description }}, {{ event.physicalAddress.locality }}-->
<!-- </span>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div v-if="!mergedOptions.hideDetails" class="details">-->
<!-- <div v-if="event.participants.length > 0 &&-->
<!-- mergedOptions.loggedPerson &&-->
<!-- event.participants[0].actor.id === mergedOptions.loggedPerson.id">-->
<!-- <b-tag type="is-info"><translate>Organizer</translate></b-tag>-->
<!-- </div>-->
<!-- <div v-else-if="event.participants.length === 1">-->
<!-- <translate-->
<!-- :translate-params="{name: event.participants[0].actor.preferredUsername}"-->
<!-- >{name} organizes this event</translate>-->
<!-- </div>-->
<!-- <div v-else>-->
<!-- <span v-for="participant in event.participants" :key="participant.actor.uuid">-->
<!-- {{ participant.actor.preferredUsername }}-->
<!-- <span v-if="participant.role === ParticipantRole.CREATOR">(organizer)</span>,-->
<!-- &lt;!&ndash; <translate-->
<!-- :translate-params="{name: participant.actor.preferredUsername}"-->
<!-- >&nbsp;{name} is in,</translate>&ndash;&gt;-->
<!-- </span>-->
<!-- </div>-->
</router-link>
</template>
<script lang="ts">
import { IEvent, IEventCardOptions, ParticipantRole } from '@/types/event.model';
import { Component, Prop, Vue } from 'vue-property-decorator';
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
import { Actor, Person } from '@/types/actor';
import { IEvent, IEventCardOptions, ParticipantRole } from "@/types/event.model";
import { Component, Prop, Vue } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { Actor, Person } from "@/types/actor";
@Component({
components: {
@@ -102,6 +109,7 @@ import { Actor, Person } from '@/types/actor';
})
export default class EventCard extends Vue {
@Prop({ required: true }) event!: IEvent;
@Prop({ required: false }) options!: IEventCardOptions;
ParticipantRole = ParticipantRole;
@@ -118,102 +126,103 @@ export default class EventCard extends Vue {
}
get actor(): Actor {
return Object.assign(new Person(), this.event.organizerActor || this.mergedOptions.organizerActor);
return Object.assign(
new Person(),
this.event.organizerActor || this.mergedOptions.organizerActor
);
}
}
</script>
<style lang="scss" scoped>
@import "../../variables";
@import "../../variables";
a.card {
display: block;
background: $secondary;
&:hover {
// box-shadow: 0 0 5px 0 rgba(0, 0, 0, 1);
transform: scale(1.01, 1.01);
&:after {
opacity: 1;
}
}
border-radius: 5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
a.card {
display: block;
background: $secondary;
&:hover {
// box-shadow: 0 0 5px 0 rgba(0, 0, 0, 1);
transform: scale(1.01, 1.01);
&:after {
content: "";
border-radius: 5px;
position: absolute;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
}
div.tag-container {
position: absolute;
top: 10px;
right: 0;
margin-right: -3px;
z-index: 10;
max-width: 40%;
span.tag {
margin: 5px auto;
text-overflow: ellipsis;
overflow: hidden;
display: block;
font-size: 1em;
line-height: 1.75em;
}
}
div.card-image {
background: $secondary;
figure.image {
background-size: cover;
background-position: center;
}
}
.card-content {
padding: 0.5rem;
.event-title {
font-size: 1.25rem;
line-height: 1.25rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 2.4rem;
}
.event-subtitle {
font-size: 0.85rem;
display: inline-flex;
flex-wrap: wrap;
span {
width: 15rem;
display: block;
overflow: hidden;
flex-grow: 1;
text-overflow: ellipsis;
white-space: nowrap;
}
}
opacity: 1;
}
}
border-radius: 5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
&:after {
content: "";
border-radius: 5px;
position: absolute;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
}
div.tag-container {
position: absolute;
top: 10px;
right: 0;
margin-right: -3px;
z-index: 10;
max-width: 40%;
span.tag {
margin: 5px auto;
text-overflow: ellipsis;
overflow: hidden;
display: block;
font-size: 1em;
line-height: 1.75em;
}
}
div.card-image {
background: $secondary;
figure.image {
background-size: cover;
background-position: center;
}
}
.card-content {
padding: 0.5rem;
.event-title {
font-size: 1.25rem;
line-height: 1.25rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 2.4rem;
}
.event-subtitle {
font-size: 0.85rem;
display: inline-flex;
flex-wrap: wrap;
span {
width: 15rem;
display: block;
overflow: hidden;
flex-grow: 1;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
</style>

View File

@@ -18,55 +18,87 @@
</docs>
<template>
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString(showStartTime) }}</span>
<span v-else-if="isSameDay() && showStartTime && showEndTime">
{{ $t('On {date} from {startTime} to {endTime}', {date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}) }}
</span>
<span v-else-if="isSameDay() && !showStartTime && showEndTime">
{{ $t('On {date} ending at {endTime}', {date: formatDate(beginsOn), endTime: formatTime(endsOn)}) }}
</span>
<span v-else-if="isSameDay() && showStartTime && !showEndTime">
{{ $t('On {date} starting at {startTime}', {date: formatDate(beginsOn), startTime: formatTime(beginsOn)}) }}
</span>
<span v-else-if="isSameDay()">
{{ $t('On {date}', {date: formatDate(beginsOn)}) }}
</span>
<span v-else-if="endsOn && showStartTime && showEndTime">
{{ $t('From the {startDate} at {startTime} to the {endDate} at {endTime}',
{startDate: formatDate(beginsOn), startTime: formatTime(beginsOn), endDate: formatDate(endsOn), endTime: formatTime(endsOn)}) }}
</span>
<span v-else-if="endsOn && showStartTime">
{{ $t('From the {startDate} at {startTime} to the {endDate}',
{startDate: formatDate(beginsOn), startTime: formatTime(beginsOn), endDate: formatDate(endsOn)}) }}
</span>
<span v-else-if="endsOn">
{{ $t('From the {startDate} to the {endDate}',
{startDate: formatDate(beginsOn), endDate: formatDate(endsOn)}) }}
</span>
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString(showStartTime) }}</span>
<span v-else-if="isSameDay() && showStartTime && showEndTime">
{{
$t("On {date} from {startTime} to {endTime}", {
date: formatDate(beginsOn),
startTime: formatTime(beginsOn),
endTime: formatTime(endsOn),
})
}}
</span>
<span v-else-if="isSameDay() && !showStartTime && showEndTime">
{{
$t("On {date} ending at {endTime}", {
date: formatDate(beginsOn),
endTime: formatTime(endsOn),
})
}}
</span>
<span v-else-if="isSameDay() && showStartTime && !showEndTime">
{{
$t("On {date} starting at {startTime}", {
date: formatDate(beginsOn),
startTime: formatTime(beginsOn),
})
}}
</span>
<span v-else-if="isSameDay()">{{ $t("On {date}", { date: formatDate(beginsOn) }) }}</span>
<span v-else-if="endsOn && showStartTime && showEndTime">
{{
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
startDate: formatDate(beginsOn),
startTime: formatTime(beginsOn),
endDate: formatDate(endsOn),
endTime: formatTime(endsOn),
})
}}
</span>
<span v-else-if="endsOn && showStartTime">
{{
$t("From the {startDate} at {startTime} to the {endDate}", {
startDate: formatDate(beginsOn),
startTime: formatTime(beginsOn),
endDate: formatDate(endsOn),
})
}}
</span>
<span v-else-if="endsOn">
{{
$t("From the {startDate} to the {endDate}", {
startDate: formatDate(beginsOn),
endDate: formatDate(endsOn),
})
}}
</span>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class EventFullDate extends Vue {
@Prop({ required: true }) beginsOn!: string;
@Prop({ required: false }) endsOn!: string;
@Prop({ required: false, default: true }) showStartTime!: boolean;
@Prop({ required: false, default: true }) showEndTime!: boolean;
formatDate(value) {
if (!this.$options.filters) return;
formatDate(value: Date): string | undefined {
if (!this.$options.filters) return undefined;
return this.$options.filters.formatDateString(value);
}
formatTime(value) {
if (!this.$options.filters) return;
formatTime(value: Date): string | undefined {
if (!this.$options.filters) return undefined;
return this.$options.filters.formatTimeString(value);
}
isSameDay() {
const sameDay = ((new Date(this.beginsOn)).toDateString()) === ((new Date(this.endsOn)).toDateString());
return this.endsOn && sameDay;
isSameDay(): boolean {
const sameDay = new Date(this.beginsOn).toDateString() === new Date(this.endsOn).toDateString();
return this.endsOn !== undefined && sameDay;
}
}
</script>

View File

@@ -1,55 +1,3 @@
<docs>
A simple card for a participation (we should rename it)
```vue
<template>
<div>
<EventListCard
:participation="participation"
/>
</div>
</template>
<script>
export default {
data() {
return {
participation: {
event: {
title: 'Vue Styleguidist first meetup: learn the basics!',
id: 5,
uuid: 'some uuid',
beginsOn: new Date(),
organizerActor: {
preferredUsername: 'tcit',
name: 'Some Random Dude',
domain: null,
id: 4,
displayName() { return 'Some random dude' }
},
options: {
maximumAttendeeCapacity: 4
},
participantStats: {
approved: 1,
notApproved: 2
}
},
actor: {
preferredUsername: 'tcit',
name: 'Some Random Dude',
domain: null,
id: 4,
displayName() { return 'Some random dude' }
},
role: 'CREATOR',
}
}
}
}
</script>
```
</docs>
<template>
<article class="box">
<div class="columns">
@@ -58,37 +6,72 @@ export default {
<div class="date-component">
<date-calendar-icon :date="participation.event.beginsOn" />
</div>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }"><h2 class="title">{{participation.event.title }}</h2></router-link>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }">
<h2 class="title">{{ participation.event.title }}</h2>
</router-link>
</div>
<div class="participation-actor has-text-grey">
<span v-if="participation.event.physicalAddress && participation.event.physicalAddress.locality">{{ participation.event.physicalAddress.locality }} - </span>
<span
v-if="
participation.event.physicalAddress && participation.event.physicalAddress.locality
"
>{{ participation.event.physicalAddress.locality }} -</span
>
<span>
<span>{{ $t('Organized by {name}', { name: participation.event.organizerActor.displayName() } ) }}</span>
<span v-if="participation.role === ParticipantRole.PARTICIPANT">{{ $t('Going as {name}', { name: participation.actor.displayName() }) }}</span>
<span>
{{
$t("Organized by {name}", {
name: participation.event.organizerActor.displayName(),
})
}}
</span>
<span v-if="participation.role === ParticipantRole.PARTICIPANT">
{{ $t("Going as {name}", { name: participation.actor.displayName() }) }}
</span>
</span>
</div>
<div class="columns">
<span class="column is-narrow">
<b-icon icon="earth" v-if="participation.event.visibility === EventVisibility.PUBLIC" />
<b-icon icon="lock-open" v-if="participation.event.visibility === EventVisibility.UNLISTED" />
<b-icon
icon="lock-open"
v-if="participation.event.visibility === EventVisibility.UNLISTED"
/>
<b-icon icon="lock" v-if="participation.event.visibility === EventVisibility.PRIVATE" />
</span>
<span class="column is-narrow participant-stats">
<span v-if="participation.event.options.maximumAttendeeCapacity !== 0">
{{ $t('{approved} / {total} seats', {approved: participation.event.participantStats.participant, total: participation.event.options.maximumAttendeeCapacity }) }}
<!-- <b-progress-->
<!-- v-if="participation.event.options.maximumAttendeeCapacity > 0"-->
<!-- size="is-medium"-->
<!-- :value="participation.event.participantStats.participant * 100 / participation.event.options.maximumAttendeeCapacity">-->
<!-- </b-progress>-->
{{
$t("{approved} / {total} seats", {
approved: participation.event.participantStats.participant,
total: participation.event.options.maximumAttendeeCapacity,
})
}}
</span>
<span v-else>
{{ $tc('{count} participants', participation.event.participantStats.participant, { count: participation.event.participantStats.participant })}}
{{
$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, params: { eventId: participation.event.uuid } })">
{{ $tc('{count} requests waiting', participation.event.participantStats.notApproved, { count: participation.event.participantStats.notApproved })}}
<span v-if="participation.event.participantStats.notApproved > 0">
<b-button
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
params: { eventId: participation.event.uuid },
})
"
>
{{
$tc(
"{count} requests waiting",
participation.event.participantStats.notApproved,
{ count: participation.event.participantStats.notApproved }
)
}}
</b-button>
</span>
</span>
@@ -96,56 +79,86 @@ export default {
</div>
<div class="actions column is-narrow">
<ul>
<li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
<li
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
"
>
<b-button
type="is-text"
@click="gotToWithCheck(participation, { name: RouteName.EDIT_EVENT, params: { eventId: participation.event.uuid } })"
icon-left="pencil"
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.EDIT_EVENT,
params: { eventId: participation.event.uuid },
})
"
icon-left="pencil"
>{{ $t("Edit") }}</b-button
>
{{ $t('Edit') }}
</b-button>
</li>
<li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))" @click="openDeleteEventModalWrapper">
<b-button type="is-text" icon-left="delete">
{{ $t('Delete') }}
</b-button>
<li
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
"
@click="openDeleteEventModalWrapper"
>
<b-button type="is-text" icon-left="delete">{{ $t("Delete") }}</b-button>
</li>
<li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
<li
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
"
>
<b-button
type="is-text"
@click="gotToWithCheck(participation, { name: RouteName.PARTICIPATIONS, params: { eventId: participation.event.uuid } })"
icon-left="account-multiple-plus"
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
params: { eventId: participation.event.uuid },
})
"
icon-left="account-multiple-plus"
>{{ $t("Manage participations") }}</b-button
>
{{ $t('Manage participations') }}
</b-button>
</li>
<li>
<b-button
tag="router-link"
icon-left="view-compact"
type="is-text"
:to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }">
{{ $t('View event page') }}
</b-button>
:to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }"
>{{ $t("View event page") }}</b-button
>
</li>
</ul>
</div>
</div>
</article>
</article>
</template>
<script lang="ts">
import { IParticipant, ParticipantRole, EventVisibility, IEventCardOptions } from '@/types/event.model';
import { Component, Prop } from 'vue-property-decorator';
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
import { IPerson } from '@/types/actor';
import { mixins } from 'vue-class-component';
import ActorMixin from '@/mixins/actor';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import EventMixin from '@/mixins/event';
import { RouteName } from '@/router';
import { changeIdentity } from '@/utils/auth';
import { Route } from 'vue-router';
import { Component, Prop } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { mixins } from "vue-class-component";
import { RawLocation } from "vue-router";
import {
IParticipant,
ParticipantRole,
EventVisibility,
IEventCardOptions,
} from "../../types/event.model";
import { IPerson } from "../../types/actor";
import ActorMixin from "../../mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import EventMixin from "../../mixins/event";
import RouteName from "../../router/name";
import { changeIdentity } from "../../utils/auth";
const defaultOptions: IEventCardOptions = {
hideDate: true,
@@ -169,15 +182,19 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
* The participation associated
*/
@Prop({ required: true }) participation!: IParticipant;
/**
* Options are merged with default options
*/
@Prop({ required: false, default: () => defaultOptions }) options!: IEventCardOptions;
@Prop({ required: false, default: () => defaultOptions })
options!: IEventCardOptions;
currentActor!: IPerson;
ParticipantRole = ParticipantRole;
EventVisibility = EventVisibility;
RouteName = RouteName;
get mergedOptions(): IEventCardOptions {
@@ -191,114 +208,116 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
await this.openDeleteEventModal(this.participation.event, this.currentActor);
}
async gotToWithCheck(participation: IParticipant, route: Route) {
async gotToWithCheck(participation: IParticipant, route: RawLocation) {
if (participation.actor.id !== this.currentActor.id && participation.event.organizerActor) {
const organizer = participation.event.organizerActor as IPerson;
await changeIdentity(this.$apollo.provider.defaultClient, organizer);
this.$buefy.notification.open({
message: this.$t('Current identity has been changed to {identityName} in order to manage this event.', {
identityName: organizer.preferredUsername,
}) as string,
type: 'is-info',
position: 'is-bottom-right',
message: this.$t(
"Current identity has been changed to {identityName} in order to manage this event.",
{
identityName: organizer.preferredUsername,
}
) as string,
type: "is-info",
position: "is-bottom-right",
duration: 5000,
});
}
return await this.$router.push(route);
return this.$router.push(route);
}
}
</script>
<style lang="scss" scoped>
@import "../../variables";
@import "../../variables";
article.box {
div.tag-container {
position: absolute;
top: 10px;
right: 0;
margin-right: -5px;
z-index: 10;
max-width: 40%;
article.box {
div.tag-container {
position: absolute;
top: 10px;
right: 0;
margin-right: -5px;
z-index: 10;
max-width: 40%;
span.tag {
margin: 5px auto;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 1);
/*word-break: break-all;*/
text-overflow: ellipsis;
span.tag {
margin: 5px auto;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 1);
/*word-break: break-all;*/
text-overflow: ellipsis;
overflow: hidden;
display: block;
/*text-align: right;*/
font-size: 1em;
/*padding: 0 1px;*/
line-height: 1.75em;
}
}
div.content {
padding: 5px;
.participation-actor span,
.participant-stats span {
padding: 0 5px;
button {
height: auto;
padding-top: 0;
}
}
div.title-wrapper {
display: flex;
align-items: center;
div.date-component {
flex: 0;
margin-right: 16px;
}
.title {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
display: block;
/*text-align: right;*/
font-size: 1em;
/*padding: 0 1px;*/
line-height: 1.75em;
}
}
div.content {
padding: 5px;
.participation-actor span, .participant-stats span {
padding: 0 5px;
button {
height: auto;
padding-top: 0;
}
}
div.title-wrapper {
display: flex;
align-items: center;
div.date-component {
flex: 0;
margin-right: 16px;
}
.title {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 400;
line-height: 1em;
font-size: 1.6em;
padding-bottom: 5px;
margin: auto 0;
}
}
/deep/ progress + .progress-value {
color: lighten($primary, 20%) !important;
font-weight: 400;
line-height: 1em;
font-size: 1.6em;
padding-bottom: 5px;
margin: auto 0;
}
}
.actions {
ul li {
margin: 0 auto;
.is-link {
cursor: pointer;
}
.button.is-text {
text-decoration: none;
/deep/ span:first-child i.mdi::before {
font-size: 24px !important;
}
/deep/ span:last-child {
padding-left: 4px;
}
}
* {
font-size: 0.8rem;
color: $primary;
}
}
/deep/ progress + .progress-value {
color: lighten($primary, 20%) !important;
}
}
.actions {
ul li {
margin: 0 auto;
.is-link {
cursor: pointer;
}
.button.is-text {
text-decoration: none;
/deep/ span:first-child i.mdi::before {
font-size: 24px !important;
}
/deep/ span:last-child {
padding-left: 4px;
}
}
* {
font-size: 0.8rem;
color: $primary;
}
}
}
}
</style>

View File

@@ -1,48 +1,3 @@
<docs>
A simple card for an event
```vue
<template>
<div>
<EventListViewCard
:event="event"
/>
</div>
</template>
<script>
export default {
data() {
return {
event: {
title: 'Vue Styleguidist first meetup: learn the basics!',
id: 5,
uuid: 'some uuid',
beginsOn: new Date(),
organizerActor: {
preferredUsername: 'tcit',
name: 'Some Random Dude',
domain: null,
id: 4,
displayName() {
return 'Some random dude'
}
},
options: {
maximumAttendeeCapacity: 4
},
participantStats: {
approved: 1,
notApproved: 2
}
}
}
}
}
}
</script>
```
</docs>
<template>
<article class="box">
<div class="columns">
@@ -51,12 +6,18 @@ export default {
<div class="date-component">
<date-calendar-icon :date="event.beginsOn" />
</div>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"><h2 class="title">{{ event.title }}</h2></router-link>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }">
<h2 class="title">{{ event.title }}</h2>
</router-link>
</div>
<div class="participation-actor has-text-grey">
<span v-if="event.physicalAddress && event.physicalAddress.locality">{{ event.physicalAddress.locality }}</span>
<span v-if="event.physicalAddress && event.physicalAddress.locality">
{{ event.physicalAddress.locality }}
</span>
<span>
<span>{{ $t('Organized by {name}', { name: event.organizerActor.displayName() } ) }}</span>
<span>
{{ $t("Organized by {name}", { name: event.organizerActor.displayName() }) }}
</span>
</span>
</div>
<div class="columns">
@@ -67,30 +28,44 @@ export default {
</span>
<span class="column is-narrow participant-stats">
<span v-if="event.options.maximumAttendeeCapacity !== 0">
{{ $t('{approved} / {total} seats', {approved: event.participantStats.participant, total: event.options.maximumAttendeeCapacity }) }}
{{
$t("{approved} / {total} seats", {
approved: event.participantStats.participant,
total: event.options.maximumAttendeeCapacity,
})
}}
</span>
<span v-else>
{{ $tc('{count} participants', event.participantStats.participant, { count: event.participantStats.participant })}}
{{
$tc("{count} participants", event.participantStats.participant, {
count: event.participantStats.participant,
})
}}
</span>
</span>
</div>
</div>
</div>
</article>
</article>
</template>
<script lang="ts">
import { IParticipant, ParticipantRole, EventVisibility, IEventCardOptions } from '@/types/event.model';
import { Component, Prop } from 'vue-property-decorator';
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
import { IPerson } from '@/types/actor';
import { mixins } from 'vue-class-component';
import ActorMixin from '@/mixins/actor';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import EventMixin from '@/mixins/event';
import { RouteName } from '@/router';
import { changeIdentity } from '@/utils/auth';
import { Route } from 'vue-router';
import {
IParticipant,
ParticipantRole,
EventVisibility,
IEventCardOptions,
} from "@/types/event.model";
import { Component, Prop } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { IPerson } from "@/types/actor";
import { mixins } from "vue-class-component";
import ActorMixin from "@/mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import EventMixin from "@/mixins/event";
import { changeIdentity } from "@/utils/auth";
import { Route } from "vue-router";
import RouteName from "../../router/name";
const defaultOptions: IEventCardOptions = {
hideDate: true,
@@ -114,58 +89,61 @@ export default class EventListViewCard extends mixins(ActorMixin, EventMixin) {
* The participation associated
*/
@Prop({ required: true }) event!: IParticipant;
/**
* Options are merged with default options
*/
@Prop({ required: false, default: () => defaultOptions }) options!: IEventCardOptions;
@Prop({ required: false, default: () => defaultOptions })
options!: IEventCardOptions;
currentActor!: IPerson;
ParticipantRole = ParticipantRole;
EventVisibility = EventVisibility;
RouteName = RouteName;
EventVisibility = EventVisibility;
RouteName = RouteName;
}
</script>
<style lang="scss" scoped>
@import "../../variables";
@import "../../variables";
article.box {
div.content {
padding: 5px;
article.box {
div.content {
padding: 5px;
.participation-actor span, .participant-stats span {
padding: 0 5px;
.participation-actor span,
.participant-stats span {
padding: 0 5px;
button {
height: auto;
padding-top: 0;
}
button {
height: auto;
padding-top: 0;
}
}
div.title-wrapper {
display: flex;
align-items: center;
div.date-component {
flex: 0;
margin-right: 16px;
}
div.title-wrapper {
display: flex;
align-items: center;
div.date-component {
flex: 0;
margin-right: 16px;
}
.title {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 400;
line-height: 1em;
font-size: 1.6em;
padding-bottom: 5px;
margin: auto 0;
}
.title {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 400;
line-height: 1em;
font-size: 1.6em;
padding-bottom: 5px;
margin: auto 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<div>
<h2>{{ title }}</h2>
<div class="eventMetadataBlock">
<b-icon v-if="icon" :icon="icon" size="is-medium" />
<p :class="{ 'padding-left': icon }">
<slot></slot>
</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class EventMetadataBlock extends Vue {
@Prop({ required: false, type: String }) icon!: string;
@Prop({ required: true, type: String }) title!: string;
}
</script>
<style lang="scss" scoped>
h2 {
font-size: 1.8rem;
font-weight: 500;
color: #f7ba30;
}
div.eventMetadataBlock {
display: flex;
align-items: center;
margin-bottom: 1.75rem;
p {
flex: 1;
&.padding-left {
padding-left: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<router-link
class="event-minimalist-card-wrapper"
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
>
<date-calendar-icon class="calendar-icon" :date="event.beginsOn" />
<div class="title-info-wrapper">
<p class="event-minimalist-title">{{ event.title }}</p>
<p v-if="event.physicalAddress" class="has-text-grey">
{{ event.physicalAddress.description }}
</p>
<p v-else>3 demandes de participation à traiter</p>
</div>
</router-link>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IEvent } from "@/types/event.model";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import RouteName from "../../router/name";
@Component({
components: {
DateCalendarIcon,
},
})
export default class EventMinimalistCard extends Vue {
@Prop({ required: true, type: Object }) event!: IEvent;
RouteName = RouteName;
}
</script>
<style lang="scss" scoped>
.event-minimalist-card-wrapper {
display: flex;
width: 100%;
color: initial;
align-items: flex-start;
.calendar-icon {
margin-right: 1rem;
}
.title-info-wrapper {
flex: 2;
.event-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-size: 1.25rem;
font-weight: 700;
}
}
}
</style>

View File

@@ -24,86 +24,131 @@ A button to set your participation
</docs>
<template>
<div class="participation-button">
<b-dropdown aria-role="list" position="is-bottom-left" v-if="participation && participation.role === ParticipantRole.PARTICIPANT">
<button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="check" />
<template>
<span>{{ $t('I participate') }}</span>
</template>
<b-icon icon="menu-down" />
</button>
<div class="participation-button">
<b-dropdown
aria-role="list"
position="is-bottom-left"
v-if="participation && participation.role === ParticipantRole.PARTICIPANT"
>
<button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="check" />
<template>
<span>{{ $t("I participate") }}</span>
</template>
<b-icon icon="menu-down" />
</button>
<b-dropdown-item :value="false" aria-role="listitem" @click="confirmLeave" class="has-text-danger">
{{ $t('Cancel my participation…')}}
</b-dropdown-item>
</b-dropdown>
<b-dropdown-item
:value="false"
aria-role="listitem"
@click="confirmLeave"
class="has-text-danger"
>{{ $t("Cancel my participation…") }}</b-dropdown-item
>
</b-dropdown>
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
<button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="timer-sand-empty" />
<template>
<span>{{ $t('I participate') }}</span>
</template>
<b-icon icon="menu-down" />
</button>
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
<button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="timer-sand-empty" />
<template>
<span>{{ $t("I participate") }}</span>
</template>
<b-icon icon="menu-down" />
</button>
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
<!-- {{ $t('Change my identity…')}}-->
<!-- </b-dropdown-item>-->
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
<!-- {{ $t('Change my identity…')}}-->
<!-- </b-dropdown-item>-->
<b-dropdown-item :value="false" aria-role="listitem" @click="confirmLeave" class="has-text-danger">
{{ $t('Cancel my participation request…')}}
</b-dropdown-item>
</b-dropdown>
<small>{{ $t('Participation requested!')}}</small><br />
<small>{{ $t('Waiting for organization team approval.')}}</small>
</div>
<div v-else-if="participation && participation.role === ParticipantRole.REJECTED">
<span>{{ $t('Unfortunately, your participation request was rejected by the organizers.')}}</span>
</div>
<b-dropdown aria-role="list" position="is-bottom-left" v-else-if="!participation && currentActor.id">
<button class="button is-primary is-large" type="button" slot="trigger">
<template>
<span>{{ $t('Participate') }}</span>
</template>
<b-icon icon="menu-down" />
</button>
<b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)">
<div class="media">
<div class="media-left">
<figure class="image is-32x32" v-if="currentActor.avatar">
<img class="is-rounded" :src="currentActor.avatar.url" alt="" />
</figure>
</div>
<div class="media-content">
<span>{{ $t('as {identity}', {identity: currentActor.name || `@${currentActor.preferredUsername}` }) }}</span>
</div>
</div>
</b-dropdown-item>
<b-dropdown-item :value="false" aria-role="listitem" @click="joinModal" v-if="identities.length > 1">
{{ $t('with another identity…')}}
</b-dropdown-item>
</b-dropdown>
<b-button tag="router-link" :to="{ name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT, params: { uuid: event.uuid } }" v-else-if="!participation && hasAnonymousParticipationMethods" type="is-primary" size="is-large" native-type="button">{{ $t('Participate') }}</b-button>
<b-button tag="router-link" :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, params: { uuid: event.uuid } }" v-else-if="!currentActor.id" type="is-primary" size="is-large" native-type="button">{{ $t('Participate') }}</b-button>
<b-dropdown-item
:value="false"
aria-role="listitem"
@click="confirmLeave"
class="has-text-danger"
>{{ $t("Cancel my participation request…") }}</b-dropdown-item
>
</b-dropdown>
<small>{{ $t("Participation requested!") }}</small>
<br />
<small>{{ $t("Waiting for organization team approval.") }}</small>
</div>
<div v-else-if="participation && participation.role === ParticipantRole.REJECTED">
<span>
{{ $t("Unfortunately, your participation request was rejected by the organizers.") }}
</span>
</div>
<b-dropdown
aria-role="list"
position="is-bottom-left"
v-else-if="!participation && currentActor.id"
>
<button class="button is-primary is-large" type="button" slot="trigger">
<template>
<span>{{ $t("Participate") }}</span>
</template>
<b-icon icon="menu-down" />
</button>
<b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)">
<div class="media">
<div class="media-left">
<figure class="image is-32x32" v-if="currentActor.avatar">
<img class="is-rounded" :src="currentActor.avatar.url" alt />
</figure>
</div>
<div class="media-content">
<span>
{{
$t("as {identity}", {
identity: currentActor.name || `@${currentActor.preferredUsername}`,
})
}}
</span>
</div>
</div>
</b-dropdown-item>
<b-dropdown-item
:value="false"
aria-role="listitem"
@click="joinModal"
v-if="identities.length > 1"
>{{ $t("with another identity…") }}</b-dropdown-item
>
</b-dropdown>
<b-button
tag="router-link"
:to="{ name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT, params: { uuid: event.uuid } }"
v-else-if="!participation && hasAnonymousParticipationMethods"
type="is-primary"
size="is-large"
native-type="button"
>{{ $t("Participate") }}</b-button
>
<b-button
tag="router-link"
:to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, params: { uuid: event.uuid } }"
v-else-if="!currentActor.id"
type="is-primary"
size="is-large"
native-type="button"
>{{ $t("Participate") }}</b-button
>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { EventJoinOptions, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
import { IPerson, Person } from '@/types/actor';
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from '@/graphql/actor';
import { CURRENT_USER_CLIENT } from '@/graphql/user';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
import { RouteName } from '@/router';
import { Component, Prop, Vue } from "vue-property-decorator";
import { EventJoinOptions, IEvent, IParticipant, ParticipantRole } from "../../types/event.model";
import { IPerson, Person } from "../../types/actor";
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from "../../graphql/actor";
import { CURRENT_USER_CLIENT } from "../../graphql/user";
import { CONFIG } from "../../graphql/config";
import { IConfig } from "../../types/config.model";
import RouteName from "../../router/name";
@Component({
apollo: {
@@ -114,7 +159,8 @@ import { RouteName } from '@/router';
config: CONFIG,
identities: {
query: IDENTITIES,
update: ({ identities }) => identities ? identities.map(identity => new Person(identity)) : [],
update: ({ identities }) =>
identities ? identities.map((identity: IPerson) => new Person(identity)) : [],
skip() {
return this.currentUser.isLoggedIn === false;
},
@@ -123,28 +169,33 @@ import { RouteName } from '@/router';
})
export default class ParticipationButton extends Vue {
@Prop({ required: true }) participation!: IParticipant;
@Prop({ required: true }) event!: IEvent;
@Prop({ required: true }) currentActor!: IPerson;
ParticipantRole = ParticipantRole;
identities: IPerson[] = [];
config!: IConfig;
RouteName = RouteName;
joinEvent(actor: IPerson) {
if (this.event.joinOptions === EventJoinOptions.RESTRICTED) {
this.$emit('joinEventWithConfirmation', actor);
this.$emit("joinEventWithConfirmation", actor);
} else {
this.$emit('joinEvent', actor);
this.$emit("joinEvent", actor);
}
}
joinModal() {
this.$emit('joinModal');
this.$emit("joinModal");
}
confirmLeave() {
this.$emit('confirmLeave');
this.$emit("confirmLeave");
}
get hasAnonymousParticipationMethods(): boolean {
@@ -154,20 +205,20 @@ export default class ParticipationButton extends Vue {
</script>
<style lang="scss" scoped>
.participation-button {
.dropdown {
display: flex;
justify-content: flex-end;
.participation-button {
.dropdown {
display: flex;
justify-content: flex-end;
&.dropdown-disabled button {
opacity: 0.5;
}
}
&.dropdown-disabled button {
opacity: 0.5;
}
}
}
.anonymousParticipationModal {
/deep/ .animation-content {
z-index: 1;
}
}
</style>
.anonymousParticipationModal {
/deep/ .animation-content {
z-index: 1;
}
}
</style>

View File

@@ -1,123 +1,164 @@
<template>
<b-table
:data="data"
ref="queueTable"
detailed
detail-key="id"
:checked-rows.sync="checkedRows"
checkable
:is-row-checkable="row => row.role !== ParticipantRole.CREATOR"
checkbox-position="left"
default-sort="insertedAt"
default-sort-direction="asc"
:show-detail-icon="false"
:loading="this.$apollo.loading"
paginated
backend-pagination
:aria-next-label="$t('Next page')"
:aria-previous-label="$t('Previous page')"
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
:total="total"
:per-page="perPage"
backend-sorting
:default-sort-direction="'desc'"
:default-sort="['insertedAt', 'desc']"
@page-change="page => $emit('page-change', page)"
@sort="(field, order) => $emit('sort', field, order)"
>
<template slot-scope="props">
<b-table-column field="insertedAt" :label="$t('Date')" sortable>
<b-tag type="is-success" class="has-text-centered">{{ props.row.insertedAt | formatDateString }}<br>{{ props.row.insertedAt | formatTimeString }}</b-tag>
</b-table-column>
<b-table-column field="role" :label="$t('Role')" sortable v-if="showRole">
<span v-if="props.row.role === ParticipantRole.CREATOR">
{{ $t('Organizer') }}
</span>
<span v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
{{ $t('Participant') }}
</span>
</b-table-column>
<b-table-column field="actor.preferredUsername" :label="$t('Participant')" sortable>
<article class="media">
<figure class="media-left" v-if="props.row.actor.avatar">
<p class="image is-48x48">
<img :src="props.row.actor.avatar.url" alt="">
</p>
</figure>
<b-icon class="media-left" v-else-if="props.row.actor.preferredUsername === 'anonymous'" size="is-large" icon="incognito" />
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<div class="media-content">
<div class="content">
<span v-if="props.row.actor.preferredUsername !== 'anonymous'">
<span v-if="props.row.actor.name">{{ props.row.actor.name }}</span><br />
<span class="is-size-7 has-text-grey">@{{ props.row.actor.preferredUsername }}</span>
</span>
<span v-else>
{{ $t('Anonymous participant') }}
</span>
</div>
</div>
</article>
</b-table-column>
<b-table-column field="metadata.message" :label="$t('Message')">
<span @click="toggleQueueDetails(props.row)" :class="{ 'ellipsed-message': props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH }" v-if="props.row.metadata && props.row.metadata.message">
{{ props.row.metadata.message | ellipsize }}
</span>
<span v-else class="has-text-grey">
{{ $t('No message') }}
</span>
</b-table-column>
</template>
<template slot="detail" slot-scope="props">
<article v-html="nl2br(props.row.metadata.message)" />
</template>
<template slot="bottom-left" v-if="checkedRows.length > 0">
<div class="buttons">
<b-button @click="acceptParticipants(checkedRows)" type="is-success" v-if="canAcceptParticipants">
{{ $tc('No participant to approve|Approve participant|Approve {number} participants', checkedRows.length, { number: checkedRows.length }) }}
</b-button>
<b-button @click="refuseParticipants(checkedRows)" type="is-danger" v-if="canRefuseParticipants">
{{ $tc('No participant to reject|Reject participant|Reject {number} participants', checkedRows.length, { number: checkedRows.length }) }}
</b-button>
<b-table
:data="data"
ref="queueTable"
detailed
detail-key="id"
:checked-rows.sync="checkedRows"
checkable
:is-row-checkable="(row) => row.role !== ParticipantRole.CREATOR"
checkbox-position="left"
:show-detail-icon="false"
:loading="this.$apollo.loading"
paginated
backend-pagination
:aria-next-label="$t('Next page')"
:aria-previous-label="$t('Previous page')"
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
:total="total"
:per-page="perPage"
backend-sorting
:default-sort-direction="'desc'"
:default-sort="['insertedAt', 'desc']"
@page-change="(page) => $emit('page-change', page)"
@sort="(field, order) => $emit('sort', field, order)"
>
<template slot-scope="props">
<b-table-column field="insertedAt" :label="$t('Date')" sortable>
<b-tag type="is-success" class="has-text-centered"
>{{ props.row.insertedAt | formatDateString }}<br />{{
props.row.insertedAt | formatTimeString
}}</b-tag
>
</b-table-column>
<b-table-column field="role" :label="$t('Role')" sortable v-if="showRole">
<span v-if="props.row.role === ParticipantRole.CREATOR">
{{ $t("Organizer") }}
</span>
<span v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
{{ $t("Participant") }}
</span>
</b-table-column>
<b-table-column field="actor.preferredUsername" :label="$t('Participant')" sortable>
<article class="media">
<figure class="media-left" v-if="props.row.actor.avatar">
<p class="image is-48x48">
<img :src="props.row.actor.avatar.url" alt="" />
</p>
</figure>
<b-icon
class="media-left"
v-else-if="props.row.actor.preferredUsername === 'anonymous'"
size="is-large"
icon="incognito"
/>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<div class="media-content">
<div class="content">
<span v-if="props.row.actor.preferredUsername !== 'anonymous'">
<span v-if="props.row.actor.name">{{ props.row.actor.name }}</span
><br />
<span class="is-size-7 has-text-grey"
>@{{ props.row.actor.preferredUsername }}</span
>
</span>
<span v-else>
{{ $t("Anonymous participant") }}
</span>
</div>
</template>
</b-table>
</div>
</article>
</b-table-column>
<b-table-column field="metadata.message" :label="$t('Message')">
<span
@click="toggleQueueDetails(props.row)"
:class="{
'ellipsed-message': props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH,
}"
v-if="props.row.metadata && props.row.metadata.message"
>
{{ props.row.metadata.message | ellipsize }}
</span>
<span v-else class="has-text-grey">
{{ $t("No message") }}
</span>
</b-table-column>
</template>
<template slot="detail" slot-scope="props">
<article v-html="nl2br(props.row.metadata.message)" />
</template>
<template slot="bottom-left" v-if="checkedRows.length > 0">
<div class="buttons">
<b-button
@click="acceptParticipants(checkedRows)"
type="is-success"
v-if="canAcceptParticipants"
>
{{
$tc(
"No participant to approve|Approve participant|Approve {number} participants",
checkedRows.length,
{ number: checkedRows.length }
)
}}
</b-button>
<b-button
@click="refuseParticipants(checkedRows)"
type="is-danger"
v-if="canRefuseParticipants"
>
{{
$tc(
"No participant to reject|Reject participant|Reject {number} participants",
checkedRows.length,
{ number: checkedRows.length }
)
}}
</b-button>
</div>
</template>
</b-table>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IParticipant, ParticipantRole } from '@/types/event.model';
import { Refs } from '@/shims-vue';
import { nl2br } from '@/utils/html';
import { asyncForEach } from '@/utils/asyncForEach';
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import { IParticipant, ParticipantRole } from "../../types/event.model";
import { nl2br } from "../../utils/html";
import { asyncForEach } from "../../utils/asyncForEach";
const MESSAGE_ELLIPSIS_LENGTH = 130;
@Component({
filters: {
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat('…'),
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat(""),
},
})
export default class ParticipationTable extends Vue {
@Prop({ required: true, type: Array }) data!: IParticipant[];
@Prop({ required: true, type: Number }) total!: number;
@Prop({ required: true, type: Function }) acceptParticipant;
@Prop({ required: true, type: Function }) refuseParticipant;
@Prop({ required: false, type: Boolean, default: false }) showRole;
@Prop({ required: false, type: Number, default: 20 }) perPage;
@Prop({ required: true, type: Function }) acceptParticipant!: Function;
@Prop({ required: true, type: Function }) refuseParticipant!: Function;
@Prop({ required: false, type: Boolean, default: false }) showRole!: boolean;
@Prop({ required: false, type: Number, default: 20 }) perPage!: number;
@Ref("queueTable") readonly queueTable!: any;
checkedRows: IParticipant[] = [];
MESSAGE_ELLIPSIS_LENGTH = MESSAGE_ELLIPSIS_LENGTH;
nl2br = nl2br;
ParticipantRole = ParticipantRole;
$refs!: Refs<{
queueTable: any,
}>;
nl2br = nl2br;
ParticipantRole = ParticipantRole;
toggleQueueDetails(row: IParticipant) {
if (row.metadata.message && row.metadata.message.length < MESSAGE_ELLIPSIS_LENGTH) return;
this.$refs.queueTable.toggleDetails(row);
this.queueTable.toggleDetails(row);
}
async acceptParticipants(participants: IParticipant[]) {
@@ -134,31 +175,33 @@ export default class ParticipationTable extends Vue {
this.checkedRows = [];
}
/**
* We can accept participants if at least one of them is not approved
*/
/**
* We can accept participants if at least one of them is not approved
*/
get canAcceptParticipants(): boolean {
return this.checkedRows.some(
(participant: IParticipant) => [ParticipantRole.NOT_APPROVED, ParticipantRole.REJECTED].includes(participant.role),
return this.checkedRows.some((participant: IParticipant) =>
[ParticipantRole.NOT_APPROVED, ParticipantRole.REJECTED].includes(participant.role)
);
}
/**
* We can refuse participants if at least one of them is something different than not approved
*/
/**
* We can refuse participants if at least one of them is something different than not approved
*/
get canRefuseParticipants(): boolean {
return this.checkedRows.some((participant: IParticipant) => participant.role !== ParticipantRole.REJECTED);
return this.checkedRows.some(
(participant: IParticipant) => participant.role !== ParticipantRole.REJECTED
);
}
}
</script>
<style lang="scss" scoped>
.ellipsed-message {
cursor: pointer;
}
.ellipsed-message {
cursor: pointer;
}
.table {
span.tag {
height: initial;
}
}
</style>
.table {
span.tag {
height: initial;
}
}
</style>

View File

@@ -1,56 +1,33 @@
<docs>
### Tag input
A special input to manage event tags
```vue
<tag-input :value="[{ title: 'toto' }]" path="title" />
```
```vue
<template>
<tag-input v-model="tags" :data="sourceTags" path="title" />
</template>
<script>
export default {
data() {
return {
sourceTags: [{ title: 'my tag'}, { title: 'my second tag' }, { title: 'another example'}],
tags: []
}
}
}
</script>
```
</docs>
<template>
<b-field>
<template slot="label">
{{ $t('Add some tags') }}
<b-tooltip type="is-dark" :label="$t('You can add tags by hitting the Enter key or by adding a comma')">
<b-icon size="is-small" icon="help-circle-outline"></b-icon>
</b-tooltip>
</template>
<b-taginput
v-model="tagsStrings"
:data="filteredTags"
autocomplete
:allow-new="true"
:field="path"
icon="label"
maxlength="20"
maxtags="10"
:placeholder="$t('Eg: Stockholm, Dance, Chess…')"
@typing="getFilteredTags"
>
</b-taginput>
</b-field>
<b-field>
<template slot="label">
{{ $t("Add some tags") }}
<b-tooltip
type="is-dark"
:label="$t('You can add tags by hitting the Enter key or by adding a comma')"
>
<b-icon size="is-small" icon="help-circle-outline"></b-icon>
</b-tooltip>
</template>
<b-taginput
v-model="tagsStrings"
:data="filteredTags"
autocomplete
:allow-new="true"
:field="path"
icon="label"
maxlength="20"
maxtags="10"
:placeholder="$t('Eg: Stockholm, Dance, Chess…')"
@typing="getFilteredTags"
>
</b-taginput>
</b-field>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { get, differenceBy } from 'lodash';
import { ITag } from '@/types/tag.model';
import { Component, Prop, Vue } from "vue-property-decorator";
import { get, differenceBy } from "lodash";
import { ITag } from "../../types/tag.model";
@Component({
computed: {
@@ -59,32 +36,30 @@ import { ITag } from '@/types/tag.model';
return this.$props.value.map((tag: ITag) => tag.title);
},
set(tagStrings) {
const tagEntities = tagStrings.map((tag) => {
if (TagInput.isTag(tag)) {
const tagEntities = tagStrings.map((tag: string | ITag) => {
if (!(tag instanceof String)) {
return tag;
}
return { title: tag, slug: tag } as ITag;
});
this.$emit('input', tagEntities);
this.$emit("input", tagEntities);
},
},
},
})
export default class TagInput extends Vue {
@Prop({ required: false, default: () => [] }) data!: ITag[];
@Prop({ required: true, default: 'value' }) path!: string;
@Prop({ required: true, default: "value" }) path!: string;
@Prop({ required: true }) value!: ITag[];
filteredTags: ITag[] = [];
getFilteredTags(text) {
this.filteredTags = differenceBy(this.data, this.value, 'id').filter((option) => {
return get(option, this.path)
.toString()
.toLowerCase()
.indexOf(text.toLowerCase()) >= 0;
});
getFilteredTags(text: string) {
this.filteredTags = differenceBy(this.data, this.value, "id").filter(
(option) => get(option, this.path).toString().toLowerCase().indexOf(text.toLowerCase()) >= 0
);
}
static isTag(x: any): x is ITag {