mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	feat: multiple UX improvements to labels (#7358)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -74,3 +74,5 @@ yalc.lock | |||||||
| /yarn-error.log | /yarn-error.log | ||||||
| yarn-debug.log* | yarn-debug.log* | ||||||
| .yarn-integrity | .yarn-integrity | ||||||
|  |  | ||||||
|  | /storybook-static | ||||||
							
								
								
									
										25
									
								
								app/javascript/dashboard/components/base/Hotkey.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/javascript/dashboard/components/base/Hotkey.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | <template> | ||||||
|  |   <kbd class="hotkey"> | ||||||
|  |     <slot /> | ||||||
|  |   </kbd> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <style lang="scss"> | ||||||
|  | kbd.hotkey { | ||||||
|  |   display: inline-flex; | ||||||
|  |   flex-shrink: 0; | ||||||
|  |   align-items: center; | ||||||
|  |   user-select: none; | ||||||
|  |   justify-content: center; | ||||||
|  |   padding: var(--space-micro); | ||||||
|  |   min-width: var(--space-normal); | ||||||
|  |   font-size: var(--font-size-micro); | ||||||
|  |   border-radius: var(--space-smaller); | ||||||
|  |   background-color: var(--color-background); | ||||||
|  |   border: 1px solid var(--color-border); | ||||||
|  |   text-transform: uppercase; | ||||||
|  |   color: var(--color-body); | ||||||
|  |   letter-spacing: var(--space-micro); | ||||||
|  |   line-height: var(--font-size-micro); | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -20,6 +20,7 @@ | |||||||
|           v-if="showSearchDropdownLabel" |           v-if="showSearchDropdownLabel" | ||||||
|           :account-labels="allLabels" |           :account-labels="allLabels" | ||||||
|           :selected-labels="selectedLabels" |           :selected-labels="selectedLabels" | ||||||
|  |           :allow-creation="isAdmin" | ||||||
|           @add="addItem" |           @add="addItem" | ||||||
|           @remove="removeItem" |           @remove="removeItem" | ||||||
|         /> |         /> | ||||||
| @@ -30,8 +31,15 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import AddLabel from 'shared/components/ui/dropdown/AddLabel'; | import AddLabel from 'shared/components/ui/dropdown/AddLabel'; | ||||||
|  | import eventListenerMixins from 'shared/mixins/eventListenerMixins'; | ||||||
| import LabelDropdown from 'shared/components/ui/label/LabelDropdown'; | import LabelDropdown from 'shared/components/ui/label/LabelDropdown'; | ||||||
| import { mixin as clickaway } from 'vue-clickaway'; | import { mixin as clickaway } from 'vue-clickaway'; | ||||||
|  | import adminMixin from 'dashboard/mixins/isAdmin'; | ||||||
|  | import { | ||||||
|  |   buildHotKeys, | ||||||
|  |   isEscape, | ||||||
|  |   isActiveElementTypeable, | ||||||
|  | } from 'shared/helpers/KeyboardHelpers'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
| @@ -39,7 +47,7 @@ export default { | |||||||
|     LabelDropdown, |     LabelDropdown, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   mixins: [clickaway], |   mixins: [clickaway, adminMixin, eventListenerMixins], | ||||||
|  |  | ||||||
|   props: { |   props: { | ||||||
|     allLabels: { |     allLabels: { | ||||||
| @@ -80,6 +88,18 @@ export default { | |||||||
|     closeDropdownLabel() { |     closeDropdownLabel() { | ||||||
|       this.showSearchDropdownLabel = false; |       this.showSearchDropdownLabel = false; | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  |     handleKeyEvents(e) { | ||||||
|  |       const keyPattern = buildHotKeys(e); | ||||||
|  |  | ||||||
|  |       if (keyPattern === 'l' && !isActiveElementTypeable(e)) { | ||||||
|  |         this.toggleLabels(); | ||||||
|  |         e.preventDefault(); | ||||||
|  |       } else if (isEscape(e) && this.showSearchDropdownLabel) { | ||||||
|  |         this.closeDropdownLabel(); | ||||||
|  |         e.preventDefault(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -33,7 +33,8 @@ | |||||||
|       "LABEL_SELECT": { |       "LABEL_SELECT": { | ||||||
|         "TITLE": "Add Labels", |         "TITLE": "Add Labels", | ||||||
|         "PLACEHOLDER": "Search labels", |         "PLACEHOLDER": "Search labels", | ||||||
|         "NO_RESULT": "No labels found" |         "NO_RESULT": "No labels found", | ||||||
|  |         "CREATE_LABEL": "Create new label" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "MERGE_CONTACT": "Merge contact", |     "MERGE_CONTACT": "Merge contact", | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ | |||||||
|               v-if="showSearchDropdownLabel" |               v-if="showSearchDropdownLabel" | ||||||
|               :account-labels="accountLabels" |               :account-labels="accountLabels" | ||||||
|               :selected-labels="savedLabels" |               :selected-labels="savedLabels" | ||||||
|  |               :allow-creation="isAdmin" | ||||||
|               @add="addLabelToConversation" |               @add="addLabelToConversation" | ||||||
|               @remove="removeLabelFromConversation" |               @remove="removeLabelFromConversation" | ||||||
|             /> |             /> | ||||||
| @@ -47,7 +48,14 @@ import Spinner from 'shared/components/Spinner'; | |||||||
| import LabelDropdown from 'shared/components/ui/label/LabelDropdown'; | import LabelDropdown from 'shared/components/ui/label/LabelDropdown'; | ||||||
| import AddLabel from 'shared/components/ui/dropdown/AddLabel'; | import AddLabel from 'shared/components/ui/dropdown/AddLabel'; | ||||||
| import { mixin as clickaway } from 'vue-clickaway'; | import { mixin as clickaway } from 'vue-clickaway'; | ||||||
|  | import adminMixin from 'dashboard/mixins/isAdmin'; | ||||||
|  | import eventListenerMixins from 'shared/mixins/eventListenerMixins'; | ||||||
| import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin'; | import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin'; | ||||||
|  | import { | ||||||
|  |   buildHotKeys, | ||||||
|  |   isEscape, | ||||||
|  |   isActiveElementTypeable, | ||||||
|  | } from 'shared/helpers/KeyboardHelpers'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
| @@ -56,7 +64,7 @@ export default { | |||||||
|     AddLabel, |     AddLabel, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   mixins: [clickaway, conversationLabelMixin], |   mixins: [clickaway, conversationLabelMixin, adminMixin, eventListenerMixins], | ||||||
|   props: { |   props: { | ||||||
|     conversationId: { |     conversationId: { | ||||||
|       type: Number, |       type: Number, | ||||||
| @@ -84,6 +92,17 @@ export default { | |||||||
|     closeDropdownLabel() { |     closeDropdownLabel() { | ||||||
|       this.showSearchDropdownLabel = false; |       this.showSearchDropdownLabel = false; | ||||||
|     }, |     }, | ||||||
|  |     handleKeyEvents(e) { | ||||||
|  |       const keyPattern = buildHotKeys(e); | ||||||
|  |  | ||||||
|  |       if (keyPattern === 'l' && !isActiveElementTypeable(e)) { | ||||||
|  |         this.toggleLabels(); | ||||||
|  |         e.preventDefault(); | ||||||
|  |       } else if (isEscape(e) && this.showSearchDropdownLabel) { | ||||||
|  |         this.closeDropdownLabel(); | ||||||
|  |         e.preventDefault(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -65,6 +65,12 @@ import { getRandomColor } from 'dashboard/helper/labelColor'; | |||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   mixins: [alertMixin, validationMixin], |   mixins: [alertMixin, validationMixin], | ||||||
|  |   props: { | ||||||
|  |     prefillTitle: { | ||||||
|  |       type: String, | ||||||
|  |       default: '', | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       color: '#000', |       color: '#000', | ||||||
| @@ -81,6 +87,7 @@ export default { | |||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     this.color = getRandomColor(); |     this.color = getRandomColor(); | ||||||
|  |     this.title = this.prefillTitle.toLowerCase(); | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     onClose() { |     onClose() { | ||||||
|   | |||||||
| @@ -45,7 +45,10 @@ export const actions = { | |||||||
|     commit(types.SET_LABEL_UI_FLAG, { isFetching: true }); |     commit(types.SET_LABEL_UI_FLAG, { isFetching: true }); | ||||||
|     try { |     try { | ||||||
|       const response = await LabelsAPI.get(true); |       const response = await LabelsAPI.get(true); | ||||||
|       commit(types.SET_LABELS, response.data.payload); |       const sortedLabels = response.data.payload.sort((a, b) => | ||||||
|  |         a.title.localeCompare(b.title) | ||||||
|  |       ); | ||||||
|  |       commit(types.SET_LABELS, sortedLabels); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       // Ignore error |       // Ignore error | ||||||
|     } finally { |     } finally { | ||||||
|   | |||||||
| @@ -1,8 +1,11 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="dropdown-search-wrap"> |   <div class="dropdown-search-wrap"> | ||||||
|  |     <div class="dropdown-title-container"> | ||||||
|       <h4 class="text-block-title"> |       <h4 class="text-block-title"> | ||||||
|         {{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.TITLE') }} |         {{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.TITLE') }} | ||||||
|       </h4> |       </h4> | ||||||
|  |       <hotkey>L</hotkey> | ||||||
|  |     </div> | ||||||
|     <div class="search-wrap"> |     <div class="search-wrap"> | ||||||
|       <input |       <input | ||||||
|         ref="searchbar" |         ref="searchbar" | ||||||
| @@ -28,6 +31,31 @@ | |||||||
|         <div v-if="noResult" class="no-result"> |         <div v-if="noResult" class="no-result"> | ||||||
|           {{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.NO_RESULT') }} |           {{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.NO_RESULT') }} | ||||||
|         </div> |         </div> | ||||||
|  |         <div v-if="allowCreation && shouldShowCreate" class="new-label"> | ||||||
|  |           <woot-button | ||||||
|  |             size="small" | ||||||
|  |             variant="clear" | ||||||
|  |             color-scheme="secondary" | ||||||
|  |             icon="add" | ||||||
|  |             is-expanded | ||||||
|  |             class="button-new-label" | ||||||
|  |             :is-disabled="hasExactMatchInResults" | ||||||
|  |             @click="showCreateModal" | ||||||
|  |           > | ||||||
|  |             {{ createLabelPlaceholder }} | ||||||
|  |             {{ parsedSearch }} | ||||||
|  |           </woot-button> | ||||||
|  |  | ||||||
|  |           <woot-modal | ||||||
|  |             :show.sync="createModalVisible" | ||||||
|  |             :on-close="hideCreateModal" | ||||||
|  |           > | ||||||
|  |             <add-label-modal | ||||||
|  |               :prefill-title="parsedSearch" | ||||||
|  |               @close="hideCreateModal" | ||||||
|  |             /> | ||||||
|  |           </woot-modal> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| @@ -35,9 +63,16 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import LabelDropdownItem from './LabelDropdownItem'; | import LabelDropdownItem from './LabelDropdownItem'; | ||||||
|  | import Hotkey from 'dashboard/components/base/Hotkey'; | ||||||
|  | import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel'; | ||||||
|  | import { picoSearch } from '@scmmishra/pico-search'; | ||||||
|  | import { sanitizeLabel } from 'shared/helpers/sanitizeData'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     LabelDropdownItem, |     LabelDropdownItem, | ||||||
|  |     AddLabelModal, | ||||||
|  |     Hotkey, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   props: { |   props: { | ||||||
| @@ -49,23 +84,49 @@ export default { | |||||||
|       type: Array, |       type: Array, | ||||||
|       default: () => [], |       default: () => [], | ||||||
|     }, |     }, | ||||||
|  |     allowCreation: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       search: '', |       search: '', | ||||||
|  |       createModalVisible: false, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   computed: { |   computed: { | ||||||
|  |     createLabelPlaceholder() { | ||||||
|  |       const label = this.$t('CONTACT_PANEL.LABELS.LABEL_SELECT.CREATE_LABEL'); | ||||||
|  |       return this.search ? `${label}:` : label; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|     filteredActiveLabels() { |     filteredActiveLabels() { | ||||||
|       return this.accountLabels.filter(label => { |       if (!this.search) return this.accountLabels; | ||||||
|         return label.title.toLowerCase().includes(this.search.toLowerCase()); |  | ||||||
|  |       return picoSearch(this.accountLabels, this.search, ['title'], { | ||||||
|  |         threshold: 0.9, | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     noResult() { |     noResult() { | ||||||
|       return this.filteredActiveLabels.length === 0 && this.search !== ''; |       return this.filteredActiveLabels.length === 0; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     hasExactMatchInResults() { | ||||||
|  |       return this.filteredActiveLabels.some( | ||||||
|  |         label => label.title === this.search | ||||||
|  |       ); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     shouldShowCreate() { | ||||||
|  |       return this.allowCreation && this.filteredActiveLabels.length < 3; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     parsedSearch() { | ||||||
|  |       return sanitizeLabel(this.search); | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
| @@ -97,11 +158,35 @@ export default { | |||||||
|         this.onAdd(label); |         this.onAdd(label); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  |     showCreateModal() { | ||||||
|  |       this.createModalVisible = true; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     hideCreateModal() { | ||||||
|  |       this.createModalVisible = false; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  | .dropdown-title-container { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   align-items: center; | ||||||
|  |   margin-bottom: var(--space-smaller); | ||||||
|  |  | ||||||
|  |   .text-block-title { | ||||||
|  |     flex-grow: 1; | ||||||
|  |     margin: 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .hotkey { | ||||||
|  |     flex-shrink: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| .dropdown-search-wrap { | .dropdown-search-wrap { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
| @@ -124,7 +209,7 @@ export default { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     input:focus { |     input:focus { | ||||||
|       border: 1px solid var(--w-500); |       outline: 1px solid var(--color-border-dark); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -143,9 +228,30 @@ export default { | |||||||
|       display: flex; |       display: flex; | ||||||
|       justify-content: center; |       justify-content: center; | ||||||
|       color: var(--s-700); |       color: var(--s-700); | ||||||
|       padding: var(--space-smaller) var(--space-one); |       padding: var(--space-normal) var(--space-one); | ||||||
|       font-weight: var(--font-weight-medium); |       font-weight: var(--font-weight-medium); | ||||||
|       font-size: var(--font-size-small); |       font-size: var(--font-size-mini); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .new-label { | ||||||
|  |       display: flex; | ||||||
|  |       padding-top: var(--space-smaller); | ||||||
|  |       border-top: 1px solid var(--s-100); | ||||||
|  |  | ||||||
|  |       .button-new-label { | ||||||
|  |         white-space: nowrap; | ||||||
|  |         text-overflow: ellipsis; | ||||||
|  |         overflow: hidden; | ||||||
|  |         align-items: center; | ||||||
|  |  | ||||||
|  |         .icon { | ||||||
|  |           min-width: 0; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .search-term { | ||||||
|  |         color: var(--s-700); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -106,6 +106,11 @@ export const hasPressedCommandPlusKKey = e => { | |||||||
|   return e.metaKey && e.keyCode === 75; |   return e.metaKey && e.keyCode === 75; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns a string representation of the hotkey pattern based on the provided event object. | ||||||
|  |  * @param {KeyboardEvent} e - The keyboard event object. | ||||||
|  |  * @returns {string} - The hotkey pattern string. | ||||||
|  |  */ | ||||||
| export const buildHotKeys = e => { | export const buildHotKeys = e => { | ||||||
|   const key = e.key.toLowerCase(); |   const key = e.key.toLowerCase(); | ||||||
|   if (['shift', 'meta', 'alt', 'control'].includes(key)) { |   if (['shift', 'meta', 'alt', 'control'].includes(key)) { | ||||||
| @@ -127,3 +132,30 @@ export const buildHotKeys = e => { | |||||||
|   hotKeyPattern += key; |   hotKeyPattern += key; | ||||||
|   return hotKeyPattern; |   return hotKeyPattern; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Determines whether the active element is typeable. | ||||||
|  |  * | ||||||
|  |  * @param {KeyboardEvent} e - The keyboard event object. | ||||||
|  |  * @returns {boolean} `true` if the active element is typeable, `false` otherwise. | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  * document.addEventListener('keydown', e => { | ||||||
|  |  *   if (isActiveElementTypeable(e)) { | ||||||
|  |  *     handleTypeableElement(e); | ||||||
|  |  *   } | ||||||
|  |  * }); | ||||||
|  |  */ | ||||||
|  | export const isActiveElementTypeable = e => { | ||||||
|  |   /** @type {HTMLElement | null} */ | ||||||
|  |   // @ts-ignore | ||||||
|  |   const activeElement = e.target || document.activeElement; | ||||||
|  |  | ||||||
|  |   return !!( | ||||||
|  |     activeElement?.tagName === 'INPUT' || | ||||||
|  |     activeElement?.tagName === 'NINJA-KEYS' || | ||||||
|  |     activeElement?.tagName === 'TEXTAREA' || | ||||||
|  |     activeElement?.contentEditable === 'true' || | ||||||
|  |     activeElement?.className?.includes('ProseMirror') | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								app/javascript/shared/helpers/sanitizeData.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/javascript/shared/helpers/sanitizeData.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | export const labelSanitizePattern = /[^a-zA-Z0-9_-]/g; | ||||||
|  | export const spacesPattern = /\s+/g; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Sanitizes a label by removing unwanted characters and replacing spaces with hyphens. | ||||||
|  |  * | ||||||
|  |  * @param {string | undefined | null} label - The label to sanitize. | ||||||
|  |  * @returns {string} The sanitized label. | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  * const label = 'My Label 123'; | ||||||
|  |  * const sanitizedLabel = sanitizeLabel(label); // 'my-label-123' | ||||||
|  |  */ | ||||||
|  | export const sanitizeLabel = (label = '') => { | ||||||
|  |   if (!label) return ''; | ||||||
|  |  | ||||||
|  |   return label | ||||||
|  |     .trim() | ||||||
|  |     .toLowerCase() | ||||||
|  |     .replace(spacesPattern, '-') | ||||||
|  |     .replace(labelSanitizePattern, ''); | ||||||
|  | }; | ||||||
| @@ -4,6 +4,7 @@ import { | |||||||
|   hasPressedShift, |   hasPressedShift, | ||||||
|   hasPressedCommand, |   hasPressedCommand, | ||||||
|   buildHotKeys, |   buildHotKeys, | ||||||
|  |   isActiveElementTypeable, | ||||||
| } from '../KeyboardHelpers'; | } from '../KeyboardHelpers'; | ||||||
|  |  | ||||||
| describe('#KeyboardHelpers', () => { | describe('#KeyboardHelpers', () => { | ||||||
| @@ -39,3 +40,37 @@ describe('#KeyboardHelpers', () => { | |||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | describe('isActiveElementTypeable', () => { | ||||||
|  |   it('should return true if the active element is an input element', () => { | ||||||
|  |     const event = { target: document.createElement('input') }; | ||||||
|  |     const result = isActiveElementTypeable(event); | ||||||
|  |     expect(result).toBe(true); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should return true if the active element is a textarea element', () => { | ||||||
|  |     const event = { target: document.createElement('textarea') }; | ||||||
|  |     const result = isActiveElementTypeable(event); | ||||||
|  |     expect(result).toBe(true); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should return true if the active element is a contentEditable element', () => { | ||||||
|  |     const element = document.createElement('div'); | ||||||
|  |     element.contentEditable = 'true'; | ||||||
|  |     const event = { target: element }; | ||||||
|  |     const result = isActiveElementTypeable(event); | ||||||
|  |     expect(result).toBe(true); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should return false if the active element is not typeable', () => { | ||||||
|  |     const event = { target: document.createElement('div') }; | ||||||
|  |     const result = isActiveElementTypeable(event); | ||||||
|  |     expect(result).toBe(false); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should return false if the active element is null', () => { | ||||||
|  |     const event = { target: null }; | ||||||
|  |     const result = isActiveElementTypeable(event); | ||||||
|  |     expect(result).toBe(false); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								app/javascript/shared/helpers/specs/sanitizeData.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/javascript/shared/helpers/specs/sanitizeData.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | import { sanitizeLabel } from '../sanitizeData'; | ||||||
|  |  | ||||||
|  | describe('sanitizeLabel', () => { | ||||||
|  |   it('should return an empty string when given an empty string', () => { | ||||||
|  |     const label = ''; | ||||||
|  |     const sanitizedLabel = sanitizeLabel(label); | ||||||
|  |     expect(sanitizedLabel).toEqual(''); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should remove leading and trailing whitespace', () => { | ||||||
|  |     const label = '  My Label  '; | ||||||
|  |     const sanitizedLabel = sanitizeLabel(label); | ||||||
|  |     expect(sanitizedLabel).toEqual('my-label'); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should convert all characters to lowercase', () => { | ||||||
|  |     const label = 'My Label'; | ||||||
|  |     const sanitizedLabel = sanitizeLabel(label); | ||||||
|  |     expect(sanitizedLabel).toEqual('my-label'); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should replace spaces with hyphens', () => { | ||||||
|  |     const label = 'My Label 123'; | ||||||
|  |     const sanitizedLabel = sanitizeLabel(label); | ||||||
|  |     expect(sanitizedLabel).toEqual('my-label-123'); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should remove any characters that are not alphanumeric, underscore, or hyphen', () => { | ||||||
|  |     const label = 'My_Label!123'; | ||||||
|  |     const sanitizedLabel = sanitizeLabel(label); | ||||||
|  |     expect(sanitizedLabel).toEqual('my_label123'); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should handle null and undefined input', () => { | ||||||
|  |     const nullLabel = null; | ||||||
|  |     const undefinedLabel = undefined; | ||||||
|  |  | ||||||
|  |     // @ts-ignore - intentionally passing null and undefined to test | ||||||
|  |     const sanitizedNullLabel = sanitizeLabel(nullLabel); | ||||||
|  |     const sanitizedUndefinedLabel = sanitizeLabel(undefinedLabel); | ||||||
|  |     expect(sanitizedNullLabel).toEqual(''); | ||||||
|  |     expect(sanitizedUndefinedLabel).toEqual(''); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { isEscape } from '../helpers/KeyboardHelpers'; | import { isActiveElementTypeable, isEscape } from '../helpers/KeyboardHelpers'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   mounted() { |   mounted() { | ||||||
| @@ -9,13 +9,9 @@ export default { | |||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     onKeyDownHandler(e) { |     onKeyDownHandler(e) { | ||||||
|       const isEventFromAnInputBox = |       const isTypeable = isActiveElementTypeable(e); | ||||||
|         e.target?.tagName === 'INPUT' || e.target?.tagName === 'TEXTAREA'; |  | ||||||
|       const isEventFromProseMirror = e.target?.className?.includes( |  | ||||||
|         'ProseMirror' |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       if (isEventFromAnInputBox || isEventFromProseMirror) { |       if (isTypeable) { | ||||||
|         if (isEscape(e)) { |         if (isEscape(e)) { | ||||||
|           e.target.blur(); |           e.target.blur(); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ class Label < ApplicationRecord | |||||||
|             uniqueness: { scope: :account_id } |             uniqueness: { scope: :account_id } | ||||||
|  |  | ||||||
|   after_update_commit :update_associated_models |   after_update_commit :update_associated_models | ||||||
|  |   default_scope { order(:title) } | ||||||
|  |  | ||||||
|   before_validation do |   before_validation do | ||||||
|     self.title = title.downcase if attribute_present?('title') |     self.title = title.downcase if attribute_present?('title') | ||||||
|   | |||||||
| @@ -37,6 +37,7 @@ | |||||||
|     "@rails/actioncable": "6.1.3", |     "@rails/actioncable": "6.1.3", | ||||||
|     "@rails/ujs": "^7.0.3-1", |     "@rails/ujs": "^7.0.3-1", | ||||||
|     "@rails/webpacker": "5.4.4", |     "@rails/webpacker": "5.4.4", | ||||||
|  |     "@scmmishra/pico-search": "^0.5.1", | ||||||
|     "@sentry/tracing": "^6.19.7", |     "@sentry/tracing": "^6.19.7", | ||||||
|     "@sentry/vue": "^6.19.7", |     "@sentry/vue": "^6.19.7", | ||||||
|     "@sindresorhus/slugify": "1.1.0", |     "@sindresorhus/slugify": "1.1.0", | ||||||
| @@ -105,6 +106,7 @@ | |||||||
|     "@storybook/addon-links": "6.5.9", |     "@storybook/addon-links": "6.5.9", | ||||||
|     "@storybook/addons": "6.5.9", |     "@storybook/addons": "6.5.9", | ||||||
|     "@storybook/vue": "6.5.9", |     "@storybook/vue": "6.5.9", | ||||||
|  |     "@types/jest": "^29.5.2", | ||||||
|     "@vue/test-utils": "1.1.4", |     "@vue/test-utils": "1.1.4", | ||||||
|     "babel-core": "^7.0.0-bridge.0", |     "babel-core": "^7.0.0-bridge.0", | ||||||
|     "babel-eslint": "^10.1.0", |     "babel-eslint": "^10.1.0", | ||||||
|   | |||||||
							
								
								
									
										150
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										150
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -2356,6 +2356,13 @@ | |||||||
|     "@types/node" "*" |     "@types/node" "*" | ||||||
|     jest-mock "^26.6.2" |     jest-mock "^26.6.2" | ||||||
|  |  | ||||||
|  | "@jest/expect-utils@^29.5.0": | ||||||
|  |   version "29.5.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.5.0.tgz#f74fad6b6e20f924582dc8ecbf2cb800fe43a036" | ||||||
|  |   integrity sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg== | ||||||
|  |   dependencies: | ||||||
|  |     jest-get-type "^29.4.3" | ||||||
|  |  | ||||||
| "@jest/fake-timers@^26.6.2": | "@jest/fake-timers@^26.6.2": | ||||||
|   version "26.6.2" |   version "26.6.2" | ||||||
|   resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" |   resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" | ||||||
| @@ -2409,6 +2416,13 @@ | |||||||
|   optionalDependencies: |   optionalDependencies: | ||||||
|     node-notifier "^8.0.0" |     node-notifier "^8.0.0" | ||||||
|  |  | ||||||
|  | "@jest/schemas@^29.4.3": | ||||||
|  |   version "29.4.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" | ||||||
|  |   integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== | ||||||
|  |   dependencies: | ||||||
|  |     "@sinclair/typebox" "^0.25.16" | ||||||
|  |  | ||||||
| "@jest/source-map@^26.6.2": | "@jest/source-map@^26.6.2": | ||||||
|   version "26.6.2" |   version "26.6.2" | ||||||
|   resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" |   resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" | ||||||
| @@ -2512,6 +2526,18 @@ | |||||||
|     "@types/yargs" "^15.0.0" |     "@types/yargs" "^15.0.0" | ||||||
|     chalk "^4.0.0" |     chalk "^4.0.0" | ||||||
|  |  | ||||||
|  | "@jest/types@^29.5.0": | ||||||
|  |   version "29.5.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593" | ||||||
|  |   integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== | ||||||
|  |   dependencies: | ||||||
|  |     "@jest/schemas" "^29.4.3" | ||||||
|  |     "@types/istanbul-lib-coverage" "^2.0.0" | ||||||
|  |     "@types/istanbul-reports" "^3.0.0" | ||||||
|  |     "@types/node" "*" | ||||||
|  |     "@types/yargs" "^17.0.8" | ||||||
|  |     chalk "^4.0.0" | ||||||
|  |  | ||||||
| "@jridgewell/gen-mapping@^0.3.0": | "@jridgewell/gen-mapping@^0.3.0": | ||||||
|   version "0.3.1" |   version "0.3.1" | ||||||
|   resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" |   resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" | ||||||
| @@ -2772,6 +2798,11 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     any-observable "^0.3.0" |     any-observable "^0.3.0" | ||||||
|  |  | ||||||
|  | "@scmmishra/pico-search@^0.5.1": | ||||||
|  |   version "0.5.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@scmmishra/pico-search/-/pico-search-0.5.1.tgz#fe10a5615259623bff0095ad9af26e15d84379cb" | ||||||
|  |   integrity sha512-OKyoGy9f0Y47khEpgP3eLw2aKujJe/ndAE0IbovpsVh210GCmECtl8DLYKL+3abcorX3+e6K8S3eFs2HdH8Ksw== | ||||||
|  |  | ||||||
| "@segment/analytics.js-video-plugins@^0.2.1": | "@segment/analytics.js-video-plugins@^0.2.1": | ||||||
|   version "0.2.1" |   version "0.2.1" | ||||||
|   resolved "https://registry.yarnpkg.com/@segment/analytics.js-video-plugins/-/analytics.js-video-plugins-0.2.1.tgz#3596fa3887dcd9df5978dc566edf4a0aea2a9b1e" |   resolved "https://registry.yarnpkg.com/@segment/analytics.js-video-plugins/-/analytics.js-video-plugins-0.2.1.tgz#3596fa3887dcd9df5978dc566edf4a0aea2a9b1e" | ||||||
| @@ -2887,6 +2918,11 @@ | |||||||
|     "@sentry/utils" "6.19.7" |     "@sentry/utils" "6.19.7" | ||||||
|     tslib "^1.9.3" |     tslib "^1.9.3" | ||||||
|  |  | ||||||
|  | "@sinclair/typebox@^0.25.16": | ||||||
|  |   version "0.25.24" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" | ||||||
|  |   integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== | ||||||
|  |  | ||||||
| "@sindresorhus/slugify@1.1.0": | "@sindresorhus/slugify@1.1.0": | ||||||
|   version "1.1.0" |   version "1.1.0" | ||||||
|   resolved "https://registry.yarnpkg.com/@sindresorhus/slugify/-/slugify-1.1.0.tgz#2f195365d9b953384305b62664b44b4036c49430" |   resolved "https://registry.yarnpkg.com/@sindresorhus/slugify/-/slugify-1.1.0.tgz#2f195365d9b953384305b62664b44b4036c49430" | ||||||
| @@ -4701,6 +4737,14 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     "@types/istanbul-lib-report" "*" |     "@types/istanbul-lib-report" "*" | ||||||
|  |  | ||||||
|  | "@types/jest@^29.5.2": | ||||||
|  |   version "29.5.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.2.tgz#86b4afc86e3a8f3005b297ed8a72494f89e6395b" | ||||||
|  |   integrity sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg== | ||||||
|  |   dependencies: | ||||||
|  |     expect "^29.0.0" | ||||||
|  |     pretty-format "^29.0.0" | ||||||
|  |  | ||||||
| "@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": | "@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": | ||||||
|   version "7.0.11" |   version "7.0.11" | ||||||
|   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" |   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" | ||||||
| @@ -4904,6 +4948,13 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     "@types/yargs-parser" "*" |     "@types/yargs-parser" "*" | ||||||
|  |  | ||||||
|  | "@types/yargs@^17.0.8": | ||||||
|  |   version "17.0.24" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" | ||||||
|  |   integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/yargs-parser" "*" | ||||||
|  |  | ||||||
| "@typescript-eslint/experimental-utils@^5.3.0": | "@typescript-eslint/experimental-utils@^5.3.0": | ||||||
|   version "5.30.6" |   version "5.30.6" | ||||||
|   resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.30.6.tgz#fe30d2800aedcad7465d9260b66068337df71612" |   resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.30.6.tgz#fe30d2800aedcad7465d9260b66068337df71612" | ||||||
| @@ -5583,6 +5634,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     color-convert "^2.0.1" |     color-convert "^2.0.1" | ||||||
|  |  | ||||||
|  | ansi-styles@^5.0.0: | ||||||
|  |   version "5.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" | ||||||
|  |   integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== | ||||||
|  |  | ||||||
| ansi-to-html@^0.6.11: | ansi-to-html@^0.6.11: | ||||||
|   version "0.6.14" |   version "0.6.14" | ||||||
|   resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8" |   resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8" | ||||||
| @@ -6893,6 +6949,11 @@ ci-info@^2.0.0: | |||||||
|   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" |   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" | ||||||
|   integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== |   integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== | ||||||
|  |  | ||||||
|  | ci-info@^3.2.0: | ||||||
|  |   version "3.8.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" | ||||||
|  |   integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== | ||||||
|  |  | ||||||
| cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: | cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: | ||||||
|   version "1.0.4" |   version "1.0.4" | ||||||
|   resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" |   resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" | ||||||
| @@ -8045,6 +8106,11 @@ diff-sequences@^26.6.2: | |||||||
|   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" |   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" | ||||||
|   integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== |   integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== | ||||||
|  |  | ||||||
|  | diff-sequences@^29.4.3: | ||||||
|  |   version "29.4.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" | ||||||
|  |   integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== | ||||||
|  |  | ||||||
| diffie-hellman@^5.0.0: | diffie-hellman@^5.0.0: | ||||||
|   version "5.0.3" |   version "5.0.3" | ||||||
|   resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" |   resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" | ||||||
| @@ -8949,6 +9015,17 @@ expect@^26.6.2: | |||||||
|     jest-message-util "^26.6.2" |     jest-message-util "^26.6.2" | ||||||
|     jest-regex-util "^26.0.0" |     jest-regex-util "^26.0.0" | ||||||
|  |  | ||||||
|  | expect@^29.0.0: | ||||||
|  |   version "29.5.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/expect/-/expect-29.5.0.tgz#68c0509156cb2a0adb8865d413b137eeaae682f7" | ||||||
|  |   integrity sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg== | ||||||
|  |   dependencies: | ||||||
|  |     "@jest/expect-utils" "^29.5.0" | ||||||
|  |     jest-get-type "^29.4.3" | ||||||
|  |     jest-matcher-utils "^29.5.0" | ||||||
|  |     jest-message-util "^29.5.0" | ||||||
|  |     jest-util "^29.5.0" | ||||||
|  |  | ||||||
| express@^4.17.1: | express@^4.17.1: | ||||||
|   version "4.18.2" |   version "4.18.2" | ||||||
|   resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" |   resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" | ||||||
| @@ -11152,6 +11229,16 @@ jest-diff@^26.6.2: | |||||||
|     jest-get-type "^26.3.0" |     jest-get-type "^26.3.0" | ||||||
|     pretty-format "^26.6.2" |     pretty-format "^26.6.2" | ||||||
|  |  | ||||||
|  | jest-diff@^29.5.0: | ||||||
|  |   version "29.5.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.5.0.tgz#e0d83a58eb5451dcc1fa61b1c3ee4e8f5a290d63" | ||||||
|  |   integrity sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw== | ||||||
|  |   dependencies: | ||||||
|  |     chalk "^4.0.0" | ||||||
|  |     diff-sequences "^29.4.3" | ||||||
|  |     jest-get-type "^29.4.3" | ||||||
|  |     pretty-format "^29.5.0" | ||||||
|  |  | ||||||
| jest-docblock@^26.0.0: | jest-docblock@^26.0.0: | ||||||
|   version "26.0.0" |   version "26.0.0" | ||||||
|   resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" |   resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" | ||||||
| @@ -11205,6 +11292,11 @@ jest-get-type@^26.3.0: | |||||||
|   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" |   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" | ||||||
|   integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== |   integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== | ||||||
|  |  | ||||||
|  | jest-get-type@^29.4.3: | ||||||
|  |   version "29.4.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" | ||||||
|  |   integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== | ||||||
|  |  | ||||||
| jest-haste-map@^25.5.1: | jest-haste-map@^25.5.1: | ||||||
|   version "25.5.1" |   version "25.5.1" | ||||||
|   resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-25.5.1.tgz#1df10f716c1d94e60a1ebf7798c9fb3da2620943" |   resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-25.5.1.tgz#1df10f716c1d94e60a1ebf7798c9fb3da2620943" | ||||||
| @@ -11298,6 +11390,16 @@ jest-matcher-utils@^26.6.2: | |||||||
|     jest-get-type "^26.3.0" |     jest-get-type "^26.3.0" | ||||||
|     pretty-format "^26.6.2" |     pretty-format "^26.6.2" | ||||||
|  |  | ||||||
|  | jest-matcher-utils@^29.5.0: | ||||||
|  |   version "29.5.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz#d957af7f8c0692c5453666705621ad4abc2c59c5" | ||||||
|  |   integrity sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw== | ||||||
|  |   dependencies: | ||||||
|  |     chalk "^4.0.0" | ||||||
|  |     jest-diff "^29.5.0" | ||||||
|  |     jest-get-type "^29.4.3" | ||||||
|  |     pretty-format "^29.5.0" | ||||||
|  |  | ||||||
| jest-message-util@^26.6.2: | jest-message-util@^26.6.2: | ||||||
|   version "26.6.2" |   version "26.6.2" | ||||||
|   resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" |   resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" | ||||||
| @@ -11313,6 +11415,21 @@ jest-message-util@^26.6.2: | |||||||
|     slash "^3.0.0" |     slash "^3.0.0" | ||||||
|     stack-utils "^2.0.2" |     stack-utils "^2.0.2" | ||||||
|  |  | ||||||
|  | jest-message-util@^29.5.0: | ||||||
|  |   version "29.5.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.5.0.tgz#1f776cac3aca332ab8dd2e3b41625435085c900e" | ||||||
|  |   integrity sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA== | ||||||
|  |   dependencies: | ||||||
|  |     "@babel/code-frame" "^7.12.13" | ||||||
|  |     "@jest/types" "^29.5.0" | ||||||
|  |     "@types/stack-utils" "^2.0.0" | ||||||
|  |     chalk "^4.0.0" | ||||||
|  |     graceful-fs "^4.2.9" | ||||||
|  |     micromatch "^4.0.4" | ||||||
|  |     pretty-format "^29.5.0" | ||||||
|  |     slash "^3.0.0" | ||||||
|  |     stack-utils "^2.0.3" | ||||||
|  |  | ||||||
| jest-mock@^26.6.2: | jest-mock@^26.6.2: | ||||||
|   version "26.6.2" |   version "26.6.2" | ||||||
|   resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" |   resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" | ||||||
| @@ -11490,6 +11607,18 @@ jest-util@^26.6.2: | |||||||
|     is-ci "^2.0.0" |     is-ci "^2.0.0" | ||||||
|     micromatch "^4.0.2" |     micromatch "^4.0.2" | ||||||
|  |  | ||||||
|  | jest-util@^29.5.0: | ||||||
|  |   version "29.5.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" | ||||||
|  |   integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@jest/types" "^29.5.0" | ||||||
|  |     "@types/node" "*" | ||||||
|  |     chalk "^4.0.0" | ||||||
|  |     ci-info "^3.2.0" | ||||||
|  |     graceful-fs "^4.2.9" | ||||||
|  |     picomatch "^2.2.3" | ||||||
|  |  | ||||||
| jest-validate@^26.6.2: | jest-validate@^26.6.2: | ||||||
|   version "26.6.2" |   version "26.6.2" | ||||||
|   resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" |   resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" | ||||||
| @@ -14546,6 +14675,15 @@ pretty-format@^26.6.2: | |||||||
|     ansi-styles "^4.0.0" |     ansi-styles "^4.0.0" | ||||||
|     react-is "^17.0.1" |     react-is "^17.0.1" | ||||||
|  |  | ||||||
|  | pretty-format@^29.0.0, pretty-format@^29.5.0: | ||||||
|  |   version "29.5.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.5.0.tgz#283134e74f70e2e3e7229336de0e4fce94ccde5a" | ||||||
|  |   integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw== | ||||||
|  |   dependencies: | ||||||
|  |     "@jest/schemas" "^29.4.3" | ||||||
|  |     ansi-styles "^5.0.0" | ||||||
|  |     react-is "^18.0.0" | ||||||
|  |  | ||||||
| pretty-hrtime@^1.0.3: | pretty-hrtime@^1.0.3: | ||||||
|   version "1.0.3" |   version "1.0.3" | ||||||
|   resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" |   resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" | ||||||
| @@ -15133,6 +15271,11 @@ react-is@^17.0.1: | |||||||
|   resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" |   resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" | ||||||
|   integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== |   integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== | ||||||
|  |  | ||||||
|  | react-is@^18.0.0: | ||||||
|  |   version "18.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" | ||||||
|  |   integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== | ||||||
|  |  | ||||||
| react-syntax-highlighter@^15.4.5: | react-syntax-highlighter@^15.4.5: | ||||||
|   version "15.5.0" |   version "15.5.0" | ||||||
|   resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20" |   resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20" | ||||||
| @@ -16451,6 +16594,13 @@ stack-utils@^2.0.2: | |||||||
|   dependencies: |   dependencies: | ||||||
|     escape-string-regexp "^2.0.0" |     escape-string-regexp "^2.0.0" | ||||||
|  |  | ||||||
|  | stack-utils@^2.0.3: | ||||||
|  |   version "2.0.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" | ||||||
|  |   integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== | ||||||
|  |   dependencies: | ||||||
|  |     escape-string-regexp "^2.0.0" | ||||||
|  |  | ||||||
| state-toggle@^1.0.0: | state-toggle@^1.0.0: | ||||||
|   version "1.0.3" |   version "1.0.3" | ||||||
|   resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" |   resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Shivam Mishra
					Shivam Mishra