Folder.vue (hoppscotch-2.2.1) | : | Folder.vue (hoppscotch-3.0.0) | ||
---|---|---|---|---|
<template> | <template> | |||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]"> | <div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]"> | |||
<div | <div | |||
class="flex items-stretch group" | class="flex items-stretch group" | |||
@dragover.prevent | @dragover.prevent | |||
@drop.prevent="dropEvent" | @drop.prevent="dropEvent" | |||
@dragover="dragging = true" | @dragover="dragging = true" | |||
@drop="dragging = false" | @drop="dragging = false" | |||
@dragleave="dragging = false" | @dragleave="dragging = false" | |||
@dragend="dragging = false" | @dragend="dragging = false" | |||
@contextmenu.prevent="options.tippy().show()" | @contextmenu.prevent="options.tippy.show()" | |||
> | > | |||
<span | <span | |||
class="cursor-pointer flex px-4 items-center justify-center" | class="flex items-center justify-center px-4 cursor-pointer" | |||
@click="toggleShowChildren()" | @click="toggleShowChildren()" | |||
> | > | |||
<SmartIcon | <component | |||
:is="getCollectionIcon" | ||||
class="svg-icons" | class="svg-icons" | |||
:class="{ 'text-accent': isSelected }" | :class="{ 'text-accent': isSelected }" | |||
:name="getCollectionIcon" | ||||
/> | /> | |||
</span> | </span> | |||
<span | <span | |||
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hov er:text-secondaryDark" | class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hov er:text-secondaryDark" | |||
@click="toggleShowChildren()" | @click="toggleShowChildren()" | |||
> | > | |||
<span class="truncate" :class="{ 'text-accent': isSelected }"> | <span class="truncate" :class="{ 'text-accent': isSelected }"> | |||
{{ folder.name ? folder.name : folder.title }} | {{ folder.name ? folder.name : folder.title }} | |||
</span> | </span> | |||
</span> | </span> | |||
<div class="flex"> | <div class="flex"> | |||
<ButtonSecondary | <ButtonSecondary | |||
v-tippy="{ theme: 'tooltip' }" | v-tippy="{ theme: 'tooltip' }" | |||
svg="folder-plus" | :icon="IconFilePlus" | |||
:title="t('request.new')" | ||||
class="hidden group-hover:inline-flex" | ||||
@click="$emit('add-request', { path: folderPath })" | ||||
/> | ||||
<ButtonSecondary | ||||
v-tippy="{ theme: 'tooltip' }" | ||||
:icon="IconFolderPlus" | ||||
:title="t('folder.new')" | :title="t('folder.new')" | |||
class="hidden group-hover:inline-flex" | class="hidden group-hover:inline-flex" | |||
@click.native="$emit('add-folder', { folder, path: folderPath })" | @click="$emit('add-folder', { folder, path: folderPath })" | |||
/> | /> | |||
<span> | <span> | |||
<tippy | <tippy | |||
ref="options" | ref="options" | |||
interactive | interactive | |||
trigger="click" | trigger="click" | |||
theme="popover" | theme="popover" | |||
arrow | arrow | |||
:on-shown="() => tippyActions.focus()" | :on-shown="() => tippyActions.focus()" | |||
> | > | |||
<template #trigger> | <ButtonSecondary | |||
<ButtonSecondary | v-tippy="{ theme: 'tooltip' }" | |||
v-tippy="{ theme: 'tooltip' }" | :title="t('action.more')" | |||
:title="t('action.more')" | :icon="IconMoreVertical" | |||
svg="more-vertical" | /> | |||
/> | <template #content="{ hide }"> | |||
<div | ||||
ref="tippyActions" | ||||
class="flex flex-col focus:outline-none" | ||||
tabindex="0" | ||||
role="menu" | ||||
@keyup.r="requestAction.$el.click()" | ||||
@keyup.n="folderAction.$el.click()" | ||||
@keyup.e="edit.$el.click()" | ||||
@keyup.delete="deleteAction.$el.click()" | ||||
@keyup.x="exportAction.$el.click()" | ||||
@keyup.escape="hide()" | ||||
> | ||||
<SmartItem | ||||
ref="requestAction" | ||||
:icon="IconFilePlus" | ||||
:label="t('request.new')" | ||||
:shortcut="['R']" | ||||
@click=" | ||||
() => { | ||||
$emit('add-request', { path: folderPath }) | ||||
hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
ref="folderAction" | ||||
:icon="IconFolderPlus" | ||||
:label="t('folder.new')" | ||||
:shortcut="['N']" | ||||
@click=" | ||||
() => { | ||||
$emit('add-folder', { folder, path: folderPath }) | ||||
hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
ref="edit" | ||||
:icon="IconEdit" | ||||
:label="t('action.edit')" | ||||
:shortcut="['E']" | ||||
@click=" | ||||
() => { | ||||
$emit('edit-folder', { | ||||
folder, | ||||
folderIndex, | ||||
collectionIndex, | ||||
folderPath, | ||||
}) | ||||
hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
ref="exportAction" | ||||
:icon="IconDownload" | ||||
:label="t('export.title')" | ||||
:shortcut="['X']" | ||||
@click=" | ||||
() => { | ||||
exportFolder() | ||||
hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
ref="deleteAction" | ||||
:icon="IconTrash2" | ||||
:label="t('action.delete')" | ||||
:shortcut="['⌫']" | ||||
@click=" | ||||
() => { | ||||
removeFolder() | ||||
hide() | ||||
} | ||||
" | ||||
/> | ||||
</div> | ||||
</template> | </template> | |||
<div | ||||
ref="tippyActions" | ||||
class="flex flex-col focus:outline-none" | ||||
tabindex="0" | ||||
@keyup.n="folderAction.$el.click()" | ||||
@keyup.e="edit.$el.click()" | ||||
@keyup.delete="deleteAction.$el.click()" | ||||
@keyup.escape="options.tippy().hide()" | ||||
> | ||||
<SmartItem | ||||
ref="folderAction" | ||||
svg="folder-plus" | ||||
:label="t('folder.new')" | ||||
:shortcut="['N']" | ||||
@click.native=" | ||||
() => { | ||||
$emit('add-folder', { folder, path: folderPath }) | ||||
options.tippy().hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
ref="edit" | ||||
svg="edit" | ||||
:label="t('action.edit')" | ||||
:shortcut="['E']" | ||||
@click.native=" | ||||
() => { | ||||
$emit('edit-folder', { | ||||
folder, | ||||
folderIndex, | ||||
collectionIndex, | ||||
folderPath, | ||||
}) | ||||
options.tippy().hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
ref="deleteAction" | ||||
svg="trash-2" | ||||
:label="t('action.delete')" | ||||
:shortcut="['⌫']" | ||||
@click.native=" | ||||
() => { | ||||
confirmRemove = true | ||||
options.tippy().hide() | ||||
} | ||||
" | ||||
/> | ||||
</div> | ||||
</tippy> | </tippy> | |||
</span> | </span> | |||
</div> | </div> | |||
</div> | </div> | |||
<div v-if="showChildren || isFiltered" class="flex"> | <div v-if="showChildren || isFiltered" class="flex"> | |||
<div | <div | |||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125" | class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125" | |||
@click="toggleShowChildren()" | @click="toggleShowChildren()" | |||
></div> | ></div> | |||
<div class="flex flex-col flex-1 truncate"> | <div class="flex flex-col flex-1 truncate"> | |||
<CollectionsMyFolder | <!-- Referring to this component only (this is recursive) --> | |||
<Folder | ||||
v-for="(subFolder, subFolderIndex) in folder.folders" | v-for="(subFolder, subFolderIndex) in folder.folders" | |||
:key="`subFolder-${subFolderIndex}`" | :key="`subFolder-${subFolderIndex}`" | |||
:folder="subFolder" | :folder="subFolder" | |||
:folder-index="subFolderIndex" | :folder-index="subFolderIndex" | |||
:collection-index="collectionIndex" | :collection-index="collectionIndex" | |||
:doc="doc" | ||||
:save-request="saveRequest" | :save-request="saveRequest" | |||
:collections-type="collectionsType" | :collections-type="collectionsType" | |||
:folder-path="`${folderPath}/${subFolderIndex}`" | :folder-path="`${folderPath}/${subFolderIndex}`" | |||
:picked="picked" | :picked="picked" | |||
@add-request="$emit('add-request', $event)" | ||||
@add-folder="$emit('add-folder', $event)" | @add-folder="$emit('add-folder', $event)" | |||
@edit-folder="$emit('edit-folder', $event)" | @edit-folder="$emit('edit-folder', $event)" | |||
@edit-request="$emit('edit-request', $event)" | @edit-request="$emit('edit-request', $event)" | |||
@duplicate-request="$emit('duplicate-request', $event)" | @duplicate-request="$emit('duplicate-request', $event)" | |||
@update-team-collections="$emit('update-team-collections')" | @update-team-collections="$emit('update-team-collections')" | |||
@select="$emit('select', $event)" | @select="$emit('select', $event)" | |||
@remove-request="removeRequest" | @remove-request="$emit('remove-request', $event)" | |||
@remove-folder="$emit('remove-folder', $event)" | ||||
/> | /> | |||
<CollectionsMyRequest | <CollectionsMyRequest | |||
v-for="(request, index) in folder.requests" | v-for="(request, index) in folder.requests" | |||
:key="`request-${index}`" | :key="`request-${index}`" | |||
:request="request" | :request="request" | |||
:collection-index="collectionIndex" | :collection-index="collectionIndex" | |||
:folder-index="folderIndex" | :folder-index="folderIndex" | |||
:folder-name="folder.name" | :folder-name="folder.name" | |||
:folder-path="folderPath" | :folder-path="folderPath" | |||
:request-index="index" | :request-index="index" | |||
:doc="doc" | ||||
:picked="picked" | :picked="picked" | |||
:save-request="saveRequest" | :save-request="saveRequest" | |||
:collections-type="collectionsType" | :collections-type="collectionsType" | |||
@edit-request="$emit('edit-request', $event)" | @edit-request="$emit('edit-request', $event)" | |||
@duplicate-request="$emit('duplicate-request', $event)" | @duplicate-request="$emit('duplicate-request', $event)" | |||
@select="$emit('select', $event)" | @select="$emit('select', $event)" | |||
@remove-request="removeRequest" | @remove-request="$emit('remove-request', $event)" | |||
/> | /> | |||
<div | <div | |||
v-if=" | v-if=" | |||
folder.folders && | folder.folders && | |||
folder.folders.length === 0 && | folder.folders.length === 0 && | |||
folder.requests && | folder.requests && | |||
folder.requests.length === 0 | folder.requests.length === 0 | |||
" | " | |||
class="flex flex-col text-secondaryLight p-4 items-center justify-cent er" | class="flex flex-col items-center justify-center p-4 text-secondaryLig ht" | |||
> | > | |||
<img | <img | |||
:src="`/images/states/${$colorMode.value}/pack.svg`" | :src="`/images/states/${colorMode.value}/pack.svg`" | |||
loading="lazy" | loading="lazy" | |||
class="flex-col object-contain object-center h-16 mb-4 w-16 inline-f lex" | class="inline-flex flex-col object-contain object-center w-16 h-16 m b-4" | |||
:alt="`${t('empty.folder')}`" | :alt="`${t('empty.folder')}`" | |||
/> | /> | |||
<span class="text-center"> | <span class="text-center"> | |||
{{ t("empty.folder") }} | {{ t("empty.folder") }} | |||
</span> | </span> | |||
</div> | </div> | |||
</div> | </div> | |||
</div> | </div> | |||
<SmartConfirmModal | ||||
:show="confirmRemove" | ||||
:title="t('confirm.remove_folder')" | ||||
@hide-modal="confirmRemove = false" | ||||
@resolve="removeFolder" | ||||
/> | ||||
</div> | </div> | |||
</template> | </template> | |||
<script lang="ts"> | <script lang="ts"> | |||
import { defineComponent, ref } from "@nuxtjs/composition-api" | import IconFilePlus from "~icons/lucide/file-plus" | |||
import { useI18n } from "~/helpers/utils/composables" | import IconFolderPlus from "~icons/lucide/folder-plus" | |||
import { | import IconMoreVertical from "~icons/lucide/more-vertical" | |||
removeRESTFolder, | import IconEdit from "~icons/lucide/edit" | |||
removeRESTRequest, | import IconDownload from "~icons/lucide/download" | |||
moveRESTRequest, | import IconTrash2 from "~icons/lucide/trash-2" | |||
} from "~/newstore/collections" | import IconFolder from "~icons/lucide/folder" | |||
import IconCheckCircle from "~icons/lucide/check-circle" | ||||
import IconFolderOpen from "~icons/lucide/folder-open" | ||||
import { defineComponent, ref } from "vue" | ||||
import { useColorMode } from "@composables/theming" | ||||
import { useToast } from "@composables/toast" | ||||
import { useI18n } from "@composables/i18n" | ||||
import { moveRESTRequest } from "~/newstore/collections" | ||||
export default defineComponent({ | export default defineComponent({ | |||
name: "Folder", | name: "Folder", | |||
props: { | props: { | |||
folder: { type: Object, default: () => {} }, | folder: { type: Object, default: () => ({}) }, | |||
folderIndex: { type: Number, default: null }, | folderIndex: { type: Number, default: null }, | |||
collectionIndex: { type: Number, default: null }, | collectionIndex: { type: Number, default: null }, | |||
folderPath: { type: String, default: null }, | folderPath: { type: String, default: null }, | |||
doc: Boolean, | ||||
saveRequest: Boolean, | saveRequest: Boolean, | |||
isFiltered: Boolean, | isFiltered: Boolean, | |||
collectionsType: { type: Object, default: () => {} }, | collectionsType: { type: Object, default: () => ({}) }, | |||
picked: { type: Object, default: () => {} }, | picked: { type: Object, default: () => ({}) }, | |||
}, | }, | |||
emits: [ | ||||
"add-request", | ||||
"add-folder", | ||||
"edit-folder", | ||||
"update-team", | ||||
"remove-folder", | ||||
"edit-request", | ||||
"duplicate-request", | ||||
"select", | ||||
"remove-request", | ||||
"update-team-collections", | ||||
], | ||||
setup() { | setup() { | |||
const t = useI18n() | const t = useI18n() | |||
return { | return { | |||
tippyActions: ref<any | null>(null), | tippyActions: ref<any | null>(null), | |||
options: ref<any | null>(null), | options: ref<any | null>(null), | |||
requestAction: ref<any | null>(null), | ||||
folderAction: ref<any | null>(null), | folderAction: ref<any | null>(null), | |||
edit: ref<any | null>(null), | edit: ref<any | null>(null), | |||
deleteAction: ref<any | null>(null), | deleteAction: ref<any | null>(null), | |||
exportAction: ref<any | null>(null), | ||||
t, | t, | |||
toast: useToast(), | ||||
colorMode: useColorMode(), | ||||
IconFilePlus, | ||||
IconFolderPlus, | ||||
IconMoreVertical, | ||||
IconEdit, | ||||
IconDownload, | ||||
IconTrash2, | ||||
} | } | |||
}, | }, | |||
data() { | data() { | |||
return { | return { | |||
showChildren: false, | showChildren: false, | |||
dragging: false, | dragging: false, | |||
confirmRemove: false, | ||||
prevCursor: "", | prevCursor: "", | |||
cursor: "", | cursor: "", | |||
} | } | |||
}, | }, | |||
computed: { | computed: { | |||
isSelected(): boolean { | isSelected(): boolean { | |||
return ( | return ( | |||
this.picked && | this.picked && | |||
this.picked.pickedType === "my-folder" && | this.picked.pickedType === "my-folder" && | |||
this.picked.folderPath === this.folderPath | this.picked.folderPath === this.folderPath | |||
) | ) | |||
}, | }, | |||
getCollectionIcon() { | getCollectionIcon() { | |||
if (this.isSelected) return "check-circle" | if (this.isSelected) return IconCheckCircle | |||
else if (!this.showChildren && !this.isFiltered) return "folder" | else if (!this.showChildren && !this.isFiltered) return IconFolder | |||
else if (this.showChildren || this.isFiltered) return "folder-open" | else if (this.showChildren || this.isFiltered) return IconFolderOpen | |||
else return "folder" | else return IconFolder | |||
}, | }, | |||
}, | }, | |||
methods: { | methods: { | |||
exportFolder() { | ||||
const folderJSON = JSON.stringify(this.folder) | ||||
const file = new Blob([folderJSON], { type: "application/json" }) | ||||
const a = document.createElement("a") | ||||
const url = URL.createObjectURL(file) | ||||
a.href = url | ||||
a.download = `${this.folder.name}.json` | ||||
document.body.appendChild(a) | ||||
a.click() | ||||
this.toast.success(this.t("state.download_started").toString()) | ||||
setTimeout(() => { | ||||
document.body.removeChild(a) | ||||
URL.revokeObjectURL(url) | ||||
}, 1000) | ||||
}, | ||||
toggleShowChildren() { | toggleShowChildren() { | |||
if (this.$props.saveRequest) | if (this.$props.saveRequest) | |||
this.$emit("select", { | this.$emit("select", { | |||
picked: { | picked: { | |||
pickedType: "my-folder", | pickedType: "my-folder", | |||
collectionIndex: this.collectionIndex, | collectionIndex: this.collectionIndex, | |||
folderName: this.folder.name, | folderName: this.folder.name, | |||
folderPath: this.folderPath, | folderPath: this.folderPath, | |||
}, | }, | |||
}) | }) | |||
this.showChildren = !this.showChildren | this.showChildren = !this.showChildren | |||
}, | }, | |||
removeFolder() { | removeFolder() { | |||
// TODO: Bubble it up ? | this.$emit("remove-folder", { | |||
// Cancel pick if picked folder was deleted | folder: this.folder, | |||
if ( | folderPath: this.folderPath, | |||
this.picked && | }) | |||
this.picked.pickedType === "my-folder" && | ||||
this.picked.folderPath === this.folderPath | ||||
) { | ||||
this.$emit("select", { picked: null }) | ||||
} | ||||
removeRESTFolder(this.folderPath) | ||||
this.$toast.success(`${this.$t("state.deleted")}`) | ||||
}, | }, | |||
dropEvent({ dataTransfer }) { | dropEvent({ dataTransfer }) { | |||
this.dragging = !this.dragging | this.dragging = !this.dragging | |||
const folderPath = dataTransfer.getData("folderPath") | const folderPath = dataTransfer.getData("folderPath") | |||
const requestIndex = dataTransfer.getData("requestIndex") | const requestIndex = dataTransfer.getData("requestIndex") | |||
moveRESTRequest(folderPath, requestIndex, this.folderPath) | moveRESTRequest(folderPath, requestIndex, this.folderPath) | |||
}, | }, | |||
removeRequest({ requestIndex }) { | ||||
// TODO: Bubble it up to root ? | ||||
// Cancel pick if the picked item is being deleted | ||||
if ( | ||||
this.picked && | ||||
this.picked.pickedType === "my-request" && | ||||
this.picked.folderPath === this.folderPath && | ||||
this.picked.requestIndex === requestIndex | ||||
) { | ||||
this.$emit("select", { picked: null }) | ||||
} | ||||
removeRESTRequest(this.folderPath, requestIndex) | ||||
}, | ||||
}, | }, | |||
}) | }) | |||
</script> | </script> | |||
End of changes. 32 change blocks. | ||||
118 lines changed or deleted | 171 lines changed or added |