Просмотр исходного кода

Merge pull request #9614 from overleaf/ab-limit-tag-length

[web] Restrict the length of tags to 50 characters

GitOrigin-RevId: fc20227e3e2171bf9e27c983105ecc7b198cf882
Davinder Singh 4 лет назад
Родитель
Сommit
70e63ca0e3

+ 8 - 0
services/web/app/src/Features/Tags/TagsHandler.js

@@ -1,6 +1,8 @@
 const { Tag } = require('../../models/Tag')
 const { promisifyAll } = require('../../util/promises')
 
+const MAX_TAG_LENGTH = 50
+
 function getAllTags(userId, callback) {
   Tag.find({ user_id: userId }, callback)
 }
@@ -9,6 +11,9 @@ function createTag(userId, name, callback) {
   if (!callback) {
     callback = function () {}
   }
+  if (name.length > MAX_TAG_LENGTH) {
+    return callback(new Error('Exceeded max tag length'))
+  }
   Tag.create({ user_id: userId, name }, function (err, tag) {
     // on duplicate key error return existing tag
     if (err && err.code === 11000) {
@@ -22,6 +27,9 @@ function renameTag(userId, tagId, name, callback) {
   if (!callback) {
     callback = function () {}
   }
+  if (name.length > MAX_TAG_LENGTH) {
+    return callback(new Error('Exceeded max tag length'))
+  }
   Tag.updateOne(
     {
       _id: tagId,

+ 3 - 1
services/web/frontend/extracted-translations.json

@@ -23,6 +23,7 @@
   "approaching_compile_timeout_limit_upgrade_for_more_compile_time": "",
   "archive": "",
   "archive_projects": "",
+  "archived": "",
   "archived_projects": "",
   "archiving_projects_wont_affect_collaborators": "",
   "are_you_still_at": "",
@@ -564,6 +565,7 @@
   "sync_to_github": "",
   "tab_connecting": "",
   "tab_no_longer_connected": "",
+  "tag_name_cannot_exceed_characters": "",
   "tags": "",
   "tags_slash_folders": "",
   "take_short_survey": "",
@@ -593,8 +595,8 @@
   "too_recently_compiled": "",
   "total_words": "",
   "trash": "",
-  "trashed": "",
   "trash_projects": "",
+  "trashed": "",
   "trashed_projects": "",
   "trashing_projects_wont_affect_collaborators": "",
   "trial_last_day": "",

+ 22 - 3
services/web/frontend/js/features/project-list/components/sidebar/create-tag-modal.tsx

@@ -1,10 +1,11 @@
-import { useCallback, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
 import { Button, Form, Modal } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { Tag } from '../../../../../../app/src/Features/Tags/types'
 import AccessibleModal from '../../../../shared/components/accessible-modal'
 import useAsync from '../../../../shared/hooks/use-async'
 import { createTag } from '../../util/api'
+import { MAX_TAG_LENGTH } from '../../util/tag'
 
 type CreateTagModalProps = {
   show: boolean
@@ -21,6 +22,7 @@ export default function CreateTagModal({
   const { isError, runAsync, status } = useAsync<Tag>()
 
   const [tagName, setTagName] = useState<string>()
+  const [validationError, setValidationError] = useState<string>()
 
   const runCreateTag = useCallback(() => {
     if (tagName) {
@@ -38,6 +40,16 @@ export default function CreateTagModal({
     [runCreateTag]
   )
 
+  useEffect(() => {
+    if (tagName && tagName.length > MAX_TAG_LENGTH) {
+      setValidationError(
+        t('tag_name_cannot_exceed_characters', { maxLength: MAX_TAG_LENGTH })
+      )
+    } else if (validationError) {
+      setValidationError(undefined)
+    }
+  }, [tagName, t, validationError])
+
   if (!show) {
     return null
   }
@@ -68,6 +80,11 @@ export default function CreateTagModal({
       </Modal.Body>
 
       <Modal.Footer>
+        {validationError && (
+          <div className="modal-footer-left">
+            <span className="text-danger error">{validationError}</span>
+          </div>
+        )}
         {isError && (
           <div className="modal-footer-left">
             <span className="text-danger error">
@@ -81,9 +98,11 @@ export default function CreateTagModal({
         <Button
           onClick={() => runCreateTag()}
           bsStyle="primary"
-          disabled={status === 'pending' || !tagName?.length}
+          disabled={
+            status === 'pending' || !tagName?.length || !!validationError
+          }
         >
-          {status === 'pending' ? t('creating') + '...' : t('create')}
+          {status === 'pending' ? t('creating') + '…' : t('create')}
         </Button>
       </Modal.Footer>
     </AccessibleModal>

+ 1 - 1
services/web/frontend/js/features/project-list/components/sidebar/delete-tag-modal.tsx

@@ -70,7 +70,7 @@ export default function DeleteTagModal({
           bsStyle="primary"
           disabled={status === 'pending'}
         >
-          {status === 'pending' ? t('deleting') + '...' : t('delete')}
+          {status === 'pending' ? t('deleting') + '…' : t('delete')}
         </Button>
       </Modal.Footer>
     </AccessibleModal>

+ 22 - 3
services/web/frontend/js/features/project-list/components/sidebar/rename-tag-modal.tsx

@@ -1,10 +1,11 @@
-import { useCallback, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
 import { Button, Form, Modal } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { Tag } from '../../../../../../app/src/Features/Tags/types'
 import AccessibleModal from '../../../../shared/components/accessible-modal'
 import useAsync from '../../../../shared/hooks/use-async'
 import { renameTag } from '../../util/api'
+import { MAX_TAG_LENGTH } from '../../util/tag'
 
 type RenameTagModalProps = {
   tag?: Tag
@@ -21,6 +22,7 @@ export default function RenameTagModal({
   const { isError, runAsync, status } = useAsync()
 
   const [newTagName, setNewTageName] = useState<string>()
+  const [validationError, setValidationError] = useState<string>()
 
   const runRenameTag = useCallback(
     (tagId: string) => {
@@ -43,6 +45,16 @@ export default function RenameTagModal({
     [tag, runRenameTag]
   )
 
+  useEffect(() => {
+    if (newTagName && newTagName.length > MAX_TAG_LENGTH) {
+      setValidationError(
+        t('tag_name_cannot_exceed_characters', { maxLength: MAX_TAG_LENGTH })
+      )
+    } else if (validationError) {
+      setValidationError(undefined)
+    }
+  }, [newTagName, t, validationError])
+
   if (!tag) {
     return null
   }
@@ -74,6 +86,11 @@ export default function RenameTagModal({
       </Modal.Body>
 
       <Modal.Footer>
+        {validationError && (
+          <div className="modal-footer-left">
+            <span className="text-danger error">{validationError}</span>
+          </div>
+        )}
         {isError && (
           <div className="modal-footer-left">
             <span className="text-danger error">
@@ -87,9 +104,11 @@ export default function RenameTagModal({
         <Button
           onClick={() => runRenameTag(tag._id)}
           bsStyle="primary"
-          disabled={status === 'pending' || !newTagName?.length}
+          disabled={
+            status === 'pending' || !newTagName?.length || !!validationError
+          }
         >
-          {status === 'pending' ? t('renaming') + '...' : t('rename')}
+          {status === 'pending' ? t('renaming') + '…' : t('rename')}
         </Button>
       </Modal.Footer>
     </AccessibleModal>

+ 1 - 0
services/web/frontend/js/features/project-list/util/tag.ts

@@ -0,0 +1 @@
+export const MAX_TAG_LENGTH = 50

+ 2 - 1
services/web/locales/en.json

@@ -1862,5 +1862,6 @@
   "descending": "Descending",
   "reverse_x_sort_order" : "Reverse __x__ sort order",
   "create_first_project": "Create First Project",
-  "you_dont_have_any_repositories": "You don’t have any repositories"
+  "you_dont_have_any_repositories": "You don’t have any repositories",
+  "tag_name_cannot_exceed_characters": "Tag name cannot exceed __maxLength__ characters"
 }

+ 27 - 0
services/web/test/unit/src/Tags/TagsHandlerTests.js

@@ -65,6 +65,19 @@ describe('TagsHandler', function () {
       })
     })
 
+    describe('when tag is too long', function () {
+      it('should throw an error', function (done) {
+        this.TagsHandler.createTag(
+          this.tag.user_id,
+          'this is a tag that is very very very very very very long',
+          err => {
+            expect(err.message).to.equal('Exceeded max tag length')
+            done()
+          }
+        )
+      })
+    })
+
     describe('when insert has duplicate key error error', function () {
       beforeEach(function () {
         this.duplicateKeyError = new Error('Duplicate')
@@ -255,5 +268,19 @@ describe('TagsHandler', function () {
         )
       })
     })
+
+    describe('when tag is too long', function () {
+      it('should throw an error', function (done) {
+        this.TagsHandler.renameTag(
+          this.userId,
+          this.tagId,
+          'this is a tag that is very very very very very very long',
+          err => {
+            expect(err.message).to.equal('Exceeded max tag length')
+            done()
+          }
+        )
+      })
+    })
   })
 })