Edit.vue (hoppscotch-2.2.1) | : | Edit.vue (hoppscotch-3.0.0) | ||
---|---|---|---|---|
<template> | <template> | |||
<SmartModal v-if="show" :title="t('team.edit')" @close="hideModal"> | <SmartModal v-if="show" dialog :title="t('team.edit')" @close="hideModal"> | |||
<template #body> | <template #body> | |||
<div class="flex flex-col px-2"> | <div class="flex flex-col"> | |||
<div class="relative flex"> | <div class="relative flex"> | |||
<input | <input | |||
id="selectLabelTeamEdit" | id="selectLabelTeamEdit" | |||
v-model="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="saveTeam" | @keyup.enter="saveTeam" | |||
skipping to change at line 26 | skipping to change at line 26 | |||
<label for="selectLabelTeamEdit"> | <label for="selectLabelTeamEdit"> | |||
{{ t("action.label") }} | {{ t("action.label") }} | |||
</label> | </label> | |||
</div> | </div> | |||
<div class="flex items-center justify-between flex-1 pt-4"> | <div class="flex items-center justify-between flex-1 pt-4"> | |||
<label for="memberList" class="p-4"> | <label for="memberList" class="p-4"> | |||
{{ t("team.members") }} | {{ t("team.members") }} | |||
</label> | </label> | |||
<div class="flex"> | <div class="flex"> | |||
<ButtonSecondary | <ButtonSecondary | |||
svg="user-plus" | :icon="IconUserPlus" | |||
:label="t('team.invite')" | :label="t('team.invite')" | |||
filled | filled | |||
@click.native=" | @click=" | |||
() => { | () => { | |||
$emit('invite-team') | emit('invite-team') | |||
} | } | |||
" | " | |||
/> | /> | |||
</div> | </div> | |||
</div> | </div> | |||
<div | <div | |||
v-if="teamDetails.loading" | v-if="teamDetails.loading" | |||
class="flex flex-col items-center justify-center" | class="flex flex-col items-center justify-center" | |||
> | > | |||
<SmartSpinner class="mb-4" /> | <SmartSpinner class="mb-4" /> | |||
<span class="text-secondaryLight">{{ t("state.loading") }}</span> | <span class="text-secondaryLight">{{ t("state.loading") }}</span> | |||
</div> | </div> | |||
<div | <div | |||
v-if=" | v-if=" | |||
!teamDetails.loading && | !teamDetails.loading && | |||
E.isRight(teamDetails.data) && | E.isRight(teamDetails.data) && | |||
teamDetails.data.right.team.teamMembers | teamDetails.data.right.team.teamMembers | |||
" | " | |||
class="border divide-y rounded divide-dividerLight border-divider" | class="border rounded divide-y divide-dividerLight border-divider" | |||
> | > | |||
<div | <div | |||
v-if="teamDetails.data.right.team.teamMembers === 0" | v-if="teamDetails.data.right.team.teamMembers === 0" | |||
class="flex flex-col items-center justify-center p-4 text-secondaryL ight" | class="flex flex-col items-center justify-center p-4 text-secondaryL ight" | |||
> | > | |||
<img | <img | |||
:src="`/images/states/${$colorMode.value}/add_group.svg`" | :src="`/images/states/${colorMode.value}/add_group.svg`" | |||
loading="lazy" | loading="lazy" | |||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4" | class="inline-flex flex-col object-contain object-center w-16 h-16 my-4" | |||
:alt="`${t('empty.members')}`" | :alt="`${t('empty.members')}`" | |||
/> | /> | |||
<span class="pb-4 text-center"> | <span class="pb-4 text-center"> | |||
{{ t("empty.members") }} | {{ t("empty.members") }} | |||
</span> | </span> | |||
<ButtonSecondary | <ButtonSecondary | |||
svg="user-plus" | :icon="IconUserPlus" | |||
:label="t('team.invite')" | :label="t('team.invite')" | |||
@click.native=" | @click=" | |||
() => { | () => { | |||
emit('invite-team') | emit('invite-team') | |||
} | } | |||
" | " | |||
/> | /> | |||
</div> | </div> | |||
<div v-else> | <div v-else> | |||
<div | <div | |||
v-for="(member, index) in membersList" | v-for="(member, index) in membersList" | |||
:key="`member-${index}`" | :key="`member-${index}`" | |||
skipping to change at line 96 | skipping to change at line 96 | |||
readonly | readonly | |||
/> | /> | |||
<span> | <span> | |||
<tippy | <tippy | |||
ref="memberOptions" | ref="memberOptions" | |||
interactive | interactive | |||
trigger="click" | trigger="click" | |||
theme="popover" | theme="popover" | |||
arrow | arrow | |||
> | > | |||
<template #trigger> | <span class="select-wrapper"> | |||
<span class="select-wrapper"> | <input | |||
<input | class="flex flex-1 px-4 py-2 bg-transparent cursor-pointer | |||
class="flex flex-1 px-4 py-2 bg-transparent cursor-point | " | |||
er" | :placeholder="`${t('team.permissions')}`" | |||
:placeholder="`${t('team.permissions')}`" | :name="'value' + index" | |||
:name="'value' + index" | :value="member.role" | |||
:value="member.role" | readonly | |||
readonly | /> | |||
</span> | ||||
<template #content="{ hide }"> | ||||
<div | ||||
class="flex flex-col" | ||||
tabindex="0" | ||||
role="menu" | ||||
@keyup.escape="hide()" | ||||
> | ||||
<SmartItem | ||||
label="OWNER" | ||||
:icon=" | ||||
member.role === 'OWNER' ? IconCircleDot : IconCircle | ||||
" | ||||
:active="member.role === 'OWNER'" | ||||
@click=" | ||||
() => { | ||||
updateMemberRole(member.userID, 'OWNER') | ||||
hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
label="EDITOR" | ||||
:icon=" | ||||
member.role === 'EDITOR' ? IconCircleDot : IconCircle | ||||
" | ||||
:active="member.role === 'EDITOR'" | ||||
@click=" | ||||
() => { | ||||
updateMemberRole(member.userID, 'EDITOR') | ||||
hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
label="VIEWER" | ||||
:icon=" | ||||
member.role === 'VIEWER' ? IconCircleDot : IconCircle | ||||
" | ||||
:active="member.role === 'VIEWER'" | ||||
@click=" | ||||
() => { | ||||
updateMemberRole(member.userID, 'VIEWER') | ||||
hide() | ||||
} | ||||
" | ||||
/> | /> | |||
</span> | </div> | |||
</template> | </template> | |||
<SmartItem | ||||
label="OWNER" | ||||
:icon=" | ||||
member.role === 'OWNER' | ||||
? 'radio_button_checked' | ||||
: 'radio_button_unchecked' | ||||
" | ||||
:active="member.role === 'OWNER'" | ||||
@click.native=" | ||||
() => { | ||||
updateMemberRole(member.userID, 'OWNER') | ||||
memberOptions[index].tippy().hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
label="EDITOR" | ||||
:icon=" | ||||
member.role === 'EDITOR' | ||||
? 'radio_button_checked' | ||||
: 'radio_button_unchecked' | ||||
" | ||||
:active="member.role === 'EDITOR'" | ||||
@click.native=" | ||||
() => { | ||||
updateMemberRole(member.userID, 'EDITOR') | ||||
memberOptions[index].tippy().hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
label="VIEWER" | ||||
:icon=" | ||||
member.role === 'VIEWER' | ||||
? 'radio_button_checked' | ||||
: 'radio_button_unchecked' | ||||
" | ||||
:active="member.role === 'VIEWER'" | ||||
@click.native=" | ||||
() => { | ||||
updateMemberRole(member.userID, 'VIEWER') | ||||
memberOptions[index].tippy().hide() | ||||
} | ||||
" | ||||
/> | ||||
</tippy> | </tippy> | |||
</span> | </span> | |||
<div class="flex"> | <div class="flex"> | |||
<ButtonSecondary | <ButtonSecondary | |||
id="member" | id="member" | |||
v-tippy="{ theme: 'tooltip' }" | v-tippy="{ theme: 'tooltip' }" | |||
:title="t('action.remove')" | :title="t('action.remove')" | |||
svg="user-minus" | :icon="IconUserMinus" | |||
color="red" | color="red" | |||
@click.native="removeExistingTeamMember(member.userID)" | :loading="isLoadingIndex === index" | |||
@click="removeExistingTeamMember(member.userID, index)" | ||||
/> | /> | |||
</div> | </div> | |||
</div> | </div> | |||
</div> | </div> | |||
</div> | </div> | |||
<div | <div | |||
v-if="!teamDetails.loading && E.isLeft(teamDetails.data)" | v-if="!teamDetails.loading && E.isLeft(teamDetails.data)" | |||
class="flex flex-col items-center" | class="flex flex-col items-center" | |||
> | > | |||
<i class="mb-4 material-icons">help_outline</i> | <component :is="IconHelpCircle" class="mb-4 svg-icons" /> | |||
{{ t("error.something_went_wrong") }} | {{ t("error.something_went_wrong") }} | |||
</div> | </div> | |||
</div> | </div> | |||
</template> | </template> | |||
<template #footer> | <template #footer> | |||
<span> | <span class="flex"> | |||
<ButtonPrimary :label="t('action.save')" @click.native="saveTeam" /> | <ButtonPrimary | |||
<ButtonSecondary | :label="t('action.save')" | |||
:label="t('action.cancel')" | :loading="isLoading" | |||
@click.native="hideModal" | @click="saveTeam" | |||
/> | /> | |||
<ButtonSecondary :label="t('action.cancel')" @click="hideModal" /> | ||||
</span> | </span> | |||
</template> | </template> | |||
</SmartModal> | </SmartModal> | |||
</template> | </template> | |||
<script setup lang="ts"> | <script setup lang="ts"> | |||
import { computed, ref, toRef, watch } from "@nuxtjs/composition-api" | import { computed, ref, toRef, watch } from "vue" | |||
import * as E from "fp-ts/Either" | import * as E from "fp-ts/Either" | |||
import { | import { | |||
GetTeamDocument, | GetTeamDocument, | |||
GetTeamQuery, | GetTeamQuery, | |||
GetTeamQueryVariables, | GetTeamQueryVariables, | |||
TeamMemberAddedDocument, | TeamMemberAddedDocument, | |||
TeamMemberRemovedDocument, | TeamMemberRemovedDocument, | |||
TeamMemberRole, | TeamMemberRole, | |||
TeamMemberUpdatedDocument, | TeamMemberUpdatedDocument, | |||
} from "~/helpers/backend/graphql" | } from "~/helpers/backend/graphql" | |||
import { | import { | |||
removeTeamMember, | removeTeamMember, | |||
renameTeam, | renameTeam, | |||
updateTeamMemberRole, | updateTeamMemberRole, | |||
} from "~/helpers/backend/mutations/Team" | } from "~/helpers/backend/mutations/Team" | |||
import { TeamNameCodec } from "~/helpers/backend/types/TeamName" | import { TeamNameCodec } from "~/helpers/backend/types/TeamName" | |||
import { useGQLQuery } from "~/helpers/backend/GQLClient" | ||||
import { useI18n, useToast } from "~/helpers/utils/composables" | import { useGQLQuery } from "~/composables/graphql" | |||
import { useI18n } from "@composables/i18n" | ||||
import { useToast } from "@composables/toast" | ||||
import { useColorMode } from "@composables/theming" | ||||
import IconCircleDot from "~icons/lucide/circle-dot" | ||||
import IconCircle from "~icons/lucide/circle" | ||||
import IconUserPlus from "~icons/lucide/user-plus" | ||||
import IconUserMinus from "~icons/lucide/user-minus" | ||||
import IconHelpCircle from "~icons/lucide/help-circle" | ||||
const t = useI18n() | const t = useI18n() | |||
const colorMode = useColorMode() | ||||
const emit = defineEmits<{ | const emit = defineEmits<{ | |||
(e: "hide-modal"): void | (e: "hide-modal"): void | |||
(e: "refetch-teams"): void | ||||
(e: "invite-team"): void | ||||
}>() | }>() | |||
const memberOptions = ref<any | null>(null) | const memberOptions = ref<any | null>(null) | |||
const props = defineProps<{ | const props = defineProps<{ | |||
show: boolean | show: boolean | |||
editingTeam: { | editingTeam: { | |||
name: string | name: string | |||
} | } | |||
editingTeamID: string | editingTeamID: string | |||
skipping to change at line 248 | skipping to change at line 263 | |||
(teamID: string) => { | (teamID: string) => { | |||
teamDetails.execute({ teamID }) | teamDetails.execute({ teamID }) | |||
} | } | |||
) | ) | |||
const teamDetails = useGQLQuery<GetTeamQuery, GetTeamQueryVariables, "">({ | const teamDetails = useGQLQuery<GetTeamQuery, GetTeamQueryVariables, "">({ | |||
query: GetTeamDocument, | query: GetTeamDocument, | |||
variables: { | variables: { | |||
teamID: props.editingTeamID, | teamID: props.editingTeamID, | |||
}, | }, | |||
pollDuration: 10000, | ||||
defer: true, | defer: true, | |||
updateSubs: computed(() => { | updateSubs: computed(() => { | |||
if (props.editingTeamID) { | if (props.editingTeamID) { | |||
return [ | return [ | |||
{ | { | |||
key: 1, | key: 1, | |||
query: TeamMemberAddedDocument, | query: TeamMemberAddedDocument, | |||
variables: { | variables: { | |||
teamID: props.editingTeamID, | teamID: props.editingTeamID, | |||
}, | }, | |||
skipping to change at line 278 | skipping to change at line 294 | |||
query: TeamMemberRemovedDocument, | query: TeamMemberRemovedDocument, | |||
variables: { | variables: { | |||
teamID: props.editingTeamID, | teamID: props.editingTeamID, | |||
}, | }, | |||
}, | }, | |||
] | ] | |||
} else return [] | } else return [] | |||
}), | }), | |||
}) | }) | |||
watch( | ||||
() => props.show, | ||||
(show) => { | ||||
if (!show) { | ||||
teamDetails.pause() | ||||
} else { | ||||
teamDetails.unpause() | ||||
} | ||||
} | ||||
) | ||||
const roleUpdates = ref< | const roleUpdates = ref< | |||
{ | { | |||
userID: string | userID: string | |||
role: TeamMemberRole | role: TeamMemberRole | |||
}[] | }[] | |||
>([]) | >([]) | |||
watch( | watch( | |||
() => teamDetails, | () => teamDetails, | |||
() => { | () => { | |||
skipping to change at line 346 | skipping to change at line 373 | |||
role: updatedRole?.role ?? member.role, | role: updatedRole?.role ?? member.role, | |||
} | } | |||
}) | }) | |||
return members | return members | |||
} | } | |||
return [] | return [] | |||
}) | }) | |||
const removeExistingTeamMember = async (userID: string) => { | const isLoadingIndex = ref<null | number>(null) | |||
const removeExistingTeamMember = async (userID: string, index: number) => { | ||||
isLoadingIndex.value = index | ||||
const removeTeamMemberResult = await removeTeamMember( | const removeTeamMemberResult = await removeTeamMember( | |||
userID, | userID, | |||
props.editingTeamID | props.editingTeamID | |||
)() | )() | |||
if (E.isLeft(removeTeamMemberResult)) { | if (E.isLeft(removeTeamMemberResult)) { | |||
toast.error(`${t("error.something_went_wrong")}`) | toast.error(`${t("error.something_went_wrong")}`) | |||
} else { | } else { | |||
toast.success(`${t("team.member_removed")}`) | toast.success(`${t("team.member_removed")}`) | |||
emit("refetch-teams") | ||||
teamDetails.execute({ teamID: props.editingTeamID }) | ||||
} | } | |||
isLoadingIndex.value = null | ||||
} | } | |||
const isLoading = ref(false) | ||||
const saveTeam = async () => { | const saveTeam = async () => { | |||
isLoading.value = true | ||||
if (name.value !== "") { | if (name.value !== "") { | |||
if (TeamNameCodec.is(name.value)) { | if (TeamNameCodec.is(name.value)) { | |||
const updateTeamNameResult = await renameTeam( | const updateTeamNameResult = await renameTeam( | |||
props.editingTeamID, | props.editingTeamID, | |||
name.value | name.value | |||
)() | )() | |||
if (E.isLeft(updateTeamNameResult)) { | if (E.isLeft(updateTeamNameResult)) { | |||
toast.error(`${t("error.something_went_wrong")}`) | toast.error(`${t("error.something_went_wrong")}`) | |||
} else { | } else { | |||
roleUpdates.value.forEach(async (update) => { | roleUpdates.value.forEach(async (update) => { | |||
skipping to change at line 383 | skipping to change at line 419 | |||
)() | )() | |||
if (E.isLeft(updateMemberRoleResult)) { | if (E.isLeft(updateMemberRoleResult)) { | |||
toast.error(`${t("error.something_went_wrong")}`) | toast.error(`${t("error.something_went_wrong")}`) | |||
console.error(updateMemberRoleResult.left.error) | console.error(updateMemberRoleResult.left.error) | |||
} | } | |||
}) | }) | |||
} | } | |||
hideModal() | hideModal() | |||
toast.success(`${t("team.saved")}`) | toast.success(`${t("team.saved")}`) | |||
} else { | } else { | |||
return toast.error(`${t("team.name_length_insufficient")}`) | toast.error(`${t("team.name_length_insufficient")}`) | |||
} | } | |||
} else { | } else { | |||
return toast.error(`${t("empty.team_name")}`) | toast.error(`${t("empty.team_name")}`) | |||
} | } | |||
isLoading.value = false | ||||
} | } | |||
const hideModal = () => { | const hideModal = () => { | |||
emit("hide-modal") | emit("hide-modal") | |||
} | } | |||
</script> | </script> | |||
End of changes. 31 change blocks. | ||||
78 lines changed or deleted | 115 lines changed or added |