From 1932acb628e3ee6095d23913c08179b97a1b0329 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Wed, 7 Aug 2024 17:03:06 +0530 Subject: [PATCH] MM-58502 Error on crossing block limit on client side (#5015) * refactor: error on crossing block limit * fix: linter * chore: updated snapshot * refactor: updated mysql/mysql-server docker version * chore: changed blob to text.length --- docker-testing/docker-compose-mysql.yml | 2 +- webapp/i18n/en.json | 1 + webapp/package-lock.json | 13 ++++- webapp/package.json | 1 + .../__snapshots__/contentBlock.test.tsx.snap | 12 +++-- .../__snapshots__/textElement.test.tsx.snap | 12 +++-- .../src/components/content/textElement.scss | 13 +++++ webapp/src/components/content/textElement.tsx | 54 ++++++++++++++----- 8 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 webapp/src/components/content/textElement.scss diff --git a/docker-testing/docker-compose-mysql.yml b/docker-testing/docker-compose-mysql.yml index 60ea44aca..111aacea2 100644 --- a/docker-testing/docker-compose-mysql.yml +++ b/docker-testing/docker-compose-mysql.yml @@ -1,7 +1,7 @@ version: '2.4' services: mysql: - image: "mysql/mysql-server:5.7.12" + image: "mysql/mysql-server:8.0.32" restart: always environment: MYSQL_ROOT_HOST: "%" diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 6dd6746d6..413d0e840 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -133,6 +133,7 @@ "ContentBlock.editCardCheckboxText": "edit card text", "ContentBlock.editCardText": "edit card text", "ContentBlock.editText": "Edit text...", + "ContentBlock.errorText": "You've exceeded the size limit for this content. Please shorten it to avoid losing data.", "ContentBlock.image": "image", "ContentBlock.insertAbove": "Insert above", "ContentBlock.moveBlock": "move card content", diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 683a36f7a..01ab0fbc2 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -1,6 +1,6 @@ { "name": "focalboard", - "version": "7.12.0", + "version": "8.0.0", "lockfileVersion": 2, "requires": true, "packages": { @@ -18,6 +18,7 @@ "@mattermost/compass-icons": "^0.1.39", "@reduxjs/toolkit": "^1.8.0", "@tippyjs/react": "4.2.6", + "classnames": "^2.5.1", "color": "^4.2.1", "draft-js": "^0.11.7", "emoji-mart": "^3.0.1", @@ -5106,6 +5107,11 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/clean-css": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", @@ -20955,6 +20961,11 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "clean-css": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", diff --git a/webapp/package.json b/webapp/package.json index a5b4a0cf9..6c16d4b8f 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -35,6 +35,7 @@ "@mattermost/compass-icons": "^0.1.39", "@reduxjs/toolkit": "^1.8.0", "@tippyjs/react": "4.2.6", + "classnames": "^2.5.1", "color": "^4.2.1", "draft-js": "^0.11.7", "emoji-mart": "^3.0.1", diff --git a/webapp/src/components/__snapshots__/contentBlock.test.tsx.snap b/webapp/src/components/__snapshots__/contentBlock.test.tsx.snap index 60813283b..c18ea1b2a 100644 --- a/webapp/src/components/__snapshots__/contentBlock.test.tsx.snap +++ b/webapp/src/components/__snapshots__/contentBlock.test.tsx.snap @@ -476,12 +476,16 @@ exports[`components/contentBlock should match snapshot with textBlock 1`] = ` style="flex: 0 0 auto; height: 100%;" />
+ class="MarkdownEditor octo-editor " + > +
+
+ class="MarkdownEditor octo-editor " + > +
+
`; diff --git a/webapp/src/components/content/textElement.scss b/webapp/src/components/content/textElement.scss new file mode 100644 index 000000000..2a648dfb8 --- /dev/null +++ b/webapp/src/components/content/textElement.scss @@ -0,0 +1,13 @@ +.TextElement { + display: inline; + + .markdown-editor-error { + background-color: rgba(210, 75, 78, 0.08); + border: 1px solid var(--error-text); + } + + .error-message { + color: var(--dnd-indicator, rgba(210, 75, 78, 1)); + margin: 16px 0; + } +} diff --git a/webapp/src/components/content/textElement.tsx b/webapp/src/components/content/textElement.tsx index 0823b0aa8..628d55d61 100644 --- a/webapp/src/components/content/textElement.tsx +++ b/webapp/src/components/content/textElement.tsx @@ -1,8 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react' +import React, {useState} from 'react' import {useIntl} from 'react-intl' +import cx from 'classnames' + import {ContentBlock} from '../../blocks/contentBlock' import {createTextBlock} from '../../blocks/textBlock' import mutator from '../../mutator' @@ -11,26 +13,52 @@ import {MarkdownEditor} from '../markdownEditor' import {contentRegistry} from './contentRegistry' +import './textElement.scss' + type Props = { block: ContentBlock readonly: boolean } -const TextElement = (props: Props): JSX.Element => { - const {block, readonly} = props +const BlockTitleMaxBytes = 65535 // Maximum size of a TEXT column in MySQL +const BlockTitleMaxRunes = BlockTitleMaxBytes / 4 // Assume a worst-case representation + +const TextElement = ({block, readonly}: Props): JSX.Element => { const intl = useIntl() + const [isError, setIsError] = useState(false) + const [blockTitle, setBlockTitle] = useState(block.title) + + const textChangedHandler = (text: string): void => { + setBlockTitle(text) + const textSize = text.length + setIsError(textSize > BlockTitleMaxRunes) + } + + const handleBlur = (text: string): void => { + if (text !== block.title || blockTitle !== block.title) { + const textSize = new Blob([text]).size + if (textSize <= BlockTitleMaxRunes) { + mutator.changeBlockTitle(block.boardId, block.id, block.title, text, intl.formatMessage({id: 'ContentBlock.editCardText', defaultMessage: 'edit card text'})). + finally(() => { + setIsError(false) + }) + } + } + } + return ( - { - if (text !== block.title) { - mutator.changeBlockTitle(block.boardId, block.id, block.title, text, intl.formatMessage({id: 'ContentBlock.editCardText', defaultMessage: 'edit card text'})) - } - }} - readonly={readonly} - /> +
+ + {isError &&
{intl.formatMessage({id: 'ContentBlock.errorText', defaultMessage: 'You\'ve exceeded the size limit for this content. Please shorten it to avoid losing data.'})}
} +
) }