Merge branch 'master' into migrate-styleguide-icons

This commit is contained in:
Alina Beck 2019-11-20 19:35:03 +03:00
commit a210dd599d
140 changed files with 7976 additions and 3264 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
backend/snapshots/* linguist-generated=true

17
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 30
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

1
.gitignore vendored
View File

@ -18,3 +18,4 @@ cypress.env.json
**/coverage
release/
*~

View File

@ -41,7 +41,7 @@ script:
- docker-compose down
- docker-compose -f docker-compose.yml up -d
- wait-on http://localhost:7474
- yarn run cypress:run
- yarn run cypress:run --record
# Coverage
- yarn run codecov

View File

@ -6,6 +6,7 @@
* [Neo4J](neo4j/README.md)
* [Backend](backend/README.md)
* [GraphQL](backend/graphql.md)
* [neo4j-graphql-js](backend/neo4j-graphql-js.md)
* [Webapp](webapp/README.md)
* [Components](webapp/components.md)
* [HTML](webapp/html.md)

View File

@ -1 +1 @@
0.1.9
0.1.10

View File

@ -1,7 +1,6 @@
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=letmein
GRAPHQL_PORT=4000
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
SMTP_HOST=

View File

@ -0,0 +1,16 @@
# neo4j-graphql.js
We use an npm package called `neo4j-graphql-js` as a cypher query builder. This
library also generates resolvers for graphql queries, unless we implement them
ourselves.
## Debugging
As you can see in their [documentation](https://github.com/neo4j-graphql/neo4j-graphql-js)
it is possible to log out the generated cypher statements. To do so, run the
backend like this:
```sh
DEBUG=neo4j-graphql-js yarn run dev
```

View File

@ -35,12 +35,12 @@
},
"dependencies": {
"@hapi/joi": "^16.1.7",
"@sentry/node": "^5.8.0",
"@sentry/node": "^5.9.0",
"apollo-cache-inmemory": "~1.6.3",
"apollo-client": "~2.6.4",
"apollo-link-context": "~1.0.19",
"apollo-link-http": "~1.5.16",
"apollo-server": "~2.9.7",
"apollo-server": "~2.9.9",
"apollo-server-express": "^2.9.7",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3",
@ -64,26 +64,26 @@
"linkifyjs": "~2.1.8",
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.3",
"metascraper": "^4.10.3",
"metascraper-audio": "^5.7.17",
"metascraper-author": "^5.7.17",
"metascraper": "^5.8.8",
"metascraper-audio": "^5.8.7",
"metascraper-author": "^5.8.7",
"metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.7.17",
"metascraper-description": "^5.7.17",
"metascraper-image": "^5.7.17",
"metascraper-lang": "^5.7.17",
"metascraper-lang-detector": "^4.8.5",
"metascraper-logo": "^5.7.17",
"metascraper-publisher": "^5.7.17",
"metascraper-soundcloud": "^5.7.17",
"metascraper-title": "^5.7.17",
"metascraper-url": "^5.7.17",
"metascraper-video": "^5.7.17",
"metascraper-youtube": "^5.7.17",
"metascraper-date": "^5.8.7",
"metascraper-description": "^5.8.7",
"metascraper-image": "^5.8.7",
"metascraper-lang": "^5.8.7",
"metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.8.7",
"metascraper-publisher": "^5.8.7",
"metascraper-soundcloud": "^5.8.7",
"metascraper-title": "^5.8.7",
"metascraper-url": "^5.8.7",
"metascraper-video": "^5.8.7",
"metascraper-youtube": "^5.8.7",
"minimatch": "^3.0.4",
"mustache": "^3.1.0",
"neo4j-driver": "~1.7.6",
"neo4j-graphql-js": "^2.8.0",
"neo4j-graphql-js": "^2.9.3",
"neode": "^0.3.3",
"node-fetch": "~2.6.0",
"nodemailer": "^6.3.1",
@ -105,24 +105,24 @@
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.7.1",
"@babel/register": "~7.7.0",
"apollo-server-testing": "~2.9.7",
"apollo-server-testing": "~2.9.9",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0",
"chai": "~4.2.0",
"cucumber": "~6.0.3",
"cucumber": "~6.0.5",
"eslint": "~6.6.0",
"eslint-config-prettier": "~6.5.0",
"eslint-config-prettier": "~6.7.0",
"eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~23.0.3",
"eslint-plugin-jest": "~23.0.4",
"eslint-plugin-node": "~10.0.0",
"eslint-plugin-prettier": "~3.1.1",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1",
"jest": "~24.9.0",
"nodemon": "~1.19.4",
"prettier": "~1.18.2",
"prettier": "~1.19.1",
"supertest": "~4.0.2"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +0,0 @@
import {
GraphQLLowerCaseDirective,
GraphQLTrimDirective,
GraphQLDefaultToDirective,
} from 'graphql-custom-directives'
export default function applyDirectives(augmentedSchema) {
const directives = [GraphQLLowerCaseDirective, GraphQLTrimDirective, GraphQLDefaultToDirective]
augmentedSchema._directives.push.apply(augmentedSchema._directives, directives)
return augmentedSchema
}

View File

@ -1,9 +0,0 @@
import { GraphQLDate, GraphQLTime, GraphQLDateTime } from 'graphql-iso-date'
export default function applyScalars(augmentedSchema) {
augmentedSchema._typeMap.Date = GraphQLDate
augmentedSchema._typeMap.Time = GraphQLTime
augmentedSchema._typeMap.DateTime = GraphQLDateTime
return augmentedSchema
}

View File

@ -16,7 +16,6 @@ const {
NEO4J_URI = 'bolt://localhost:7687',
NEO4J_USERNAME = 'neo4j',
NEO4J_PASSWORD = 'neo4j',
GRAPHQL_PORT = 4000,
CLIENT_URI = 'http://localhost:3000',
GRAPHQL_URI = 'http://localhost:4000',
} = process.env
@ -36,7 +35,6 @@ export const smtpConfigs = {
}
export const neo4jConfigs = { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD }
export const serverConfigs = {
GRAPHQL_PORT,
CLIENT_URI,
GRAPHQL_URI,
PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true',

View File

@ -0,0 +1,5 @@
//* This is a fake ES2015 template string, just to benefit of syntax
// highlighting of `gql` template strings in certain editors.
export function gql(strings) {
return strings.join('')
}

View File

@ -2,7 +2,8 @@ import createServer from './server'
import CONFIG from './config'
const { app } = createServer()
app.listen({ port: CONFIG.GRAPHQL_PORT }, () => {
const url = new URL(CONFIG.GRAPHQL_URI)
app.listen({ port: url.port }, () => {
/* eslint-disable-next-line no-console */
console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`)
})

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,4 +1,4 @@
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories'
import { createTestClient } from 'apollo-server-testing'
import { neode, getDriver } from '../../bootstrap/neo4j'

View File

@ -1,4 +1,4 @@
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories'
import { createTestClient } from 'apollo-server-testing'
import { neode, getDriver } from '../../bootstrap/neo4j'

View File

@ -1,5 +1,5 @@
import { gql } from '../helpers/jest'
import Factory from '../seed/factories'
import { gql } from '../jest/helpers'
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../server'

View File

@ -41,20 +41,6 @@ const isMySocialMedia = rule({
return socialMedia.ownedBy.node.id === user.id
})
/* TODO: decide if we want to remove this check: the check
* `onlyEnabledContent` throws authorization errors only if you have
* arguments for `disabled` or `deleted` assuming these are filter
* parameters. Soft-delete middleware obfuscates data on its way out
* anyways. Furthermore, `neo4j-graphql-js` offers many ways to filter for
* data so I believe, this is not a good check anyways.
*/
const onlyEnabledContent = rule({
cache: 'strict',
})(async (parent, args, ctx, info) => {
const { disabled, deleted } = args
return !(disabled || deleted)
})
const invitationLimitReached = rule({
cache: 'no_cache',
})(async (parent, args, { user, driver }) => {
@ -125,7 +111,8 @@ const permissions = shield(
reports: isModerator,
statistics: allow,
currentUser: allow,
Post: or(onlyEnabledContent, isModerator),
Post: allow,
profilePagePosts: allow,
Comment: allow,
User: or(noEmailFilter, isAdmin),
isLoggedIn: allow,
@ -134,7 +121,6 @@ const permissions = shield(
PostsEmotionsByCurrentUser: isAuthenticated,
blockedUsers: isAuthenticated,
notifications: isAuthenticated,
profilePagePosts: or(onlyEnabledContent, isModerator),
Donations: isAuthenticated,
},
Mutation: {

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../server'
import Factory from '../seed/factories'
import { gql } from '../jest/helpers'
import { gql } from '../helpers/jest'
import { getDriver, neode as getNeode } from '../bootstrap/neo4j'
const factory = Factory()

View File

@ -25,9 +25,5 @@ export default {
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info)
},
CreateCategory: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Category')))
return resolve(root, args, context, info)
},
},
}

View File

@ -1,5 +1,5 @@
import Factory from '../seed/factories'
import { gql } from '../jest/helpers'
import { gql } from '../helpers/jest'
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
import createServer from '../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -3,9 +3,7 @@ const isModerator = ({ user }) => {
}
const setDefaultFilters = (resolve, root, args, context, info) => {
if (typeof args.deleted !== 'boolean') {
args.deleted = false
}
if (!isModerator(context)) {
args.disabled = false

View File

@ -1,5 +1,5 @@
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
@ -341,76 +341,6 @@ describe('softDeleteMiddleware', () => {
})
})
})
describe('filter (deleted: true)', () => {
beforeEach(() => {
graphqlQuery = gql`
{
Post(deleted: true) {
title
}
}
`
})
describe('as user', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
})
it('throws authorisation error', async () => {
const { data, errors } = await action()
expect(data).toEqual({ Post: null })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('as moderator', () => {
beforeEach(async () => {
authenticatedUser = await moderator.toJson()
})
it('does not show deleted posts', async () => {
const expected = { data: { Post: [{ title: 'UNAVAILABLE' }] } }
await expect(action()).resolves.toMatchObject(expected)
})
})
})
describe('filter (disabled: true)', () => {
beforeEach(() => {
graphqlQuery = gql`
{
Post(disabled: true) {
title
}
}
`
})
describe('as user', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
})
it('throws authorisation error', async () => {
const { data, errors } = await action()
expect(data).toEqual({ Post: null })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('as moderator', () => {
beforeEach(async () => {
authenticatedUser = await moderator.toJson()
})
it('shows disabled posts', async () => {
const expected = { data: { Post: [{ title: 'Disabled post' }] } }
await expect(action()).resolves.toMatchObject(expected)
})
})
})
})
})
})

View File

@ -127,6 +127,10 @@ module.exports = {
type: 'boolean',
default: false,
},
showShoutsPublicly: {
type: 'boolean',
default: false,
},
locale: {
type: 'string',
allow: [null],

View File

@ -1,9 +0,0 @@
export const undefinedToNull = list => {
const resolvers = {}
list.forEach(key => {
resolvers[key] = async (parent, params, context, resolveInfo) => {
return typeof parent[key] === 'undefined' ? null : parent[key]
}
})
return resolvers
}

View File

@ -1,13 +1,8 @@
import { makeAugmentedSchema } from 'neo4j-graphql-js'
import CONFIG from './../config'
import applyScalars from './../bootstrap/scalars'
import applyDirectives from './../bootstrap/directives'
import typeDefs from './types'
import resolvers from './resolvers'
export default applyScalars(
applyDirectives(
makeAugmentedSchema({
export default makeAugmentedSchema({
typeDefs,
resolvers,
config: {
@ -26,31 +21,7 @@ export default applyScalars(
'REPORTED',
'Donations',
],
// add 'User' here as soon as possible
},
mutation: {
exclude: [
'Badge',
'Embed',
'InvitationCode',
'EmailAddress',
'Notfication',
'Post',
'Comment',
'Statistics',
'LoggedInUser',
'Location',
'SocialMedia',
'User',
'EMOTED',
'NOTIFIED',
'REPORTED',
'Donations',
],
// add 'User' here as soon as possible
mutation: false,
},
debug: !!CONFIG.DEBUG,
},
}),
),
)
})

View File

@ -3,7 +3,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
export default {
Query: {
Badge: async (object, args, context, resolveInfo) => {
return neo4jgraphql(object, args, context, resolveInfo, false)
return neo4jgraphql(object, args, context, resolveInfo)
},
},
}

View File

@ -1,5 +1,5 @@
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'

View File

@ -1,5 +1,5 @@
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { getDriver, neode as getNeode } from '../../bootstrap/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -3,7 +3,7 @@ import fs from 'fs'
import path from 'path'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
jest.mock('node-fetch')
const { Response } = jest.requireActual('node-fetch')
@ -15,15 +15,12 @@ afterEach(() => {
let variables = {}
const HumanConnectionOrg = fs.readFileSync(
path.join(__dirname, '../../jest/snapshots/embeds/HumanConnectionOrg.html'),
'utf8',
)
const pr960 = fs.readFileSync(
path.join(__dirname, '../../jest/snapshots/embeds/pr960.html'),
path.join(__dirname, '../../../snapshots/embeds/HumanConnectionOrg.html'),
'utf8',
)
const pr960 = fs.readFileSync(path.join(__dirname, '../../../snapshots/embeds/pr960.html'), 'utf8')
const babyLovesCat = fs.readFileSync(
path.join(__dirname, '../../jest/snapshots/embeds/babyLovesCat.html'),
path.join(__dirname, '../../../snapshots/embeds/babyLovesCat.html'),
'utf8',
)
@ -88,7 +85,7 @@ describe('Query', () => {
})
it('shows some default data', async () => {
const expected = expect.objectContaining({
await expect(embedAction(variables)).resolves.toMatchObject({
data: {
embed: {
audio: null,
@ -98,7 +95,7 @@ describe('Query', () => {
html: null,
image: null,
lang: null,
publisher: 'YouTube',
publisher: null,
sources: ['resource'],
title: null,
type: 'link',
@ -106,8 +103,8 @@ describe('Query', () => {
video: null,
},
},
errors: undefined,
})
await expect(embedAction(variables)).resolves.toEqual(expected)
})
})
@ -120,7 +117,7 @@ describe('Query', () => {
})
it('does not crash if embed provider returns invalid JSON', async () => {
const expected = expect.objectContaining({
await expect(embedAction(variables)).resolves.toMatchObject({
data: {
embed: {
audio: null,
@ -140,8 +137,8 @@ describe('Query', () => {
video: null,
},
},
errors: undefined,
})
await expect(embedAction(variables)).resolves.toEqual(expected)
})
})
@ -154,7 +151,7 @@ describe('Query', () => {
})
it('returns meta data even if no embed html can be retrieved', async () => {
const expected = expect.objectContaining({
await expect(embedAction(variables)).resolves.toMatchObject({
data: {
embed: {
type: 'link',
@ -174,8 +171,8 @@ describe('Query', () => {
html: null,
},
},
errors: undefined,
})
await expect(embedAction(variables)).resolves.toEqual(expected)
})
})
@ -188,7 +185,7 @@ describe('Query', () => {
})
it('returns meta data plus youtube iframe html', async () => {
const expected = expect.objectContaining({
await expect(embedAction(variables)).resolves.toMatchObject({
data: {
embed: {
type: 'video',
@ -208,8 +205,8 @@ describe('Query', () => {
'<iframe width="480" height="270" src="https://www.youtube.com/embed/qkdXAtO40Fo?start=18&feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
},
},
})
await expect(embedAction(variables)).resolves.toEqual(expected)
errors: undefined,
})
})
})
})

View File

@ -2,7 +2,7 @@ import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import { getDriver, neode as getNeode } from '../../bootstrap/neo4j'
import createServer from '../../server'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
const factory = Factory()
const driver = getDriver()

View File

@ -1,5 +1,5 @@
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'

View File

@ -1,5 +1,5 @@
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { getDriver } from '../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'

View File

@ -1,5 +1,5 @@
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createPasswordReset from './helpers/createPasswordReset'
import createServer from '../../server'

View File

@ -29,7 +29,7 @@ const filterForBlockedUsers = async (params, context) => {
}
const maintainPinnedPosts = params => {
const pinnedPostFilter = { pinnedBy_in: { role_in: ['admin'] } }
const pinnedPostFilter = { pinned: true }
if (isEmpty(params.filter)) {
params.filter = { OR: [pinnedPostFilter, {}] }
} else {
@ -43,15 +43,15 @@ export default {
Post: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context)
params = await maintainPinnedPosts(params)
return neo4jgraphql(object, params, context, resolveInfo, false)
return neo4jgraphql(object, params, context, resolveInfo)
},
findPosts: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context)
return neo4jgraphql(object, params, context, resolveInfo, false)
return neo4jgraphql(object, params, context, resolveInfo)
},
profilePagePosts: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context)
return neo4jgraphql(object, params, context, resolveInfo, false)
return neo4jgraphql(object, params, context, resolveInfo)
},
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
const session = context.driver.session()

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'
@ -210,6 +210,7 @@ describe('Post', () => {
data: {
Post: expect.arrayContaining(expected),
},
errors: undefined,
})
})
})
@ -229,7 +230,9 @@ describe('Post', () => {
await user.relateTo(followedUser, 'following')
variables = { filter: { author: { followedBy_some: { id: 'current-user' } } } }
const expected = {
await expect(
query({ query: postQueryFilteredByUsersFollowed, variables }),
).resolves.toMatchObject({
data: {
Post: [
{
@ -238,10 +241,8 @@ describe('Post', () => {
},
],
},
}
await expect(
query({ query: postQueryFilteredByUsersFollowed, variables }),
).resolves.toMatchObject(expected)
errors: undefined,
})
})
})
})

View File

@ -1,5 +1,5 @@
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { getDriver, neode as getNeode } from '../../bootstrap/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { getDriver, neode as getNeode } from '../../bootstrap/neo4j'
const factory = Factory()

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { neode, getDriver } from '../../bootstrap/neo4j'
const driver = getDriver()

View File

@ -1,7 +1,7 @@
import jwt from 'jsonwebtoken'
import CONFIG from './../../config'
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { createTestClient } from 'apollo-server-testing'
import createServer, { context } from '../../server'
import encode from '../../jwt/encode'

View File

@ -54,7 +54,7 @@ export default {
user = await user.toJson()
return [user.node]
}
return neo4jgraphql(object, args, context, resolveInfo, false)
return neo4jgraphql(object, args, context, resolveInfo)
},
},
Mutation: {
@ -177,6 +177,8 @@ export default {
'termsAndConditionsAgreedVersion',
'termsAndConditionsAgreedAt',
'allowEmbedIframes',
'showShoutsPublicly',
'locale',
],
boolean: {
followedByCurrentUser:

View File

@ -1,5 +1,5 @@
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server'
import Factory from '../../../seed/factories'
import { gql } from '../../../jest/helpers'
import { gql } from '../../../helpers/jest'
import { neode, getDriver } from '../../../bootstrap/neo4j'
const driver = getDriver()

View File

@ -1 +0,0 @@
scalar Date

View File

@ -1 +0,0 @@
scalar DateTime

View File

@ -1 +0,0 @@
scalar Time

View File

@ -3,8 +3,6 @@ type Badge {
type: BadgeType!
status: BadgeStatus!
icon: String!
#createdAt: DateTime
#updatedAt: DateTime
createdAt: String
updatedAt: String

View File

@ -1,13 +1,41 @@
enum _CategoryOrdering {
id_asc
id_desc
name_asc
name_desc
slug_asc
slug_desc
icon_asc
icon_desc
createdAt_asc
createdAt_desc
updatedAt_asc
updatedAt_desc
postCount_asc
postCount_desc
}
type Category {
id: ID!
name: String!
slug: String
icon: String!
#createdAt: DateTime
#updatedAt: DateTime
createdAt: String
updatedAt: String
posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN")
postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)")
}
type Query {
Category(
id: ID
name: String
slug: String
icon: String
createdAt: String
updatedAt: String
first: Int
offset: Int
orderBy: [_CategoryOrdering]
): [Category]
}

View File

@ -1,3 +1,41 @@
enum _CommentOrdering {
id_asc
id_desc
content_asc
content_desc
createdAt_asc
createdAt_desc
updatedAt_asc
updatedAt_desc
}
input _CommentFilter {
AND: [_CommentFilter!]
OR: [_CommentFilter!]
id: ID
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
author: _UserFilter
author_not: _UserFilter
author_in: [_UserFilter!]
author_not_in: [_UserFilter!]
content: String
content_not: String
content_in: [String!]
content_not_in: [String!]
content_contains: String
content_not_contains: String
content_starts_with: String
content_not_starts_with: String
content_ends_with: String
content_not_ends_with: String
post: _PostFilter
post_not: _PostFilter
post_in: [_PostFilter!]
post_not_in: [_PostFilter!]
}
type Comment {
id: ID!
activityId: String
@ -12,6 +50,19 @@ type Comment {
disabledBy: User @relation(name: "DISABLED", direction: "IN")
}
type Query {
Comment(
id: ID
content: String
createdAt: String
updatedAt: String
first: Int
offset: Int
orderBy: [_CommentOrdering]
filter: _CommentFilter
): [Comment]
}
type Mutation {
CreateComment(
id: ID

View File

@ -3,8 +3,6 @@ type EMOTED @relation(name: "EMOTED") {
to: Post
emotion: Emotion
# createdAt: DateTime
# updatedAt: DateTime
createdAt: String
updatedAt: String
}

View File

@ -2,9 +2,6 @@ type InvitationCode {
id: ID!
token: String
generatedBy: User @relation(name: "GENERATED", direction: "IN")
#createdAt: DateTime
#usedAt: DateTime
createdAt: String
}

View File

@ -1,26 +1,101 @@
input _PostFilter {
AND: [_PostFilter!]
OR: [_PostFilter!]
id: ID
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
author: _UserFilter
author_not: _UserFilter
author_in: [_UserFilter!]
author_not_in: [_UserFilter!]
title: String
title_not: String
title_in: [String!]
title_not_in: [String!]
title_contains: String
title_not_contains: String
title_starts_with: String
title_not_starts_with: String
title_ends_with: String
title_not_ends_with: String
slug: String
slug_not: String
slug_in: [String!]
slug_not_in: [String!]
slug_contains: String
slug_not_contains: String
slug_starts_with: String
slug_not_starts_with: String
slug_ends_with: String
slug_not_ends_with: String
content: String
content_not: String
content_in: [String!]
content_not_in: [String!]
content_contains: String
content_not_contains: String
content_starts_with: String
content_not_starts_with: String
content_ends_with: String
content_not_ends_with: String
image: String
visibility: Visibility
visibility_not: Visibility
visibility_in: [Visibility!]
visibility_not_in: [Visibility!]
language: String
language_not: String
language_in: [String!]
language_not_in: [String!]
pinned: Boolean # required for `maintainPinnedPost`
tags: _TagFilter
tags_not: _TagFilter
tags_in: [_TagFilter!]
tags_not_in: [_TagFilter!]
tags_some: _TagFilter
tags_none: _TagFilter
tags_single: _TagFilter
tags_every: _TagFilter
categories: _CategoryFilter
categories_not: _CategoryFilter
categories_in: [_CategoryFilter!]
categories_not_in: [_CategoryFilter!]
categories_some: _CategoryFilter
categories_none: _CategoryFilter
categories_single: _CategoryFilter
categories_every: _CategoryFilter
comments: _CommentFilter
comments_not: _CommentFilter
comments_in: [_CommentFilter!]
comments_not_in: [_CommentFilter!]
comments_some: _CommentFilter
comments_none: _CommentFilter
comments_single: _CommentFilter
comments_every: _CommentFilter
emotions: _PostEMOTEDFilter
emotions_not: _PostEMOTEDFilter
emotions_in: [_PostEMOTEDFilter!]
emotions_not_in: [_PostEMOTEDFilter!]
emotions_some: _PostEMOTEDFilter
emotions_none: _PostEMOTEDFilter
emotions_single: _PostEMOTEDFilter
emotions_every: _PostEMOTEDFilter
}
enum _PostOrdering {
id_asc
id_desc
activityId_asc
activityId_desc
objectId_asc
objectId_desc
title_asc
title_desc
slug_asc
slug_desc
content_asc
content_desc
contentExcerpt_asc
contentExcerpt_desc
image_asc
image_desc
visibility_asc
visibility_desc
deleted_asc
deleted_desc
disabled_asc
disabled_desc
createdAt_asc
createdAt_desc
updatedAt_asc
@ -128,6 +203,22 @@ type Mutation {
}
type Query {
Post(
id: ID
title: String
slug: String
content: String
image: String
visibility: Visibility
pinned: Boolean
createdAt: String
updatedAt: String
language: String
first: Int
offset: Int
orderBy: [_PostOrdering]
filter: _PostFilter
): [Post]
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
PostsEmotionsByCurrentUser(postId: ID!): [String]
profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post]

View File

@ -1,3 +1,20 @@
input _TagFilter {
AND: [_TagFilter!]
OR: [_TagFilter!]
id: ID
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
taggedPosts: _PostFilter
taggedPosts_not: _PostFilter
taggedPosts_in: [_PostFilter!]
taggedPosts_not_in: [_PostFilter!]
taggedPosts_some: _PostFilter
taggedPosts_none: _PostFilter
taggedPosts_single: _PostFilter
taggedPosts_every: _PostFilter
}
type Tag {
id: ID!
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
@ -6,3 +23,22 @@ type Tag {
deleted: Boolean
disabled: Boolean
}
enum _TagOrdering {
id_asc
id_desc
taggedCount_asc
taggedCount_desc
taggedCountUnique_asc
taggedCountUnique_desc
}
type Query {
Tag(
id: ID
first: Int
offset: Int
orderBy: [_TagOrdering]
filter: _TagFilter
): [Tag]
}

View File

@ -1,3 +1,28 @@
enum _UserOrdering {
id_asc
id_desc
name_asc
name_desc
slug_asc
slug_desc
avatar_asc
avatar_desc
coverImg_asc
coverImg_desc
role_asc
role_desc
locationName_asc
locationName_desc
about_asc
about_desc
createdAt_asc
createdAt_desc
updatedAt_asc
updatedAt_desc
locale_asc
locale_desc
}
type User {
id: ID!
actorId: String
@ -28,6 +53,7 @@ type User {
termsAndConditionsAgreedAt: String
allowEmbedIframes: Boolean
showShoutsPublicly: Boolean
locale: String
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[: FRIENDS]->(r: User) RETURN COUNT(DISTINCT r)")
@ -91,12 +117,6 @@ input _UserFilter {
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
id_contains: ID
id_not_contains: ID
id_starts_with: ID
id_not_starts_with: ID
id_ends_with: ID
id_not_ends_with: ID
friends: _UserFilter
friends_not: _UserFilter
friends_in: [_UserFilter!]
@ -127,8 +147,7 @@ input _UserFilter {
type Query {
User(
id: ID
email: String
actorId: String
email: String # admins need to search for a user sometimes
name: String
slug: String
avatar: String
@ -138,14 +157,6 @@ type Query {
about: String
createdAt: String
updatedAt: String
friendsCount: Int
followingCount: Int
followedByCount: Int
followedByCurrentUser: Boolean
contributionsCount: Int
commentedCount: Int
shoutedCount: Int
badgesCount: Int
first: Int
offset: Int
orderBy: [_UserOrdering]
@ -170,6 +181,8 @@ type Mutation {
termsAndConditionsAgreedVersion: String
termsAndConditionsAgreedAt: String
allowEmbedIframes: Boolean
showShoutsPublicly: Boolean
locale: String
): User

View File

@ -17,6 +17,7 @@ export default function create() {
termsAndConditionsAgreedVersion: '0.0.1',
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
allowEmbedIframes: false,
showShoutsPublicly: false,
locale: 'en',
}
defaults.slug = slugify(defaults.name, { lower: true })

View File

@ -4,7 +4,7 @@ import { createTestClient } from 'apollo-server-testing'
import createServer from '../server'
import Factory from './factories'
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
import { gql } from '../jest/helpers'
import { gql } from '../helpers/jest'
const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']

View File

@ -4,7 +4,6 @@ import { expect } from 'chai'
// import { client } from '../../../src/activitypub/apollo-client'
import { GraphQLClient } from 'graphql-request'
import Factory from '../../../src/seed/factories'
import { host } from '../../../src/jest/helpers'
const debug = require('debug')('ea:test:steps')
const factory = Factory()

View File

@ -1075,10 +1075,10 @@
url-regex "~4.1.1"
video-extensions "~1.1.0"
"@metascraper/helpers@^5.7.17":
version "5.7.17"
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.7.17.tgz#401897c7239090ca7149b83e581712845bbb3709"
integrity sha512-t21LqfDpaIrWg2JaivXG6mVzUsIVW05cAsKySA5Tj9Hgi9oZXxaaNes5XipOzk6P242RI48SDo7CkSbYiio7Tw==
"@metascraper/helpers@^5.8.7":
version "5.8.7"
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.8.7.tgz#b05f83f2a90001f7753c18a8b1bb978bd7c2f9d9"
integrity sha512-gDErMAA3d1CdkGxvAG4cDi7D2+fReZpD6lzYNJ/gsq45U3Pdz7ltsAvbp4amK92bGKYYPZtnUq85Wrr+Q+e06Q==
dependencies:
audio-extensions "0.0.0"
chrono-node "~1.3.11"
@ -1093,7 +1093,7 @@
isostring "0.0.1"
lodash "~4.17.15"
memoize-one "~5.1.1"
mime-types "~2.1.24"
mime-types "~2.1.25"
normalize-url "~4.5.0"
smartquotes "~2.3.1"
title "~3.4.1"
@ -1183,10 +1183,10 @@
"@sentry/types" "5.7.1"
tslib "^1.9.3"
"@sentry/node@^5.8.0":
version "5.8.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.8.0.tgz#68ec032b0e7fb840cc8ccc1b39c09ac6febc1046"
integrity sha512-hIzt1BysyQJez8ChgWpFkLcGq3t/HaLMqzrXF5vu+Uuekl5OfwsvzZ+8Dlv78rI4CvlL9a2EuI/94iqUNwhOSQ==
"@sentry/node@^5.9.0":
version "5.9.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.9.0.tgz#9a8da70990e64c88a391ef86dcf29f43e0a52e59"
integrity sha512-1CWwSGhRfMr4Bvt1i0vIms+BBZd4dBzlDyWpyCboodCXF1rTJRci9roQ+Wh9XWwFEWvgDD2PzuKzfvu638v2Wg==
dependencies:
"@sentry/core" "5.8.0"
"@sentry/hub" "5.8.0"
@ -1764,10 +1764,10 @@ apollo-server-caching@^0.5.0:
dependencies:
lru-cache "^5.0.0"
apollo-server-core@^2.9.7:
version "2.9.7"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.7.tgz#0f32344af90dec445ac780be95350bfa736fc416"
integrity sha512-EqKyROy+21sM93YHjGpy6wlnzK/vH0fnZh7RCf3uB69aQ3OjgdP4AQ5oWRQ62NDN+aoic7OLhChSDJeDonq/NQ==
apollo-server-core@^2.9.9:
version "2.9.9"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.9.tgz#73df4989ac0ad09d20c20ef3e06f8c816bc7a13f"
integrity sha512-JxtYDasqeem5qUwPrCVh2IsBOgSQF4MKrRgy8dpxd+ymWfaaVelCUows1VE8vghgRxqDExnM9ibOxcZeI6mO6g==
dependencies:
"@apollographql/apollo-tools" "^0.4.0"
"@apollographql/graphql-playground-html" "1.6.24"
@ -1804,10 +1804,10 @@ apollo-server-errors@^2.3.4:
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.4.tgz#b70ef01322f616cbcd876f3e0168a1a86b82db34"
integrity sha512-Y0PKQvkrb2Kd18d1NPlHdSqmlr8TgqJ7JQcNIfhNDgdb45CnqZlxL1abuIRhr8tiw8OhVOcFxz2KyglBi8TKdA==
apollo-server-express@^2.9.7:
version "2.9.7"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.7.tgz#54fbaf93b68f0123ecb1dead26cbfda5b15bd10e"
integrity sha512-+DuJk1oq34Zx0bLYzgBgJH/eXS0JNxw2JycHQvV0+PAQ0Qi01oomJRA2r1S5isnfnSAnHb2E9jyBTptoHdw3MQ==
apollo-server-express@^2.9.7, apollo-server-express@^2.9.9:
version "2.9.9"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.9.tgz#2a379217d7a7be012f0329be8bf89a63e181d42e"
integrity sha512-qltC3ttGz8zvrut7HzrcqKOUg0vHpvVyYeeOy8jvghZpqXyWFuJhnw6uxAFcKNKCPl3mJ1psji83P1Um2ceJgg==
dependencies:
"@apollographql/graphql-playground-html" "1.6.24"
"@types/accepts" "^1.3.5"
@ -1815,7 +1815,7 @@ apollo-server-express@^2.9.7:
"@types/cors" "^2.8.4"
"@types/express" "4.17.1"
accepts "^1.3.5"
apollo-server-core "^2.9.7"
apollo-server-core "^2.9.9"
apollo-server-types "^0.2.5"
body-parser "^1.18.3"
cors "^2.8.4"
@ -1833,12 +1833,12 @@ apollo-server-plugin-base@^0.6.5:
dependencies:
apollo-server-types "^0.2.5"
apollo-server-testing@~2.9.7:
version "2.9.7"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.7.tgz#8d05058ddda4a715fac2fefb2b8e973e409a7672"
integrity sha512-yy18ceSyX2a9UYcs6X7K0xFZwcS1riEh99zdWU0XB/yzzTIdGZkFYeJmV/zjpGL3CFyXF7Va/muo6otl4nDOsA==
apollo-server-testing@~2.9.9:
version "2.9.9"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.9.tgz#451836fa2e077e93f45182dde50ca72c15be2e84"
integrity sha512-ejbFJLrprMDBZWdi4hOZkZUSMzNJvX5NVDXWWUFHAySbY2zDsbHrQ9jE/2KQJrI3Q93jUgmpUTAu6kS0cjxt4Q==
dependencies:
apollo-server-core "^2.9.7"
apollo-server-core "^2.9.9"
apollo-server-types@^0.2.5:
version "0.2.5"
@ -1849,13 +1849,13 @@ apollo-server-types@^0.2.5:
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server@~2.9.7:
version "2.9.7"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.7.tgz#aab337b75c04ddea0fa9b171b30c4e91932c04d8"
integrity sha512-maGGCsK4Ft5ucox5ZJf6oaKhgPvzHY3jXWbA1F/mn0/EYX8e1RVO3Qtj8aQQ0/vCKx8r4vYgj+ctqBVaN/nr4A==
apollo-server@~2.9.9:
version "2.9.9"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.9.tgz#f10249fa9884be2a0ad59876e301fdfccb456208"
integrity sha512-b4IfGxZDzhOnfaPTinAD0rx8XpgxkVMjNuwooRULOJEeYG8Vd/OiBYSS7LSGy1g3hdiLBgJhMFC0ce7pjdcyFw==
dependencies:
apollo-server-core "^2.9.7"
apollo-server-express "^2.9.7"
apollo-server-core "^2.9.9"
apollo-server-express "^2.9.9"
express "^4.0.0"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
@ -2583,6 +2583,11 @@ commander@^2.8.1, commander@~2.20.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
commander@^2.9.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
@ -2778,6 +2783,11 @@ css-what@2.1:
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
cssfilter@0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae"
integrity sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=
cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
version "0.3.8"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
@ -2790,10 +2800,10 @@ cssstyle@^1.0.0:
dependencies:
cssom "0.3.x"
cucumber-expressions@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cucumber-expressions/-/cucumber-expressions-8.0.1.tgz#47eb87dcb626e90a4672986da1130f3c470b9e3d"
integrity sha512-g+A+tUEafNofe6ErwvOkqaMvDj9NuOr0GouGotpw4r5yK2d4144o9/6sQpXBr2YXbRy5ItmER/2bzAyDAzhPyQ==
cucumber-expressions@^8.1.0:
version "8.2.1"
resolved "https://registry.yarnpkg.com/cucumber-expressions/-/cucumber-expressions-8.2.1.tgz#e250063993350df106a8664c90a414814f555e2d"
integrity sha512-6n5JKbAzXfIiwyu2UyUcOmO83QmuSme25+Dw2taK6VNOybOfRkh4yNMA9VtuAJHOmsX3/8l0OVjTbE8lHnjOHA==
dependencies:
becke-ch--regex--s0-0-v1--base--pl--lib "^1.4.0"
xregexp "^4.2.4"
@ -2803,17 +2813,17 @@ cucumber-tag-expressions@^2.0.2:
resolved "https://registry.yarnpkg.com/cucumber-tag-expressions/-/cucumber-tag-expressions-2.0.2.tgz#aac27aae3690818ec15235bd056282dad8a2d2b8"
integrity sha512-DohmT4X641KX/sb96bdb7J2kXNcQBPrYmf3Oc5kiHCLfzFMWx/o2kB4JvjvQPZnYuA9lRt6pqtArM5gvUn4uzw==
cucumber@~6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/cucumber/-/cucumber-6.0.3.tgz#bf69ecc992772e580dabe265b2ed06ddab13d076"
integrity sha512-FSx7xdAQfFjcxp/iRBAuCFSXp2iJP1tF2Q5k/a67YgHiYbnwsD9F+UNv9ZG90LFHNsNQhb+67AmVxHkp4JRDpg==
cucumber@~6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cucumber/-/cucumber-6.0.5.tgz#cdc752ad18b551bcf7bc92774c925302f4408714"
integrity sha512-x+W9Fwk6TvcapQsYMxwFU5AsQJDOIJVGrPKmH15OC7jzb9/Dk7Hb0ZAyw4WcpaDcUDRc8bi2k2yJejDp5eTRlg==
dependencies:
assertion-error-formatter "^3.0.0"
bluebird "^3.4.1"
cli-table3 "^0.5.1"
colors "^1.1.2"
commander "^3.0.1"
cucumber-expressions "^8.0.1"
cucumber-expressions "^8.1.0"
cucumber-tag-expressions "^2.0.2"
duration "^0.2.1"
escape-string-regexp "^2.0.0"
@ -3289,10 +3299,10 @@ escodegen@^1.9.1:
optionalDependencies:
source-map "~0.6.1"
eslint-config-prettier@~6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.5.0.tgz#aaf9a495e2a816865e541bfdbb73a65cc162b3eb"
integrity sha512-cjXp8SbO9VFGW/Z7mbTydqS9to8Z58E5aYhj3e1+Hx7lS9s6gL5ILKNpCqZAFOVYRcSkWPFYljHrEh8QFEK5EQ==
eslint-config-prettier@~6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.7.0.tgz#9a876952e12df2b284adbd3440994bf1f39dfbb9"
integrity sha512-FamQVKM3jjUVwhG4hEMnbtsq7xOIDm+SY5iBPfR8gKsJoAB2IQnNF+bk1+8Fy44Nq7PPJaLvkRxILYdJWoguKQ==
dependencies:
get-stdin "^6.0.0"
@ -3342,10 +3352,10 @@ eslint-plugin-import@~2.18.2:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~23.0.3:
version "23.0.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.0.3.tgz#d3f157f7791f97713372c13259ba1dfc436eb4c1"
integrity sha512-9cNxr66zeOyz1S9AkQL4/ouilR6QHpYj8vKOQZ60fu9hAt5PJWS4KqWqfr1aqN5NFEZSPjFOla2Azn+KTWiGwg==
eslint-plugin-jest@~23.0.4:
version "23.0.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.0.4.tgz#1ab81ffe3b16c5168efa72cbd4db14d335092aa0"
integrity sha512-OaP8hhT8chJNodUPvLJ6vl8gnalcsU/Ww1t9oR3HnGdEWjm/DdCCUXLOral+IPGAeWu/EwgVQCK/QtxALpH1Yw==
dependencies:
"@typescript-eslint/experimental-utils" "^2.5.0"
@ -3981,7 +3991,7 @@ glob-parent@^5.0.0:
dependencies:
is-glob "^4.0.1"
glob@7.1.6:
glob@7.1.6, glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@ -3993,18 +4003,6 @@ glob@7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.1.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0"
integrity sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
global-dirs@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
@ -5689,6 +5687,11 @@ map-cache@^0.2.2:
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
map-values-deep@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/map-values-deep/-/map-values-deep-1.0.2.tgz#b0176a1c463158ae33e24de0ce8150621a2b30d3"
integrity sha512-br+tp4aANql3WnpDRjD14H7hHopPlJRnzCL0ZlGCRHAQZTU0g0x1rUQFq/ikb3zZQK+lW2AG7RJi+CFfQ8kSPA==
map-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@ -5730,19 +5733,19 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
metascraper-audio@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.7.17.tgz#b8e78a797deb155b02f30bcbe39da554bf1bf898"
integrity sha512-g11lRNVor5Pu4D1j3tL7aakSQM51CUl2Evp8QgFKcuYGjF+a1RiGq6veojiTf/9nWcKX8dUSTUJkQSIzdoJrFQ==
metascraper-audio@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.8.7.tgz#ce27b1f4056c1d1cbaa2cec0e819c3704f38fff4"
integrity sha512-ew9KZKOIl3u0500j7qIR/ZNiVtSohuyyiIWSxJVEeeguEOwAhMpOrpYAEkvKRo5CB89F2PNBIsXJIzMC4BWFrw==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
metascraper-author@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-5.7.17.tgz#0403eaa4d1992152246f01616fac1d52b0583c8a"
integrity sha512-vaMAn6glCr9f2PGvNObqMI7ECtQ7+CMkXSxKyn3fyxRVKnV95fBR+xi4+UJ2DWqTvVQ6t7gZwlzFWA4CwxfniQ==
metascraper-author@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-5.8.7.tgz#c29db97a24af801101008a547caea6a33a56e467"
integrity sha512-PwuCZvHnDm10Q1zMQllpCLjtlYR1zSF+rDCRkf/TUuBC/ozz27/JkXDL+ml2nmK8IQGLGRUQKOzrQ0vVMFKvQw==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
lodash "~4.17.15"
metascraper-clearbit-logo@^5.3.0:
@ -5752,28 +5755,28 @@ metascraper-clearbit-logo@^5.3.0:
dependencies:
got "~9.6.0"
metascraper-date@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-date/-/metascraper-date-5.7.17.tgz#8777bc5deaccce1235ed0b2eb8f0746c981ee245"
integrity sha512-OPKXu7S+S6JoZNVV9Dox6OIG2x5hzDx2J3IzMwzQwVdKzulMPSFMLCcJU8zLZ03dajSOszRf8aL1eSBfZscpIw==
metascraper-date@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-date/-/metascraper-date-5.8.7.tgz#146733ecce34f8d4a53c7c6ddcfc51c033287757"
integrity sha512-9+IslaGg+J+4cwPU5qu/MEexkoWj7sBxycmCA6vgfuCQCqNwlQ68vk2a/UVDw8OJOYjwX81JGrzxOqrQP0/kXw==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
metascraper-description@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.7.17.tgz#b0daa54d0345546ececcc033065790402aabb5ec"
integrity sha512-cQfg9Spl3FLK2x8O7DvecwSYEBUmRjtdZW2y1EVqHsOKwT13SeUy1kp+lZa8+8vFh4o8oJPzXHxgbLhAfAmVqQ==
metascraper-description@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.8.7.tgz#e85ce218daf33b74813b1523ad7dc7dc3fb128af"
integrity sha512-KOv5gnQVvGF1CgpUczu7KJm76rWJ7SH5UFcqFST60hRNgR9xy0y3aHbVDOhZkjNN4UKqnxMF6XTS/WaQxCK/AA==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
metascraper-image@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-image/-/metascraper-image-5.7.17.tgz#186b29979cb8aefc6c21d0342c386a8fef80be55"
integrity sha512-bwAUJrJibJ+fJGxL8T789Ki1z+8sqsz0sqb3W+mfR/ZLkhCu+jWLYqPVtMgTPM9Zaqqqxg5uTQs1uAVrnguKDA==
metascraper-image@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-image/-/metascraper-image-5.8.7.tgz#d24697c5b5a6ba688948c48fadcb5fffeb6c703d"
integrity sha512-OMK+PFnHeavCSuEJY5tFkG5tdl/luYmPys7PKkJIwC8A8q5qoAC0InIUu+c0SDrdf4nzOj083DZTp32YQxYF5A==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
metascraper-lang-detector@^4.8.5:
metascraper-lang-detector@^4.10.2:
version "4.10.2"
resolved "https://registry.yarnpkg.com/metascraper-lang-detector/-/metascraper-lang-detector-4.10.2.tgz#45744bc331125c098e8b27716d76740161b121d2"
integrity sha512-Lz1d5v/i1j08gQYz7sCdoxjOx94ArLV4UucUhGZeQpR4E6dK47V6aqfYwODRe2XAqhaU+3oLnbAipoHkOeZXiw==
@ -5782,81 +5785,80 @@ metascraper-lang-detector@^4.8.5:
franc "~4.0.0"
iso-639-3 "~1.1.0"
metascraper-lang@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.7.17.tgz#3952db650bcd909fff0308d1d2254e954a0c0028"
integrity sha512-G/XqySeDpZmoV1rgWeMs/hmX1NFX0IN2w4viNdgdMRXB+lhqeyk5Z20x9ssPAqiJ4Ab6tyR274NkgYa0ZNRMDw==
metascraper-lang@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.8.7.tgz#5214af961d55b7b4c98e679fffe7477a0f3f9c53"
integrity sha512-ASidffvAmnankJtb9BIqVyRRlcz0uJ5mAbkAoWL1xkd9GyUxRLvkCjKq/pvsapASNabfqjwbgSj7hO8mv5hbkQ==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
metascraper-logo@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.7.17.tgz#b26e2fb38e94cfe9ec9dfc7e28d8da26a0a0689d"
integrity sha512-S4aqxN4Qi3UXDLN4HhinEuQHUopYXbFw0Y5Cwj9TbGKfESeQ1n6Jm4eOgGifEYyyZMSeRR9li189EK3YPnYcFg==
metascraper-logo@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.8.7.tgz#5efb7e6c5f91ccad812e2d9ec3facfef179f40b6"
integrity sha512-QudGVJBBeXLWU54Xw2PmnsTf+qPUnbyYaOl4aFLg2wkLLza1GbuvOYGMiH9Y8k0WcRoesi9sQk+P0a/611blew==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
metascraper-publisher@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-publisher/-/metascraper-publisher-5.7.17.tgz#38455e035d8d34c42eff529316ee15f31726d641"
integrity sha512-BxiweB0vxXX0UF2YVxzwC7Y8X0A5mU+eaa6TsTrTGHPBWeZCUJaLJ2Ge35c00SIC+USgdu8KFyzF6+pJBObwvQ==
metascraper-publisher@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-publisher/-/metascraper-publisher-5.8.7.tgz#2b67f04db46123f9c6d57eaa3de610921fd28e01"
integrity sha512-vVfoyqGPxKWWQjvBL0gz4Xyol3QYdr5HWSs9DI7cLrlIDExOByPPah5bZVSijeseeKymyf36BvCm54+chOZN5g==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
metascraper-soundcloud@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.7.17.tgz#925fc91505b69f1e3e7f0c535567c7918f8afbd9"
integrity sha512-yllxXR0AHQmJLXCua+CJtjzmNr9I+mU/H23ED+S2t9Yd07xQDmqL8pkkuD8DAAy7aC6oIL0qghQPwk8qdM97Ug==
metascraper-soundcloud@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.8.7.tgz#a557f070671978730ea06d18be3d5668cf323ab5"
integrity sha512-qzwT7igIUi0k8NYC31lfLBeJEIUSxgJvQX3LC1JMxrEue5YMmE86SZRYASGemhMzhW5LtM/oA9jQECT3a8enJA==
dependencies:
"@metascraper/helpers" "^5.7.17"
memoize-one "~5.1.1"
"@metascraper/helpers" "^5.8.7"
tldts "~5.6.1"
metascraper-title@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.7.17.tgz#5b947635361bfb4d7557eadcb623489c812322e6"
integrity sha512-YCEbiU2MbPMLulXmLbSBN/N7ti9tBVr45yqMKSuFsWiNJ98bFsM1IQp1LN5KqRQmNkOg+8JsYgK+R9vqYwaGjg==
metascraper-title@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.8.7.tgz#aecbbd9515bd74d2aeafa587c83447d926508ba0"
integrity sha512-u+5KeJbsFKpi+pMnG71Gd49OLDQpkjiGIRTddhCZQhb45qHoTlGKN1nZuQ8nqJI6+ARWicFqtquomkaRXfBEnw==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
lodash "~4.17.15"
metascraper-url@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-url/-/metascraper-url-5.7.17.tgz#e8ba40a17a59b54139f42d6e3cf430dc6f32e7d7"
integrity sha512-7OOhCXpxdMiJatrbxa9rqLmUT/t/s34PDgtknoE/2FfmZY7X/xyORamcuqUHjV37sOpCPTun+GcJL4l3ddCi3Q==
metascraper-url@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-url/-/metascraper-url-5.8.7.tgz#8c04a8f9b82af1058145f21788655b7b6b04fd9c"
integrity sha512-K79mT509wV6B1Ak9vSslAbDPQMMRjjWowVgjcby5bOyFpO2j7mQkQIZYobEFpYLHlpb2R9myWJaTKAZe9KrF0A==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
metascraper-video@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.7.17.tgz#414d4641fbea667e73c42fe3706d673ee4c4aec5"
integrity sha512-lftJGynCVNfC15eyMW7tN3QWJl9T2sVNCgP0dZsW8OC1hWQM7WY3PW8yYd2PP6nUuwOTjNLL1F4oWNhldWrE8A==
metascraper-video@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.8.7.tgz#7a5d1e8955f9a65891908eef319683b6176765a2"
integrity sha512-J4OJlB+nla8ITwqH2H6dgQ+nrecYILVhsGFKG54p2qsSokXwgZrQ4P7WhUMd0VpBsYuebcRgdzY8OGUDb+7l0Q==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
lodash "~4.17.15"
metascraper-youtube@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.7.17.tgz#a3bdf06bbc9aa3766f08a779fa880d8a3fda9f8c"
integrity sha512-CZX03wX8ui8fjx+iBZCiAGdSKy4dMFiDrVSPmTMK2W8sn2guYv2QQ41g8gruFJgrF+m+mCOUG6KYgy3B/v5LdQ==
metascraper-youtube@^5.8.7:
version "5.8.7"
resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.8.7.tgz#8a799602788d90ed34a885f4754fc98aa5e917ca"
integrity sha512-00b+KNoRxDYc+Pbx25a74ZV2hX4ARqKY9J70AFZm/kstmxh2VOApyuIkuNkQM8PgTqEMXm3lAFiz6aYMnPcVMg==
dependencies:
"@metascraper/helpers" "^5.7.17"
"@metascraper/helpers" "^5.8.7"
get-video-id "~3.1.4"
is-reachable "~4.0.0"
memoize-one "~5.1.1"
p-locate "~4.1.0"
metascraper@^4.10.3:
version "4.10.3"
resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-4.10.3.tgz#8a97ed2e914e81d1dbc1f17a5b1e64f1b804493f"
integrity sha512-wNQm5A/PIxWcahaMwI+b3rOmmXRDNmjyF6Q15dHYXEqYoGl3dFaaT4lnTTm8yntvE+fOj8+o51ON2FBdstxbsA==
metascraper@^5.8.8:
version "5.8.8"
resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-5.8.8.tgz#9fbf6913f55bb448a9195e40e38f3599bc5a818f"
integrity sha512-z4G3SXGBVnd0+FSHqR3LJF+6emO03GlY2KoOTqsFCnRuY0B72nJyR/NRRYLn4PRX6PMQ6QZ+GWKa7oxBX6hZqQ==
dependencies:
"@metascraper/helpers" "^4.10.2"
"@metascraper/helpers" "^5.8.7"
cheerio "~1.0.0-rc.2"
cheerio-advanced-selectors "~2.0.1"
lodash "~4.17.11"
p-reduce "~2.0.0"
lodash "~4.17.15"
map-values-deep "~1.0.2"
whoops "~4.0.2"
xss "~1.0.6"
methods@^1.1.1, methods@^1.1.2, methods@~1.1.2:
version "1.1.2"
@ -5887,6 +5889,11 @@ mime-db@1.40.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==
mime-db@1.42.0:
version "1.42.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac"
integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==
mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.22, mime-types@~2.1.24:
version "2.1.24"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
@ -5894,6 +5901,13 @@ mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.22, mime-types@~2.1.24:
dependencies:
mime-db "1.40.0"
mime-types@~2.1.25:
version "2.1.25"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437"
integrity sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==
dependencies:
mime-db "1.42.0"
mime@1.6.0, mime@^1.4.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
@ -6070,10 +6084,10 @@ neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.6:
text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2"
neo4j-graphql-js@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.8.0.tgz#58035b9213656e17b6ed4c6cbf4dfe1c56a8a219"
integrity sha512-nDuzmi6W/YGIIVm+GAXCr/8CLABsU/RfeLebLH32vqeKViFATMfm4eT66aOq/GwHJ0838+o20yCbIFdx5rTP/A==
neo4j-graphql-js@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.9.3.tgz#91afb0631eb35014110022a74e572c9eb065d281"
integrity sha512-SzIX3BYE3EsKp/XU8Wog97TzfsrQdrKp/t7le7tnODojcBd5eSVJyKPrbaKqcnWMkLzKzO/SRX9PMQ2cDdXUKw==
dependencies:
"@babel/runtime" "^7.5.5"
"@babel/runtime-corejs2" "^7.5.5"
@ -6532,11 +6546,6 @@ p-reduce@^1.0.0:
resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=
p-reduce@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-2.0.0.tgz#365a26916213650711124881a6bdc4e32c2bfe36"
integrity sha512-VcNNEqiYIkRCGeUHELY5dUrnQHCRwL6eIH/L9oSbl/PsvyHQXD1ws/MFwuEb+6dgH/URCfROVUqOYL37eHi2kQ==
p-some@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-some/-/p-some-4.1.0.tgz#28e73bc1e0d62db54c2ed513acd03acba30d5c04"
@ -6784,10 +6793,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@~1.18.2:
version "1.18.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
prettier@~1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
pretty-format@^24.9.0:
version "24.9.0"
@ -8625,6 +8634,14 @@ xregexp@^4.2.4:
dependencies:
"@babel/runtime-corejs2" "^7.2.0"
xss@~1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.6.tgz#eaf11e9fc476e3ae289944a1009efddd8a124b51"
integrity sha512-6Q9TPBeNyoTRxgZFk5Ggaepk/4vUOYdOsIUYvLehcsIZTFjaavbVnsuAkLA5lIFuug5hw8zxcB9tm01gsjph2A==
dependencies:
commander "^2.9.0"
cssfilter "0.0.10"
xtend@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"

View File

@ -5,9 +5,12 @@ import {
} from "cypress-cucumber-preprocessor/steps";
import helpers from "../../support/helpers";
import { VERSION } from '../../constants/terms-and-conditions-version.js'
import locales from '../../../webapp/locales'
import orderBy from 'lodash/orderBy'
/* global cy */
const languages = orderBy(locales, 'name')
let lastPost = {};
let loginCredentials = {
@ -245,6 +248,12 @@ Then("I select a category", () => {
.click();
});
When("I choose {string} as the language for the post", (languageCode) => {
cy.get('.ds-flex-item > .ds-form-item .ds-select ')
.click().get('.ds-select-option')
.eq(languages.findIndex(l => l.code === languageCode)).click()
})
Then("the post shows up on the landing page at position {int}", index => {
cy.openPage("landing");
const selector = `.post-card:nth-child(${index}) > .ds-card-content`;

View File

@ -20,6 +20,7 @@ Feature: Notification for a mention
"""
And mention "@matt-rider" in the text
And I select a category
And I choose "en" as the language for the post
And I click on "Save"
When I log out
And I log in with the following credentials:

View File

@ -17,7 +17,8 @@ Feature: Create a post
Human Connection is a free and open-source social network
for active citizenship.
"""
Then I select a category
And I select a category
And I choose "en" as the language for the post
And I click on "Save"
Then I get redirected to ".../my-first-post"
And the post was saved successfully

View File

@ -17,7 +17,7 @@ import "cypress-file-upload";
import helpers from "./helpers";
import users from "../fixtures/users.json";
import { GraphQLClient, request } from 'graphql-request'
import { gql } from '../../backend/src/jest/helpers'
import { gql } from '../../backend/src/helpers/jest'
const backendHost = Cypress.env('BACKEND_HOST')
const switchLang = name => {

View File

@ -4,7 +4,6 @@
data:
SMTP_HOST: "mailserver.human-connection"
SMTP_PORT: "25"
GRAPHQL_PORT: "4000"
GRAPHQL_URI: "http://nitro-backend.human-connection:4000"
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
NEO4J_AUTH: "none"

View File

@ -2,11 +2,15 @@ version: "3.4"
services:
webapp:
environment:
- "CI=${CI}"
image: humanconnection/nitro-web:build-and-test
build:
context: webapp
target: build-and-test
backend:
environment:
- "CI=${CI}"
image: humanconnection/nitro-backend:build-and-test
build:
context: backend

View File

@ -15,7 +15,6 @@ services:
environment:
- NEO4J_dbms_security_auth__enabled=false
- NEO4J_dbms_memory_heap_max__size=2G
- GRAPHQL_PORT=4000
- GRAPHQL_URI=http://localhost:4000
- CLIENT_URI=http://localhost:3000
- JWT_SECRET=b/&&7b78BF&fv/Vd

View File

@ -39,7 +39,6 @@ services:
- uploads:/nitro-backend/public/uploads
environment:
- NEO4J_URI=bolt://neo4j:7687
- GRAPHQL_PORT=4000
- GRAPHQL_URI=http://backend:4000
- CLIENT_URI=http://localhost:3000
- JWT_SECRET=b/&&7b78BF&fv/Vd

View File

@ -8,15 +8,14 @@
"nonGlobalStepDefinitions": true
},
"scripts": {
"install:all": "yarn install && cd backend && yarn install && cd ../webapp && yarn install",
"db:seed": "cd backend && yarn run db:seed",
"db:reset": "cd backend && yarn run db:reset",
"cypress:backend:server": "cd backend && yarn run test:before:server",
"cypress:backend:seeder": "cd backend && yarn run test:before:seeder",
"cypress:webapp": "cd webapp && cross-env GRAPHQL_URI=http://localhost:4123 yarn run dev",
"cypress:setup": "run-p cypress:backend:* cypress:webapp",
"cypress:run": "cypress run --browser chromium",
"cypress:open": "cypress open --browser chromium",
"test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov",
"cypress:backend": "cd backend && yarn run dev",
"cypress:webapp": "cd webapp && yarn run dev",
"cypress:setup": "run-p cypress:backend cypress:webapp",
"cypress:run": "cross-env cypress run --browser chromium",
"cypress:open": "cross-env cypress open --browser chromium",
"version": "auto-changelog -p"
},
"devDependencies": {

View File

@ -1,4 +1,4 @@
FROM node:13.0.1-alpine as base
FROM node:13.1.0-alpine as base
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000

View File

@ -1,4 +1,4 @@
FROM node:13.0.1-alpine as build
FROM node:13.1.0-alpine as build
LABEL Description="Maintenance page of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000

View File

@ -69,7 +69,7 @@ export default {
if (!this.user.slug) {
return []
}
let routes = [
const routes = [
{
name: this.$t('profile.name'),
path: `/profile/${this.user.id}/${this.user.slug}`,

View File

@ -6,10 +6,12 @@ const localVue = global.localVue
describe('CategoriesSelect.vue', () => {
let wrapper
let mocks
let provide
let democracyAndPolitics
let environmentAndNature
let consumptionAndSustainablity
const propsData = { model: 'categoryIds' }
const categories = [
{
id: 'cat9',
@ -33,6 +35,11 @@ describe('CategoriesSelect.vue', () => {
},
]
beforeEach(() => {
provide = {
$parentForm: {
update: jest.fn(),
},
}
mocks = {
$t: jest.fn(),
}
@ -40,7 +47,7 @@ describe('CategoriesSelect.vue', () => {
describe('shallowMount', () => {
const Wrapper = () => {
return mount(CategoriesSelect, { mocks, localVue })
return mount(CategoriesSelect, { propsData, mocks, localVue, provide })
}
beforeEach(() => {
@ -58,8 +65,8 @@ describe('CategoriesSelect.vue', () => {
expect(wrapper.vm.selectedCategoryIds).toEqual([categories[0].id])
})
it('emits an updateCategories event when the selectedCategoryIds changes', () => {
expect(wrapper.emitted().updateCategories[0][0]).toEqual([categories[0].id])
it('calls $parent.update with selected category ids', () => {
expect(provide.$parentForm.update).toHaveBeenCalledWith('categoryIds', ['cat9'])
})
it('removes categories when clicked a second time', () => {

View File

@ -5,6 +5,7 @@
<ds-flex-item>
<ds-button
size="small"
:data-test="categoryButtonsId(category.id)"
@click.prevent="toggleCategory(category.id)"
:primary="isActive(category.id)"
:disabled="isDisabled(category.id)"
@ -28,16 +29,23 @@
<script>
import CategoryQuery from '~/graphql/CategoryQuery'
import xor from 'lodash/xor'
export default {
inject: {
$parentForm: {
default: null,
},
},
props: {
existingCategoryIds: { type: Array, default: () => [] },
model: { type: String, required: true },
},
data() {
return {
categories: null,
selectedMax: 3,
selectedCategoryIds: [],
selectedCategoryIds: this.existingCategoryIds,
}
},
computed: {
@ -48,39 +56,22 @@ export default {
return this.selectedCount >= this.selectedMax
},
},
watch: {
selectedCategoryIds(categoryIds) {
this.$emit('updateCategories', categoryIds)
},
existingCategoryIds: {
immediate: true,
handler: function(existingCategoryIds) {
if (!existingCategoryIds || !existingCategoryIds.length) {
return
}
this.selectedCategoryIds = existingCategoryIds
},
},
},
methods: {
toggleCategory(id) {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1) {
this.selectedCategoryIds.splice(index, 1)
} else {
this.selectedCategoryIds.push(id)
this.selectedCategoryIds = xor(this.selectedCategoryIds, [id])
if (this.$parentForm) {
this.$parentForm.update(this.model, this.selectedCategoryIds)
}
},
isActive(id) {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1) {
return true
}
return false
return this.selectedCategoryIds.includes(id)
},
isDisabled(id) {
return !!(this.reachedMaximum && !this.isActive(id))
},
categoryButtonsId(categoryId) {
return `category-buttons-${categoryId}`
},
},
apollo: {
Category: {

View File

@ -8,7 +8,7 @@ describe('Category', () => {
let icon
let name
let Wrapper = () => {
const Wrapper = () => {
return shallowMount(Category, {
localVue,
propsData: {

View File

@ -53,7 +53,7 @@ export default {
},
computed: {
routes() {
let routes = []
const routes = []
if (this.resourceType === 'contribution') {
if (this.isOwner) {

View File

@ -16,15 +16,45 @@ config.stubs['client-only'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>'
config.stubs['v-popover'] = '<span><slot /></span>'
const categories = [
{
id: 'cat3',
slug: 'health-wellbeing',
icon: 'medkit',
},
{
id: 'cat12',
slug: 'it-internet-data-privacy',
icon: 'mouse-pointer',
},
{
id: 'cat9',
slug: 'democracy-politics',
icon: 'university',
},
{
id: 'cat15',
slug: 'consumption-sustainability',
icon: 'shopping-cart',
},
{
id: 'cat4',
slug: 'environment-nature',
icon: 'tree',
},
]
describe('ContributionForm.vue', () => {
let wrapper
let postTitleInput
let expectedParams
let deutschOption
let cancelBtn
let mocks
let propsData
let categoryIds
let wrapper,
postTitleInput,
expectedParams,
cancelBtn,
mocks,
propsData,
categoryIds,
englishLanguage,
deutschLanguage,
dataPrivacyButton
const postTitle = 'this is a title for a post'
const postTitleTooShort = 'xx'
let postTitleTooLong = ''
@ -32,11 +62,6 @@ describe('ContributionForm.vue', () => {
postTitleTooLong += 'x'
}
const postContent = 'this is a post'
const postContentTooShort = 'xx'
let postContentTooLong = ''
for (let i = 0; i < 2001; i++) {
postContentTooLong += 'x'
}
const imageUpload = {
file: {
filename: 'avataar.svg',
@ -105,90 +130,59 @@ describe('ContributionForm.vue', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.setData({
form: {
languageOptions: [
{
label: 'Deutsch',
value: 'de',
},
],
},
})
})
describe('CreatePost', () => {
describe('language placeholder', () => {
it("displays the name that corresponds with the user's location code", () => {
expect(wrapper.find('.ds-select-placeholder').text()).toEqual('English')
})
})
describe('invalid form submission', () => {
it('title and content should not be empty ', async () => {
wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
beforeEach(async () => {
wrapper.find(CategoriesSelect).setData({ categories })
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContent)
englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English')
englishLanguage.trigger('click')
dataPrivacyButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat12"]')
dataPrivacyButton.trigger('click')
})
it('title should not be empty', async () => {
await wrapper.vm.updateEditorContent(postContent)
wrapper.find('.submit-button-for-test').trigger('click')
postTitleInput.setValue('')
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('title should not be too long', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitleTooLong)
await wrapper.vm.updateEditorContent(postContent)
wrapper.find('.submit-button-for-test').trigger('click')
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('title should not be too short', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitleTooShort)
await wrapper.vm.updateEditorContent(postContent)
wrapper.find('.submit-button-for-test').trigger('click')
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('content should not be empty', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('content should not be too short', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContentTooShort)
wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('content should not be too long', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContentTooLong)
wrapper.find('.submit-button-for-test').trigger('click')
await wrapper.vm.updateEditorContent('')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('should have at least one category', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContent)
wrapper.find('.submit-button-for-test').trigger('click')
dataPrivacyButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat12"]')
dataPrivacyButton.trigger('click')
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('should have not have more than three categories', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContent)
wrapper.vm.form.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27']
wrapper.find('.submit-button-for-test').trigger('click')
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
})
@ -210,43 +204,51 @@ describe('ContributionForm.vue', () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContent)
categoryIds = ['cat12']
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
wrapper.find(CategoriesSelect).setData({ categories })
englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English')
englishLanguage.trigger('click')
dataPrivacyButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat12"]')
dataPrivacyButton.trigger('click')
})
it('creates a post with valid title, content, and at least one category', async () => {
await wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it("sends a fallback language based on a user's locale", () => {
wrapper.find('.submit-button-for-test').trigger('click')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it('supports changing the language', async () => {
expectedParams.variables.language = 'de'
deutschOption = wrapper.findAll('li').at(0)
deutschOption.trigger('click')
wrapper.find('.submit-button-for-test').trigger('click')
deutschLanguage = wrapper.findAll('li').filter(language => language.text() === 'Deutsch')
deutschLanguage.trigger('click')
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it('supports adding a teaser image', async () => {
expectedParams.variables.imageUpload = imageUpload
wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload)
await wrapper.find('.submit-button-for-test').trigger('click')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it('content is valid with just a link', async () => {
await wrapper.vm.updateEditorContent(
'<a href="https://www.youtube.com/watch?v=smoEelV6FUk" target="_blank"></a>',
)
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it("pushes the user to the post's page", async () => {
wrapper.find('.submit-button-for-test').trigger('click')
wrapper.find('form').trigger('submit')
await mocks.$apollo.mutate
expect(mocks.$router.push).toHaveBeenCalledTimes(1)
})
it('shows a success toaster', async () => {
wrapper.find('.submit-button-for-test').trigger('click')
wrapper.find('form').trigger('submit')
await mocks.$apollo.mutate
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
@ -271,11 +273,17 @@ describe('ContributionForm.vue', () => {
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContent)
categoryIds = ['cat12']
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
wrapper.find(CategoriesSelect).setData({ categories })
englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English')
englishLanguage.trigger('click')
dataPrivacyButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat12"]')
dataPrivacyButton.trigger('click')
})
it('shows an error toaster when apollo mutation rejects', async () => {
await wrapper.find('.submit-button-for-test').trigger('click')
await wrapper.find('form').trigger('submit')
await mocks.$apollo.mutate
await expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!')
})
@ -337,11 +345,11 @@ describe('ContributionForm.vue', () => {
expectedParams = {
mutation: PostMutations().UpdatePost,
variables: {
title: postTitle,
content: postContent,
title: propsData.contribution.title,
content: propsData.contribution.content,
language: propsData.contribution.language,
id: propsData.contribution.id,
categoryIds,
categoryIds: ['cat12'],
image,
imageUpload: null,
},
@ -349,22 +357,20 @@ describe('ContributionForm.vue', () => {
})
it('calls the UpdatePost apollo mutation', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
expectedParams.variables.content = postContent
wrapper.vm.updateEditorContent(postContent)
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
wrapper.find('.submit-button-for-test').trigger('click')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it('supports updating categories', async () => {
const categoryIds = ['cat3', 'cat51', 'cat37']
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
wrapper.vm.updateEditorContent(postContent)
expectedParams.variables.categoryIds = categoryIds
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
await wrapper.find('.submit-button-for-test').trigger('click')
expectedParams.variables.categoryIds.push('cat3')
wrapper.find(CategoriesSelect).setData({ categories })
const healthWellbeingButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat3"]')
healthWellbeingButton.trigger('click')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
})

View File

@ -1,5 +1,11 @@
<template>
<ds-form ref="contributionForm" v-model="form" :schema="formSchema">
<ds-form
class="contribution-form"
ref="contributionForm"
v-model="form"
:schema="formSchema"
@submit="submit"
>
<template slot-scope="{ errors }">
<hc-teaser-image :contribution="contribution" @addTeaserImage="addTeaserImage">
<img
@ -21,7 +27,13 @@
name="title"
autofocus
/>
<small class="smallTag">{{ form.title.length }}/{{ formSchema.title.max }}</small>
<ds-text align="right">
<ds-chip v-if="errors && errors.title" color="danger" size="base">
{{ form.title.length }}/{{ formSchema.title.max }}
<ds-icon name="warning"></ds-icon>
</ds-chip>
<ds-chip v-else size="base">{{ form.title.length }}/{{ formSchema.title.max }}</ds-chip>
</ds-text>
<client-only>
<hc-editor
:users="users"
@ -29,27 +41,43 @@
:hashtags="hashtags"
@input="updateEditorContent"
/>
<small class="smallTag">{{ form.contentLength }}</small>
<ds-text align="right">
<ds-chip v-if="errors && errors.content" color="danger" size="base">
{{ contentLength }}
<ds-icon name="warning"></ds-icon>
</ds-chip>
<ds-chip v-else size="base">
{{ contentLength }}
</ds-chip>
</ds-text>
</client-only>
<ds-space margin-bottom="small" />
<hc-categories-select
model="categoryIds"
@updateCategories="updateCategories"
:existingCategoryIds="form.categoryIds"
/>
<hc-categories-select model="categoryIds" :existingCategoryIds="form.categoryIds" />
<ds-text align="right">
<ds-chip v-if="errors && errors.categoryIds" color="danger" size="base">
{{ form.categoryIds.length }} / 3
<ds-icon name="warning"></ds-icon>
</ds-chip>
<ds-chip v-else size="base">{{ form.categoryIds.length }} / 3</ds-chip>
</ds-text>
<ds-flex class="contribution-form-footer">
<ds-flex-item :width="{ base: '10%', sm: '10%', md: '10%', lg: '15%' }" />
<ds-flex-item :width="{ base: '80%', sm: '30%', md: '30%', lg: '20%' }">
<ds-flex-item :width="{ lg: '50%', md: '50%', sm: '100%' }" />
<ds-flex-item>
<ds-space margin-bottom="small" />
<ds-select
model="language"
:options="form.languageOptions"
:options="languageOptions"
icon="globe"
:placeholder="locale"
:placeholder="$t('contribution.languageSelectText')"
:label="$t('contribution.languageSelectLabel')"
/>
</ds-flex-item>
</ds-flex>
<ds-text align="right">
<ds-chip v-if="errors && errors.language" size="base" color="danger">
<ds-icon name="warning"></ds-icon>
</ds-chip>
</ds-text>
<ds-space />
<div slot="footer" style="text-align: right">
<ds-button
@ -60,15 +88,7 @@
>
{{ $t('actions.cancel') }}
</ds-button>
<ds-button
class="submit-button-for-test"
type="submit"
icon="check"
:loading="loading"
:disabled="failsValidations || errors"
primary
@click.prevent="submit"
>
<ds-button type="submit" icon="check" :loading="loading" :disabled="errors" primary>
{{ $t('actions.save') }}
</ds-button>
</div>
@ -100,73 +120,78 @@ export default {
contribution: { type: Object, default: () => {} },
},
data() {
return {
form: {
const languageOptions = orderBy(locales, 'name').map(locale => {
return { label: locale.name, value: locale.code }
})
const formDefaults = {
title: '',
content: '',
contentLength: 0,
teaserImage: null,
image: null,
language: null,
languageOptions: [],
categoryIds: [],
},
}
let id = null
let slug = null
const form = { ...formDefaults }
if (this.contribution && this.contribution.id) {
id = this.contribution.id
slug = this.contribution.slug
form.title = this.contribution.title
form.content = this.contribution.content
form.image = this.contribution.image
form.language =
this.contribution && this.contribution.language
? languageOptions.find(o => this.contribution.language === o.value)
: null
form.categoryIds = this.categoryIds(this.contribution.categories)
}
return {
form,
formSchema: {
title: { required: true, min: 3, max: 100 },
content: [{ required: true }],
content: { required: true },
categoryIds: {
type: 'array',
required: true,
validator: (rule, value) => {
const errors = []
if (!(value && value.length >= 1 && value.length <= 3)) {
errors.push(new Error(this.$t('common.validations.categories')))
}
return errors
},
id: null,
},
language: { required: true },
},
languageOptions,
id,
slug,
loading: false,
slug: null,
users: [],
contentMin: 3,
failsValidations: true,
hashtags: [],
}
},
watch: {
contribution: {
immediate: true,
handler: function(contribution) {
if (!contribution || !contribution.id) {
return
}
this.id = contribution.id
this.slug = contribution.slug
this.form.title = contribution.title
this.form.content = contribution.content
this.form.image = contribution.image
this.form.categoryIds = this.categoryIds(contribution.categories)
this.manageContent(this.form.content)
},
},
},
computed: {
locale() {
const locale =
this.contribution && this.contribution.language
? locales.find(loc => this.contribution.language === loc.code)
: locales.find(loc => this.$i18n.locale() === loc.code)
return locale.name
contentLength() {
return this.$filters.removeHtml(this.form.content).length
},
...mapGetters({
currentUser: 'auth/user',
}),
},
mounted() {
this.availableLocales()
},
methods: {
submit() {
const { title, content, image, teaserImage, categoryIds } = this.form
let language
if (this.form.language) {
language = this.form.language.value
} else if (this.contribution && this.contribution.language) {
language = this.contribution.language
} else {
language = this.$i18n.locale()
}
const {
language: { value: language },
title,
content,
image,
teaserImage,
categoryIds,
} = this.form
this.loading = true
this.$apollo
.mutate({
@ -185,7 +210,6 @@ export default {
this.loading = false
this.$toast.success(this.$t('contribution.success'))
const result = data[this.id ? 'UpdatePost' : 'CreatePost']
this.failedValidations = false
this.$router.push({
name: 'post-id-slug',
@ -195,45 +219,16 @@ export default {
.catch(err => {
this.$toast.error(err.message)
this.loading = false
this.failedValidations = true
})
},
updateEditorContent(value) {
// TODO: Do smth????? what is happening
this.$refs.contributionForm.update('content', value)
this.manageContent(value)
},
manageContent(content) {
// filter HTML out of content value
const str = content.replace(/<\/?[^>]+(>|$)/gm, '')
// Set counter length of text
this.form.contentLength = str.length
this.validatePost()
},
availableLocales() {
orderBy(locales, 'name').map(locale => {
this.form.languageOptions.push({ label: locale.name, value: locale.code })
})
},
updateCategories(ids) {
this.form.categoryIds = ids
this.validatePost()
},
addTeaserImage(file) {
this.form.teaserImage = file
},
categoryIds(categories) {
let categoryIds = []
categories.map(categoryId => {
categoryIds.push(categoryId.id)
})
return categoryIds
},
validatePost() {
const passesContentValidations = this.form.contentLength >= this.contentMin
const passesCategoryValidations =
this.form.categoryIds.length > 0 && this.form.categoryIds.length <= 3
this.failsValidations = !(passesContentValidations && passesCategoryValidations)
return categories.map(c => c.id)
},
},
apollo: {
@ -288,4 +283,10 @@ export default {
padding-right: 0;
}
}
.contribution-form {
.ds-chip {
cursor: default;
}
}
</style>

View File

@ -101,7 +101,7 @@ export default {
}
},
handleSubmit() {
let resourceArgs = []
const resourceArgs = []
if (this.deleteContributions) {
resourceArgs.push('Post')
}

View File

@ -78,7 +78,7 @@ describe('Editor.vue', () => {
describe('limists suggestion list to 15 users', () => {
beforeEach(() => {
let manyUsersList = []
const manyUsersList = []
for (let i = 0; i < 25; i++) {
manyUsersList.push({ id: `user${i}` })
}
@ -119,7 +119,7 @@ describe('Editor.vue', () => {
describe('limists suggestion list to 15 hashtags', () => {
beforeEach(() => {
let manyHashtagsList = []
const manyHashtagsList = []
for (let i = 0; i < 25; i++) {
manyHashtagsList.push({ id: `hashtag${i}` })
}

View File

@ -27,7 +27,11 @@ const plugins = [
]
helpers.init({ plugins })
const users = [{ id: 1, slug: 'peter' }, { id: 2, slug: 'sandra' }, { id: 3, slug: 'jane' }]
const users = [
{ id: 1, slug: 'peter' },
{ id: 2, slug: 'sandra' },
{ id: 3, slug: 'jane' },
]
storiesOf('Editor', module)
.addDecorator(withA11y)

View File

@ -377,6 +377,7 @@ li > p {
.embed-preview-image {
width: 100%;
height: auto;
max-height: 450px;
}
.embed-preview-image--clickable {

View File

@ -63,30 +63,26 @@ describe('defaultExtensions', () => {
it('recognizes embed code', () => {
const editor = createEditor()
const expected = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
text: 'Baby loves cat:',
type: 'text',
},
],
type: 'paragraph',
},
{
content: [
{
type: 'embed',
attrs: {
dataEmbedUrl: 'https://www.youtube.com/watch?v=qkdXAtO40Fo',
},
type: 'embed',
},
],
type: 'paragraph',
},
],
type: 'doc',
}
expect(editor.getJSON()).toEqual(expected)
})
})

View File

@ -38,8 +38,8 @@ export default class Embed extends Node {
default: null,
},
},
group: 'inline',
inline: true,
group: 'block',
inline: false,
parseDOM: [
{
tag: 'a[href].embed',

View File

@ -5,6 +5,7 @@ export default class EventHandler extends Extension {
get name() {
return 'event_handler'
}
get plugins() {
return [
new Plugin({

View File

@ -9,7 +9,7 @@ config.stubs['nuxt-link'] = '<span><slot /></span>'
describe('Hashtag', () => {
let id
let Wrapper = () => {
const Wrapper = () => {
return shallowMount(Hashtag, {
localVue,
propsData: {

View File

@ -0,0 +1,18 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import Hashtag from './Hashtag.vue'
import helpers from '~/storybook/helpers'
helpers.init()
storiesOf('Hashtag', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('clickable', () => ({
components: { Hashtag },
store: helpers.store,
data: () => ({
hashtag: 'SomeHashtag',
}),
template: '<hashtag :id="hashtag" />',
}))

View File

@ -1,7 +0,0 @@
### Example
Tag "Liebe"
```
<hc-hashtag name="Liebe" />
```

View File

@ -58,7 +58,7 @@ export default {
return find(this.locales, { code: this.$i18n.locale() })
},
routes() {
let routes = this.locales.map(locale => {
const routes = this.locales.map(locale => {
return {
name: locale.name,
path: locale.code,

View File

@ -5,7 +5,7 @@ const localVue = global.localVue
localVue.filter('truncate', string => string)
config.stubs['dropdown'] = '<span class="dropdown"><slot /></span>'
config.stubs.dropdown = '<span class="dropdown"><slot /></span>'
describe('NotificationMenu.vue', () => {
let wrapper

View File

@ -5,13 +5,13 @@ import Paginate from './Paginate'
const localVue = global.localVue
describe('Paginate.vue', () => {
let propsData, wrapper, Wrapper, nextButton, backButton
let propsData, wrapper, nextButton, backButton
beforeEach(() => {
propsData = {}
})
Wrapper = () => {
const Wrapper = () => {
return mount(Paginate, { propsData, localVue })
}
describe('mount', () => {

View File

@ -1,5 +1,6 @@
<template>
<ds-card
:lang="post.language"
:image="post.image | proxyApiUrl"
:class="{ 'post-card': true, 'disabled-content': post.disabled, 'post--pinned': isPinned }"
>

View File

@ -4,6 +4,7 @@ import Signup, { SignupMutation, SignupByInvitationMutation } from './Signup'
const localVue = global.localVue
config.stubs['sweetalert-icon'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>'
describe('Signup', () => {
let wrapper

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