refactor(backend): remove content excerpt (#9441)

This commit is contained in:
Ulf Gebhardt 2026-03-27 14:38:46 +01:00 committed by GitHub
parent 33f522ed16
commit b088ec9e62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 108 additions and 122 deletions

View File

@ -199,9 +199,6 @@ Factory.define('post')
// Convert false to null // Convert false to null
return pinned || null return pinned || null
}) })
.attr('contentExcerpt', ['contentExcerpt', 'content'], (contentExcerpt, content) => {
return contentExcerpt || content
})
.attr('slug', ['slug', 'title'], (slug, title) => { .attr('slug', ['slug', 'title'], (slug, title) => {
return slug || slugify(title, { lower: true }) return slug || slugify(title, { lower: true })
}) })
@ -294,9 +291,6 @@ Factory.define('comment')
id: uuid, id: uuid,
content: faker.lorem.sentence, content: faker.lorem.sentence,
}) })
.attr('contentExcerpt', ['contentExcerpt', 'content'], (contentExcerpt, content) => {
return contentExcerpt || content
})
.after(async (buildObject, options) => { .after(async (buildObject, options) => {
const [comment, author, post] = await Promise.all([ const [comment, author, post] = await Promise.all([
neode.create('Comment', buildObject), neode.create('Comment', buildObject),

View File

@ -0,0 +1,35 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { getDriver } from '@db/neo4j'
export const description = 'Remove contentExcerpt property from Post and Comment nodes'
export async function up(_next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
await transaction.run(`
MATCH (n)
WHERE n:Post OR n:Comment
REMOVE n.contentExcerpt
`)
await transaction.commit()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
throw new Error(error)
} finally {
await session.close()
}
}
export function down(_next) {
throw new Error(
'Irreversible migration: contentExcerpt was removed and cannot be restored without regenerating from content',
)
}

View File

@ -10,7 +10,6 @@ export default {
default: () => new Date().toISOString(), default: () => new Date().toISOString(),
}, },
content: { type: 'string', disallow: [null], min: 3 }, content: { type: 'string', disallow: [null], min: 3 },
contentExcerpt: { type: 'string', allow: [null] },
deleted: { type: 'boolean', default: false }, deleted: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false },
post: { post: {

View File

@ -19,7 +19,6 @@ export default {
title: { type: 'string', disallow: [null], min: 3 }, title: { type: 'string', disallow: [null], min: 3 },
slug: { type: 'string', allow: [null], unique: 'true' }, slug: { type: 'string', allow: [null], unique: 'true' },
content: { type: 'string', disallow: [null], required: true, min: 3 }, content: { type: 'string', disallow: [null], required: true, min: 3 },
contentExcerpt: { type: 'string', allow: [null] },
deleted: { type: 'boolean', default: false }, deleted: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false },
clickedCount: { type: 'int', default: 0 }, clickedCount: { type: 'int', default: 0 },

View File

@ -2,7 +2,6 @@ import type { Integer, Node } from 'neo4j-driver'
export interface CommentDbProperties { export interface CommentDbProperties {
content: string content: string
contentExcerpt: string
createdAt: string createdAt: string
deleted: boolean deleted: boolean
disabled: boolean disabled: boolean

View File

@ -3,7 +3,6 @@ import type { Integer, Node } from 'neo4j-driver'
export interface PostDbProperties { export interface PostDbProperties {
clickedCount: number clickedCount: number
content: string content: string
contentExcerpt: string
createdAt: string createdAt: string
deleted: boolean deleted: boolean
disabled: boolean disabled: boolean

View File

@ -2,7 +2,6 @@ mutation DeleteComment($id: ID!) {
DeleteComment(id: $id) { DeleteComment(id: $id) {
id id
content content
contentExcerpt
deleted deleted
} }
} }

View File

@ -3,7 +3,6 @@ mutation DeletePost($id: ID!) {
id id
deleted deleted
content content
contentExcerpt
image { image {
url url
} }
@ -11,7 +10,6 @@ mutation DeletePost($id: ID!) {
id id
deleted deleted
content content
contentExcerpt
} }
} }
} }

View File

@ -3,7 +3,6 @@ query Post($id: ID, $filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [
id id
title title
content content
contentExcerpt
eventStart eventStart
pinned pinned
createdAt createdAt

View File

@ -7,19 +7,16 @@ mutation DeleteUser($id: ID!, $resource: [Deletable]) {
contributions { contributions {
id id
content content
contentExcerpt
deleted deleted
comments { comments {
id id
content content
contentExcerpt
deleted deleted
} }
} }
comments { comments {
id id
content content
contentExcerpt
deleted deleted
} }
} }

View File

@ -29,7 +29,6 @@ query User($id: ID, $name: String, $email: String) {
comments { comments {
id id
content content
contentExcerpt
} }
contributions { contributions {
id id
@ -39,7 +38,6 @@ query User($id: ID, $name: String, $email: String) {
url url
} }
content content
contentExcerpt
} }
} }
isMuted isMuted

View File

@ -30,7 +30,6 @@ query UserEmail($id: ID, $name: String, $email: String) {
comments { comments {
id id
content content
contentExcerpt
} }
contributions { contributions {
id id
@ -40,7 +39,6 @@ query UserEmail($id: ID, $name: String, $email: String) {
url url
} }
content content
contentExcerpt
} }
} }
isMuted isMuted

View File

@ -29,7 +29,6 @@ query UserEmailNotificationSettings($id: ID, $name: String, $email: String) {
comments { comments {
id id
content content
contentExcerpt
} }
contributions { contributions {
id id
@ -39,7 +38,6 @@ query UserEmailNotificationSettings($id: ID, $name: String, $email: String) {
url url
} }
content content
contentExcerpt
} }
} }
isMuted isMuted

View File

@ -240,7 +240,6 @@ describe('DeleteComment', () => {
id: 'c456', id: 'c456',
deleted: true, deleted: true,
content: 'UNAVAILABLE', content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
}, },
} }
expect(data).toMatchObject(expected) expect(data).toMatchObject(expected)

View File

@ -83,7 +83,6 @@ export default {
MATCH (comment:Comment {id: $commentId}) MATCH (comment:Comment {id: $commentId})
SET comment.deleted = TRUE SET comment.deleted = TRUE
SET comment.content = 'UNAVAILABLE' SET comment.content = 'UNAVAILABLE'
SET comment.contentExcerpt = 'UNAVAILABLE'
RETURN comment RETURN comment
`, `,
{ commentId: args.id }, { commentId: args.id },

View File

@ -2000,7 +2000,6 @@ describe('DeletePost', () => {
id: 'p4711', id: 'p4711',
deleted: true, deleted: true,
content: 'UNAVAILABLE', content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
image: null, image: null,
comments: [], comments: [],
}, },
@ -2015,7 +2014,6 @@ describe('DeletePost', () => {
'comment', 'comment',
{ {
content: 'to be deleted comment content', content: 'to be deleted comment content',
contentExcerpt: 'to be deleted comment content',
}, },
{ {
postId: 'p4711', postId: 'p4711',
@ -2030,14 +2028,12 @@ describe('DeletePost', () => {
id: 'p4711', id: 'p4711',
deleted: true, deleted: true,
content: 'UNAVAILABLE', content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
image: null, image: null,
comments: [ comments: [
{ {
deleted: true, deleted: true,
// Should we black out the comment content in the database, too? // Should we black out the comment content in the database, too?
content: 'UNAVAILABLE', content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
}, },
], ],
}, },

View File

@ -296,7 +296,6 @@ export default {
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment) OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
SET post.deleted = TRUE SET post.deleted = TRUE
SET post.content = 'UNAVAILABLE' SET post.content = 'UNAVAILABLE'
SET post.contentExcerpt = 'UNAVAILABLE'
SET post.title = 'UNAVAILABLE' SET post.title = 'UNAVAILABLE'
SET comment.deleted = TRUE SET comment.deleted = TRUE
RETURN post {.*} RETURN post {.*}

View File

@ -354,13 +354,11 @@ describe('Delete a User as admin', () => {
{ {
id: 'p139', id: 'p139',
content: 'Post by user u343', content: 'Post by user u343',
contentExcerpt: 'Post by user u343',
deleted: false, deleted: false,
comments: [ comments: [
{ {
id: 'c156', id: 'c156',
content: "A comment by someone else on user u343's post", content: "A comment by someone else on user u343's post",
contentExcerpt: "A comment by someone else on user u343's post",
deleted: false, deleted: false,
}, },
], ],
@ -370,7 +368,6 @@ describe('Delete a User as admin', () => {
{ {
id: 'c155', id: 'c155',
content: 'Comment by user u343', content: 'Comment by user u343',
contentExcerpt: 'Comment by user u343',
deleted: false, deleted: false,
}, },
], ],
@ -400,13 +397,11 @@ describe('Delete a User as admin', () => {
{ {
id: 'p139', id: 'p139',
content: 'UNAVAILABLE', content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true, deleted: true,
comments: [ comments: [
{ {
id: 'c156', id: 'c156',
content: 'UNAVAILABLE', content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true, deleted: true,
}, },
], ],
@ -416,7 +411,6 @@ describe('Delete a User as admin', () => {
{ {
id: 'c155', id: 'c155',
content: 'UNAVAILABLE', content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true, deleted: true,
}, },
], ],

View File

@ -223,7 +223,6 @@ export default {
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment) OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
SET resource.deleted = true SET resource.deleted = true
SET resource.content = 'UNAVAILABLE' SET resource.content = 'UNAVAILABLE'
SET resource.contentExcerpt = 'UNAVAILABLE'
SET resource.language = 'UNAVAILABLE' SET resource.language = 'UNAVAILABLE'
SET resource.createdAt = 'UNAVAILABLE' SET resource.createdAt = 'UNAVAILABLE'
SET resource.updatedAt = 'UNAVAILABLE' SET resource.updatedAt = 'UNAVAILABLE'

View File

@ -41,7 +41,6 @@ type Comment {
activityId: String activityId: String
author: User @relation(name: "WROTE", direction: "IN") author: User @relation(name: "WROTE", direction: "IN")
content: String! content: String!
contentExcerpt: String
post: Post @relation(name: "COMMENTS", direction: "OUT") post: Post @relation(name: "COMMENTS", direction: "OUT")
createdAt: String createdAt: String
updatedAt: String updatedAt: String
@ -81,7 +80,7 @@ type Query {
} }
type Mutation { type Mutation {
CreateComment(id: ID, postId: ID!, content: String!, contentExcerpt: String): Comment CreateComment(id: ID, postId: ID!, content: String!): Comment
UpdateComment(id: ID!, content: String!, contentExcerpt: String): Comment UpdateComment(id: ID!, content: String!): Comment
DeleteComment(id: ID!): Comment DeleteComment(id: ID!): Comment
} }

View File

@ -129,7 +129,6 @@ type Post {
title: String! title: String!
slug: String! slug: String!
content: String! content: String!
contentExcerpt: String
image: Image @relation(name: "HERO_IMAGE", direction: "OUT") image: Image @relation(name: "HERO_IMAGE", direction: "OUT")
visibility: Visibility visibility: Visibility
deleted: Boolean deleted: Boolean
@ -230,7 +229,6 @@ type Mutation {
visibility: Visibility visibility: Visibility
language: String language: String
categoryIds: [ID] categoryIds: [ID]
contentExcerpt: String
groupId: ID groupId: ID
postType: PostType = Article postType: PostType = Article
eventInput: _EventInput eventInput: _EventInput
@ -240,7 +238,6 @@ type Mutation {
title: String! title: String!
slug: String slug: String
content: String! content: String!
contentExcerpt: String
image: ImageInput image: ImageInput
visibility: Visibility visibility: Visibility
language: String language: String

View File

@ -20,33 +20,9 @@ const updateGroup: IMiddlewareResolver = async (resolve, root, args, context, in
return resolve(root, args, context, info) return resolve(root, args, context, info)
} }
const createPost: IMiddlewareResolver = async (resolve, root, args, context, info) => {
args.contentExcerpt = trunc(args.content, 120).html
return resolve(root, args, context, info)
}
const updatePost: IMiddlewareResolver = async (resolve, root, args, context, info) => {
args.contentExcerpt = trunc(args.content, 120).html
return resolve(root, args, context, info)
}
const createComment: IMiddlewareResolver = async (resolve, root, args, context, info) => {
args.contentExcerpt = trunc(args.content, 180).html
return resolve(root, args, context, info)
}
const updateComment: IMiddlewareResolver = async (resolve, root, args, context, info) => {
args.contentExcerpt = trunc(args.content, 180).html
return resolve(root, args, context, info)
}
export default { export default {
Mutation: { Mutation: {
CreateGroup: createGroup, CreateGroup: createGroup,
UpdateGroup: updateGroup, UpdateGroup: updateGroup,
CreatePost: createPost,
UpdatePost: updatePost,
CreateComment: createComment,
UpdateComment: updateComment,
}, },
} }

View File

@ -116,7 +116,6 @@ beforeAll(async () => {
id: 'p2', id: 'p2',
title: 'Disabled post', title: 'Disabled post',
content: 'This is an offensive post content', content: 'This is an offensive post content',
contentExcerpt: 'This is an offensive post content',
deleted: false, deleted: false,
}, },
{ {
@ -132,7 +131,6 @@ beforeAll(async () => {
{ {
id: 'c1', id: 'c1',
content: 'Disabled comment', content: 'Disabled comment',
contentExcerpt: 'Disabled comment',
}, },
{ {
author: troll, author: troll,
@ -252,9 +250,6 @@ describe('softDeleteMiddleware', () => {
it('displays content', () => { it('displays content', () => {
expect(subject.content).toEqual('This is an offensive post content') expect(subject.content).toEqual('This is an offensive post content')
}) })
it('displays contentExcerpt', () => {
expect(subject.contentExcerpt).toEqual('This is an offensive post content')
})
it('displays image', () => { it('displays image', () => {
expect(subject.image).toEqual({ expect(subject.image).toEqual({
url: expect.stringMatching('http://localhost/some/offensive/image.jpg'), url: expect.stringMatching('http://localhost/some/offensive/image.jpg'),
@ -268,9 +263,6 @@ describe('softDeleteMiddleware', () => {
it('displays content', () => { it('displays content', () => {
expect(subject.content).toEqual('Disabled comment') expect(subject.content).toEqual('Disabled comment')
}) })
it('displays contentExcerpt', () => {
expect(subject.contentExcerpt).toEqual('Disabled comment')
})
}) })
}) })
@ -308,9 +300,6 @@ describe('softDeleteMiddleware', () => {
it('obfuscates content', () => { it('obfuscates content', () => {
expect(subject.content).toEqual('UNAVAILABLE') expect(subject.content).toEqual('UNAVAILABLE')
}) })
it('obfuscates contentExcerpt', () => {
expect(subject.contentExcerpt).toEqual('UNAVAILABLE')
})
it('obfuscates image', () => { it('obfuscates image', () => {
expect(subject.image).toEqual(null) expect(subject.image).toEqual(null)
}) })
@ -322,9 +311,6 @@ describe('softDeleteMiddleware', () => {
it('obfuscates content', () => { it('obfuscates content', () => {
expect(subject.content).toEqual('UNAVAILABLE') expect(subject.content).toEqual('UNAVAILABLE')
}) })
it('obfuscates contentExcerpt', () => {
expect(subject.contentExcerpt).toEqual('UNAVAILABLE')
})
}) })
}) })
}) })

View File

@ -20,7 +20,6 @@ const setDefaultFilters: IMiddlewareResolver = async (resolve, root, args, conte
const obfuscate: IMiddlewareResolver = async (resolve, root, args, context, info) => { const obfuscate: IMiddlewareResolver = async (resolve, root, args, context, info) => {
if (root.deleted || (!isModerator(context) && root.disabled)) { if (root.deleted || (!isModerator(context) && root.disabled)) {
root.content = 'UNAVAILABLE' root.content = 'UNAVAILABLE'
root.contentExcerpt = 'UNAVAILABLE'
root.title = 'UNAVAILABLE' root.title = 'UNAVAILABLE'
root.slug = 'UNAVAILABLE' root.slug = 'UNAVAILABLE'
root.avatar = null root.avatar = null

View File

@ -41,7 +41,7 @@ const walkRecursive = (data, fields, fieldName, callback, _key?) => {
// exclamation mark separates field names, that should not be sanitized // exclamation mark separates field names, that should not be sanitized
const fields = [ const fields = [
{ field: 'content', excludes: ['CreateMessage', 'Message'] }, { field: 'content', excludes: ['CreateMessage', 'Message'] },
{ field: 'contentExcerpt' },
{ field: 'reasonDescription' }, { field: 'reasonDescription' },
{ field: 'description', excludes: ['embed'] }, { field: 'description', excludes: ['embed'] },
{ field: 'descriptionExcerpt' }, { field: 'descriptionExcerpt' },

View File

@ -34,7 +34,7 @@ defineStep('I see all the reported posts including the one from above', () => {
} }
... on Comment { ... on Comment {
id id
contentExcerpt content
disabled disabled
deleted deleted
author { author {

View File

@ -159,7 +159,7 @@ export default {
titleIdent: 'delete.comment.title', titleIdent: 'delete.comment.title',
messageIdent: 'delete.comment.message', messageIdent: 'delete.comment.message',
messageParams: { messageParams: {
name: this.$filters.truncate(this.comment.contentExcerpt, 30), name: this.$filters.truncate(this.$filters.removeHtml(this.comment.content), 30),
}, },
buttons: { buttons: {
confirm: { confirm: {

View File

@ -100,7 +100,7 @@ describe('NotificationsTable.vue', () => {
it("renders the Post's content", () => { it("renders the Post's content", () => {
const boldTags = firstRowNotification.findAll('p') const boldTags = firstRowNotification.findAll('p')
const content = boldTags.filter( const content = boldTags.filter(
(element) => element.text() === postNotification.from.contentExcerpt, (element) => element.text() === postNotification.from.content,
) )
expect(content.exists()).toBe(true) expect(content.exists()).toBe(true)
}) })
@ -133,12 +133,34 @@ describe('NotificationsTable.vue', () => {
it("renders the Post's content", () => { it("renders the Post's content", () => {
const boldTags = secondRowNotification.findAll('p') const boldTags = secondRowNotification.findAll('p')
const content = boldTags.filter( const content = boldTags.filter(
(element) => element.text() === commentNotification.from.contentExcerpt, (element) => element.text() === commentNotification.from.content,
) )
expect(content.exists()).toBe(true) expect(content.exists()).toBe(true)
}) })
}) })
describe('fallback to descriptionExcerpt when content is empty', () => {
it('renders descriptionExcerpt if content is missing', () => {
const fallbackNotification = {
read: false,
reason: 'mentioned_in_post',
from: {
__typename: 'Post',
id: 'post-fallback',
title: 'fallback post',
slug: 'fallback-post',
content: '',
descriptionExcerpt: 'fallback description text',
author: { id: 'u1', slug: 'user', name: 'User' },
},
}
propsData.notifications = [fallbackNotification]
wrapper = Wrapper()
const description = wrapper.find('.notification-description')
expect(description.text()).toBe('fallback description text')
})
})
describe('unread status', () => { describe('unread status', () => {
it('does not have class `notification-status`', () => { it('does not have class `notification-status`', () => {
expect(wrapper.find('.notification-status').exists()).toBe(false) expect(wrapper.find('.notification-status').exists()).toBe(false)

View File

@ -32,8 +32,6 @@ export const notifications = [
deleted: false, deleted: false,
content: content:
'<p><a class="mention" href="/profile/u1" data-mention-id="u1" target="_blank">@peter-lustig</a> </p><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra magna ac placerat. Tempor id eu nisl nunc mi ipsum faucibus vitae. Nibh praesent tristique magna sit amet purus gravida quis blandit. Magna eget est lorem ipsum dolor. In fermentum posuere urna nec. Eleifend donec pretium vulputate sapien nec sagittis aliquam. Augue interdum velit euismod in pellentesque. Id diam maecenas ultricies mi eget mauris pharetra. Donec pretium vulputate sapien nec. Dolor morbi non arcu risus quis varius quam quisque. Blandit turpis cursus in hac habitasse. Est ultricies integer quis auctor elit sed vulputate mi sit. Nunc consequat interdum varius sit amet mattis vulputate enim. Semper feugiat nibh sed pulvinar. Eget felis eget nunc lobortis mattis aliquam. Ultrices vitae auctor eu augue. Tellus molestie nunc non blandit massa enim nec dui. Pharetra massa massa ultricies mi quis hendrerit dolor. Nisl suscipit adipiscing bibendum est ultricies integer.</p>', '<p><a class="mention" href="/profile/u1" data-mention-id="u1" target="_blank">@peter-lustig</a> </p><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra magna ac placerat. Tempor id eu nisl nunc mi ipsum faucibus vitae. Nibh praesent tristique magna sit amet purus gravida quis blandit. Magna eget est lorem ipsum dolor. In fermentum posuere urna nec. Eleifend donec pretium vulputate sapien nec sagittis aliquam. Augue interdum velit euismod in pellentesque. Id diam maecenas ultricies mi eget mauris pharetra. Donec pretium vulputate sapien nec. Dolor morbi non arcu risus quis varius quam quisque. Blandit turpis cursus in hac habitasse. Est ultricies integer quis auctor elit sed vulputate mi sit. Nunc consequat interdum varius sit amet mattis vulputate enim. Semper feugiat nibh sed pulvinar. Eget felis eget nunc lobortis mattis aliquam. Ultrices vitae auctor eu augue. Tellus molestie nunc non blandit massa enim nec dui. Pharetra massa massa ultricies mi quis hendrerit dolor. Nisl suscipit adipiscing bibendum est ultricies integer.</p>',
contentExcerpt:
'<p><a href="/profile/u1" target="_blank">@peter-lustig</a> </p><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra …</p>',
...post, ...post,
author: user, author: user,
}, },
@ -53,8 +51,6 @@ export const notifications = [
deleted: false, deleted: false,
content: content:
'<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra magna ac placerat. Tempor id eu nisl nunc mi ipsum faucibus vitae. Nibh praesent tristique magna sit amet purus gravida quis blandit. Magna eget est lorem ipsum dolor. In fermentum posuere urna nec. Eleifend donec pretium vulputate sapien nec sagittis aliquam. Augue interdum velit euismod in pellentesque. Id diam maecenas ultricies mi eget mauris pharetra. Donec pretium vulputate sapien nec. Dolor morbi non arcu risus quis varius quam quisque. Blandit turpis cursus in hac habitasse. Est ultricies integer quis auctor elit sed vulputate mi sit. Nunc consequat interdum varius sit amet mattis vulputate enim. Semper feugiat nibh sed pulvinar. Eget felis eget nunc lobortis mattis aliquam. Ultrices vitae auctor eu augue. Tellus molestie nunc non blandit massa enim nec dui. Pharetra massa massa ultricies mi quis hendrerit dolor. Nisl suscipit adipiscing bibendum est ultricies integer.</p><p><a class="mention" href="/profile/u1" data-mention-id="u1" target="_blank">@peter-lustig</a> </p><p></p>', '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra magna ac placerat. Tempor id eu nisl nunc mi ipsum faucibus vitae. Nibh praesent tristique magna sit amet purus gravida quis blandit. Magna eget est lorem ipsum dolor. In fermentum posuere urna nec. Eleifend donec pretium vulputate sapien nec sagittis aliquam. Augue interdum velit euismod in pellentesque. Id diam maecenas ultricies mi eget mauris pharetra. Donec pretium vulputate sapien nec. Dolor morbi non arcu risus quis varius quam quisque. Blandit turpis cursus in hac habitasse. Est ultricies integer quis auctor elit sed vulputate mi sit. Nunc consequat interdum varius sit amet mattis vulputate enim. Semper feugiat nibh sed pulvinar. Eget felis eget nunc lobortis mattis aliquam. Ultrices vitae auctor eu augue. Tellus molestie nunc non blandit massa enim nec dui. Pharetra massa massa ultricies mi quis hendrerit dolor. Nisl suscipit adipiscing bibendum est ultricies integer.</p><p><a class="mention" href="/profile/u1" data-mention-id="u1" target="_blank">@peter-lustig</a> </p><p></p>',
contentExcerpt:
'<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra magna ac …</p>',
...post, ...post,
author: user, author: user,
}, },

View File

@ -72,8 +72,8 @@
:class="{ 'notification-status': notification.read }" :class="{ 'notification-status': notification.read }"
> >
{{ {{
notification.from.contentExcerpt || $filters.removeHtml(notification.from.content) ||
notification.from.descriptionExcerpt | removeHtml $filters.removeHtml(notification.from.descriptionExcerpt)
}} }}
</p> </p>
</div> </div>

View File

@ -66,9 +66,7 @@
/> />
</div> </div>
</client-only> </client-only>
<!-- TODO: replace editor content with tiptap render view --> <div class="content hyphenate-text">{{ excerpt }}</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="content hyphenate-text" v-html="excerpt" />
<footer <footer
class="footer" class="footer"
v-observe-visibility="(isVisible, entry) => visibilityChanged(isVisible, entry, post.id)" v-observe-visibility="(isVisible, entry) => visibilityChanged(isVisible, entry, post.id)"
@ -232,7 +230,7 @@ export default {
user: 'auth/user', user: 'auth/user',
}), }),
excerpt() { excerpt() {
return this.$filters.removeLinks(this.post.contentExcerpt) return this.$filters.removeHtml(this.post.content)
}, },
isAuthor() { isAuthor() {
const { author } = this.post const { author } = this.post
@ -399,6 +397,10 @@ export default {
.content { .content {
flex-grow: 1; flex-grow: 1;
margin-bottom: $space-small; margin-bottom: $space-small;
display: -webkit-box;
-webkit-line-clamp: 6;
-webkit-box-orient: vertical;
overflow: hidden;
} }
.footer { .footer {

View File

@ -90,7 +90,7 @@ export default {
name: name:
report.resource.name || report.resource.name ||
this.$filters.truncate(report.resource.title, 30) || this.$filters.truncate(report.resource.title, 30) ||
this.$filters.truncate(this.$filters.removeHtml(report.resource.contentExcerpt), 30), this.$filters.truncate(this.$filters.removeHtml(report.resource.content), 30),
}, },
buttons: { buttons: {
confirm: { confirm: {

View File

@ -153,9 +153,7 @@ export default {
} }
}, },
linkText() { linkText() {
return ( return this.report.resource.title || this.$filters.removeHtml(this.report.resource.content)
this.report.resource.title || this.$filters.removeHtml(this.report.resource.contentExcerpt)
)
}, },
statusIconName() { statusIconName() {
return this.isDisabled ? this.icons.eyeSlash : this.icons.eye return this.isDisabled ? this.icons.eyeSlash : this.icons.eye

View File

@ -7,7 +7,7 @@ export const notifications = [
id: 'post-1', id: 'post-1',
title: 'some post title', title: 'some post title',
slug: 'some-post-title', slug: 'some-post-title',
contentExcerpt: 'this is a post content', content: 'this is a post content',
author: { author: {
id: 'john-1', id: 'john-1',
slug: 'john-doe', slug: 'john-doe',
@ -21,12 +21,12 @@ export const notifications = [
from: { from: {
__typename: 'Comment', __typename: 'Comment',
id: 'comment-2', id: 'comment-2',
contentExcerpt: 'this is yet another post content', content: 'this is yet another post content',
post: { post: {
id: 'post-1', id: 'post-1',
title: 'some post on a comment', title: 'some post on a comment',
slug: 'some-post-on-a-comment', slug: 'some-post-on-a-comment',
contentExcerpt: 'this is a post content', content: 'this is a post content',
author: { author: {
id: 'john-1', id: 'john-1',
slug: 'john-doe', slug: 'john-doe',

View File

@ -9,7 +9,7 @@ export default () => {
mutation ($postId: ID!, $content: String!) { mutation ($postId: ID!, $content: String!) {
CreateComment(postId: $postId, content: $content) { CreateComment(postId: $postId, content: $content) {
id id
contentExcerpt
content content
createdAt createdAt
updatedAt updatedAt
@ -45,7 +45,7 @@ export default () => {
mutation ($content: String!, $id: ID!) { mutation ($content: String!, $id: ID!) {
UpdateComment(content: $content, id: $id) { UpdateComment(content: $content, id: $id) {
id id
contentExcerpt
content content
createdAt createdAt
updatedAt updatedAt
@ -70,7 +70,7 @@ export default () => {
mutation ($id: ID!) { mutation ($id: ID!) {
DeleteComment(id: $id) { DeleteComment(id: $id) {
id id
contentExcerpt
content content
createdAt createdAt
disabled disabled

View File

@ -8,7 +8,7 @@ export default () => {
query Comment($postId: ID) { query Comment($postId: ID) {
Comment(postId: $postId) { Comment(postId: $postId) {
id id
contentExcerpt content
createdAt createdAt
author { author {
id id

View File

@ -42,7 +42,7 @@ export const reportsListQuery = () => {
} }
... on Comment { ... on Comment {
id id
contentExcerpt content
disabled disabled
deleted deleted
author { author {

View File

@ -32,7 +32,7 @@ export default () => {
slug slug
title title
content content
contentExcerpt
language language
image { image {
...imageUrls ...imageUrls
@ -83,7 +83,7 @@ export default () => {
title title
slug slug
content content
contentExcerpt
language language
image { image {
...imageUrls ...imageUrls
@ -146,7 +146,7 @@ export default () => {
title title
slug slug
content content
contentExcerpt
language language
pinnedBy { pinnedBy {
id id
@ -163,7 +163,7 @@ export default () => {
title title
slug slug
content content
contentExcerpt
language language
pinnedBy { pinnedBy {
id id
@ -180,7 +180,7 @@ export default () => {
title title
slug slug
content content
contentExcerpt
language language
pinnedBy { pinnedBy {
id id
@ -197,7 +197,7 @@ export default () => {
title title
slug slug
content content
contentExcerpt
language language
pinnedBy { pinnedBy {
id id
@ -214,7 +214,7 @@ export default () => {
title title
slug slug
content content
contentExcerpt
language language
pinnedBy { pinnedBy {
id id
@ -231,7 +231,7 @@ export default () => {
title title
slug slug
content content
contentExcerpt
language language
pinnedBy { pinnedBy {
id id

View File

@ -8,7 +8,6 @@ export const comment = gql`
disabled disabled
deleted deleted
content content
contentExcerpt
isPostObservedByMe isPostObservedByMe
postObservingUsersCount postObservingUsersCount
shoutedByCurrentUser shoutedByCurrentUser

View File

@ -8,7 +8,6 @@ export const post = gql`
id id
title title
content content
contentExcerpt
createdAt createdAt
updatedAt updatedAt
sortDate sortDate

View File

@ -434,7 +434,7 @@ export default {
id: post.id, id: post.id,
slug: post.slug, slug: post.slug,
name: post.title, name: post.title,
description: post.contentExcerpt, description: this.$filters.removeHtml(post.content),
}, },
geometry: { geometry: {
type: 'Point', type: 'Point',

View File

@ -87,11 +87,26 @@ export default ({ app = {} }) => {
if (!content) return '' if (!content) return ''
let contentExcerpt = content let contentExcerpt = content
if (replaceLinebreaks) { if (replaceLinebreaks) {
// replace linebreaks with spaces first // replace linebreaks and block-level closing tags with spaces
contentExcerpt = contentExcerpt.replace(/<br>/gim, ' ').trim() contentExcerpt = contentExcerpt
.replace(/<\/(p|h[1-6]|li|div|blockquote)>/gim, ' ')
.replace(/<br\s*\/?>/gim, ' ')
.trim()
} }
// remove the rest of the HTML // remove the rest of the HTML
contentExcerpt = contentExcerpt.replace(/<(?:.|\n)*?>/gm, '').trim() contentExcerpt = contentExcerpt.replace(/<(?:.|\n)*?>/gm, '').trim()
// normalize multiple spaces into one
contentExcerpt = contentExcerpt.replace(/ {2,}/g, ' ')
// decode common HTML entities
const entities = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
contentExcerpt = contentExcerpt.replace(/&(?:amp|lt|gt|quot|#39|nbsp);/g, (m) => entities[m])
return contentExcerpt return contentExcerpt
}, },