Merge branch 'feature/add-styleguidist' into 'master'

Add basic styleguide

Closes #182

See merge request framasoft/mobilizon!222
This commit is contained in:
Thomas Citharel 2019-10-07 13:47:09 +02:00
commit 470e7e3082
22 changed files with 2337 additions and 366 deletions

View File

@ -1,10 +1,8 @@
image: tcitworld/mobilizon-ci image: tcitworld/mobilizon-ci
stages: stages:
- deps - check
- front - test
- back
- e2e
- deploy - deploy
variables: variables:
@ -22,135 +20,67 @@ variables:
GEOLITE_CITIES_PATH: "/usr/share/GeoIP/GeoLite2-City.mmdb" GEOLITE_CITIES_PATH: "/usr/share/GeoIP/GeoLite2-City.mmdb"
MOBILIZON_INSTANCE_REGISTRATIONS_OPEN: "true" MOBILIZON_INSTANCE_REGISTRATIONS_OPEN: "true"
setup_elixir_deps: cache:
stage: deps key: ${CI_COMMIT_REF_SLUG}
paths:
- ~/.cache/Cypress
- build/
- deps/
- js/node_modules
- cache/Cypress
lint:
stage: check
script: script:
- export EXITVALUE=0
- mix deps.get - mix deps.get
- mix compile - mix credo -a || export EXITVALUE=1
cache: - mix format --check-formatted --dry-run || export EXITVALUE=1
paths:
- deps
- _build
setup_js_deps:
stage: deps
before_script:
- cd js
script:
- yarn install
after_script:
- cd ../
cache:
paths:
- js/node_modules
js:
stage: front
before_script:
- cd js - cd js
- yarn install - yarn install
script: - yarn run lint || export EXITVALUE=1
- yarn run build - yarn run build
after_script:
- cd ../ - cd ../
cache: - exit $EXITVALUE
paths:
- js/node_modules
artifacts: artifacts:
expire_in: 1 day
when: on_success
paths: paths:
- priv/static - priv/static
untracked: false
expire_in: 30 days
deps:
js_deps: stage: check
stage: front
before_script:
- cd js
- yarn install
script: script:
- yarn outdated - export EXITVALUE=0
after_script: - mix deps.get
- cd ../ - mix hex.outdated || export EXITVALUE=1
cache: - cd js
paths: - yarn outdated || export EXITVALUE=1
- js/node_modules - exit $EXITVALUE
allow_failure: true allow_failure: true
js_check: exunit:
stage: front stage: test
before_script:
- cd js
- yarn install
script:
- yarn run lint
after_script:
- cd ../
cache:
paths:
- js/node_modules
elixir_check:
stage: back
before_script:
- mix deps.get
script:
- mix credo list
- mix format --check-formatted --dry-run
cache:
paths:
- deps
elixir_deps:
stage: back
before_script:
- mix deps.get
script:
- mix hex.outdated
allow_failure: true
cache:
paths:
- deps
pages:
stage: deploy
before_script:
- mix deps.get
script:
- mix docs
- mv doc public
cache:
paths:
- deps
only:
- master
artifacts:
expire_in: 1 hour
paths:
- public
mix:
stage: back
services: services:
- name: mdillon/postgis:10 - name: mdillon/postgis:11
alias: postgres alias: postgres
dependencies:
- js
before_script: before_script:
- cd js
- yarn install
- yarn run build
- cd ../
- mix deps.get - mix deps.get
- MIX_ENV=test mix ecto.create - MIX_ENV=test mix ecto.create
- MIX_ENV=test mix ecto.migrate - MIX_ENV=test mix ecto.migrate
dependencies:
- lint
script: script:
- mix coveralls - mix coveralls
cache:
paths:
- deps
- _build
e2e: cypress:
stage: e2e stage: test
services: services:
- name: mdillon/postgis:10 - name: mdillon/postgis:11
alias: postgres alias: postgres
script: script:
- mix deps.get - mix deps.get
@ -168,4 +98,23 @@ e2e:
expire_in: 2 day expire_in: 2 day
paths: paths:
- js/tests/e2e/screenshots/**/*.png - js/tests/e2e/screenshots/**/*.png
- js/tests/e2e/videos/**/*.mp4 - js/tests/e2e/videos/**/*.mp4
pages:
stage: deploy
script:
- mkdir public
- mix deps.get
- mix docs
- mv doc public/backend
- cd js
- yarn install
- yarn run styleguide:build
- mv styleguide ../public/frontend
only:
- master
artifacts:
expire_in: 1 hour
paths:
- public

2
js/.gitignore vendored
View File

@ -7,6 +7,8 @@ node_modules
/tests/e2e/videos/ /tests/e2e/videos/
selenium-debug.log selenium-debug.log
styleguide/
# local env files # local env files
.env.local .env.local
.env.*.local .env.*.local

View File

@ -0,0 +1,19 @@
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import Buefy from 'buefy';
import 'bulma/css/bulma.min.css';
import 'buefy/dist/buefy.min.css';
import filters from '@/filters';
Vue.use(VueI18n);
Vue.use(Buefy);
Vue.use(filters);
Vue.component('router-link', {
props: {
tag: { type: String, default: 'a' }
},
render(createElement) {
return createElement(this.tag, {}, this.$slots.default)
}
});

View File

@ -0,0 +1,18 @@
import VueI18n from 'vue-i18n'
import messages from '@/i18n/index';
const language = (window.navigator).userLanguage || window.navigator.language;
const i18n = new VueI18n({
locale: language.replace('-', '_'), // set locale
messages, // set locale messages
});
export default previewComponent => {
// https://vuejs.org/v2/guide/render-function.html
return {
i18n,
render(createElement) {
return createElement(previewComponent)
}
}
}

0
js/docs/components.md Normal file
View File

1
js/docs/directives.md Normal file
View File

@ -0,0 +1 @@
No directives right now.

3
js/docs/index.md Normal file
View File

@ -0,0 +1,3 @@
# Introduction
This page presents the various Vue.js components used in the front-end for Mobilizon.

View File

@ -9,6 +9,8 @@
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint",
"analyze-bundle": "yarn run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json", "analyze-bundle": "yarn run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json",
"dev": "vue-cli-service build --watch", "dev": "vue-cli-service build --watch",
"styleguide": "vue-cli-service styleguidist",
"styleguide:build": "vue-cli-service styleguidist:build",
"vue-i18n-extract": "vue-i18n-extract" "vue-i18n-extract": "vue-i18n-extract"
}, },
"dependencies": { "dependencies": {
@ -61,6 +63,7 @@
"tslint": "^5.20.0", "tslint": "^5.20.0",
"tslint-config-airbnb": "^5.11.2", "tslint-config-airbnb": "^5.11.2",
"typescript": "^3.6.3", "typescript": "^3.6.3",
"vue-cli-plugin-styleguidist": "^3.24.2",
"vue-cli-plugin-webpack-bundle-analyzer": "^1.3.0", "vue-cli-plugin-webpack-bundle-analyzer": "^1.3.0",
"vue-i18n-extract": "^1.0.2", "vue-i18n-extract": "^1.0.2",
"vue-svg-inline-loader": "^1.3.0", "vue-svg-inline-loader": "^1.3.0",

View File

@ -1,11 +1,63 @@
<docs>
A simple link to an actor, local or remote link
```vue
<template>
<ActorLink :actor="localActor">
<template>
<span>{{ localActor.preferredUsername }}</span>
</template>
</ActorLink>
</template>
<script>
export default {
data() {
return {
localActor: {
domain: null,
preferredUsername: 'localActor'
},
}
}
}
</script>
```
```vue
<template>
<ActorLink :actor="remoteActor">
<template>
<span>{{ remoteActor.preferredUsername }}</span>
</template>
</ActorLink>
</template>
<script>
export default {
data() {
return {
remoteActor: {
domain: 'mobilizon.org',
url: 'https://mobilizon.org/@Framasoft',
preferredUsername: 'Framasoft'
},
}
}
}
</script>
```
</docs>
<template> <template>
<span> <span>
<span v-if="actor.domain === null" <span v-if="actor.domain === null"
:to="{name: 'Profile', params: { name: actor.preferredUsername } }" :to="{name: 'Profile', params: { name: actor.preferredUsername } }"
> >
<!-- @slot What to put inside the link -->
<slot></slot> <slot></slot>
</span> </span>
<a v-else :href="actor.url"> <a v-else :href="actor.url">
<!-- @slot What to put inside the link -->
<slot></slot> <slot></slot>
</a> </a>
</span> </span>
@ -16,6 +68,9 @@ import { IActor } from '@/types/actor';
@Component @Component
export default class ActorLink extends Vue { export default class ActorLink extends Vue {
@Prop() actor!: IActor; /**
* The actor you want to make a link to
*/
@Prop({ required: true }) actor!: IActor;
} }
</script> </script>

View File

@ -29,30 +29,6 @@
</section> </section>
</template> </template>
<style lang="scss" scoped>
.identities {
border-right: 1px solid grey;
padding: 15px 0;
}
.media.identity {
align-items: center;
font-size: 1.3rem;
padding-bottom: 0;
margin-bottom: 15px;
color: #000;
&.is-current-identity {
background-color: rgba(0, 0, 0, 0.1);
}
}
.title {
margin-bottom: 30px;
}
</style>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { IDENTITIES } from '@/graphql/actor'; import { IDENTITIES } from '@/graphql/actor';
@ -80,3 +56,27 @@ export default class Identities extends Vue {
} }
} }
</script> </script>
<style lang="scss" scoped>
.identities {
border-right: 1px solid grey;
padding: 15px 0;
}
.media.identity {
align-items: center;
font-size: 1.3rem;
padding-bottom: 0;
margin-bottom: 15px;
color: #000;
&.is-current-identity {
background-color: rgba(0, 0, 0, 0.1);
}
}
.title {
margin-bottom: 30px;
}
</style>

View File

@ -1,3 +1,20 @@
<docs>
```vue
<participant-card :participant="{ actor: { preferredUsername: 'user1', name: 'someoneIDontLike' }, role: 'REJECTED' }" />
```
```vue
<participant-card :participant="{ actor: { preferredUsername: 'user2', name: 'someoneWhoWillWait' }, role: 'NOT_APPROVED' }" />
```
```vue
<participant-card :participant="{ actor: { preferredUsername: 'user3', name: 'a_participant' }, role: 'PARTICIPANT' }" />
```
```vue
<participant-card :participant="{ actor: { preferredUsername: 'me', name: 'myself' }, role: 'CREATOR' }" />
```
</docs>
<template> <template>
<article class="card"> <article class="card">
<div class="card-content"> <div class="card-content">
@ -28,7 +45,7 @@ import { IActor, IPerson, Person } from '@/types/actor';
import { IParticipant, ParticipantRole } from '@/types/event.model'; import { IParticipant, ParticipantRole } from '@/types/event.model';
@Component @Component
export default class ActorCard extends Vue { export default class ParticipantCard extends Vue {
@Prop({ required: true }) participant!: IParticipant; @Prop({ required: true }) participant!: IParticipant;
@Prop({ type: Function }) accept; @Prop({ type: Function }) accept;
@Prop({ type: Function }) reject; @Prop({ type: Function }) reject;

View File

@ -1,3 +1,16 @@
<docs>
### Example
```vue
<DateCalendarIcon date="2019-10-05T18:41:11.720Z" />
```
```vue
<DateCalendarIcon
:date="new Date()"
/>
```
</docs>
<template> <template>
<time class="datetime-container" :datetime="dateObj.getUTCSeconds()"> <time class="datetime-container" :datetime="dateObj.getUTCSeconds()">
<span class="month">{{ month }}</span> <span class="month">{{ month }}</span>
@ -9,6 +22,9 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
@Component @Component
export default class DateCalendarIcon extends Vue { export default class DateCalendarIcon extends Vue {
/**
* `date` can be a string or an actual date object.
*/
@Prop({ required: true }) date!: string; @Prop({ required: true }) date!: string;
get dateObj() { get dateObj() {

View File

@ -1,3 +1,16 @@
<docs>
### Datetime Picker
> We're wrapping the Buefy datepicker & an input
### Defaults
- step: 10
### Example
```vue
<DateTimePicker :value="new Date()" />
```
</docs>
<template> <template>
<b-field grouped horizontal :label="label"> <b-field grouped horizontal :label="label">
<b-datepicker expanded v-model="date" :placeholder="$t('Click to select')" icon="calendar"></b-datepicker> <b-datepicker expanded v-model="date" :placeholder="$t('Click to select')" icon="calendar"></b-datepicker>
@ -8,8 +21,20 @@
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
@Component @Component
export default class DateTimePicker extends Vue { export default class DateTimePicker extends Vue {
@Prop({ required: true, type: Date }) value!: Date; /**
* @model
* The DateTime value
*/
@Prop({ required: true, type: Date, default: () => new Date() }) value!: Date;
/**
* 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; @Prop({ required: false, type: Number, default: 1 }) step!: number;
date: Date = this.value; date: Date = this.value;
@ -23,19 +48,24 @@ export default class DateTimePicker extends Vue {
} }
@Watch('time') @Watch('time')
updateTime(time) { updateTime(time: string) {
const [hours, minutes] = time.split(':', 2); const [hours, minutes] = time.split(':', 2);
this.date.setHours(hours); this.date.setHours(parseInt(hours, 10));
this.date.setMinutes(minutes); this.date.setMinutes(parseInt(minutes, 10));
this.updateDateTime(); this.updateDateTime();
} }
@Watch('date') @Watch('date')
updateDate() { updateDate() {
this.updateDateTime(); this.updateDateTime();
} }
updateDateTime() { updateDateTime() {
/**
* Returns the updated date
*
* @type {DateTime}
*/
this.$emit('input', this.date); this.$emit('input', this.date);
} }
} }

View File

@ -1,3 +1,31 @@
<docs>
### EventCard
A simple card for an event
```vue
<EventCard
:event="{
title: 'Vue Styleguidist first meetup: learn the basics!',
beginsOn: new Date(),
tags: [
{
slug: 'test', title: 'Test'
},
{
slug: 'mobilizon', title: 'Mobilizon'
},
],
organizerActor: {
preferredUsername: 'tcit',
name: 'Some Random Dude',
domain: null
}
}"
/>
```
</docs>
<template> <template>
<router-link class="card" :to="{ name: 'Event', params: { uuid: event.uuid } }"> <router-link class="card" :to="{ name: 'Event', params: { uuid: event.uuid } }">
<div class="card-image" v-if="!event.image"> <div class="card-image" v-if="!event.image">

View File

@ -1,3 +1,22 @@
<docs>
#### Give a translated and localized text that give the starting and ending datetime for an event.
##### Start date with no ending
```vue
<EventFullDate beginsOn="2015-10-06T18:41:11.720Z" />
```
##### Start date with an ending the same day
```vue
<EventFullDate beginsOn="2015-10-06T18:41:11.720Z" endsOn="2015-10-06T20:41:11.720Z" />
```
##### Start date with an ending on a different day
```vue
<EventFullDate beginsOn="2015-10-06T18:41:11.720Z" endsOn="2032-10-06T18:41:11.720Z" />
```
</docs>
<template> <template>
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString }}</span> <span v-if="!endsOn">{{ beginsOn | formatDateTimeString }}</span>
<span v-else-if="isSameDay()"> <span v-else-if="isSameDay()">

View File

@ -1,3 +1,55 @@
<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,
unapproved: 2
}
},
actor: {
preferredUsername: 'tcit',
name: 'Some Random Dude',
domain: null,
id: 4,
displayName() { return 'Some random dude' }
},
role: 'CREATOR',
}
}
}
}
</script>
```
</docs>
<template> <template>
<article class="box"> <article class="box">
<div class="title-wrapper"> <div class="title-wrapper">
@ -79,6 +131,13 @@ import { ICurrentUser } from '@/types/current-user.model';
import { IEventCardOptions } from './EventCard.vue'; import { IEventCardOptions } from './EventCard.vue';
const lineClamp = require('line-clamp'); const lineClamp = require('line-clamp');
const defaultOptions: IEventCardOptions = {
hideDate: true,
loggedPerson: false,
hideDetails: false,
organizerActor: null,
};
@Component({ @Component({
components: { components: {
DateCalendarIcon, DateCalendarIcon,
@ -93,8 +152,14 @@ const lineClamp = require('line-clamp');
}, },
}) })
export default class EventListCard extends mixins(ActorMixin, EventMixin) { export default class EventListCard extends mixins(ActorMixin, EventMixin) {
/**
* The participation associated
*/
@Prop({ required: true }) participation!: IParticipant; @Prop({ required: true }) participation!: IParticipant;
@Prop({ required: false }) options!: IEventCardOptions; /**
* Options are merged with default options
*/
@Prop({ required: false, default: () => defaultOptions }) options!: IEventCardOptions;
currentActor!: IPerson; currentActor!: IPerson;
@ -102,15 +167,8 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
EventVisibility = EventVisibility; EventVisibility = EventVisibility;
RouteName = RouteName; RouteName = RouteName;
defaultOptions: IEventCardOptions = {
hideDate: true,
loggedPerson: false,
hideDetails: false,
organizerActor: null,
};
get mergedOptions(): IEventCardOptions { get mergedOptions(): IEventCardOptions {
return { ...this.defaultOptions, ...this.options }; return { ...defaultOptions, ...this.options };
} }
/** /**

View File

@ -1,3 +1,27 @@
<docs>
A button to set your participation
##### If the participant has been confirmed
```vue
<ParticipationButton :participation="{ role: 'PARTICIPANT' }" :currentActor="{ preferredUsername: 'test', avatar: { url: 'https://huit.re/EPX7vs1j' } }" />
```
##### If the participant has not being approved yet
```vue
<ParticipationButton :participation="{ role: 'NOT_APPROVED' }" :currentActor="{ preferredUsername: 'test', avatar: { url: 'https://huit.re/EPX7vs1j' } }" />
```
##### If the participant has been rejected
```vue
<ParticipationButton :participation="{ role: 'REJECTED' }" :currentActor="{ preferredUsername: 'test', avatar: { url: 'https://huit.re/EPX7vs1j' } }" />
```
##### If the participant doesn't exist yet
```vue
<ParticipationButton :participation="null" :currentActor="{ preferredUsername: 'test', avatar: { url: 'https://huit.re/EPX7vs1j' } }" />
```
</docs>
<template> <template>
<div class="participation-button"> <div class="participation-button">
<b-dropdown aria-role="list" position="is-bottom-left" v-if="participation && participation.role === ParticipantRole.PARTICIPANT"> <b-dropdown aria-role="list" position="is-bottom-left" v-if="participation && participation.role === ParticipantRole.PARTICIPANT">

View File

@ -1,3 +1,29 @@
<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> <template>
<b-field :label="$t('Enter some tags')"> <b-field :label="$t('Enter some tags')">
<b-taginput <b-taginput

View File

@ -1,3 +1,8 @@
<docs>
```vue
<report-card :report="{ reported: { name: 'Some bad guy', preferredUsername: 'kevin' }, reporter: { preferredUsername: 'somePerson34' }, reportContent: 'This is not good'}" />
```
</docs>
<template> <template>
<div class="card" v-if="report"> <div class="card" v-if="report">
<div class="card-content"> <div class="card-content">

View File

@ -45,6 +45,11 @@ export default class EventMixin extends mixins(Vue) {
actorId: currentActor.id, actorId: currentActor.id,
}, },
}); });
/**
* When the event corresponding has been deleted (by the organizer). A notification is already triggered.
*
* @type {string}
*/
this.$emit('eventDeleted', event.id); this.$emit('eventDeleted', event.id);
this.$buefy.notification.open({ this.$buefy.notification.open({

45
js/styleguide.config.js Normal file
View File

@ -0,0 +1,45 @@
const path = require('path');
module.exports = {
// set your styleguidist configuration here
title: 'Mobilizon Style Guide',
components: 'src/components/**/[A-Z]*.vue',
exampleMode: 'expand',
usageMode: 'expand',
require: [
path.join(__dirname, 'config/global.requires.js'),
],
pagePerSection: true,
minimize: true,
verbose: false,
renderRootJsx: path.join(__dirname, 'config/styleguide.root.js'),
sections: [
{
name: 'Introduction',
content: 'docs/index.md',
exampleMode: 'hide',
usageMode: 'hide'
},
{
name: 'Directives',
content: 'docs/directives.md'
},
{
name: 'Components',
content: 'docs/components.md',
components: 'src/components/*/*.vue',
}
],
template: {
head: {
links: [
{
rel: 'stylesheet',
href:
'https://cdn.materialdesignicons.com/4.4.95/css/materialdesignicons.min.css',
crossorigin: 'anonymous'
}
]
}
}
}

File diff suppressed because it is too large Load Diff