Merged master in

This commit is contained in:
Grzegorz Leoniec 2019-02-28 10:31:40 +01:00
commit 5b33257f5a
No known key found for this signature in database
GPG Key ID: 3AA43686D4EB1377
1147 changed files with 4181 additions and 32609 deletions

View File

@ -1,11 +1,23 @@
{
"presets": [
["env", { "modules": false }]
[
"@babel/preset-env",
{
"modules": false
}
]
],
"env": {
"test": {
"presets": [
["env", { "targets": { "node": "current" }}]
[
"@babel/preset-env",
{
"targets": {
"node": "10"
}
}
]
]
}
}

View File

@ -1,6 +1,6 @@
.vscode/
styleguide/node_modules/
styleguide/
node_modules/
npm-debug.log

2
.gitignore vendored
View File

@ -30,6 +30,7 @@ build/Release
# Dependency directories
node_modules/
styleguide/
# TypeScript v1 declaration files
typings/
@ -79,6 +80,7 @@ static/uploads
cypress/videos
cypress/screenshots/
cypress.env.json
# Apple macOS folder attribute file
.DS_Store

View File

@ -18,27 +18,33 @@ before_install:
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
- cp cypress.env.template.json cypress.env.json
install:
- docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-web .
- docker-compose -f docker-compose.yml -f docker-compose.travis.yml up -d
- git clone https://github.com/Human-Connection/Nitro-Backend.git ../Nitro-Backend
- git -C "../Nitro-Backend" checkout $BACKEND_BRANCH || git -C "../Nitro-Backend" checkout master
- docker-compose -f ../Nitro-Backend/docker-compose.yml -f ../Nitro-Backend/docker-compose.travis.yml up -d
- cd ../Nitro-Backend && yarn install && cd -
- docker-compose -f ../Nitro-Backend/docker-compose.yml -f ../Nitro-Backend/docker-compose.cypress.yml up -d
- yarn global add cypress wait-on
- yarn add cypress-cucumber-preprocessor
script:
- docker-compose exec -e NODE_ENV=test webapp yarn run lint
- docker-compose exec -e NODE_ENV=test webapp yarn run test
- docker-compose -f ../Nitro-Backend/docker-compose.yml exec backend yarn run db:seed
- wait-on http://localhost:3000
- cypress run --record --key $CYPRESS_TOKEN
- wait-on http://localhost:7474 && docker-compose -f ../Nitro-Backend/docker-compose.yml exec neo4j migrate
- wait-on http://localhost:3000 && cypress run --record --key $CYPRESS_TOKEN
after_success:
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
- chmod +x send.sh
- ./send.sh success $WEBHOOK_URL
- if [ $TRAVIS_BRANCH == "master" ] && [ $TRAVIS_EVENT_TYPE == "push" ]; then
wget https://raw.githubusercontent.com/Human-Connection/Discord-Bot/develop/tester.sh &&
chmod +x tester.sh &&
./tester.sh staging $WEBHOOK_URL;
fi
after_failure:
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh

View File

@ -19,9 +19,6 @@ COPY . .
FROM base as build-and-test
RUN cp .env.template .env
RUN yarn install --production=false --frozen-lockfile --non-interactive
RUN cd styleguide && yarn install --production=false --frozen-lockfile --non-interactive \
&& cd .. \
&& yarn run styleguide:build
RUN yarn run build
FROM base as production

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Human-Connection gGmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,5 +1,12 @@
# Human Connection - NITRO Web
[![Build Status](https://travis-ci.com/Human-Connection/Nitro-Web.svg?branch=master)](https://travis-ci.com/Human-Connection/Nitro-Web)
<p align="center">
<a href="https://human-connection.org"><img align="center" src="static/img/sign-up/humanconnection.png" height="200" alt="Human Connection" /></a>
</p>
# NITRO Web
[![Build Status](https://img.shields.io/travis/com/Human-Connection/Nitro-Web/master.svg)](https://travis-ci.com/Human-Connection/Nitro-Web)
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Web/blob/master/LICENSE.md)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web?ref=badge_shield)
[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3)
![UI Screenshot](screenshot.png)
@ -10,16 +17,15 @@
### Install
``` bash
# install all dependencies
$ cd styleguide && yarn install && cd ..
$ yarn styleguide:build
$ yarn install
```
Copy:
```
cp .env.template .env
cp cypress.env.template.json cypress.env.json
```
Configure the file `.env` according to your needs and your local setup.
Configure the files according to your needs and your local setup.
### Development
``` bash
@ -30,7 +36,6 @@ $ yarn dev
### Build for production
``` bash
# build for production and launch server
$ yarn styleguide:build
$ yarn build
$ yarn start
```
@ -41,10 +46,8 @@ All reusable Components (for example avatar) should be done inside the styleguid
![Styleguide Screenshot](screenshot-styleguide.png)
### To show the styleguide
``` bash
$ yarn styleguide
```
More information can be found here: https://github.com/Human-Connection/Nitro-Styleguide
## Internationalization (i18n)
@ -57,3 +60,6 @@ Thanks lokalise.co that we can use your premium account!
## Attributions
<div>Locale Icons made by <a href="http://www.freepik.com/" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div>
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Web?ref=badge_large)

View File

@ -78,45 +78,6 @@ blockquote {
}
}
.hc-editor-content {
h1,
h2,
h3,
h4,
h5,
h6 {
&:not(:first-child) {
margin-top: 2rem;
}
}
p {
&:not(:last-child) {
margin-top: 0;
margin-bottom: 0;
}
}
dl,
ol,
ul,
blockquote,
pre,
table {
&:not(:first-child) {
margin-top: 15px;
}
}
*:first-child {
margin-top: 0;
}
*:last-child {
margin-bottom: 0;
}
// avoid double breaks
br + p {
margin-top: 0;
}
}
hr {
border: 0;
width: 100%;

View File

@ -8,14 +8,19 @@
slot="default"
slot-scope="{toggleMenu}"
>
<ds-button
class="content-menu-trigger"
size="small"
ghost
@click.prevent="toggleMenu"
<slot
name="button"
:toggleMenu="toggleMenu"
>
<ds-icon name="ellipsis-v" />
</ds-button>
<ds-button
class="content-menu-trigger"
size="small"
ghost
@click.prevent="toggleMenu"
>
<ds-icon name="ellipsis-v" />
</ds-button>
</slot>
</template>
<div
slot="popover"
@ -49,6 +54,7 @@ export default {
placement: { type: String, default: 'top-end' },
itemId: { type: String, required: true },
name: { type: String, required: true },
isOwner: { type: Boolean, default: false },
context: {
type: String,
required: true,
@ -59,20 +65,54 @@ export default {
},
computed: {
routes() {
let routes = [
{
let routes = []
if (this.isOwner && this.context === 'contribution') {
routes.push({
name: this.$t(`contribution.edit`),
path: this.$router.resolve({
name: 'post-edit-id',
params: {
id: this.itemId
}
}).href,
icon: 'edit'
})
}
if (this.isOwner && this.context === 'comment') {
routes.push({
name: this.$t(`comment.edit`),
callback: () => {
console.log('EDIT COMMENT')
},
icon: 'edit'
})
}
if (!this.isOwner) {
routes.push({
name: this.$t(`report.${this.context}.title`),
callback: this.openReportDialog,
icon: 'flag'
}
]
if (this.isModerator) {
})
}
if (!this.isOwner && this.isModerator) {
routes.push({
name: this.$t(`disable.${this.context}.title`),
callback: this.openDisableDialog,
callback: () => {},
icon: 'eye-slash'
})
}
if (this.isOwner && this.context === 'user') {
routes.push({
name: this.$t(`settings.data.name`),
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
callback: () => this.$router.push('/settings'),
icon: 'edit'
})
}
return routes
},
isModerator() {

View File

@ -0,0 +1,145 @@
<template>
<ds-form
ref="contributionForm"
v-model="form"
:schema="formSchema"
@submit="submit"
>
<template slot-scope="{ errors }">
<ds-card>
<ds-input
model="title"
class="post-title"
placeholder="Title"
name="title"
autofocus
/>
<no-ssr>
<hc-editor
:value="form.content"
@input="updateEditorContent"
/>
</no-ssr>
<div
slot="footer"
style="text-align: right"
>
<ds-button
:disabled="loading || disabled"
ghost
@click.prevent="$router.back()"
>
{{ $t('actions.cancel') }}
</ds-button>
<ds-button
icon="check"
type="submit"
:loading="loading"
:disabled="disabled || errors"
primary
>
{{ $t('actions.save') }}
</ds-button>
</div>
</ds-card>
</template>
</ds-form>
</template>
<script>
import gql from 'graphql-tag'
import HcEditor from '~/components/Editor/Editor.vue'
export default {
components: {
HcEditor
},
props: {
contribution: { type: Object, default: () => {} }
},
data() {
return {
form: {
title: '',
content: ''
},
formSchema: {
title: { required: true, min: 3, max: 64 },
content: { required: true, min: 3 }
},
id: null,
loading: false,
disabled: false,
slug: null
}
},
watch: {
contribution: {
immediate: true,
handler: function(contribution) {
if (!contribution || !contribution.id) {
return
}
this.id = contribution.id
this.slug = contribution.slug
this.form.content = contribution.content
this.form.title = contribution.title
}
}
},
methods: {
submit() {
const postMutations = require('~/graphql/PostMutations.js').default(this)
this.loading = true
this.$apollo
.mutate({
mutation: this.id
? postMutations.UpdatePost
: postMutations.CreatePost,
variables: {
id: this.id,
title: this.form.title,
content: this.form.content
}
})
.then(res => {
this.loading = false
this.$toast.success('Saved!')
this.disabled = true
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
this.$router.push({
name: 'post-slug',
params: { slug: result.slug }
})
})
.catch(err => {
this.$toast.error(err.message)
this.loading = false
this.disabled = false
})
},
updateEditorContent(value) {
// this.form.content = value
this.$refs.contributionForm.update('content', value)
}
}
}
</script>
<style lang="scss">
.post-title {
margin-top: $space-x-small;
margin-bottom: $space-xx-small;
input {
border: 0;
font-size: $font-size-x-large;
font-weight: bold;
padding-left: 0;
padding-right: 0;
}
}
</style>

View File

@ -0,0 +1,366 @@
<template>
<div class="editor">
<editor-menu-bubble :editor="editor">
<div
ref="menu"
slot-scope="{ commands, getMarkAttrs, isActive, menu }"
class="menububble tooltip"
x-placement="top"
:class="{ 'is-active': menu.isActive || linkMenuIsActive }"
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
>
<div class="tooltip-wrapper">
<template v-if="linkMenuIsActive">
<ds-input
ref="linkInput"
v-model="linkUrl"
class="editor-menu-link-input"
placeholder="http://"
@blur.native.capture="hideMenu(menu.isActive)"
@keydown.native.esc.prevent="hideMenu(menu.isActive)"
@keydown.native.enter.prevent="setLinkUrl(commands.link, linkUrl)"
/>
</template>
<template v-else>
<ds-button
class="menububble__button"
size="small"
:hover="isActive.bold()"
ghost
@click.prevent="() => {}"
@mousedown.native.prevent="commands.bold"
>
<ds-icon name="bold" />
</ds-button>
<ds-button
class="menububble__button"
size="small"
:hover="isActive.italic()"
ghost
@click.prevent="() => {}"
@mousedown.native.prevent="commands.italic"
>
<ds-icon name="italic" />
</ds-button>
<ds-button
class="menububble__button"
size="small"
:hover="isActive.link()"
ghost
@click.prevent="() => {}"
@mousedown.native.prevent="showLinkMenu(getMarkAttrs('link'))"
>
<ds-icon name="link" />
</ds-button>
</template>
</div>
<div class="tooltip-arrow" />
</div>
</editor-menu-bubble>
<editor-floating-menu :editor="editor">
<div
slot-scope="{ commands, isActive, menu }"
class="editor__floating-menu"
:class="{ 'is-active': menu.isActive }"
:style="`top: ${menu.top}px`"
>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.paragraph()"
@click.prevent="commands.paragraph()"
>
<ds-icon name="paragraph" />
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.heading({ level: 3 })"
@click.prevent="commands.heading({ level: 3 })"
>
H3
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.heading({ level: 4 })"
@click.prevent="commands.heading({ level: 4 })"
>
H4
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.bullet_list()"
@click.prevent="commands.bullet_list()"
>
<ds-icon name="list-ul" />
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.ordered_list()"
@click.prevent="commands.ordered_list()"
>
<ds-icon name="list-ol" />
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.blockquote()"
@click.prevent="commands.blockquote"
>
<ds-icon name="quote-right" />
</ds-button>
<ds-button
class="menubar__button"
size="small"
:ghost="!isActive.horizontal_rule()"
@click.prevent="commands.horizontal_rule"
>
<ds-icon name="minus" />
</ds-button>
</div>
</editor-floating-menu>
<editor-content :editor="editor" />
</div>
</template>
<script>
import linkify from 'linkify-it'
import stringHash from 'string-hash'
import {
Editor,
EditorContent,
EditorFloatingMenu,
EditorMenuBubble
} from 'tiptap'
import EventHandler from './plugins/eventHandler.js'
import {
Heading,
HardBreak,
Blockquote,
ListItem,
BulletList,
OrderedList,
HorizontalRule,
Placeholder,
Bold,
Italic,
Strike,
Underline,
Link,
History
} from 'tiptap-extensions'
let throttleInputEvent
export default {
components: {
EditorContent,
EditorFloatingMenu,
EditorMenuBubble
},
props: {
value: { type: String, default: '' },
doc: { type: Object, default: () => {} }
},
data() {
return {
lastValueHash: null,
editor: new Editor({
content: this.value || '',
doc: this.doc,
extensions: [
new EventHandler(),
new Heading(),
new HardBreak(),
new Blockquote(),
new BulletList(),
new OrderedList(),
new HorizontalRule(),
new Bold(),
new Italic(),
new Strike(),
new Underline(),
new Link(),
new Heading({ levels: [3, 4] }),
new ListItem(),
new Placeholder({
emptyNodeClass: 'is-empty',
emptyNodeText: 'Schreib etwas inspirerendes…'
}),
new History()
],
onUpdate: e => {
clearTimeout(throttleInputEvent)
throttleInputEvent = setTimeout(() => this.onUpdate(e), 300)
}
}),
linkUrl: null,
linkMenuIsActive: false
}
},
watch: {
value: {
immediate: true,
handler: function(content, old) {
const contentHash = stringHash(content)
if (!content || contentHash === this.lastValueHash) {
return
}
this.lastValueHash = contentHash
this.editor.setContent(content)
}
}
},
beforeDestroy() {
this.editor.destroy()
},
methods: {
onUpdate(e) {
const content = e.getHTML()
const contentHash = stringHash(content)
if (contentHash !== this.lastValueHash) {
this.lastValueHash = contentHash
this.$emit('input', content)
}
},
showLinkMenu(attrs) {
this.linkUrl = attrs.href
this.linkMenuIsActive = true
this.$nextTick(() => {
try {
const $el = this.$refs.linkInput.$el.getElementsByTagName('input')[0]
$el.focus()
$el.select()
} catch (err) {}
})
},
hideLinkMenu() {
this.linkUrl = null
this.linkMenuIsActive = false
this.editor.focus()
},
hideMenu(isActive) {
isActive = false
this.hideLinkMenu()
},
setLinkUrl(command, url) {
const links = linkify().match(url)
if (links) {
// add valid link
command({
href: links.pop().url
})
this.hideLinkMenu()
this.editor.focus()
} else if (!url) {
// remove link
command({ href: null })
}
}
}
}
</script>
<style lang="scss">
.ProseMirror {
padding: $space-base;
margin: -$space-base;
min-height: $space-large;
}
.ProseMirror:focus {
outline: none;
}
.editor p.is-empty:first-child::before {
content: attr(data-empty-text);
float: left;
color: $text-color-disabled;
padding-left: $space-xx-small;
pointer-events: none;
height: 0;
}
.menubar__button {
font-weight: normal;
}
li > p {
margin-top: $space-xx-small;
margin-bottom: $space-xx-small;
}
.editor {
&__floating-menu {
position: absolute;
margin-top: -0.25rem;
margin-left: $space-xx-small;
visibility: hidden;
opacity: 0;
transition: opacity 0.2s, visibility 0.2s;
background-color: #fff;
&.is-active {
opacity: 1;
visibility: visible;
}
}
.menububble {
position: absolute;
// margin-top: -0.5rem;
visibility: hidden;
opacity: 0;
transition: opacity 200ms, visibility 200ms;
// transition-delay: 50ms;
transform: translate(-50%, -10%);
background-color: $background-color-inverse-soft;
// color: $text-color-inverse;
border-radius: $border-radius-base;
padding: $space-xx-small;
box-shadow: $box-shadow-large;
.ds-button {
color: $text-color-inverse;
&.ds-button-hover,
&:hover {
color: $text-color-base;
}
}
&.is-active {
opacity: 1;
visibility: visible;
}
.tooltip-arrow {
left: calc(50% - 10px);
}
input,
button {
border: none;
border-radius: 2px;
}
.ds-input {
height: auto;
}
input {
padding: $space-xx-small $space-x-small;
}
}
}
</style>

View File

@ -0,0 +1,83 @@
import { Extension, Plugin } from 'tiptap'
// import { Slice, Fragment } from 'prosemirror-model'
export default class EventHandler extends Extension {
get name() {
return 'event_handler'
}
get plugins() {
return [
new Plugin({
props: {
transformPastedText(text) {
// console.log('#### transformPastedText', text)
return text.trim()
},
transformPastedHTML(html) {
html = html
// remove all tags with "space only"
.replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
// remove all iframes
.replace(
/(<iframe(?!.*?src=(['"]).*?\2)[^>]*)(>)[^>]*\/*>/gim,
''
)
.replace(/[\n]{3,}/gim, '\n\n')
.replace(/(\r\n|\n\r|\r|\n)/g, '<br>$1')
// replace all p tags with line breaks (and spaces) only by single linebreaks
// limit linebreaks to max 2 (equivalent to html "br" linebreak)
.replace(/(<br ?\/?>\s*){2,}/gim, '<br>')
// remove additional linebreaks after p tags
.replace(
/<\/(p|div|th|tr)>\s*(<br ?\/?>\s*)+\s*<(p|div|th|tr)>/gim,
'</p><p>'
)
// remove additional linebreaks inside p tags
.replace(
/<[a-z-]+>(<[a-z-]+>)*\s*(<br ?\/?>\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim,
''
)
// remove additional linebreaks when first child inside p tags
.replace(/<p>(\s*<br ?\/?>\s*)+/gim, '<p>')
// remove additional linebreaks when last child inside p tags
.replace(/(\s*<br ?\/?>\s*)+<\/p>/gim, '</p>')
// console.log('#### transformPastedHTML', html)
return html
}
// transformPasted(slice) {
// // console.log('#### transformPasted', slice.content)
// let content = []
// let size = 0
// slice.content.forEach((node, offset, index) => {
// // console.log(node)
// // console.log('isBlock', node.type.isBlock)
// // console.log('childCount', node.content.childCount)
// // console.log('index', index)
// if (node.content.childCount) {
// content.push(node.content)
// size += node.content.size
// }
// })
// console.log(content)
// console.log(slice.content)
// let fragment = Fragment.fromArray(content)
// fragment.size = size
// console.log('#fragment', fragment, slice.content)
// console.log('----')
// console.log('#1', slice)
// // const newSlice = new Slice(fragment, slice.openStart, slice.openEnd)
// slice.fragment = fragment
// // slice.content.content = fragment.content
// // slice.content.size = fragment.size
// console.log('#2', slice)
// // console.log(newSlice)
// console.log('----')
// return slice
// // return newSlice
// }
}
})
]
}
}

View File

@ -55,6 +55,7 @@
context="contribution"
:item-id="post.id"
:name="post.title"
:is-owner="isAuthor"
/>
</no-ssr>
</div>
@ -86,13 +87,16 @@ export default {
computed: {
excerpt() {
// remove all links from excerpt to prevent issues with the serounding link
let excerpt = this.post.contentExcerpt.replace(/<a.*>(.+)<\/a>/gim, '')
let excerpt = this.post.contentExcerpt.replace(/<a.*>(.+)<\/a>/gim, '$1')
// do not display content that is only linebreaks
if (excerpt.replace(/<br>/gim, '').trim() === '') {
excerpt = ''
}
return excerpt
},
isAuthor() {
return this.$store.getters['auth/user'].id === this.post.author.id
}
},
methods: {

View File

@ -0,0 +1,6 @@
{
"SEED_SERVER_HOST": "http://localhost:4001",
"NEO4J_URI": "bolt://localhost:7687",
"NEO4J_USERNAME": "neo4j",
"NEO4J_PASSWORD": "letmein"
}

View File

@ -4,11 +4,7 @@ Feature: Authentication
In order to attribute posts and other contributions to their authors
Background:
Given my account has the following details:
| name | email | password | type
| Peter Lustig | admin@example.org | 1234 | Admin
| Bob der Bausmeister | moderator@example.org | 1234 | Moderator
| Jenny Rostock" | user@example.org | 1234 | User
Given I have a user account
Scenario: Log in
When I visit the "/login" page

View File

@ -14,28 +14,27 @@ Feature: Tags and Categories
looking at the popularity of a tag.
Background:
Given we have a selection of tags and categories as well as posts
And my user account has the role "administrator"
Given I am logged in
Given my user account has the role "admin"
And we have a selection of tags and categories as well as posts
And I am logged in
Scenario: See an overview of categories
When I navigate to the administration dashboard
And I click on "Categories"
Then I can see a list of categories ordered by post count:
| Icon | Name | Post Count |
| | Just For Fun | 5 |
| | Happyness & Values | 2 |
| | Health & Wellbeing | 1 |
| Icon | Name | Posts |
| | Just For Fun | 2 |
| | Happyness & Values | 1 |
| | Health & Wellbeing | 0 |
Scenario: See an overview of tags
When I navigate to the administration dashboard
And I click on "Tags"
Then I can see a list of tags ordered by user and post count:
| # | Name | Nutzer | Beiträge |
| 1 | Naturschutz | 2 | 2 |
| 2 | Freiheit | 2 | 2 |
| 3 | Umwelt | 1 | 1 |
| 4 | Demokratie | 1 | 1 |
Then I can see a list of tags ordered by user count:
| # | Name | Users | Posts |
| 1 | Democracy | 2 | 3 |
| 2 | Ecology | 1 | 1 |
| 3 | Nature | 1 | 2 |

View File

@ -7,21 +7,18 @@ Feature: About me and location
to search for users by location.
Background:
Given I am logged in
Given I have a user account
And I am logged in
And I am on the "settings" page
Scenario: Change username
When I save "Hansi" as my new name
Then I can see my new name "Hansi" when I click on my profile picture in the top right
Scenario: Keep changes after refresh
When I changed my username to "Hansi" previously
And I refresh the page
Then my new username is still there
And when I refresh the page
Then the name "Hansi" is still there
Scenario Outline: I set my location to "<location>"
When I save "<location>" as my location
And my username is "Peter Lustig"
When people visit my profile page
Then they can see the location in the info box below my avatar
@ -36,10 +33,5 @@ Feature: About me and location
"""
Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei
"""
And my username is "Peter Lustig"
When people visit my profile page
Then they can see the text in the info box below my avatar

View File

@ -9,13 +9,13 @@ Feature: Report and Moderate
Background:
Given we have the following posts in our database:
| Author | Title | Content | Slug |
| David Irving | The Truth about the Holocaust | It never existed! | the-truth-about-the-holocaust |
| Author | id | title | content |
| David Irving | p1 | The Truth about the Holocaust | It never existed! |
Scenario Outline: Report a post from various pages
Given I am logged in with a "user" role
And I see David Irving's post on the <Page>
When I click on "Report Contribution" from the triple dot menu of the post
When I see David Irving's post on the <Page>
And I click on "Report Contribution" from the triple dot menu of the post
And I confirm the reporting dialog because it is a criminal act under German law:
"""
Do you really want to report the contribution "The Truth about the Holocaust"?
@ -45,8 +45,8 @@ Feature: Report and Moderate
Scenario: Review reported content
Given somebody reported the following posts:
| Slug |
| the-truth-about-the-holocaust |
| id |
| p1 |
And I am logged in with a "moderator" role
When I click on the avatar menu in the top right corner
And I click on "Moderation"

View File

@ -0,0 +1,25 @@
Feature: Create a post
As a user
I would like to create a post
To say something to everyone in the community
Background:
Given I have a user account
And I am logged in
And I am on the "landing" page
Scenario: Create a post
When I click on the big plus icon in the bottom right corner to create post
And I choose "My first post" as the title of the post
And I type in the following text:
"""
Human Connection is a free and open-source social network
for active citizenship.
"""
And I click on "Save"
Then I get redirected to "/post/my-first-post/"
And the post was saved successfully
Scenario: See a post on the landing page
Given I previously created a post
Then the post shows up on the landing page at position 1

View File

@ -2,18 +2,6 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps'
/* global cy */
const lastColumnIsSortedInDescendingOrder = () => {
cy.get('tbody')
.find('tr td:last-child')
.then(lastColumn => {
cy.wrap(lastColumn)
const values = lastColumn
.map((i, td) => parseInt(td.textContent))
.toArray()
const orderedDescending = values.slice(0).sort((a, b) => b - a)
return cy.wrap(values).should('deep.eq', orderedDescending)
})
}
When('I navigate to the administration dashboard', () => {
cy.get('.avatar-menu').click()
@ -23,17 +11,27 @@ When('I navigate to the administration dashboard', () => {
})
Then('I can see a list of categories ordered by post count:', table => {
// TODO: match the table in the feature with the html table
cy.get('thead')
.find('tr th')
.should('have.length', 3)
lastColumnIsSortedInDescendingOrder()
table.hashes().forEach(({Name, Posts}, index) => {
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(2)`)
.should('contain', Name)
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(3)`)
.should('contain', Posts)
})
})
Then('I can see a list of tags ordered by user and post count:', table => {
// TODO: match the table in the feature with the html table
Then('I can see a list of tags ordered by user count:', table => {
cy.get('thead')
.find('tr th')
.should('have.length', 4)
lastColumnIsSortedInDescendingOrder()
table.hashes().forEach(({Name, Users, Posts}, index) => {
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(2)`)
.should('contain', Name)
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(3)`)
.should('contain', Users)
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(4)`)
.should('contain', Posts)
})
})

View File

@ -3,9 +3,9 @@ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
/* global cy */
let lastReportTitle
let dummyReportedPostTitle = 'Hacker, Freaks und Funktionäre'
let dummyReportedPostSlug = 'hacker-freaks-und-funktionareder-ccc'
let dummyAuthorName = 'Jenny Rostock'
let davidIrvingPostTitle = 'The Truth about the Holocaust'
let davidIrvingPostSlug = 'the-truth-about-the-holocaust'
let davidIrvingName = 'David Irving'
const savePostTitle = $post => {
return $post
@ -23,21 +23,27 @@ Given("I see David Irving's post on the landing page", page => {
})
Given("I see David Irving's post on the post page", page => {
cy.visit(`/post/${dummyReportedPostSlug}`)
cy.contains(dummyReportedPostTitle) // wait
cy.visit(`/post/${davidIrvingPostSlug}`)
cy.contains(davidIrvingPostTitle) // wait
})
Given('I am logged in with a {string} role', role => {
cy.loginAs(role)
cy.factory().create('User', {
email: `${role}@example.org`,
password: '1234',
role
})
cy.login({
email: `${role}@example.org`,
password: '1234'
})
})
When(
'I click on "Report Contribution" from the triple dot menu of the post',
() => {
//TODO: match the created post title, not a dummy post title
cy.contains('.ds-card', dummyReportedPostTitle)
cy.contains('.ds-card', davidIrvingPostTitle)
.find('.content-menu-trigger')
.first()
.click()
cy.get('.popover .ds-menu-item-link')
@ -49,8 +55,7 @@ When(
When(
'I click on "Report User" from the triple dot menu in the user info box',
() => {
//TODO: match the created post author, not a dummy author
cy.contains('.ds-card', dummyAuthorName)
cy.contains('.ds-card', davidIrvingName)
.find('.content-menu-trigger')
.first()
.click()
@ -106,12 +111,8 @@ Then(`I can't see the moderation menu item`, () => {
.should('not.exist')
})
When(/^I confirm the reporting dialog .*:$/, () => {
//TODO: take message from method argument
//TODO: match the right post
const message = 'Do you really want to report the'
When(/^I confirm the reporting dialog .*:$/, (message) => {
cy.contains(message) // wait for element to become visible
//TODO: cy.get('.ds-modal').contains(dummyReportedPostTitle)
cy.get('.ds-modal').within(() => {
cy.get('button')
.contains('Send Report')
@ -120,22 +121,28 @@ When(/^I confirm the reporting dialog .*:$/, () => {
})
Given('somebody reported the following posts:', table => {
table.hashes().forEach(row => {
//TODO: calll factory here
// const options = Object.assign({}, row, { reported: true })
//create('post', options)
table.hashes().forEach(({ id }) => {
const reporter = {
email: `reporter${id}@example.org`,
password: '1234'
}
cy.factory()
.create('User', reporter)
.authenticateAs(reporter)
.create('Report', {
description: "I don't like this post",
resource: { id, type: 'contribution' }
})
})
})
Then('I see all the reported posts including the one from above', () => {
//TODO: match the right post
cy.get('table tbody').within(() => {
cy.contains('tr', dummyReportedPostTitle)
cy.contains('tr', davidIrvingPostTitle)
})
})
Then('each list item links to the post page', () => {
//TODO: match the right post
cy.contains(dummyReportedPostTitle).click()
cy.contains(davidIrvingPostTitle).click()
cy.location('pathname').should('contain', '/post')
})

View File

@ -4,7 +4,6 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps'
let aboutMeText
let myLocation
let myName
const matchNameInUserMenu = name => {
cy.get('.avatar-menu').click() // open
@ -12,18 +11,13 @@ const matchNameInUserMenu = name => {
cy.get('.avatar-menu').click() // close again
}
const setUserName = name => {
When('I save {string} as my new name', name => {
cy.get('input[id=name]')
.clear()
.type(name)
cy.get('[type=submit]')
.click()
.not('[disabled]')
myName = name
}
When('I save {string} as my new name', name => {
setUserName(name)
})
When('I save {string} as my location', location => {
@ -47,31 +41,20 @@ When('I have the following self-description:', text => {
aboutMeText = text
})
When('my username is {string}', name => {
if (myName !== name) {
setUserName(name)
}
matchNameInUserMenu(name)
})
When('people visit my profile page', url => {
cy.visitMyProfile()
cy.openPage('/profile/peter-pan')
})
When('they can see the text in the info box below my avatar', () => {
cy.contains(aboutMeText)
})
When('I changed my username to {string} previously', name => {
myName = name
})
Then('they can see the location in the info box below my avatar', () => {
matchNameInUserMenu(myName)
cy.contains(myLocation)
})
Then('my new username is still there', () => {
matchNameInUserMenu(myName)
Then('the name {string} is still there', name => {
matchNameInUserMenu(name)
})
Then(

View File

@ -1,26 +1,87 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
import { getLangByName } from '../../support/helpers'
import users from '../../fixtures/users.json'
/* global cy */
let lastPost = {}
const loginCredentials = {
email: 'peterpan@example.org',
password: '1234'
}
const narratorParams = {
name: 'Peter Pan',
...loginCredentials
}
Given('I am logged in', () => {
cy.loginAs('admin')
})
Given('I am logged in as {string}', userType => {
cy.loginAs(userType)
cy.login(loginCredentials)
})
Given('we have a selection of tags and categories as well as posts', () => {
// TODO: use db factories instead of seed data
cy.factory()
.authenticateAs(loginCredentials)
.create('Category', {
id: 'cat1',
name: 'Just For Fun',
slug: 'justforfun',
icon: 'smile'
})
.create('Category', {
id: 'cat2',
name: 'Happyness & Values',
slug: 'happyness-values',
icon: 'heart-o'
})
.create('Category', {
id: 'cat3',
name: 'Health & Wellbeing',
slug: 'health-wellbeing',
icon: 'medkit'
})
.create('Tag', { id: 't1', name: 'Ecology' })
.create('Tag', { id: 't2', name: 'Nature' })
.create('Tag', { id: 't3', name: 'Democracy' })
const someAuthor = {
id: 'authorId',
email: 'author@example.org',
password: '1234'
}
cy.factory()
.create('User', someAuthor)
.authenticateAs(someAuthor)
.create('Post', { id: 'p0' })
.create('Post', { id: 'p1' })
cy.factory()
.authenticateAs(loginCredentials)
.create('Post', { id: 'p2' })
.relate('Post', 'Categories', { from: 'p0', to: 'cat1' })
.relate('Post', 'Categories', { from: 'p1', to: 'cat2' })
.relate('Post', 'Categories', { from: 'p2', to: 'cat1' })
.relate('Post', 'Tags', { from: 'p0', to: 't1' })
.relate('Post', 'Tags', { from: 'p0', to: 't2' })
.relate('Post', 'Tags', { from: 'p0', to: 't3' })
.relate('Post', 'Tags', { from: 'p1', to: 't2' })
.relate('Post', 'Tags', { from: 'p1', to: 't3' })
.relate('Post', 'Tags', { from: 'p2', to: 't3' })
})
Given('my account has the following details:', table => {
// TODO: use db factories instead of seed data
Given('we have the following user accounts:', table => {
table.hashes().forEach(params => {
cy.factory().create('User', params)
})
})
Given('I have a user account', () => {
cy.factory().create('User', narratorParams)
})
Given('my user account has the role {string}', role => {
// TODO: use db factories instead of seed data
cy.factory().create('User', {
role,
...loginCredentials
})
})
When('I log out', cy.logout)
@ -34,10 +95,10 @@ Given('I am on the {string} page', page => {
})
When('I fill in my email and password combination and click submit', () => {
cy.login('admin@example.org', 1234)
cy.login(loginCredentials)
})
When('I refresh the page', () => {
When(/(?:when )?I refresh the page/, () => {
cy.reload()
})
@ -49,7 +110,7 @@ When('I log out through the menu in the top right corner', () => {
})
Then('I can see my name {string} in the dropdown menu', () => {
cy.get('.avatar-menu-popover').should('contain', users.admin.name)
cy.get('.avatar-menu-popover').should('contain', narratorParams.name)
})
Then('I see the login screen again', () => {
@ -63,7 +124,7 @@ Then('I can click on my profile picture in the top right corner', () => {
Then('I am still logged in', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover').contains(users.admin.name)
cy.get('.avatar-menu-popover').contains(narratorParams.name)
})
When('I select {string} in the language menu', name => {
@ -90,9 +151,18 @@ When('I press {string}', label => {
})
Given('we have the following posts in our database:', table => {
table.hashes().forEach(row => {
//TODO: calll factory here
//create('post', row)
table.hashes().forEach(({ Author, id, title, content }) => {
cy.factory()
.create('User', {
name: Author,
email: `${Author}@example.org`,
password: '1234'
})
.authenticateAs({
email: `${Author}@example.org`,
password: '1234'
})
.create('Post', { id, title, content })
})
})
@ -103,3 +173,42 @@ Then('I see a success message:', message => {
When('I click on the avatar menu in the top right corner', () => {
cy.get('.avatar-menu').click()
})
When(
'I click on the big plus icon in the bottom right corner to create post',
() => {
cy.get('.post-add-button').click()
}
)
Given('I previously created a post', () => {
cy.factory()
.authenticateAs(loginCredentials)
.create('Post', lastPost)
})
When('I choose {string} as the title of the post', title => {
lastPost.title = title.replace('\n', ' ')
cy.get('input[name="title"]').type(lastPost.title)
})
When('I type in the following text:', text => {
lastPost.content = text.replace('\n', ' ')
cy.get('.ProseMirror').type(lastPost.content)
})
Then('the post shows up on the landing page at position {int}', index => {
cy.openPage('landing')
const selector = `:nth-child(${index}) > .ds-card > .ds-card-content`
cy.get(selector).should('contain', lastPost.title)
cy.get(selector).should('contain', lastPost.content)
})
Then('I get redirected to {string}', route => {
cy.location('pathname').should('contain', route)
})
Then('the post was saved successfully', () => {
cy.get('.ds-card-header > .ds-heading').should('contain', lastPost.title)
cy.get('.content').should('contain', lastPost.content)
})

View File

@ -35,14 +35,7 @@ Cypress.Commands.add('switchLanguage', (name, force) => {
}
})
Cypress.Commands.add('visitMyProfile', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover')
.find('a[href^="/profile/"]')
.click()
})
Cypress.Commands.add('login', (email, password) => {
Cypress.Commands.add('login', ({ email, password }) => {
cy.visit(`/login`)
cy.get('input[name=email]')
.trigger('focus')
@ -56,11 +49,6 @@ Cypress.Commands.add('login', (email, password) => {
cy.location('pathname').should('eq', '/') // we're in!
})
Cypress.Commands.add('loginAs', role => {
role = role || 'admin'
cy.login(users[role].email, users[role].password)
})
Cypress.Commands.add('logout', (email, password) => {
cy.visit(`/logout`)
cy.location('pathname').should('contain', '/login') // we're out

View File

@ -0,0 +1,43 @@
// TODO: find a better way how to import the factories
import Factory from '../../../Nitro-Backend/src/seed/factories'
import { getDriver } from '../../../Nitro-Backend/src/bootstrap/neo4j'
const neo4jDriver = getDriver({
uri: Cypress.env('NEO4J_URI'),
username: Cypress.env('NEO4J_USERNAME'),
password: Cypress.env('NEO4J_PASSWORD')
})
const factory = Factory({ neo4jDriver })
const seedServerHost = Cypress.env('SEED_SERVER_HOST')
beforeEach(async () => {
await factory.cleanDatabase({ seedServerHost, neo4jDriver })
})
Cypress.Commands.add('factory', () => {
return Factory({seedServerHost})
})
Cypress.Commands.add(
'create',
{ prevSubject: true },
(factory, node, properties) => {
return factory.create(node, properties)
}
)
Cypress.Commands.add(
'relate',
{ prevSubject: true },
(factory, node, relationship, properties) => {
return factory.relate(node, relationship, properties)
}
)
Cypress.Commands.add(
'authenticateAs',
{ prevSubject: true },
(factory, loginCredentials) => {
return factory.authenticateAs(loginCredentials)
}
)

View File

@ -15,6 +15,7 @@
// Import commands.js using ES2015 syntax:
import './commands'
import './factories'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -8,9 +8,9 @@ services:
volumes:
- .:/nitro-web
- node_modules:/nitro-web/node_modules
- node_modules_styleguide:/nitro-web/styleguide/node_modules
- nuxt:/nitro-web/.nuxt
command: yarn run dev
volumes:
node_modules:
node_modules_styleguide:
nuxt:

View File

@ -5,3 +5,5 @@ services:
build:
context: .
target: build-and-test
environment:
- BACKEND_URL=http://backend:4123

28
graphql/PostMutations.js Normal file
View File

@ -0,0 +1,28 @@
import gql from 'graphql-tag'
export default app => {
return {
CreatePost: gql(`
mutation($title: String!, $content: String!) {
CreatePost(title: $title, content: $content) {
id
title
slug
content
contentExcerpt
}
}
`),
UpdatePost: gql(`
mutation($id: ID!, $title: String!, $content: String!) {
UpdatePost(id: $id, title: $title, content: $content) {
id
title
slug
content
contentExcerpt
}
}
`)
}
}

View File

@ -5,7 +5,8 @@
"create": "Erstellen",
"save": "Speichern",
"edit": "Bearbeiten",
"delete": "Löschen"
"delete": "Löschen",
"cancel": "Abbrechen"
},
"login": {
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
@ -105,6 +106,14 @@
"reporter": "gemeldet von"
}
},
"contribution": {
"edit": "Beitrag bearbeiten",
"delete": "Beitrag löschen"
},
"comment": {
"edit": "Kommentar bearbeiten",
"delete": "Kommentar löschen"
},
"disable": {
"user": {
"title": "Nutzer sperren",

View File

@ -5,7 +5,8 @@
"create": "Create",
"save": "Save",
"edit": "Edit",
"delete": "Delete"
"delete": "Delete",
"cancel": "Cancel"
},
"login": {
"copy": "If you already have a human-connection account, login here.",
@ -105,6 +106,14 @@
"reporter": "reported by"
}
},
"contribution": {
"edit": "Edit Contribution",
"delete": "Delete Contribution"
},
"comment": {
"edit": "Edit Comment",
"delete": "Delete Comment"
},
"disable": {
"user": {
"title": "Disable User",

View File

@ -9,6 +9,8 @@ module.exports = {
dev: dev,
debug: dev ? 'nuxt:*,app' : null,
modern: 'server',
transition: {
name: 'slide-up',
mode: 'out-in'
@ -41,7 +43,14 @@ module.exports = {
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: pkg.description }
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
script: [
{
src:
'https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver',
body: true
}
]
},
/*
@ -61,10 +70,10 @@ module.exports = {
** Plugins to load before mounting the App
*/
plugins: [
{ src: '~/plugins/styleguide.js', ssr: true },
{ src: '~/plugins/i18n.js', ssr: true },
{ src: '~/plugins/axios.js', ssr: false },
{ src: '~/plugins/keep-alive.js', ssr: false },
{ src: '~/plugins/design-system.js', ssr: true },
{ src: '~/plugins/vue-directives.js', ssr: false },
{ src: '~/plugins/v-tooltip.js', ssr: false },
{ src: '~/plugins/izi-toast.js', ssr: false },
@ -89,13 +98,14 @@ module.exports = {
'cookie-universal-nuxt',
'@nuxtjs/apollo',
'@nuxtjs/axios',
'portal-vue/nuxt',
[
'nuxt-sass-resources-loader',
path.resolve(__dirname, './styleguide/src/system/styles/shared.scss')
]
'@nuxtjs/style-resources',
'portal-vue/nuxt'
],
styleResources: {
scss: ['@human-connection/styleguide/dist/shared.scss']
},
/*
** Axios module configuration
*/
@ -157,12 +167,6 @@ module.exports = {
** Build configuration
*/
build: {
/*
* TODO: import the polyfill instead of using the deprecated vendor key
* Polyfill missing ES6 & 7 Methods to work on older Browser
*/
vendor: ['@babel/polyfill'],
/*
** You can extend webpack config here
*/
@ -176,14 +180,6 @@ module.exports = {
exclude: /(node_modules)/
})
}
config.resolve.alias['@@'] = path.resolve(
__dirname,
'./styleguide/src/system'
)
config.module.rules.push({
resourceQuery: /blockType=docs/,
loader: require.resolve('./styleguide/src/loader/docs-trim-loader.js')
})
const svgRule = config.module.rules.find(rule => rule.test.test('.svg'))
svgRule.test = /\.(png|jpe?g|gif|webp)$/
config.module.rules.push({

View File

@ -10,8 +10,8 @@
"start": "cross-env node server/index.js",
"generate": "nuxt generate",
"lint": "eslint --ext .js,.vue .",
"styleguide": "cd ./styleguide && yarn dev",
"styleguide:build": "cd ./styleguide && yarn build:lib && cd ../",
"styleguide": "echo 'Command styleguide is Deprecated!'",
"styleguide:build": "echo 'Command styleguide:build is Deprecated!'",
"test": "jest",
"precommit": "yarn lint",
"e2e:local": "cypress run --headed",
@ -32,55 +32,59 @@
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1",
"^@@/(.*)$": "<rootDir>/styleguide/src/system/$1",
"^~/(.*)$": "<rootDir>/$1"
},
"modulePathIgnorePatterns": [
"<rootDir>/styleguide"
]
}
},
"dependencies": {
"@nuxtjs/apollo": "^4.0.0-rc3",
"@nuxtjs/axios": "^5.3.6",
"@nuxtjs/dotenv": "^1.3.0",
"accounting": "^0.4.1",
"cookie-universal-nuxt": "^2.0.14",
"cross-env": "^5.2.0",
"date-fns": "^2.0.0-alpha.26",
"express": "^4.16.3",
"global": "^4.3.2",
"graphql": "^14.1.1",
"graphql-tag": "^2.10.1",
"jsonwebtoken": "^8.3.0",
"nuxt": "^2.0.0",
"nuxt-env": "^0.0.4",
"nuxt-sass-resources-loader": "^2.0.5",
"@nuxtjs/apollo": "4.0.0-rc4",
"@nuxtjs/axios": "~5.3.6",
"@nuxtjs/dotenv": "~1.3.0",
"@nuxtjs/style-resources": "~0.1.2",
"accounting": "~0.4.1",
"apollo-cache-inmemory": "~1.5.0",
"apollo-client": "~2.5.1",
"cookie-universal-nuxt": "~2.0.14",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.27",
"express": "~4.16.4",
"graphql": "~14.1.1",
"jsonwebtoken": "~8.5.0",
"linkify-it": "~2.1.0",
"nuxt": "~2.4.5",
"nuxt-env": "~0.1.0",
"portal-vue": "~1.5.1",
"v-tooltip": "^2.0.0-rc.33",
"vue-count-to": "^1.0.13",
"@human-connection/styleguide": "~0.5.0",
"v-tooltip": "~2.0.0-rc.33",
"vue-count-to": "~1.0.13",
"string-hash": "^1.1.3",
"tiptap": "^1.13.0",
"tiptap-extensions": "^1.13.0",
"vue-izitoast": "1.1.2",
"vue-sweetalert-icons": "^3.2.0",
"vuex-i18n": "^1.11.0"
"vue-sweetalert-icons": "~3.2.0",
"vuex-i18n": "~1.11.0"
},
"devDependencies": {
"@vue/eslint-config-prettier": "^4.0.1",
"@vue/server-test-utils": "^1.0.0-beta.29",
"@vue/test-utils": "^1.0.0-beta.28",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-preset-env": "^1.7.0",
"cypress-cucumber-preprocessor": "^1.9.1",
"eslint": "^5.13.0",
"eslint-config-prettier": "^3.1.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-prettier": "3.0.1",
"eslint-plugin-vue": "^5.1.0",
"jest": "^23.6.0",
"node-sass": "^4.11.0",
"nodemon": "^1.18.9",
"prettier": "1.14.3",
"sass-loader": "^7.1.0",
"vue-jest": "^3.0.2",
"vue-svg-loader": "^0.11.0"
"@babel/core": "~7.3.4",
"@babel/preset-env": "~7.3.1",
"@vue/cli-shared-utils": "~3.4.0",
"@vue/eslint-config-prettier": "~4.0.1",
"@vue/server-test-utils": "~1.0.0-beta.29",
"@vue/test-utils": "~1.0.0-beta.29",
"babel-core": "~7.0.0-bridge.0",
"babel-eslint": "~10.0.1",
"babel-jest": "~24.1.0",
"cypress-cucumber-preprocessor": "~1.11.0",
"eslint": "~5.14.1",
"eslint-config-prettier": "~3.6.0",
"eslint-loader": "~2.1.2",
"eslint-plugin-prettier": "~3.0.1",
"eslint-plugin-vue": "~5.2.2",
"jest": "~24.1.0",
"node-sass": "~4.11.0",
"nodemon": "~1.18.10",
"prettier": "~1.14.3",
"sass-loader": "~7.1.0",
"vue-jest": "~3.0.3",
"vue-svg-loader": "~0.11.0"
}
}

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('admin.notifications.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('admin.organizations.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('admin.pages.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('admin.settings.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('admin.users.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -13,6 +13,16 @@
<hc-post-card :post="post" />
</ds-flex-item>
</ds-flex>
<no-ssr>
<ds-button
v-tooltip="{content: 'Create a new Post', placement: 'left', delay: { show: 500 }}"
:path="{ name: 'post-create' }"
class="post-add-button"
icon="plus"
size="x-large"
primary
/>
</no-ssr>
<hc-load-more
v-if="true"
:loading="$apollo.loading"
@ -82,7 +92,7 @@ export default {
query() {
return gql(`
query Post($first: Int, $offset: Int) {
Post(first: $first, offset: $offset) {
Post(first: $first, offset: $offset, orderBy: createdAt_desc) {
id
title
contentExcerpt
@ -123,8 +133,20 @@ export default {
first: this.pageSize,
offset: 0
}
}
},
fetchPolicy: 'cache-and-network'
}
}
}
</script>
<style lang="scss">
.post-add-button {
z-index: 100;
position: fixed;
top: 100vh;
left: 100vw;
transform: translate(-120%, -120%);
box-shadow: $box-shadow-x-large;
}
</style>

View File

@ -113,16 +113,16 @@ export default {
}
}
},
asyncData({ store, redirect }) {
if (store.getters['auth/isLoggedIn']) {
redirect('/')
}
},
computed: {
pending() {
return this.$store.getters['auth/pending']
}
},
asyncData({ store, redirect }) {
if (store.getters['auth/isLoggedIn']) {
redirect('/')
}
},
mounted() {
setTimeout(() => {
// NOTE: quick fix for jumping flexbox implementation

View File

@ -12,6 +12,7 @@
context="contribution"
:item-id="post.id"
:name="post.title"
:is-owner="isAuthor(post.author.id)"
/>
</no-ssr>
<ds-space margin-bottom="small" />
@ -97,6 +98,7 @@
style="float-right"
:item-id="comment.id"
:name="comment.author.name"
:is-owner="isAuthor(comment.author.id)"
/>
</no-ssr>
<!-- eslint-disable vue/no-v-html -->
@ -160,6 +162,11 @@ export default {
this.title = this.post.title
}
},
methods: {
isAuthor(id) {
return this.$store.getters['auth/user'].id === id
}
},
apollo: {
Post: {
query() {

View File

@ -38,7 +38,7 @@
<ds-flex-item
v-for="relatedPost in post.relatedContributions"
:key="relatedPost.id"
:width="{ base: '50%' }"
:width="{ base: '100%', lg: 1 }"
>
<hc-post-card :post="relatedPost" />
</ds-flex-item>
@ -70,7 +70,7 @@ export default {
},
computed: {
post() {
return this.Post ? this.Post[0] : {}
return this.Post ? this.Post[0] || {} : {}
}
},
apollo: {

View File

@ -2,7 +2,7 @@
<ds-card header="Werde aktiv!">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

24
pages/post/create.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<ds-flex
:width="{ base: '100%' }"
gutter="base"
>
<ds-flex-item :width="{ base: '100%', md: 3 }">
<hc-contribution-form />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
&nbsp;
</ds-flex-item>
</ds-flex>
</template>
<script>
import gql from 'graphql-tag'
import HcContributionForm from '~/components/ContributionForm.vue'
export default {
components: {
HcContributionForm
}
}
</script>

77
pages/post/edit/_id.vue Normal file
View File

@ -0,0 +1,77 @@
<template>
<ds-flex
:width="{ base: '100%' }"
gutter="base"
>
<ds-flex-item :width="{ base: '100%', md: 3 }">
<hc-contribution-form :contribution="contribution" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
&nbsp;
</ds-flex-item>
</ds-flex>
</template>
<script>
import gql from 'graphql-tag'
import HcContributionForm from '~/components/ContributionForm.vue'
export default {
components: {
HcContributionForm
},
computed: {
user() {
return this.$store.getters['auth/user']
},
author() {
return this.contribution ? this.contribution.author : {}
},
contribution() {
return this.Post ? this.Post[0] : {}
}
},
watch: {
contribution() {
if (this.author.id !== this.user.id) {
throw new Error(`You can't edit that!`)
}
}
},
apollo: {
Post: {
query() {
return gql(`
query($id: ID!) {
Post(id: $id) {
id
title
content
createdAt
slug
image
author {
id
}
tags {
name
}
categories {
id
name
icon
}
}
}
`)
},
variables() {
return {
id: this.$route.params.id || 'p1'
}
},
fetchPolicy: 'cache-and-network'
}
}
}
</script>

View File

@ -23,6 +23,7 @@
context="user"
:item-id="user.id"
:name="user.name"
:is-owner="myProfile"
/>
</no-ssr>
<ds-space margin="small">
@ -251,6 +252,17 @@
</ds-flex>
</ds-card>
</ds-flex-item>
<ds-flex-item style="text-align: center">
<ds-button
v-if="myProfile"
v-tooltip="{content: 'Create a new Post', placement: 'left', delay: { show: 500 }}"
:path="{ name: 'post-create' }"
class="profile-post-add-button"
icon="plus"
size="large"
primary
/>
</ds-flex-item>
<template v-if="activePosts.length">
<ds-flex-item
v-for="post in activePosts"

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('settings.download.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('settings.delete.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('settings.invites.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('settings.languages.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('settings.organizations.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -2,7 +2,7 @@
<ds-card :header="$t('settings.security.name')">
<hc-empty
icon="tasks"
message="Comming Soon…"
message="Coming Soon…"
/>
</ds-card>
</template>

View File

@ -1,5 +0,0 @@
import Vue from 'vue'
import DesignSystem from '@@'
import '@@/styles/main.scss'
Vue.use(DesignSystem)

6
plugins/styleguide.js Normal file
View File

@ -0,0 +1,6 @@
import Vue from 'vue'
import Styleguide from '@human-connection/styleguide'
import '@human-connection/styleguide/dist/system.css'
// import '@human-connection/styleguide/dist/shared.scss'
Vue.use(Styleguide)

View File

@ -1,3 +0,0 @@
> 1%
last 2 versions
not ie <= 8

View File

@ -1,14 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: ['plugin:vue/strongly-recommended', '@vue/prettier'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: 'babel-eslint'
}
}

23
styleguide/.gitignore vendored
View File

@ -1,23 +0,0 @@
.DS_Store
node_modules
# local env files
.env.local
.env.*.local
/src/system/tokens/generated
/src/system/icons/generated
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

View File

@ -1,6 +0,0 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"bracketSpacing": true
}

View File

@ -1,57 +0,0 @@
# CION Vue Design System
CION is a Design System build primary for Vue applications. You can use it as a starting point for building your own Design System.
The system utilizes design tokens, a living styleguide with integrated code playgrounds and reusable components for common UI tasks.
Living styleguide demo: https://styleguide.cion.visualjerk.de
Landing page demo: https://cion.visualjerk.de
Integrate it in your application: [Quick Start](https://github.com/visualjerk/vue-cion-design-system/wiki/Quick-Start)
[![Screenshot](./preview/customize.png)](https://github.com/visualjerk/vue-cion-design-system/raw/master/preview/customize.png)
## Project setup
```
yarn install
```
## Developing
Compiles and hot-reloads living styleguide
```
yarn dev
```
## Building
### Living styleguide
Compiles living styleguide to `./docs`
```
yarn build
```
### Library
Compiles design system as a library to `./dist`
```
yarn build:lib
```
## Helper
### Serve living styleguide locally
```
yarn serve
```
### Lints and fixes files
```
yarn lint
```

View File

@ -1,4 +0,0 @@
module.exports = {
presets: ['@vue/app'],
plugins: ['@babel/plugin-syntax-dynamic-import']
}

View File

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Single Page Apps for GitHub Pages</title>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// https://github.com/rafrex/spa-github-pages
// Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
// ----------------------------------------------------------------------
// This script takes the current url and converts the path and query
// string into just a query string, and then redirects the browser
// to the new url with only a query string and hash fragment,
// e.g. http://www.foo.tld/one/two?a=b&c=d#qwe, becomes
// http://www.foo.tld/?p=/one/two&q=a=b~and~c=d#qwe
// Note: this 404.html file must be at least 512 bytes for it to work
// with Internet Explorer (it is currently > 512 bytes)
// If you're creating a Project Pages site and NOT using a custom domain,
// then set segmentCount to 1 (enterprise users may need to set it to > 1).
// This way the code will only replace the route part of the path, and not
// the real directory in which the app resides, for example:
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
// https://username.github.io/repo-name/?p=/one/two&q=a=b~and~c=d#qwe
// Otherwise, leave segmentCount as 0.
var segmentCount = 0;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + segmentCount).join('/') + '/?p=/' +
l.pathname.slice(1).split('/').slice(segmentCount).join('/').replace(/&/g, '~and~') +
(l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>

View File

@ -1 +0,0 @@
styleguide.cion.visualjerk.de

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

@ -1 +0,0 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><script src=/babel-standalone.js></script><title>CION - Vue Design System</title><link href=/css/app.e38d5069.css rel=preload as=style><link href=/css/chunk-vendors.7a428b56.css rel=preload as=style><link href=/js/app.f428b946.js rel=preload as=script><link href=/js/chunk-vendors.0a7f54fd.js rel=preload as=script><link href=/css/chunk-vendors.7a428b56.css rel=stylesheet><link href=/css/app.e38d5069.css rel=stylesheet></head><body><noscript><strong>We're sorry but this CION doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.0a7f54fd.js></script><script src=/js/app.f428b946.js></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,24 +0,0 @@
module.exports = {
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue'
],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@@/(.*)$': '<rootDir>/src/system/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)|**/spec.(js|jsx|ts|tsx)'
],
testURL: 'http://localhost/'
}

View File

@ -1,76 +0,0 @@
{
"name": "vue-cion-design-system",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "http-server ./docs -o -s",
"build": "yarn theo && vue-cli-service build",
"lint": "vue-cli-service lint",
"dev": "npm-run-all --parallel theo:onchange theo servedev",
"servedev": "vue-cli-service serve --open",
"build:lib": "yarn theo && cross-env BUILD=library vue-cli-service build --target lib --name system ./src/library.js",
"theo": "theo ./src/system/tokens/tokens.yml --transform web --format map.scss,scss,raw.json,json --dest ./src/system/tokens/generated",
"theo:onchange": "onchange \"./src/system/tokens/*.yml\" -- npm run theo",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"portal-vue": "^1.5.1",
"vue": "^2.5.17"
},
"devDependencies": {
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/standalone": "^7.0.0-beta.56",
"@vue/cli-plugin-babel": "^3.0.0-rc.12",
"@vue/cli-plugin-eslint": "^3.0.0-rc.12",
"@vue/cli-plugin-unit-jest": "^3.0.1",
"@vue/cli-service": "^3.0.0-rc.12",
"@vue/eslint-config-prettier": "^4.0.1",
"@vue/test-utils": "^1.0.0-beta.20",
"async-validator": "^1.8.5",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^23.0.1",
"babel-plugin-transform-require-context": "^0.0.3",
"cheerio": "^1.0.0-rc.2",
"clipboard-copy": "^2.0.1",
"clone-deep": "^4.0.0",
"codemirror": "^5.39.2",
"cross-env": "^5.2.0",
"dot-prop": "^4.2.0",
"lodash": "^4.17.10",
"markdown-it": "^8.4.2",
"markdown-it-abbr": "^1.0.4",
"markdown-it-deflist": "^2.0.3",
"markdown-it-emoji": "^1.4.0",
"markdown-it-footnote": "^3.0.1",
"markdown-it-ins": "^2.0.0",
"markdown-it-katex": "^2.0.3",
"markdown-it-mark": "^2.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"markdown-it-task-lists": "^2.1.1",
"node-sass": "^4.9.3",
"npm-run-all": "^4.1.3",
"onchange": "^4.1.0",
"raw-loader": "^0.5.1",
"sass-loader": "^7.1.0",
"theo": "^8.0.0-beta.2",
"vue-click-outside": "^1.0.7",
"vue-docgen-api": "^2.3.13",
"vue-router": "^3.0.1",
"vue-svg-loader": "^0.8.0",
"vue-template-compiler": "^2.5.17",
"vuep": "git+https://github.com/visualjerk/vuep.git#fix-iframe-firefox",
"webpack-bundle-analyzer": "^2.13.1",
"webpack-merge-and-include-globally": "^2.0.11"
},
"author": "visualjerk",
"main": "./dist/system.umd.min.js",
"files": [
"dist/*",
"src/*",
"public/*",
"*.json",
"*.js"
],
"license": "MIT"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

View File

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Single Page Apps for GitHub Pages</title>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// https://github.com/rafrex/spa-github-pages
// Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
// ----------------------------------------------------------------------
// This script takes the current url and converts the path and query
// string into just a query string, and then redirects the browser
// to the new url with only a query string and hash fragment,
// e.g. http://www.foo.tld/one/two?a=b&c=d#qwe, becomes
// http://www.foo.tld/?p=/one/two&q=a=b~and~c=d#qwe
// Note: this 404.html file must be at least 512 bytes for it to work
// with Internet Explorer (it is currently > 512 bytes)
// If you're creating a Project Pages site and NOT using a custom domain,
// then set segmentCount to 1 (enterprise users may need to set it to > 1).
// This way the code will only replace the route part of the path, and not
// the real directory in which the app resides, for example:
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
// https://username.github.io/repo-name/?p=/one/two&q=a=b~and~c=d#qwe
// Otherwise, leave segmentCount as 0.
var segmentCount = 0;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + segmentCount).join('/') + '/?p=/' +
l.pathname.slice(1).split('/').slice(segmentCount).join('/').replace(/&/g, '~and~') +
(l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>

View File

@ -1 +0,0 @@
styleguide.cion.visualjerk.de

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script src="/babel-standalone.js"></script>
<title>Human Connection - Styleguide</title>
</head>
<body>
<noscript>
<strong>We're sorry but this CION doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,8 +0,0 @@
import system from './system'
import { tokens } from './system/tokens'
import * as utils from './system/utils'
import * as mixins from './system/mixins'
export { tokens, utils, mixins }
export default system

View File

@ -1,9 +0,0 @@
module.exports = function(source, map) {
this.callback(
null,
`export default function (Component) {
Component.options.__docs = ${JSON.stringify(source)}
}`,
map
)
}

View File

@ -1,7 +0,0 @@
module.exports = function(source, map) {
this.callback(
null,
`export default function () {}`,
map
)
}

Some files were not shown because too many files have changed in this diff Show More