mirror of
				https://github.com/Fabio286/antares.git
				synced 2025-06-05 21:59:22 +02:00 
			
		
		
		
	feat: new notes system
This commit is contained in:
		| @@ -54,7 +54,7 @@ const updateWindow = () => { | ||||
|    const totalScrollHeight = props.items.length * props.itemHeight; | ||||
|    const offset = 50; | ||||
|  | ||||
|    const scrollTop = localScrollElement.value.scrollTop; | ||||
|    const scrollTop = localScrollElement.value?.scrollTop; | ||||
|  | ||||
|    const firstVisibleIndex = Math.floor(scrollTop / props.itemHeight); | ||||
|    const lastVisibleIndex = firstVisibleIndex + visibleItemsCount; | ||||
|   | ||||
| @@ -163,7 +163,7 @@ const localSearchTerm = ref(''); | ||||
|  | ||||
| const connectionName = computed(() => getConnectionName(props.connection.uid)); | ||||
| const history: ComputedRef<HistoryRecord[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || [])); | ||||
| const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(searchTerm.value.toLowerCase()) >= 0)); | ||||
| const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(localSearchTerm.value.toLowerCase()) >= 0)); | ||||
|  | ||||
| watch(searchTerm, () => { | ||||
|    clearTimeout(searchTermInterval.value); | ||||
|   | ||||
| @@ -1,5 +1,11 @@ | ||||
| <template> | ||||
|    <ConfirmModal size="400"> | ||||
|    <ConfirmModal | ||||
|       size="medium" | ||||
|       :disable-autofocus="true" | ||||
|       :close-on-confirm="!!newNote.note.length" | ||||
|       @confirm="createNote" | ||||
|       @hide="$emit('hide')" | ||||
|    > | ||||
|       <template #header> | ||||
|          <div class="d-flex"> | ||||
|             <BaseIcon | ||||
| @@ -11,17 +17,6 @@ | ||||
|       </template> | ||||
|       <template #body> | ||||
|          <form class="form"> | ||||
|             <div class="form-group columns"> | ||||
|                <div class="column col-12"> | ||||
|                   <label class="form-label">{{ t('general.title') }}</label> | ||||
|                   <input | ||||
|                      ref="firstInput" | ||||
|                      v-model="newNote.title" | ||||
|                      class="form-input" | ||||
|                      type="text" | ||||
|                   > | ||||
|                </div> | ||||
|             </div> | ||||
|             <div class="form-group columns"> | ||||
|                <div class="column col-8"> | ||||
|                   <label class="form-label">{{ t('connection.connection') }}</label> | ||||
| @@ -48,7 +43,11 @@ | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                <label class="form-label">{{ t('general.content') }}</label> | ||||
|                <BaseTextEditor :mode="editorMode" :show-line-numbers="false" /> | ||||
|                <BaseTextEditor | ||||
|                   v-model="newNote.note" | ||||
|                   :mode="editorMode" | ||||
|                   :show-line-numbers="false" | ||||
|                /> | ||||
|             </div> | ||||
|          </form> | ||||
|       </template> | ||||
| @@ -64,12 +63,18 @@ import ConfirmModal from '@/components/BaseConfirmModal.vue'; | ||||
| import BaseIcon from '@/components/BaseIcon.vue'; | ||||
| import BaseSelect from '@/components/BaseSelect.vue'; | ||||
| import BaseTextEditor from '@/components/BaseTextEditor.vue'; | ||||
| import { ConnectionNote, TagCode } from '@/stores/scratchpad'; | ||||
| import { ConnectionNote, TagCode, useScratchpadStore } from '@/stores/scratchpad'; | ||||
|  | ||||
| const { t } = useI18n(); | ||||
| const { addNote } = useScratchpadStore(); | ||||
|  | ||||
| const emit = defineEmits(['hide']); | ||||
|  | ||||
| const noteTags = inject<{code: TagCode; name: string}[]>('noteTags'); | ||||
| const selectedConnection = inject<Ref<null | string>>('selectedConnection'); | ||||
| const selectedTag = inject<Ref<TagCode>>('selectedTag'); | ||||
| const connectionOptions = inject<{code: string; name: string}[]>('connectionOptions'); | ||||
|  | ||||
| const editorMode = ref('markdown'); | ||||
|  | ||||
| const newNote: Ref<ConnectionNote> = ref({ | ||||
| @@ -78,9 +83,21 @@ const newNote: Ref<ConnectionNote> = ref({ | ||||
|    title: undefined, | ||||
|    note: '', | ||||
|    date: new Date(), | ||||
|    type: 'note' | ||||
|    type: 'note', | ||||
|    isArchived: false | ||||
| }); | ||||
|  | ||||
| const createNote = () => { | ||||
|    if (newNote.value.note) { | ||||
|       if (!newNote.value.title)// Set a default title | ||||
|          newNote.value.title = `${newNote.value.type.toLocaleUpperCase()}: ${newNote.value.uid}`; | ||||
|  | ||||
|       newNote.value.date = new Date(); | ||||
|       addNote(newNote.value); | ||||
|       emit('hide'); | ||||
|    } | ||||
| }; | ||||
|  | ||||
| watch(() => newNote.value.type, () => { | ||||
|    if (newNote.value.type === 'query') | ||||
|       editorMode.value = 'sql'; | ||||
| @@ -88,4 +105,9 @@ watch(() => newNote.value.type, () => { | ||||
|       editorMode.value = 'markdown'; | ||||
| }); | ||||
|  | ||||
| newNote.value.cUid = selectedConnection.value; | ||||
|  | ||||
| if (selectedTag.value !== 'all') | ||||
|    newNote.value.type = selectedTag.value; | ||||
|  | ||||
| </script> | ||||
|   | ||||
							
								
								
									
										328
									
								
								src/renderer/components/ScratchpadNote.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								src/renderer/components/ScratchpadNote.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,328 @@ | ||||
| <template> | ||||
|    <div | ||||
|       class="tile my-2" | ||||
|       tabindex="0" | ||||
|       @click="$emit('select-note', note.uid)" | ||||
|    > | ||||
|       <BaseIcon | ||||
|          v-if="isBig" | ||||
|          class="tile-compress c-hand" | ||||
|          :icon-name="isSelected ? 'mdiChevronUp' : 'mdiChevronDown'" | ||||
|          :size="36" | ||||
|          @click.stop="$emit('toggle-note', note.uid)" | ||||
|       /> | ||||
|       <div class="tile-icon"> | ||||
|          <BaseIcon | ||||
|             :icon-name="note.type === 'query' | ||||
|                ? 'mdiStarOutline' | ||||
|                : note.type === 'todo' | ||||
|                   ? note.isArchived | ||||
|                      ? 'mdiCheckboxMarkedOutline' | ||||
|                      : 'mdiCheckboxBlankOutline' | ||||
|                   : 'mdiNoteEditOutline'" | ||||
|             :size="36" | ||||
|          /> | ||||
|          <div class="tile-icon-type"> | ||||
|             {{ note.type }} | ||||
|          </div> | ||||
|       </div> | ||||
|       <div class="tile-content"> | ||||
|          <div class="tile-content-message" :class="[{'opened': isSelected}]"> | ||||
|             <code | ||||
|                v-if="note.type === 'query'" | ||||
|                class="cut-text" | ||||
|                v-html="highlightWord(note.note)" | ||||
|             /> | ||||
|             <div | ||||
|                v-else | ||||
|                ref="noteParagraph" | ||||
|                class="tile-paragraph" | ||||
|                v-html="parseMarkdown(highlightWord(note.note))" | ||||
|             /> | ||||
|             <div v-if="isBig && !isSelected" class="tile-paragraph-overlay" /> | ||||
|          </div> | ||||
|          <div class="tile-bottom-content"> | ||||
|             <small class="tile-subtitle">{{ getConnectionName(note.cUid) || t('general.all') }} · {{ formatDate(note.date) }}</small> | ||||
|             <div class="tile-history-buttons"> | ||||
|                <button | ||||
|                   v-if="note.type === 'todo' && !note.isArchived" | ||||
|                   class="btn btn-link pl-1" | ||||
|                   @click="$emit('archive-note', note.uid)" | ||||
|                > | ||||
|                   <BaseIcon | ||||
|                      icon-name="mdiCheck" | ||||
|                      class="pr-1" | ||||
|                      :size="22" | ||||
|                   /> {{ t('general.archive') }} | ||||
|                </button> | ||||
|                <button | ||||
|                   v-if="note.type === 'todo' && note.isArchived" | ||||
|                   class="btn btn-link pl-1" | ||||
|                   @click="$emit('restore-note', note.uid)" | ||||
|                > | ||||
|                   <BaseIcon | ||||
|                      icon-name="mdiRestore" | ||||
|                      class="pr-1" | ||||
|                      :size="22" | ||||
|                   /> {{ t('general.undo') }} | ||||
|                </button> | ||||
|                <button | ||||
|                   v-if="note.type === 'query'" | ||||
|                   class="btn btn-link pl-1" | ||||
|                   @click="copyText(note.note)" | ||||
|                > | ||||
|                   <BaseIcon | ||||
|                      icon-name="mdiContentCopy" | ||||
|                      class="pr-1" | ||||
|                      :size="22" | ||||
|                   /> {{ t('general.copy') }} | ||||
|                </button> | ||||
|                <button | ||||
|                   v-if=" !note.isArchived" | ||||
|                   class="btn btn-link pl-1" | ||||
|                   @click="null" | ||||
|                > | ||||
|                   <BaseIcon | ||||
|                      icon-name="mdiPencil" | ||||
|                      class="pr-1" | ||||
|                      :size="22" | ||||
|                   /> {{ t('general.edit') }} | ||||
|                </button> | ||||
|                <button class="btn btn-link pl-1" @click="$emit('delete-note', note.uid)"> | ||||
|                   <BaseIcon | ||||
|                      icon-name="mdiDeleteForever" | ||||
|                      class="pr-1" | ||||
|                      :size="22" | ||||
|                   /> {{ t('general.delete') }} | ||||
|                </button> | ||||
|             </div> | ||||
|          </div> | ||||
|       </div> | ||||
|    </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { useElementBounding } from '@vueuse/core'; | ||||
| import { marked } from 'marked'; | ||||
| import { computed, PropType, Ref, ref } from 'vue'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
|  | ||||
| import BaseIcon from '@/components/BaseIcon.vue'; | ||||
| import { useFilters } from '@/composables/useFilters'; | ||||
| import { copyText } from '@/libs/copyText'; | ||||
| import { useConnectionsStore } from '@/stores/connections'; | ||||
| import { ConnectionNote } from '@/stores/scratchpad'; | ||||
|  | ||||
| const props = defineProps({ | ||||
|    note: { | ||||
|       type: Object as PropType<ConnectionNote>, | ||||
|       required: true | ||||
|    }, | ||||
|    searchTerm: { | ||||
|       type: String, | ||||
|       default: () => '' | ||||
|    }, | ||||
|    selectedNote: { | ||||
|       type: String, | ||||
|       default: () => '' | ||||
|    } | ||||
| }); | ||||
|  | ||||
| const { t } = useI18n(); | ||||
| const { formatDate } = useFilters(); | ||||
| const { getConnectionName } = useConnectionsStore(); | ||||
|  | ||||
| defineEmits(['delete-note', 'select-note', 'toggle-note', 'archive-note', 'restore-note']); | ||||
|  | ||||
| const noteParagraph: Ref<HTMLDivElement> = ref(null); | ||||
| const noteHeight = ref(useElementBounding(noteParagraph)?.height); | ||||
|  | ||||
| const isSelected = computed(() => props.selectedNote === props.note.uid); | ||||
| const isBig = computed(() => noteHeight.value > 75); | ||||
|  | ||||
| const parseMarkdown = (text: string) => { | ||||
|    const renderer = { | ||||
|       listitem (text: string) { | ||||
|          return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`; | ||||
|       }, | ||||
|       link (href: string, title: string, text: string) { | ||||
|          return `<a>${text}</a>`; | ||||
|       } | ||||
|    }; | ||||
|  | ||||
|    marked.use({ renderer }); | ||||
|  | ||||
|    return marked(text); | ||||
| }; | ||||
|  | ||||
| const highlightWord = (string: string) => { | ||||
|    string = string.replaceAll('<', '<').replaceAll('>', '>'); | ||||
|  | ||||
|    if (props.searchTerm) { | ||||
|       const regexp = new RegExp(`(${props.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); | ||||
|       return string.replace(regexp, '<span class="text-primary text-bold">$1</span>'); | ||||
|    } | ||||
|    else | ||||
|       return string; | ||||
| }; | ||||
| </script> | ||||
| <style scoped lang="scss"> | ||||
| .tile { | ||||
|    border-radius: $border-radius; | ||||
|    display: flex; | ||||
|    position: relative; | ||||
|    transition: none; | ||||
|  | ||||
|    &:hover, | ||||
|    &:focus { | ||||
|      .tile-content { | ||||
|        .tile-bottom-content { | ||||
|          .tile-history-buttons { | ||||
|            opacity: 1; | ||||
|          } | ||||
|        } | ||||
|      } | ||||
|    } | ||||
|  | ||||
|    .tile-compress { | ||||
|       position: absolute; | ||||
|       right: 2px; | ||||
|       top: 0px; | ||||
|       opacity: .7; | ||||
|       z-index: 9; | ||||
|    } | ||||
|  | ||||
|    .tile-icon { | ||||
|      font-size: 1.2rem; | ||||
|      margin-left: 0.3rem; | ||||
|      margin-right: 0.3rem; | ||||
|      margin-top: 0.6rem; | ||||
|      width: 40px; | ||||
|      display: flex; | ||||
|      align-items: center; | ||||
|      flex-direction: column; | ||||
|      justify-content: center; | ||||
|      opacity: .8; | ||||
|  | ||||
|      .tile-icon-type { | ||||
|        text-transform: uppercase; | ||||
|        font-size: .5rem; | ||||
|      } | ||||
|    } | ||||
|  | ||||
|    .tile-content { | ||||
|      padding: 0.3rem; | ||||
|      padding-left: 0.1rem; | ||||
|      min-height: 75px; | ||||
|      display: flex; | ||||
|      flex-direction: column; | ||||
|      justify-content: space-between; | ||||
|  | ||||
|      .tile-content-message{ | ||||
|          position: relative; | ||||
|  | ||||
|          &:not(.opened) { | ||||
|             max-height: 36px; | ||||
|             overflow: hidden; | ||||
|          } | ||||
|  | ||||
|          .tile-paragraph-overlay { | ||||
|             height: 36px; | ||||
|             width: 100%; | ||||
|             position: absolute; | ||||
|             top: 0; | ||||
|          } | ||||
|      } | ||||
|  | ||||
|      code { | ||||
|        max-width: 100%; | ||||
|        display: inline-block; | ||||
|        font-size: 100%; | ||||
|        // color: $primary-color; | ||||
|        opacity: 0.8; | ||||
|        font-weight: 600; | ||||
|      } | ||||
|  | ||||
|      .tile-subtitle { | ||||
|        opacity: 0.8; | ||||
|      } | ||||
|  | ||||
|      .tile-bottom-content { | ||||
|        display: flex; | ||||
|        justify-content: space-between; | ||||
|  | ||||
|        .tile-history-buttons { | ||||
|           opacity: 0; | ||||
|           transition: opacity 0.2s; | ||||
|  | ||||
|           button { | ||||
|              font-size: 0.7rem; | ||||
|              height: 1rem; | ||||
|              line-height: 1rem; | ||||
|              display: inline-flex; | ||||
|              align-items: center; | ||||
|              justify-content: center; | ||||
|           } | ||||
|        } | ||||
|      } | ||||
|    } | ||||
|  } | ||||
|  | ||||
|  .theme-dark { | ||||
|    .tile { | ||||
|       .tile-paragraph-overlay { | ||||
|          background-image: linear-gradient( | ||||
|             to bottom, | ||||
|             rgba(255,0,0,0) 70%, | ||||
|             $body-bg-dark); | ||||
|       } | ||||
|  | ||||
|       &:focus { | ||||
|          .tile-paragraph-overlay { | ||||
|             background-image: linear-gradient( | ||||
|                to bottom, | ||||
|                rgba(255,0,0,0)70%, | ||||
|                #323232); | ||||
|          } | ||||
|       } | ||||
|  | ||||
|       &:hover{ | ||||
|          .tile-paragraph-overlay { | ||||
|             background-image: linear-gradient( | ||||
|                to bottom, | ||||
|                rgba(255,0,0,0) 70%, | ||||
|                $bg-color-light-dark); | ||||
|          } | ||||
|       } | ||||
|    } | ||||
|  } | ||||
|  | ||||
|  .theme-light { | ||||
|    .tile { | ||||
|       .tile-paragraph-overlay { | ||||
|          background-image: linear-gradient( | ||||
|             to bottom, | ||||
|             rgba(255,0,0,0) 70%, | ||||
|             #FFFF); | ||||
|       } | ||||
|  | ||||
|       &:hover, | ||||
|       &:focus { | ||||
|          .tile-paragraph-overlay { | ||||
|             background-image: linear-gradient( | ||||
|                to bottom, | ||||
|                rgba(255,0,0,0) 70%, | ||||
|                $bg-color-light-gray); | ||||
|          } | ||||
|       } | ||||
|    } | ||||
|  } | ||||
|  </style> | ||||
|  <style lang="scss"> | ||||
|  .tile-paragraph { | ||||
|    white-space: initial; | ||||
|  | ||||
|     h1, h2, h3, h4, h5, h6, p, li { | ||||
|        margin: 0; | ||||
|     } | ||||
|  } | ||||
|  </style> | ||||
| @@ -1,176 +1,144 @@ | ||||
| <template> | ||||
|    <ConfirmModal | ||||
|       :confirm-text="t('application.update')" | ||||
|       :cancel-text="t('general.close')" | ||||
|       size="medium" | ||||
|       :hide-footer="true" | ||||
|       :disable-autofocus="true" | ||||
|       @hide="hideScratchpad" | ||||
|    > | ||||
|       <template #header> | ||||
|          <div class="d-flex"> | ||||
|             <BaseIcon | ||||
|                icon-name="mdiNotebookOutline" | ||||
|                class="mr-1" | ||||
|                :size="24" | ||||
|             /> {{ t('application.note', 2) }} | ||||
|          </div> | ||||
|       </template> | ||||
|       <template #body> | ||||
|          <div class="p-relative"> | ||||
|             <div class="d-flex p-vcentered" style="gap: 0 10px"> | ||||
|                <div style="flex: 1;"> | ||||
|                   <BaseSelect | ||||
|                      v-model="localConnection" | ||||
|                      class="form-select" | ||||
|                      :options="connectionOptions" | ||||
|                      option-track-by="code" | ||||
|                      option-label="name" | ||||
|                      @change="null" | ||||
|                   /> | ||||
|                </div> | ||||
|                <div class="btn-group btn-group-block text-uppercase"> | ||||
|                   <div | ||||
|                      v-for="tag in [{ code: 'all', name: t('general.all') }, ...noteTags]" | ||||
|                      :key="tag.code" | ||||
|                      class="btn" | ||||
|                      :class="[selectedTab === tag.code ? 'btn-primary': 'btn-dark']" | ||||
|                      @click="selectedTab = tag.code" | ||||
|                   > | ||||
|                      {{ tag.name }} | ||||
|    <Teleport to="#window-content"> | ||||
|       <div class="modal active"> | ||||
|          <a class="modal-overlay" @click.stop="hideScratchpad" /> | ||||
|          <div ref="trapRef" class="modal-container p-0 pb-4"> | ||||
|             <div class="modal-header pl-2"> | ||||
|                <div class="modal-title h6"> | ||||
|                   <div class="d-flex"> | ||||
|                      <BaseIcon | ||||
|                         icon-name="mdiNotebookOutline" | ||||
|                         class="mr-1" | ||||
|                         :size="24" | ||||
|                      /> | ||||
|                      <span>{{ t('application.note', 2) }}</span> | ||||
|                   </div> | ||||
|                </div> | ||||
|                <div class=""> | ||||
|                <a class="btn btn-clear c-hand" @click.stop="hideScratchpad" /> | ||||
|             </div> | ||||
|             <div class="modal-body p-0 workspace-query-results"> | ||||
|                <div | ||||
|                   ref="noteFilters" | ||||
|                   class="d-flex p-vcentered p-2" | ||||
|                   style="gap: 0 10px" | ||||
|                > | ||||
|                   <div style="flex: 1;"> | ||||
|                      <BaseSelect | ||||
|                         v-model="localConnection" | ||||
|                         class="form-select" | ||||
|                         :options="connectionOptions" | ||||
|                         option-track-by="code" | ||||
|                         option-label="name" | ||||
|                         @change="null" | ||||
|                      /> | ||||
|                   </div> | ||||
|                   <div class="btn-group btn-group-block text-uppercase"> | ||||
|                      <div | ||||
|                         v-for="tag in [{ code: 'all', name: t('general.all') }, ...noteTags]" | ||||
|                         :key="tag.code" | ||||
|                         class="btn" | ||||
|                         :class="[selectedTag === tag.code ? 'btn-primary': 'btn-dark']" | ||||
|                         @click="setTag(tag.code)" | ||||
|                      > | ||||
|                         {{ tag.name }} | ||||
|                      </div> | ||||
|                   </div> | ||||
|                   <div class=""> | ||||
|                      <div | ||||
|                         class="btn px-1 tooltip tooltip-left s-rounded archived-button" | ||||
|                         :class="[showArchived ? 'btn-primary' : 'btn-link']" | ||||
|                         :data-tooltip="showArchived ? t('application.hideArchivedNotes') : t('application.showArchivedNotes')" | ||||
|                         @click="showArchived = !showArchived" | ||||
|                      > | ||||
|                         <BaseIcon | ||||
|                            :icon-name="!showArchived ? 'mdiArchiveEyeOutline' : 'mdiArchiveCancelOutline'" | ||||
|                            class="" | ||||
|                            :size="24" | ||||
|                         /> | ||||
|                      </div> | ||||
|                   </div> | ||||
|                </div> | ||||
|                <div> | ||||
|                   <div | ||||
|                      class="btn px-1 tooltip tooltip-left s-rounded archived-button" | ||||
|                      :class="[showArchived ? 'btn-primary' : 'btn-link']" | ||||
|                      :data-tooltip="showArchived ? t('application.hideArchivedNotes') : t('application.showArchivedNotes')" | ||||
|                      @click="showArchived = !showArchived" | ||||
|                      v-show="filteredNotes.length || searchTerm.length" | ||||
|                      ref="searchForm" | ||||
|                      class="form-group has-icon-right m-0 p-2" | ||||
|                   > | ||||
|                      <input | ||||
|                         v-model="searchTerm" | ||||
|                         class="form-input" | ||||
|                         type="text" | ||||
|                         :placeholder="t('general.search')" | ||||
|                      > | ||||
|                      <BaseIcon | ||||
|                         :icon-name="!showArchived ? 'mdiArchiveEyeOutline' : 'mdiArchiveCancelOutline'" | ||||
|                         class="" | ||||
|                         :size="24" | ||||
|                         v-if="!searchTerm" | ||||
|                         icon-name="mdiMagnify" | ||||
|                         class="form-icon pr-2" | ||||
|                         :size="18" | ||||
|                      /> | ||||
|                      <BaseIcon | ||||
|                         v-else | ||||
|                         icon-name="mdiBackspace" | ||||
|                         class="form-icon c-hand pr-2" | ||||
|                         :size="18" | ||||
|                         @click="searchTerm = ''" | ||||
|                      /> | ||||
|                   </div> | ||||
|                </div> | ||||
|             </div> | ||||
|             <div> | ||||
|  | ||||
|                <div | ||||
|                   v-if="filteredNotes.length" | ||||
|                   ref="searchForm" | ||||
|                   class="form-group has-icon-right m-0" | ||||
|                   ref="tableWrapper" | ||||
|                   class="vscroll px-2" | ||||
|                   :style="{'height': resultsSize+'px'}" | ||||
|                > | ||||
|                   <div ref="table"> | ||||
|                      <BaseVirtualScroll | ||||
|                         ref="resultTable" | ||||
|                         :items="filteredNotes" | ||||
|                         :item-height="83" | ||||
|                         :visible-height="resultsSize" | ||||
|                         :scroll-element="scrollElement" | ||||
|                      > | ||||
|                         <template #default="{ items }"> | ||||
|                            <ScratchpadNote | ||||
|                               v-for="note in items" | ||||
|                               :key="note.uid" | ||||
|                               :search-term="searchTerm" | ||||
|                               :note="note" | ||||
|                               :selected-note="selectedNote" | ||||
|                               @select-note="selectedNote = note.uid" | ||||
|                               @toggle-note="toggleNote" | ||||
|                               @delete-note="deleteNote" | ||||
|                               @archive-note="archiveNote" | ||||
|                               @restore-note="restoreNote" | ||||
|                            /> | ||||
|                         </template> | ||||
|                      </BaseVirtualScroll> | ||||
|                   </div> | ||||
|                </div> | ||||
|                <div v-else class="empty"> | ||||
|                   <div class="empty-icon"> | ||||
|                      <BaseIcon icon-name="mdiNoteSearch" :size="48" /> | ||||
|                   </div> | ||||
|                   <p class="empty-title h5"> | ||||
|                      {{ t('application.thereAreNoNotesYet') }} | ||||
|                   </p> | ||||
|                </div> | ||||
|                <div | ||||
|                   class="btn btn-primary p-0 add-button tooltip tooltip-left" | ||||
|                   :data-tooltip="t('application.addNote')" | ||||
|                   @click="isAddModal = true" | ||||
|                > | ||||
|                   <input | ||||
|                      v-model="searchTerm" | ||||
|                      class="form-input" | ||||
|                      type="text" | ||||
|                      :placeholder="t('general.search')" | ||||
|                   > | ||||
|                   <BaseIcon | ||||
|                      v-if="!searchTerm" | ||||
|                      icon-name="mdiMagnify" | ||||
|                      class="form-icon pr-2" | ||||
|                      :size="18" | ||||
|                   /> | ||||
|                   <BaseIcon | ||||
|                      v-else | ||||
|                      icon-name="mdiBackspace" | ||||
|                      class="form-icon c-hand pr-2" | ||||
|                      :size="18" | ||||
|                      @click="searchTerm = ''" | ||||
|                      icon-name="mdiPlus" | ||||
|                      :size="48" | ||||
|                   /> | ||||
|                </div> | ||||
|             </div> | ||||
|             <div | ||||
|                v-if="connectionNotes.length" | ||||
|                ref="tableWrapper" | ||||
|                class="vscroll px-1" | ||||
|                :style="{'height': resultsSize+'px'}" | ||||
|             > | ||||
|                <div ref="table"> | ||||
|                   <BaseVirtualScroll | ||||
|                      ref="resultTable" | ||||
|                      :items="filteredNotes" | ||||
|                      :item-height="66" | ||||
|                      :visible-height="resultsSize" | ||||
|                      :scroll-element="scrollElement" | ||||
|                   > | ||||
|                      <template #default="{ items }"> | ||||
|                         <div | ||||
|                            v-for="note in items" | ||||
|                            :key="note.uid" | ||||
|                            class="tile my-2" | ||||
|                            tabindex="0" | ||||
|                         > | ||||
|                            <div class="tile-icon"> | ||||
|                               <BaseIcon | ||||
|                                  icon-name="mdiCodeTags" | ||||
|                                  class="pr-1" | ||||
|                                  :size="24" | ||||
|                               /> | ||||
|                            </div> | ||||
|                            <div class="tile-content"> | ||||
|                               <div class="tile-title"> | ||||
|                                  <code | ||||
|                                     class="cut-text" | ||||
|                                     :title="note.note" | ||||
|                                     v-html="highlightWord(note.note)" | ||||
|                                  /> | ||||
|                               </div> | ||||
|                               <div class="tile-bottom-content"> | ||||
|                                  <!-- <small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small> | ||||
|                                  <div class="tile-history-buttons"> | ||||
|                                     <button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)"> | ||||
|                                        <BaseIcon | ||||
|                                           icon-name="mdiOpenInApp" | ||||
|                                           class="pr-1" | ||||
|                                           :size="22" | ||||
|                                        /> {{ t('general.select') }} | ||||
|                                     </button> | ||||
|                                     <button class="btn btn-link pl-1" @click="copyQuery(query.sql)"> | ||||
|                                        <BaseIcon | ||||
|                                           icon-name="mdiContentCopy" | ||||
|                                           class="pr-1" | ||||
|                                           :size="22" | ||||
|                                        /> {{ t('general.copy') }} | ||||
|                                     </button> | ||||
|                                     <button class="btn btn-link pl-1" @click="deleteQuery(query)"> | ||||
|                                        <BaseIcon | ||||
|                                           icon-name="mdiDeleteForever" | ||||
|                                           class="pr-1" | ||||
|                                           :size="22" | ||||
|                                        /> {{ t('general.delete') }} | ||||
|                                     </button> | ||||
|                                  </div> --> | ||||
|                               </div> | ||||
|                            </div> | ||||
|                         </div> | ||||
|                      </template> | ||||
|                   </BaseVirtualScroll> | ||||
|                </div> | ||||
|             </div> | ||||
|             <div v-else class="empty"> | ||||
|                <div class="empty-icon"> | ||||
|                   <BaseIcon icon-name="mdiNoteSearch" :size="48" /> | ||||
|                </div> | ||||
|                <p class="empty-title h5"> | ||||
|                   {{ t('application.thereAreNoNotesYet') }} | ||||
|                </p> | ||||
|             </div> | ||||
|             <div | ||||
|                class="btn btn-primary p-0 add-button p-absolute tooltip tooltip-left" | ||||
|                :data-tooltip="t('application.addNote')" | ||||
|                @click="isAddModal = true" | ||||
|             > | ||||
|                <BaseIcon | ||||
|                   icon-name="mdiPlus" | ||||
|                   :size="48" | ||||
|                /> | ||||
|             </div> | ||||
|          </div> | ||||
|       </template> | ||||
|    </ConfirmModal> | ||||
|       </div> | ||||
|    </Teleport> | ||||
|    <ModalNewNote v-if="isAddModal" @hide="isAddModal = false" /> | ||||
| </template> | ||||
|  | ||||
| @@ -185,18 +153,20 @@ import { | ||||
|    onUpdated, | ||||
|    provide, | ||||
|    Ref, | ||||
|    ref | ||||
|    ref, | ||||
|    watch | ||||
| } from 'vue'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
|  | ||||
| import ConfirmModal from '@/components/BaseConfirmModal.vue'; | ||||
| import BaseIcon from '@/components/BaseIcon.vue'; | ||||
| import BaseSelect from '@/components/BaseSelect.vue'; | ||||
| import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue'; | ||||
| import ModalNewNote from '@/components/ModalNewNote.vue'; | ||||
| import ScratchpadNote from '@/components/ScratchpadNote.vue'; | ||||
| import { useApplicationStore } from '@/stores/application'; | ||||
| import { useConnectionsStore } from '@/stores/connections'; | ||||
| import { TagCode, useScratchpadStore } from '@/stores/scratchpad'; | ||||
| import { useWorkspacesStore } from '@/stores/workspaces'; | ||||
|  | ||||
| const { t } = useI18n(); | ||||
|  | ||||
| @@ -208,14 +178,15 @@ const { changeNotes } = scratchpadStore; | ||||
| const { hideScratchpad } = applicationStore; | ||||
| const { getConnectionName } = useConnectionsStore(); | ||||
| const { connections } = storeToRefs(useConnectionsStore()); | ||||
| const { getSelected: selectedWorkspace } = storeToRefs(useWorkspacesStore()); | ||||
|  | ||||
| const localConnection = ref(null); | ||||
| const table: Ref<HTMLDivElement> = ref(null); | ||||
| const resultTable: Ref<Component & { updateWindow: () => void }> = ref(null); | ||||
| const tableWrapper: Ref<HTMLDivElement> = ref(null); | ||||
| const noteFilters: Ref<HTMLInputElement> = ref(null); | ||||
| const searchForm: Ref<HTMLInputElement> = ref(null); | ||||
| const resultsSize = ref(1000); | ||||
| const localNotes = ref(connectionNotes.value); | ||||
| const searchTermInterval: Ref<NodeJS.Timeout> = ref(null); | ||||
| const scrollElement: Ref<HTMLDivElement> = ref(null); | ||||
| const searchTerm = ref(''); | ||||
| @@ -223,14 +194,20 @@ const localSearchTerm = ref(''); | ||||
| const showArchived = ref(false); | ||||
| const isAddModal = ref(false); | ||||
| const isEditModal = ref(false); | ||||
| const selectedTab = ref('all'); | ||||
| const selectedTag = ref('all'); | ||||
| const selectedNote = ref(null); | ||||
|  | ||||
| const noteTags: ComputedRef<{code: TagCode; name: string}[]> = computed(() => [ | ||||
|    { code: 'note', name: t('application.note') }, | ||||
|    { code: 'todo', name: 'TODO' }, | ||||
|    { code: 'query', name: 'Query' } | ||||
| ]); | ||||
| const filteredNotes = computed(() => localNotes.value); | ||||
| const filteredNotes = computed(() => connectionNotes.value.filter(n => ( | ||||
|    (n.type === selectedTag.value || selectedTag.value === 'all') && | ||||
|    (n.cUid === localConnection.value || localConnection.value === null) && | ||||
|    (!n.isArchived || showArchived.value) && | ||||
|    (n.note.toLowerCase().search(localSearchTerm.value.toLowerCase()) >= 0) | ||||
| ))); | ||||
| const connectionOptions = computed(() => { | ||||
|    return [ | ||||
|       { code: null, name: t('general.all') }, | ||||
| @@ -240,13 +217,15 @@ const connectionOptions = computed(() => { | ||||
|  | ||||
| provide('noteTags', noteTags); | ||||
| provide('connectionOptions', connectionOptions); | ||||
| provide('selectedConnection', localConnection); | ||||
| provide('selectedTag', selectedTag); | ||||
|  | ||||
| const resizeResults = () => { | ||||
|    if (resultTable.value) { | ||||
|       const el = tableWrapper.value.parentElement; | ||||
|  | ||||
|       if (el) | ||||
|          resultsSize.value = el.offsetHeight - searchForm.value.offsetHeight; | ||||
|          resultsSize.value = el.offsetHeight - searchForm.value.offsetHeight - noteFilters.value.offsetHeight; | ||||
|  | ||||
|       resultTable.value.updateWindow(); | ||||
|    } | ||||
| @@ -254,17 +233,45 @@ const resizeResults = () => { | ||||
|  | ||||
| const refreshScroller = () => resizeResults(); | ||||
|  | ||||
| const highlightWord = (string: string) => { | ||||
|    string = string.replaceAll('<', '<').replaceAll('>', '>'); | ||||
|  | ||||
|    if (searchTerm.value) { | ||||
|       const regexp = new RegExp(`(${searchTerm.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); | ||||
|       return string.replace(regexp, '<span class="text-primary text-bold">$1</span>'); | ||||
|    } | ||||
|    else | ||||
|       return string; | ||||
| const setTag = (tag: string) => { | ||||
|    selectedTag.value = tag; | ||||
| }; | ||||
|  | ||||
| const toggleNote = (uid: string) => { | ||||
|    selectedNote.value = selectedNote.value !== uid ? uid : null; | ||||
| }; | ||||
|  | ||||
| const archiveNote = (uid: string) => { | ||||
|    const remappedNotes = connectionNotes.value.map(n => { | ||||
|       if (n.uid === uid) | ||||
|          n.isArchived = true; | ||||
|       return n; | ||||
|    }); | ||||
|    changeNotes(remappedNotes); | ||||
| }; | ||||
|  | ||||
| const restoreNote = (uid: string) => { | ||||
|    const remappedNotes = connectionNotes.value.map(n => { | ||||
|       if (n.uid === uid) | ||||
|          n.isArchived = false; | ||||
|       return n; | ||||
|    }); | ||||
|    changeNotes(remappedNotes); | ||||
| }; | ||||
|  | ||||
| const deleteNote = (uid: string) => { | ||||
|    const otherNotes = connectionNotes.value.filter(n => n.uid !== uid); | ||||
|    changeNotes(otherNotes); | ||||
| }; | ||||
|  | ||||
| watch(searchTerm, () => { | ||||
|    clearTimeout(searchTermInterval.value); | ||||
|  | ||||
|    searchTermInterval.value = setTimeout(() => { | ||||
|       localSearchTerm.value = searchTerm.value; | ||||
|    }, 200); | ||||
| }); | ||||
|  | ||||
| onUpdated(() => { | ||||
|    if (table.value) | ||||
|       refreshScroller(); | ||||
| @@ -276,6 +283,9 @@ onUpdated(() => { | ||||
| onMounted(() => { | ||||
|    resizeResults(); | ||||
|    window.addEventListener('resize', resizeResults); | ||||
|  | ||||
|    if (selectedWorkspace.value && selectedWorkspace.value !== 'NEW') | ||||
|       localConnection.value = selectedWorkspace.value; | ||||
| }); | ||||
|  | ||||
| onBeforeUnmount(() => { | ||||
| @@ -286,13 +296,20 @@ onBeforeUnmount(() => { | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .vscroll { | ||||
|   height: 1000px; | ||||
|   overflow: auto; | ||||
|   overflow-anchor: none; | ||||
| } | ||||
|  | ||||
| .add-button{ | ||||
|    bottom: 15px; | ||||
|    right: 0; | ||||
|    border: none; | ||||
|    height: 48px; | ||||
|    width: 48px; | ||||
|    border-radius: 50%; | ||||
|    position: fixed; | ||||
|    margin-top: -40px; | ||||
|    margin-left: 580px; | ||||
| } | ||||
| .archived-button { | ||||
|    border-radius: 50%; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user