feat : smart tree component (#2865)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com> Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
@@ -391,6 +391,7 @@
|
|||||||
"copy_link": "Copy link",
|
"copy_link": "Copy link",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"enter_curl": "Enter cURL command",
|
"enter_curl": "Enter cURL command",
|
||||||
|
"duplicated": "Request duplicated",
|
||||||
"generate_code": "Generate code",
|
"generate_code": "Generate code",
|
||||||
"generated_code": "Generated code",
|
"generated_code": "Generated code",
|
||||||
"header_list": "Header List",
|
"header_list": "Header List",
|
||||||
|
|||||||
14
packages/hoppscotch-common/src/components.d.ts
vendored
14
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -32,7 +32,7 @@ declare module '@vue/runtime-core' {
|
|||||||
CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
|
CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
|
||||||
CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default']
|
CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default']
|
||||||
CollectionsAddRequest: typeof import('./components/collections/AddRequest.vue')['default']
|
CollectionsAddRequest: typeof import('./components/collections/AddRequest.vue')['default']
|
||||||
CollectionsChooseType: typeof import('./components/collections/ChooseType.vue')['default']
|
CollectionsCollection: typeof import('./components/collections/Collection.vue')['default']
|
||||||
CollectionsEdit: typeof import('./components/collections/Edit.vue')['default']
|
CollectionsEdit: typeof import('./components/collections/Edit.vue')['default']
|
||||||
CollectionsEditFolder: typeof import('./components/collections/EditFolder.vue')['default']
|
CollectionsEditFolder: typeof import('./components/collections/EditFolder.vue')['default']
|
||||||
CollectionsEditRequest: typeof import('./components/collections/EditRequest.vue')['default']
|
CollectionsEditRequest: typeof import('./components/collections/EditRequest.vue')['default']
|
||||||
@@ -48,13 +48,11 @@ declare module '@vue/runtime-core' {
|
|||||||
CollectionsGraphqlImportExport: typeof import('./components/collections/graphql/ImportExport.vue')['default']
|
CollectionsGraphqlImportExport: typeof import('./components/collections/graphql/ImportExport.vue')['default']
|
||||||
CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
|
CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
|
||||||
CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
|
CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
|
||||||
CollectionsMyCollection: typeof import('./components/collections/my/Collection.vue')['default']
|
CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
|
||||||
CollectionsMyFolder: typeof import('./components/collections/my/Folder.vue')['default']
|
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
||||||
CollectionsMyRequest: typeof import('./components/collections/my/Request.vue')['default']
|
|
||||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||||
CollectionsTeamsCollection: typeof import('./components/collections/teams/Collection.vue')['default']
|
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||||
CollectionsTeamsFolder: typeof import('./components/collections/teams/Folder.vue')['default']
|
CollectionsTeamSelect: typeof import('./components/collections/TeamSelect.vue')['default']
|
||||||
CollectionsTeamsRequest: typeof import('./components/collections/teams/Request.vue')['default']
|
|
||||||
Environments: typeof import('./components/environments/index.vue')['default']
|
Environments: typeof import('./components/environments/index.vue')['default']
|
||||||
EnvironmentsChooseType: typeof import('./components/environments/ChooseType.vue')['default']
|
EnvironmentsChooseType: typeof import('./components/environments/ChooseType.vue')['default']
|
||||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||||
@@ -152,6 +150,8 @@ declare module '@vue/runtime-core' {
|
|||||||
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
||||||
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
||||||
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
||||||
|
SmartTree: typeof import('./components/smart/Tree.vue')['default']
|
||||||
|
SmartTreeBranch: typeof import('./components/smart/TreeBranch.vue')['default']
|
||||||
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
|
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
|
||||||
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
|
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
|
||||||
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
||||||
|
|||||||
@@ -41,47 +41,52 @@
|
|||||||
</SmartModal>
|
</SmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue"
|
import { watch, ref } from "vue"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
|
||||||
export default defineComponent({
|
const toast = useToast()
|
||||||
props: {
|
const t = useI18n()
|
||||||
show: Boolean,
|
|
||||||
loadingState: Boolean,
|
const props = withDefaults(
|
||||||
},
|
defineProps<{
|
||||||
emits: ["submit", "hide-modal"],
|
show: boolean
|
||||||
setup() {
|
loadingState: boolean
|
||||||
return {
|
}>(),
|
||||||
toast: useToast(),
|
{
|
||||||
t: useI18n(),
|
show: false,
|
||||||
|
loadingState: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "submit", name: string): void
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const name = ref("")
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(show) => {
|
||||||
|
if (!show) {
|
||||||
|
name.value = ""
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
data() {
|
)
|
||||||
return {
|
|
||||||
name: null,
|
const addNewCollection = () => {
|
||||||
}
|
if (!name.value) {
|
||||||
},
|
toast.error(t("collection.invalid_name"))
|
||||||
watch: {
|
return
|
||||||
show(isShowing: boolean) {
|
}
|
||||||
if (!isShowing) {
|
|
||||||
this.name = null
|
emit("submit", name.value)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
const hideModal = () => {
|
||||||
methods: {
|
name.value = ""
|
||||||
addNewCollection() {
|
emit("hide-modal")
|
||||||
if (!this.name) {
|
}
|
||||||
this.toast.error(this.t("collection.invalid_name"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$emit("submit", this.name)
|
|
||||||
},
|
|
||||||
hideModal() {
|
|
||||||
this.name = null
|
|
||||||
this.$emit("hide-modal")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
v-if="show"
|
v-if="show"
|
||||||
dialog
|
dialog
|
||||||
:title="t('folder.new')"
|
:title="t('folder.new')"
|
||||||
@close="$emit('hide-modal')"
|
@close="emit('hide-modal')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -41,52 +41,51 @@
|
|||||||
</SmartModal>
|
</SmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
|
|
||||||
export default defineComponent({
|
const toast = useToast()
|
||||||
props: {
|
const t = useI18n()
|
||||||
show: Boolean,
|
|
||||||
folder: { type: Object, default: () => ({}) },
|
const props = withDefaults(
|
||||||
folderPath: { type: String, default: null },
|
defineProps<{
|
||||||
collectionIndex: { type: Number, default: null },
|
show: boolean
|
||||||
loadingState: Boolean,
|
loadingState: boolean
|
||||||
},
|
}>(),
|
||||||
emits: ["hide-modal", "add-folder"],
|
{
|
||||||
setup() {
|
show: false,
|
||||||
return {
|
loadingState: false,
|
||||||
toast: useToast(),
|
}
|
||||||
t: useI18n(),
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
(e: "add-folder", name: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const name = ref("")
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(show) => {
|
||||||
|
if (!show) {
|
||||||
|
name.value = ""
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
data() {
|
)
|
||||||
return {
|
|
||||||
name: null,
|
const addFolder = () => {
|
||||||
}
|
if (name.value.trim() === "") {
|
||||||
},
|
toast.error(t("folder.invalid_name"))
|
||||||
watch: {
|
return
|
||||||
show(isShowing: boolean) {
|
}
|
||||||
if (!isShowing) this.name = null
|
emit("add-folder", name.value)
|
||||||
},
|
}
|
||||||
},
|
|
||||||
methods: {
|
const hideModal = () => {
|
||||||
addFolder() {
|
name.value = ""
|
||||||
if (!this.name) {
|
emit("hide-modal")
|
||||||
this.toast.error(this.t("folder.invalid_name"))
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$emit("add-folder", {
|
|
||||||
name: this.name,
|
|
||||||
folder: this.folder,
|
|
||||||
path: this.folderPath || `${this.collectionIndex}`,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
hideModal() {
|
|
||||||
this.name = null
|
|
||||||
this.$emit("hide-modal")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -48,23 +48,20 @@ import { getRESTRequest } from "~/newstore/RESTSession"
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
show: boolean
|
defineProps<{
|
||||||
loadingState: boolean
|
show: boolean
|
||||||
folder?: object
|
loadingState: boolean
|
||||||
folderPath?: string
|
}>(),
|
||||||
}>()
|
{
|
||||||
|
show: false,
|
||||||
|
loadingState: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "hide-modal"): void
|
(event: "hide-modal"): void
|
||||||
(
|
(event: "add-request", name: string): void
|
||||||
e: "add-request",
|
|
||||||
v: {
|
|
||||||
name: string
|
|
||||||
folder: object | undefined
|
|
||||||
path: string | undefined
|
|
||||||
}
|
|
||||||
): void
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const name = ref("")
|
const name = ref("")
|
||||||
@@ -79,15 +76,11 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const addRequest = () => {
|
const addRequest = () => {
|
||||||
if (!name.value) {
|
if (name.value.trim() === "") {
|
||||||
toast.error(`${t("error.empty_req_name")}`)
|
toast.error(`${t("error.empty_req_name")}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
emit("add-request", {
|
emit("add-request", name.value)
|
||||||
name: name.value,
|
|
||||||
folder: props.folder,
|
|
||||||
path: props.folderPath,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<SmartTabs
|
|
||||||
:id="'collections_tab'"
|
|
||||||
v-model="selectedCollectionTab"
|
|
||||||
render-inactive-tabs
|
|
||||||
>
|
|
||||||
<SmartTab
|
|
||||||
:id="'my-collections'"
|
|
||||||
:label="`${t('collection.my_collections')}`"
|
|
||||||
/>
|
|
||||||
<SmartTab
|
|
||||||
:id="'team-collections'"
|
|
||||||
:label="`${t('collection.team_collections')}`"
|
|
||||||
>
|
|
||||||
<SmartIntersection @intersecting="onTeamSelectIntersect">
|
|
||||||
<tippy
|
|
||||||
interactive
|
|
||||||
trigger="click"
|
|
||||||
theme="popover"
|
|
||||||
placement="bottom"
|
|
||||||
:on-shown="() => tippyActions.focus()"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="`${t('collection.select_team')}`"
|
|
||||||
class="bg-transparent border-b border-dividerLight select-wrapper"
|
|
||||||
>
|
|
||||||
<ButtonSecondary
|
|
||||||
v-if="collectionsType.selectedTeam"
|
|
||||||
:icon="IconUsers"
|
|
||||||
:label="collectionsType.selectedTeam.name"
|
|
||||||
class="flex-1 !justify-start pr-8 rounded-none"
|
|
||||||
/>
|
|
||||||
<ButtonSecondary
|
|
||||||
v-else
|
|
||||||
:label="`${t('collection.select_team')}`"
|
|
||||||
class="flex-1 !justify-start pr-8 rounded-none"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<template #content="{ hide }">
|
|
||||||
<div
|
|
||||||
ref="tippyActions"
|
|
||||||
class="flex flex-col focus:outline-none"
|
|
||||||
tabindex="0"
|
|
||||||
@keyup.escape="hide()"
|
|
||||||
>
|
|
||||||
<SmartItem
|
|
||||||
v-for="(team, index) in myTeams"
|
|
||||||
:key="`team-${index}`"
|
|
||||||
:label="team.name"
|
|
||||||
:info-icon="
|
|
||||||
team.id === collectionsType.selectedTeam?.id
|
|
||||||
? IconDone
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
:active-info-icon="
|
|
||||||
team.id === collectionsType.selectedTeam?.id
|
|
||||||
"
|
|
||||||
:icon="IconUsers"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
updateSelectedTeam(team)
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</SmartIntersection>
|
|
||||||
</SmartTab>
|
|
||||||
</SmartTabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import IconUsers from "~icons/lucide/users"
|
|
||||||
import IconDone from "~icons/lucide/check"
|
|
||||||
import { nextTick, ref, watch } from "vue"
|
|
||||||
import { GetMyTeamsQuery, Team } from "~/helpers/backend/graphql"
|
|
||||||
import { currentUserInfo$ } from "~/helpers/teams/BackendUserInfo"
|
|
||||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
|
||||||
import { onLoggedIn } from "@composables/auth"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useLocalState } from "~/newstore/localstate"
|
|
||||||
import { invokeAction } from "~/helpers/actions"
|
|
||||||
|
|
||||||
type TeamData = GetMyTeamsQuery["myTeams"][number]
|
|
||||||
|
|
||||||
type CollectionTabs = "my-collections" | "team-collections"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
// Template refs
|
|
||||||
const tippyActions = ref<any | null>(null)
|
|
||||||
const selectedCollectionTab = ref<CollectionTabs>("my-collections")
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
collectionsType: {
|
|
||||||
type: "my-collections" | "team-collections"
|
|
||||||
selectedTeam: Team | undefined
|
|
||||||
}
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "update-collection-type", tabID: string): void
|
|
||||||
(e: "update-selected-team", team: TeamData | undefined): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(currentUserInfo$, null)
|
|
||||||
|
|
||||||
const adapter = new TeamListAdapter(true)
|
|
||||||
const myTeams = useReadonlyStream(adapter.teamList$, null)
|
|
||||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
|
||||||
let teamListFetched = false
|
|
||||||
|
|
||||||
watch(myTeams, (teams) => {
|
|
||||||
if (teams && !teamListFetched) {
|
|
||||||
teamListFetched = true
|
|
||||||
if (REMEMBERED_TEAM_ID.value && currentUser) {
|
|
||||||
const team = teams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
|
|
||||||
if (team) updateSelectedTeam(team)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onLoggedIn(() => {
|
|
||||||
adapter.initialize()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => currentUser.value,
|
|
||||||
(user) => {
|
|
||||||
if (!user) {
|
|
||||||
selectedCollectionTab.value = "my-collections"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const onTeamSelectIntersect = () => {
|
|
||||||
// Load team data as soon as intersection
|
|
||||||
adapter.fetchList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCollectionsType = (tabID: string) => {
|
|
||||||
emit("update-collection-type", tabID)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSelectedTeam = (team: TeamData | undefined) => {
|
|
||||||
REMEMBERED_TEAM_ID.value = team?.id
|
|
||||||
emit("update-selected-team", team)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(selectedCollectionTab, (newValue: CollectionTabs) => {
|
|
||||||
if (newValue === "team-collections" && !currentUser.value) {
|
|
||||||
invokeAction("modals.login.toggle")
|
|
||||||
nextTick(() => (selectedCollectionTab.value = "my-collections"))
|
|
||||||
} else updateCollectionsType(newValue)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||||
|
<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')"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
<div v-if="!hasNoTeamAccess" class="flex">
|
||||||
|
<ButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconFilePlus"
|
||||||
|
:title="t('request.new')"
|
||||||
|
class="hidden group-hover:inline-flex"
|
||||||
|
@click="emit('add-request')"
|
||||||
|
/>
|
||||||
|
<ButtonSecondary
|
||||||
|
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()"
|
||||||
|
>
|
||||||
|
<ButtonSecondary
|
||||||
|
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()"
|
||||||
|
>
|
||||||
|
<SmartItem
|
||||||
|
ref="requestAction"
|
||||||
|
:icon="IconFilePlus"
|
||||||
|
:label="t('request.new')"
|
||||||
|
:shortcut="['R']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('add-request')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<SmartItem
|
||||||
|
ref="folderAction"
|
||||||
|
:icon="IconFolderPlus"
|
||||||
|
:label="t('folder.new')"
|
||||||
|
:shortcut="['N']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('add-folder')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<SmartItem
|
||||||
|
ref="edit"
|
||||||
|
:icon="IconEdit"
|
||||||
|
:label="t('action.edit')"
|
||||||
|
:shortcut="['E']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('edit-collection')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<SmartItem
|
||||||
|
ref="exportAction"
|
||||||
|
:icon="IconDownload"
|
||||||
|
:label="t('export.title')"
|
||||||
|
:shortcut="['X']"
|
||||||
|
:loading="exportLoading"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('export-data'),
|
||||||
|
collectionsType === 'my-collections' ? hide() : null
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<SmartItem
|
||||||
|
ref="deleteAction"
|
||||||
|
:icon="IconTrash2"
|
||||||
|
:label="t('action.delete')"
|
||||||
|
:shortcut="['⌫']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('remove-collection')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
|
import IconFilePlus from "~icons/lucide/file-plus"
|
||||||
|
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||||
|
import IconDownload from "~icons/lucide/download"
|
||||||
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
import IconEdit from "~icons/lucide/edit"
|
||||||
|
import IconFolder from "~icons/lucide/folder"
|
||||||
|
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||||
|
import { PropType, ref, computed, watch } from "vue"
|
||||||
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { TippyComponent } from "vue-tippy"
|
||||||
|
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||||
|
|
||||||
|
type CollectionType = "my-collections" | "team-collections"
|
||||||
|
type FolderType = "collection" | "folder"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object as PropType<HoppCollection<HoppRESTRequest> | TeamCollection>,
|
||||||
|
default: () => ({}),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
collectionsType: {
|
||||||
|
type: String as PropType<CollectionType>,
|
||||||
|
default: "my-collections",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Collection component can be used for both collections and folders.
|
||||||
|
* folderType is used to determine which one it is.
|
||||||
|
*/
|
||||||
|
folderType: {
|
||||||
|
type: String as PropType<FolderType>,
|
||||||
|
default: "collection",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isSelected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
exportLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
hasNoTeamAccess: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "toggle-children"): void
|
||||||
|
(event: "add-request"): void
|
||||||
|
(event: "add-folder"): void
|
||||||
|
(event: "edit-collection"): void
|
||||||
|
(event: "export-data"): void
|
||||||
|
(event: "remove-collection"): void
|
||||||
|
(event: "drop-event", payload: DataTransfer): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const tippyActions = ref<TippyComponent | null>(null)
|
||||||
|
const requestAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const folderAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const edit = ref<HTMLButtonElement | null>(null)
|
||||||
|
const deleteAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const exportAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const options = ref<TippyComponent | null>(null)
|
||||||
|
|
||||||
|
const dragging = ref(false)
|
||||||
|
|
||||||
|
const collectionIcon = computed(() => {
|
||||||
|
if (props.isSelected) return IconCheckCircle
|
||||||
|
else if (!props.isOpen) return IconFolder
|
||||||
|
else if (props.isOpen) return IconFolderOpen
|
||||||
|
else return IconFolder
|
||||||
|
})
|
||||||
|
|
||||||
|
const collectionName = computed(() => {
|
||||||
|
if ((props.data as HoppCollection<HoppRESTRequest>).name)
|
||||||
|
return (props.data as HoppCollection<HoppRESTRequest>).name
|
||||||
|
else return (props.data as TeamCollection).title
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.exportLoading,
|
||||||
|
(val) => {
|
||||||
|
if (!val) {
|
||||||
|
options.value!.tippy.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const dropEvent = ({ dataTransfer }: DragEvent) => {
|
||||||
|
if (dataTransfer) {
|
||||||
|
dragging.value = !dragging.value
|
||||||
|
emit("drop-event", dataTransfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -41,46 +41,52 @@
|
|||||||
</SmartModal>
|
</SmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
|
||||||
export default defineComponent({
|
const t = useI18n()
|
||||||
props: {
|
const toast = useToast()
|
||||||
show: Boolean,
|
|
||||||
editingCollectionName: { type: String, default: null },
|
const props = withDefaults(
|
||||||
loadingState: Boolean,
|
defineProps<{
|
||||||
},
|
show: boolean
|
||||||
emits: ["submit", "hide-modal"],
|
loadingState: boolean
|
||||||
setup() {
|
editingCollectionName: string
|
||||||
return {
|
}>(),
|
||||||
toast: useToast(),
|
{
|
||||||
t: useI18n(),
|
show: false,
|
||||||
}
|
loadingState: false,
|
||||||
},
|
editingCollectionName: "",
|
||||||
data() {
|
}
|
||||||
return {
|
)
|
||||||
name: null,
|
|
||||||
}
|
const emit = defineEmits<{
|
||||||
},
|
(e: "submit", name: string): void
|
||||||
watch: {
|
(e: "hide-modal"): void
|
||||||
editingCollectionName(val) {
|
}>()
|
||||||
this.name = val
|
|
||||||
},
|
const name = ref("")
|
||||||
},
|
|
||||||
methods: {
|
watch(
|
||||||
saveCollection() {
|
() => props.editingCollectionName,
|
||||||
if (!this.name) {
|
(newName) => {
|
||||||
this.toast.error(this.t("collection.invalid_name"))
|
name.value = newName
|
||||||
return
|
}
|
||||||
}
|
)
|
||||||
this.$emit("submit", this.name)
|
|
||||||
},
|
const saveCollection = () => {
|
||||||
hideModal() {
|
if (name.value.trim() === "") {
|
||||||
this.name = null
|
toast.error(t("collection.invalid_name"))
|
||||||
this.$emit("hide-modal")
|
return
|
||||||
},
|
}
|
||||||
},
|
|
||||||
})
|
emit("submit", name.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideModal = () => {
|
||||||
|
name.value = ""
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
v-if="show"
|
v-if="show"
|
||||||
dialog
|
dialog
|
||||||
:title="t('folder.edit')"
|
:title="t('folder.edit')"
|
||||||
@close="$emit('hide-modal')"
|
@close="emit('hide-modal')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -41,46 +41,52 @@
|
|||||||
</SmartModal>
|
</SmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
|
|
||||||
export default defineComponent({
|
const t = useI18n()
|
||||||
props: {
|
const toast = useToast()
|
||||||
show: Boolean,
|
|
||||||
editingFolderName: { type: String, default: null },
|
const props = withDefaults(
|
||||||
loadingState: Boolean,
|
defineProps<{
|
||||||
},
|
show: boolean
|
||||||
emits: ["submit", "hide-modal"],
|
loadingState: boolean
|
||||||
setup() {
|
editingFolderName: string
|
||||||
return {
|
}>(),
|
||||||
t: useI18n(),
|
{
|
||||||
toast: useToast(),
|
show: false,
|
||||||
}
|
loadingState: false,
|
||||||
},
|
editingFolderName: "",
|
||||||
data() {
|
}
|
||||||
return {
|
)
|
||||||
name: null,
|
|
||||||
}
|
const emit = defineEmits<{
|
||||||
},
|
(e: "submit", name: string): void
|
||||||
watch: {
|
(e: "hide-modal"): void
|
||||||
editingFolderName(val) {
|
}>()
|
||||||
this.name = val
|
|
||||||
},
|
const name = ref("")
|
||||||
},
|
|
||||||
methods: {
|
watch(
|
||||||
editFolder() {
|
() => props.editingFolderName,
|
||||||
if (!this.name) {
|
(newName) => {
|
||||||
this.toast.error(this.t("folder.invalid_name"))
|
name.value = newName
|
||||||
return
|
}
|
||||||
}
|
)
|
||||||
this.$emit("submit", this.name)
|
|
||||||
},
|
const editFolder = () => {
|
||||||
hideModal() {
|
if (name.value.trim() === "") {
|
||||||
this.name = null
|
toast.error(t("folder.invalid_name"))
|
||||||
this.$emit("hide-modal")
|
return
|
||||||
},
|
}
|
||||||
},
|
|
||||||
})
|
emit("submit", name.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideModal = () => {
|
||||||
|
name.value = ""
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,13 +9,13 @@
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<input
|
<input
|
||||||
id="selectLabelEditReq"
|
id="selectLabelEditReq"
|
||||||
v-model="requestUpdateData.name"
|
v-model="name"
|
||||||
v-focus
|
v-focus
|
||||||
class="input floating-input"
|
class="input floating-input"
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@keyup.enter="saveRequest"
|
@keyup.enter="editRequest"
|
||||||
/>
|
/>
|
||||||
<label for="selectLabelEditReq">
|
<label for="selectLabelEditReq">
|
||||||
{{ t("action.label") }}
|
{{ t("action.label") }}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
:label="t('action.save')"
|
:label="t('action.save')"
|
||||||
:loading="loadingState"
|
:loading="loadingState"
|
||||||
outline
|
outline
|
||||||
@click="saveRequest"
|
@click="editRequest"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="t('action.cancel')"
|
:label="t('action.cancel')"
|
||||||
@@ -41,48 +41,52 @@
|
|||||||
</SmartModal>
|
</SmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
|
|
||||||
export default defineComponent({
|
const toast = useToast()
|
||||||
props: {
|
const t = useI18n()
|
||||||
show: Boolean,
|
|
||||||
editingRequestName: { type: String, default: null },
|
const props = withDefaults(
|
||||||
loadingState: Boolean,
|
defineProps<{
|
||||||
},
|
show: boolean
|
||||||
emits: ["submit", "hide-modal"],
|
loadingState: boolean
|
||||||
setup() {
|
editingRequestName: string
|
||||||
return {
|
}>(),
|
||||||
t: useI18n(),
|
{
|
||||||
toast: useToast(),
|
show: false,
|
||||||
}
|
loadingState: false,
|
||||||
},
|
editingRequestName: "",
|
||||||
data() {
|
}
|
||||||
return {
|
)
|
||||||
requestUpdateData: {
|
|
||||||
name: null,
|
const emit = defineEmits<{
|
||||||
},
|
(e: "submit", name: string): void
|
||||||
}
|
(e: "hide-modal"): void
|
||||||
},
|
}>()
|
||||||
watch: {
|
|
||||||
editingRequestName(val) {
|
const name = ref("")
|
||||||
this.requestUpdateData.name = val
|
|
||||||
},
|
watch(
|
||||||
},
|
() => props.editingRequestName,
|
||||||
methods: {
|
(newName) => {
|
||||||
saveRequest() {
|
name.value = newName
|
||||||
if (!this.requestUpdateData.name) {
|
}
|
||||||
this.toast.error(this.t("request.invalid_name"))
|
)
|
||||||
return
|
|
||||||
}
|
const editRequest = () => {
|
||||||
this.$emit("submit", this.requestUpdateData)
|
if (name.value.trim() === "") {
|
||||||
},
|
toast.error(t("request.invalid_name"))
|
||||||
hideModal() {
|
return
|
||||||
this.requestUpdateData = { name: null }
|
}
|
||||||
this.$emit("hide-modal")
|
|
||||||
},
|
emit("submit", name.value)
|
||||||
},
|
}
|
||||||
})
|
|
||||||
|
const hideModal = () => {
|
||||||
|
name.value = ""
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<SmartModal
|
<SmartModal
|
||||||
v-if="show"
|
v-if="show"
|
||||||
dialog
|
dialog
|
||||||
:title="`${t('modal.collections')}`"
|
:title="t('modal.collections')"
|
||||||
styles="sm:max-w-md"
|
styles="sm:max-w-md"
|
||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
@@ -81,7 +81,6 @@
|
|||||||
<div class="select-wrapper">
|
<div class="select-wrapper">
|
||||||
<select
|
<select
|
||||||
v-model="mySelectedCollectionID"
|
v-model="mySelectedCollectionID"
|
||||||
type="text"
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="select"
|
class="select"
|
||||||
autofocus
|
autofocus
|
||||||
@@ -93,6 +92,7 @@
|
|||||||
v-for="(collection, collectionIndex) in myCollections"
|
v-for="(collection, collectionIndex) in myCollections"
|
||||||
:key="`collection-${collectionIndex}`"
|
:key="`collection-${collectionIndex}`"
|
||||||
:value="collectionIndex"
|
:value="collectionIndex"
|
||||||
|
class="bg-primary"
|
||||||
>
|
>
|
||||||
{{ collection.name }}
|
{{ collection.name }}
|
||||||
</option>
|
</option>
|
||||||
@@ -126,8 +126,9 @@
|
|||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('action.download_file')"
|
:title="t('action.download_file')"
|
||||||
:icon="IconDownload"
|
:icon="IconDownload"
|
||||||
|
:loading="exportingTeamCollections"
|
||||||
:label="t('export.as_json')"
|
:label="t('export.as_json')"
|
||||||
@click="exportJSON"
|
@click="emit('export-json-collection')"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -149,12 +150,9 @@
|
|||||||
: false
|
: false
|
||||||
"
|
"
|
||||||
:icon="IconGithub"
|
:icon="IconGithub"
|
||||||
|
:loading="creatingGistCollection"
|
||||||
:label="t('export.create_secret_gist')"
|
:label="t('export.create_secret_gist')"
|
||||||
@click="
|
@click="emit('create-collection-gist')"
|
||||||
() => {
|
|
||||||
createCollectionGist()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,11 +165,10 @@
|
|||||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||||
import IconDownload from "~icons/lucide/download"
|
import IconDownload from "~icons/lucide/download"
|
||||||
import IconGithub from "~icons/lucide/github"
|
import IconGithub from "~icons/lucide/github"
|
||||||
import { computed, ref, watch } from "vue"
|
import { computed, PropType, ref, watch } from "vue"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
||||||
import axios from "axios"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
@@ -179,205 +176,85 @@ import { currentUser$ } from "~/helpers/fb/auth"
|
|||||||
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
|
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
|
||||||
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
|
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
|
||||||
import { StepReturnValue } from "~/helpers/import-export/steps"
|
import { StepReturnValue } from "~/helpers/import-export/steps"
|
||||||
import { runGQLQuery, runMutation } from "~/helpers/backend/GQLClient"
|
|
||||||
import {
|
|
||||||
ExportAsJsonDocument,
|
|
||||||
ImportFromJsonDocument,
|
|
||||||
} from "~/helpers/backend/graphql"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const toast = useToast()
|
||||||
show: boolean
|
const t = useI18n()
|
||||||
collectionsType:
|
|
||||||
| {
|
type CollectionType = "team-collections" | "my-collections"
|
||||||
type: "team-collections"
|
|
||||||
selectedTeam: {
|
const props = defineProps({
|
||||||
id: string
|
show: {
|
||||||
}
|
type: Boolean,
|
||||||
}
|
default: false,
|
||||||
| { type: "my-collections" }
|
required: true,
|
||||||
}>()
|
},
|
||||||
|
collectionsType: {
|
||||||
|
type: String as PropType<CollectionType>,
|
||||||
|
default: "my-collections",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
exportingTeamCollections: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
creatingGistCollection: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
importingMyCollections: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
(e: "update-team-collections"): void
|
(e: "update-team-collections"): void
|
||||||
|
(e: "export-json-collection"): void
|
||||||
|
(e: "create-collection-gist"): void
|
||||||
|
(e: "import-to-teams", payload: HoppCollection<HoppRESTRequest>[]): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const toast = useToast()
|
const hasFile = ref(false)
|
||||||
const t = useI18n()
|
const hasGist = ref(false)
|
||||||
const myCollections = useReadonlyStream(restCollections$, [])
|
|
||||||
const currentUser = useReadonlyStream(currentUser$, null)
|
|
||||||
|
|
||||||
// Template refs
|
const importerType = ref<number | null>(null)
|
||||||
const mode = ref("import_export")
|
|
||||||
const mySelectedCollectionID = ref<undefined | number>(undefined)
|
|
||||||
const collectionJson = ref("")
|
|
||||||
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
|
||||||
const inputChooseGistToImportFrom = ref<string>("")
|
|
||||||
|
|
||||||
const getJSONCollection = async () => {
|
|
||||||
if (props.collectionsType.type === "my-collections") {
|
|
||||||
collectionJson.value = JSON.stringify(myCollections.value, null, 2)
|
|
||||||
} else {
|
|
||||||
collectionJson.value = pipe(
|
|
||||||
await runGQLQuery({
|
|
||||||
query: ExportAsJsonDocument,
|
|
||||||
variables: {
|
|
||||||
teamID: props.collectionsType.selectedTeam.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
E.matchW(
|
|
||||||
// TODO: Handle error case gracefully ?
|
|
||||||
() => {
|
|
||||||
throw new Error("Error exporting collection to JSON")
|
|
||||||
},
|
|
||||||
(x) => x.exportCollectionsToJSON
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return collectionJson.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCollectionGist = async () => {
|
|
||||||
if (!currentUser.value) {
|
|
||||||
toast.error(t("profile.no_permission").toString())
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await getJSONCollection()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await axios.post(
|
|
||||||
"https://api.github.com/gists",
|
|
||||||
{
|
|
||||||
files: {
|
|
||||||
"hoppscotch-collections.json": {
|
|
||||||
content: collectionJson.value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `token ${currentUser.value.accessToken}`,
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
toast.success(t("export.gist_created").toString())
|
|
||||||
window.open(res.html_url)
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(t("error.something_went_wrong").toString())
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileImported = () => {
|
|
||||||
toast.success(t("state.file_imported").toString())
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const failedImport = () => {
|
|
||||||
toast.error(t("import.failed").toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideModal = () => {
|
|
||||||
mode.value = "import_export"
|
|
||||||
mySelectedCollectionID.value = undefined
|
|
||||||
resetImport()
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
const stepResults = ref<StepReturnValue[]>([])
|
const stepResults = ref<StepReturnValue[]>([])
|
||||||
|
|
||||||
|
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
||||||
|
const mySelectedCollectionID = ref<number | undefined>(undefined)
|
||||||
|
const inputChooseGistToImportFrom = ref<string>("")
|
||||||
|
|
||||||
|
const importerModules = computed(() =>
|
||||||
|
RESTCollectionImporters.filter(
|
||||||
|
(i) => i.applicableTo?.includes(props.collectionsType) ?? true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const importerModule = computed(() => {
|
||||||
|
if (importerType.value === null) return null
|
||||||
|
return importerModules.value[importerType.value]
|
||||||
|
})
|
||||||
|
|
||||||
|
const importerSteps = computed(() => importerModule.value?.steps ?? null)
|
||||||
|
|
||||||
|
const enableImportButton = computed(
|
||||||
|
() => !(stepResults.value.length === importerSteps.value?.length)
|
||||||
|
)
|
||||||
|
|
||||||
watch(mySelectedCollectionID, (newValue) => {
|
watch(mySelectedCollectionID, (newValue) => {
|
||||||
if (newValue === undefined) return
|
if (newValue === undefined) return
|
||||||
stepResults.value = []
|
stepResults.value = []
|
||||||
stepResults.value.push(newValue)
|
stepResults.value.push(newValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
const importingMyCollections = ref(false)
|
watch(inputChooseGistToImportFrom, (url) => {
|
||||||
|
|
||||||
const importToTeams = async (content: HoppCollection<HoppRESTRequest>) => {
|
|
||||||
importingMyCollections.value = true
|
|
||||||
if (props.collectionsType.type !== "team-collections") return
|
|
||||||
|
|
||||||
const result = await runMutation(ImportFromJsonDocument, {
|
|
||||||
jsonString: JSON.stringify(content),
|
|
||||||
teamID: props.collectionsType.selectedTeam.id,
|
|
||||||
})()
|
|
||||||
|
|
||||||
if (E.isLeft(result)) {
|
|
||||||
console.error(result.left)
|
|
||||||
} else {
|
|
||||||
emit("update-team-collections")
|
|
||||||
}
|
|
||||||
|
|
||||||
importingMyCollections.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportJSON = async () => {
|
|
||||||
await getJSONCollection()
|
|
||||||
|
|
||||||
const dataToWrite = collectionJson.value
|
|
||||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
|
||||||
const a = document.createElement("a")
|
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
a.href = url
|
|
||||||
|
|
||||||
// TODO: get uri from meta
|
|
||||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
toast.success(t("state.download_started").toString())
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.removeChild(a)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const importerModules = computed(() =>
|
|
||||||
RESTCollectionImporters.filter(
|
|
||||||
(i) => i.applicableTo?.includes(props.collectionsType.type) ?? true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const importerType = ref<number | null>(null)
|
|
||||||
|
|
||||||
const importerModule = computed(() =>
|
|
||||||
importerType.value !== null ? importerModules.value[importerType.value] : null
|
|
||||||
)
|
|
||||||
|
|
||||||
const importerSteps = computed(() => importerModule.value?.steps ?? null)
|
|
||||||
|
|
||||||
const finishImport = async () => {
|
|
||||||
await importerAction(stepResults.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const importerAction = async (stepResults: any[]) => {
|
|
||||||
if (!importerModule.value) return
|
|
||||||
const result = await importerModule.value?.importer(stepResults as any)()
|
|
||||||
if (E.isLeft(result)) {
|
|
||||||
failedImport()
|
|
||||||
console.error("error", result.left)
|
|
||||||
} else if (E.isRight(result)) {
|
|
||||||
if (props.collectionsType.type === "team-collections") {
|
|
||||||
importToTeams(result.right)
|
|
||||||
fileImported()
|
|
||||||
} else {
|
|
||||||
appendRESTCollections(result.right)
|
|
||||||
fileImported()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasFile = ref(false)
|
|
||||||
const hasGist = ref(false)
|
|
||||||
|
|
||||||
watch(inputChooseGistToImportFrom, (v) => {
|
|
||||||
stepResults.value = []
|
stepResults.value = []
|
||||||
if (v === "") {
|
if (url === "") {
|
||||||
hasGist.value = false
|
hasGist.value = false
|
||||||
} else {
|
} else {
|
||||||
hasGist.value = true
|
hasGist.value = true
|
||||||
@@ -385,17 +262,46 @@ watch(inputChooseGistToImportFrom, (v) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const myCollections = useReadonlyStream(restCollections$, [])
|
||||||
|
const currentUser = useReadonlyStream(currentUser$, null)
|
||||||
|
|
||||||
|
const importerAction = async (stepResults: StepReturnValue[]) => {
|
||||||
|
if (!importerModule.value) return
|
||||||
|
|
||||||
|
pipe(
|
||||||
|
await importerModule.value.importer(stepResults as any)(),
|
||||||
|
E.match(
|
||||||
|
(err) => {
|
||||||
|
failedImport()
|
||||||
|
console.error("error", err)
|
||||||
|
},
|
||||||
|
(result) => {
|
||||||
|
if (props.collectionsType === "team-collections") {
|
||||||
|
emit("import-to-teams", result)
|
||||||
|
} else {
|
||||||
|
appendRESTCollections(result)
|
||||||
|
fileImported()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const finishImport = async () => {
|
||||||
|
await importerAction(stepResults.value)
|
||||||
|
}
|
||||||
|
|
||||||
const onFileChange = () => {
|
const onFileChange = () => {
|
||||||
stepResults.value = []
|
stepResults.value = []
|
||||||
if (!inputChooseFileToImportFrom.value[0]) {
|
|
||||||
|
const inputFileToImport = inputChooseFileToImportFrom.value[0]
|
||||||
|
|
||||||
|
if (!inputFileToImport) {
|
||||||
hasFile.value = false
|
hasFile.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
|
||||||
!inputChooseFileToImportFrom.value[0].files ||
|
|
||||||
inputChooseFileToImportFrom.value[0].files.length === 0
|
|
||||||
) {
|
|
||||||
inputChooseFileToImportFrom.value[0].value = ""
|
inputChooseFileToImportFrom.value[0].value = ""
|
||||||
hasFile.value = false
|
hasFile.value = false
|
||||||
toast.show(t("action.choose_file").toString())
|
toast.show(t("action.choose_file").toString())
|
||||||
@@ -403,6 +309,7 @@ const onFileChange = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
|
|
||||||
reader.onload = ({ target }) => {
|
reader.onload = ({ target }) => {
|
||||||
const content = target!.result as string | null
|
const content = target!.result as string | null
|
||||||
if (!content) {
|
if (!content) {
|
||||||
@@ -414,20 +321,29 @@ const onFileChange = () => {
|
|||||||
stepResults.value.push(content)
|
stepResults.value.push(content)
|
||||||
hasFile.value = !!content?.length
|
hasFile.value = !!content?.length
|
||||||
}
|
}
|
||||||
reader.readAsText(inputChooseFileToImportFrom.value[0].files[0])
|
|
||||||
|
reader.readAsText(inputFileToImport.files[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
const enableImportButton = computed(
|
const fileImported = () => {
|
||||||
() => !(stepResults.value.length === importerSteps.value?.length)
|
toast.success(t("state.file_imported").toString())
|
||||||
)
|
hideModal()
|
||||||
|
}
|
||||||
|
const failedImport = () => {
|
||||||
|
toast.error(t("import.failed").toString())
|
||||||
|
}
|
||||||
|
const hideModal = () => {
|
||||||
|
resetImport()
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
|
|
||||||
const resetImport = () => {
|
const resetImport = () => {
|
||||||
importerType.value = null
|
importerType.value = null
|
||||||
|
hasFile.value = false
|
||||||
|
hasGist.value = false
|
||||||
stepResults.value = []
|
stepResults.value = []
|
||||||
inputChooseFileToImportFrom.value = ""
|
inputChooseFileToImportFrom.value = ""
|
||||||
hasFile.value = false
|
|
||||||
inputChooseGistToImportFrom.value = ""
|
inputChooseGistToImportFrom.value = ""
|
||||||
hasGist.value = false
|
|
||||||
mySelectedCollectionID.value = undefined
|
mySelectedCollectionID.value = undefined
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,619 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<div
|
||||||
|
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||||
|
:style="
|
||||||
|
saveRequest
|
||||||
|
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
|
||||||
|
: 'top: var(--upper-primary-sticky-fold)'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ButtonSecondary
|
||||||
|
:icon="IconPlus"
|
||||||
|
:label="t('action.new')"
|
||||||
|
class="!rounded-none"
|
||||||
|
@click="emit('display-modal-add')"
|
||||||
|
/>
|
||||||
|
<span class="flex">
|
||||||
|
<ButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
to="https://docs.hoppscotch.io/features/collections"
|
||||||
|
blank
|
||||||
|
:title="t('app.wiki')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
/>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-if="!saveRequest"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconArchive"
|
||||||
|
:title="t('modal.import_export')"
|
||||||
|
@click="emit('display-modal-import-export')"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<SmartTree :adapter="myAdapter">
|
||||||
|
<template #content="{ node, toggleChildren, isOpen }">
|
||||||
|
<CollectionsCollection
|
||||||
|
v-if="node.data.type === 'collections'"
|
||||||
|
:data="node.data.data.data"
|
||||||
|
:collections-type="collectionsType.type"
|
||||||
|
:is-open="isOpen"
|
||||||
|
:is-selected="
|
||||||
|
isSelected({
|
||||||
|
collectionIndex: parseInt(node.id),
|
||||||
|
})
|
||||||
|
"
|
||||||
|
folder-type="collection"
|
||||||
|
@add-request="
|
||||||
|
node.data.type === 'collections' &&
|
||||||
|
emit('add-request', {
|
||||||
|
path: node.id,
|
||||||
|
folder: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@add-folder="
|
||||||
|
node.data.type === 'collections' &&
|
||||||
|
emit('add-folder', {
|
||||||
|
path: node.id,
|
||||||
|
folder: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@edit-collection="
|
||||||
|
node.data.type === 'collections' &&
|
||||||
|
emit('edit-collection', {
|
||||||
|
collectionIndex: node.id,
|
||||||
|
collection: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@export-data="
|
||||||
|
node.data.type === 'collections' &&
|
||||||
|
emit('export-data', node.data.data.data)
|
||||||
|
"
|
||||||
|
@remove-collection="emit('remove-collection', node.id)"
|
||||||
|
@drop-event="dropEvent($event, node.id)"
|
||||||
|
@toggle-children="
|
||||||
|
() => {
|
||||||
|
toggleChildren(),
|
||||||
|
saveRequest &&
|
||||||
|
emit('select', {
|
||||||
|
pickedType: 'my-collection',
|
||||||
|
collectionIndex: parseInt(node.id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<CollectionsCollection
|
||||||
|
v-if="node.data.type === 'folders'"
|
||||||
|
:data="node.data.data.data"
|
||||||
|
:collections-type="collectionsType.type"
|
||||||
|
:is-open="isOpen"
|
||||||
|
:is-selected="
|
||||||
|
isSelected({
|
||||||
|
folderPath: node.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
folder-type="folder"
|
||||||
|
@add-request="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('add-request', {
|
||||||
|
path: node.id,
|
||||||
|
folder: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@add-folder="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('add-folder', {
|
||||||
|
path: node.id,
|
||||||
|
folder: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@edit-collection="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('edit-folder', {
|
||||||
|
folderPath: node.id,
|
||||||
|
folder: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@export-data="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('export-data', node.data.data.data)
|
||||||
|
"
|
||||||
|
@remove-collection="emit('remove-folder', node.id)"
|
||||||
|
@drop-event="dropEvent($event, node.id)"
|
||||||
|
@toggle-children="
|
||||||
|
() => {
|
||||||
|
toggleChildren(),
|
||||||
|
saveRequest &&
|
||||||
|
emit('select', {
|
||||||
|
pickedType: 'my-folder',
|
||||||
|
folderPath: node.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<CollectionsRequest
|
||||||
|
v-if="node.data.type === 'requests'"
|
||||||
|
:request="node.data.data.data"
|
||||||
|
:collections-type="collectionsType.type"
|
||||||
|
:save-request="saveRequest"
|
||||||
|
:is-active="
|
||||||
|
isActiveRequest(
|
||||||
|
node.data.data.parentIndex,
|
||||||
|
parseInt(pathToIndex(node.id))
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:is-selected="
|
||||||
|
isSelected({
|
||||||
|
folderPath: node.data.data.parentIndex,
|
||||||
|
requestIndex: parseInt(pathToIndex(node.id)),
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@edit-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
emit('edit-request', {
|
||||||
|
folderPath: node.data.data.parentIndex,
|
||||||
|
requestIndex: pathToIndex(node.id),
|
||||||
|
request: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@duplicate-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
emit('duplicate-request', {
|
||||||
|
folderPath: node.data.data.parentIndex,
|
||||||
|
request: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@remove-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
emit('remove-request', {
|
||||||
|
folderPath: node.data.data.parentIndex,
|
||||||
|
requestIndex: pathToIndex(node.id),
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@select-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
selectRequest({
|
||||||
|
request: node.data.data.data,
|
||||||
|
folderPath: node.data.data.parentIndex,
|
||||||
|
requestIndex: pathToIndex(node.id),
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@drag-request="
|
||||||
|
dragRequest($event, {
|
||||||
|
folderPath: node.data.data.parentIndex,
|
||||||
|
requestIndex: pathToIndex(node.id),
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #emptyNode="{ node }">
|
||||||
|
<div v-if="node === null">
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
>
|
||||||
|
<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.collection')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collections") }}
|
||||||
|
</span>
|
||||||
|
<ButtonSecondary
|
||||||
|
:label="t('add.new')"
|
||||||
|
filled
|
||||||
|
class="mb-4"
|
||||||
|
outline
|
||||||
|
@click="emit('display-modal-add')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="node.data.type === 'collections'"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
>
|
||||||
|
<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.collection')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collection") }}
|
||||||
|
</span>
|
||||||
|
<ButtonSecondary
|
||||||
|
:label="t('add.new')"
|
||||||
|
filled
|
||||||
|
class="mb-4"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<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.folder')}`"
|
||||||
|
/>
|
||||||
|
<span class="text-center">
|
||||||
|
{{ t("empty.folder") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
filterText.length !== 0 &&
|
||||||
|
filteredCollections.length === 0 &&
|
||||||
|
node === null
|
||||||
|
"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
>
|
||||||
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
|
<span class="my-2 text-center">
|
||||||
|
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SmartTree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconArchive from "~icons/lucide/archive"
|
||||||
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { computed, PropType, Ref, toRef } from "vue"
|
||||||
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
|
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useColorMode } from "@composables/theming"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
import { restSaveContext$ } from "~/newstore/RESTSession"
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as O from "fp-ts/Option"
|
||||||
|
import { Picked } from "~/helpers/types/HoppPicked.js"
|
||||||
|
|
||||||
|
export type Collection = {
|
||||||
|
type: "collections"
|
||||||
|
data: {
|
||||||
|
parentIndex: null
|
||||||
|
data: HoppCollection<HoppRESTRequest>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Folder = {
|
||||||
|
type: "folders"
|
||||||
|
data: {
|
||||||
|
parentIndex: string
|
||||||
|
data: HoppCollection<HoppRESTRequest>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Requests = {
|
||||||
|
type: "requests"
|
||||||
|
data: {
|
||||||
|
parentIndex: string
|
||||||
|
data: HoppRESTRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||||
|
|
||||||
|
type CollectionType =
|
||||||
|
| {
|
||||||
|
type: "team-collections"
|
||||||
|
selectedTeam: SelectedTeam
|
||||||
|
}
|
||||||
|
| { type: "my-collections"; selectedTeam: undefined }
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
filteredCollections: {
|
||||||
|
type: Array as PropType<HoppCollection<HoppRESTRequest>[]>,
|
||||||
|
default: () => [],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
collectionsType: {
|
||||||
|
type: Object as PropType<CollectionType>,
|
||||||
|
default: () => ({ type: "my-collections", selectedTeam: undefined }),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
filterText: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: "",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
saveRequest: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
picked: {
|
||||||
|
type: Object as PropType<Picked | null>,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "display-modal-add"): void
|
||||||
|
(
|
||||||
|
event: "add-request",
|
||||||
|
payload: {
|
||||||
|
path: string
|
||||||
|
folder: HoppCollection<HoppRESTRequest>
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "add-folder",
|
||||||
|
payload: {
|
||||||
|
path: string
|
||||||
|
folder: HoppCollection<HoppRESTRequest>
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "edit-collection",
|
||||||
|
payload: {
|
||||||
|
collectionIndex: string
|
||||||
|
collection: HoppCollection<HoppRESTRequest>
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "edit-folder",
|
||||||
|
payload: {
|
||||||
|
folderPath: string
|
||||||
|
folder: HoppCollection<HoppRESTRequest>
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "edit-request",
|
||||||
|
payload: {
|
||||||
|
folderPath: string
|
||||||
|
requestIndex: string
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "duplicate-request",
|
||||||
|
payload: {
|
||||||
|
folderPath: string
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(event: "export-data", payload: HoppCollection<HoppRESTRequest>): void
|
||||||
|
(event: "remove-collection", payload: string): void
|
||||||
|
(event: "remove-folder", payload: string): void
|
||||||
|
(
|
||||||
|
event: "remove-request",
|
||||||
|
payload: {
|
||||||
|
folderPath: string | null
|
||||||
|
requestIndex: string
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "select-request",
|
||||||
|
payload: {
|
||||||
|
request: HoppRESTRequest
|
||||||
|
folderPath: string
|
||||||
|
requestIndex: string
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "drop-request",
|
||||||
|
payload: {
|
||||||
|
folderPath: string
|
||||||
|
requestIndex: string
|
||||||
|
collectionIndex: string
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(event: "select", payload: Picked | null): void
|
||||||
|
(event: "display-modal-import-export"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const refFilterCollection = toRef(props, "filteredCollections")
|
||||||
|
|
||||||
|
const pathToIndex = computed(() => {
|
||||||
|
return (path: string) => {
|
||||||
|
const pathArr = path.split("/")
|
||||||
|
return pathArr[pathArr.length - 1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSelected = computed(() => {
|
||||||
|
return ({
|
||||||
|
collectionIndex,
|
||||||
|
folderPath,
|
||||||
|
requestIndex,
|
||||||
|
}: {
|
||||||
|
collectionIndex?: number | undefined
|
||||||
|
folderPath?: string | undefined
|
||||||
|
requestIndex?: number | undefined
|
||||||
|
}) => {
|
||||||
|
if (collectionIndex !== undefined) {
|
||||||
|
return (
|
||||||
|
props.picked &&
|
||||||
|
props.picked.pickedType === "my-collection" &&
|
||||||
|
props.picked.collectionIndex === collectionIndex
|
||||||
|
)
|
||||||
|
} else if (requestIndex !== undefined && folderPath !== undefined) {
|
||||||
|
return (
|
||||||
|
props.picked &&
|
||||||
|
props.picked.pickedType === "my-request" &&
|
||||||
|
props.picked.folderPath === folderPath &&
|
||||||
|
props.picked.requestIndex === requestIndex
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
props.picked &&
|
||||||
|
props.picked.pickedType === "my-folder" &&
|
||||||
|
props.picked.folderPath === folderPath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const active = useReadonlyStream(restSaveContext$, null)
|
||||||
|
|
||||||
|
const isActiveRequest = computed(() => {
|
||||||
|
return (folderPath: string, requestIndex: number) => {
|
||||||
|
return pipe(
|
||||||
|
active.value,
|
||||||
|
O.fromNullable,
|
||||||
|
O.filter(
|
||||||
|
(active) =>
|
||||||
|
active.originLocation === "user-collection" &&
|
||||||
|
active.folderPath === folderPath &&
|
||||||
|
active.requestIndex === requestIndex
|
||||||
|
),
|
||||||
|
O.isSome
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectRequest = (data: {
|
||||||
|
request: HoppRESTRequest
|
||||||
|
folderPath: string
|
||||||
|
requestIndex: string
|
||||||
|
}) => {
|
||||||
|
const { request, folderPath, requestIndex } = data
|
||||||
|
if (props.saveRequest) {
|
||||||
|
emit("select", {
|
||||||
|
pickedType: "my-request",
|
||||||
|
folderPath: folderPath,
|
||||||
|
requestIndex: parseInt(requestIndex),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
emit("select-request", {
|
||||||
|
request,
|
||||||
|
folderPath,
|
||||||
|
requestIndex,
|
||||||
|
isActive: isActiveRequest.value(folderPath, parseInt(requestIndex)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragRequest = (
|
||||||
|
dataTransfer: DataTransfer,
|
||||||
|
{
|
||||||
|
folderPath,
|
||||||
|
requestIndex,
|
||||||
|
}: { folderPath: string | null; requestIndex: string }
|
||||||
|
) => {
|
||||||
|
if (!folderPath) return
|
||||||
|
dataTransfer.setData("folderPath", folderPath)
|
||||||
|
dataTransfer.setData("requestIndex", requestIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
|
||||||
|
const folderPath = dataTransfer.getData("folderPath")
|
||||||
|
const requestIndex = dataTransfer.getData("requestIndex")
|
||||||
|
emit("drop-request", {
|
||||||
|
folderPath,
|
||||||
|
requestIndex,
|
||||||
|
collectionIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type MyCollectionNode = Collection | Folder | Requests
|
||||||
|
|
||||||
|
class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
||||||
|
constructor(public data: Ref<HoppCollection<HoppRESTRequest>[]>) {}
|
||||||
|
|
||||||
|
navigateToFolderWithIndexPath(
|
||||||
|
collections: HoppCollection<HoppRESTRequest>[],
|
||||||
|
indexPaths: number[]
|
||||||
|
) {
|
||||||
|
if (indexPaths.length === 0) return null
|
||||||
|
|
||||||
|
let target = collections[indexPaths.shift() as number]
|
||||||
|
|
||||||
|
while (indexPaths.length > 0)
|
||||||
|
target = target.folders[indexPaths.shift() as number]
|
||||||
|
|
||||||
|
return target !== undefined ? target : null
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildren(id: string | null): Ref<ChildrenResult<MyCollectionNode>> {
|
||||||
|
return computed(() => {
|
||||||
|
if (id === null) {
|
||||||
|
const data = this.data.value.map((item, index) => ({
|
||||||
|
id: index.toString(),
|
||||||
|
data: {
|
||||||
|
type: "collections",
|
||||||
|
data: {
|
||||||
|
parentIndex: null,
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return {
|
||||||
|
status: "loaded",
|
||||||
|
data: data,
|
||||||
|
} as ChildrenResult<Collection>
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexPath = id.split("/").map((x) => parseInt(x))
|
||||||
|
|
||||||
|
const item = this.navigateToFolderWithIndexPath(
|
||||||
|
this.data.value,
|
||||||
|
indexPath
|
||||||
|
)
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
const data = [
|
||||||
|
...item.folders.map((item, index) => ({
|
||||||
|
id: `${id}/${index}`,
|
||||||
|
data: {
|
||||||
|
type: "folders",
|
||||||
|
data: {
|
||||||
|
parentIndex: id,
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...item.requests.map((item, index) => ({
|
||||||
|
id: `${id}/${index}`,
|
||||||
|
data: {
|
||||||
|
type: "requests",
|
||||||
|
data: {
|
||||||
|
parentIndex: id,
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "loaded",
|
||||||
|
data: data,
|
||||||
|
} as ChildrenResult<Folder | Requests>
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: "loaded",
|
||||||
|
data: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const myAdapter: SmartTreeAdapter<MyCollectionNode> = new MyCollectionsAdapter(
|
||||||
|
refFilterCollection
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||||
|
<div
|
||||||
|
class="flex items-stretch group"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="dragStart"
|
||||||
|
@dragover.stop
|
||||||
|
@dragleave="dragging = false"
|
||||||
|
@dragend="dragging = false"
|
||||||
|
@contextmenu.prevent="options?.tippy.show()"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
|
||||||
|
:class="requestLabelColor"
|
||||||
|
@click="selectRequest()"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="IconCheckCircle"
|
||||||
|
v-if="isSelected"
|
||||||
|
class="svg-icons"
|
||||||
|
:class="{ 'text-accent': isSelected }"
|
||||||
|
/>
|
||||||
|
<span v-else class="font-semibold truncate text-tiny">
|
||||||
|
{{ request.method }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||||
|
@click="selectRequest()"
|
||||||
|
>
|
||||||
|
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||||
|
{{ request.name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="isActive"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
||||||
|
:title="`${t('collection.request_in_use')}`"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div v-if="!hasNoTeamAccess" class="flex">
|
||||||
|
<ButtonSecondary
|
||||||
|
v-if="!saveRequest"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconRotateCCW"
|
||||||
|
:title="t('action.restore')"
|
||||||
|
class="hidden group-hover:inline-flex"
|
||||||
|
@click="selectRequest()"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<tippy
|
||||||
|
ref="options"
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => tippyActions!.focus()"
|
||||||
|
>
|
||||||
|
<ButtonSecondary
|
||||||
|
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.e="edit?.$el.click()"
|
||||||
|
@keyup.d="duplicate?.$el.click()"
|
||||||
|
@keyup.delete="deleteAction?.$el.click()"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<SmartItem
|
||||||
|
ref="edit"
|
||||||
|
:icon="IconEdit"
|
||||||
|
:label="t('action.edit')"
|
||||||
|
:shortcut="['E']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('edit-request')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<SmartItem
|
||||||
|
ref="duplicate"
|
||||||
|
:icon="IconCopy"
|
||||||
|
:label="t('action.duplicate')"
|
||||||
|
:loading="duplicateLoading"
|
||||||
|
:shortcut="['D']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('duplicate-request'),
|
||||||
|
collectionsType === 'my-collections' ? hide() : null
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<SmartItem
|
||||||
|
ref="deleteAction"
|
||||||
|
:icon="IconTrash2"
|
||||||
|
:label="t('action.delete')"
|
||||||
|
:shortcut="['⌫']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('remove-request')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
|
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||||
|
import IconEdit from "~icons/lucide/edit"
|
||||||
|
import IconCopy from "~icons/lucide/copy"
|
||||||
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||||
|
import { ref, PropType, watch, computed } from "vue"
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { TippyComponent } from "vue-tippy"
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as RR from "fp-ts/ReadonlyRecord"
|
||||||
|
import * as O from "fp-ts/Option"
|
||||||
|
|
||||||
|
type CollectionType = "my-collections" | "team-collections"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
request: {
|
||||||
|
type: Object as PropType<HoppRESTRequest>,
|
||||||
|
default: () => ({}),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
collectionsType: {
|
||||||
|
type: String as PropType<CollectionType>,
|
||||||
|
default: "my-collections",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
duplicateLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
saveRequest: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
hasNoTeamAccess: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
isSelected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "edit-request"): void
|
||||||
|
(event: "duplicate-request"): void
|
||||||
|
(event: "remove-request"): void
|
||||||
|
(event: "select-request"): void
|
||||||
|
(event: "drag-request", payload: DataTransfer): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const tippyActions = ref<TippyComponent | null>(null)
|
||||||
|
const edit = ref<HTMLButtonElement | null>(null)
|
||||||
|
const deleteAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const options = ref<TippyComponent | null>(null)
|
||||||
|
const duplicate = ref<HTMLButtonElement | null>(null)
|
||||||
|
|
||||||
|
const dragging = ref(false)
|
||||||
|
|
||||||
|
const requestMethodLabels = {
|
||||||
|
get: "text-green-500",
|
||||||
|
post: "text-yellow-500",
|
||||||
|
put: "text-blue-500",
|
||||||
|
delete: "text-red-500",
|
||||||
|
default: "text-gray-500",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const requestLabelColor = computed(() =>
|
||||||
|
pipe(
|
||||||
|
requestMethodLabels,
|
||||||
|
RR.lookup(props.request.method.toLowerCase()),
|
||||||
|
O.getOrElseW(() => requestMethodLabels.default)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.duplicateLoading,
|
||||||
|
(val) => {
|
||||||
|
if (!val) {
|
||||||
|
options.value!.tippy.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectRequest = () => {
|
||||||
|
emit("select-request")
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragStart = ({ dataTransfer }: DragEvent) => {
|
||||||
|
if (dataTransfer) {
|
||||||
|
dragging.value = !dragging.value
|
||||||
|
emit("drag-request", dataTransfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
:picked="picked"
|
:picked="picked"
|
||||||
:save-request="true"
|
:save-request="true"
|
||||||
@select="onSelect"
|
@select="onSelect"
|
||||||
@update-collection="updateColl"
|
@update-team="updateTeam"
|
||||||
@update-coll-type="onUpdateCollType"
|
@update-collection-type="updateCollectionType"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
:label="`${t('action.save')}`"
|
:label="`${t('action.save')}`"
|
||||||
|
:loading="modalLoadingState"
|
||||||
outline
|
outline
|
||||||
@click="saveRequestAs"
|
@click="saveRequestAs"
|
||||||
/>
|
/>
|
||||||
@@ -61,99 +62,75 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, watch } from "vue"
|
import { useI18n } from "@composables/i18n"
|
||||||
import * as E from "fp-ts/Either"
|
import { useToast } from "@composables/toast"
|
||||||
import { HoppGQLRequest, isHoppRESTRequest } from "@hoppscotch/data"
|
|
||||||
import { cloneDeep } from "lodash-es"
|
|
||||||
import {
|
import {
|
||||||
editGraphqlRequest,
|
HoppGQLRequest,
|
||||||
editRESTRequest,
|
HoppRESTRequest,
|
||||||
saveGraphqlRequestAs,
|
isHoppRESTRequest,
|
||||||
saveRESTRequestAs,
|
} from "@hoppscotch/data"
|
||||||
} from "~/newstore/collections"
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import { cloneDeep } from "lodash-es"
|
||||||
|
import { reactive, ref, watch } from "vue"
|
||||||
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
|
import {
|
||||||
|
createRequestInCollection,
|
||||||
|
updateTeamRequest,
|
||||||
|
} from "~/helpers/backend/mutations/TeamRequest"
|
||||||
|
import { Picked } from "~/helpers/types/HoppPicked"
|
||||||
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
||||||
import {
|
import {
|
||||||
getRESTRequest,
|
getRESTRequest,
|
||||||
setRESTSaveContext,
|
setRESTSaveContext,
|
||||||
useRESTRequestName,
|
useRESTRequestName,
|
||||||
} from "~/newstore/RESTSession"
|
} from "~/newstore/RESTSession"
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
|
||||||
import {
|
import {
|
||||||
CreateRequestInCollectionDocument,
|
editGraphqlRequest,
|
||||||
UpdateRequestDocument,
|
editRESTRequest,
|
||||||
} from "~/helpers/backend/graphql"
|
saveGraphqlRequestAs,
|
||||||
|
saveRESTRequestAs,
|
||||||
|
} from "~/newstore/collections"
|
||||||
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||||
|
|
||||||
type CollectionType =
|
type CollectionType =
|
||||||
| {
|
|
||||||
type: "my-collections"
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
type: "team-collections"
|
type: "team-collections"
|
||||||
// TODO: Figure this type out
|
selectedTeam: SelectedTeam
|
||||||
selectedTeam: {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
| { type: "my-collections"; selectedTeam: undefined }
|
||||||
|
|
||||||
type Picked =
|
const props = withDefaults(
|
||||||
| {
|
defineProps<{
|
||||||
pickedType: "my-request"
|
show: boolean
|
||||||
folderPath: string
|
mode: "rest" | "graphql"
|
||||||
requestIndex: number
|
}>(),
|
||||||
}
|
{
|
||||||
| {
|
show: false,
|
||||||
pickedType: "my-folder"
|
mode: "rest",
|
||||||
folderPath: string
|
}
|
||||||
}
|
)
|
||||||
| {
|
|
||||||
pickedType: "my-collection"
|
|
||||||
collectionIndex: number
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
pickedType: "teams-request"
|
|
||||||
requestID: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
pickedType: "teams-folder"
|
|
||||||
folderID: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
pickedType: "teams-collection"
|
|
||||||
collectionID: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
pickedType: "gql-my-request"
|
|
||||||
folderPath: string
|
|
||||||
requestIndex: number
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
pickedType: "gql-my-folder"
|
|
||||||
folderPath: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
pickedType: "gql-my-collection"
|
|
||||||
collectionIndex: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
mode: "rest" | "graphql"
|
|
||||||
show: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
event: "edit-request",
|
||||||
|
payload: {
|
||||||
|
folderPath: string
|
||||||
|
requestIndex: string
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
): void
|
||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const toast = useToast()
|
const requestName = ref(
|
||||||
|
|
||||||
// TODO: Use a better implementation with computed ?
|
|
||||||
// This implementation can't work across updates to mode prop (which won't happen tho)
|
|
||||||
const requestName =
|
|
||||||
props.mode === "rest" ? useRESTRequestName() : useGQLRequestName()
|
props.mode === "rest" ? useRESTRequestName() : useGQLRequestName()
|
||||||
|
)
|
||||||
|
|
||||||
const requestData = reactive({
|
const requestData = reactive({
|
||||||
name: requestName,
|
name: requestName,
|
||||||
@@ -164,11 +141,13 @@ const requestData = reactive({
|
|||||||
|
|
||||||
const collectionsType = ref<CollectionType>({
|
const collectionsType = ref<CollectionType>({
|
||||||
type: "my-collections",
|
type: "my-collections",
|
||||||
|
selectedTeam: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Figure this type out
|
|
||||||
const picked = ref<Picked | null>(null)
|
const picked = ref<Picked | null>(null)
|
||||||
|
|
||||||
|
const modalLoadingState = ref(false)
|
||||||
|
|
||||||
// Resets
|
// Resets
|
||||||
watch(
|
watch(
|
||||||
() => requestData.collectionIndex,
|
() => requestData.collectionIndex,
|
||||||
@@ -184,20 +163,18 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// All the methods
|
const updateTeam = (newTeam: SelectedTeam) => {
|
||||||
const onUpdateCollType = (newCollType: CollectionType) => {
|
collectionsType.value.selectedTeam = newTeam
|
||||||
collectionsType.value = newCollType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSelect = ({ picked: pickedVal }: { picked: Picked | null }) => {
|
const updateCollectionType = (type: CollectionType["type"]) => {
|
||||||
|
collectionsType.value.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelect = (pickedVal: Picked | null) => {
|
||||||
picked.value = pickedVal
|
picked.value = pickedVal
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideModal = () => {
|
|
||||||
picked.value = null
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveRequestAs = async () => {
|
const saveRequestAs = async () => {
|
||||||
if (!requestName.value) {
|
if (!requestName.value) {
|
||||||
toast.error(`${t("error.empty_req_name")}`)
|
toast.error(`${t("error.empty_req_name")}`)
|
||||||
@@ -208,35 +185,25 @@ const saveRequestAs = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone Deep because objects are shared by reference so updating
|
|
||||||
// just one bit will update other referenced shared instances
|
|
||||||
const requestUpdated =
|
const requestUpdated =
|
||||||
props.mode === "rest"
|
props.mode === "rest"
|
||||||
? cloneDeep(getRESTRequest())
|
? cloneDeep(getRESTRequest())
|
||||||
: cloneDeep(getGQLSession().request)
|
: cloneDeep(getGQLSession().request)
|
||||||
|
|
||||||
// // Filter out all REST file inputs
|
if (picked.value.pickedType === "my-collection") {
|
||||||
// if (this.mode === "rest" && requestUpdated.bodyParams) {
|
|
||||||
// requestUpdated.bodyParams = requestUpdated.bodyParams.map((param) =>
|
|
||||||
// param?.value?.[0] instanceof File ? { ...param, value: "" } : param
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (picked.value.pickedType === "my-request") {
|
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
throw new Error("requestUpdated is not a REST Request")
|
throw new Error("requestUpdated is not a REST Request")
|
||||||
|
|
||||||
editRESTRequest(
|
const insertionIndex = saveRESTRequestAs(
|
||||||
picked.value.folderPath,
|
`${picked.value.collectionIndex}`,
|
||||||
picked.value.requestIndex,
|
|
||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
setRESTSaveContext({
|
setRESTSaveContext({
|
||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: picked.value.folderPath,
|
folderPath: `${picked.value.collectionIndex}`,
|
||||||
requestIndex: picked.value.requestIndex,
|
requestIndex: insertionIndex,
|
||||||
req: cloneDeep(requestUpdated),
|
req: requestUpdated,
|
||||||
})
|
})
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
@@ -253,114 +220,68 @@ const saveRequestAs = async () => {
|
|||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: picked.value.folderPath,
|
folderPath: picked.value.folderPath,
|
||||||
requestIndex: insertionIndex,
|
requestIndex: insertionIndex,
|
||||||
req: cloneDeep(requestUpdated),
|
req: requestUpdated,
|
||||||
})
|
})
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
} else if (picked.value.pickedType === "my-collection") {
|
} else if (picked.value.pickedType === "my-request") {
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
throw new Error("requestUpdated is not a REST Request")
|
throw new Error("requestUpdated is not a REST Request")
|
||||||
|
|
||||||
const insertionIndex = saveRESTRequestAs(
|
editRESTRequest(
|
||||||
`${picked.value.collectionIndex}`,
|
picked.value.folderPath,
|
||||||
|
picked.value.requestIndex,
|
||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
setRESTSaveContext({
|
setRESTSaveContext({
|
||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: `${picked.value.collectionIndex}`,
|
folderPath: picked.value.folderPath,
|
||||||
requestIndex: insertionIndex,
|
requestIndex: picked.value.requestIndex,
|
||||||
req: cloneDeep(requestUpdated),
|
req: requestUpdated,
|
||||||
})
|
})
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
} else if (picked.value.pickedType === "teams-request") {
|
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
|
||||||
throw new Error("requestUpdated is not a REST Request")
|
|
||||||
|
|
||||||
if (collectionsType.value.type !== "team-collections")
|
|
||||||
throw new Error("Collections Type mismatch")
|
|
||||||
|
|
||||||
runMutation(UpdateRequestDocument, {
|
|
||||||
requestID: picked.value.requestID,
|
|
||||||
data: {
|
|
||||||
request: JSON.stringify(requestUpdated),
|
|
||||||
title: requestUpdated.name,
|
|
||||||
},
|
|
||||||
})().then((result) => {
|
|
||||||
if (E.isLeft(result)) {
|
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
|
||||||
throw new Error(`${result.left}`)
|
|
||||||
} else {
|
|
||||||
requestSaved()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setRESTSaveContext({
|
|
||||||
originLocation: "team-collection",
|
|
||||||
requestID: picked.value.requestID,
|
|
||||||
req: cloneDeep(requestUpdated),
|
|
||||||
})
|
|
||||||
} else if (picked.value.pickedType === "teams-folder") {
|
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
|
||||||
throw new Error("requestUpdated is not a REST Request")
|
|
||||||
|
|
||||||
if (collectionsType.value.type !== "team-collections")
|
|
||||||
throw new Error("Collections Type mismatch")
|
|
||||||
|
|
||||||
const result = await runMutation(CreateRequestInCollectionDocument, {
|
|
||||||
collectionID: picked.value.folderID,
|
|
||||||
data: {
|
|
||||||
request: JSON.stringify(requestUpdated),
|
|
||||||
teamID: collectionsType.value.selectedTeam.id,
|
|
||||||
title: requestUpdated.name,
|
|
||||||
},
|
|
||||||
})()
|
|
||||||
|
|
||||||
if (E.isLeft(result)) {
|
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
|
||||||
console.error(result.left)
|
|
||||||
} else {
|
|
||||||
setRESTSaveContext({
|
|
||||||
originLocation: "team-collection",
|
|
||||||
requestID: result.right.createRequestInCollection.id,
|
|
||||||
teamID: collectionsType.value.selectedTeam.id,
|
|
||||||
collectionID: picked.value.folderID,
|
|
||||||
req: cloneDeep(requestUpdated),
|
|
||||||
})
|
|
||||||
|
|
||||||
requestSaved()
|
|
||||||
}
|
|
||||||
} else if (picked.value.pickedType === "teams-collection") {
|
} else if (picked.value.pickedType === "teams-collection") {
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
throw new Error("requestUpdated is not a REST Request")
|
throw new Error("requestUpdated is not a REST Request")
|
||||||
|
|
||||||
if (collectionsType.value.type !== "team-collections")
|
updateTeamCollectionOrFolder(picked.value.collectionID, requestUpdated)
|
||||||
|
} else if (picked.value.pickedType === "teams-folder") {
|
||||||
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
|
throw new Error("requestUpdated is not a REST Request")
|
||||||
|
|
||||||
|
updateTeamCollectionOrFolder(picked.value.folderID, requestUpdated)
|
||||||
|
} else if (picked.value.pickedType === "teams-request") {
|
||||||
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
|
throw new Error("requestUpdated is not a REST Request")
|
||||||
|
|
||||||
|
if (
|
||||||
|
collectionsType.value.type !== "team-collections" ||
|
||||||
|
!collectionsType.value.selectedTeam
|
||||||
|
)
|
||||||
throw new Error("Collections Type mismatch")
|
throw new Error("Collections Type mismatch")
|
||||||
|
|
||||||
const result = await runMutation(CreateRequestInCollectionDocument, {
|
modalLoadingState.value = true
|
||||||
collectionID: picked.value.collectionID,
|
|
||||||
data: {
|
|
||||||
title: requestUpdated.name,
|
|
||||||
request: JSON.stringify(requestUpdated),
|
|
||||||
teamID: collectionsType.value.selectedTeam.id,
|
|
||||||
},
|
|
||||||
})()
|
|
||||||
|
|
||||||
if (E.isLeft(result)) {
|
const data = {
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
request: JSON.stringify(requestUpdated),
|
||||||
console.error(result.left)
|
title: requestUpdated.name,
|
||||||
} else {
|
|
||||||
setRESTSaveContext({
|
|
||||||
originLocation: "team-collection",
|
|
||||||
requestID: result.right.createRequestInCollection.id,
|
|
||||||
teamID: collectionsType.value.selectedTeam.id,
|
|
||||||
collectionID: picked.value.collectionID,
|
|
||||||
req: cloneDeep(requestUpdated),
|
|
||||||
})
|
|
||||||
|
|
||||||
requestSaved()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pipe(
|
||||||
|
updateTeamRequest(picked.value.requestID, data),
|
||||||
|
TE.match(
|
||||||
|
(err: GQLError<string>) => {
|
||||||
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
|
modalLoadingState.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
modalLoadingState.value = false
|
||||||
|
requestSaved()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
} else if (picked.value.pickedType === "gql-my-request") {
|
} else if (picked.value.pickedType === "gql-my-request") {
|
||||||
// TODO: Check for GQL request ?
|
// TODO: Check for GQL request ?
|
||||||
editGraphqlRequest(
|
editGraphqlRequest(
|
||||||
@@ -389,12 +310,81 @@ const saveRequestAs = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a team collection or folder and sets the save context to the updated request
|
||||||
|
* @param collectionID - ID of the collection or folder
|
||||||
|
* @param requestUpdated - Updated request
|
||||||
|
*/
|
||||||
|
const updateTeamCollectionOrFolder = (
|
||||||
|
collectionID: string,
|
||||||
|
requestUpdated: HoppRESTRequest
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
collectionsType.value.type !== "team-collections" ||
|
||||||
|
!collectionsType.value.selectedTeam
|
||||||
|
)
|
||||||
|
throw new Error("Collections Type mismatch")
|
||||||
|
|
||||||
|
modalLoadingState.value = true
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
title: requestUpdated.name,
|
||||||
|
request: JSON.stringify(requestUpdated),
|
||||||
|
teamID: collectionsType.value.selectedTeam.id,
|
||||||
|
}
|
||||||
|
pipe(
|
||||||
|
createRequestInCollection(collectionID, data),
|
||||||
|
TE.match(
|
||||||
|
(err: GQLError<string>) => {
|
||||||
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
|
modalLoadingState.value = false
|
||||||
|
},
|
||||||
|
(result) => {
|
||||||
|
const { createRequestInCollection } = result
|
||||||
|
|
||||||
|
setRESTSaveContext({
|
||||||
|
originLocation: "team-collection",
|
||||||
|
requestID: createRequestInCollection.id,
|
||||||
|
collectionID: createRequestInCollection.collection.id,
|
||||||
|
teamID: createRequestInCollection.collection.team.id,
|
||||||
|
req: requestUpdated,
|
||||||
|
})
|
||||||
|
modalLoadingState.value = false
|
||||||
|
requestSaved()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
|
|
||||||
const requestSaved = () => {
|
const requestSaved = () => {
|
||||||
toast.success(`${t("request.added")}`)
|
toast.success(`${t("request.added")}`)
|
||||||
hideModal()
|
hideModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateColl = (ev: CollectionType["type"]) => {
|
const hideModal = () => {
|
||||||
collectionsType.value.type = ev
|
picked.value = null
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
|
|
||||||
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
|
console.error(err)
|
||||||
|
if (err.type === "network_error") {
|
||||||
|
return t("error.network_error")
|
||||||
|
} else {
|
||||||
|
switch (err.error) {
|
||||||
|
case "team_coll/short_title":
|
||||||
|
return t("collection.name_length_insufficient")
|
||||||
|
case "team/invalid_coll_id":
|
||||||
|
return t("team.invalid_id")
|
||||||
|
case "team/not_required_role":
|
||||||
|
return t("profile.no_permission")
|
||||||
|
case "team_req/not_required_role":
|
||||||
|
return t("profile.no_permission")
|
||||||
|
case "Forbidden resource":
|
||||||
|
return t("profile.no_permission")
|
||||||
|
default:
|
||||||
|
return t("error.something_went_wrong")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,625 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<div
|
||||||
|
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||||
|
:style="
|
||||||
|
saveRequest
|
||||||
|
? 'top: calc(var(--upper-secondary-sticky-fold) - var(--line-height-body))'
|
||||||
|
: 'top: var(--upper-secondary-sticky-fold)'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-if="hasNoTeamAccess"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
disabled
|
||||||
|
class="!rounded-none"
|
||||||
|
:icon="IconPlus"
|
||||||
|
:title="t('team.no_access')"
|
||||||
|
:label="t('action.new')"
|
||||||
|
/>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-else
|
||||||
|
:icon="IconPlus"
|
||||||
|
:label="t('action.new')"
|
||||||
|
class="!rounded-none"
|
||||||
|
@click="emit('display-modal-add')"
|
||||||
|
/>
|
||||||
|
<span class="flex">
|
||||||
|
<ButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
to="https://docs.hoppscotch.io/features/collections"
|
||||||
|
blank
|
||||||
|
:title="t('app.wiki')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
/>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-if="!saveRequest"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:disabled="
|
||||||
|
collectionsType.type === 'team-collections' &&
|
||||||
|
collectionsType.selectedTeam === undefined
|
||||||
|
"
|
||||||
|
:icon="IconArchive"
|
||||||
|
:title="t('modal.import_export')"
|
||||||
|
@click="emit('display-modal-import-export')"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<SmartTree :adapter="teamAdapter">
|
||||||
|
<template #content="{ node, toggleChildren, isOpen }">
|
||||||
|
<CollectionsCollection
|
||||||
|
v-if="node.data.type === 'collections'"
|
||||||
|
:data="node.data.data.data"
|
||||||
|
:collections-type="collectionsType.type"
|
||||||
|
:is-open="isOpen"
|
||||||
|
:export-loading="exportLoading"
|
||||||
|
:has-no-team-access="hasNoTeamAccess"
|
||||||
|
:is-selected="
|
||||||
|
isSelected({
|
||||||
|
collectionID: node.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
folder-type="collection"
|
||||||
|
@add-request="
|
||||||
|
node.data.type === 'collections' &&
|
||||||
|
emit('add-request', {
|
||||||
|
path: node.id,
|
||||||
|
folder: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@add-folder="
|
||||||
|
node.data.type === 'collections' &&
|
||||||
|
emit('add-folder', {
|
||||||
|
path: node.id,
|
||||||
|
folder: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@edit-collection="
|
||||||
|
node.data.type === 'collections' &&
|
||||||
|
emit('edit-collection', {
|
||||||
|
collectionIndex: node.id,
|
||||||
|
collection: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@export-data="
|
||||||
|
node.data.type === 'collections' &&
|
||||||
|
emit('export-data', node.data.data.data)
|
||||||
|
"
|
||||||
|
@remove-collection="emit('remove-collection', node.id)"
|
||||||
|
@toggle-children="
|
||||||
|
() => {
|
||||||
|
toggleChildren(),
|
||||||
|
saveRequest &&
|
||||||
|
emit('select', {
|
||||||
|
pickedType: 'teams-collection',
|
||||||
|
collectionID: node.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<CollectionsCollection
|
||||||
|
v-if="node.data.type === 'folders'"
|
||||||
|
:data="node.data.data.data"
|
||||||
|
:collections-type="collectionsType.type"
|
||||||
|
:is-open="isOpen"
|
||||||
|
:export-loading="exportLoading"
|
||||||
|
:has-no-team-access="hasNoTeamAccess"
|
||||||
|
:is-selected="
|
||||||
|
isSelected({
|
||||||
|
folderID: node.data.data.data.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
folder-type="folder"
|
||||||
|
@add-request="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('add-request', {
|
||||||
|
path: node.id,
|
||||||
|
folder: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@add-folder="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('add-folder', {
|
||||||
|
path: node.id,
|
||||||
|
folder: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@edit-collection="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('edit-folder', {
|
||||||
|
folder: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@export-data="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('export-data', node.data.data.data)
|
||||||
|
"
|
||||||
|
@remove-collection="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('remove-folder', node.data.data.data.id)
|
||||||
|
"
|
||||||
|
@toggle-children="
|
||||||
|
() => {
|
||||||
|
toggleChildren(),
|
||||||
|
saveRequest &&
|
||||||
|
emit('select', {
|
||||||
|
pickedType: 'teams-folder',
|
||||||
|
folderID: node.data.data.data.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<CollectionsRequest
|
||||||
|
v-if="node.data.type === 'requests'"
|
||||||
|
:request="node.data.data.data.request"
|
||||||
|
:collections-type="collectionsType.type"
|
||||||
|
:duplicate-loading="duplicateLoading"
|
||||||
|
:is-active="isActiveRequest(node.data.data.data.id)"
|
||||||
|
:has-no-team-access="hasNoTeamAccess"
|
||||||
|
:is-selected="
|
||||||
|
isSelected({
|
||||||
|
requestID: node.data.data.data.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@edit-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
emit('edit-request', {
|
||||||
|
requestIndex: node.data.data.data.id,
|
||||||
|
request: node.data.data.data.request,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@duplicate-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
emit('duplicate-request', {
|
||||||
|
folderPath: node.data.data.parentIndex,
|
||||||
|
request: node.data.data.data.request,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@remove-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
emit('remove-request', {
|
||||||
|
folderPath: null,
|
||||||
|
requestIndex: node.data.data.data.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@select-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
selectRequest({
|
||||||
|
request: node.data.data.data.request,
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<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.collection')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collections") }}
|
||||||
|
</span>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-if="hasNoTeamAccess"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
disabled
|
||||||
|
filled
|
||||||
|
outline
|
||||||
|
:title="t('team.no_access')"
|
||||||
|
:label="t('add.new')"
|
||||||
|
/>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-else
|
||||||
|
:label="t('add.new')"
|
||||||
|
filled
|
||||||
|
outline
|
||||||
|
@click="emit('display-modal-add')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="node.data.type === 'collections'"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
>
|
||||||
|
<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.collection')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collection") }}
|
||||||
|
</span>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-if="hasNoTeamAccess"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
disabled
|
||||||
|
filled
|
||||||
|
outline
|
||||||
|
:title="t('team.no_access')"
|
||||||
|
:label="t('action.new')"
|
||||||
|
/>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-else
|
||||||
|
:icon="IconPlus"
|
||||||
|
:label="t('action.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"
|
||||||
|
>
|
||||||
|
<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.folder')}`"
|
||||||
|
/>
|
||||||
|
<span class="text-center">
|
||||||
|
{{ t("empty.folder") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SmartTree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconArchive from "~icons/lucide/archive"
|
||||||
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
|
import { computed, PropType, Ref, toRef } from "vue"
|
||||||
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useColorMode } from "@composables/theming"
|
||||||
|
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||||
|
import { TeamRequest } from "~/helpers/teams/TeamRequest"
|
||||||
|
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
||||||
|
import { cloneDeep } from "lodash-es"
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
import { restSaveContext$ } from "~/newstore/RESTSession"
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as O from "fp-ts/Option"
|
||||||
|
import { Picked } from "~/helpers/types/HoppPicked.js"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||||
|
|
||||||
|
type CollectionType =
|
||||||
|
| {
|
||||||
|
type: "team-collections"
|
||||||
|
selectedTeam: SelectedTeam
|
||||||
|
}
|
||||||
|
| { type: "my-collections"; selectedTeam: undefined }
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
collectionsType: {
|
||||||
|
type: Object as PropType<CollectionType>,
|
||||||
|
default: () => ({ type: "my-collections", selectedTeam: undefined }),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
teamCollectionList: {
|
||||||
|
type: Array as PropType<TeamCollection[]>,
|
||||||
|
default: () => [],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
teamLoadingCollections: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: () => [],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
saveRequest: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
exportLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
duplicateLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
picked: {
|
||||||
|
type: Object as PropType<Picked | null>,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
event: "add-request",
|
||||||
|
payload: {
|
||||||
|
path: string
|
||||||
|
folder: TeamCollection
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "add-folder",
|
||||||
|
payload: {
|
||||||
|
path: string
|
||||||
|
folder: TeamCollection
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "edit-collection",
|
||||||
|
payload: {
|
||||||
|
collectionIndex: string
|
||||||
|
collection: TeamCollection
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "edit-folder",
|
||||||
|
payload: {
|
||||||
|
folder: TeamCollection
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "edit-request",
|
||||||
|
payload: {
|
||||||
|
requestIndex: string
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "duplicate-request",
|
||||||
|
payload: {
|
||||||
|
folderPath: string
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(event: "export-data", payload: TeamCollection): void
|
||||||
|
(event: "remove-collection", payload: string): void
|
||||||
|
(event: "remove-folder", payload: string): void
|
||||||
|
(
|
||||||
|
event: "remove-request",
|
||||||
|
payload: {
|
||||||
|
folderPath: string | null
|
||||||
|
requestIndex: string
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(
|
||||||
|
event: "select-request",
|
||||||
|
payload: {
|
||||||
|
request: HoppRESTRequest
|
||||||
|
requestIndex: string
|
||||||
|
isActive: boolean
|
||||||
|
folderPath?: string | undefined
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(event: "select", payload: Picked | null): void
|
||||||
|
(event: "expand-team-collection", payload: string): void
|
||||||
|
(event: "display-modal-add"): void
|
||||||
|
(event: "display-modal-import-export"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const teamCollectionsList = toRef(props, "teamCollectionList")
|
||||||
|
|
||||||
|
const hasNoTeamAccess = computed(
|
||||||
|
() =>
|
||||||
|
props.collectionsType.type === "team-collections" &&
|
||||||
|
(props.collectionsType.selectedTeam === undefined ||
|
||||||
|
props.collectionsType.selectedTeam.myRole === "VIEWER")
|
||||||
|
)
|
||||||
|
|
||||||
|
const isSelected = computed(() => {
|
||||||
|
return ({
|
||||||
|
collectionID,
|
||||||
|
folderID,
|
||||||
|
requestID,
|
||||||
|
}: {
|
||||||
|
collectionID?: string | undefined
|
||||||
|
folderID?: string | undefined
|
||||||
|
requestID?: string | undefined
|
||||||
|
}) => {
|
||||||
|
if (collectionID !== undefined) {
|
||||||
|
return (
|
||||||
|
props.picked &&
|
||||||
|
props.picked.pickedType === "teams-collection" &&
|
||||||
|
props.picked.collectionID === collectionID
|
||||||
|
)
|
||||||
|
} else if (requestID !== undefined) {
|
||||||
|
return (
|
||||||
|
props.picked &&
|
||||||
|
props.picked.pickedType === "teams-request" &&
|
||||||
|
props.picked.requestID === requestID
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
props.picked &&
|
||||||
|
props.picked.pickedType === "teams-folder" &&
|
||||||
|
props.picked.folderID === folderID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const active = useReadonlyStream(restSaveContext$, null)
|
||||||
|
|
||||||
|
const isActiveRequest = computed(() => {
|
||||||
|
return (requestID: string) => {
|
||||||
|
return pipe(
|
||||||
|
active.value,
|
||||||
|
O.fromNullable,
|
||||||
|
O.filter(
|
||||||
|
(active) =>
|
||||||
|
active.originLocation === "team-collection" &&
|
||||||
|
active.requestID === requestID
|
||||||
|
),
|
||||||
|
O.isSome
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectRequest = (data: {
|
||||||
|
request: HoppRESTRequest
|
||||||
|
requestIndex: string
|
||||||
|
}) => {
|
||||||
|
const { request, requestIndex } = data
|
||||||
|
if (props.saveRequest) {
|
||||||
|
emit("select", {
|
||||||
|
pickedType: "teams-request",
|
||||||
|
requestID: requestIndex,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
emit("select-request", {
|
||||||
|
request: request,
|
||||||
|
requestIndex: requestIndex,
|
||||||
|
isActive: isActiveRequest.value(requestIndex),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeamCollections = {
|
||||||
|
type: "collections"
|
||||||
|
data: {
|
||||||
|
parentIndex: null
|
||||||
|
data: TeamCollection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeamFolder = {
|
||||||
|
type: "folders"
|
||||||
|
data: {
|
||||||
|
parentIndex: string
|
||||||
|
data: TeamCollection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeamRequests = {
|
||||||
|
type: "requests"
|
||||||
|
data: {
|
||||||
|
parentIndex: string
|
||||||
|
data: TeamRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeamCollectionNode = TeamCollections | TeamFolder | TeamRequests
|
||||||
|
|
||||||
|
class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
|
||||||
|
constructor(public data: Ref<TeamCollection[]>) {}
|
||||||
|
|
||||||
|
findCollInTree(
|
||||||
|
tree: TeamCollection[],
|
||||||
|
targetID: string
|
||||||
|
): TeamCollection | null {
|
||||||
|
for (const coll of tree) {
|
||||||
|
// If the direct child matched, then return that
|
||||||
|
if (coll.id === targetID) return coll
|
||||||
|
|
||||||
|
// Else run it in the children
|
||||||
|
if (coll.children) {
|
||||||
|
const result = this.findCollInTree(coll.children, targetID)
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing matched, return null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildren(id: string | null): Ref<ChildrenResult<TeamCollectionNode>> {
|
||||||
|
return computed(() => {
|
||||||
|
if (id === null) {
|
||||||
|
if (props.teamLoadingCollections.includes("root")) {
|
||||||
|
return {
|
||||||
|
status: "loading",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const data = this.data.value.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
data: {
|
||||||
|
type: "collections",
|
||||||
|
data: {
|
||||||
|
parentIndex: null,
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return {
|
||||||
|
status: "loaded",
|
||||||
|
data: cloneDeep(data),
|
||||||
|
} as ChildrenResult<TeamCollections>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const parsedID = id.split("/")[id.split("/").length - 1]
|
||||||
|
|
||||||
|
!props.teamLoadingCollections.includes(parsedID) &&
|
||||||
|
emit("expand-team-collection", parsedID)
|
||||||
|
|
||||||
|
if (props.teamLoadingCollections.includes(parsedID)) {
|
||||||
|
return {
|
||||||
|
status: "loading",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const items = this.findCollInTree(this.data.value, parsedID)
|
||||||
|
if (items) {
|
||||||
|
const data = [
|
||||||
|
...(items.children
|
||||||
|
? items.children.map((item) => ({
|
||||||
|
id: `${id}/${item.id}`,
|
||||||
|
data: {
|
||||||
|
type: "folders",
|
||||||
|
data: {
|
||||||
|
parentIndex: parsedID,
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
...(items.requests
|
||||||
|
? items.requests.map((item) => ({
|
||||||
|
id: `${id}/${item.id}`,
|
||||||
|
data: {
|
||||||
|
type: "requests",
|
||||||
|
data: {
|
||||||
|
parentIndex: parsedID,
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
status: "loaded",
|
||||||
|
data: cloneDeep(data),
|
||||||
|
} as ChildrenResult<TeamFolder | TeamRequests>
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: "loaded",
|
||||||
|
data: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamAdapter: SmartTreeAdapter<TeamCollectionNode> =
|
||||||
|
new TeamCollectionsAdapter(teamCollectionsList)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-1">
|
||||||
|
<SmartIntersection
|
||||||
|
class="flex flex-1 flex-col"
|
||||||
|
@intersecting="onTeamSelectIntersect"
|
||||||
|
>
|
||||||
|
<tippy
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
placement="bottom"
|
||||||
|
:on-shown="() => tippyActions!.focus()"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="`${t('collection.select_team')}`"
|
||||||
|
class="bg-transparent border-b border-dividerLight select-wrapper"
|
||||||
|
>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-if="collectionsType.selectedTeam"
|
||||||
|
:icon="IconUsers"
|
||||||
|
:label="collectionsType.selectedTeam.name"
|
||||||
|
class="flex-1 !justify-start pr-8 rounded-none"
|
||||||
|
/>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-else
|
||||||
|
:label="`${t('collection.select_team')}`"
|
||||||
|
class="flex-1 !justify-start pr-8 rounded-none"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="tippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isTeamListLoading && myTeams.length === 0"
|
||||||
|
class="flex flex-col flex-1 items-center justify-center p-2"
|
||||||
|
>
|
||||||
|
<SmartSpinner class="my-2" />
|
||||||
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="myTeams.length > 0" class="flex flex-col">
|
||||||
|
<SmartItem
|
||||||
|
v-for="(team, index) in myTeams"
|
||||||
|
:key="`team-${index}`"
|
||||||
|
:label="team.name"
|
||||||
|
:info-icon="
|
||||||
|
team.id === collectionsType.selectedTeam?.id
|
||||||
|
? IconDone
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
:active-info-icon="team.id === collectionsType.selectedTeam?.id"
|
||||||
|
:icon="IconUsers"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
updateSelectedTeam(team)
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<SmartItem
|
||||||
|
:icon="IconPlus"
|
||||||
|
:label="t('team.create_new')"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
displayTeamModalAdd(true)
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col items-center justify-center text-secondaryLight p-2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-14 h-14 mb-4"
|
||||||
|
:alt="`${t('empty.teams')}`"
|
||||||
|
/>
|
||||||
|
<span class="text-center pb-4">
|
||||||
|
{{ t("empty.teams") }}
|
||||||
|
</span>
|
||||||
|
<ButtonSecondary
|
||||||
|
:label="t('team.create_new')"
|
||||||
|
filled
|
||||||
|
outline
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
displayTeamModalAdd(true)
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</SmartIntersection>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconUsers from "~icons/lucide/users"
|
||||||
|
import IconDone from "~icons/lucide/check"
|
||||||
|
import { PropType, ref } from "vue"
|
||||||
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
|
import { TippyComponent } from "vue-tippy"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useColorMode } from "@composables/theming"
|
||||||
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||||
|
|
||||||
|
type CollectionType =
|
||||||
|
| {
|
||||||
|
type: "team-collections"
|
||||||
|
selectedTeam: SelectedTeam
|
||||||
|
}
|
||||||
|
| { type: "my-collections"; selectedTeam: undefined }
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
collectionsType: {
|
||||||
|
type: Object as PropType<CollectionType>,
|
||||||
|
default: () => ({ type: "my-collections", selectedTeam: undefined }),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
myTeams: {
|
||||||
|
type: Array as PropType<GetMyTeamsQuery["myTeams"]>,
|
||||||
|
default: () => [],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isTeamListLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const tippyActions = ref<TippyComponent | null>(null)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update-selected-team", payload: SelectedTeam): void
|
||||||
|
(e: "team-select-intersect", payload: boolean): void
|
||||||
|
(e: "display-team-modal-add", payload: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const updateSelectedTeam = (team: SelectedTeam) => {
|
||||||
|
emit("update-selected-team", team)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTeamSelectIntersect = () => {
|
||||||
|
emit("team-select-intersect", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayTeamModalAdd = (display: boolean) => {
|
||||||
|
emit("display-team-modal-add", display)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,354 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
|
||||||
<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="toggleShowChildren()"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="getCollectionIcon"
|
|
||||||
class="svg-icons"
|
|
||||||
:class="{ 'text-accent': isSelected }"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
|
||||||
@click="toggleShowChildren()"
|
|
||||||
>
|
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
|
||||||
{{ collection.name }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div class="flex">
|
|
||||||
<ButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconFilePlus"
|
|
||||||
:title="t('request.new')"
|
|
||||||
class="hidden group-hover:inline-flex"
|
|
||||||
@click="
|
|
||||||
$emit('add-request', {
|
|
||||||
path: `${collectionIndex}`,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<ButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconFolderPlus"
|
|
||||||
:title="t('folder.new')"
|
|
||||||
class="hidden group-hover:inline-flex"
|
|
||||||
@click="
|
|
||||||
$emit('add-folder', {
|
|
||||||
folder: collection,
|
|
||||||
path: `${collectionIndex}`,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
<tippy
|
|
||||||
ref="options"
|
|
||||||
interactive
|
|
||||||
trigger="click"
|
|
||||||
theme="popover"
|
|
||||||
:on-shown="() => tippyActions.focus()"
|
|
||||||
>
|
|
||||||
<ButtonSecondary
|
|
||||||
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()"
|
|
||||||
>
|
|
||||||
<SmartItem
|
|
||||||
ref="requestAction"
|
|
||||||
:icon="IconFilePlus"
|
|
||||||
:label="t('request.new')"
|
|
||||||
:shortcut="['R']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
$emit('add-request', {
|
|
||||||
path: `${collectionIndex}`,
|
|
||||||
})
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="folderAction"
|
|
||||||
:icon="IconFolderPlus"
|
|
||||||
:label="t('folder.new')"
|
|
||||||
:shortcut="['N']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
$emit('add-folder', {
|
|
||||||
folder: collection,
|
|
||||||
path: `${collectionIndex}`,
|
|
||||||
})
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="edit"
|
|
||||||
:icon="IconEdit"
|
|
||||||
:label="t('action.edit')"
|
|
||||||
:shortcut="['E']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
$emit('edit-collection')
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="exportAction"
|
|
||||||
:icon="IconDownload"
|
|
||||||
:label="t('export.title')"
|
|
||||||
:shortcut="['X']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
exportCollection()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="deleteAction"
|
|
||||||
:icon="IconTrash2"
|
|
||||||
:label="t('action.delete')"
|
|
||||||
:shortcut="['⌫']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
removeCollection()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="showChildren || isFiltered" class="flex">
|
|
||||||
<div
|
|
||||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
|
||||||
@click="toggleShowChildren()"
|
|
||||||
></div>
|
|
||||||
<div class="flex flex-col flex-1 truncate">
|
|
||||||
<CollectionsMyFolder
|
|
||||||
v-for="(folder, index) in collection.folders"
|
|
||||||
:key="`folder-${index}`"
|
|
||||||
:folder="folder"
|
|
||||||
:folder-index="index"
|
|
||||||
:folder-path="`${collectionIndex}/${index}`"
|
|
||||||
:collection-index="collectionIndex"
|
|
||||||
:save-request="saveRequest"
|
|
||||||
:collections-type="collectionsType"
|
|
||||||
:is-filtered="isFiltered"
|
|
||||||
:picked="picked"
|
|
||||||
@add-request="$emit('add-request', $event)"
|
|
||||||
@add-folder="$emit('add-folder', $event)"
|
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
|
||||||
@edit-request="$emit('edit-request', $event)"
|
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
|
||||||
@select="$emit('select', $event)"
|
|
||||||
@remove-request="$emit('remove-request', $event)"
|
|
||||||
@remove-folder="$emit('remove-folder', $event)"
|
|
||||||
/>
|
|
||||||
<CollectionsMyRequest
|
|
||||||
v-for="(request, index) in collection.requests"
|
|
||||||
:key="`request-${index}`"
|
|
||||||
:request="request"
|
|
||||||
:collection-index="collectionIndex"
|
|
||||||
:folder-index="-1"
|
|
||||||
:folder-name="collection.name"
|
|
||||||
:folder-path="`${collectionIndex}`"
|
|
||||||
:request-index="index"
|
|
||||||
:save-request="saveRequest"
|
|
||||||
:collections-type="collectionsType"
|
|
||||||
:picked="picked"
|
|
||||||
@edit-request="$emit('edit-request', $event)"
|
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
|
||||||
@select="$emit('select', $event)"
|
|
||||||
@remove-request="$emit('remove-request', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
(collection.folders == undefined ||
|
|
||||||
collection.folders.length === 0) &&
|
|
||||||
(collection.requests == undefined ||
|
|
||||||
collection.requests.length === 0)
|
|
||||||
"
|
|
||||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
|
||||||
>
|
|
||||||
<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.collection')}`"
|
|
||||||
/>
|
|
||||||
<span class="text-center">
|
|
||||||
{{ t("empty.collection") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import IconCircle from "~icons/lucide/circle"
|
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
|
||||||
import IconFilePlus from "~icons/lucide/file-plus"
|
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
|
||||||
import IconDownload from "~icons/lucide/download"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
|
||||||
import IconEdit from "~icons/lucide/edit"
|
|
||||||
import IconFolder from "~icons/lucide/folder"
|
|
||||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
|
||||||
import { useColorMode } from "@composables/theming"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { defineComponent, ref, markRaw } from "vue"
|
|
||||||
import { moveRESTRequest } from "~/newstore/collections"
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
collectionIndex: { type: Number, default: null },
|
|
||||||
collection: { type: Object, default: () => ({}) },
|
|
||||||
isFiltered: Boolean,
|
|
||||||
saveRequest: Boolean,
|
|
||||||
collectionsType: { type: Object, default: () => ({}) },
|
|
||||||
picked: { type: Object, default: () => ({}) },
|
|
||||||
},
|
|
||||||
emits: [
|
|
||||||
"select",
|
|
||||||
"expand-collection",
|
|
||||||
"add-collection",
|
|
||||||
"remove-collection",
|
|
||||||
"add-folder",
|
|
||||||
"add-request",
|
|
||||||
"edit-folder",
|
|
||||||
"edit-request",
|
|
||||||
"duplicate-request",
|
|
||||||
"remove-folder",
|
|
||||||
"remove-request",
|
|
||||||
"select-collection",
|
|
||||||
"unselect-collection",
|
|
||||||
"edit-collection",
|
|
||||||
],
|
|
||||||
setup() {
|
|
||||||
return {
|
|
||||||
colorMode: useColorMode(),
|
|
||||||
toast: useToast(),
|
|
||||||
t: useI18n(),
|
|
||||||
|
|
||||||
// Template refs
|
|
||||||
tippyActions: ref<any | null>(null),
|
|
||||||
options: ref<any | null>(null),
|
|
||||||
requestAction: ref<any | null>(null),
|
|
||||||
folderAction: ref<any | null>(null),
|
|
||||||
edit: ref<any | null>(null),
|
|
||||||
deleteAction: ref<any | null>(null),
|
|
||||||
exportAction: ref<any | null>(null),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
IconCircle: markRaw(IconCircle),
|
|
||||||
IconCheckCircle: markRaw(IconCheckCircle),
|
|
||||||
IconFilePlus: markRaw(IconFilePlus),
|
|
||||||
IconFolderPlus: markRaw(IconFolderPlus),
|
|
||||||
IconMoreVertical: markRaw(IconMoreVertical),
|
|
||||||
IconEdit: markRaw(IconEdit),
|
|
||||||
IconDownload: markRaw(IconDownload),
|
|
||||||
IconTrash2: markRaw(IconTrash2),
|
|
||||||
|
|
||||||
showChildren: false,
|
|
||||||
dragging: false,
|
|
||||||
selectedFolder: {},
|
|
||||||
prevCursor: "",
|
|
||||||
cursor: "",
|
|
||||||
pageNo: 0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isSelected(): boolean {
|
|
||||||
return (
|
|
||||||
this.picked &&
|
|
||||||
this.picked.pickedType === "my-collection" &&
|
|
||||||
this.picked.collectionIndex === this.collectionIndex
|
|
||||||
)
|
|
||||||
},
|
|
||||||
getCollectionIcon() {
|
|
||||||
if (this.isSelected) return IconCheckCircle
|
|
||||||
else if (!this.showChildren && !this.isFiltered) return IconFolder
|
|
||||||
else if (this.showChildren || this.isFiltered) return IconFolderOpen
|
|
||||||
else return IconFolder
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
exportCollection() {
|
|
||||||
const collectionJSON = JSON.stringify(this.collection)
|
|
||||||
|
|
||||||
const file = new Blob([collectionJSON], { type: "application/json" })
|
|
||||||
const a = document.createElement("a")
|
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
a.href = url
|
|
||||||
|
|
||||||
a.download = `${this.collection.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() {
|
|
||||||
if (this.$props.saveRequest)
|
|
||||||
this.$emit("select", {
|
|
||||||
picked: {
|
|
||||||
pickedType: "my-collection",
|
|
||||||
collectionIndex: this.collectionIndex,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$emit("expand-collection", this.collection.id)
|
|
||||||
this.showChildren = !this.showChildren
|
|
||||||
},
|
|
||||||
removeCollection() {
|
|
||||||
this.$emit("remove-collection", {
|
|
||||||
collectionIndex: this.collectionIndex,
|
|
||||||
collectionID: this.collection.id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
dropEvent({ dataTransfer }: any) {
|
|
||||||
this.dragging = !this.dragging
|
|
||||||
const folderPath = dataTransfer.getData("folderPath")
|
|
||||||
const requestIndex = dataTransfer.getData("requestIndex")
|
|
||||||
moveRESTRequest(folderPath, requestIndex, `${this.collectionIndex}`)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
|
||||||
<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="toggleShowChildren()"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="getCollectionIcon"
|
|
||||||
class="svg-icons"
|
|
||||||
:class="{ 'text-accent': isSelected }"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
|
||||||
@click="toggleShowChildren()"
|
|
||||||
>
|
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
|
||||||
{{ folder.name ? folder.name : folder.title }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div class="flex">
|
|
||||||
<ButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
: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')"
|
|
||||||
class="hidden group-hover:inline-flex"
|
|
||||||
@click="$emit('add-folder', { folder, path: folderPath })"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
<tippy
|
|
||||||
ref="options"
|
|
||||||
interactive
|
|
||||||
trigger="click"
|
|
||||||
theme="popover"
|
|
||||||
:on-shown="() => tippyActions.focus()"
|
|
||||||
>
|
|
||||||
<ButtonSecondary
|
|
||||||
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()"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="showChildren || isFiltered" class="flex">
|
|
||||||
<div
|
|
||||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
|
||||||
@click="toggleShowChildren()"
|
|
||||||
></div>
|
|
||||||
<div class="flex flex-col flex-1 truncate">
|
|
||||||
<!-- Referring to this component only (this is recursive) -->
|
|
||||||
<Folder
|
|
||||||
v-for="(subFolder, subFolderIndex) in folder.folders"
|
|
||||||
:key="`subFolder-${subFolderIndex}`"
|
|
||||||
:folder="subFolder"
|
|
||||||
:folder-index="subFolderIndex"
|
|
||||||
:collection-index="collectionIndex"
|
|
||||||
:save-request="saveRequest"
|
|
||||||
:collections-type="collectionsType"
|
|
||||||
:folder-path="`${folderPath}/${subFolderIndex}`"
|
|
||||||
:picked="picked"
|
|
||||||
@add-request="$emit('add-request', $event)"
|
|
||||||
@add-folder="$emit('add-folder', $event)"
|
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
|
||||||
@edit-request="$emit('edit-request', $event)"
|
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
|
||||||
@update-team-collections="$emit('update-team-collections')"
|
|
||||||
@select="$emit('select', $event)"
|
|
||||||
@remove-request="$emit('remove-request', $event)"
|
|
||||||
@remove-folder="$emit('remove-folder', $event)"
|
|
||||||
/>
|
|
||||||
<CollectionsMyRequest
|
|
||||||
v-for="(request, index) in folder.requests"
|
|
||||||
:key="`request-${index}`"
|
|
||||||
:request="request"
|
|
||||||
:collection-index="collectionIndex"
|
|
||||||
:folder-index="folderIndex"
|
|
||||||
:folder-name="folder.name"
|
|
||||||
:folder-path="folderPath"
|
|
||||||
:request-index="index"
|
|
||||||
:picked="picked"
|
|
||||||
:save-request="saveRequest"
|
|
||||||
:collections-type="collectionsType"
|
|
||||||
@edit-request="$emit('edit-request', $event)"
|
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
|
||||||
@select="$emit('select', $event)"
|
|
||||||
@remove-request="$emit('remove-request', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
folder.folders &&
|
|
||||||
folder.folders.length === 0 &&
|
|
||||||
folder.requests &&
|
|
||||||
folder.requests.length === 0
|
|
||||||
"
|
|
||||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
|
||||||
>
|
|
||||||
<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.folder')}`"
|
|
||||||
/>
|
|
||||||
<span class="text-center">
|
|
||||||
{{ t("empty.folder") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import IconFilePlus from "~icons/lucide/file-plus"
|
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
|
||||||
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 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({
|
|
||||||
name: "Folder",
|
|
||||||
props: {
|
|
||||||
folder: { type: Object, default: () => ({}) },
|
|
||||||
folderIndex: { type: Number, default: null },
|
|
||||||
collectionIndex: { type: Number, default: null },
|
|
||||||
folderPath: { type: String, default: null },
|
|
||||||
saveRequest: Boolean,
|
|
||||||
isFiltered: Boolean,
|
|
||||||
collectionsType: { 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() {
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Template refs
|
|
||||||
tippyActions: ref<any | null>(null),
|
|
||||||
options: ref<any | null>(null),
|
|
||||||
requestAction: ref<any | null>(null),
|
|
||||||
folderAction: ref<any | null>(null),
|
|
||||||
edit: ref<any | null>(null),
|
|
||||||
deleteAction: ref<any | null>(null),
|
|
||||||
exportAction: ref<any | null>(null),
|
|
||||||
t,
|
|
||||||
toast: useToast(),
|
|
||||||
colorMode: useColorMode(),
|
|
||||||
IconFilePlus,
|
|
||||||
IconFolderPlus,
|
|
||||||
IconMoreVertical,
|
|
||||||
IconEdit,
|
|
||||||
IconDownload,
|
|
||||||
IconTrash2,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showChildren: false,
|
|
||||||
dragging: false,
|
|
||||||
prevCursor: "",
|
|
||||||
cursor: "",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isSelected(): boolean {
|
|
||||||
return (
|
|
||||||
this.picked &&
|
|
||||||
this.picked.pickedType === "my-folder" &&
|
|
||||||
this.picked.folderPath === this.folderPath
|
|
||||||
)
|
|
||||||
},
|
|
||||||
getCollectionIcon() {
|
|
||||||
if (this.isSelected) return IconCheckCircle
|
|
||||||
else if (!this.showChildren && !this.isFiltered) return IconFolder
|
|
||||||
else if (this.showChildren || this.isFiltered) return IconFolderOpen
|
|
||||||
else return IconFolder
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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() {
|
|
||||||
if (this.$props.saveRequest)
|
|
||||||
this.$emit("select", {
|
|
||||||
picked: {
|
|
||||||
pickedType: "my-folder",
|
|
||||||
collectionIndex: this.collectionIndex,
|
|
||||||
folderName: this.folder.name,
|
|
||||||
folderPath: this.folderPath,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.showChildren = !this.showChildren
|
|
||||||
},
|
|
||||||
removeFolder() {
|
|
||||||
this.$emit("remove-folder", {
|
|
||||||
folder: this.folder,
|
|
||||||
folderPath: this.folderPath,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
dropEvent({ dataTransfer }) {
|
|
||||||
this.dragging = !this.dragging
|
|
||||||
const folderPath = dataTransfer.getData("folderPath")
|
|
||||||
const requestIndex = dataTransfer.getData("requestIndex")
|
|
||||||
moveRESTRequest(folderPath, requestIndex, this.folderPath)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,433 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
|
||||||
<div
|
|
||||||
class="flex items-stretch group"
|
|
||||||
draggable="true"
|
|
||||||
@dragstart="dragStart"
|
|
||||||
@dragover.stop
|
|
||||||
@dragleave="dragging = false"
|
|
||||||
@dragend="dragging = false"
|
|
||||||
@contextmenu.prevent="options.tippy.show()"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
|
|
||||||
:class="getRequestLabelColor(request.method)"
|
|
||||||
@click="selectRequest()"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="IconCheckCircle"
|
|
||||||
v-if="isSelected"
|
|
||||||
class="svg-icons"
|
|
||||||
:class="{ 'text-accent': isSelected }"
|
|
||||||
/>
|
|
||||||
<span v-else class="font-semibold truncate text-tiny">
|
|
||||||
{{ request.method }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
|
||||||
@click="selectRequest()"
|
|
||||||
>
|
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
|
||||||
{{ request.name }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="isActive"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
|
||||||
:title="`${t('collection.request_in_use')}`"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div class="flex">
|
|
||||||
<ButtonSecondary
|
|
||||||
v-if="!saveRequest"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconRotateCCW"
|
|
||||||
:title="t('action.restore')"
|
|
||||||
class="hidden group-hover:inline-flex"
|
|
||||||
@click="selectRequest()"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
<tippy
|
|
||||||
ref="options"
|
|
||||||
interactive
|
|
||||||
trigger="click"
|
|
||||||
theme="popover"
|
|
||||||
:on-shown="() => tippyActions.focus()"
|
|
||||||
>
|
|
||||||
<ButtonSecondary
|
|
||||||
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.e="edit.$el.click()"
|
|
||||||
@keyup.d="duplicate.$el.click()"
|
|
||||||
@keyup.delete="deleteAction.$el.click()"
|
|
||||||
@keyup.escape="hide()"
|
|
||||||
>
|
|
||||||
<SmartItem
|
|
||||||
ref="edit"
|
|
||||||
:icon="IconEdit"
|
|
||||||
:label="t('action.edit')"
|
|
||||||
:shortcut="['E']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
emit('edit-request', {
|
|
||||||
collectionIndex,
|
|
||||||
folderIndex,
|
|
||||||
folderName,
|
|
||||||
request,
|
|
||||||
requestIndex,
|
|
||||||
folderPath,
|
|
||||||
})
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="duplicate"
|
|
||||||
:icon="IconCopy"
|
|
||||||
:label="t('action.duplicate')"
|
|
||||||
:shortcut="['D']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
emit('duplicate-request', {
|
|
||||||
collectionIndex,
|
|
||||||
folderIndex,
|
|
||||||
folderName,
|
|
||||||
request,
|
|
||||||
requestIndex,
|
|
||||||
folderPath,
|
|
||||||
})
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="deleteAction"
|
|
||||||
:icon="IconTrash2"
|
|
||||||
:label="t('action.delete')"
|
|
||||||
:shortcut="['⌫']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
removeRequest()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<HttpReqChangeConfirmModal
|
|
||||||
:show="confirmChange"
|
|
||||||
@hide-modal="confirmChange = false"
|
|
||||||
@save-change="saveRequestChange"
|
|
||||||
@discard-change="discardRequestChange"
|
|
||||||
/>
|
|
||||||
<CollectionsSaveRequest
|
|
||||||
mode="rest"
|
|
||||||
:show="showSaveRequestModal"
|
|
||||||
@hide-modal="showSaveRequestModal = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
|
||||||
import IconEdit from "~icons/lucide/edit"
|
|
||||||
import IconCopy from "~icons/lucide/copy"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
|
||||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
|
||||||
import { ref, computed } from "vue"
|
|
||||||
import {
|
|
||||||
HoppRESTRequest,
|
|
||||||
safelyExtractRESTRequest,
|
|
||||||
translateToNewRequest,
|
|
||||||
isEqualHoppRESTRequest,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import { cloneDeep } from "lodash-es"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
|
||||||
import {
|
|
||||||
getDefaultRESTRequest,
|
|
||||||
getRESTRequest,
|
|
||||||
restSaveContext$,
|
|
||||||
setRESTRequest,
|
|
||||||
setRESTSaveContext,
|
|
||||||
getRESTSaveContext,
|
|
||||||
} from "~/newstore/RESTSession"
|
|
||||||
import { editRESTRequest } from "~/newstore/collections"
|
|
||||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
|
||||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
|
||||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
request: HoppRESTRequest
|
|
||||||
collectionIndex: number
|
|
||||||
folderIndex: number
|
|
||||||
folderName: string
|
|
||||||
requestIndex: number
|
|
||||||
saveRequest: boolean
|
|
||||||
collectionsType: object
|
|
||||||
folderPath: string
|
|
||||||
picked?: {
|
|
||||||
pickedType: string
|
|
||||||
collectionIndex: number
|
|
||||||
folderPath: string
|
|
||||||
folderName: string
|
|
||||||
requestIndex: number
|
|
||||||
}
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(
|
|
||||||
e: "select",
|
|
||||||
data:
|
|
||||||
| {
|
|
||||||
picked: {
|
|
||||||
pickedType: string
|
|
||||||
collectionIndex: number
|
|
||||||
folderPath: string
|
|
||||||
folderName: string
|
|
||||||
requestIndex: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
): void
|
|
||||||
|
|
||||||
(
|
|
||||||
e: "remove-request",
|
|
||||||
data: {
|
|
||||||
folderPath: string
|
|
||||||
requestIndex: number
|
|
||||||
}
|
|
||||||
): void
|
|
||||||
|
|
||||||
(
|
|
||||||
e: "duplicate-request",
|
|
||||||
data: {
|
|
||||||
collectionIndex: number
|
|
||||||
folderIndex: number
|
|
||||||
folderName: string
|
|
||||||
request: HoppRESTRequest
|
|
||||||
folderPath: string
|
|
||||||
requestIndex: number
|
|
||||||
}
|
|
||||||
): void
|
|
||||||
|
|
||||||
(
|
|
||||||
e: "edit-request",
|
|
||||||
data: {
|
|
||||||
collectionIndex: number
|
|
||||||
folderIndex: number
|
|
||||||
folderName: string
|
|
||||||
request: HoppRESTRequest
|
|
||||||
folderPath: string
|
|
||||||
requestIndex: number
|
|
||||||
}
|
|
||||||
): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const dragging = ref(false)
|
|
||||||
const requestMethodLabels = {
|
|
||||||
get: "text-green-500",
|
|
||||||
post: "text-yellow-500",
|
|
||||||
put: "text-blue-500",
|
|
||||||
delete: "text-red-500",
|
|
||||||
default: "text-gray-500",
|
|
||||||
}
|
|
||||||
const confirmChange = ref(false)
|
|
||||||
const showSaveRequestModal = ref(false)
|
|
||||||
|
|
||||||
// Template refs
|
|
||||||
const tippyActions = ref<any | null>(null)
|
|
||||||
const options = ref<any | null>(null)
|
|
||||||
const edit = ref<any | null>(null)
|
|
||||||
const duplicate = ref<any | null>(null)
|
|
||||||
const deleteAction = ref<any | null>(null)
|
|
||||||
|
|
||||||
const active = useReadonlyStream(restSaveContext$, null)
|
|
||||||
|
|
||||||
const isSelected = computed(
|
|
||||||
() =>
|
|
||||||
props.picked &&
|
|
||||||
props.picked.pickedType === "my-request" &&
|
|
||||||
props.picked.folderPath === props.folderPath &&
|
|
||||||
props.picked.requestIndex === props.requestIndex
|
|
||||||
)
|
|
||||||
|
|
||||||
const isActive = computed(
|
|
||||||
() =>
|
|
||||||
active.value &&
|
|
||||||
active.value.originLocation === "user-collection" &&
|
|
||||||
active.value.folderPath === props.folderPath &&
|
|
||||||
active.value.requestIndex === props.requestIndex
|
|
||||||
)
|
|
||||||
|
|
||||||
const dragStart = ({ dataTransfer }: DragEvent) => {
|
|
||||||
if (dataTransfer) {
|
|
||||||
dragging.value = !dragging.value
|
|
||||||
dataTransfer.setData("folderPath", props.folderPath)
|
|
||||||
dataTransfer.setData("requestIndex", props.requestIndex.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeRequest = () => {
|
|
||||||
emit("remove-request", {
|
|
||||||
folderPath: props.folderPath,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRequestLabelColor = (method: string) =>
|
|
||||||
requestMethodLabels[
|
|
||||||
method.toLowerCase() as keyof typeof requestMethodLabels
|
|
||||||
] || requestMethodLabels.default
|
|
||||||
|
|
||||||
const setRestReq = (request: any) => {
|
|
||||||
setRESTRequest(
|
|
||||||
cloneDeep(
|
|
||||||
safelyExtractRESTRequest(
|
|
||||||
translateToNewRequest(request),
|
|
||||||
getDefaultRESTRequest()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
{
|
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: props.folderPath,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
req: cloneDeep(request),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Loads request from the save once, checks for unsaved changes, but ignores default values */
|
|
||||||
const selectRequest = () => {
|
|
||||||
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
|
|
||||||
if (props.saveRequest) {
|
|
||||||
emit("select", {
|
|
||||||
picked: {
|
|
||||||
pickedType: "my-request",
|
|
||||||
collectionIndex: props.collectionIndex,
|
|
||||||
folderPath: props.folderPath,
|
|
||||||
folderName: props.folderName,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
|
|
||||||
confirmChange.value = false
|
|
||||||
setRestReq(props.request)
|
|
||||||
} else if (!active.value) {
|
|
||||||
// If the current request is the same as the request to be loaded in, there is no data loss
|
|
||||||
const currentReq = getRESTRequest()
|
|
||||||
|
|
||||||
if (isEqualHoppRESTRequest(currentReq, props.request)) {
|
|
||||||
setRestReq(props.request)
|
|
||||||
} else {
|
|
||||||
confirmChange.value = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const currentReqWithNoChange = active.value.req
|
|
||||||
const currentFullReq = getRESTRequest()
|
|
||||||
|
|
||||||
// Check if whether user clicked the same request or not
|
|
||||||
if (!isActive.value && currentReqWithNoChange !== undefined) {
|
|
||||||
// Check if there is any changes done on the current request
|
|
||||||
if (isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)) {
|
|
||||||
setRestReq(props.request)
|
|
||||||
} else {
|
|
||||||
confirmChange.value = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setRESTSaveContext(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Save current request to the collection */
|
|
||||||
const saveRequestChange = () => {
|
|
||||||
const saveCtx = getRESTSaveContext()
|
|
||||||
saveCurrentRequest(saveCtx)
|
|
||||||
confirmChange.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Discard changes and change the current request and context */
|
|
||||||
const discardRequestChange = () => {
|
|
||||||
setRestReq(props.request)
|
|
||||||
if (!isActive.value) {
|
|
||||||
setRESTSaveContext({
|
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: props.folderPath,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
req: cloneDeep(props.request),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmChange.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveCurrentRequest = (saveCtx: HoppRequestSaveContext | null) => {
|
|
||||||
if (!saveCtx) {
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (saveCtx.originLocation === "user-collection") {
|
|
||||||
try {
|
|
||||||
editRESTRequest(
|
|
||||||
saveCtx.folderPath,
|
|
||||||
saveCtx.requestIndex,
|
|
||||||
getRESTRequest()
|
|
||||||
)
|
|
||||||
setRestReq(props.request)
|
|
||||||
toast.success(`${t("request.saved")}`)
|
|
||||||
} catch (e) {
|
|
||||||
setRESTSaveContext(null)
|
|
||||||
saveCurrentRequest(saveCtx)
|
|
||||||
}
|
|
||||||
} else if (saveCtx.originLocation === "team-collection") {
|
|
||||||
const req = getRESTRequest()
|
|
||||||
try {
|
|
||||||
runMutation(UpdateRequestDocument, {
|
|
||||||
requestID: saveCtx.requestID,
|
|
||||||
data: {
|
|
||||||
title: req.name,
|
|
||||||
request: JSON.stringify(req),
|
|
||||||
},
|
|
||||||
})().then((result) => {
|
|
||||||
if (E.isLeft(result)) {
|
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
|
||||||
} else {
|
|
||||||
toast.success(`${t("request.saved")}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setRestReq(props.request)
|
|
||||||
} catch (error) {
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
toast.error(`${t("error.something_went_wrong")}`)
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,408 +0,0 @@
|
|||||||
<template>
|
|
||||||
<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="toggleShowChildren()"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="getCollectionIcon"
|
|
||||||
class="svg-icons"
|
|
||||||
:class="{ 'text-accent': isSelected }"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
|
||||||
@click="toggleShowChildren()"
|
|
||||||
>
|
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
|
||||||
{{ collection.title }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div class="flex">
|
|
||||||
<ButtonSecondary
|
|
||||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconFilePlus"
|
|
||||||
:title="t('request.new')"
|
|
||||||
class="hidden group-hover:inline-flex"
|
|
||||||
@click="
|
|
||||||
$emit('add-request', {
|
|
||||||
folder: collection,
|
|
||||||
path: `${collectionIndex}`,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<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: collection,
|
|
||||||
path: `${collectionIndex}`,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
<tippy
|
|
||||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
|
||||||
ref="options"
|
|
||||||
interactive
|
|
||||||
trigger="click"
|
|
||||||
theme="popover"
|
|
||||||
:on-shown="() => tippyActions!.focus()"
|
|
||||||
>
|
|
||||||
<ButtonSecondary
|
|
||||||
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()"
|
|
||||||
>
|
|
||||||
<SmartItem
|
|
||||||
ref="requestAction"
|
|
||||||
:icon="IconFilePlus"
|
|
||||||
:label="t('request.new')"
|
|
||||||
:shortcut="['R']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
$emit('add-request', {
|
|
||||||
folder: collection,
|
|
||||||
path: `${collectionIndex}`,
|
|
||||||
})
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="folderAction"
|
|
||||||
:icon="IconFolderPlus"
|
|
||||||
:label="t('folder.new')"
|
|
||||||
:shortcut="['N']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
$emit('add-folder', {
|
|
||||||
folder: collection,
|
|
||||||
path: `${collectionIndex}`,
|
|
||||||
})
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="edit"
|
|
||||||
:icon="IconEdit"
|
|
||||||
:label="t('action.edit')"
|
|
||||||
:shortcut="['E']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
$emit('edit-collection')
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="exportAction"
|
|
||||||
:icon="IconDownload"
|
|
||||||
:label="t('export.title')"
|
|
||||||
:shortcut="['X']"
|
|
||||||
:loading="exportLoading"
|
|
||||||
@click="exportCollection"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="deleteAction"
|
|
||||||
:icon="IconTrash2"
|
|
||||||
:label="t('action.delete')"
|
|
||||||
:shortcut="['⌫']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
removeCollection()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="showChildren || isFiltered" class="flex">
|
|
||||||
<div
|
|
||||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
|
||||||
@click="toggleShowChildren()"
|
|
||||||
></div>
|
|
||||||
<div class="flex flex-col flex-1 truncate">
|
|
||||||
<CollectionsTeamsFolder
|
|
||||||
v-for="(folder, index) in collection.children"
|
|
||||||
:key="`folder-${index}`"
|
|
||||||
:folder="folder"
|
|
||||||
:folder-index="index"
|
|
||||||
:folder-path="`${collectionIndex}/${index}`"
|
|
||||||
:collection-index="collectionIndex"
|
|
||||||
:save-request="saveRequest"
|
|
||||||
:collections-type="collectionsType"
|
|
||||||
:is-filtered="isFiltered"
|
|
||||||
:picked="picked"
|
|
||||||
:loading-collection-i-ds="loadingCollectionIDs"
|
|
||||||
@add-request="$emit('add-request', $event)"
|
|
||||||
@add-folder="$emit('add-folder', $event)"
|
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
|
||||||
@edit-request="$emit('edit-request', $event)"
|
|
||||||
@select="$emit('select', $event)"
|
|
||||||
@expand-collection="expandCollection"
|
|
||||||
@remove-request="$emit('remove-request', $event)"
|
|
||||||
@remove-folder="$emit('remove-folder', $event)"
|
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
|
||||||
/>
|
|
||||||
<CollectionsTeamsRequest
|
|
||||||
v-for="(request, index) in collection.requests"
|
|
||||||
:key="`request-${index}`"
|
|
||||||
:request="request.request"
|
|
||||||
:collection-index="collectionIndex"
|
|
||||||
:folder-index="-1"
|
|
||||||
:folder-name="collection.name"
|
|
||||||
:request-index="request.id"
|
|
||||||
:save-request="saveRequest"
|
|
||||||
:collection-i-d="collection.id"
|
|
||||||
:collections-type="collectionsType"
|
|
||||||
:picked="picked"
|
|
||||||
@edit-request="editRequest($event)"
|
|
||||||
@select="$emit('select', $event)"
|
|
||||||
@remove-request="$emit('remove-request', $event)"
|
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="loadingCollectionIDs.includes(collection.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="
|
|
||||||
(collection.children == undefined ||
|
|
||||||
collection.children.length === 0) &&
|
|
||||||
(collection.requests == undefined ||
|
|
||||||
collection.requests.length === 0)
|
|
||||||
"
|
|
||||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
|
||||||
>
|
|
||||||
<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.collection')}`"
|
|
||||||
/>
|
|
||||||
<span class="text-center">
|
|
||||||
{{ t("empty.collection") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
|
||||||
import IconDownload from "~icons/lucide/download"
|
|
||||||
import IconEdit from "~icons/lucide/edit"
|
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
|
||||||
import IconFilePlus from "~icons/lucide/file-plus"
|
|
||||||
import IconCircle from "~icons/lucide/circle"
|
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
|
||||||
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 {
|
|
||||||
getCompleteCollectionTree,
|
|
||||||
teamCollToHoppRESTColl,
|
|
||||||
} from "~/helpers/backend/helpers"
|
|
||||||
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
|
|
||||||
import { useColorMode } from "@composables/theming"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { TippyComponent } from "vue-tippy"
|
|
||||||
import SmartItem from "@components/smart/Item.vue"
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
collectionIndex: { type: Number, default: null },
|
|
||||||
collection: { type: Object, default: () => ({}) },
|
|
||||||
isFiltered: Boolean,
|
|
||||||
saveRequest: Boolean,
|
|
||||||
collectionsType: { type: Object, default: () => ({}) },
|
|
||||||
picked: { type: Object, default: () => ({}) },
|
|
||||||
loadingCollectionIDs: { type: Array, default: () => [] },
|
|
||||||
},
|
|
||||||
emits: [
|
|
||||||
"edit-collection",
|
|
||||||
"add-request",
|
|
||||||
"add-folder",
|
|
||||||
"edit-folder",
|
|
||||||
"edit-request",
|
|
||||||
"remove-folder",
|
|
||||||
"select",
|
|
||||||
"remove-request",
|
|
||||||
"duplicate-request",
|
|
||||||
"expand-collection",
|
|
||||||
"remove-collection",
|
|
||||||
],
|
|
||||||
setup() {
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Template refs
|
|
||||||
tippyActions: ref<TippyComponent | null>(null),
|
|
||||||
options: ref<TippyComponent | null>(null),
|
|
||||||
requestAction: ref<typeof SmartItem | null>(null),
|
|
||||||
folderAction: ref<typeof SmartItem | null>(null),
|
|
||||||
edit: ref<typeof SmartItem | null>(null),
|
|
||||||
deleteAction: ref<typeof SmartItem | null>(null),
|
|
||||||
exportAction: ref<typeof SmartItem | null>(null),
|
|
||||||
exportLoading: ref<boolean>(false),
|
|
||||||
t,
|
|
||||||
toast: useToast(),
|
|
||||||
colorMode: useColorMode(),
|
|
||||||
|
|
||||||
IconCheckCircle,
|
|
||||||
IconCircle,
|
|
||||||
IconFilePlus,
|
|
||||||
IconFolderPlus,
|
|
||||||
IconEdit,
|
|
||||||
IconDownload,
|
|
||||||
IconTrash2,
|
|
||||||
IconMoreVertical,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showChildren: false,
|
|
||||||
dragging: false,
|
|
||||||
selectedFolder: {},
|
|
||||||
prevCursor: "",
|
|
||||||
cursor: "",
|
|
||||||
pageNo: 0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isSelected(): boolean {
|
|
||||||
return (
|
|
||||||
this.picked &&
|
|
||||||
this.picked.pickedType === "teams-collection" &&
|
|
||||||
this.picked.collectionID === this.collection.id
|
|
||||||
)
|
|
||||||
},
|
|
||||||
getCollectionIcon() {
|
|
||||||
if (this.isSelected) return IconCheckCircle
|
|
||||||
else if (!this.showChildren && !this.isFiltered) return IconFolder
|
|
||||||
else if (this.showChildren || this.isFiltered) return IconFolderOpen
|
|
||||||
else return IconFolder
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async exportCollection() {
|
|
||||||
this.exportLoading = true
|
|
||||||
|
|
||||||
const result = await getCompleteCollectionTree(this.collection.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()
|
|
||||||
},
|
|
||||||
editRequest(event: any) {
|
|
||||||
this.$emit("edit-request", event)
|
|
||||||
if (this.$props.saveRequest)
|
|
||||||
this.$emit("select", {
|
|
||||||
picked: {
|
|
||||||
pickedType: "teams-collection",
|
|
||||||
collectionID: this.collection.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
toggleShowChildren() {
|
|
||||||
if (this.$props.saveRequest)
|
|
||||||
this.$emit("select", {
|
|
||||||
picked: {
|
|
||||||
pickedType: "teams-collection",
|
|
||||||
collectionID: this.collection.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$emit("expand-collection", this.collection.id)
|
|
||||||
this.showChildren = !this.showChildren
|
|
||||||
},
|
|
||||||
removeCollection() {
|
|
||||||
this.$emit("remove-collection", {
|
|
||||||
collectionIndex: this.collectionIndex,
|
|
||||||
collectionID: this.collection.id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
expandCollection(collectionID: string) {
|
|
||||||
this.$emit("expand-collection", collectionID)
|
|
||||||
},
|
|
||||||
async dropEvent({ dataTransfer }: any) {
|
|
||||||
this.dragging = !this.dragging
|
|
||||||
const requestIndex = dataTransfer.getData("requestIndex")
|
|
||||||
const moveRequestResult = await moveRESTTeamRequest(
|
|
||||||
requestIndex,
|
|
||||||
this.collection.id
|
|
||||||
)()
|
|
||||||
if (E.isLeft(moveRequestResult))
|
|
||||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
|
||||||
<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="toggleShowChildren()"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="getCollectionIcon"
|
|
||||||
class="svg-icons"
|
|
||||||
:class="{ 'text-accent': isSelected }"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
|
||||||
@click="toggleShowChildren()"
|
|
||||||
>
|
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
|
||||||
{{ folder.name ? folder.name : folder.title }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div class="flex">
|
|
||||||
<ButtonSecondary
|
|
||||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconFilePlus"
|
|
||||||
:title="t('request.new')"
|
|
||||||
class="hidden group-hover:inline-flex"
|
|
||||||
@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>
|
|
||||||
<tippy
|
|
||||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
|
||||||
ref="options"
|
|
||||||
interactive
|
|
||||||
trigger="click"
|
|
||||||
theme="popover"
|
|
||||||
:on-shown="() => tippyActions!.focus()"
|
|
||||||
>
|
|
||||||
<ButtonSecondary
|
|
||||||
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()"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="showChildren || isFiltered" class="flex">
|
|
||||||
<div
|
|
||||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
|
||||||
@click="toggleShowChildren()"
|
|
||||||
></div>
|
|
||||||
<div class="flex flex-col flex-1 truncate">
|
|
||||||
<!-- Referring to this component only (this is recursive) -->
|
|
||||||
<Folder
|
|
||||||
v-for="(subFolder, subFolderIndex) in folder.children"
|
|
||||||
:key="`subFolder-${subFolderIndex}`"
|
|
||||||
:folder="subFolder"
|
|
||||||
:folder-index="subFolderIndex"
|
|
||||||
:collection-index="collectionIndex"
|
|
||||||
:save-request="saveRequest"
|
|
||||||
:collections-type="collectionsType"
|
|
||||||
:folder-path="`${folderPath}/${subFolderIndex}`"
|
|
||||||
:picked="picked"
|
|
||||||
:loading-collection-i-ds="loadingCollectionIDs"
|
|
||||||
@add-request="$emit('add-request', $event)"
|
|
||||||
@add-folder="$emit('add-folder', $event)"
|
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
|
||||||
@edit-request="$emit('edit-request', $event)"
|
|
||||||
@update-team-collections="$emit('update-team-collections')"
|
|
||||||
@select="$emit('select', $event)"
|
|
||||||
@expand-collection="expandCollection"
|
|
||||||
@remove-request="$emit('remove-request', $event)"
|
|
||||||
@remove-folder="$emit('remove-folder', $event)"
|
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
|
||||||
/>
|
|
||||||
<CollectionsTeamsRequest
|
|
||||||
v-for="(request, index) in folder.requests"
|
|
||||||
:key="`request-${index}`"
|
|
||||||
:request="request.request"
|
|
||||||
:collection-index="collectionIndex"
|
|
||||||
:folder-index="folderIndex"
|
|
||||||
:folder-name="folder.name"
|
|
||||||
:request-index="request.id"
|
|
||||||
:save-request="saveRequest"
|
|
||||||
:collections-type="collectionsType"
|
|
||||||
:picked="picked"
|
|
||||||
:collection-i-d="folder.id"
|
|
||||||
@edit-request="$emit('edit-request', $event)"
|
|
||||||
@select="$emit('select', $event)"
|
|
||||||
@remove-request="$emit('remove-request', $event)"
|
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
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.requests == undefined || folder.requests.length === 0)
|
|
||||||
"
|
|
||||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
|
||||||
>
|
|
||||||
<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.folder')}`"
|
|
||||||
/>
|
|
||||||
<span class="text-center">
|
|
||||||
{{ t("empty.folder") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
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 {
|
|
||||||
getCompleteCollectionTree,
|
|
||||||
teamCollToHoppRESTColl,
|
|
||||||
} from "~/helpers/backend/helpers"
|
|
||||||
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { useColorMode } from "@composables/theming"
|
|
||||||
import { TippyComponent } from "vue-tippy"
|
|
||||||
import SmartItem from "@components/smart/Item.vue"
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Folder",
|
|
||||||
props: {
|
|
||||||
folder: { type: Object, default: () => ({}) },
|
|
||||||
folderIndex: { type: Number, default: null },
|
|
||||||
collectionIndex: { type: Number, default: null },
|
|
||||||
folderPath: { type: String, default: null },
|
|
||||||
saveRequest: Boolean,
|
|
||||||
isFiltered: Boolean,
|
|
||||||
collectionsType: { 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() {
|
|
||||||
return {
|
|
||||||
// Template refs
|
|
||||||
tippyActions: ref<TippyComponent | null>(null),
|
|
||||||
options: ref<TippyComponent | null>(null),
|
|
||||||
requestAction: ref<typeof SmartItem | null>(null),
|
|
||||||
folderAction: ref<typeof SmartItem | null>(null),
|
|
||||||
edit: ref<typeof SmartItem | null>(null),
|
|
||||||
deleteAction: ref<typeof SmartItem | null>(null),
|
|
||||||
exportAction: ref<typeof SmartItem | null>(null),
|
|
||||||
exportLoading: ref<boolean>(false),
|
|
||||||
toast: useToast(),
|
|
||||||
t: useI18n(),
|
|
||||||
colorMode: useColorMode(),
|
|
||||||
IconFilePlus,
|
|
||||||
IconFolderPlus,
|
|
||||||
IconCheckCircle,
|
|
||||||
IconFolder,
|
|
||||||
IconFolderOpen,
|
|
||||||
IconMoreVertical,
|
|
||||||
IconEdit,
|
|
||||||
IconDownload,
|
|
||||||
IconTrash2,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showChildren: false,
|
|
||||||
dragging: false,
|
|
||||||
prevCursor: "",
|
|
||||||
cursor: "",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isSelected(): boolean {
|
|
||||||
return (
|
|
||||||
this.picked &&
|
|
||||||
this.picked.pickedType === "teams-folder" &&
|
|
||||||
this.picked.folderID === this.folder.id
|
|
||||||
)
|
|
||||||
},
|
|
||||||
getCollectionIcon() {
|
|
||||||
if (this.isSelected) return IconCheckCircle
|
|
||||||
else if (!this.showChildren && !this.isFiltered) return IconFolder
|
|
||||||
else if (this.showChildren || this.isFiltered) return IconFolderOpen
|
|
||||||
else return IconFolder
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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() {
|
|
||||||
if (this.$props.saveRequest)
|
|
||||||
this.$emit("select", {
|
|
||||||
picked: {
|
|
||||||
pickedType: "teams-folder",
|
|
||||||
folderID: this.folder.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$emit("expand-collection", this.$props.folder.id)
|
|
||||||
this.showChildren = !this.showChildren
|
|
||||||
},
|
|
||||||
removeFolder() {
|
|
||||||
this.$emit("remove-folder", {
|
|
||||||
collectionsType: this.collectionsType,
|
|
||||||
folder: this.folder,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
expandCollection(collectionID: number) {
|
|
||||||
this.$emit("expand-collection", collectionID)
|
|
||||||
},
|
|
||||||
async dropEvent({ dataTransfer }: any) {
|
|
||||||
this.dragging = !this.dragging
|
|
||||||
const requestIndex = dataTransfer.getData("requestIndex")
|
|
||||||
const moveRequestResult = await moveRESTTeamRequest(
|
|
||||||
requestIndex,
|
|
||||||
this.folder.id
|
|
||||||
)()
|
|
||||||
if (E.isLeft(moveRequestResult))
|
|
||||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,405 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
|
||||||
<div
|
|
||||||
class="flex items-stretch group"
|
|
||||||
draggable="true"
|
|
||||||
@dragstart="dragStart"
|
|
||||||
@dragover.stop
|
|
||||||
@dragleave="dragging = false"
|
|
||||||
@dragend="dragging = false"
|
|
||||||
@contextmenu.prevent="options.tippy.show()"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
|
|
||||||
:class="getRequestLabelColor(request.method)"
|
|
||||||
@click="selectRequest()"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="IconCheckCircle"
|
|
||||||
v-if="isSelected"
|
|
||||||
class="svg-icons"
|
|
||||||
:class="{ 'text-accent': isSelected }"
|
|
||||||
/>
|
|
||||||
<span v-else class="font-semibold truncate text-tiny">
|
|
||||||
{{ request.method }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
|
||||||
@click="selectRequest()"
|
|
||||||
>
|
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
|
||||||
{{ request.name }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="isActive"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
|
||||||
:title="`${t('collection.request_in_use')}`"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div class="flex">
|
|
||||||
<ButtonSecondary
|
|
||||||
v-if="!saveRequest"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconRotateCCW"
|
|
||||||
:title="t('action.restore')"
|
|
||||||
class="hidden group-hover:inline-flex"
|
|
||||||
@click="selectRequest()"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
<tippy
|
|
||||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
|
||||||
ref="options"
|
|
||||||
interactive
|
|
||||||
trigger="click"
|
|
||||||
theme="popover"
|
|
||||||
:on-shown="() => tippyActions.focus()"
|
|
||||||
>
|
|
||||||
<ButtonSecondary
|
|
||||||
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.e="edit.$el.click()"
|
|
||||||
@keyup.d="duplicate.$el.click()"
|
|
||||||
@keyup.delete="deleteAction.$el.click()"
|
|
||||||
@keyup.escape="hide()"
|
|
||||||
>
|
|
||||||
<SmartItem
|
|
||||||
ref="edit"
|
|
||||||
:icon="IconEdit"
|
|
||||||
:label="t('action.edit')"
|
|
||||||
:shortcut="['E']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
emit('edit-request', {
|
|
||||||
collectionIndex,
|
|
||||||
folderIndex,
|
|
||||||
folderName,
|
|
||||||
request,
|
|
||||||
requestIndex,
|
|
||||||
})
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="duplicate"
|
|
||||||
:icon="IconCopy"
|
|
||||||
:label="t('action.duplicate')"
|
|
||||||
:shortcut="['D']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
emit('duplicate-request', {
|
|
||||||
request,
|
|
||||||
requestIndex,
|
|
||||||
collectionID,
|
|
||||||
})
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<SmartItem
|
|
||||||
ref="deleteAction"
|
|
||||||
:icon="IconTrash2"
|
|
||||||
:label="t('action.delete')"
|
|
||||||
:shortcut="['⌫']"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
removeRequest()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<HttpReqChangeConfirmModal
|
|
||||||
:show="confirmChange"
|
|
||||||
@hide-modal="confirmChange = false"
|
|
||||||
@save-change="saveRequestChange"
|
|
||||||
@discard-change="discardRequestChange"
|
|
||||||
/>
|
|
||||||
<CollectionsSaveRequest
|
|
||||||
mode="rest"
|
|
||||||
:show="showSaveRequestModal"
|
|
||||||
@hide-modal="showSaveRequestModal = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
|
||||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
|
||||||
import IconEdit from "~icons/lucide/edit"
|
|
||||||
import IconCopy from "~icons/lucide/copy"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
|
||||||
import { ref, computed } from "vue"
|
|
||||||
import {
|
|
||||||
HoppRESTRequest,
|
|
||||||
isEqualHoppRESTRequest,
|
|
||||||
safelyExtractRESTRequest,
|
|
||||||
translateToNewRequest,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
|
||||||
import {
|
|
||||||
getDefaultRESTRequest,
|
|
||||||
restSaveContext$,
|
|
||||||
setRESTRequest,
|
|
||||||
setRESTSaveContext,
|
|
||||||
getRESTSaveContext,
|
|
||||||
getRESTRequest,
|
|
||||||
} from "~/newstore/RESTSession"
|
|
||||||
import { editRESTRequest } from "~/newstore/collections"
|
|
||||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
|
||||||
import { Team, UpdateRequestDocument } from "~/helpers/backend/graphql"
|
|
||||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
request: HoppRESTRequest
|
|
||||||
collectionIndex: number
|
|
||||||
folderIndex: number
|
|
||||||
folderName?: string
|
|
||||||
requestIndex: string
|
|
||||||
saveRequest: boolean
|
|
||||||
collectionsType: {
|
|
||||||
type: "my-collections" | "team-collections"
|
|
||||||
selectedTeam: Team | undefined
|
|
||||||
}
|
|
||||||
collectionID: string
|
|
||||||
picked?: {
|
|
||||||
pickedType: string
|
|
||||||
requestID: string
|
|
||||||
}
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(
|
|
||||||
e: "select",
|
|
||||||
data:
|
|
||||||
| {
|
|
||||||
picked: {
|
|
||||||
pickedType: string
|
|
||||||
requestID: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
): void
|
|
||||||
|
|
||||||
(
|
|
||||||
e: "remove-request",
|
|
||||||
data: {
|
|
||||||
folderPath: string | undefined
|
|
||||||
requestIndex: string
|
|
||||||
}
|
|
||||||
): void
|
|
||||||
|
|
||||||
(
|
|
||||||
e: "edit-request",
|
|
||||||
data: {
|
|
||||||
collectionIndex: number
|
|
||||||
folderIndex: number
|
|
||||||
folderName: string | undefined
|
|
||||||
requestIndex: string
|
|
||||||
request: HoppRESTRequest
|
|
||||||
}
|
|
||||||
): void
|
|
||||||
|
|
||||||
(
|
|
||||||
e: "duplicate-request",
|
|
||||||
data: {
|
|
||||||
collectionID: number | string
|
|
||||||
requestIndex: string
|
|
||||||
request: HoppRESTRequest
|
|
||||||
}
|
|
||||||
): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const dragging = ref(false)
|
|
||||||
const requestMethodLabels = {
|
|
||||||
get: "text-green-500",
|
|
||||||
post: "text-yellow-500",
|
|
||||||
put: "text-blue-500",
|
|
||||||
delete: "text-red-500",
|
|
||||||
default: "text-gray-500",
|
|
||||||
}
|
|
||||||
const confirmChange = ref(false)
|
|
||||||
const showSaveRequestModal = ref(false)
|
|
||||||
|
|
||||||
// Template refs
|
|
||||||
const tippyActions = ref<any | null>(null)
|
|
||||||
const options = ref<any | null>(null)
|
|
||||||
const edit = ref<any | null>(null)
|
|
||||||
const duplicate = ref<any | null>(null)
|
|
||||||
const deleteAction = ref<any | null>(null)
|
|
||||||
|
|
||||||
const active = useReadonlyStream(restSaveContext$, null)
|
|
||||||
|
|
||||||
const isSelected = computed(
|
|
||||||
() =>
|
|
||||||
props.picked &&
|
|
||||||
props.picked.pickedType === "teams-request" &&
|
|
||||||
props.picked.requestID === props.requestIndex
|
|
||||||
)
|
|
||||||
|
|
||||||
const isActive = computed(
|
|
||||||
() =>
|
|
||||||
active.value &&
|
|
||||||
active.value.originLocation === "team-collection" &&
|
|
||||||
active.value.requestID === props.requestIndex
|
|
||||||
)
|
|
||||||
|
|
||||||
const dragStart = ({ dataTransfer }: DragEvent) => {
|
|
||||||
if (dataTransfer) {
|
|
||||||
dragging.value = !dragging.value
|
|
||||||
dataTransfer.setData("requestIndex", props.requestIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeRequest = () => {
|
|
||||||
emit("remove-request", {
|
|
||||||
folderPath: props.folderName,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRequestLabelColor = (method: string): string => {
|
|
||||||
return (
|
|
||||||
(requestMethodLabels as any)[method.toLowerCase()] ||
|
|
||||||
requestMethodLabels.default
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setRestReq = (request: HoppRESTRequest) => {
|
|
||||||
setRESTRequest(
|
|
||||||
safelyExtractRESTRequest(
|
|
||||||
translateToNewRequest(request),
|
|
||||||
getDefaultRESTRequest()
|
|
||||||
),
|
|
||||||
{
|
|
||||||
originLocation: "team-collection",
|
|
||||||
requestID: props.requestIndex,
|
|
||||||
req: request,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectRequest = () => {
|
|
||||||
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
|
|
||||||
if (props.saveRequest) {
|
|
||||||
emit("select", {
|
|
||||||
picked: {
|
|
||||||
pickedType: "teams-request",
|
|
||||||
requestID: props.requestIndex,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
|
|
||||||
confirmChange.value = false
|
|
||||||
setRestReq(props.request)
|
|
||||||
} else if (!active.value) {
|
|
||||||
confirmChange.value = true
|
|
||||||
} else {
|
|
||||||
const currentReqWithNoChange = active.value.req
|
|
||||||
const currentFullReq = getRESTRequest()
|
|
||||||
|
|
||||||
// Check if whether user clicked the same request or not
|
|
||||||
if (!isActive.value && currentReqWithNoChange) {
|
|
||||||
// Check if there is any changes done on the current request
|
|
||||||
if (isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)) {
|
|
||||||
setRestReq(props.request)
|
|
||||||
} else {
|
|
||||||
confirmChange.value = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setRESTSaveContext(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Save current request to the collection */
|
|
||||||
const saveRequestChange = () => {
|
|
||||||
const saveCtx = getRESTSaveContext()
|
|
||||||
saveCurrentRequest(saveCtx)
|
|
||||||
confirmChange.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Discard changes and change the current request and context */
|
|
||||||
const discardRequestChange = () => {
|
|
||||||
setRestReq(props.request)
|
|
||||||
if (!isActive.value) {
|
|
||||||
setRESTSaveContext({
|
|
||||||
originLocation: "team-collection",
|
|
||||||
requestID: props.requestIndex,
|
|
||||||
req: props.request,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
confirmChange.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveCurrentRequest = (saveCtx: HoppRequestSaveContext | null) => {
|
|
||||||
if (!saveCtx) {
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (saveCtx.originLocation === "team-collection") {
|
|
||||||
const req = getRESTRequest()
|
|
||||||
try {
|
|
||||||
runMutation(UpdateRequestDocument, {
|
|
||||||
requestID: saveCtx.requestID,
|
|
||||||
data: {
|
|
||||||
title: req.name,
|
|
||||||
request: JSON.stringify(req),
|
|
||||||
},
|
|
||||||
})().then((result) => {
|
|
||||||
if (E.isLeft(result)) {
|
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
|
||||||
} else {
|
|
||||||
toast.success(`${t("request.saved")}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setRestReq(props.request)
|
|
||||||
} catch (error) {
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
toast.error(`${t("error.something_went_wrong")}`)
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
} else if (saveCtx.originLocation === "user-collection") {
|
|
||||||
try {
|
|
||||||
editRESTRequest(
|
|
||||||
saveCtx.folderPath,
|
|
||||||
saveCtx.requestIndex,
|
|
||||||
getRESTRequest()
|
|
||||||
)
|
|
||||||
setRestReq(props.request)
|
|
||||||
toast.success(`${t("request.saved")}`)
|
|
||||||
} catch (e) {
|
|
||||||
setRESTSaveContext(null)
|
|
||||||
saveCurrentRequest(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -331,14 +331,25 @@ const setRestReq = (request: HoppRESTRequest | null | undefined) => {
|
|||||||
// (That is not a really good behaviour tho ¯\_(ツ)_/¯)
|
// (That is not a really good behaviour tho ¯\_(ツ)_/¯)
|
||||||
const useHistory = (entry: RESTHistoryEntry) => {
|
const useHistory = (entry: RESTHistoryEntry) => {
|
||||||
const currentFullReq = getRESTRequest()
|
const currentFullReq = getRESTRequest()
|
||||||
|
|
||||||
|
const currentReqWithNoChange = getRESTSaveContext()?.req
|
||||||
|
|
||||||
|
// checks if the current request is the same as the save context request if present
|
||||||
|
if (
|
||||||
|
currentReqWithNoChange &&
|
||||||
|
isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)
|
||||||
|
) {
|
||||||
|
props.page === "rest" && setRestReq(entry.request)
|
||||||
|
clickedHistory.value = entry
|
||||||
|
}
|
||||||
// Initial state trigers a popup
|
// Initial state trigers a popup
|
||||||
if (!clickedHistory.value) {
|
else if (!clickedHistory.value) {
|
||||||
clickedHistory.value = entry
|
clickedHistory.value = entry
|
||||||
confirmChange.value = true
|
confirmChange.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Checks if there are any change done in current request and the history request
|
// Checks if there are any change done in current request and the history request
|
||||||
if (
|
else if (
|
||||||
!isEqualHoppRESTRequest(
|
!isEqualHoppRESTRequest(
|
||||||
currentFullReq,
|
currentFullReq,
|
||||||
clickedHistory.value.request as HoppRESTRequest
|
clickedHistory.value.request as HoppRESTRequest
|
||||||
@@ -347,7 +358,7 @@ const useHistory = (entry: RESTHistoryEntry) => {
|
|||||||
clickedHistory.value = entry
|
clickedHistory.value = entry
|
||||||
confirmChange.value = true
|
confirmChange.value = true
|
||||||
} else {
|
} else {
|
||||||
props.page === "rest" && setRestReq(entry.request as HoppRESTRequest)
|
props.page === "rest" && setRestReq(entry.request)
|
||||||
clickedHistory.value = entry
|
clickedHistory.value = entry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,15 @@
|
|||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
v-focus
|
v-focus
|
||||||
:label="t('action.save')"
|
:label="t('action.save')"
|
||||||
|
:loading="loading"
|
||||||
outline
|
outline
|
||||||
@click="saveApiChange"
|
@click="saveChange"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="t('action.dont_save')"
|
:label="t('action.dont_save')"
|
||||||
outline
|
outline
|
||||||
filled
|
filled
|
||||||
@click="discardApiChange"
|
@click="discardChange"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
@@ -43,6 +44,7 @@ const t = useI18n()
|
|||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
|
loading?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -51,11 +53,11 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const saveApiChange = () => {
|
const saveChange = () => {
|
||||||
emit("save-change")
|
emit("save-change")
|
||||||
}
|
}
|
||||||
|
|
||||||
const discardApiChange = () => {
|
const discardChange = () => {
|
||||||
emit("discard-change")
|
emit("discard-change")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
packages/hoppscotch-common/src/components/smart/Tree.vue
Normal file
65
packages/hoppscotch-common/src/components/smart/Tree.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<div
|
||||||
|
v-if="rootNodes.status === 'loaded' && rootNodes.data.length > 0"
|
||||||
|
class="flex flex-col"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="rootNode in rootNodes.data"
|
||||||
|
:key="rootNode.id"
|
||||||
|
class="flex flex-col flex-1"
|
||||||
|
>
|
||||||
|
<SmartTreeBranch
|
||||||
|
:node-item="rootNode"
|
||||||
|
:adapter="adapter as SmartTreeAdapter<T>"
|
||||||
|
>
|
||||||
|
<template #default="{ node, toggleChildren, isOpen }">
|
||||||
|
<slot
|
||||||
|
name="content"
|
||||||
|
:node="node as TreeNode<T>"
|
||||||
|
:toggle-children="toggleChildren as () => void"
|
||||||
|
:is-open="isOpen as boolean"
|
||||||
|
></slot>
|
||||||
|
</template>
|
||||||
|
<template #emptyNode="{ node }">
|
||||||
|
<slot name="emptyNode" :node="node"></slot>
|
||||||
|
</template>
|
||||||
|
</SmartTreeBranch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="rootNodes.status === 'loading'"
|
||||||
|
class="flex flex-1 flex-col items-center justify-center p-4"
|
||||||
|
>
|
||||||
|
<SmartSpinner class="my-4" />
|
||||||
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="rootNodes.status === 'loaded' && rootNodes.data.length === 0"
|
||||||
|
class="flex flex-col flex-1"
|
||||||
|
>
|
||||||
|
<slot name="emptyNode" :node="(null as TreeNode<T> | null)"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends any">
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { SmartTreeAdapter, TreeNode } from "~/helpers/treeAdapter"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/**
|
||||||
|
* The adapter that will be used to fetch the tree data
|
||||||
|
* @template T The type of the data that will be stored in the tree
|
||||||
|
*/
|
||||||
|
adapter: SmartTreeAdapter<T>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the root nodes from the adapter by passing the node id as null
|
||||||
|
*/
|
||||||
|
const rootNodes = computed(() => props.adapter.getChildren(null).value)
|
||||||
|
</script>
|
||||||
103
packages/hoppscotch-common/src/components/smart/TreeBranch.vue
Normal file
103
packages/hoppscotch-common/src/components/smart/TreeBranch.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<slot
|
||||||
|
:node="nodeItem"
|
||||||
|
:toggle-children="toggleNodeChildren"
|
||||||
|
:is-open="isNodeOpen"
|
||||||
|
></slot>
|
||||||
|
|
||||||
|
<!-- This is a performance optimization trick -->
|
||||||
|
<!-- Once expanded, Vue will traverse through the children and expand the tree up
|
||||||
|
but when we collapse, the tree and the components are disposed. This is wasteful
|
||||||
|
and comes with performance issues if the children list is expensive to render.
|
||||||
|
Hence, here we render children only when first expanded, and after that, even if collapsed,
|
||||||
|
we just hide the children.
|
||||||
|
-->
|
||||||
|
<div v-if="childrenRendered" v-show="showChildren" class="flex">
|
||||||
|
<div
|
||||||
|
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
|
||||||
|
@click="toggleNodeChildren"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
v-if="childNodes.status === 'loaded' && childNodes.data.length > 0"
|
||||||
|
class="flex flex-col flex-1 truncate"
|
||||||
|
>
|
||||||
|
<TreeBranch
|
||||||
|
v-for="childNode in childNodes.data"
|
||||||
|
:key="childNode.id"
|
||||||
|
:node-item="childNode"
|
||||||
|
:adapter="adapter"
|
||||||
|
>
|
||||||
|
<!-- The child slot is given a dynamic name in order to not break Volar -->
|
||||||
|
<template #[CHILD_SLOT_NAME]="{ node, toggleChildren, isOpen }">
|
||||||
|
<!-- Casting to help with type checking -->
|
||||||
|
<slot
|
||||||
|
:node="node as TreeNode<T>"
|
||||||
|
:toggle-children="toggleChildren as () => void"
|
||||||
|
:is-open="isOpen as boolean"
|
||||||
|
></slot>
|
||||||
|
</template>
|
||||||
|
<template #emptyNode="{ node }">
|
||||||
|
<slot name="emptyNode" :node="node"></slot>
|
||||||
|
</template>
|
||||||
|
</TreeBranch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="childNodes.status === 'loading'"
|
||||||
|
class="flex flex-1 flex-col items-center justify-center p-4"
|
||||||
|
>
|
||||||
|
<SmartSpinner class="my-4" />
|
||||||
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="childNodes.status === 'loaded' && childNodes.data.length === 0"
|
||||||
|
class="flex flex-col flex-1"
|
||||||
|
>
|
||||||
|
<slot name="emptyNode" :node="nodeItem"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends any">
|
||||||
|
import { computed, ref } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { SmartTreeAdapter, TreeNode } from "~/helpers/treeAdapter"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/**
|
||||||
|
* The node item that will be used to render the tree branch
|
||||||
|
* @template T The type of the data passed to the tree branch
|
||||||
|
*/
|
||||||
|
adapter: SmartTreeAdapter<T>
|
||||||
|
/**
|
||||||
|
* The node item that will be used to render the tree branch content
|
||||||
|
*/
|
||||||
|
nodeItem: TreeNode<T>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const CHILD_SLOT_NAME = "default"
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks whether the children on this branch were ever rendered
|
||||||
|
* See the usage inside '<template>' for more info
|
||||||
|
*/
|
||||||
|
const childrenRendered = ref(false)
|
||||||
|
|
||||||
|
const showChildren = ref(false)
|
||||||
|
const isNodeOpen = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the child nodes from the adapter by passing the node id of the current node
|
||||||
|
*/
|
||||||
|
const childNodes = computed(
|
||||||
|
() => props.adapter.getChildren(props.nodeItem.id).value
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggleNodeChildren = () => {
|
||||||
|
if (!childrenRendered.value) childrenRendered.value = true
|
||||||
|
|
||||||
|
showChildren.value = !showChildren.value
|
||||||
|
isNodeOpen.value = !isNodeOpen.value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -12,6 +12,7 @@ import { TeamCollection } from "../teams/TeamCollection"
|
|||||||
import { TeamRequest } from "../teams/TeamRequest"
|
import { TeamRequest } from "../teams/TeamRequest"
|
||||||
import { GQLError, runGQLQuery } from "./GQLClient"
|
import { GQLError, runGQLQuery } from "./GQLClient"
|
||||||
import {
|
import {
|
||||||
|
ExportAsJsonDocument,
|
||||||
GetCollectionChildrenIDsDocument,
|
GetCollectionChildrenIDsDocument,
|
||||||
GetCollectionRequestsDocument,
|
GetCollectionRequestsDocument,
|
||||||
GetCollectionTitleDocument,
|
GetCollectionTitleDocument,
|
||||||
@@ -125,3 +126,23 @@ export const teamCollToHoppRESTColl = (
|
|||||||
folders: coll.children?.map(teamCollToHoppRESTColl) ?? [],
|
folders: coll.children?.map(teamCollToHoppRESTColl) ?? [],
|
||||||
requests: coll.requests?.map((x) => x.request) ?? [],
|
requests: coll.requests?.map((x) => x.request) ?? [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the JSON string of all the collection of the specified team
|
||||||
|
* @param teamID - ID of the team
|
||||||
|
* @returns Either of the JSON string of the collection or the error
|
||||||
|
*/
|
||||||
|
export const getTeamCollectionJSON = async (teamID: string) => {
|
||||||
|
const data = await runGQLQuery({
|
||||||
|
query: ExportAsJsonDocument,
|
||||||
|
variables: {
|
||||||
|
teamID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (E.isLeft(data)) {
|
||||||
|
return E.left(data.left)
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.right(data.right)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { runMutation } from "../GQLClient"
|
||||||
|
import {
|
||||||
|
CreateChildCollectionDocument,
|
||||||
|
CreateChildCollectionMutation,
|
||||||
|
CreateChildCollectionMutationVariables,
|
||||||
|
CreateNewRootCollectionDocument,
|
||||||
|
CreateNewRootCollectionMutation,
|
||||||
|
CreateNewRootCollectionMutationVariables,
|
||||||
|
DeleteCollectionDocument,
|
||||||
|
DeleteCollectionMutation,
|
||||||
|
DeleteCollectionMutationVariables,
|
||||||
|
ImportFromJsonDocument,
|
||||||
|
ImportFromJsonMutation,
|
||||||
|
ImportFromJsonMutationVariables,
|
||||||
|
RenameCollectionDocument,
|
||||||
|
RenameCollectionMutation,
|
||||||
|
RenameCollectionMutationVariables,
|
||||||
|
} from "../graphql"
|
||||||
|
|
||||||
|
type CreateNewRootCollectionError = "team_coll/short_title"
|
||||||
|
type CreateChildCollectionError = "team_coll/short_title"
|
||||||
|
type RenameCollectionError = "team_coll/short_title"
|
||||||
|
type DeleteCollectionError = "team/invalid_coll_id"
|
||||||
|
|
||||||
|
export const createNewRootCollection = (title: string, teamID: string) =>
|
||||||
|
runMutation<
|
||||||
|
CreateNewRootCollectionMutation,
|
||||||
|
CreateNewRootCollectionMutationVariables,
|
||||||
|
CreateNewRootCollectionError
|
||||||
|
>(CreateNewRootCollectionDocument, {
|
||||||
|
title,
|
||||||
|
teamID,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const createChildCollection = (
|
||||||
|
childTitle: string,
|
||||||
|
collectionID: string
|
||||||
|
) =>
|
||||||
|
runMutation<
|
||||||
|
CreateChildCollectionMutation,
|
||||||
|
CreateChildCollectionMutationVariables,
|
||||||
|
CreateChildCollectionError
|
||||||
|
>(CreateChildCollectionDocument, {
|
||||||
|
childTitle,
|
||||||
|
collectionID,
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Can be used to rename both collection and folder (considered same in BE) */
|
||||||
|
export const renameCollection = (collectionID: string, newTitle: string) =>
|
||||||
|
runMutation<
|
||||||
|
RenameCollectionMutation,
|
||||||
|
RenameCollectionMutationVariables,
|
||||||
|
RenameCollectionError
|
||||||
|
>(RenameCollectionDocument, {
|
||||||
|
collectionID,
|
||||||
|
newTitle,
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Can be used to delete both collection and folder (considered same in BE) */
|
||||||
|
export const deleteCollection = (collectionID: string) =>
|
||||||
|
runMutation<
|
||||||
|
DeleteCollectionMutation,
|
||||||
|
DeleteCollectionMutationVariables,
|
||||||
|
DeleteCollectionError
|
||||||
|
>(DeleteCollectionDocument, {
|
||||||
|
collectionID,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const importJSONToTeam = (collectionJSON: string, teamID: string) =>
|
||||||
|
runMutation<ImportFromJsonMutation, ImportFromJsonMutationVariables, "">(
|
||||||
|
ImportFromJsonDocument,
|
||||||
|
{
|
||||||
|
jsonString: collectionJSON,
|
||||||
|
teamID,
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,14 +1,66 @@
|
|||||||
import { runMutation } from "../GQLClient"
|
import { runMutation } from "../GQLClient"
|
||||||
import {
|
import {
|
||||||
|
CreateRequestInCollectionDocument,
|
||||||
|
CreateRequestInCollectionMutation,
|
||||||
|
CreateRequestInCollectionMutationVariables,
|
||||||
|
DeleteRequestDocument,
|
||||||
|
DeleteRequestMutation,
|
||||||
|
DeleteRequestMutationVariables,
|
||||||
MoveRestTeamRequestDocument,
|
MoveRestTeamRequestDocument,
|
||||||
MoveRestTeamRequestMutation,
|
MoveRestTeamRequestMutation,
|
||||||
MoveRestTeamRequestMutationVariables,
|
MoveRestTeamRequestMutationVariables,
|
||||||
|
UpdateRequestDocument,
|
||||||
|
UpdateRequestMutation,
|
||||||
|
UpdateRequestMutationVariables,
|
||||||
} from "../graphql"
|
} from "../graphql"
|
||||||
|
|
||||||
type MoveRestTeamRequestErrors =
|
type MoveRestTeamRequestErrors =
|
||||||
| "team_req/not_found"
|
| "team_req/not_found"
|
||||||
| "team_req/invalid_target_id"
|
| "team_req/invalid_target_id"
|
||||||
|
|
||||||
|
type DeleteRequestErrors = "team_req/not_found"
|
||||||
|
|
||||||
|
export const createRequestInCollection = (
|
||||||
|
collectionID: string,
|
||||||
|
data: {
|
||||||
|
request: string
|
||||||
|
teamID: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
runMutation<
|
||||||
|
CreateRequestInCollectionMutation,
|
||||||
|
CreateRequestInCollectionMutationVariables,
|
||||||
|
""
|
||||||
|
>(CreateRequestInCollectionDocument, {
|
||||||
|
collectionID,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateTeamRequest = (
|
||||||
|
requestID: string,
|
||||||
|
data: {
|
||||||
|
request: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
runMutation<UpdateRequestMutation, UpdateRequestMutationVariables, "">(
|
||||||
|
UpdateRequestDocument,
|
||||||
|
{
|
||||||
|
requestID,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const deleteTeamRequest = (requestID: string) =>
|
||||||
|
runMutation<
|
||||||
|
DeleteRequestMutation,
|
||||||
|
DeleteRequestMutationVariables,
|
||||||
|
DeleteRequestErrors
|
||||||
|
>(DeleteRequestDocument, {
|
||||||
|
requestID,
|
||||||
|
})
|
||||||
|
|
||||||
export const moveRESTTeamRequest = (requestID: string, collectionID: string) =>
|
export const moveRESTTeamRequest = (requestID: string, collectionID: string) =>
|
||||||
runMutation<
|
runMutation<
|
||||||
MoveRestTeamRequestMutation,
|
MoveRestTeamRequestMutation,
|
||||||
|
|||||||
34
packages/hoppscotch-common/src/helpers/gist.ts
Normal file
34
packages/hoppscotch-common/src/helpers/gist.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an gist on GitHub with the collection JSON
|
||||||
|
* @param collectionJSON - JSON string of the collection
|
||||||
|
* @param accessToken - GitHub access token
|
||||||
|
* @returns Either of the response of the GitHub Gist API or the error
|
||||||
|
*/
|
||||||
|
export const createCollectionGists = (
|
||||||
|
collectionJSON: string,
|
||||||
|
accessToken: string
|
||||||
|
) => {
|
||||||
|
return TE.tryCatch(
|
||||||
|
async () =>
|
||||||
|
axios.post(
|
||||||
|
"https://api.github.com/gists",
|
||||||
|
{
|
||||||
|
files: {
|
||||||
|
"hoppscotch-collections.json": {
|
||||||
|
content: collectionJSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${accessToken}`,
|
||||||
|
Accept: "application/vnd.github.v3+json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(reason) => reason
|
||||||
|
)
|
||||||
|
}
|
||||||
34
packages/hoppscotch-common/src/helpers/treeAdapter.ts
Normal file
34
packages/hoppscotch-common/src/helpers/treeAdapter.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Ref } from "vue"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a tree node in the SmartTreeAdapter.
|
||||||
|
*/
|
||||||
|
export type TreeNode<T> = {
|
||||||
|
id: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of children result from a tree node when there will be a loading state.
|
||||||
|
*/
|
||||||
|
export type ChildrenResult<T> =
|
||||||
|
| {
|
||||||
|
status: "loading"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: "loaded"
|
||||||
|
data: Array<TreeNode<T>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tree adapter that can be used with the SmartTree component.
|
||||||
|
* @template T The type of data that is stored in the tree.
|
||||||
|
*/
|
||||||
|
export interface SmartTreeAdapter<T> {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param nodeID - id of the node to get children for
|
||||||
|
* @returns - Ref that contains the children of the node. It is reactive and will be updated when the children are changed.
|
||||||
|
*/
|
||||||
|
getChildren: (nodeID: string | null) => Ref<ChildrenResult<T>>
|
||||||
|
}
|
||||||
47
packages/hoppscotch-common/src/helpers/types/HoppPicked.ts
Normal file
47
packages/hoppscotch-common/src/helpers/types/HoppPicked.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Picked is used to defrentiate
|
||||||
|
* the select item in the save request dialog
|
||||||
|
* The save request dialog can be used
|
||||||
|
* to save a request, folder or a collection
|
||||||
|
* seperately for my and teams for REST.
|
||||||
|
* also for graphQL collections
|
||||||
|
*/
|
||||||
|
export type Picked =
|
||||||
|
| {
|
||||||
|
pickedType: "my-request"
|
||||||
|
folderPath: string
|
||||||
|
requestIndex: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
pickedType: "my-folder"
|
||||||
|
folderPath: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
pickedType: "my-collection"
|
||||||
|
collectionIndex: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
pickedType: "teams-request"
|
||||||
|
requestID: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
pickedType: "teams-folder"
|
||||||
|
folderID: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
pickedType: "teams-collection"
|
||||||
|
collectionID: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
pickedType: "gql-my-request"
|
||||||
|
folderPath: string
|
||||||
|
requestIndex: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
pickedType: "gql-my-folder"
|
||||||
|
folderPath: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
pickedType: "gql-my-collection"
|
||||||
|
collectionIndex: number
|
||||||
|
}
|
||||||
@@ -36,5 +36,9 @@
|
|||||||
"src/**/*.d.ts",
|
"src/**/*.d.ts",
|
||||||
"src/**/*.tsx",
|
"src/**/*.tsx",
|
||||||
"src/**/*.vue",
|
"src/**/*.vue",
|
||||||
]
|
],
|
||||||
|
"vueCompilerOptions": {
|
||||||
|
"jsxTemplates": true,
|
||||||
|
"experimentalRfc436": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default defineConfig({
|
|||||||
lowerSecondaryStickyFold: "var(--lower-secondary-sticky-fold)",
|
lowerSecondaryStickyFold: "var(--lower-secondary-sticky-fold)",
|
||||||
lowerTertiaryStickyFold: "var(--lower-tertiary-sticky-fold)",
|
lowerTertiaryStickyFold: "var(--lower-tertiary-sticky-fold)",
|
||||||
sidebarPrimaryStickyFold: "var(--sidebar-primary-sticky-fold)",
|
sidebarPrimaryStickyFold: "var(--sidebar-primary-sticky-fold)",
|
||||||
|
sidebarSecondaryStickyFold: "var(--line-height-body)",
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
primary: "var(--primary-color)",
|
primary: "var(--primary-color)",
|
||||||
|
|||||||
37
packages/hoppscotch-ui/src/components.d.ts
vendored
37
packages/hoppscotch-ui/src/components.d.ts
vendored
@@ -1,37 +0,0 @@
|
|||||||
// generated by unplugin-vue-components
|
|
||||||
// We suggest you to commit this file into source control
|
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
|
||||||
import '@vue/runtime-core'
|
|
||||||
|
|
||||||
export {}
|
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
|
||||||
export interface GlobalComponents {
|
|
||||||
ButtonPrimary: typeof import('./components/button/Primary.vue')['default']
|
|
||||||
ButtonSecondary: typeof import('./components/button/Secondary.vue')['default']
|
|
||||||
IconLucideLoader: typeof import('~icons/lucide/loader')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
|
||||||
SmartAnchor: typeof import('./components/smart/Anchor.vue')['default']
|
|
||||||
SmartAutoComplete: typeof import('./components/smart/AutoComplete.vue')['default']
|
|
||||||
SmartCheckbox: typeof import('./components/smart/Checkbox.vue')['default']
|
|
||||||
SmartConfirmModal: typeof import('./components/smart/ConfirmModal.vue')['default']
|
|
||||||
SmartExpand: typeof import('./components/smart/Expand.vue')['default']
|
|
||||||
SmartFileChip: typeof import('./components/smart/FileChip.vue')['default']
|
|
||||||
SmartIntersection: typeof import('./components/smart/Intersection.vue')['default']
|
|
||||||
SmartItem: typeof import('./components/smart/Item.vue')['default']
|
|
||||||
SmartLink: typeof import('./components/smart/Link.vue')['default']
|
|
||||||
SmartModal: typeof import('./components/smart/Modal.vue')['default']
|
|
||||||
SmartProgressRing: typeof import('./components/smart/ProgressRing.vue')['default']
|
|
||||||
SmartRadio: typeof import('./components/smart/Radio.vue')['default']
|
|
||||||
SmartRadioGroup: typeof import('./components/smart/RadioGroup.vue')['default']
|
|
||||||
SmartSlideOver: typeof import('./components/smart/SlideOver.vue')['default']
|
|
||||||
SmartSpinner: typeof import('./components/smart/Spinner.vue')['default']
|
|
||||||
SmartTab: typeof import('./components/smart/Tab.vue')['default']
|
|
||||||
SmartTabs: typeof import('./components/smart/Tabs.vue')['default']
|
|
||||||
SmartToggle: typeof import('./components/smart/Toggle.vue')['default']
|
|
||||||
SmartWindow: typeof import('./components/smart/Window.vue')['default']
|
|
||||||
SmartWindows: typeof import('./components/smart/Windows.vue')['default']
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -63,78 +63,82 @@
|
|||||||
</SmartLink>
|
</SmartLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script lang="ts">
|
||||||
defineProps({
|
import { defineComponent } from "vue"
|
||||||
to: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
exact: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
blank: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* This will be a component!
|
|
||||||
*/
|
|
||||||
icon: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* This will be a component!
|
|
||||||
*/
|
|
||||||
svg: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
reverse: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
outline: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
shortcut: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
activeInfoIcon: {
|
export default defineComponent({
|
||||||
type: Boolean,
|
props: {
|
||||||
default: false,
|
to: {
|
||||||
},
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
exact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
blank: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* This will be a component!
|
||||||
|
*/
|
||||||
|
icon: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* This will be a component!
|
||||||
|
*/
|
||||||
|
svg: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
reverse: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
activeInfoIcon: {
|
||||||
* This will be a component!
|
type: Boolean,
|
||||||
*/
|
default: false,
|
||||||
infoIcon: {
|
},
|
||||||
type: Object,
|
|
||||||
default: null,
|
/**
|
||||||
|
* This will be a component!
|
||||||
|
*/
|
||||||
|
infoIcon: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -106,36 +106,25 @@ import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
|
|||||||
const { t, onModalOpen, onModalClose } =
|
const { t, onModalOpen, onModalClose } =
|
||||||
inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||||
|
|
||||||
defineProps({
|
withDefaults(
|
||||||
dialog: {
|
defineProps<{
|
||||||
type: Boolean,
|
dialog: boolean,
|
||||||
default: false,
|
title: string,
|
||||||
},
|
dimissible: boolean,
|
||||||
title: {
|
placement: string,
|
||||||
type: String,
|
fullWidth: boolean,
|
||||||
default: "",
|
styles: string,
|
||||||
},
|
closeText: string | null,
|
||||||
dimissible: {
|
}>(), {
|
||||||
type: Boolean,
|
dialog: false,
|
||||||
default: true,
|
title: "",
|
||||||
},
|
dimissible: true,
|
||||||
placement: {
|
placement: "top",
|
||||||
type: String,
|
fullWidth: false,
|
||||||
default: "top",
|
styles: "sm:max-w-lg",
|
||||||
},
|
closeText: null
|
||||||
fullWidth: {
|
}
|
||||||
type: Boolean,
|
)
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
styles: {
|
|
||||||
type: String,
|
|
||||||
default: "sm:max-w-lg",
|
|
||||||
},
|
|
||||||
closeText: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "close"): void
|
(e: "close"): void
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<icon-lucide-loader class="animate-spin svg-icons" />
|
<icon-lucide-loader class="animate-spin svg-icons" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue"
|
||||||
|
|
||||||
|
export default defineComponent({})
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user