mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of https://github.com/Ocelot-Social-Community/Ocelot-Social into 2144-Add_Search_Results_Page
This commit is contained in:
commit
16c3d2ab87
@ -5,6 +5,7 @@ import { hashSync } from 'bcryptjs'
|
|||||||
import { Factory } from 'rosie'
|
import { Factory } from 'rosie'
|
||||||
import { getDriver, getNeode } from './neo4j'
|
import { getDriver, getNeode } from './neo4j'
|
||||||
import CONFIG from '../config/index.js'
|
import CONFIG from '../config/index.js'
|
||||||
|
import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode.js'
|
||||||
|
|
||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
|
|
||||||
@ -205,7 +206,7 @@ const emailDefaults = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Factory.define('emailAddress')
|
Factory.define('emailAddress')
|
||||||
.attr(emailDefaults)
|
.attrs(emailDefaults)
|
||||||
.after((buildObject, options) => {
|
.after((buildObject, options) => {
|
||||||
return neode.create('EmailAddress', buildObject)
|
return neode.create('EmailAddress', buildObject)
|
||||||
})
|
})
|
||||||
@ -216,6 +217,28 @@ Factory.define('unverifiedEmailAddress')
|
|||||||
return neode.create('UnverifiedEmailAddress', buildObject)
|
return neode.create('UnverifiedEmailAddress', buildObject)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const inviteCodeDefaults = {
|
||||||
|
code: () => generateInviteCode(),
|
||||||
|
createdAt: () => new Date().toISOString(),
|
||||||
|
expiresAt: () => null,
|
||||||
|
}
|
||||||
|
|
||||||
|
Factory.define('inviteCode')
|
||||||
|
.attrs(inviteCodeDefaults)
|
||||||
|
.option('generatedById', null)
|
||||||
|
.option('generatedBy', ['generatedById'], (generatedById) => {
|
||||||
|
if (generatedById) return neode.find('User', generatedById)
|
||||||
|
return Factory.build('user')
|
||||||
|
})
|
||||||
|
.after(async (buildObject, options) => {
|
||||||
|
const [inviteCode, generatedBy] = await Promise.all([
|
||||||
|
neode.create('InviteCode', buildObject),
|
||||||
|
options.generatedBy,
|
||||||
|
])
|
||||||
|
await Promise.all([inviteCode.relateTo(generatedBy, 'generated')])
|
||||||
|
return inviteCode
|
||||||
|
})
|
||||||
|
|
||||||
Factory.define('location')
|
Factory.define('location')
|
||||||
.attrs({
|
.attrs({
|
||||||
name: 'Germany',
|
name: 'Germany',
|
||||||
|
|||||||
@ -541,6 +541,16 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
await Factory.build(
|
||||||
|
'inviteCode',
|
||||||
|
{
|
||||||
|
code: 'AAAAAA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
generatedBy: jennyRostock,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
authenticatedUser = await louie.toJson()
|
authenticatedUser = await louie.toJson()
|
||||||
const mention1 =
|
const mention1 =
|
||||||
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
|
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
|
||||||
|
|||||||
@ -109,6 +109,8 @@ export default shield(
|
|||||||
notifications: isAuthenticated,
|
notifications: isAuthenticated,
|
||||||
Donations: isAuthenticated,
|
Donations: isAuthenticated,
|
||||||
userData: isAuthenticated,
|
userData: isAuthenticated,
|
||||||
|
MyInviteCodes: isAuthenticated,
|
||||||
|
isValidInviteCode: allow,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
'*': deny,
|
'*': deny,
|
||||||
@ -152,6 +154,7 @@ export default shield(
|
|||||||
pinPost: isAdmin,
|
pinPost: isAdmin,
|
||||||
unpinPost: isAdmin,
|
unpinPost: isAdmin,
|
||||||
UpdateDonations: isAdmin,
|
UpdateDonations: isAdmin,
|
||||||
|
GenerateInviteCode: isAuthenticated,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: or(isMyOwn, isAdmin),
|
email: or(isMyOwn, isAdmin),
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import { UserInputError } from 'apollo-server'
|
|||||||
|
|
||||||
const COMMENT_MIN_LENGTH = 1
|
const COMMENT_MIN_LENGTH = 1
|
||||||
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
||||||
const NO_CATEGORIES_ERR_MESSAGE =
|
|
||||||
'You cannot save a post without at least one category or more than three'
|
|
||||||
const USERNAME_MIN_LENGTH = 3
|
const USERNAME_MIN_LENGTH = 3
|
||||||
const validateCreateComment = async (resolve, root, args, context, info) => {
|
const validateCreateComment = async (resolve, root, args, context, info) => {
|
||||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||||
@ -46,20 +44,6 @@ const validateUpdateComment = async (resolve, root, args, context, info) => {
|
|||||||
return resolve(root, args, context, info)
|
return resolve(root, args, context, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatePost = async (resolve, root, args, context, info) => {
|
|
||||||
const { categoryIds } = args
|
|
||||||
if (!Array.isArray(categoryIds) || !categoryIds.length || categoryIds.length > 3) {
|
|
||||||
throw new UserInputError(NO_CATEGORIES_ERR_MESSAGE)
|
|
||||||
}
|
|
||||||
return resolve(root, args, context, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateUpdatePost = async (resolve, root, args, context, info) => {
|
|
||||||
const { categoryIds } = args
|
|
||||||
if (typeof categoryIds === 'undefined') return resolve(root, args, context, info)
|
|
||||||
return validatePost(resolve, root, args, context, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateReport = async (resolve, root, args, context, info) => {
|
const validateReport = async (resolve, root, args, context, info) => {
|
||||||
const { resourceId } = args
|
const { resourceId } = args
|
||||||
const { user } = context
|
const { user } = context
|
||||||
@ -138,8 +122,6 @@ export default {
|
|||||||
Mutation: {
|
Mutation: {
|
||||||
CreateComment: validateCreateComment,
|
CreateComment: validateCreateComment,
|
||||||
UpdateComment: validateUpdateComment,
|
UpdateComment: validateUpdateComment,
|
||||||
CreatePost: validatePost,
|
|
||||||
UpdatePost: validateUpdatePost,
|
|
||||||
UpdateUser: validateUpdateUser,
|
UpdateUser: validateUpdateUser,
|
||||||
fileReport: validateReport,
|
fileReport: validateReport,
|
||||||
review: validateReview,
|
review: validateReview,
|
||||||
|
|||||||
@ -30,27 +30,7 @@ const updateCommentMutation = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const createPostMutation = gql`
|
|
||||||
mutation($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) {
|
|
||||||
CreatePost(
|
|
||||||
id: $id
|
|
||||||
title: $title
|
|
||||||
content: $content
|
|
||||||
language: $language
|
|
||||||
categoryIds: $categoryIds
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const updatePostMutation = gql`
|
|
||||||
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
|
|
||||||
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const reportMutation = gql`
|
const reportMutation = gql`
|
||||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
fileReport(
|
fileReport(
|
||||||
@ -227,104 +207,6 @@ describe('validateCreateComment', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('validatePost', () => {
|
|
||||||
let createPostVariables
|
|
||||||
beforeEach(async () => {
|
|
||||||
createPostVariables = {
|
|
||||||
title: 'I am a title',
|
|
||||||
content: 'Some content',
|
|
||||||
}
|
|
||||||
authenticatedUser = await commentingUser.toJson()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('categories', () => {
|
|
||||||
describe('null', () => {
|
|
||||||
it('throws UserInputError', async () => {
|
|
||||||
createPostVariables = { ...createPostVariables, categoryIds: null }
|
|
||||||
await expect(
|
|
||||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: { CreatePost: null },
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message: 'You cannot save a post without at least one category or more than three',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('empty', () => {
|
|
||||||
it('throws UserInputError', async () => {
|
|
||||||
createPostVariables = { ...createPostVariables, categoryIds: [] }
|
|
||||||
await expect(
|
|
||||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: { CreatePost: null },
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message: 'You cannot save a post without at least one category or more than three',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('more than 3 categoryIds', () => {
|
|
||||||
it('throws UserInputError', async () => {
|
|
||||||
createPostVariables = {
|
|
||||||
...createPostVariables,
|
|
||||||
categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'],
|
|
||||||
}
|
|
||||||
await expect(
|
|
||||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: { CreatePost: null },
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message: 'You cannot save a post without at least one category or more than three',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('validateUpdatePost', () => {
|
|
||||||
describe('post created without categories somehow', () => {
|
|
||||||
let owner, updatePostVariables
|
|
||||||
beforeEach(async () => {
|
|
||||||
const postSomehowCreated = await neode.create('Post', {
|
|
||||||
id: 'how-was-this-created',
|
|
||||||
})
|
|
||||||
owner = await neode.create('User', {
|
|
||||||
id: 'author-of-post-without-category',
|
|
||||||
slug: 'hacker',
|
|
||||||
})
|
|
||||||
await postSomehowCreated.relateTo(owner, 'author')
|
|
||||||
authenticatedUser = await owner.toJson()
|
|
||||||
updatePostVariables = {
|
|
||||||
id: 'how-was-this-created',
|
|
||||||
title: 'I am a title',
|
|
||||||
content: 'Some content',
|
|
||||||
categoryIds: [],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('requires at least one category for successful update', async () => {
|
|
||||||
await expect(
|
|
||||||
mutate({ mutation: updatePostMutation, variables: updatePostVariables }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: { UpdatePost: null },
|
|
||||||
errors: [
|
|
||||||
{ message: 'You cannot save a post without at least one category or more than three' },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('validateReport', () => {
|
describe('validateReport', () => {
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
export default {
|
export default {
|
||||||
|
code: { type: 'string', primary: true },
|
||||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
token: { type: 'string', primary: true, token: true },
|
expiresAt: { type: 'string', isoDate: true, default: null },
|
||||||
generatedBy: {
|
generated: {
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationship: 'GENERATED',
|
relationship: 'GENERATED',
|
||||||
target: 'User',
|
target: 'User',
|
||||||
direction: 'in',
|
direction: 'in',
|
||||||
},
|
},
|
||||||
activated: {
|
redeemed: {
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationship: 'ACTIVATED',
|
relationship: 'REDEEMED',
|
||||||
target: 'EmailAddress',
|
target: 'User',
|
||||||
direction: 'out',
|
direction: 'in',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -100,6 +100,18 @@ export default {
|
|||||||
target: 'User',
|
target: 'User',
|
||||||
direction: 'in',
|
direction: 'in',
|
||||||
},
|
},
|
||||||
|
inviteCodes: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'GENERATED',
|
||||||
|
target: 'InviteCode',
|
||||||
|
direction: 'out',
|
||||||
|
},
|
||||||
|
redeemedInviteCode: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'REDEEMED',
|
||||||
|
target: 'InviteCode',
|
||||||
|
direction: 'out',
|
||||||
|
},
|
||||||
termsAndConditionsAgreedVersion: {
|
termsAndConditionsAgreedVersion: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
allow: [null],
|
allow: [null],
|
||||||
|
|||||||
@ -15,4 +15,5 @@ export default {
|
|||||||
Donations: require('./Donations.js').default,
|
Donations: require('./Donations.js').default,
|
||||||
Report: require('./Report.js').default,
|
Report: require('./Report.js').default,
|
||||||
Migration: require('./Migration.js').default,
|
Migration: require('./Migration.js').default,
|
||||||
|
InviteCode: require('./InviteCode.js').default,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
export default function generateInviteCode() {
|
||||||
|
// 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z])
|
||||||
|
return Array.from({ length: 6 }, (n = Math.floor(Math.random() * 36)) => {
|
||||||
|
// n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65
|
||||||
|
// else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48
|
||||||
|
return String.fromCharCode(n > 9 ? n + 55 : n + 48)
|
||||||
|
}).join('')
|
||||||
|
}
|
||||||
109
backend/src/schema/resolvers/inviteCodes.js
Normal file
109
backend/src/schema/resolvers/inviteCodes.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import generateInviteCode from './helpers/generateInviteCode'
|
||||||
|
import Resolver from './helpers/Resolver'
|
||||||
|
|
||||||
|
const uniqueInviteCode = async (session, code) => {
|
||||||
|
return session.readTransaction(async (txc) => {
|
||||||
|
const result = await txc.run(`MATCH (ic:InviteCode { id: $code }) RETURN count(ic) AS count`, {
|
||||||
|
code,
|
||||||
|
})
|
||||||
|
return parseInt(String(result.records[0].get('count'))) === 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Query: {
|
||||||
|
MyInviteCodes: async (_parent, args, context, _resolveInfo) => {
|
||||||
|
const {
|
||||||
|
user: { id: userId },
|
||||||
|
} = context
|
||||||
|
const session = context.driver.session()
|
||||||
|
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||||
|
const result = await txc.run(
|
||||||
|
`MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode)
|
||||||
|
RETURN properties(ic) AS inviteCodes`,
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return result.records.map((record) => record.get('inviteCodes'))
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const txResult = await readTxResultPromise
|
||||||
|
return txResult
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isValidInviteCode: async (_parent, args, context, _resolveInfo) => {
|
||||||
|
const { code } = args
|
||||||
|
if (!code) return false
|
||||||
|
const session = context.driver.session()
|
||||||
|
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||||
|
const result = await txc.run(
|
||||||
|
`MATCH (ic:InviteCode { code: toUpper($code) })
|
||||||
|
RETURN
|
||||||
|
CASE
|
||||||
|
WHEN ic.expiresAt IS NULL THEN true
|
||||||
|
WHEN datetime(ic.expiresAt) >= datetime() THEN true
|
||||||
|
ELSE false END AS result`,
|
||||||
|
{
|
||||||
|
code,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return result.records.map((record) => record.get('result'))
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const txResult = await readTxResultPromise
|
||||||
|
return !!txResult[0]
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mutation: {
|
||||||
|
GenerateInviteCode: async (_parent, args, context, _resolveInfo) => {
|
||||||
|
const {
|
||||||
|
user: { id: userId },
|
||||||
|
} = context
|
||||||
|
const session = context.driver.session()
|
||||||
|
let code = generateInviteCode()
|
||||||
|
while (!(await uniqueInviteCode(session, code))) {
|
||||||
|
code = generateInviteCode()
|
||||||
|
}
|
||||||
|
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||||
|
const result = await txc.run(
|
||||||
|
`MATCH (user:User {id: $userId})
|
||||||
|
MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code })
|
||||||
|
ON CREATE SET
|
||||||
|
ic.createdAt = toString(datetime()),
|
||||||
|
ic.expiresAt = $expiresAt
|
||||||
|
RETURN ic AS inviteCode`,
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
code,
|
||||||
|
expiresAt: args.expiresAt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return result.records.map((record) => record.get('inviteCode').properties)
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const txResult = await writeTxResultPromise
|
||||||
|
return txResult[0]
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InviteCode: {
|
||||||
|
...Resolver('InviteCode', {
|
||||||
|
idAttribute: 'code',
|
||||||
|
undefinedToNull: ['expiresAt'],
|
||||||
|
hasOne: {
|
||||||
|
generatedBy: '<-[:GENERATED]-(related:User)',
|
||||||
|
},
|
||||||
|
hasMany: {
|
||||||
|
redeemedBy: '<-[:REDEEMED]-(related:User)',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
200
backend/src/schema/resolvers/inviteCodes.spec.js
Normal file
200
backend/src/schema/resolvers/inviteCodes.spec.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
|
import { getDriver } from '../../db/neo4j'
|
||||||
|
import { gql } from '../../helpers/jest'
|
||||||
|
import createServer from '../../server'
|
||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|
||||||
|
let user
|
||||||
|
let query
|
||||||
|
let mutate
|
||||||
|
|
||||||
|
const driver = getDriver()
|
||||||
|
|
||||||
|
const generateInviteCodeMutation = gql`
|
||||||
|
mutation($expiresAt: String = null) {
|
||||||
|
GenerateInviteCode(expiresAt: $expiresAt) {
|
||||||
|
code
|
||||||
|
createdAt
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const myInviteCodesQuery = gql`
|
||||||
|
query {
|
||||||
|
MyInviteCodes {
|
||||||
|
code
|
||||||
|
createdAt
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const isValidInviteCodeQuery = gql`
|
||||||
|
query($code: ID!) {
|
||||||
|
isValidInviteCode(code: $code)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await cleanDatabase()
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
driver,
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
query = createTestClient(server).query
|
||||||
|
mutate = createTestClient(server).mutate
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('inviteCodes', () => {
|
||||||
|
describe('as unauthenticated user', () => {
|
||||||
|
it('cannot generate invite codes', async () => {
|
||||||
|
await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
extensions: { code: 'INTERNAL_SERVER_ERROR' },
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
data: {
|
||||||
|
GenerateInviteCode: null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot query invite codes', async () => {
|
||||||
|
await expect(query({ query: myInviteCodesQuery })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
extensions: { code: 'INTERNAL_SERVER_ERROR' },
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
data: {
|
||||||
|
MyInviteCodes: null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('as authenticated user', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const authenticatedUser = await Factory.build(
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'user@example.org',
|
||||||
|
password: '1234',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
user = await authenticatedUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('generates an invite code without expiresAt', async () => {
|
||||||
|
await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: undefined,
|
||||||
|
data: {
|
||||||
|
GenerateInviteCode: {
|
||||||
|
code: expect.stringMatching(/^[0-9A-Z]{6,6}$/),
|
||||||
|
expiresAt: null,
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('generates an invite code with expiresAt', async () => {
|
||||||
|
const nextWeek = new Date()
|
||||||
|
nextWeek.setDate(nextWeek.getDate() + 7)
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: generateInviteCodeMutation,
|
||||||
|
variables: { expiresAt: nextWeek.toISOString() },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: undefined,
|
||||||
|
data: {
|
||||||
|
GenerateInviteCode: {
|
||||||
|
code: expect.stringMatching(/^[0-9A-Z]{6,6}$/),
|
||||||
|
expiresAt: nextWeek.toISOString(),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
let inviteCodes
|
||||||
|
|
||||||
|
it('returns the created invite codes when queried', async () => {
|
||||||
|
const response = await query({ query: myInviteCodesQuery })
|
||||||
|
inviteCodes = response.data.MyInviteCodes
|
||||||
|
expect(inviteCodes).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not return the created invite codes of other users when queried', async () => {
|
||||||
|
await Factory.build('inviteCode')
|
||||||
|
const response = await query({ query: myInviteCodesQuery })
|
||||||
|
inviteCodes = response.data.MyInviteCodes
|
||||||
|
expect(inviteCodes).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validates an invite code without expiresAt', async () => {
|
||||||
|
const unExpiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt === null)[0].code
|
||||||
|
const result = await query({
|
||||||
|
query: isValidInviteCodeQuery,
|
||||||
|
variables: { code: unExpiringInviteCode },
|
||||||
|
})
|
||||||
|
expect(result.data.isValidInviteCode).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validates an invite code in lower case', async () => {
|
||||||
|
const unExpiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt === null)[0].code
|
||||||
|
const result = await query({
|
||||||
|
query: isValidInviteCodeQuery,
|
||||||
|
variables: { code: unExpiringInviteCode.toLowerCase() },
|
||||||
|
})
|
||||||
|
expect(result.data.isValidInviteCode).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validates an invite code with expiresAt in the future', async () => {
|
||||||
|
const expiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt !== null)[0].code
|
||||||
|
const result = await query({
|
||||||
|
query: isValidInviteCodeQuery,
|
||||||
|
variables: { code: expiringInviteCode },
|
||||||
|
})
|
||||||
|
expect(result.data.isValidInviteCode).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not validate an invite code which expired in the past', async () => {
|
||||||
|
const lastWeek = new Date()
|
||||||
|
lastWeek.setDate(lastWeek.getDate() - 7)
|
||||||
|
const inviteCode = await Factory.build('inviteCode', {
|
||||||
|
expiresAt: lastWeek.toISOString(),
|
||||||
|
})
|
||||||
|
const code = inviteCode.get('code')
|
||||||
|
const result = await query({ query: isValidInviteCodeQuery, variables: { code } })
|
||||||
|
expect(result.data.isValidInviteCode).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not validate an invite code which does not exits', async () => {
|
||||||
|
const result = await query({ query: isValidInviteCodeQuery, variables: { code: 'AAA' } })
|
||||||
|
expect(result.data.isValidInviteCode).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -76,7 +76,6 @@ export default {
|
|||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreatePost: async (_parent, params, context, _resolveInfo) => {
|
CreatePost: async (_parent, params, context, _resolveInfo) => {
|
||||||
const { categoryIds } = params
|
|
||||||
const { image: imageInput } = params
|
const { image: imageInput } = params
|
||||||
delete params.categoryIds
|
delete params.categoryIds
|
||||||
delete params.image
|
delete params.image
|
||||||
@ -92,13 +91,9 @@ export default {
|
|||||||
WITH post
|
WITH post
|
||||||
MATCH (author:User {id: $userId})
|
MATCH (author:User {id: $userId})
|
||||||
MERGE (post)<-[:WROTE]-(author)
|
MERGE (post)<-[:WROTE]-(author)
|
||||||
WITH post
|
|
||||||
UNWIND $categoryIds AS categoryId
|
|
||||||
MATCH (category:Category {id: categoryId})
|
|
||||||
MERGE (post)-[:CATEGORIZED]->(category)
|
|
||||||
RETURN post {.*}
|
RETURN post {.*}
|
||||||
`,
|
`,
|
||||||
{ userId: context.user.id, categoryIds, params },
|
{ userId: context.user.id, params },
|
||||||
)
|
)
|
||||||
const [post] = createPostTransactionResponse.records.map((record) => record.get('post'))
|
const [post] = createPostTransactionResponse.records.map((record) => record.get('post'))
|
||||||
if (imageInput) {
|
if (imageInput) {
|
||||||
|
|||||||
@ -293,6 +293,7 @@ export default {
|
|||||||
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
|
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
|
||||||
invitedBy: '<-[:INVITED]-(related:User)',
|
invitedBy: '<-[:INVITED]-(related:User)',
|
||||||
location: '-[:IS_IN]->(related:Location)',
|
location: '-[:IS_IN]->(related:Location)',
|
||||||
|
redeemedInviteCode: '-[:REDEEMED]->(related:InviteCode)',
|
||||||
},
|
},
|
||||||
hasMany: {
|
hasMany: {
|
||||||
followedBy: '<-[:FOLLOWS]-(related:User)',
|
followedBy: '<-[:FOLLOWS]-(related:User)',
|
||||||
@ -304,6 +305,7 @@ export default {
|
|||||||
shouted: '-[:SHOUTED]->(related:Post)',
|
shouted: '-[:SHOUTED]->(related:Post)',
|
||||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||||
badges: '<-[:REWARDED]-(related:Badge)',
|
badges: '<-[:REWARDED]-(related:Badge)',
|
||||||
|
inviteCodes: '-[:GENERATED]->(related:InviteCode)',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
17
backend/src/schema/types/type/InviteCode.gql
Normal file
17
backend/src/schema/types/type/InviteCode.gql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
type InviteCode {
|
||||||
|
code: ID!
|
||||||
|
createdAt: String!
|
||||||
|
generatedBy: User @relation(name: "GENERATED", direction: "IN")
|
||||||
|
redeemedBy: [User] @relation(name: "REDEEMED", direction: "IN")
|
||||||
|
expiresAt: String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
GenerateInviteCode(expiresAt: String = null): InviteCode
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
MyInviteCodes: [InviteCode]
|
||||||
|
isValidInviteCode(code: ID!): Boolean
|
||||||
|
}
|
||||||
@ -136,7 +136,7 @@ type Post {
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
|
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
|
||||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
||||||
|
|
||||||
comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
|
comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
|
||||||
commentsCount: Int!
|
commentsCount: Int!
|
||||||
|
|||||||
@ -56,6 +56,9 @@ type User {
|
|||||||
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
|
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
|
||||||
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
|
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
|
||||||
|
|
||||||
|
inviteCodes: [InviteCode] @relation(name: "GENERATED", direction: "OUT")
|
||||||
|
redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT")
|
||||||
|
|
||||||
# Is the currently logged in user following that user?
|
# Is the currently logged in user following that user?
|
||||||
followedByCurrentUser: Boolean! @cypher(
|
followedByCurrentUser: Boolean! @cypher(
|
||||||
statement: """
|
statement: """
|
||||||
@ -104,7 +107,7 @@ type User {
|
|||||||
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
|
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
|
||||||
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
|
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
|
||||||
|
|
||||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
||||||
|
|
||||||
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
||||||
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
||||||
|
|||||||
@ -6,21 +6,18 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: webapp
|
context: webapp
|
||||||
target: build-and-test
|
target: build-and-test
|
||||||
volumes:
|
|
||||||
- ./webapp:/develop-webapp
|
|
||||||
environment:
|
environment:
|
||||||
- NUXT_BUILD=/tmp/nuxt # avoid file permission issues when `rm -rf .nuxt/`
|
- NUXT_BUILD=/tmp/nuxt # avoid file permission issues when `rm -rf .nuxt/`
|
||||||
- PUBLIC_REGISTRATION=true
|
- PUBLIC_REGISTRATION=true
|
||||||
command: yarn run dev
|
command: yarn run dev
|
||||||
volumes:
|
volumes:
|
||||||
- webapp_node_modules:/nitro-web/node_modules
|
- ./webapp:/develop-webapp
|
||||||
|
- webapp_node_modules:/develop-webapp/node_modules
|
||||||
backend:
|
backend:
|
||||||
image: ocelotsocialnetwork/develop-backend:build-and-test
|
image: ocelotsocialnetwork/develop-backend:build-and-test
|
||||||
build:
|
build:
|
||||||
context: backend
|
context: backend
|
||||||
target: build-and-test
|
target: build-and-test
|
||||||
volumes:
|
|
||||||
- ./backend:/develop-backend
|
|
||||||
command: yarn run dev
|
command: yarn run dev
|
||||||
environment:
|
environment:
|
||||||
- SMTP_HOST=mailserver
|
- SMTP_HOST=mailserver
|
||||||
@ -29,6 +26,7 @@ services:
|
|||||||
- "DEBUG=${DEBUG}"
|
- "DEBUG=${DEBUG}"
|
||||||
- PUBLIC_REGISTRATION=false
|
- PUBLIC_REGISTRATION=false
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./backend:/develop-backend
|
||||||
- backend_node_modules:/develop-backend/node_modules
|
- backend_node_modules:/develop-backend/node_modules
|
||||||
- uploads:/develop-backend/public/uploads
|
- uploads:/develop-backend/public/uploads
|
||||||
neo4j:
|
neo4j:
|
||||||
|
|||||||
@ -16,6 +16,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./webapp:/develop-webapp
|
||||||
- webapp_node_modules:/develop-webapp/node_modules
|
- webapp_node_modules:/develop-webapp/node_modules
|
||||||
environment:
|
environment:
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
@ -35,6 +36,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 4000:4000
|
- 4000:4000
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./backend:/develop-backend
|
||||||
- backend_node_modules:/develop-backend/node_modules
|
- backend_node_modules:/develop-backend/node_modules
|
||||||
- uploads:/develop-backend/public/uploads
|
- uploads:/develop-backend/public/uploads
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import ContributionForm from './ContributionForm.vue'
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import PostMutations from '~/graphql/PostMutations.js'
|
import PostMutations from '~/graphql/PostMutations.js'
|
||||||
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
|
||||||
|
|
||||||
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
||||||
import MutationObserver from 'mutation-observer'
|
import MutationObserver from 'mutation-observer'
|
||||||
@ -17,34 +16,6 @@ config.stubs['client-only'] = '<span><slot /></span>'
|
|||||||
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||||
config.stubs['v-popover'] = '<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', () => {
|
describe('ContributionForm.vue', () => {
|
||||||
let wrapper,
|
let wrapper,
|
||||||
postTitleInput,
|
postTitleInput,
|
||||||
@ -54,8 +25,7 @@ describe('ContributionForm.vue', () => {
|
|||||||
propsData,
|
propsData,
|
||||||
categoryIds,
|
categoryIds,
|
||||||
englishLanguage,
|
englishLanguage,
|
||||||
deutschLanguage,
|
deutschLanguage
|
||||||
dataPrivacyButton
|
|
||||||
const postTitle = 'this is a title for a post'
|
const postTitle = 'this is a title for a post'
|
||||||
const postTitleTooShort = 'xx'
|
const postTitleTooShort = 'xx'
|
||||||
let postTitleTooLong = ''
|
let postTitleTooLong = ''
|
||||||
@ -136,7 +106,6 @@ describe('ContributionForm.vue', () => {
|
|||||||
describe('CreatePost', () => {
|
describe('CreatePost', () => {
|
||||||
describe('invalid form submission', () => {
|
describe('invalid form submission', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
wrapper.find(CategoriesSelect).setData({ categories })
|
|
||||||
postTitleInput = wrapper.find('.ds-input')
|
postTitleInput = wrapper.find('.ds-input')
|
||||||
postTitleInput.setValue(postTitle)
|
postTitleInput.setValue(postTitle)
|
||||||
await wrapper.vm.updateEditorContent(postContent)
|
await wrapper.vm.updateEditorContent(postContent)
|
||||||
@ -144,10 +113,6 @@ describe('ContributionForm.vue', () => {
|
|||||||
.findAll('li')
|
.findAll('li')
|
||||||
.filter((language) => language.text() === 'English')
|
.filter((language) => language.text() === 'English')
|
||||||
englishLanguage.trigger('click')
|
englishLanguage.trigger('click')
|
||||||
dataPrivacyButton = await wrapper
|
|
||||||
.find(CategoriesSelect)
|
|
||||||
.find('[data-test="category-buttons-cat12"]')
|
|
||||||
dataPrivacyButton.trigger('click')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('title cannot be empty', async () => {
|
it('title cannot be empty', async () => {
|
||||||
@ -173,22 +138,6 @@ describe('ContributionForm.vue', () => {
|
|||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('form').trigger('submit')
|
||||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has at least one category', async () => {
|
|
||||||
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('has no more than three categories', async () => {
|
|
||||||
wrapper.vm.formData.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27']
|
|
||||||
await Vue.nextTick()
|
|
||||||
wrapper.find('form').trigger('submit')
|
|
||||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('valid form submission', () => {
|
describe('valid form submission', () => {
|
||||||
@ -200,23 +149,17 @@ describe('ContributionForm.vue', () => {
|
|||||||
content: postContent,
|
content: postContent,
|
||||||
language: 'en',
|
language: 'en',
|
||||||
id: null,
|
id: null,
|
||||||
categoryIds: ['cat12'],
|
|
||||||
image: null,
|
image: null,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
postTitleInput = wrapper.find('.ds-input')
|
postTitleInput = wrapper.find('.ds-input')
|
||||||
postTitleInput.setValue(postTitle)
|
postTitleInput.setValue(postTitle)
|
||||||
await wrapper.vm.updateEditorContent(postContent)
|
await wrapper.vm.updateEditorContent(postContent)
|
||||||
wrapper.find(CategoriesSelect).setData({ categories })
|
|
||||||
englishLanguage = wrapper
|
englishLanguage = wrapper
|
||||||
.findAll('li')
|
.findAll('li')
|
||||||
.filter((language) => language.text() === 'English')
|
.filter((language) => language.text() === 'English')
|
||||||
englishLanguage.trigger('click')
|
englishLanguage.trigger('click')
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
dataPrivacyButton = await wrapper
|
|
||||||
.find(CategoriesSelect)
|
|
||||||
.find('[data-test="category-buttons-cat12"]')
|
|
||||||
dataPrivacyButton.trigger('click')
|
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -293,16 +236,11 @@ describe('ContributionForm.vue', () => {
|
|||||||
postTitleInput.setValue(postTitle)
|
postTitleInput.setValue(postTitle)
|
||||||
await wrapper.vm.updateEditorContent(postContent)
|
await wrapper.vm.updateEditorContent(postContent)
|
||||||
categoryIds = ['cat12']
|
categoryIds = ['cat12']
|
||||||
wrapper.find(CategoriesSelect).setData({ categories })
|
|
||||||
englishLanguage = wrapper
|
englishLanguage = wrapper
|
||||||
.findAll('li')
|
.findAll('li')
|
||||||
.filter((language) => language.text() === 'English')
|
.filter((language) => language.text() === 'English')
|
||||||
englishLanguage.trigger('click')
|
englishLanguage.trigger('click')
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
dataPrivacyButton = await wrapper
|
|
||||||
.find(CategoriesSelect)
|
|
||||||
.find('[data-test="category-buttons-cat12"]')
|
|
||||||
dataPrivacyButton.trigger('click')
|
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -365,7 +303,6 @@ describe('ContributionForm.vue', () => {
|
|||||||
content: propsData.contribution.content,
|
content: propsData.contribution.content,
|
||||||
language: propsData.contribution.language,
|
language: propsData.contribution.language,
|
||||||
id: propsData.contribution.id,
|
id: propsData.contribution.id,
|
||||||
categoryIds: ['cat12'],
|
|
||||||
image: {
|
image: {
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
},
|
},
|
||||||
@ -380,18 +317,6 @@ describe('ContributionForm.vue', () => {
|
|||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('supports updating categories', async () => {
|
|
||||||
expectedParams.variables.categoryIds.push('cat3')
|
|
||||||
wrapper.find(CategoriesSelect).setData({ categories })
|
|
||||||
await Vue.nextTick()
|
|
||||||
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))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('supports deleting a teaser image', async () => {
|
it('supports deleting a teaser image', async () => {
|
||||||
expectedParams.variables.image = null
|
expectedParams.variables.image = null
|
||||||
propsData.contribution.image = { url: '/uploads/someimage.png' }
|
propsData.contribution.image = { url: '/uploads/someimage.png' }
|
||||||
|
|||||||
@ -50,11 +50,6 @@
|
|||||||
{{ contentLength }}
|
{{ contentLength }}
|
||||||
<base-icon v-if="errors && errors.content" name="warning" />
|
<base-icon v-if="errors && errors.content" name="warning" />
|
||||||
</ds-chip>
|
</ds-chip>
|
||||||
<categories-select model="categoryIds" :existingCategoryIds="formData.categoryIds" />
|
|
||||||
<ds-chip size="base" :color="errors && errors.categoryIds && 'danger'">
|
|
||||||
{{ formData.categoryIds.length }} / 3
|
|
||||||
<base-icon v-if="errors && errors.categoryIds" name="warning" />
|
|
||||||
</ds-chip>
|
|
||||||
<ds-select
|
<ds-select
|
||||||
model="language"
|
model="language"
|
||||||
icon="globe"
|
icon="globe"
|
||||||
@ -86,14 +81,12 @@ import { mapGetters } from 'vuex'
|
|||||||
import HcEditor from '~/components/Editor/Editor'
|
import HcEditor from '~/components/Editor/Editor'
|
||||||
import locales from '~/locales'
|
import locales from '~/locales'
|
||||||
import PostMutations from '~/graphql/PostMutations.js'
|
import PostMutations from '~/graphql/PostMutations.js'
|
||||||
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
|
||||||
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
||||||
import links from '~/constants/links.js'
|
import links from '~/constants/links.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HcEditor,
|
HcEditor,
|
||||||
CategoriesSelect,
|
|
||||||
ImageUploader,
|
ImageUploader,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@ -103,7 +96,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const { title, content, image, language, categories } = this.contribution
|
const { title, content, image, language } = this.contribution
|
||||||
|
|
||||||
const languageOptions = orderBy(locales, 'name').map((locale) => {
|
const languageOptions = orderBy(locales, 'name').map((locale) => {
|
||||||
return { label: locale.name, value: locale.code }
|
return { label: locale.name, value: locale.code }
|
||||||
@ -119,21 +112,10 @@ export default {
|
|||||||
imageAspectRatio,
|
imageAspectRatio,
|
||||||
imageBlurred,
|
imageBlurred,
|
||||||
language: languageOptions.find((option) => option.value === language) || null,
|
language: languageOptions.find((option) => option.value === language) || null,
|
||||||
categoryIds: categories ? categories.map((category) => category.id) : [],
|
|
||||||
},
|
},
|
||||||
formSchema: {
|
formSchema: {
|
||||||
title: { required: true, min: 3, max: 100 },
|
title: { required: true, min: 3, max: 100 },
|
||||||
content: { required: true },
|
content: { required: true },
|
||||||
categoryIds: {
|
|
||||||
type: 'array',
|
|
||||||
required: true,
|
|
||||||
validator: (_, value = []) => {
|
|
||||||
if (value.length === 0 || value.length > 3) {
|
|
||||||
return [new Error(this.$t('common.validations.categories'))]
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
language: { required: true },
|
language: { required: true },
|
||||||
imageBlurred: { required: false },
|
imageBlurred: { required: false },
|
||||||
},
|
},
|
||||||
@ -155,7 +137,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
let image = null
|
let image = null
|
||||||
const { title, content, categoryIds } = this.formData
|
const { title, content } = this.formData
|
||||||
if (this.formData.image) {
|
if (this.formData.image) {
|
||||||
image = {
|
image = {
|
||||||
sensitive: this.formData.imageBlurred,
|
sensitive: this.formData.imageBlurred,
|
||||||
@ -172,7 +154,6 @@ export default {
|
|||||||
variables: {
|
variables: {
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
categoryIds,
|
|
||||||
id: this.contribution.id || null,
|
id: this.contribution.id || null,
|
||||||
language: this.formData.language.value,
|
language: this.formData.language.value,
|
||||||
image,
|
image,
|
||||||
|
|||||||
@ -81,7 +81,7 @@
|
|||||||
"stack-utils": "^2.0.1",
|
"stack-utils": "^2.0.1",
|
||||||
"tippy.js": "^4.3.5",
|
"tippy.js": "^4.3.5",
|
||||||
"tiptap": "~1.26.6",
|
"tiptap": "~1.26.6",
|
||||||
"tiptap-extensions": "~1.34.0",
|
"tiptap-extensions": "~1.28.8",
|
||||||
"trunc-html": "^1.1.2",
|
"trunc-html": "^1.1.2",
|
||||||
"v-tooltip": "~2.0.3",
|
"v-tooltip": "~2.0.3",
|
||||||
"validator": "^13.0.0",
|
"validator": "^13.0.0",
|
||||||
|
|||||||
@ -46,21 +46,6 @@
|
|||||||
<content-viewer class="content hyphenate-text" :content="post.content" />
|
<content-viewer class="content hyphenate-text" :content="post.content" />
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
<ds-space margin="xx-large" />
|
<ds-space margin="xx-large" />
|
||||||
<!-- Categories -->
|
|
||||||
<div class="categories">
|
|
||||||
<ds-space margin="xx-small" />
|
|
||||||
<hc-category
|
|
||||||
v-for="category in post.categories"
|
|
||||||
:key="category.id"
|
|
||||||
:icon="category.icon"
|
|
||||||
:name="$t(`contribution.category.name.${category.slug}`)"
|
|
||||||
/>
|
|
||||||
<!-- Post language -->
|
|
||||||
<ds-tag v-if="post.language" class="category-tag language">
|
|
||||||
<base-icon name="globe" />
|
|
||||||
{{ post.language.toUpperCase() }}
|
|
||||||
</ds-tag>
|
|
||||||
</div>
|
|
||||||
<ds-space margin-bottom="small" />
|
<ds-space margin-bottom="small" />
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<div v-if="post.tags && post.tags.length" class="tags">
|
<div v-if="post.tags && post.tags.length" class="tags">
|
||||||
@ -110,7 +95,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||||
import HcCategory from '~/components/Category'
|
|
||||||
import HcHashtag from '~/components/Hashtag/Hashtag'
|
import HcHashtag from '~/components/Hashtag/Hashtag'
|
||||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||||
@ -134,7 +118,6 @@ export default {
|
|||||||
mode: 'out-in',
|
mode: 'out-in',
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
HcCategory,
|
|
||||||
HcHashtag,
|
HcHashtag,
|
||||||
UserTeaser,
|
UserTeaser,
|
||||||
HcShoutButton,
|
HcShoutButton,
|
||||||
|
|||||||
220
webapp/yarn.lock
220
webapp/yarn.lock
@ -9576,11 +9576,6 @@ hex-color-regex@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
||||||
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
||||||
|
|
||||||
highlight.js@~10.5.0:
|
|
||||||
version "10.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f"
|
|
||||||
integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==
|
|
||||||
|
|
||||||
highlight.js@~9.12.0:
|
highlight.js@~9.12.0:
|
||||||
version "9.12.0"
|
version "9.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e"
|
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e"
|
||||||
@ -9591,6 +9586,11 @@ highlight.js@~9.13.0:
|
|||||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
|
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
|
||||||
integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==
|
integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==
|
||||||
|
|
||||||
|
highlight.js@~9.16.0:
|
||||||
|
version "9.16.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.16.2.tgz#68368d039ffe1c6211bcc07e483daf95de3e403e"
|
||||||
|
integrity sha512-feMUrVLZvjy0oC7FVJQcSQRqbBq9kwqnYE4+Kj9ZjbHh3g+BisiPgF49NyQbVLNdrL/qqZr3Ca9yOKwgn2i/tw==
|
||||||
|
|
||||||
hmac-drbg@^1.0.0:
|
hmac-drbg@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||||
@ -11857,13 +11857,13 @@ lowercase-keys@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
||||||
integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
|
integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
|
||||||
|
|
||||||
lowlight@^1.17.0:
|
lowlight@1.13.1:
|
||||||
version "1.18.0"
|
version "1.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.18.0.tgz#cfff11cfb125ca66f1c12cb43d27fff68cbeafa9"
|
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.13.1.tgz#c4f0e03906ebd23fedf2d258f6ab2f6324cf90eb"
|
||||||
integrity sha512-Zlc3GqclU71HRw5fTOy00zz5EOlqAdKMYhOFIO8ay4SQEDQgFuhR8JNwDIzAGMLoqTsWxe0elUNmq5o2USRAzw==
|
integrity sha512-kQ71/T6RksEVz9AlPq07/2m+SU/1kGvt9k39UtvHX760u4SaWakaYH7hYgH5n6sTsCWk4MVYzUzLU59aN5CSmQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
fault "^1.0.0"
|
fault "^1.0.0"
|
||||||
highlight.js "~10.5.0"
|
highlight.js "~9.16.0"
|
||||||
|
|
||||||
lowlight@~1.11.0:
|
lowlight@~1.11.0:
|
||||||
version "1.11.0"
|
version "1.11.0"
|
||||||
@ -14309,7 +14309,7 @@ property-information@^5.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xtend "^4.0.1"
|
xtend "^4.0.1"
|
||||||
|
|
||||||
prosemirror-collab@^1.2.2:
|
prosemirror-collab@1.2.2:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.2.2.tgz#8d2c0e82779cfef5d051154bd0836428bd6d9c4a"
|
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.2.2.tgz#8d2c0e82779cfef5d051154bd0836428bd6d9c4a"
|
||||||
integrity sha512-tBnHKMLgy5Qmx9MYVcLfs3pAyjtcqYYDd9kp3y+LSiQzkhMQDfZSV3NXWe4Gsly32adSef173BvObwfoSQL5MA==
|
integrity sha512-tBnHKMLgy5Qmx9MYVcLfs3pAyjtcqYYDd9kp3y+LSiQzkhMQDfZSV3NXWe4Gsly32adSef173BvObwfoSQL5MA==
|
||||||
@ -14325,24 +14325,6 @@ prosemirror-commands@1.1.3:
|
|||||||
prosemirror-state "^1.0.0"
|
prosemirror-state "^1.0.0"
|
||||||
prosemirror-transform "^1.0.0"
|
prosemirror-transform "^1.0.0"
|
||||||
|
|
||||||
prosemirror-commands@1.1.4:
|
|
||||||
version "1.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.1.4.tgz#991563e67623acab4f8c510fad1570f8b4693780"
|
|
||||||
integrity sha512-kj4Qi+8h3EpJtZuuEDwZ9h2/QNGWDsIX/CzjmClxi9GhxWyBUMVUvIFk0mgdqHyX20lLeGmOpc0TLA5aPzgpWg==
|
|
||||||
dependencies:
|
|
||||||
prosemirror-model "^1.0.0"
|
|
||||||
prosemirror-state "^1.0.0"
|
|
||||||
prosemirror-transform "^1.0.0"
|
|
||||||
|
|
||||||
prosemirror-commands@^1.1.4:
|
|
||||||
version "1.1.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.1.5.tgz#3f07a5b13e424ad8728168b6b45e1b17e47c2b81"
|
|
||||||
integrity sha512-4CKAnDxLTtUHpjRZZVEF/LLMUYh7NbS3Ze3mP5UEAgar4WRWQYg3Js01wnp/GMqaZueNHhsp9UVvOrAK+7DWbQ==
|
|
||||||
dependencies:
|
|
||||||
prosemirror-model "^1.0.0"
|
|
||||||
prosemirror-state "^1.0.0"
|
|
||||||
prosemirror-transform "^1.0.0"
|
|
||||||
|
|
||||||
prosemirror-dropcursor@1.3.2:
|
prosemirror-dropcursor@1.3.2:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.3.2.tgz#28738c4ed7102e814d7a8a26d70018523fc7cd6d"
|
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.3.2.tgz#28738c4ed7102e814d7a8a26d70018523fc7cd6d"
|
||||||
@ -14362,17 +14344,7 @@ prosemirror-gapcursor@1.1.4:
|
|||||||
prosemirror-state "^1.0.0"
|
prosemirror-state "^1.0.0"
|
||||||
prosemirror-view "^1.0.0"
|
prosemirror-view "^1.0.0"
|
||||||
|
|
||||||
prosemirror-gapcursor@1.1.5:
|
prosemirror-history@1.1.3:
|
||||||
version "1.1.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.1.5.tgz#0c37fd6cbb1d7c46358c2e7397f8da9a8b5c6246"
|
|
||||||
integrity sha512-SjbUZq5pgsBDuV3hu8GqgIpZR5eZvGLM+gPQTqjVVYSMUCfKW3EGXTEYaLHEl1bGduwqNC95O3bZflgtAb4L6w==
|
|
||||||
dependencies:
|
|
||||||
prosemirror-keymap "^1.0.0"
|
|
||||||
prosemirror-model "^1.0.0"
|
|
||||||
prosemirror-state "^1.0.0"
|
|
||||||
prosemirror-view "^1.0.0"
|
|
||||||
|
|
||||||
prosemirror-history@^1.1.3:
|
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.1.3.tgz#4f76a1e71db4ef7cdf0e13dec6d8da2aeaecd489"
|
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.1.3.tgz#4f76a1e71db4ef7cdf0e13dec6d8da2aeaecd489"
|
||||||
integrity sha512-zGDotijea+vnfnyyUGyiy1wfOQhf0B/b6zYcCouBV8yo6JmrE9X23M5q7Nf/nATywEZbgRLG70R4DmfSTC+gfg==
|
integrity sha512-zGDotijea+vnfnyyUGyiy1wfOQhf0B/b6zYcCouBV8yo6JmrE9X23M5q7Nf/nATywEZbgRLG70R4DmfSTC+gfg==
|
||||||
@ -14389,14 +14361,6 @@ prosemirror-inputrules@1.1.2:
|
|||||||
prosemirror-state "^1.0.0"
|
prosemirror-state "^1.0.0"
|
||||||
prosemirror-transform "^1.0.0"
|
prosemirror-transform "^1.0.0"
|
||||||
|
|
||||||
prosemirror-inputrules@1.1.3, prosemirror-inputrules@^1.1.2:
|
|
||||||
version "1.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.1.3.tgz#93f9199ca02473259c30d7e352e4c14022d54638"
|
|
||||||
integrity sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw==
|
|
||||||
dependencies:
|
|
||||||
prosemirror-state "^1.0.0"
|
|
||||||
prosemirror-transform "^1.0.0"
|
|
||||||
|
|
||||||
prosemirror-keymap@1.1.3, prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2:
|
prosemirror-keymap@1.1.3, prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.3.tgz#be22d6108df2521608e9216a87b1a810f0ed361e"
|
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.3.tgz#be22d6108df2521608e9216a87b1a810f0ed361e"
|
||||||
@ -14405,37 +14369,22 @@ prosemirror-keymap@1.1.3, prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2:
|
|||||||
prosemirror-state "^1.0.0"
|
prosemirror-state "^1.0.0"
|
||||||
w3c-keyname "^2.2.0"
|
w3c-keyname "^2.2.0"
|
||||||
|
|
||||||
prosemirror-keymap@1.1.4:
|
prosemirror-model@1.9.1, prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.8.1:
|
||||||
version "1.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.4.tgz#8b481bf8389a5ac40d38dbd67ec3da2c7eac6a6d"
|
|
||||||
integrity sha512-Al8cVUOnDFL4gcI5IDlG6xbZ0aOD/i3B17VT+1JbHWDguCgt/lBHVTHUBcKvvbSg6+q/W4Nj1Fu6bwZSca3xjg==
|
|
||||||
dependencies:
|
|
||||||
prosemirror-state "^1.0.0"
|
|
||||||
w3c-keyname "^2.2.0"
|
|
||||||
|
|
||||||
prosemirror-model@1.13.1, prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.13.1, prosemirror-model@^1.8.1:
|
|
||||||
version "1.13.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.13.1.tgz#fa3dc888cf6928bd3968620588ffe6458d201f9f"
|
|
||||||
integrity sha512-PNH+b5bilAJi1B5yJ8QzoNY3ZV+nlD0jKG3XCBk7PmE/YUTJomBkFBS005vfU+3M9yeVR8/6spAEDsfVFUhNeQ==
|
|
||||||
dependencies:
|
|
||||||
orderedmap "^1.1.0"
|
|
||||||
|
|
||||||
prosemirror-model@1.9.1:
|
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.9.1.tgz#8c08cf556f593c5f015548d2c1a6825661df087f"
|
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.9.1.tgz#8c08cf556f593c5f015548d2c1a6825661df087f"
|
||||||
integrity sha512-Qblh8pm1c7Ll64sYLauwwzjimo/tFg1zW3Q3IWhKRhvfOEgRKqa6dC5pRrAa+XHOIjBFEYrqbi52J5bqA2dV8Q==
|
integrity sha512-Qblh8pm1c7Ll64sYLauwwzjimo/tFg1zW3Q3IWhKRhvfOEgRKqa6dC5pRrAa+XHOIjBFEYrqbi52J5bqA2dV8Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
orderedmap "^1.1.0"
|
orderedmap "^1.1.0"
|
||||||
|
|
||||||
prosemirror-schema-list@^1.1.4:
|
prosemirror-schema-list@1.1.2:
|
||||||
version "1.1.4"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.1.4.tgz#471f9caf2d2bed93641d2e490434c0d2d4330df1"
|
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.1.2.tgz#310809209094b03425da7f5c337105074913da6c"
|
||||||
integrity sha512-pNTuZflacFOBlxrTcWSdWhjoB8BaucwfJVp/gJNxztOwaN3wQiC65axclXyplf6TKgXD/EkWfS/QAov3/Znadw==
|
integrity sha512-dgM9PwtM4twa5WsgSYMB+J8bwjnR43DAD3L9MsR9rKm/nZR5Y85xcjB7gusVMSsbQ2NomMZF03RE6No6mTnclQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-model "^1.0.0"
|
prosemirror-model "^1.0.0"
|
||||||
prosemirror-transform "^1.0.0"
|
prosemirror-transform "^1.0.0"
|
||||||
|
|
||||||
prosemirror-state@1.3.3, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.3.3:
|
prosemirror-state@1.3.3, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.3.3.tgz#b2862866b14dec2b3ae1ab18229f2bd337651a2c"
|
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.3.3.tgz#b2862866b14dec2b3ae1ab18229f2bd337651a2c"
|
||||||
integrity sha512-PLXh2VJsIgvlgSTH6I2Yg6vk1CzPDp21DFreVpQtDMY2S6WaMmrQgDTLRcsrD8X38v8Yc873H7+ogdGzyIPn+w==
|
integrity sha512-PLXh2VJsIgvlgSTH6I2Yg6vk1CzPDp21DFreVpQtDMY2S6WaMmrQgDTLRcsrD8X38v8Yc873H7+ogdGzyIPn+w==
|
||||||
@ -14443,10 +14392,10 @@ prosemirror-state@1.3.3, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, pro
|
|||||||
prosemirror-model "^1.0.0"
|
prosemirror-model "^1.0.0"
|
||||||
prosemirror-transform "^1.0.0"
|
prosemirror-transform "^1.0.0"
|
||||||
|
|
||||||
prosemirror-tables@^1.1.1:
|
prosemirror-tables@1.0.0:
|
||||||
version "1.1.1"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz#ad66300cc49500455cf1243bb129c9e7d883321e"
|
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.0.0.tgz#ec3d0b11e638c6a92dd14ae816d0a2efd1719b70"
|
||||||
integrity sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA==
|
integrity sha512-zFw5Us4G5Vdq0yIj8GiqZOGA6ud5UKpMKElux9O0HrfmhkuGa1jf1PCpz2R5pmIQJv+tIM24H1mox/ODBAX37Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-keymap "^1.1.2"
|
prosemirror-keymap "^1.1.2"
|
||||||
prosemirror-model "^1.8.1"
|
prosemirror-model "^1.8.1"
|
||||||
@ -14454,19 +14403,19 @@ prosemirror-tables@^1.1.1:
|
|||||||
prosemirror-transform "^1.2.1"
|
prosemirror-transform "^1.2.1"
|
||||||
prosemirror-view "^1.13.3"
|
prosemirror-view "^1.13.3"
|
||||||
|
|
||||||
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.2.8:
|
prosemirror-transform@1.2.4, prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1:
|
||||||
version "1.2.8"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.8.tgz#4b86544fa43637fe381549fb7b019f4fb71fe65c"
|
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.4.tgz#8d5843834f5ccedfb614faa9220672bb4834b00a"
|
||||||
integrity sha512-hKqceqv9ZmMQXNQkhFjr0KFGPvkhygaWND+uIM0GxRpALrKfxP97SsgHTBs3OpJhDmh5N+mB4D/CksB291Eavg==
|
integrity sha512-0A668uf0EN89L9O9brE05kHcqp7FHmT5YN7Tom58Kj926QqOBs7iNRHDLWxrSaQB5MNZtzDOD9T3EyJ88YDcBg==
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-model "^1.0.0"
|
prosemirror-model "^1.0.0"
|
||||||
|
|
||||||
prosemirror-utils@^0.9.6:
|
prosemirror-utils@0.9.6:
|
||||||
version "0.9.6"
|
version "0.9.6"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973"
|
resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973"
|
||||||
integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA==
|
integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA==
|
||||||
|
|
||||||
prosemirror-view@1.14.6:
|
prosemirror-view@1.14.6, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3:
|
||||||
version "1.14.6"
|
version "1.14.6"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.14.6.tgz#fa1e7ed14a38f2cb234f622037a07dbd9d2830de"
|
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.14.6.tgz#fa1e7ed14a38f2cb234f622037a07dbd9d2830de"
|
||||||
integrity sha512-0qNSFWVBHPrdQaZtIO3aou/NRsxMGER3IuI3cePHYbk5pf9wSsbMIWWaeHtXqblL+rqtgkLfcw0D2na6+WBgpA==
|
integrity sha512-0qNSFWVBHPrdQaZtIO3aou/NRsxMGER3IuI3cePHYbk5pf9wSsbMIWWaeHtXqblL+rqtgkLfcw0D2na6+WBgpA==
|
||||||
@ -14475,24 +14424,6 @@ prosemirror-view@1.14.6:
|
|||||||
prosemirror-state "^1.0.0"
|
prosemirror-state "^1.0.0"
|
||||||
prosemirror-transform "^1.1.0"
|
prosemirror-transform "^1.1.0"
|
||||||
|
|
||||||
prosemirror-view@1.16.5:
|
|
||||||
version "1.16.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.16.5.tgz#1a4646832e16c1cf116b54b9becf4b0663821125"
|
|
||||||
integrity sha512-cFEjzhqQZIRDALEgQt8CNn+Qb+BUOvNxxaljaWoCbAYlsWGMiNNQG06I1MwbRNDcwnZKeFmOGpLEB4eorYYGig==
|
|
||||||
dependencies:
|
|
||||||
prosemirror-model "^1.1.0"
|
|
||||||
prosemirror-state "^1.0.0"
|
|
||||||
prosemirror-transform "^1.1.0"
|
|
||||||
|
|
||||||
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.16.5:
|
|
||||||
version "1.17.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.17.2.tgz#666c865ae79e129a8933112bdcdf218a42c5a3b5"
|
|
||||||
integrity sha512-8jHmdl1q/Mvjfv185I5FldBitkkVpNOIK0Z/jIuan4cZIqXRpKu7DxxeLrTouJDzgrf1kWvfG/szEb6Bg9/4dA==
|
|
||||||
dependencies:
|
|
||||||
prosemirror-model "^1.1.0"
|
|
||||||
prosemirror-state "^1.0.0"
|
|
||||||
prosemirror-transform "^1.1.0"
|
|
||||||
|
|
||||||
proto-list@~1.2.1:
|
proto-list@~1.2.1:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||||
@ -16917,65 +16848,48 @@ tippy.js@^4.3.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
popper.js "^1.14.7"
|
popper.js "^1.14.7"
|
||||||
|
|
||||||
tiptap-commands@^1.12.7, tiptap-commands@^1.16.0:
|
tiptap-commands@^1.12.7:
|
||||||
version "1.16.0"
|
version "1.12.7"
|
||||||
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.16.0.tgz#02ba31c386ab22c3999ea620787761e014b99809"
|
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.12.7.tgz#7161a84e9fffb9c6b48f4a7d95cd8a72916abfcf"
|
||||||
integrity sha512-/8QUHLOqyGc0d8KVzGlFJtf71gtK4+yxF/BURbUQyee1YshhbUYFV1xMG0muSyqdxWDuvKB5BUPqyEfckdiYeg==
|
integrity sha512-y63MEA9Nyj8zw0klSqKuQsqsRcvgvm3WLtBkcJ/FWRTEL+wufQzT7/AshUuX/Tb1Ss2Fl6Id5S7N1Rr/NaCsaA==
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-commands "^1.1.4"
|
prosemirror-commands "1.1.3"
|
||||||
prosemirror-inputrules "^1.1.2"
|
prosemirror-inputrules "1.1.2"
|
||||||
prosemirror-model "^1.13.1"
|
prosemirror-model "1.9.1"
|
||||||
prosemirror-schema-list "^1.1.4"
|
prosemirror-schema-list "1.1.2"
|
||||||
prosemirror-state "^1.3.3"
|
|
||||||
prosemirror-tables "^1.1.1"
|
|
||||||
prosemirror-utils "^0.9.6"
|
|
||||||
tiptap-utils "^1.12.0"
|
|
||||||
|
|
||||||
tiptap-extensions@~1.34.0:
|
|
||||||
version "1.34.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.34.0.tgz#59889315ebb843c2b7a7326090a0f5ec13873bd0"
|
|
||||||
integrity sha512-aKTGGPW6dWdVQfyXnuG4KLF+wWE5h7RZYCY72VkaybE1xft2lVcMvWl5G1wi4mvo9RVZYR8SJSGFzLsWLetOkg==
|
|
||||||
dependencies:
|
|
||||||
lowlight "^1.17.0"
|
|
||||||
prosemirror-collab "^1.2.2"
|
|
||||||
prosemirror-history "^1.1.3"
|
|
||||||
prosemirror-model "^1.13.1"
|
|
||||||
prosemirror-state "^1.3.3"
|
|
||||||
prosemirror-tables "^1.1.1"
|
|
||||||
prosemirror-transform "^1.2.8"
|
|
||||||
prosemirror-utils "^0.9.6"
|
|
||||||
prosemirror-view "^1.16.5"
|
|
||||||
tiptap "^1.31.0"
|
|
||||||
tiptap-commands "^1.16.0"
|
|
||||||
tiptap-utils "^1.12.0"
|
|
||||||
|
|
||||||
tiptap-utils@^1.12.0, tiptap-utils@^1.8.4:
|
|
||||||
version "1.12.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.12.0.tgz#56b8cb95db8af8172083f1c7e0cab74a8c6d6ca9"
|
|
||||||
integrity sha512-FdygaOf2EbC55Ba9cEAz2DPuyOD9XAfSo3ICxuCjP5JGV7o9nULF1ABZbVHVdx6A52vsu7v4MOHs8f5hDrW7pw==
|
|
||||||
dependencies:
|
|
||||||
prosemirror-model "^1.13.1"
|
|
||||||
prosemirror-state "^1.3.3"
|
|
||||||
prosemirror-tables "^1.1.1"
|
|
||||||
prosemirror-utils "^0.9.6"
|
|
||||||
|
|
||||||
tiptap@^1.31.0:
|
|
||||||
version "1.31.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.31.0.tgz#298775603b5e16afe36448c486a2bd1e63ffa690"
|
|
||||||
integrity sha512-FY0juyY7yQwASvGKzle9ndCXlqMzBHZxUQDx2ybI8ghWVNavkMWUUDa+QGbscITYlQc2y43G0QEOqhzzZGLZ7g==
|
|
||||||
dependencies:
|
|
||||||
prosemirror-commands "1.1.4"
|
|
||||||
prosemirror-dropcursor "1.3.2"
|
|
||||||
prosemirror-gapcursor "1.1.5"
|
|
||||||
prosemirror-inputrules "1.1.3"
|
|
||||||
prosemirror-keymap "1.1.4"
|
|
||||||
prosemirror-model "1.13.1"
|
|
||||||
prosemirror-state "1.3.3"
|
prosemirror-state "1.3.3"
|
||||||
prosemirror-view "1.16.5"
|
prosemirror-tables "1.0.0"
|
||||||
tiptap-commands "^1.16.0"
|
prosemirror-utils "0.9.6"
|
||||||
tiptap-utils "^1.12.0"
|
tiptap-utils "^1.8.4"
|
||||||
|
|
||||||
tiptap@~1.26.6:
|
tiptap-extensions@~1.28.8:
|
||||||
|
version "1.28.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.28.8.tgz#1a631fa2c7878c81616e50ea37e61c9be388e486"
|
||||||
|
integrity sha512-1e2LiZWEDL8df0Na3RnciAFP2OePeT1MGem54kjTouId9rP6ouDF6u3jfqMw1NF4VpeSClLdnKrNtEDqCkRfWw==
|
||||||
|
dependencies:
|
||||||
|
lowlight "1.13.1"
|
||||||
|
prosemirror-collab "1.2.2"
|
||||||
|
prosemirror-history "1.1.3"
|
||||||
|
prosemirror-model "1.9.1"
|
||||||
|
prosemirror-state "1.3.3"
|
||||||
|
prosemirror-tables "1.0.0"
|
||||||
|
prosemirror-transform "1.2.4"
|
||||||
|
prosemirror-utils "0.9.6"
|
||||||
|
prosemirror-view "1.14.6"
|
||||||
|
tiptap "^1.26.8"
|
||||||
|
tiptap-commands "^1.12.7"
|
||||||
|
|
||||||
|
tiptap-utils@^1.8.4:
|
||||||
|
version "1.8.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.8.4.tgz#5465c41abbbd0ddb127d22a1bb56e64cf3f3ff03"
|
||||||
|
integrity sha512-n8nYB96rphfjmDnPBYgLzGpyLH30H1PoBVqFzmQ+K8sNMkW7vHTA5Yrt5E3rcfgt15HF7VldqUTpKyAjDwdkCw==
|
||||||
|
dependencies:
|
||||||
|
prosemirror-model "1.9.1"
|
||||||
|
prosemirror-state "1.3.3"
|
||||||
|
prosemirror-tables "1.0.0"
|
||||||
|
prosemirror-utils "0.9.6"
|
||||||
|
|
||||||
|
tiptap@^1.26.8, tiptap@~1.26.6:
|
||||||
version "1.26.8"
|
version "1.26.8"
|
||||||
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.26.8.tgz#31017a0d3f5c51464caab4f1ac1581f21474da43"
|
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.26.8.tgz#31017a0d3f5c51464caab4f1ac1581f21474da43"
|
||||||
integrity sha512-Bd80+ymPCsfkDkwpBbuJpx913BjkMi7ZHYqoFLoZ7V37tAznvJRQ35966r0s5imxD195lnlrKzN7af7E+/6lLA==
|
integrity sha512-Bd80+ymPCsfkDkwpBbuJpx913BjkMi7ZHYqoFLoZ7V37tAznvJRQ35966r0s5imxD195lnlrKzN7af7E+/6lLA==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user