@devtron-labs/devtron-fe-common-lib 0.0.10-beta-21 → 0.0.10-beta-22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devtron-labs/devtron-fe-common-lib",
3
- "version": "0.0.10-beta-21",
3
+ "version": "0.0.10-beta-22",
4
4
  "description": "Supporting common component library",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -0,0 +1,5 @@
1
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M3.74216 4.67419H1.49216V2.42419" fill="#767D84"/>
3
+ <path d="M3.74216 4.67419H1.49216V2.42419" stroke="#596168" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M3.08318 8.91682C3.66007 9.49371 4.39507 9.88658 5.19525 10.0457C5.99542 10.2049 6.82482 10.1232 7.57856 9.811C8.33231 9.49879 8.97654 8.97008 9.42981 8.29173C9.88307 7.61338 10.125 6.81585 10.125 6C10.125 5.18415 9.88307 4.38663 9.42981 3.70827C8.97654 3.02992 8.33231 2.50121 7.57856 2.189C6.82482 1.87679 5.99542 1.7951 5.19525 1.95426C4.39507 2.11343 3.66007 2.50629 3.08318 3.08319L1.49219 4.67418" stroke="#596168" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
5
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M12.6667 4C12.4899 4 12.3203 4.07024 12.1953 4.19526C12.0703 4.32029 12.0001 4.48986 12.0001 4.66667V7.33333C12.0001 7.51014 11.9298 7.67971 11.8048 7.80474C11.6798 7.92976 11.5102 8 11.3334 8H4.94005L5.80672 7.14C5.93226 7.01446 6.00278 6.8442 6.00278 6.66667C6.00278 6.48913 5.93226 6.31887 5.80672 6.19333C5.68118 6.0678 5.51092 5.99727 5.33339 5.99727C5.15585 5.99727 4.98559 6.0678 4.86005 6.19333L2.86005 8.19333C2.79936 8.25674 2.75178 8.3315 2.72005 8.41333C2.65337 8.57564 2.65337 8.75769 2.72005 8.92C2.75178 9.00183 2.79936 9.0766 2.86005 9.14L4.86005 11.14C4.92203 11.2025 4.99576 11.2521 5.077 11.2859C5.15824 11.3198 5.24538 11.3372 5.33339 11.3372C5.42139 11.3372 5.50853 11.3198 5.58977 11.2859C5.67101 11.2521 5.74474 11.2025 5.80672 11.14C5.86921 11.078 5.9188 11.0043 5.95265 10.9231C5.98649 10.8418 6.00392 10.7547 6.00392 10.6667C6.00392 10.5787 5.98649 10.4915 5.95265 10.4103C5.9188 10.329 5.86921 10.2553 5.80672 10.1933L4.94005 9.33333H11.3334C11.8638 9.33333 12.3725 9.12262 12.7476 8.74755C13.1227 8.37247 13.3334 7.86377 13.3334 7.33333V4.66667C13.3334 4.48986 13.2631 4.32029 13.1381 4.19526C13.0131 4.07024 12.8435 4 12.6667 4Z" fill="#3B444C"/>
3
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
2
+ <g fill="none" fill-rule="evenodd">
3
+ <path fill="#FFF" d="M5 5H15V15H5z"/>
4
+ <path fill="#F33E3E" d="M10 18.333c-4.6 0-8.333-3.733-8.333-8.333S5.4 1.667 10 1.667 18.333 5.4 18.333 10 14.6 18.333 10 18.333zm0-12.5c-.46 0-.833.373-.833.834V10c0 .46.373.833.833.833.46 0 .833-.373.833-.833V6.667c0-.46-.373-.834-.833-.834zm0 6.667c-.46 0-.833.373-.833.833 0 .46.373.834.833.834.46 0 .833-.373.833-.834 0-.46-.373-.833-.833-.833z"/>
5
+ </g>
6
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
2
+ <path fill="#959BA1" class="fill-color" fill-rule="evenodd" d="M10.5 7.583h-7c-.322 0-.583-.26-.583-.583 0-.322.26-.583.583-.583h7c.322 0 .583.26.583.583 0 .322-.26.583-.583.583z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M10.625 4.99991L15 9.37491M7.5 16.8749H3.75C3.58424 16.8749 3.42527 16.8091 3.30806 16.6918C3.19085 16.5746 3.125 16.4157 3.125 16.2499V12.7588C3.125 12.6767 3.14117 12.5954 3.17258 12.5196C3.20398 12.4438 3.25002 12.3749 3.30806 12.3168L12.6831 2.94185C12.8003 2.82464 12.9592 2.75879 13.125 2.75879C13.2908 2.75879 13.4497 2.82464 13.5669 2.94185L17.0581 6.43296C17.1753 6.55017 17.2411 6.70915 17.2411 6.87491C17.2411 7.04067 17.1753 7.19964 17.0581 7.31685L7.5 16.8749Z" stroke="#596168" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -1,7 +1,7 @@
1
- import { get } from './Api';
1
+ import {get, post} from './Api';
2
2
  import { ROUTES } from './Constants'
3
3
  import { sortCallback } from './Helper';
4
- import { TeamList } from './Types';
4
+ import { TeamList ,ResponseType } from './Types';
5
5
 
6
6
  export const getTeamListMin = (): Promise<TeamList> => {
7
7
  // ignore active field
@@ -20,4 +20,19 @@ export const getTeamListMin = (): Promise<TeamList> => {
20
20
  result: list,
21
21
  };
22
22
  });
23
- };
23
+ };
24
+
25
+ interface UserRole extends ResponseType {
26
+ result?: {
27
+ roles: string[]
28
+ superAdmin: boolean
29
+ }
30
+ }
31
+
32
+ export function getUserRole(appName?: string): Promise<UserRole> {
33
+ return get(`${ROUTES.USER_CHECK_ROLE}${appName ? `?appName=${appName}` : ''}`)
34
+ }
35
+
36
+ export function setImageTags(request, pipelineId: number, artifactId: number){
37
+ return post(`${ROUTES.IMAGE_TAGGING}/${pipelineId}/${artifactId}`,request )
38
+ }
@@ -20,6 +20,8 @@ export const URLS = {
20
20
 
21
21
  export const ROUTES = {
22
22
  PROJECT_LIST_MIN: 'team/autocomplete',
23
+ USER_CHECK_ROLE: 'user/check/roles',
24
+ IMAGE_TAGGING:'app/image-tagging',
23
25
  }
24
26
 
25
27
  export enum KEY_VALUE {
@@ -0,0 +1,594 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
+ import { ReactComponent as Add } from '../Assets/Icon/ic-add.svg'
3
+ import { ReactComponent as Close } from '../Assets/Icon/ic-cross.svg'
4
+ import { ReactComponent as QuestionFilled } from '../Assets/Icon/ic-help.svg'
5
+ import { ReactComponent as Question } from '../Assets/Icon/ic-help-outline.svg'
6
+
7
+ import { ReactComponent as EditIcon } from '../Assets/Icon/ic-pencil.svg'
8
+ import { ReactComponent as Redo } from '../Assets/Icon/ic-arrow-counter-clockwise.svg'
9
+ import { ReactComponent as Minus } from '../Assets/Icon/ic-minus.svg'
10
+ import { toast } from 'react-toastify'
11
+ import { ReactComponent as Info } from '../Assets/Icon/ic-info-filled.svg'
12
+ import { ReactComponent as Error } from '../Assets/Icon/ic-warning.svg'
13
+ import { ReactComponent as Warning } from '../Assets/Icon/ic-error-exclamation.svg'
14
+ import { ReactComponent as Enter } from '../Assets/Icon/ic-enter.svg'
15
+ import { ImageButtonType, ImageTaggingContainerType, ReleaseTag, TippyTheme } from './Types'
16
+ import { showError, stopPropagation } from './Helper'
17
+ import { TippyCustomized } from './TippyCustomized'
18
+ import { setImageTags, getUserRole } from './Common.service'
19
+ import Tippy from '@tippyjs/react'
20
+ import { ServerErrors } from './ServerError'
21
+
22
+ export const ImageTagsContainer = ({
23
+ ciPipelineId,
24
+ artifactId,
25
+ imageComment,
26
+ imageReleaseTags,
27
+ updateCurrentAppMaterial,
28
+ appReleaseTagNames,
29
+ setAppReleaseTagNames,
30
+ tagsEditable,
31
+ setTagsEditable,
32
+ toggleCardMode,
33
+ hideHardDelete,
34
+ forceReInit,
35
+ }: ImageTaggingContainerType) => {
36
+ const [initialTags, setInitialTags] = useState<ReleaseTag[]>(imageReleaseTags ? imageReleaseTags : [])
37
+ const [initialDescription, setInitialDescription] = useState(imageComment ? imageComment.comment : '')
38
+ const [existingTags, setExistingTags] = useState(appReleaseTagNames ? appReleaseTagNames : [])
39
+ const [newDescription, setNewDescription] = useState(imageComment ? imageComment.comment : '')
40
+ const [isEditing, setIsEditing] = useState(false)
41
+ const [showTagsWarning, setShowTagsWarning] = useState(false)
42
+ const [displayedTags, setDisplayedTags] = useState<ReleaseTag[]>(imageReleaseTags ? imageReleaseTags : [])
43
+ const [tagErrorMessage, setTagErrorMessage] = useState('')
44
+ const [createTags, setCreateTags] = useState<ReleaseTag[]>([])
45
+ const [softDeleteTags, setSoftDeleteTags] = useState<ReleaseTag[]>([])
46
+ const [hardDeleteTags, setHardDeleteTags] = useState<ReleaseTag[]>([])
47
+ const [isSuperAdmin, setSuperAdmin] = useState<boolean>(false)
48
+ const [descriptionValidationMessage, setDescriptionValidationMessage] = useState<string>('')
49
+ const [textInput, setTextInput] = useState<string>('')
50
+ const [saveData, setSaveData] = useState<boolean>(false)
51
+
52
+ useEffect(() => {
53
+ initialise()
54
+ }, [])
55
+
56
+ useEffect(() => {
57
+ reInitState()
58
+ }, [imageReleaseTags, imageComment, tagsEditable])
59
+
60
+ useEffect(() => {
61
+ setExistingTags(appReleaseTagNames ? appReleaseTagNames : [])
62
+ }, [appReleaseTagNames])
63
+
64
+ useEffect(() => {
65
+ if (saveData) {
66
+ handleSave()
67
+ setSaveData(false)
68
+ }
69
+ }, [saveData])
70
+
71
+ async function initialise() {
72
+ try {
73
+ const userRole = await getUserRole()
74
+
75
+ const superAdmin = userRole?.result?.roles?.includes('role:super-admin___')
76
+ setSuperAdmin(superAdmin)
77
+ } catch (err) {
78
+ showError(err)
79
+ }
80
+ }
81
+
82
+ const reInitState = () => {
83
+ if (forceReInit || !isEditing) {
84
+ setInitialTags(imageReleaseTags ? imageReleaseTags : [])
85
+ setInitialDescription(imageComment ? imageComment.comment : '')
86
+ setNewDescription(imageComment ? imageComment.comment : '')
87
+ setDisplayedTags(imageReleaseTags ? imageReleaseTags : [])
88
+ setIsEditing(false)
89
+ }
90
+ }
91
+
92
+ const handleEditClick = () => {
93
+ setIsEditing(!isEditing)
94
+ if (typeof toggleCardMode === 'function') {
95
+ toggleCardMode(artifactId)
96
+ }
97
+ }
98
+
99
+ const handleDescriptionChange = (e) => {
100
+ const description = e.target.value
101
+ description?.length > 500
102
+ ? setDescriptionValidationMessage('comment length cannot exceed 500 characters')
103
+ : setDescriptionValidationMessage('')
104
+ setNewDescription(description)
105
+ }
106
+
107
+ const handleCancel = () => {
108
+ setDisplayedTags(initialTags)
109
+ setNewDescription(initialDescription)
110
+ setCreateTags([])
111
+ setSoftDeleteTags([])
112
+ setHardDeleteTags([])
113
+ handleEditClick()
114
+ setShowTagsWarning(false)
115
+ setTagErrorMessage('')
116
+ }
117
+
118
+ const validateTag = (lowercaseValue) : boolean => {
119
+ if (
120
+ lowercaseValue.length == 0 ||
121
+ lowercaseValue.length >= 128 ||
122
+ lowercaseValue[0] == '.' ||
123
+ lowercaseValue[0] == '-'
124
+ ) {
125
+ setTagErrorMessage('tag name cannot be empty or exceed 128 characters or cannot start with . or -')
126
+ return false
127
+ }
128
+ setTagErrorMessage('')
129
+ const isTagExistsInExistingTags = existingTags.includes(lowercaseValue)
130
+ let isTagExistsInDisplayedTags = false
131
+ for (let i = 0; i < displayedTags?.length; i++) {
132
+ if (displayedTags[i].tagName.toLowerCase() === lowercaseValue) isTagExistsInDisplayedTags = true
133
+ }
134
+ if (isTagExistsInExistingTags || isTagExistsInDisplayedTags || lowercaseValue === 'latest') {
135
+ setTagErrorMessage('This tag is already being used in this application')
136
+ return false
137
+ }
138
+ return true
139
+ }
140
+
141
+ const handleTagCreate = (newValue) => {
142
+ const lowercaseValue = newValue.toLowerCase().trim()
143
+ if(validateTag(lowercaseValue)) {
144
+ const newTag: ReleaseTag = {
145
+ id: 0,
146
+ tagName: lowercaseValue,
147
+ appId: 0,
148
+ deleted: false,
149
+ artifactId: 0,
150
+ duplicateTag: false,
151
+ }
152
+ setCreateTags([...createTags, newTag])
153
+ setDisplayedTags([...displayedTags, newTag])
154
+ setShowTagsWarning(true)
155
+ setTextInput('')
156
+ }
157
+ }
158
+
159
+ const handleTagSoftDelete = (index) => {
160
+ const updatedTags = [...displayedTags]
161
+ updatedTags[index] = {
162
+ ...updatedTags[index],
163
+ deleted: !updatedTags[index].deleted,
164
+ }
165
+ const updatedTag = updatedTags[index]
166
+ let updatedSoftDeleteTags = [...softDeleteTags]
167
+ const tagIndex = updatedSoftDeleteTags.findIndex((tag) => tag.tagName === updatedTag.tagName)
168
+ if (tagIndex !== -1) {
169
+ // Tag already exists in softDeleteTags array, remove it
170
+ updatedSoftDeleteTags.splice(tagIndex, 1)
171
+ } else {
172
+ // Tag doesn't exist in softDeleteTags array, add it
173
+ updatedSoftDeleteTags.push(updatedTag)
174
+ }
175
+ setDisplayedTags(updatedTags)
176
+ setSoftDeleteTags(updatedSoftDeleteTags)
177
+ }
178
+
179
+ const handleTagHardDelete = (index) => {
180
+ const deletedTag = displayedTags[index]
181
+ const updatedCreateTags = createTags.filter((tag) => tag.tagName !== deletedTag.tagName)
182
+ setCreateTags(updatedCreateTags)
183
+ const updatedDisplayedTags = [...displayedTags]
184
+ updatedDisplayedTags.splice(index, 1)
185
+ setDisplayedTags(updatedDisplayedTags)
186
+ const updatedExistingTags = existingTags.filter((tag) => tag.toLowerCase() !== deletedTag.tagName.toLowerCase())
187
+ setExistingTags(updatedExistingTags)
188
+ if (deletedTag.id !== 0) {
189
+ const updatedHardDeleteTags = [...hardDeleteTags, deletedTag]
190
+ setHardDeleteTags(updatedHardDeleteTags)
191
+ }
192
+ }
193
+
194
+ const errorStateHandling = (err) => {
195
+ err.forEach(({ userMessage }) => {
196
+ const tagList = createTags.map((tags) => {
197
+ if (userMessage?.appReleaseTags?.includes(tags.tagName)) {
198
+ return {
199
+ ...tags,
200
+ duplicateTag: true,
201
+ }
202
+ }
203
+ return tags
204
+ })
205
+ setCreateTags(tagList)
206
+
207
+ const displayList = displayedTags.map((obj) => {
208
+ const matchingTag = tagList.find((tag) => tag.tagName === obj.tagName)
209
+ if (matchingTag) {
210
+ return { ...obj, duplicateTag: matchingTag.duplicateTag }
211
+ }
212
+ return obj
213
+ })
214
+
215
+ setDisplayedTags(displayList)
216
+ })
217
+ }
218
+
219
+ const handleSave = async () => {
220
+ if (tagErrorMessage) return
221
+
222
+ const payload = {
223
+ createTags: createTags,
224
+ softDeleteTags: softDeleteTags,
225
+ imageComment: {
226
+ id: 0,
227
+ comment: newDescription.trim(),
228
+ artifactId: 0,
229
+ },
230
+ hardDeleteTags: hardDeleteTags,
231
+ }
232
+
233
+ // set loading state true
234
+ setImageTags(payload, ciPipelineId, artifactId)
235
+ .then((res) => {
236
+ const tags = res.result?.imageReleaseTags?.map((tag) => ({
237
+ id: tag.id,
238
+ tagName: tag.tagName,
239
+ deleted: tag.deleted,
240
+ appId: 0,
241
+ artifactId: 0,
242
+ }))
243
+ if (setAppReleaseTagNames) {
244
+ setAppReleaseTagNames(res.result?.appReleaseTags)
245
+ }
246
+ if (setTagsEditable) {
247
+ setTagsEditable(res.result?.tagsEditable)
248
+ }
249
+ setInitialTags(tags)
250
+ setInitialDescription(res.result?.imageComment?.comment)
251
+ setDisplayedTags(tags)
252
+ setNewDescription(res.result?.imageComment?.comment)
253
+ setCreateTags([])
254
+ setSoftDeleteTags([])
255
+ setHardDeleteTags([])
256
+ handleEditClick()
257
+ setShowTagsWarning(false)
258
+ setTagErrorMessage('')
259
+ if(updateCurrentAppMaterial)updateCurrentAppMaterial(artifactId,tags,res.result?.imageComment)
260
+ })
261
+ .catch((err) => {
262
+ // Fix toast message
263
+ if (err.errors?.[0]?.userMessage?.appReleaseTags?.length) {
264
+ toast.error(err.errors?.[0]?.internalMessage)
265
+ errorStateHandling(err.errors)
266
+ } else {
267
+ showError(err)
268
+ }
269
+ })
270
+ }
271
+
272
+ const renderInfoCard = (): JSX.Element => {
273
+ return (
274
+ <TippyCustomized
275
+ theme={TippyTheme.white}
276
+ className="w-300 fcv-5"
277
+ placement="right"
278
+ Icon={QuestionFilled}
279
+ heading="Release tags"
280
+ showCloseButton={true}
281
+ trigger="click"
282
+ interactive={true}
283
+ documentationLinkText="View Documentation"
284
+ additionalContent={getBuildContextAdditionalContent()}
285
+ >
286
+ <div className="icon-dim-16 fcn-5 ml-8 cursor">
287
+ <Question />
288
+ </div>
289
+ </TippyCustomized>
290
+ )
291
+ }
292
+
293
+ const getBuildContextAdditionalContent = () => {
294
+ return (
295
+ <div className="h-250 fs-13 dc__overflow-scroll p-12">
296
+ <div>Release tags allow you to tag container images with readable and relatable tags eg. v1.0.</div>
297
+ <ul className="pl-20 mt-8">
298
+ <li>
299
+ A release tag can only be added if a workflow has CD pipelines deploying to Production
300
+ environments.
301
+ </li>
302
+ <li>Multiple tags can be added to an image.</li>
303
+ <li>Multiple images in an application cannot have the same tag.</li>
304
+ <li>
305
+ Tags cannot be deleted once saved. Although, you can soft delete a tag if an unwanted tag has
306
+ been added.
307
+ </li>
308
+ </ul>
309
+ </div>
310
+ )
311
+ }
312
+
313
+ const creatableRef = useRef(null)
314
+
315
+ if (newDescription === '' && displayedTags.length === 0 && !isEditing) {
316
+ return tagsEditable ? (
317
+ <div className="bcn-0 mt-8 add-image-button">
318
+ <AddImageButton handleEditClick={handleEditClick} />
319
+ </div>
320
+ ) : (
321
+ <div></div>
322
+ )
323
+ }
324
+
325
+ const handleKeydown = (event) => {
326
+ if (event.key === 'Enter' && event.target.value.trim()) {
327
+ handleTagCreate(event.target.value.trim())
328
+ }
329
+ }
330
+
331
+ const setInputValue = (e) => {
332
+ e.target.value && validateTag(e.target.value)
333
+ setTextInput(e.target.value)
334
+ }
335
+
336
+ const enterTagCreate = (e) => {
337
+ stopPropagation(e)
338
+ if (textInput.trim()) {
339
+ handleTagCreate(textInput.trim())
340
+ }
341
+ }
342
+
343
+ const onClickSave = () => {
344
+ if (textInput.trim()) {
345
+ handleTagCreate(textInput)
346
+ }
347
+ setSaveData(true)
348
+ }
349
+
350
+ return (
351
+ <div className="mt-8">
352
+ {isEditing && tagsEditable ? (
353
+ <div className="bcn-0 dc__border-top-n1 ">
354
+ <div className="cn-7 mt-12 flex left">
355
+ <span>Release tags (eg. v1.0)</span>
356
+ <div className="flex row ml-0">{renderInfoCard()}</div>
357
+ </div>
358
+ <div className="mt-6 dc__position-rel-imp" data-testid="add-tag-text-area">
359
+ <input
360
+ className="form__input"
361
+ value={textInput}
362
+ onChange={setInputValue}
363
+ placeholder="Type a tag and press enter"
364
+ onKeyDown={handleKeydown}
365
+ ref={creatableRef}
366
+ />
367
+ {textInput.length > 0 && (
368
+ <div
369
+ onClick={enterTagCreate}
370
+ className="dc__position-abs bcn-0 flex cursor pt-2 pb-2 pr-4 pl-4 br-2 cn-7 dc__border dc__border-bottom-2"
371
+ style={{
372
+ top: '50%',
373
+ fontFamily: 'monospace',
374
+ right: '8px',
375
+ transform: 'translateY(-50%)',
376
+ }}
377
+ >
378
+ <Enter className="icon-dim-16 mr-4" />
379
+ Enter
380
+ </div>
381
+ )}
382
+ </div>
383
+
384
+ {tagErrorMessage && (
385
+ <div className="flex left">
386
+ <Error className="form__icon form__icon--error" />
387
+ <div className="form__error">{tagErrorMessage}</div>
388
+ </div>
389
+ )}
390
+ <div className="flex-wrap mt-8 flex left">
391
+ {displayedTags?.map((tag, index) => (
392
+ <ImageTagButton
393
+ key={tag.id}
394
+ text={tag?.tagName}
395
+ isSoftDeleted={tag?.deleted}
396
+ isEditing={isEditing}
397
+ onSoftDeleteClick={(e) => {
398
+ stopPropagation(e)
399
+ handleTagSoftDelete(index)
400
+ }}
401
+ onHardDeleteClick={(e) => {
402
+ stopPropagation(e)
403
+ handleTagHardDelete(index)
404
+ }}
405
+ tagId={tag.id}
406
+ softDeleteTags={softDeleteTags}
407
+ isSuperAdmin={isSuperAdmin}
408
+ duplicateTag={tag?.duplicateTag}
409
+ hideHardDelete={hideHardDelete}
410
+ />
411
+ ))}
412
+ </div>
413
+ {showTagsWarning && (
414
+ <div className="cn-7 mb-8 flex left">
415
+ <Info className="form__icon--info icon-dim-16 mr-2" />
416
+ Tags cannot be edited/removed later
417
+ </div>
418
+ )}
419
+ <div className="cn-7 mt-12">Comment</div>
420
+ <div
421
+ className="flex left flex-wrap dc__gap-8 w-100 mt-6 mb-12"
422
+ data-testid="add-image-comment-text-area"
423
+ >
424
+ <textarea
425
+ value={newDescription}
426
+ onChange={handleDescriptionChange}
427
+ className="flex left flex-wrap dc__gap-8 dc__description-textarea"
428
+ style={{ height: '90px !important' }}
429
+ />
430
+ </div>
431
+ {descriptionValidationMessage !== '' && (
432
+ <div className="flex left">
433
+ <Error className="form__icon form__icon--error" />
434
+ <div className="form__error">{descriptionValidationMessage}</div>
435
+ </div>
436
+ )}
437
+ <div className="w-100 flex right mt-12">
438
+ <button
439
+ className="cta flex cancel h-32 lh-32-imp"
440
+ type="button"
441
+ onClick={(e) => {
442
+ stopPropagation(e)
443
+ handleCancel()
444
+ }}
445
+ >
446
+ Cancel
447
+ </button>
448
+ <button
449
+ className="cta flex h-32 lh-32-imp ml-12"
450
+ data-testid="save-tags-button"
451
+ type="button"
452
+ onClick={(e) => {
453
+ stopPropagation(e)
454
+ onClickSave()
455
+ }}
456
+ >
457
+ Save
458
+ </button>
459
+ </div>
460
+ </div>
461
+ ) : (
462
+ <div className="top br-4 bcn-0 image-tags-container flexbox">
463
+ <div
464
+ className="flex left image-tag-left-border w-100 mt-8 mb-8 pr-10 pl-10"
465
+ data-testid="image-tags-container-hover"
466
+ >
467
+ <div className='w-100'>
468
+ {initialDescription && <div className="mb-6 fs-13 dc__word-break-all">{initialDescription}</div>}
469
+ <div className="flex-wrap flex left">
470
+ {initialTags?.map((tag, index) => (
471
+ <ImageTagButton
472
+ key={tag?.id}
473
+ text={tag?.tagName}
474
+ isSoftDeleted={tag?.deleted}
475
+ isEditing={isEditing}
476
+ onSoftDeleteClick={(e) => {
477
+ stopPropagation(e)
478
+ handleTagSoftDelete(index)
479
+ }}
480
+ onHardDeleteClick={(e) => {
481
+ stopPropagation(e)
482
+ handleTagHardDelete(index)
483
+ }}
484
+ tagId={tag.id}
485
+ softDeleteTags={softDeleteTags}
486
+ isSuperAdmin={isSuperAdmin}
487
+ duplicateTag={tag?.duplicateTag}
488
+ hideHardDelete={hideHardDelete}
489
+ />
490
+ ))}
491
+ </div>
492
+ </div>
493
+ </div>
494
+ <div className="mt-8 mr-6 icon-dim-16">
495
+ {tagsEditable && (
496
+ <EditIcon
497
+ className="icon-dim-16 image-tags-container-edit__icon cursor"
498
+ data-testid="edit-tags-icon"
499
+ onClick={(e) => {
500
+ stopPropagation(e)
501
+ handleEditClick()
502
+ }}
503
+ />
504
+ )}
505
+ </div>
506
+ </div>
507
+ )}
508
+ </div>
509
+ )
510
+ }
511
+
512
+ export const ImageTagButton = ({
513
+ text,
514
+ isSoftDeleted,
515
+ isEditing,
516
+ onSoftDeleteClick,
517
+ onHardDeleteClick,
518
+ tagId,
519
+ softDeleteTags,
520
+ isSuperAdmin,
521
+ duplicateTag,
522
+ hideHardDelete,
523
+ }: ImageButtonType) => {
524
+ const IconComponent = isSoftDeleted ? Redo : Minus
525
+ const isInSoftDeleteTags = isSoftDeleted && softDeleteTags.some((tag) => tag.tagName === text)
526
+ const canTagBeHardDelete = tagId === 0 || (isSuperAdmin && !hideHardDelete)
527
+
528
+ const tabColor = () => {
529
+ if (duplicateTag) {
530
+ return 'cr-5 bcr-1 er-2'
531
+ } else if (isSoftDeleted) {
532
+ return 'cy-7 bcy-1 dc__strike-through ey-2'
533
+ } else {
534
+ return 'cn-9'
535
+ }
536
+ }
537
+
538
+ return (
539
+ <div
540
+ className={`br-4 en-2 bw-1 mr-8 bcn-0 mb-4 pt-2 pr-8 pb-2 pl-8 tag-class dc__word-break-all flex ${
541
+ isEditing ? 'icon-hover' : ''
542
+ } ${tabColor()}`}
543
+ >
544
+ {duplicateTag ? (
545
+ <Warning className="icon-dim-12 mr-4" />
546
+ ) : (
547
+ <Tippy
548
+ className="default-tt"
549
+ arrow={true}
550
+ placement="top"
551
+ content={isInSoftDeleteTags ? 'Restore tag' : 'Soft delete tag'}
552
+ >
553
+ <div
554
+ className={`action-icon mr-4 lh-16 pt-3 ${
555
+ isInSoftDeleteTags || (tagId !== 0 && !isSoftDeleted) ? 'show-icon' : ''
556
+ }`}
557
+ >
558
+ <IconComponent
559
+ className={`icon-dim-12 cursor${isSoftDeleted ? 'scn-6' : 'fcn-6'} `}
560
+ data-testid={`${text}-tag-soft-delete`}
561
+ onClick={onSoftDeleteClick}
562
+ />
563
+ </div>
564
+ </Tippy>
565
+ )}
566
+ {text}
567
+ <Tippy className="default-tt" arrow={true} placement="top" content="Remove tag">
568
+ <div className={`action-icon ml-4 lh-16 pt-3 ${canTagBeHardDelete ? 'show-icon' : ''}`}>
569
+ <Close
570
+ className="icon-dim-12 fcn-6 cursor"
571
+ data-testid={`${text}-tag-hard-delete`}
572
+ onClick={onHardDeleteClick}
573
+ />
574
+ </div>
575
+ </Tippy>
576
+ </div>
577
+ )
578
+ }
579
+
580
+ const AddImageButton = ({ handleEditClick }) => {
581
+ const handleClick = (e) => {
582
+ stopPropagation(e)
583
+ handleEditClick()
584
+ }
585
+
586
+ return (
587
+ <div className="add-tag-button flex pt-12 pr-12" data-testid="add-tags-button" onClick={handleClick}>
588
+ <div className="lh-16 flex">
589
+ <Add className="icon-dim-16 cn-6" />
590
+ <span className="cn-7">Add tags/comment</span>
591
+ </div>
592
+ </div>
593
+ )
594
+ }