Merge branch 'extract-styleguide' into add-styleguide-dev-mode

This commit is contained in:
Grzegorz Leoniec 2019-02-26 17:48:31 +01:00
commit 25c759c48c
No known key found for this signature in database
GPG Key ID: 3AA43686D4EB1377
41 changed files with 1413 additions and 222 deletions

1
.gitignore vendored
View File

@ -80,3 +80,4 @@ static/uploads
cypress/videos
cypress/screenshots/
cypress.env.json

View File

@ -18,22 +18,23 @@ 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

View File

@ -23,8 +23,9 @@ $ 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

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,7 +8,9 @@ services:
volumes:
- .:/nitro-web
- node_modules:/nitro-web/node_modules
- nuxt:/nitro-web/.nuxt
command: yarn run dev
volumes:
node_modules:
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.",
@ -100,6 +101,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.",
@ -100,6 +101,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

@ -46,18 +46,22 @@
"express": "~4.16.4",
"graphql": "~14.1.1",
"jsonwebtoken": "~8.5.0",
"linkify-it": "~2.1.0",
"nuxt": "~2.4.3",
"nuxt-env": "~0.1.0",
"portal-vue": "~1.5.1",
"@human-connection/styleguide": "~0.5.2",
"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"
},
"devDependencies": {
"@babel/core": "~7.3.3",
"@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",

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

@ -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: {

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

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>bold</title>
<path d="M16 7h-9v18h11c2.8 0 5-2.2 5-5 0-2.2-1.4-4-3.3-4.7 0.8-0.9 1.3-2 1.3-3.3 0-2.8-2.2-5-5-5zM9 15v-6h7c1.7 0 3 1.3 3 3s-1.3 3-3 3h-7zM9 23v-6h9c1.7 0 3 1.3 3 3s-1.3 3-3 3h-9zM16 5v0c3.9 0 7 3.1 7 7 0 0.9-0.2 1.8-0.5 2.6 1.5 1.3 2.5 3.3 2.5 5.4 0 3.9-3.1 7-7 7h-13v-22h11zM11 11v0 2h5c0.6 0 1-0.4 1-1s-0.4-1-1-1h-5zM11 19v0 2h7c0.6 0 1-0.4 1-1s-0.4-1-1-1h-7z"></path>
</svg>

After

Width:  |  Height:  |  Size: 531 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>italic</title>
<path d="M11.75 5h10.031l-0.094 1.063-0.188 3-0.063 0.938h-2l-0.875 12h2l-0.063 1.063-0.188 3-0.063 0.938h-10.031l0.094-1.063 0.188-3 0.063-0.938h2l0.875-12h-2l0.063-1.063 0.188-3zM13.625 7l-0.063 1h2l-0.063 1.063-1 14-0.063 0.938h-2l-0.063 1h6l0.063-1h-2l0.063-1.063 1-14 0.063-0.938h2l0.063-1h-6z"></path>
</svg>

After

Width:  |  Height:  |  Size: 468 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>list-ol</title>
<path d="M5.969 3h2.031v7h-2v-4.531c-0.444 0.255-0.913 0.531-1.594 0.531v-2c0.494 0 1.25-0.656 1.25-0.656zM11 6h17v2h-17v-2zM6.5 12c1.383 0 2.5 1.117 2.5 2.5 0 0.481-0.248 1.090-0.75 1.5l0.031 0.031-0.125 0.094-0.875 0.875h1.719v2h-5v-1.625l0.313-0.281 2.688-2.594c0-0.217-0.283-0.5-0.5-0.5s-0.5 0.283-0.5 0.5v0.5h-2v-0.5c0-1.383 1.117-2.5 2.5-2.5zM11 15h17v2h-17v-2zM4 21h4v1.469l-0.125 0.25-0.406 0.688c0.853 0.398 1.531 1.089 1.531 2.094 0 1.383-1.117 2.5-2.5 2.5h-2.5v-2h2.5c0.217 0 0.5-0.283 0.5-0.5s-0.283-0.5-0.5-0.5h-1.5v-1.375l0.125-0.219 0.25-0.406h-1.375v-2zM11 24h17v2h-17v-2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 759 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>list-ul</title>
<path d="M4 5h6v6h-6v-6zM6 7v2h2v-2h-2zM12 7h15v2h-15v-2zM4 13h6v6h-6v-6zM6 15v2h2v-2h-2zM12 15h15v2h-15v-2zM4 21h6v6h-6v-6zM6 23v2h2v-2h-2zM12 23h15v2h-15v-2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>paragraph</title>
<path d="M12 5h12v2h-2v20h-2v-20h-2v20h-2v-10h-4c-3.302 0-6-2.698-6-6s2.698-6 6-6zM12 7c-2.22 0-4 1.78-4 4s1.78 4 4 4h4v-8h-4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>quote-right</title>
<path d="M4 8h10v10c0 3.302-2.698 6-6 6v-2c2.22 0 4-1.78 4-4h-8v-10zM18 8h10v10c0 3.302-2.698 6-6 6v-2c2.22 0 4-1.78 4-4h-8v-10zM6 10v6h6v-6h-6zM20 10v6h6v-6h-6z"></path>
</svg>

After

Width:  |  Height:  |  Size: 336 B

274
yarn.lock
View File

@ -41,18 +41,18 @@
semver "^5.4.1"
source-map "^0.5.0"
"@babel/core@^7.1.0", "@babel/core@^7.2.2", "@babel/core@~7.3.3":
version "7.3.3"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.3.tgz#d090d157b7c5060d05a05acaebc048bd2b037947"
integrity sha512-w445QGI2qd0E0GlSnq6huRZWPMmQGCp5gd5ZWS4hagn0EiwzxD5QMFkpchyusAyVC1n27OKXzQ0/88aVU9n4xQ==
"@babel/core@^7.1.0", "@babel/core@^7.2.2", "@babel/core@~7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b"
integrity sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA==
dependencies:
"@babel/code-frame" "^7.0.0"
"@babel/generator" "^7.3.3"
"@babel/generator" "^7.3.4"
"@babel/helpers" "^7.2.0"
"@babel/parser" "^7.3.3"
"@babel/parser" "^7.3.4"
"@babel/template" "^7.2.2"
"@babel/traverse" "^7.2.2"
"@babel/types" "^7.3.3"
"@babel/traverse" "^7.3.4"
"@babel/types" "^7.3.4"
convert-source-map "^1.1.0"
debug "^4.1.0"
json5 "^2.1.0"
@ -61,12 +61,12 @@
semver "^5.4.1"
source-map "^0.5.0"
"@babel/generator@^7.0.0", "@babel/generator@^7.2.2", "@babel/generator@^7.3.3":
version "7.3.3"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.3.tgz#185962ade59a52e00ca2bdfcfd1d58e528d4e39e"
integrity sha512-aEADYwRRZjJyMnKN7llGIlircxTCofm3dtV5pmY6ob18MSIuipHpA2yZWkPlycwu5HJcx/pADS3zssd8eY7/6A==
"@babel/generator@^7.0.0", "@babel/generator@^7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e"
integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg==
dependencies:
"@babel/types" "^7.3.3"
"@babel/types" "^7.3.4"
jsesc "^2.5.1"
lodash "^4.17.11"
source-map "^0.5.0"
@ -264,10 +264,10 @@
esutils "^2.0.2"
js-tokens "^4.0.0"
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3", "@babel/parser@^7.3.3":
version "7.3.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.3.tgz#092d450db02bdb6ccb1ca8ffd47d8774a91aef87"
integrity sha512-xsH1CJoln2r74hR+y7cg2B5JCPaTh+Hd+EbBRk9nWGSNspuo6krjhX0Om6RnRQuIvFq8wVXCLKH3kwKDYhanSg==
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c"
integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==
"@babel/plugin-proposal-async-generator-functions@^7.1.0", "@babel/plugin-proposal-async-generator-functions@^7.2.0":
version "7.2.0"
@ -816,25 +816,25 @@
"@babel/parser" "^7.2.2"
"@babel/types" "^7.2.2"
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3":
version "7.2.3"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8"
integrity sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.3", "@babel/traverse@^7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06"
integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ==
dependencies:
"@babel/code-frame" "^7.0.0"
"@babel/generator" "^7.2.2"
"@babel/generator" "^7.3.4"
"@babel/helper-function-name" "^7.1.0"
"@babel/helper-split-export-declaration" "^7.0.0"
"@babel/parser" "^7.2.3"
"@babel/types" "^7.2.2"
"@babel/parser" "^7.3.4"
"@babel/types" "^7.3.4"
debug "^4.1.0"
globals "^11.1.0"
lodash "^4.17.10"
lodash "^4.17.11"
"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3":
version "7.3.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.3.tgz#6c44d1cdac2a7625b624216657d5bc6c107ab436"
integrity sha512-2tACZ80Wg09UnPg5uGAOUvvInaqLk3l/IAhQzlxLQOIXacr6bMsra5SH6AWw/hIDRCSbCdHP2KzSOD+cT7TzMQ==
"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed"
integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==
dependencies:
esutils "^2.0.2"
lodash "^4.17.11"
@ -4930,6 +4930,13 @@ fastparse@^1.1.1:
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==
fault@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa"
integrity sha512-o2eo/X2syzzERAtN5LcGbiVQ0WwZSlN3qLtadwAz3X8Bu+XWD16dja/KMsjZLiQr+BLGPDnHGkc4yUJf1Xpkpw==
dependencies:
format "^0.2.2"
fb-watchman@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
@ -5142,6 +5149,11 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
format@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -5608,6 +5620,11 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
highlight.js@~9.13.0:
version "9.13.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -7118,6 +7135,13 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
linkify-it@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.1.0.tgz#c4caf38a6cd7ac2212ef3c7d2bde30a91561f9db"
integrity sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==
dependencies:
uc.micro "^1.0.1"
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@ -7335,6 +7359,14 @@ lowercase-keys@^1.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
lowlight@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc"
integrity sha512-xrGGN6XLL7MbTMdPD6NfWPwY43SNkjf/d0mecSx/CW36fUZTjRHEq0/Cdug3TWKtRXLWi7iMl1eP0olYxj/a4A==
dependencies:
fault "^1.0.2"
highlight.js "~9.13.0"
lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.3:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
@ -8222,6 +8254,11 @@ ora@^3.0.0:
strip-ansi "^5.0.0"
wcwidth "^1.0.1"
orderedmap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.0.0.tgz#d90fc2ba1ed085190907d601dec6e6a53f8d41ba"
integrity sha1-2Q/Cuh7QhRkJB9YB3sbmpT+NQbo=
os-browserify@^0.3.0, os-browserify@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
@ -9317,6 +9354,114 @@ prompts@^2.0.1:
kleur "^3.0.2"
sisteransi "^1.0.0"
prosemirror-commands@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.0.7.tgz#e5a2ba821e29ea7065c88277fe2c3d7f6b0b9d37"
integrity sha512-IR8yMSdw7XlKuF68tydAak1J9P/lLD5ohsrL7pzoLsJAJAQU7mVPDXtGbQrrm0mesddFjcc1zNo/cJQN3lRYnA==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-dropcursor@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.1.1.tgz#c60ed1ed6c58804a06a75db06a0d993b087b7622"
integrity sha512-GeUyMO/tOEf8MXrP7Xb7UIMrfK86OGh0fnyBrHfhav4VjY9cw65mNoqHy87CklE5711AhCP5Qzfp8RL/hVKusg==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
prosemirror-view "^1.1.0"
prosemirror-gapcursor@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.0.3.tgz#acc6537fc5a35e9b38966f91a199a382dfc715c4"
integrity sha512-X+hJhr42PcHWiSWL+lI5f/UeOhXCxlBFb8M6O8aG1hssmaRrW7sS2/Fjg5jFV+pTdS1REFkmm1occh01FMdDIQ==
dependencies:
prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-view "^1.0.0"
prosemirror-history@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.0.3.tgz#5fb8591adfc272afaaf0b41bec64ee7d9522a118"
integrity sha512-IfFGbhafSx+R3aq7nLJGkXeu2iaUiP8mkU3aRu2uQcIIjU8Fq7RJfuvhIOJ2RNUoSyqF/ANkdTjnZ74F5eHs1Q==
dependencies:
prosemirror-state "^1.2.2"
prosemirror-transform "^1.0.0"
rope-sequence "^1.2.0"
prosemirror-inputrules@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.0.1.tgz#f63305fd966379f218e82ca76a2a9b328b66dc7b"
integrity sha512-UHy22NmwxS5WIMQYkzraDttQAF8mpP82FfbJsmKFfx6jwkR/SZa+ZhbkLY0zKQ5fBdJN7euj36JG/B5iAlrpxA==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.0.1.tgz#03ef32b828e3a859dfb570eb84928bf2e5330bc2"
integrity sha512-e79ApE7PXXZMFtPz7WbjycjAFd1NPjgY1MkecVz98tqwlBSggXWXYQnWFk6x7UkmnBYRHHbXHkR/RXmu2wyBJg==
dependencies:
prosemirror-state "^1.0.0"
w3c-keyname "^1.1.8"
prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.7.0.tgz#1fde0fd7cb2f9ead0be1581ad9f04593002a65aa"
integrity sha512-/6ul6guiqyAl5I+0qbnL7SlmuX0DEfYqjvzeLUVEnb7nwF/vmKZuWqbjEG2tqi/9SSudvd3UxQTBDHvxy9hQwA==
dependencies:
orderedmap "^1.0.0"
prosemirror-schema-list@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.0.2.tgz#8381fb0c1eaf439d848059f62e2fac517033c2ef"
integrity sha512-IJ4DEpUEymfO+NNA4DAgCMF39XiQqpmCoPYY3SXa1jYcVgObGpGfJlSjZYVFEpimoLI7/mLoOLDhCtpGCRhTfg==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-state@^1.0.0, prosemirror-state@^1.2.1, prosemirror-state@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.2.2.tgz#8df26d95fd6fd327c0f9984a760e84d863204154"
integrity sha512-j8aC/kf9BJSCQau485I/9pj39XQoce+TqH5xzekT7WWFARTsRYFLJtiXBcCKakv1VSeev+sC3bJP0pLfz7Ft8g==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-tables@^0.7.10, prosemirror-tables@^0.7.9:
version "0.7.10"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-0.7.10.tgz#4b0f623422b4b8f84cdc9c559f8a87579846b3ba"
integrity sha512-VIu7UGS9keYEHs0Y6AEOTGbNE9QI2rL1OKng4vV6yoTshW/lYcb+s3hGXI12i+WLMjDVm7ujhfdWrpKpvFZOkQ==
dependencies:
prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-view "^1.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.1.3.tgz#28cfdf1f9ee514edc40466be7b7db39eed545fdf"
integrity sha512-1O6Di5lOL1mp4nuCnQNkHY7l2roIW5y8RH4ZG3hMYmkmDEWzTaFFnxxAAHsE5ipGLBSRcTlP7SsDhYBIdSuLpQ==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-utils@^0.7.5:
version "0.7.6"
resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.7.6.tgz#c462ddfbf2452e56e4b25d1f02b34caccddb0f33"
integrity sha512-vzsCBTiJ56R3nRDpIJnKOJzsZP7KFO8BkXk7zvQgQiXpml2o/djPCRhuyaFc7VTqSHlLPQHVI1feTLAwHp+prQ==
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.7.1.tgz#f0ea75faa6d7bd25ea22897dd5bae35708c59a28"
integrity sha512-UFY/h4i5H1Yen8u2ZTve0WL+nh/y1qU3geC3SrWl5yIKSgGbvllD5vr5LxmeRgVsY8hb+oDXRHk5KvLwqmu7Lg==
dependencies:
prosemirror-model "^1.1.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@ -9889,6 +10034,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rope-sequence@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.2.2.tgz#49c4e5c2f54a48e990b050926771e2871bcb31ce"
integrity sha1-ScTlwvVKSOmQsFCSZ3HihxvLMc4=
rsvp@^3.3.3:
version "3.6.2"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
@ -10508,6 +10658,11 @@ string-argv@0.0.2:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.0.2.tgz#dac30408690c21f3c3630a3ff3a05877bdcbd736"
integrity sha1-2sMECGkMIfPDYwo/86BYd73L1zY=
string-hash@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"
integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=
string-length@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
@ -10890,6 +11045,57 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiptap-commands@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.6.0.tgz#567b7b218bd7d1c1214534a2007acbb7b8d06688"
integrity sha512-9HO8UYJz1qGyqsHn0+PifmndlRTInqfcb7vjNDvqWQA2P7r8koJqrP8CYMR0DXQHlys1druJnaBaOzLa1d5PQQ==
dependencies:
prosemirror-commands "^1.0.7"
prosemirror-inputrules "^1.0.1"
prosemirror-schema-list "^1.0.2"
prosemirror-state "^1.2.2"
tiptap-utils "^1.2.0"
tiptap-extensions@^1.13.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.13.0.tgz#1c78223d68f4c321909c5448764d2b5a32bad7f3"
integrity sha512-56y5uXAnkdZ/9MmTuk2fmbglUwmfECVOz2DZh/5OI5nsPclVNk+OFjNFEbZ91rb3fC4NOtFdfNQVKyqrBe9pNA==
dependencies:
lowlight "^1.11.0"
prosemirror-history "^1.0.3"
prosemirror-state "^1.2.2"
prosemirror-tables "^0.7.10"
prosemirror-utils "^0.7.5"
prosemirror-view "^1.7.1"
tiptap "^1.13.0"
tiptap-commands "^1.6.0"
tiptap-utils@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.2.0.tgz#e1e566af1212acff6b2e3c4ca6edc21ebf306440"
integrity sha512-p8Q0UfNhYHXqMDSwvCc6x0Vm95AYgM/f1V+8oNu9FI0aRWwXpTwIJj+1CAGO1mb6NFUSxn9HcZaUvEcBKR5WzQ==
dependencies:
prosemirror-model "^1.7.0"
prosemirror-state "^1.2.2"
prosemirror-tables "^0.7.9"
prosemirror-utils "^0.7.5"
tiptap@^1.13.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.13.0.tgz#52b086fc8d4df7534123d31571366dd90ee62a23"
integrity sha512-kwzgtOY5PnbSfzMyNPFchI/Cyi1O3kFNiRP7K4p6t0zcNCXy9EaWgaM/U2bKArI0/HxG/GSPM6RTTCCOF3I6EA==
dependencies:
prosemirror-commands "^1.0.7"
prosemirror-dropcursor "^1.1.1"
prosemirror-gapcursor "^1.0.3"
prosemirror-inputrules "^1.0.1"
prosemirror-keymap "^1.0.1"
prosemirror-model "^1.7.0"
prosemirror-state "^1.2.1"
prosemirror-view "^1.7.1"
tiptap-commands "^1.6.0"
tiptap-utils "^1.2.0"
title-case@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa"
@ -11100,6 +11306,11 @@ ua-parser-js@^0.7.19:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
uc.micro@^1.0.1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
uglify-js@3.4.x, uglify-js@^3.1.4:
version "3.4.9"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
@ -11581,6 +11792,11 @@ w3c-hr-time@^1.0.1:
dependencies:
browser-process-hrtime "^0.1.2"
w3c-keyname@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-1.1.8.tgz#4e2219663760fd6535b7a1550f1552d71fc9372c"
integrity sha512-2HAdug8GTiu3b4NYhssdtY8PXRue3ICnh1IlxvZYl+hiINRq0GfNWei3XOPDg8L0PsxbmYjWVLuLj6BMRR/9vA==
walker@~1.0.5:
version "1.0.7"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"