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-if="collectionsType.selectedTeam.myRole !== 'VIEWER'" | v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'" | |||
v-tippy="{ theme: 'tooltip' }" | v-tippy="{ theme: 'tooltip' }" | |||
svg="folder-plus" | :icon="IconFilePlus" | |||
:title="$t('folder.new')" | :title="t('request.new')" | |||
class="hidden group-hover:inline-flex" | class="hidden group-hover:inline-flex" | |||
@click.native="$emit('add-folder', { folder, path: folderPath })" | @click="$emit('add-request', { folder, path: folderPath })" | |||
/> | ||||
<ButtonSecondary | ||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'" | ||||
v-tippy="{ theme: 'tooltip' }" | ||||
:icon="IconFolderPlus" | ||||
:title="t('folder.new')" | ||||
class="hidden group-hover:inline-flex" | ||||
@click="$emit('add-folder', { folder, path: folderPath })" | ||||
/> | /> | |||
<span> | <span> | |||
<tippy | <tippy | |||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'" | v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'" | |||
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="options.tippy().hide()" | ||||
> | ||||
<SmartItem | ||||
ref="requestAction" | ||||
:icon="IconFilePlus" | ||||
:label="t('request.new')" | ||||
:shortcut="['R']" | ||||
@click=" | ||||
() => { | ||||
$emit('add-request', { folder, 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']" | ||||
:loading="exportLoading" | ||||
@click="exportFolder" | ||||
/> | ||||
<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"> | |||
<CollectionsTeamsFolder | <!-- Referring to this component only (this is recursive) --> | |||
<Folder | ||||
v-for="(subFolder, subFolderIndex) in folder.children" | v-for="(subFolder, subFolderIndex) in folder.children" | |||
: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" | |||
:loading-collection-i-ds="loadingCollectionIDs" | ||||
@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)" | |||
@update-team-collections="$emit('update-team-collections')" | @update-team-collections="$emit('update-team-collections')" | |||
@select="$emit('select', $event)" | @select="$emit('select', $event)" | |||
@expand-collection="expandCollection" | @expand-collection="expandCollection" | |||
@remove-request="removeRequest" | @remove-request="$emit('remove-request', $event)" | |||
@remove-folder="$emit('remove-folder', $event)" | ||||
@duplicate-request="$emit('duplicate-request', $event)" | @duplicate-request="$emit('duplicate-request', $event)" | |||
/> | /> | |||
<CollectionsTeamsRequest | <CollectionsTeamsRequest | |||
v-for="(request, index) in folder.requests" | v-for="(request, index) in folder.requests" | |||
:key="`request-${index}`" | :key="`request-${index}`" | |||
:request="request.request" | :request="request.request" | |||
:collection-index="collectionIndex" | :collection-index="collectionIndex" | |||
:folder-index="folderIndex" | :folder-index="folderIndex" | |||
:folder-name="folder.name" | :folder-name="folder.name" | |||
:request-index="request.id" | :request-index="request.id" | |||
:doc="doc" | ||||
:save-request="saveRequest" | :save-request="saveRequest" | |||
:collections-type="collectionsType" | :collections-type="collectionsType" | |||
:picked="picked" | :picked="picked" | |||
:collection-i-d="folder.id" | :collection-i-d="folder.id" | |||
@edit-request="$emit('edit-request', $event)" | @edit-request="$emit('edit-request', $event)" | |||
@select="$emit('select', $event)" | @select="$emit('select', $event)" | |||
@remove-request="removeRequest" | @remove-request="$emit('remove-request', $event)" | |||
@duplicate-request="$emit('duplicate-request', $event)" | @duplicate-request="$emit('duplicate-request', $event)" | |||
/> | /> | |||
<div | <div | |||
v-if=" | v-if="loadingCollectionIDs.includes(folder.id)" | |||
class="flex flex-col items-center justify-center p-4" | ||||
> | ||||
<SmartSpinner class="my-4" /> | ||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span> | ||||
</div> | ||||
<div | ||||
v-else-if=" | ||||
(folder.children == undefined || folder.children.length === 0) && | (folder.children == undefined || folder.children.length === 0) && | |||
(folder.requests == undefined || folder.requests.length === 0) | (folder.requests == undefined || 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 | class="inline-flex flex-col object-contain object-center w-16 h-16 m | |||
lex" | 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 IconMoreVertical from "~icons/lucide/more-vertical" | |||
import IconEdit from "~icons/lucide/edit" | ||||
import IconDownload from "~icons/lucide/download" | ||||
import IconTrash2 from "~icons/lucide/trash-2" | ||||
import IconFilePlus from "~icons/lucide/file-plus" | ||||
import IconCheckCircle from "~icons/lucide/check-circle" | ||||
import IconFolderPlus from "~icons/lucide/folder-plus" | ||||
import IconFolder from "~icons/lucide/folder" | ||||
import IconFolderOpen from "~icons/lucide/folder-open" | ||||
import { defineComponent, ref } from "vue" | ||||
import * as E from "fp-ts/Either" | import * as E from "fp-ts/Either" | |||
import { | ||||
getCompleteCollectionTree, | ||||
teamCollToHoppRESTColl, | ||||
} from "~/helpers/backend/helpers" | ||||
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest" | import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest" | |||
import * as teamUtils from "~/helpers/teams/utils" | import { useI18n } from "@composables/i18n" | |||
import { useToast } from "@composables/toast" | ||||
import { useColorMode } from "@composables/theming" | ||||
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: () => ({}) }, | |||
loadingCollectionIDs: { type: Array, default: () => [] }, | ||||
}, | }, | |||
emits: [ | ||||
"add-request", | ||||
"add-folder", | ||||
"edit-folder", | ||||
"update-team-collections", | ||||
"edit-request", | ||||
"remove-request", | ||||
"duplicate-request", | ||||
"select", | ||||
"remove-folder", | ||||
"expand-collection", | ||||
], | ||||
setup() { | setup() { | |||
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), | ||||
exportLoading: ref<boolean>(false), | ||||
toast: useToast(), | ||||
t: useI18n(), | ||||
colorMode: useColorMode(), | ||||
IconFilePlus, | ||||
IconFolderPlus, | ||||
IconCheckCircle, | ||||
IconFolder, | ||||
IconFolderOpen, | ||||
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 === "teams-folder" && | this.picked.pickedType === "teams-folder" && | |||
this.picked.folderID === this.folder.id | this.picked.folderID === this.folder.id | |||
) | ) | |||
}, | }, | |||
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: { | |||
async exportFolder() { | ||||
this.exportLoading = true | ||||
const result = await getCompleteCollectionTree(this.folder.id)() | ||||
if (E.isLeft(result)) { | ||||
this.toast.error(this.t("error.something_went_wrong").toString()) | ||||
console.log(result.left) | ||||
this.exportLoading = false | ||||
this.options.tippy().hide() | ||||
return | ||||
} | ||||
const hoppColl = teamCollToHoppRESTColl(result.right) | ||||
const collectionJSON = JSON.stringify(hoppColl) | ||||
const file = new Blob([collectionJSON], { type: "application/json" }) | ||||
const a = document.createElement("a") | ||||
const url = URL.createObjectURL(file) | ||||
a.href = url | ||||
a.download = `${hoppColl.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) | ||||
this.exportLoading = false | ||||
this.options.tippy().hide() | ||||
}, | ||||
toggleShowChildren() { | toggleShowChildren() { | |||
if (this.$props.saveRequest) | if (this.$props.saveRequest) | |||
this.$emit("select", { | this.$emit("select", { | |||
picked: { | picked: { | |||
pickedType: "teams-folder", | pickedType: "teams-folder", | |||
folderID: this.folder.id, | folderID: this.folder.id, | |||
}, | }, | |||
}) | }) | |||
this.$emit("expand-collection", this.$props.folder.id) | this.$emit("expand-collection", this.$props.folder.id) | |||
this.showChildren = !this.showChildren | this.showChildren = !this.showChildren | |||
}, | }, | |||
removeFolder() { | removeFolder() { | |||
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") { | this.$emit("remove-folder", { | |||
// Cancel pick if picked collection folder was deleted | collectionsType: this.collectionsType, | |||
if ( | folder: this.folder, | |||
this.picked && | }) | |||
this.picked.pickedType === "teams-folder" && | ||||
this.picked.folderID === this.folder.id | ||||
) { | ||||
this.$emit("select", { picked: null }) | ||||
} | ||||
teamUtils | ||||
.deleteCollection(this.$apollo, this.folder.id) | ||||
.then(() => { | ||||
this.$toast.success(`${this.$t("state.deleted")}`) | ||||
this.$emit("update-team-collections") | ||||
}) | ||||
.catch((e) => { | ||||
this.$toast.error(`${this.$t("error.something_went_wrong")}`) | ||||
console.error(e) | ||||
}) | ||||
this.$emit("update-team-collections") | ||||
} | ||||
}, | }, | |||
expandCollection(collectionID) { | expandCollection(collectionID: number) { | |||
this.$emit("expand-collection", collectionID) | this.$emit("expand-collection", collectionID) | |||
}, | }, | |||
async dropEvent({ dataTransfer }) { | async dropEvent({ dataTransfer }: any) { | |||
this.dragging = !this.dragging | this.dragging = !this.dragging | |||
const requestIndex = dataTransfer.getData("requestIndex") | const requestIndex = dataTransfer.getData("requestIndex") | |||
const moveRequestResult = await moveRESTTeamRequest( | const moveRequestResult = await moveRESTTeamRequest( | |||
requestIndex, | requestIndex, | |||
this.folder.id | this.folder.id | |||
)() | )() | |||
if (E.isLeft(moveRequestResult)) | if (E.isLeft(moveRequestResult)) | |||
this.$toast.error(`${this.$t("error.something_went_wrong")}`) | this.toast.error(`${this.t("error.something_went_wrong")}`) | |||
}, | ||||
removeRequest({ collectionIndex, folderName, requestIndex }) { | ||||
this.$emit("remove-request", { | ||||
collectionIndex, | ||||
folderName, | ||||
requestIndex, | ||||
}) | ||||
}, | }, | |||
}, | }, | |||
}) | }) | |||
</script> | </script> | |||
End of changes. 37 change blocks. | ||||
126 lines changed or deleted | 213 lines changed or added |