feat: added reordering and moving for collection (#2916)
This commit is contained in:
@@ -1,138 +1,160 @@
|
||||
<template>
|
||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
@dragover.prevent
|
||||
@drop.prevent="dropEvent"
|
||||
@dragover="dragging = true"
|
||||
@drop="dragging = false"
|
||||
@dragleave="dragging = false"
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options?.tippy.show()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center px-4 cursor-pointer"
|
||||
@click="emit('toggle-children')"
|
||||
class="h-1 w-full transition"
|
||||
:class="[
|
||||
{
|
||||
'bg-accentDark': ordering && notSameDestination,
|
||||
},
|
||||
]"
|
||||
@drop="orderUpdateCollectionEvent"
|
||||
@dragover.prevent="ordering = true"
|
||||
@dragleave="ordering = false"
|
||||
@dragend="resetDragState"
|
||||
></div>
|
||||
<div class="flex flex-col relative">
|
||||
<div
|
||||
class="absolute bg-accent opacity-0 pointer-events-none inset-0 z-1 transition"
|
||||
:class="{
|
||||
'opacity-25': dragging && notSameDestination,
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="flex items-stretch group relative z-3"
|
||||
:draggable="!hasNoTeamAccess"
|
||||
@dragstart="dragStart"
|
||||
@drop="dropEvent"
|
||||
@dragover="dragging = true"
|
||||
@dragleave="dragging = false"
|
||||
@dragend="resetDragState"
|
||||
@contextmenu.prevent="options?.tippy.show()"
|
||||
>
|
||||
<component
|
||||
:is="collectionIcon"
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 transition cursor-pointer group-hover:text-secondaryDark"
|
||||
@click="emit('toggle-children')"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ collectionName }}
|
||||
<span
|
||||
class="flex items-center justify-center px-4 cursor-pointer"
|
||||
@click="emit('toggle-children')"
|
||||
>
|
||||
<HoppSmartSpinner v-if="isCollLoading" />
|
||||
<component
|
||||
:is="collectionIcon"
|
||||
v-else
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<div v-if="!hasNoTeamAccess" class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="emit('add-request')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFolderPlus"
|
||||
:title="t('folder.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="emit('add-folder')"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@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()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
ref="requestAction"
|
||||
:icon="IconFilePlus"
|
||||
:label="t('request.new')"
|
||||
:shortcut="['R']"
|
||||
@click="
|
||||
() => {
|
||||
emit('add-request')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="folderAction"
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('folder.new')"
|
||||
:shortcut="['N']"
|
||||
@click="
|
||||
() => {
|
||||
emit('add-folder')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
emit('edit-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="exportAction"
|
||||
:icon="IconDownload"
|
||||
:label="t('export.title')"
|
||||
:shortcut="['X']"
|
||||
:loading="exportLoading"
|
||||
@click="
|
||||
() => {
|
||||
emit('export-data'),
|
||||
collectionsType === 'my-collections' ? hide() : null
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
emit('remove-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<span
|
||||
class="flex flex-1 min-w-0 py-2 pr-2 transition cursor-pointer group-hover:text-secondaryDark"
|
||||
@click="emit('toggle-children')"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ collectionName }}
|
||||
</span>
|
||||
</span>
|
||||
<div v-if="!hasNoTeamAccess" class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="emit('add-request')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconFolderPlus"
|
||||
:title="t('folder.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="emit('add-folder')"
|
||||
/>
|
||||
<span>
|
||||
<tippy
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMoreVertical"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@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()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
ref="requestAction"
|
||||
:icon="IconFilePlus"
|
||||
:label="t('request.new')"
|
||||
:shortcut="['R']"
|
||||
@click="
|
||||
() => {
|
||||
emit('add-request')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="folderAction"
|
||||
:icon="IconFolderPlus"
|
||||
:label="t('folder.new')"
|
||||
:shortcut="['N']"
|
||||
@click="
|
||||
() => {
|
||||
emit('add-folder')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="edit"
|
||||
:icon="IconEdit"
|
||||
:label="t('action.edit')"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
emit('edit-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="exportAction"
|
||||
:icon="IconDownload"
|
||||
:label="t('export.title')"
|
||||
:shortcut="['X']"
|
||||
:loading="exportLoading"
|
||||
@click="
|
||||
() => {
|
||||
emit('export-data'),
|
||||
collectionsType === 'my-collections' ? hide() : null
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="t('action.delete')"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
emit('remove-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,6 +182,11 @@ type FolderType = "collection" | "folder"
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Object as PropType<HoppCollection<HoppRESTRequest> | TeamCollection>,
|
||||
default: () => ({}),
|
||||
@@ -185,7 +212,7 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
type: Boolean as PropType<boolean | null>,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
@@ -199,6 +226,11 @@ const props = defineProps({
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
collectionMoveLoading: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -209,6 +241,9 @@ const emit = defineEmits<{
|
||||
(event: "export-data"): void
|
||||
(event: "remove-collection"): void
|
||||
(event: "drop-event", payload: DataTransfer): void
|
||||
(event: "drag-event", payload: DataTransfer): void
|
||||
(event: "dragging", payload: boolean): void
|
||||
(event: "update-collection-order", payload: DataTransfer): void
|
||||
}>()
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
@@ -220,6 +255,21 @@ const exportAction = ref<HTMLButtonElement | null>(null)
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
|
||||
const dragging = ref(false)
|
||||
const ordering = ref(false)
|
||||
const dropItemID = ref("")
|
||||
|
||||
// Used to determine if the collection is being dragged to a different destination
|
||||
// This is used to make the highlight effect work
|
||||
watch(
|
||||
() => dragging.value,
|
||||
(val) => {
|
||||
if (val && notSameDestination.value) {
|
||||
emit("dragging", true)
|
||||
} else {
|
||||
emit("dragging", false)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const collectionIcon = computed(() => {
|
||||
if (props.isSelected) return IconCheckCircle
|
||||
@@ -243,10 +293,47 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const dropEvent = ({ dataTransfer }: DragEvent) => {
|
||||
const dragStart = ({ dataTransfer }: DragEvent) => {
|
||||
if (dataTransfer) {
|
||||
emit("drag-event", dataTransfer)
|
||||
dropItemID.value = dataTransfer.getData("collectionIndex")
|
||||
dragging.value = !dragging.value
|
||||
emit("drop-event", dataTransfer)
|
||||
}
|
||||
}
|
||||
|
||||
const dropEvent = (e: DragEvent) => {
|
||||
if (e.dataTransfer) {
|
||||
e.stopPropagation()
|
||||
emit("drop-event", e.dataTransfer)
|
||||
dragging.value = !dragging.value
|
||||
dropItemID.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const orderUpdateCollectionEvent = (e: DragEvent) => {
|
||||
if (e.dataTransfer) {
|
||||
e.stopPropagation()
|
||||
emit("update-collection-order", e.dataTransfer)
|
||||
ordering.value = !ordering.value
|
||||
dropItemID.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const notSameDestination = computed(() => {
|
||||
return dropItemID.value !== props.id
|
||||
})
|
||||
|
||||
const isCollLoading = computed(() => {
|
||||
if (props.collectionMoveLoading.length > 0 && props.data.id) {
|
||||
return props.collectionMoveLoading.includes(props.data.id)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const resetDragState = () => {
|
||||
dragging.value = false
|
||||
ordering.value = false
|
||||
dropItemID.value = ""
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col flex-1 bg-primary">
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||
:style="
|
||||
@@ -33,9 +33,12 @@
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<SmartTree :adapter="myAdapter">
|
||||
<template #content="{ node, toggleChildren, isOpen }">
|
||||
<template
|
||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
<CollectionsCollection
|
||||
v-if="node.data.type === 'collections'"
|
||||
:id="node.id"
|
||||
:data="node.data.data.data"
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
@@ -72,6 +75,11 @@
|
||||
"
|
||||
@remove-collection="emit('remove-collection', node.id)"
|
||||
@drop-event="dropEvent($event, node.id)"
|
||||
@drag-event="dragEvent($event, node.id)"
|
||||
@update-collection-order="updateCollectionOrder($event, node.id)"
|
||||
@dragging="
|
||||
(isDraging) => highlightChildren(isDraging ? node.id : null)
|
||||
"
|
||||
@toggle-children="
|
||||
() => {
|
||||
toggleChildren(),
|
||||
@@ -85,6 +93,7 @@
|
||||
/>
|
||||
<CollectionsCollection
|
||||
v-if="node.data.type === 'folders'"
|
||||
:id="node.id"
|
||||
:data="node.data.data.data"
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
@@ -121,6 +130,11 @@
|
||||
"
|
||||
@remove-collection="emit('remove-folder', node.id)"
|
||||
@drop-event="dropEvent($event, node.id)"
|
||||
@drag-event="dragEvent($event, node.id)"
|
||||
@update-collection-order="updateCollectionOrder($event, node.id)"
|
||||
@dragging="
|
||||
(isDraging) => highlightChildren(isDraging ? node.id : null)
|
||||
"
|
||||
@toggle-children="
|
||||
() => {
|
||||
toggleChildren(),
|
||||
@@ -182,7 +196,13 @@
|
||||
@drag-request="
|
||||
dragRequest($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: pathToIndex(node.id),
|
||||
requestIndex: node.id,
|
||||
})
|
||||
"
|
||||
@update-request-order="
|
||||
updateRequestOrder($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: node.id,
|
||||
})
|
||||
"
|
||||
/>
|
||||
@@ -413,7 +433,29 @@ const emit = defineEmits<{
|
||||
payload: {
|
||||
folderPath: string
|
||||
requestIndex: string
|
||||
collectionIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "drop-collection",
|
||||
payload: {
|
||||
collectionIndexDragged: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "update-request-order",
|
||||
payload: {
|
||||
dragedRequestIndex: string
|
||||
destinationRequestIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "update-collection-order",
|
||||
payload: {
|
||||
dragedCollectionIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(event: "select", payload: Picked | null): void
|
||||
@@ -502,6 +544,10 @@ const selectRequest = (data: {
|
||||
}
|
||||
}
|
||||
|
||||
const dragEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
|
||||
dataTransfer.setData("collectionIndex", collectionIndex)
|
||||
}
|
||||
|
||||
const dragRequest = (
|
||||
dataTransfer: DataTransfer,
|
||||
{
|
||||
@@ -514,13 +560,56 @@ const dragRequest = (
|
||||
dataTransfer.setData("requestIndex", requestIndex)
|
||||
}
|
||||
|
||||
const dropEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
|
||||
const dropEvent = (
|
||||
dataTransfer: DataTransfer,
|
||||
destinationCollectionIndex: string
|
||||
) => {
|
||||
const folderPath = dataTransfer.getData("folderPath")
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
emit("drop-request", {
|
||||
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
|
||||
|
||||
if (folderPath && requestIndex) {
|
||||
emit("drop-request", {
|
||||
folderPath,
|
||||
requestIndex,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
} else {
|
||||
emit("drop-collection", {
|
||||
collectionIndexDragged,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateRequestOrder = (
|
||||
dataTransfer: DataTransfer,
|
||||
{
|
||||
folderPath,
|
||||
requestIndex,
|
||||
collectionIndex,
|
||||
}: { folderPath: string | null; requestIndex: string }
|
||||
) => {
|
||||
if (!folderPath) return
|
||||
const dragedRequestIndex = dataTransfer.getData("requestIndex")
|
||||
const destinationRequestIndex = requestIndex
|
||||
const destinationCollectionIndex = folderPath
|
||||
|
||||
emit("update-request-order", {
|
||||
dragedRequestIndex,
|
||||
destinationRequestIndex,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
}
|
||||
|
||||
const updateCollectionOrder = (
|
||||
dataTransfer: DataTransfer,
|
||||
destinationCollectionIndex: string
|
||||
) => {
|
||||
const dragedCollectionIndex = dataTransfer.getData("collectionIndex")
|
||||
|
||||
emit("update-collection-order", {
|
||||
dragedCollectionIndex,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
<template>
|
||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="h-1"
|
||||
:class="[
|
||||
{
|
||||
'bg-accentDark': ordering,
|
||||
},
|
||||
]"
|
||||
@drop="dropEvent"
|
||||
@dragover.prevent="ordering = true"
|
||||
@dragleave="ordering = false"
|
||||
@dragend="ordering = false"
|
||||
></div>
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
draggable="true"
|
||||
:draggable="!hasNoTeamAccess"
|
||||
@dragstart="dragStart"
|
||||
@dragover.stop
|
||||
@dragover.prevent="dragging = true"
|
||||
@dragleave="dragging = false"
|
||||
@dragend="dragging = false"
|
||||
@contextmenu.prevent="options?.tippy.show()"
|
||||
@@ -20,6 +32,7 @@
|
||||
class="svg-icons"
|
||||
:class="{ 'text-accent': isSelected }"
|
||||
/>
|
||||
<HoppSmartSpinner v-else-if="isRequestLoading" />
|
||||
<span v-else class="font-semibold truncate text-tiny">
|
||||
{{ request.method }}
|
||||
</span>
|
||||
@@ -149,6 +162,11 @@ const props = defineProps({
|
||||
default: () => ({}),
|
||||
required: true,
|
||||
},
|
||||
requestID: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
collectionsType: {
|
||||
type: String as PropType<CollectionType>,
|
||||
default: "my-collections",
|
||||
@@ -175,10 +193,15 @@ const props = defineProps({
|
||||
required: false,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
type: Boolean as PropType<boolean | null>,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
requestMoveLoading: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -187,6 +210,7 @@ const emit = defineEmits<{
|
||||
(event: "remove-request"): void
|
||||
(event: "select-request"): void
|
||||
(event: "drag-request", payload: DataTransfer): void
|
||||
(event: "update-request-order", payload: DataTransfer): void
|
||||
}>()
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
@@ -196,6 +220,7 @@ const options = ref<TippyComponent | null>(null)
|
||||
const duplicate = ref<HTMLButtonElement | null>(null)
|
||||
|
||||
const dragging = ref(false)
|
||||
const ordering = ref(false)
|
||||
|
||||
const requestMethodLabels = {
|
||||
get: "text-green-500",
|
||||
@@ -228,8 +253,24 @@ const selectRequest = () => {
|
||||
|
||||
const dragStart = ({ dataTransfer }: DragEvent) => {
|
||||
if (dataTransfer) {
|
||||
dragging.value = !dragging.value
|
||||
emit("drag-request", dataTransfer)
|
||||
dragging.value = !dragging.value
|
||||
}
|
||||
}
|
||||
|
||||
const dropEvent = (e: DragEvent) => {
|
||||
if (e.dataTransfer) {
|
||||
e.stopPropagation()
|
||||
ordering.value = !ordering.value
|
||||
emit("update-request-order", e.dataTransfer)
|
||||
}
|
||||
}
|
||||
|
||||
const isRequestLoading = computed(() => {
|
||||
if (props.requestMoveLoading.length > 0 && props.requestID) {
|
||||
return props.requestMoveLoading.includes(props.requestID)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col flex-1 bg-primary">
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||
:style="
|
||||
@@ -47,14 +47,18 @@
|
||||
</div>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<SmartTree :adapter="teamAdapter">
|
||||
<template #content="{ node, toggleChildren, isOpen }">
|
||||
<template
|
||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
<CollectionsCollection
|
||||
v-if="node.data.type === 'collections'"
|
||||
:id="node.data.data.data.id"
|
||||
:data="node.data.data.data"
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
:export-loading="exportLoading"
|
||||
:has-no-team-access="hasNoTeamAccess"
|
||||
:collection-move-loading="collectionMoveLoading"
|
||||
:is-selected="
|
||||
isSelected({
|
||||
collectionID: node.id,
|
||||
@@ -87,6 +91,15 @@
|
||||
emit('export-data', node.data.data.data)
|
||||
"
|
||||
@remove-collection="emit('remove-collection', node.id)"
|
||||
@drop-event="dropEvent($event, node.id)"
|
||||
@drag-event="dragEvent($event, node.id)"
|
||||
@update-collection-order="
|
||||
updateCollectionOrder($event, node.data.data.data.id)
|
||||
"
|
||||
@dragging="
|
||||
(isDraging) =>
|
||||
highlightChildren(isDraging ? node.data.data.data.id : null)
|
||||
"
|
||||
@toggle-children="
|
||||
() => {
|
||||
toggleChildren(),
|
||||
@@ -100,11 +113,13 @@
|
||||
/>
|
||||
<CollectionsCollection
|
||||
v-if="node.data.type === 'folders'"
|
||||
:id="node.data.data.data.id"
|
||||
:data="node.data.data.data"
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
:export-loading="exportLoading"
|
||||
:has-no-team-access="hasNoTeamAccess"
|
||||
:collection-move-loading="collectionMoveLoading"
|
||||
:is-selected="
|
||||
isSelected({
|
||||
folderID: node.data.data.data.id,
|
||||
@@ -139,6 +154,15 @@
|
||||
node.data.type === 'folders' &&
|
||||
emit('remove-folder', node.data.data.data.id)
|
||||
"
|
||||
@drop-event="dropEvent($event, node.data.data.data.id)"
|
||||
@drag-event="dragEvent($event, node.data.data.data.id)"
|
||||
@update-collection-order="
|
||||
updateCollectionOrder($event, node.data.data.data.id)
|
||||
"
|
||||
@dragging="
|
||||
(isDraging) =>
|
||||
highlightChildren(isDraging ? node.data.data.data.id : null)
|
||||
"
|
||||
@toggle-children="
|
||||
() => {
|
||||
toggleChildren(),
|
||||
@@ -153,10 +177,12 @@
|
||||
<CollectionsRequest
|
||||
v-if="node.data.type === 'requests'"
|
||||
:request="node.data.data.data.request"
|
||||
:request-i-d="node.data.data.data.id"
|
||||
:collections-type="collectionsType.type"
|
||||
:duplicate-loading="duplicateLoading"
|
||||
:is-active="isActiveRequest(node.data.data.data.id)"
|
||||
:has-no-team-access="hasNoTeamAccess"
|
||||
:request-move-loading="requestMoveLoading"
|
||||
:is-selected="
|
||||
isSelected({
|
||||
requestID: node.data.data.data.id,
|
||||
@@ -190,18 +216,31 @@
|
||||
requestIndex: node.data.data.data.id,
|
||||
})
|
||||
"
|
||||
@drag-request="
|
||||
dragRequest($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: node.data.data.data.id,
|
||||
})
|
||||
"
|
||||
@update-request-order="
|
||||
updateRequestOrder($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: node.data.data.data.id,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #emptyNode="{ node }">
|
||||
<div v-if="node === null">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:alt="`${t('empty.collection')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collections") }}
|
||||
@@ -213,11 +252,12 @@
|
||||
filled
|
||||
outline
|
||||
:title="t('team.no_access')"
|
||||
:label="t('add.new')"
|
||||
:label="t('action.new')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
:label="t('action.new')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-add')"
|
||||
@@ -227,6 +267,7 @@
|
||||
<div
|
||||
v-else-if="node.data.type === 'collections'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
@@ -235,34 +276,13 @@
|
||||
:alt="`${t('empty.collection')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collection") }}
|
||||
{{ t("empty.collections") }}
|
||||
</span>
|
||||
<HoppButtonSecondary
|
||||
v-if="hasNoTeamAccess"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
filled
|
||||
outline
|
||||
:title="t('team.no_access')"
|
||||
:label="t('add.new')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
node.data.type === 'collections' &&
|
||||
emit('add-folder', {
|
||||
path: node.id,
|
||||
folder: node.data.data.data,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="node.data.type === 'folders'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
@@ -347,6 +367,16 @@ const props = defineProps({
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
collectionMoveLoading: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
requestMoveLoading: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -410,6 +440,36 @@ const emit = defineEmits<{
|
||||
folderPath?: string | undefined
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "drop-request",
|
||||
payload: {
|
||||
folderPath: string
|
||||
requestIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "drop-collection",
|
||||
payload: {
|
||||
collectionIndexDragged: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "update-request-order",
|
||||
payload: {
|
||||
dragedRequestIndex: string
|
||||
destinationRequestIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "update-collection-order",
|
||||
payload: {
|
||||
dragedCollectionIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}
|
||||
): void
|
||||
(event: "select", payload: Picked | null): void
|
||||
(event: "expand-team-collection", payload: string): void
|
||||
(event: "display-modal-add"): void
|
||||
@@ -493,6 +553,74 @@ const selectRequest = (data: {
|
||||
}
|
||||
}
|
||||
|
||||
const dragRequest = (
|
||||
dataTransfer: DataTransfer,
|
||||
{
|
||||
folderPath,
|
||||
requestIndex,
|
||||
}: { folderPath: string | null; requestIndex: string }
|
||||
) => {
|
||||
if (!folderPath) return
|
||||
dataTransfer.setData("folderPath", folderPath)
|
||||
dataTransfer.setData("requestIndex", requestIndex)
|
||||
}
|
||||
|
||||
const dragEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
|
||||
dataTransfer.setData("collectionIndex", collectionIndex)
|
||||
}
|
||||
|
||||
const dropEvent = (
|
||||
dataTransfer: DataTransfer,
|
||||
destinationCollectionIndex: string
|
||||
) => {
|
||||
const folderPath = dataTransfer.getData("folderPath")
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
|
||||
if (folderPath && requestIndex) {
|
||||
emit("drop-request", {
|
||||
folderPath,
|
||||
requestIndex,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
} else {
|
||||
emit("drop-collection", {
|
||||
collectionIndexDragged,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateRequestOrder = (
|
||||
dataTransfer: DataTransfer,
|
||||
{
|
||||
folderPath,
|
||||
requestIndex,
|
||||
}: { folderPath: string | null; requestIndex: string }
|
||||
) => {
|
||||
if (!folderPath) return
|
||||
const dragedRequestIndex = dataTransfer.getData("requestIndex")
|
||||
const destinationRequestIndex = requestIndex
|
||||
const destinationCollectionIndex = folderPath
|
||||
|
||||
emit("update-request-order", {
|
||||
dragedRequestIndex,
|
||||
destinationRequestIndex,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
}
|
||||
|
||||
const updateCollectionOrder = (
|
||||
dataTransfer: DataTransfer,
|
||||
destinationCollectionIndex: string
|
||||
) => {
|
||||
const dragedCollectionIndex = dataTransfer.getData("collectionIndex")
|
||||
|
||||
emit("update-collection-order", {
|
||||
dragedCollectionIndex,
|
||||
destinationCollectionIndex,
|
||||
})
|
||||
}
|
||||
|
||||
type TeamCollections = {
|
||||
type: "collections"
|
||||
data: {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<template>
|
||||
<div :class="{ 'rounded border border-divider': saveRequest }">
|
||||
<div
|
||||
:class="{
|
||||
'rounded border border-divider': saveRequest,
|
||||
'bg-primaryDark': draggingToRoot,
|
||||
}"
|
||||
class="flex-1"
|
||||
@drop.prevent="dropToRoot"
|
||||
@dragover.prevent="draggingToRoot = true"
|
||||
@dragend="draggingToRoot = false"
|
||||
>
|
||||
<div
|
||||
class="sticky z-10 flex flex-col flex-shrink-0 overflow-x-auto rounded-t bg-primary"
|
||||
:style="
|
||||
@@ -44,6 +53,9 @@
|
||||
@export-data="exportData"
|
||||
@remove-collection="removeCollection"
|
||||
@remove-folder="removeFolder"
|
||||
@drop-collection="dropCollection"
|
||||
@update-request-order="updateRequestOrder"
|
||||
@update-collection-order="updateCollectionOrder"
|
||||
@edit-request="editRequest"
|
||||
@duplicate-request="duplicateRequest"
|
||||
@remove-request="removeRequest"
|
||||
@@ -83,6 +95,8 @@
|
||||
:duplicate-loading="duplicateLoading"
|
||||
:save-request="saveRequest"
|
||||
:picked="picked"
|
||||
:collection-move-loading="collectionMoveLoading"
|
||||
:request-move-loading="requestMoveLoading"
|
||||
@add-request="addRequest"
|
||||
@add-folder="addFolder"
|
||||
@edit-collection="editCollection"
|
||||
@@ -95,12 +109,22 @@
|
||||
@remove-request="removeRequest"
|
||||
@select-request="selectRequest"
|
||||
@select="selectPicked"
|
||||
@drop-request="dropRequest"
|
||||
@drop-collection="dropCollection"
|
||||
@update-request-order="updateRequestOrder"
|
||||
@update-collection-order="updateCollectionOrder"
|
||||
@expand-team-collection="expandTeamCollection"
|
||||
@display-modal-add="displayModalAdd(true)"
|
||||
@display-modal-import-export="displayModalImportExport(true)"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
<div
|
||||
class="hidden bg-primaryDark flex-col flex-1 items-center py-15 justify-center px-4 text-secondaryLight"
|
||||
:class="{ '!flex': draggingToRoot }"
|
||||
>
|
||||
<component :is="IconListEnd" class="svg-icons !w-8 !h-8" />
|
||||
</div>
|
||||
<CollectionsAdd
|
||||
:show="showModalAdd"
|
||||
:loading-state="modalLoadingState"
|
||||
@@ -195,12 +219,15 @@ import {
|
||||
editRESTCollection,
|
||||
editRESTFolder,
|
||||
editRESTRequest,
|
||||
moveRESTFolder,
|
||||
moveRESTRequest,
|
||||
removeRESTCollection,
|
||||
removeRESTFolder,
|
||||
removeRESTRequest,
|
||||
restCollections$,
|
||||
saveRESTRequestAs,
|
||||
updateRESTRequestOrder,
|
||||
updateRESTCollectionOrder,
|
||||
} from "~/newstore/collections"
|
||||
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
|
||||
import {
|
||||
@@ -226,11 +253,15 @@ import {
|
||||
renameCollection,
|
||||
deleteCollection,
|
||||
importJSONToTeam,
|
||||
moveRESTTeamCollection,
|
||||
updateOrderRESTTeamCollection,
|
||||
} from "~/helpers/backend/mutations/TeamCollection"
|
||||
import {
|
||||
updateTeamRequest,
|
||||
createRequestInCollection,
|
||||
deleteTeamRequest,
|
||||
moveRESTTeamRequest,
|
||||
updateOrderRESTTeamRequest,
|
||||
} from "~/helpers/backend/mutations/TeamRequest"
|
||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
import { Collection as NodeCollection } from "./MyCollections.vue"
|
||||
@@ -244,6 +275,7 @@ import * as E from "fp-ts/Either"
|
||||
import { platform } from "~/platform"
|
||||
import { createCollectionGists } from "~/helpers/gist"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
import IconListEnd from "~icons/lucide/list-end"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -324,6 +356,11 @@ const currentUser = useReadonlyStream(
|
||||
)
|
||||
const myCollections = useReadonlyStream(restCollections$, [], "deep")
|
||||
|
||||
// Draging
|
||||
const draggingToRoot = ref(false)
|
||||
const collectionMoveLoading = ref<string[]>([])
|
||||
const requestMoveLoading = ref<string[]>([])
|
||||
|
||||
// Export - Import refs
|
||||
const collectionJSON = ref("")
|
||||
const exportingTeamCollections = ref(false)
|
||||
@@ -1333,16 +1370,314 @@ const discardRequestChange = () => {
|
||||
confirmChangeToRequest.value = false
|
||||
}
|
||||
|
||||
// Drag and drop functions
|
||||
/**
|
||||
* Used to get the index of the request from the path
|
||||
* @param path The path of the request
|
||||
* @returns The index of the request
|
||||
*/
|
||||
const pathToIndex = computed(() => {
|
||||
return (path: string) => {
|
||||
const pathArr = path.split("/")
|
||||
return parseInt(pathArr[pathArr.length - 1])
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* This function is called when the user drops the request inside a collection
|
||||
* @param payload Object that contains the folder path, request index and the destination collection index
|
||||
*/
|
||||
const dropRequest = (payload: {
|
||||
folderPath: string
|
||||
folderPath?: string | undefined
|
||||
requestIndex: string
|
||||
collectionIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}) => {
|
||||
const { folderPath, requestIndex, collectionIndex } = payload
|
||||
moveRESTRequest(folderPath, parseInt(requestIndex), collectionIndex)
|
||||
const { folderPath, requestIndex, destinationCollectionIndex } = payload
|
||||
if (!requestIndex || !destinationCollectionIndex) return
|
||||
if (collectionsType.value.type === "my-collections" && folderPath) {
|
||||
moveRESTRequest(
|
||||
folderPath,
|
||||
pathToIndex.value(requestIndex),
|
||||
destinationCollectionIndex
|
||||
)
|
||||
toast.success(`${t("request.moved")}`)
|
||||
draggingToRoot.value = false
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
// add the request index to the loading array
|
||||
requestMoveLoading.value.push(requestIndex)
|
||||
|
||||
pipe(
|
||||
moveRESTTeamRequest(destinationCollectionIndex, requestIndex),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
requestMoveLoading.value.splice(
|
||||
requestMoveLoading.value.indexOf(requestIndex),
|
||||
1
|
||||
)
|
||||
},
|
||||
() => {
|
||||
// remove the request index from the loading array
|
||||
requestMoveLoading.value.splice(
|
||||
requestMoveLoading.value.indexOf(requestIndex),
|
||||
1
|
||||
)
|
||||
toast.success(`${t("request.moved")}`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the user moves the collection
|
||||
* to a different collection or folder
|
||||
* @param payload - object containing the collection index dragged and the destination collection index
|
||||
*/
|
||||
const dropCollection = (payload: {
|
||||
collectionIndexDragged: string
|
||||
destinationCollectionIndex: string
|
||||
}) => {
|
||||
const { collectionIndexDragged, destinationCollectionIndex } = payload
|
||||
if (!collectionIndexDragged || !destinationCollectionIndex) return
|
||||
if (collectionIndexDragged === destinationCollectionIndex) return
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
moveRESTFolder(collectionIndexDragged, destinationCollectionIndex)
|
||||
draggingToRoot.value = false
|
||||
toast.success(`${t("collection.moved")}`)
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
// add the collection index to the loading array
|
||||
collectionMoveLoading.value.push(collectionIndexDragged)
|
||||
pipe(
|
||||
moveRESTTeamCollection(
|
||||
collectionIndexDragged,
|
||||
destinationCollectionIndex
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
collectionMoveLoading.value.splice(
|
||||
collectionMoveLoading.value.indexOf(collectionIndexDragged),
|
||||
1
|
||||
)
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("collection.moved")}`)
|
||||
// remove the collection index from the loading array
|
||||
collectionMoveLoading.value.splice(
|
||||
collectionMoveLoading.value.indexOf(collectionIndexDragged),
|
||||
1
|
||||
)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the collection is already in the root
|
||||
* @param id - path of the collection
|
||||
* @returns boolean - true if the collection is already in the root
|
||||
*/
|
||||
const isAlreadyInRoot = computed(() => {
|
||||
return (id: string) => {
|
||||
const indexPath = id.split("/").map((i) => parseInt(i))
|
||||
return indexPath.length === 1
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* This function is called when the user drops the collection
|
||||
* to the root
|
||||
* @param payload - object containing the collection index dragged
|
||||
*/
|
||||
const dropToRoot = ({ dataTransfer }: DragEvent) => {
|
||||
if (dataTransfer) {
|
||||
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
|
||||
if (!collectionIndexDragged) return
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
// check if the collection is already in the root
|
||||
if (isAlreadyInRoot.value(collectionIndexDragged)) {
|
||||
toast.error(`${t("collection.invalid_root_move")}`)
|
||||
} else {
|
||||
moveRESTFolder(collectionIndexDragged, null)
|
||||
toast.success(`${t("collection.moved")}`)
|
||||
}
|
||||
|
||||
draggingToRoot.value = false
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
// add the collection index to the loading array
|
||||
collectionMoveLoading.value.push(collectionIndexDragged)
|
||||
|
||||
// destination collection index is null since we are moving to root
|
||||
pipe(
|
||||
moveRESTTeamCollection(collectionIndexDragged, null),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
collectionMoveLoading.value.splice(
|
||||
collectionMoveLoading.value.indexOf(collectionIndexDragged),
|
||||
1
|
||||
)
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
// remove the collection index from the loading array
|
||||
collectionMoveLoading.value.splice(
|
||||
collectionMoveLoading.value.indexOf(collectionIndexDragged),
|
||||
1
|
||||
)
|
||||
toast.success(`${t("collection.moved")}`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check if the request/collection is being moved to the same parent since reorder is only allowed within the same parent
|
||||
* @param draggedReq - path index of the dragged request
|
||||
* @param destinationReq - path index of the destination request
|
||||
* @returns boolean - true if the request is being moved to the same parent
|
||||
*/
|
||||
const isSameSameParent = computed(
|
||||
() => (draggedReq: string, destinationReq: string) => {
|
||||
const draggedReqIndex = draggedReq.split("/").map((i) => parseInt(i))
|
||||
const destinationReqIndex = destinationReq
|
||||
.split("/")
|
||||
.map((i) => parseInt(i))
|
||||
|
||||
// length of 1 means the request is in the root
|
||||
if (draggedReqIndex.length === 1 && destinationReqIndex.length === 1) {
|
||||
return true
|
||||
} else if (
|
||||
draggedReqIndex[draggedReqIndex.length - 2] ===
|
||||
destinationReqIndex[destinationReqIndex.length - 2]
|
||||
) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* This function is called when the user updates the request order in a collection
|
||||
* @param payload - object containing the request index dragged and the destination request index
|
||||
* with the destination collection index
|
||||
*/
|
||||
const updateRequestOrder = (payload: {
|
||||
dragedRequestIndex: string
|
||||
destinationRequestIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}) => {
|
||||
const {
|
||||
dragedRequestIndex,
|
||||
destinationRequestIndex,
|
||||
destinationCollectionIndex,
|
||||
} = payload
|
||||
|
||||
if (
|
||||
!dragedRequestIndex ||
|
||||
!destinationRequestIndex ||
|
||||
!destinationCollectionIndex
|
||||
)
|
||||
return
|
||||
|
||||
if (dragedRequestIndex === destinationRequestIndex) return
|
||||
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
if (!isSameSameParent.value(dragedRequestIndex, destinationRequestIndex)) {
|
||||
toast.error(`${t("collection.different_parent")}`)
|
||||
} else {
|
||||
updateRESTRequestOrder(
|
||||
pathToIndex.value(dragedRequestIndex),
|
||||
pathToIndex.value(destinationRequestIndex),
|
||||
destinationCollectionIndex
|
||||
)
|
||||
toast.success(`${t("request.order_changed")}`)
|
||||
}
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
// add the request index to the loading array
|
||||
requestMoveLoading.value.push(dragedRequestIndex)
|
||||
|
||||
pipe(
|
||||
updateOrderRESTTeamRequest(
|
||||
dragedRequestIndex,
|
||||
destinationRequestIndex,
|
||||
destinationCollectionIndex
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
requestMoveLoading.value.splice(
|
||||
requestMoveLoading.value.indexOf(dragedRequestIndex),
|
||||
1
|
||||
)
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("request.order_changed")}`)
|
||||
|
||||
// remove the request index from the loading array
|
||||
requestMoveLoading.value.splice(
|
||||
requestMoveLoading.value.indexOf(dragedRequestIndex),
|
||||
1
|
||||
)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the user updates the collection or folder order
|
||||
* @param payload - object containing the collection index dragged and the destination collection index
|
||||
*/
|
||||
const updateCollectionOrder = (payload: {
|
||||
dragedCollectionIndex: string
|
||||
destinationCollectionIndex: string
|
||||
}) => {
|
||||
const { dragedCollectionIndex, destinationCollectionIndex } = payload
|
||||
if (!dragedCollectionIndex || !destinationCollectionIndex) return
|
||||
if (dragedCollectionIndex === destinationCollectionIndex) return
|
||||
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
if (
|
||||
!isSameSameParent.value(dragedCollectionIndex, destinationCollectionIndex)
|
||||
) {
|
||||
toast.error(`${t("collection.different_parent")}`)
|
||||
} else {
|
||||
updateRESTCollectionOrder(
|
||||
dragedCollectionIndex,
|
||||
destinationCollectionIndex
|
||||
)
|
||||
toast.success(`${t("collection.order_changed")}`)
|
||||
}
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
collectionMoveLoading.value.push(dragedCollectionIndex)
|
||||
pipe(
|
||||
updateOrderRESTTeamCollection(
|
||||
dragedCollectionIndex,
|
||||
destinationCollectionIndex
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
collectionMoveLoading.value.splice(
|
||||
collectionMoveLoading.value.indexOf(dragedCollectionIndex),
|
||||
1
|
||||
)
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("collection.order_changed")}`)
|
||||
collectionMoveLoading.value.splice(
|
||||
collectionMoveLoading.value.indexOf(dragedCollectionIndex),
|
||||
1
|
||||
)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
}
|
||||
// Import - Export Collection functions
|
||||
/**
|
||||
* Export the whole my collection or specific team collection to JSON
|
||||
@@ -1525,28 +1860,38 @@ const resetSelectedData = () => {
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
if (err.type === "network_error") {
|
||||
console.error(err)
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_coll/short_title":
|
||||
console.error(err)
|
||||
return t("collection.name_length_insufficient")
|
||||
case "team/invalid_coll_id":
|
||||
console.error(err)
|
||||
return t("team.invalid_id")
|
||||
case "bug/team_coll/no_coll_id":
|
||||
case "team_req/invalid_target_id":
|
||||
return t("team.invalid_coll_id")
|
||||
case "team/not_required_role":
|
||||
console.error(err)
|
||||
return t("profile.no_permission")
|
||||
case "team_req/not_required_role":
|
||||
console.error(err)
|
||||
return t("profile.no_permission")
|
||||
case "Forbidden resource":
|
||||
console.error(err)
|
||||
return t("profile.no_permission")
|
||||
case "team_req/not_found":
|
||||
return t("team.no_request_found")
|
||||
case "bug/team_req/no_req_id":
|
||||
return t("team.no_request_found")
|
||||
case "team/collection_is_parent_coll":
|
||||
return t("team.parent_coll_move")
|
||||
case "team/target_and_destination_collection_are_same":
|
||||
return t("team.same_target_destination")
|
||||
case "team/target_collection_is_already_root_collection":
|
||||
return t("collection.invalid_root_move")
|
||||
case "team_req/requests_not_from_same_collection":
|
||||
return t("request.different_collection")
|
||||
case "team/team_collections_have_different_parents":
|
||||
return t("collection.different_parent")
|
||||
default:
|
||||
console.error(err)
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,15 @@
|
||||
:node-item="rootNode"
|
||||
:adapter="adapter as SmartTreeAdapter<T>"
|
||||
>
|
||||
<template #default="{ node, toggleChildren, isOpen }">
|
||||
<template
|
||||
#default="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
<slot
|
||||
name="content"
|
||||
:node="node as TreeNode<T>"
|
||||
:toggle-children="toggleChildren as () => void"
|
||||
:is-open="isOpen as boolean"
|
||||
:highlight-children="(id:string|null) => highlightChildren(id)"
|
||||
></slot>
|
||||
</template>
|
||||
<template #emptyNode="{ node }">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
:node="nodeItem"
|
||||
:toggle-children="toggleNodeChildren"
|
||||
:is-open="isNodeOpen"
|
||||
:highlight-children="(id:string|null) => highlightNodeChildren(id)"
|
||||
></slot>
|
||||
|
||||
<!-- This is a performance optimization trick -->
|
||||
@@ -20,6 +21,9 @@
|
||||
<div
|
||||
v-if="childNodes.status === 'loaded' && childNodes.data.length > 0"
|
||||
class="flex flex-col flex-1 truncate"
|
||||
:class="{
|
||||
'bg-divider': highlightNode,
|
||||
}"
|
||||
>
|
||||
<TreeBranch
|
||||
v-for="childNode in childNodes.data"
|
||||
@@ -28,12 +32,20 @@
|
||||
:adapter="adapter"
|
||||
>
|
||||
<!-- The child slot is given a dynamic name in order to not break Volar -->
|
||||
<template #[CHILD_SLOT_NAME]="{ node, toggleChildren, isOpen }">
|
||||
<template
|
||||
#[CHILD_SLOT_NAME]="{
|
||||
node,
|
||||
toggleChildren,
|
||||
isOpen,
|
||||
highlightChildren,
|
||||
}"
|
||||
>
|
||||
<!-- Casting to help with type checking -->
|
||||
<slot
|
||||
:node="node as TreeNode<T>"
|
||||
:toggle-children="toggleChildren as () => void"
|
||||
:is-open="isOpen as boolean"
|
||||
:highlight-children="(id:string|null) => highlightChildren(id) as void"
|
||||
></slot>
|
||||
</template>
|
||||
<template #emptyNode="{ node }">
|
||||
@@ -87,6 +99,8 @@ const childrenRendered = ref(false)
|
||||
const showChildren = ref(false)
|
||||
const isNodeOpen = ref(false)
|
||||
|
||||
const highlightNode = ref(false)
|
||||
|
||||
/**
|
||||
* Fetch the child nodes from the adapter by passing the node id of the current node
|
||||
*/
|
||||
@@ -100,4 +114,12 @@ const toggleNodeChildren = () => {
|
||||
showChildren.value = !showChildren.value
|
||||
isNodeOpen.value = !isNodeOpen.value
|
||||
}
|
||||
|
||||
const highlightNodeChildren = (id: string | null) => {
|
||||
if (id) {
|
||||
highlightNode.value = true
|
||||
} else {
|
||||
highlightNode.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user