From 8a030d935bda6fdf91015d6001d985919b022394 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis <vaartis@kotobank.ch>
Date: Fri, 19 Jan 2024 23:16:21 +0300
Subject: [PATCH] Separate emoji editing and upload into a separate component

Handle all state in that component
---
 .../settings_modal/admin_tabs/emoji_tab.js    | 108 ++-------
 .../settings_modal/admin_tabs/emoji_tab.scss  |  11 -
 .../settings_modal/admin_tabs/emoji_tab.vue   | 160 +++-----------
 .../helpers/emoji_editing_popover.vue         | 207 ++++++++++++++++++
 4 files changed, 253 insertions(+), 233 deletions(-)
 create mode 100644 src/components/settings_modal/helpers/emoji_editing_popover.vue

diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.js b/src/components/settings_modal/admin_tabs/emoji_tab.js
index 940f0c6cf..58f3ceae2 100644
--- a/src/components/settings_modal/admin_tabs/emoji_tab.js
+++ b/src/components/settings_modal/admin_tabs/emoji_tab.js
@@ -7,8 +7,7 @@ import Select from 'components/select/select.vue'
 import Popover from 'components/popover/popover.vue'
 import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
 import ModifiedIndicator from '../helpers/modified_indicator.vue'
-
-const newEmojiUploadBase = { shortcode: '', file: '', upload: [] }
+import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue'
 
 const EmojiTab = {
   components: {
@@ -19,24 +18,31 @@ const EmojiTab = {
     Select,
     Popover,
     ConfirmModal,
-    ModifiedIndicator
+    ModifiedIndicator,
+    EmojiEditingPopover
   },
 
   data () {
     return {
       knownLocalPacks: { },
       knownRemotePacks: { },
-      editedParts: { },
       editedMetadata: { },
       packName: '',
       newPackName: '',
       deleteModalVisible: false,
-      newEmojiUpload: clone(newEmojiUploadBase),
       remotePackInstance: '',
       remotePackDownloadAs: ''
     }
   },
 
+  provide () {
+    return {
+      // Functions
+      emojiAddr: this.emojiAddr,
+      displayError: this.displayError
+    }
+  },
+
   computed: {
     pack () {
       return this.packName !== '' ? this.knownPacks[this.packName] : undefined
@@ -58,11 +64,6 @@ const EmojiTab = {
       }
 
       return result
-    },
-    newEmojiUploadPreview () {
-      if (this.newEmojiUpload.upload.length > 0) {
-        return URL.createObjectURL(this.newEmojiUpload.upload[0])
-      }
     }
   },
 
@@ -82,27 +83,6 @@ const EmojiTab = {
       }
     },
 
-    uploadEmoji () {
-      this.$refs.addEmojiPopover.hidePopover()
-
-      this.$store.state.api.backendInteractor.addNewEmojiFile({
-        packName: this.packName,
-        file: this.newEmojiUpload.upload[0],
-        shortcode: this.newEmojiUpload.shortcode,
-        filename: this.newEmojiUpload.file
-      }).then(resp => resp.json()).then(resp => {
-        if (resp.error !== undefined) {
-          this.displayError(resp.error)
-          return
-        }
-
-        this.pack.files = resp
-        this.sortPackFiles(this.packName)
-
-        this.newEmojiUpload = clone(newEmojiUploadBase)
-      })
-    },
-
     createEmojiPack () {
       this.$store.state.api.backendInteractor.createEmojiPack(
         { name: this.newPackName }
@@ -132,7 +112,6 @@ const EmojiTab = {
         }
       }).then(done => {
         delete this.editedMetadata[this.packName]
-        delete this.editedParts[this.packName]
 
         this.deleteModalVisible = false
         this.packName = ''
@@ -162,68 +141,9 @@ const EmojiTab = {
       })
     },
 
-    revertEmoji (shortcode) {
-      // Delete current changes and overwrite them with defaults. If the window is closed, they'll get cleared anyway
-      delete this.editedParts[this.packName][shortcode]
-      this.editEmoji(shortcode)
-    },
-    editEmoji (shortcode) {
-      if (this.editedParts[this.packName] === undefined) { this.editedParts[this.packName] = {} }
-
-      if (this.editedParts[this.packName][shortcode] === undefined) {
-        this.editedParts[this.packName][shortcode] = {
-          shortcode, file: this.pack.files[shortcode]
-        }
-      }
-    },
-    deleteEmoji (shortcode) {
-      this.editedParts[this.packName][shortcode].deleteModalVisible = false
-
-      this.$store.state.api.backendInteractor.deleteEmojiFile(
-        { packName: this.packName, shortcode }
-      ).then(resp => resp.json()).then(resp => {
-        if (resp.error !== undefined) {
-          this.displayError(resp.error)
-          return
-        }
-
-        this.pack.files = resp
-        delete this.editedParts[this.packName][shortcode]
-
-        this.sortPackFiles(this.packName)
-      })
-    },
-    closedEditedEmoji (shortcode) {
-      const edited = this.editedParts[this.packName][shortcode]
-
-      if (edited.shortcode === shortcode && edited.file === this.pack.files[shortcode]) {
-        delete this.editedParts[this.packName][shortcode]
-
-        return true
-      }
-
-      return false
-    },
-    saveEditedEmoji (shortcode) {
-      if (this.closedEditedEmoji(shortcode)) return
-
-      const edited = this.editedParts[this.packName][shortcode]
-
-      this.$store.state.api.backendInteractor.updateEmojiFile(
-        { packName: this.packName, shortcode, newShortcode: edited.shortcode, newFilename: edited.file, force: false }
-      ).then(resp => {
-        if (resp.error !== undefined) {
-          this.displayError(resp.error)
-          return Promise.reject(resp.error)
-        }
-
-        return resp.json()
-      }).then(resp => {
-        this.pack.files = resp
-        delete this.editedParts[this.packName][shortcode]
-
-        this.sortPackFiles(this.packName)
-      })
+    updatePackFiles (newFiles) {
+      this.pack.files = newFiles
+      this.sortPackFiles(this.packName)
     },
 
     loadPacksPaginated (listFunction) {
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.scss b/src/components/settings_modal/admin_tabs/emoji_tab.scss
index 71f46cc26..68711aee1 100644
--- a/src/components/settings_modal/admin_tabs/emoji_tab.scss
+++ b/src/components/settings_modal/admin_tabs/emoji_tab.scss
@@ -39,17 +39,6 @@
   margin-left: 0.5em;
 }
 
-.emoji-tab-edit-popover {
-  padding-left: 0.5em;
-  padding-right: 0.5em;
-  padding-bottom: 0.5em;
-
-  .emoji {
-    width: 32px;
-    height: 32px;
-  }
-}
-
 .emoji-tab-popover-input {
   margin-bottom: 0.5em;
 
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.vue b/src/components/settings_modal/admin_tabs/emoji_tab.vue
index 6b4268bc1..dcb8e8433 100644
--- a/src/components/settings_modal/admin_tabs/emoji_tab.vue
+++ b/src/components/settings_modal/admin_tabs/emoji_tab.vue
@@ -97,38 +97,44 @@
         </li>
       </ul>
 
-      <div v-if="packName !== ''">
+      <div v-if="pack">
         <div class="pack-info-wrapper">
           <ul class="setting-list">
             <li>
-              <div>
+              <label>
                 {{ $t('admin_dash.emoji.description') }}
                 <ModifiedIndicator :changed="metaEdited('description')" message-key="admin_dash.emoji.metadata_changed" />
-              </div>
-              <textarea
-                v-model="packMeta.description"
-                :disabled="pack.remote !== undefined"
-                class="bio resize-height" />
+
+                <textarea
+                  v-model="packMeta.description"
+                  :disabled="pack.remote !== undefined"
+                  class="bio resize-height" />
+              </label>
             </li>
             <li>
-              <div>
+              <label>
                 {{ $t('admin_dash.emoji.homepage') }}
                 <ModifiedIndicator :changed="metaEdited('homepage')" message-key="admin_dash.emoji.metadata_changed" />
-              </div>
+
               <input
                 class="emoji-info-input" v-model="packMeta.homepage"
                 :disabled="pack.remote !== undefined">
+              </label>
             </li>
             <li>
-              <div>
+              <label>
                 {{ $t('admin_dash.emoji.fallback_src') }}
                 <ModifiedIndicator :changed="metaEdited('fallback-src')" message-key="admin_dash.emoji.metadata_changed" />
-              </div>
-              <input class="emoji-info-input" v-model="packMeta['fallback-src']" :disabled="pack.remote !== undefined">
+
+                <input class="emoji-info-input" v-model="packMeta['fallback-src']" :disabled="pack.remote !== undefined">
+              </label>
             </li>
             <li>
-              <div>{{ $t('admin_dash.emoji.fallback_sha256') }}</div>
-              <input :disabled="true" class="emoji-info-input" v-model="packMeta['fallback-src-sha256']">
+              <label>
+                {{ $t('admin_dash.emoji.fallback_sha256') }}
+
+                <input :disabled="true" class="emoji-info-input" v-model="packMeta['fallback-src-sha256']">
+              </label>
             </li>
             <li>
               <Checkbox :disabled="pack.remote !== undefined" v-model="packMeta['share-files']">
@@ -218,132 +224,30 @@
             {{ $t('admin_dash.emoji.files') }}
 
             <ModifiedIndicator v-if="pack"
-              :changed="editedParts[packName] && Object.keys(editedParts[packName]).length > 0"
+              :changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)"
               message-key="admin_dash.emoji.emoji_changed"/>
           </h4>
 
           <div class="emoji-list" v-if="pack">
-            <Popover
+            <EmojiEditingPopover
               v-if="pack.remote === undefined"
-              ref="addEmojiPopover"
-              trigger="click"
-              placement="bottom"
-              bound-to-selector=".emoji-tab"
-              popover-class="emoji-tab-edit-popover popover-default"
-              :bound-to="{ x: 'container' }"
-              :offset="{ y: 5 }"
+              placement="bottom" new-upload
+              :title="$t('admin_dash.emoji.adding_new')"
+              :packName="packName" @updatePackFiles="updatePackFiles"
             >
               <template #trigger>
                 <FAIcon icon="plus" size="2x" :title="$t('admin_dash.emoji.add_file')" />
               </template>
-              <template #content>
-                <h3>
-                  {{ $t('admin_dash.emoji.adding_new') }}
-                </h3>
-
-                <StillImage
-                  class="emoji" v-if="newEmojiUploadPreview"
-                  :src="newEmojiUploadPreview"
-                />
-                <div v-else class="emoji"></div>
-
-                <div class="emoji-tab-popover-input">
-                  <input
-                    type="file"
-                    accept="image/*"
-                    class="emoji-tab-popover-file"
-                    @change="newEmojiUpload.upload = $event.target.files"
-                  >
-                </div>
-                <div>
-                  <div class="emoji-tab-popover-input">
-                    <div>{{ $t('admin_dash.emoji.shortcode') }}</div>
-                    <input class="emoji-data-input"
-                      v-model="newEmojiUpload.shortcode"
-                      :placeholder="$t('admin_dash.emoji.new_shortcode')">
-                  </div>
+            </EmojiEditingPopover>
 
-                  <div class="emoji-tab-popover-input">
-                    <div>{{ $t('admin_dash.emoji.filename') }}</div>
-                    <input class="emoji-data-input"
-                      v-model="newEmojiUpload.file"
-                      :placeholder="$t('admin_dash.emoji.new_filename')">
-                  </div>
-
-                  <button
-                    class="button button-default btn"
-                    type="button"
-                    :disabled="this.newEmojiUpload.upload.length == 0"
-                    @click="uploadEmoji">
-                    {{ $t('admin_dash.emoji.save') }}
-                  </button>
-                </div>
-              </template>
-            </Popover>
-
-            <Popover
+            <EmojiEditingPopover
+              placement="top" ref="emojiPopovers"
               v-for="(file, shortcode) in pack.files" :key="shortcode"
-              trigger="click"
+              :title="$t('admin_dash.emoji.editing', [shortcode])"
               :disabled="pack.remote !== undefined"
-              placement="top"
-              :class="{'emoji-unsaved': editedParts[packName] !== undefined && editedParts[packName][shortcode] !== undefined}"
-              popover-class="emoji-tab-edit-popover popover-default"
-              bound-to-selector=".emoji-list"
-              :bound-to="{ x: 'container' }"
-              :offset="{ y: 5 }"
-              @show="editEmoji(shortcode)"
-              @close="closedEditedEmoji(shortcode)"
+              :shortcode="shortcode" :file="file" :packName="packName"
+              @updatePackFiles="updatePackFiles"
             >
-              <template #content>
-                <h3>
-                  {{ $t('admin_dash.emoji.editing', [shortcode]) }}
-                </h3>
-
-                <StillImage class="emoji" :src="emojiAddr(file)" />
-
-                <div v-if="editedParts[packName] !== undefined && editedParts[packName][shortcode] !== undefined">
-                  <div class="emoji-tab-popover-input">
-                    <label for="emoji-edit-shortcode">{{ $t('admin_dash.emoji.shortcode') }}</label>
-                    <input id="emoji-edit-shortcode" class="emoji-data-input"
-                      v-model="editedParts[packName][shortcode].shortcode">
-                  </div>
-                  <div class="emoji-tab-popover-input">
-                    <label for="emoji-edit-filename">{{ $t('admin_dash.emoji.filename') }}</label>
-                    <input id="emoji-edit-filename" class="emoji-data-input"
-                      v-model="editedParts[packName][shortcode].file">
-                  </div>
-
-                  <div>
-                    <button
-                      class="button button-default btn emoji-tab-popover-button"
-                      type="button"
-                      @click="saveEditedEmoji(shortcode)">
-                      {{ $t('admin_dash.emoji.save') }}
-                    </button>
-                    <button
-                      class="button button-default btn emoji-tab-popover-button"
-                      type="button"
-                      @click="editedParts[packName][shortcode].deleteModalVisible = true">
-                      {{ $t('admin_dash.emoji.delete') }}
-                    </button>
-                    <button
-                      class="button button-default btn emoji-tab-popover-button"
-                      type="button"
-                      @click="revertEmoji(shortcode)">
-                      {{ $t('admin_dash.emoji.revert') }}
-                    </button>
-                    <ConfirmModal
-                      v-if="editedParts[packName][shortcode].deleteModalVisible"
-                      :title="$t('admin_dash.emoji.delete_title')"
-                      :cancel-text="$t('status.delete_confirm_cancel_button')"
-                      :confirm-text="$t('status.delete_confirm_accept_button')"
-                      @cancelled="editedParts[packName][shortcode].deleteModalVisible = false"
-                      @accepted="deleteEmoji(shortcode)" >
-                      {{ $t('admin_dash.emoji.delete_confirm', [shortcode]) }}
-                    </ConfirmModal>
-                  </div>
-                </div>
-              </template>
               <template #trigger>
                 <StillImage
                   class="emoji"
@@ -352,7 +256,7 @@
                   :alt="`:${shortcode}:`"
                 />
               </template>
-            </Popover>
+            </EmojiEditingPopover>
           </div>
         </ul>
       </div>
diff --git a/src/components/settings_modal/helpers/emoji_editing_popover.vue b/src/components/settings_modal/helpers/emoji_editing_popover.vue
new file mode 100644
index 000000000..ec426be8d
--- /dev/null
+++ b/src/components/settings_modal/helpers/emoji_editing_popover.vue
@@ -0,0 +1,207 @@
+<template>
+  <Popover
+    trigger="click"
+    :placement="placement"
+    bound-to-selector=".emoji-list"
+    popover-class="emoji-tab-edit-popover popover-default"
+    ref="emojiPopover"
+    :bound-to="{ x: 'container' }"
+    :offset="{ y: 5 }"
+    :disabled="disabled"
+    :class="{'emoji-unsaved': isEdited}"
+  >
+    <template #trigger>
+      <slot name="trigger" />
+    </template>
+    <template #content>
+      <h3>
+        {{ title }}
+      </h3>
+
+      <StillImage class="emoji" v-if="emojiPreview" :src="emojiPreview" />
+      <div v-else class="emoji"></div>
+
+      <div class="emoji-tab-popover-input" v-if="newUpload">
+        <input
+          type="file"
+          accept="image/*"
+          class="emoji-tab-popover-file"
+          @change="uploadFile = $event.target.files">
+      </div>
+      <div>
+        <div class="emoji-tab-popover-input">
+          <label>
+            {{ $t('admin_dash.emoji.shortcode') }}
+            <input class="emoji-data-input"
+              v-model="editedShortcode"
+              :placeholder="$t('admin_dash.emoji.new_shortcode')">
+          </label>
+        </div>
+
+        <div class="emoji-tab-popover-input">
+          <label>
+            {{ $t('admin_dash.emoji.filename') }}
+
+            <input class="emoji-data-input"
+              v-model="editedFile"
+              :placeholder="$t('admin_dash.emoji.new_filename')">
+          </label>
+        </div>
+
+        <button
+          class="button button-default btn"
+          type="button"
+          :disabled="uploadFile.length == 0"
+          @click="newUpload ? uploadEmoji() : saveEditedEmoji()">
+          {{ $t('admin_dash.emoji.save') }}
+        </button>
+
+        <template v-if="!newUpload">
+          <button
+            class="button button-default btn emoji-tab-popover-button"
+            type="button"
+            @click="deleteModalVisible = true">
+            {{ $t('admin_dash.emoji.delete') }}
+          </button>
+          <button
+            class="button button-default btn emoji-tab-popover-button"
+            type="button"
+            @click="revertEmoji">
+            {{ $t('admin_dash.emoji.revert') }}
+          </button>
+          <ConfirmModal
+            v-if="deleteModalVisible"
+            :title="$t('admin_dash.emoji.delete_title')"
+            :cancel-text="$t('status.delete_confirm_cancel_button')"
+            :confirm-text="$t('status.delete_confirm_accept_button')"
+            @cancelled="deleteModalVisible = false"
+            @accepted="deleteEmoji" >
+            {{ $t('admin_dash.emoji.delete_confirm', [shortcode]) }}
+          </ConfirmModal>
+        </template>
+      </div>
+    </template>
+  </Popover>
+</template>
+
+<script>
+import Popover from 'components/popover/popover.vue'
+import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
+import StillImage from 'components/still-image/still-image.vue'
+
+export default {
+  components: { Popover, ConfirmModal, StillImage },
+  data () {
+    return {
+      uploadFile: [],
+      editedShortcode: this.shortcode,
+      editedFile: this.file,
+      deleteModalVisible: false
+    }
+  },
+  computed: {
+    emojiPreview () {
+      if (this.newUpload && this.uploadFile.length > 0) {
+        return URL.createObjectURL(this.uploadFile[0])
+      } else if (!this.newUpload) {
+        return this.emojiAddr(this.file)
+      }
+
+      return null
+    },
+    isEdited () {
+      return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file)
+    }
+  },
+  inject: ['emojiAddr', 'displayError'],
+  methods: {
+    saveEditedEmoji () {
+      if (!this.isEdited) return
+
+      this.$store.state.api.backendInteractor.updateEmojiFile(
+        { packName: this.packName, shortcode: this.shortcode, newShortcode: this.editedShortcode, newFilename: this.editedFile, force: false }
+      ).then(resp => {
+        if (resp.error !== undefined) {
+          this.displayError(resp.error)
+          return Promise.reject(resp.error)
+        }
+
+        return resp.json()
+      }).then(resp => this.$emit('updatePackFiles', resp))
+    },
+    uploadEmoji () {
+      this.$store.state.api.backendInteractor.addNewEmojiFile({
+        packName: this.packName,
+        file: this.uploadFile[0],
+        shortcode: this.editedShortcode,
+        filename: this.editedFile
+      }).then(resp => resp.json()).then(resp => {
+        if (resp.error !== undefined) {
+          this.displayError(resp.error)
+          return
+        }
+
+        this.$emit('updatePackFiles', resp)
+        this.$refs.emojiPopover.hidePopover()
+
+        this.editedFile = ''
+        this.editedShortcode = ''
+        this.uploadFile = []
+      })
+    },
+    revertEmoji () {
+      this.editedFile = this.file
+      this.editedShortcode = this.shortcode
+    },
+    deleteEmoji () {
+      this.deleteModalVisible = false
+
+      this.$store.state.api.backendInteractor.deleteEmojiFile(
+        { packName: this.packName, shortcode: this.editedShortcode }
+      ).then(resp => resp.json()).then(resp => {
+        if (resp.error !== undefined) {
+          this.displayError(resp.error)
+          return
+        }
+
+        this.$emit('updatePackFiles', resp)
+      })
+    }
+  },
+  props: {
+    placement: String,
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+
+    newUpload: Boolean,
+
+    title: String,
+    packName: String,
+    shortcode: {
+      type: String,
+      // Only exists when this is not a new upload
+      default: ''
+    },
+    file: {
+      type: String,
+      // Only exists when this is not a new upload
+      default: ''
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+  .emoji-tab-edit-popover {
+    padding-left: 0.5em;
+    padding-right: 0.5em;
+    padding-bottom: 0.5em;
+
+    .emoji {
+      width: 32px;
+      height: 32px;
+    }
+  }
+</style>
-- 
GitLab