Edit.vue (hoppscotch-2.0.0) | : | Edit.vue (hoppscotch-2.1.0) | ||
---|---|---|---|---|
skipping to change at line 13 | skipping to change at line 13 | |||
<template #body> | <template #body> | |||
<div class="flex flex-col px-2"> | <div class="flex flex-col px-2"> | |||
<div class="flex relative"> | <div class="flex relative"> | |||
<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" | ||||
@keyup.enter="saveTeam" | @keyup.enter="saveTeam" | |||
/> | /> | |||
<label for="selectLabelTeamEdit"> | <label for="selectLabelTeamEdit"> | |||
{{ $t("action.label") }} | {{ $t("action.label") }} | |||
</label> | </label> | |||
</div> | </div> | |||
<div class="flex flex-1 justify-between items-center"> | <div class="flex pt-4 flex-1 justify-between items-center"> | |||
<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 | |||
icon="add" | svg="user-plus" | |||
:label="$t('add.new')" | :label="$t('team.invite')" | |||
@click.native="addTeamMember" | filled | |||
@click.native=" | ||||
() => { | ||||
$emit('invite-team') | ||||
} | ||||
" | ||||
/> | /> | |||
</div> | </div> | |||
</div> | </div> | |||
<div class="divide-y divide-dividerLight border-divider border rounded"> | <div | |||
<div | v-if="teamDetails.loading" | |||
v-for="(member, index) in members" | class="flex flex-col items-center justify-center" | |||
:key="`member-${index}`" | > | |||
class="divide-x divide-dividerLight flex" | <SmartSpinner class="mb-4" /> | |||
> | <span class="text-secondaryLight">{{ $t("state.loading") }}</span> | |||
<input | </div> | |||
class="bg-transparent flex flex-1 py-2 px-4" | <div | |||
:placeholder="$t('team.email')" | v-if=" | |||
:name="'param' + index" | !teamDetails.loading && | |||
:value="member.user.email" | E.isRight(teamDetails.data) && | |||
readonly | teamDetails.data.right.team.teamMembers | |||
/> | " | |||
<span> | class="divide-y divide-dividerLight border-divider border rounded" | |||
<tippy | > | |||
:ref="`memberOptions-${index}`" | ||||
interactive | ||||
trigger="click" | ||||
theme="popover" | ||||
arrow | ||||
> | ||||
<template #trigger> | ||||
<span class="select-wrapper"> | ||||
<input | ||||
class=" | ||||
bg-transparent | ||||
cursor-pointer | ||||
flex flex-1 | ||||
py-2 | ||||
px-4 | ||||
" | ||||
:placeholder="$t('team.permissions')" | ||||
:name="'value' + index" | ||||
:value=" | ||||
typeof member.role === 'string' | ||||
? member.role | ||||
: JSON.stringify(member.role) | ||||
" | ||||
readonly | ||||
/> | ||||
</span> | ||||
</template> | ||||
<SmartItem | ||||
label="OWNER" | ||||
@click.native="updateMemberRole(index, 'OWNER')" | ||||
/> | ||||
<SmartItem | ||||
label="EDITOR" | ||||
@click.native="updateMemberRole(index, 'EDITOR')" | ||||
/> | ||||
<SmartItem | ||||
label="VIEWER" | ||||
@click.native="updateMemberRole(index, 'VIEWER')" | ||||
/> | ||||
</tippy> | ||||
</span> | ||||
<div class="flex"> | ||||
<ButtonSecondary | ||||
id="member" | ||||
v-tippy="{ theme: 'tooltip' }" | ||||
:title="$t('action.remove')" | ||||
icon="remove_circle_outline" | ||||
color="red" | ||||
@click.native="removeExistingTeamMember(member.user.uid)" | ||||
/> | ||||
</div> | ||||
</div> | ||||
<div | ||||
v-for="(member, index) in newMembers" | ||||
:key="`new-member-${index}`" | ||||
class="divide-x divide-dividerLight flex" | ||||
> | ||||
<input | ||||
v-model="member.key" | ||||
class="bg-transparent flex flex-1 py-2 px-4" | ||||
:placeholder="$t('team.email')" | ||||
:name="'member' + index" | ||||
autofocus | ||||
/> | ||||
<span> | ||||
<tippy | ||||
:ref="`newMemberOptions-${index}`" | ||||
interactive | ||||
trigger="click" | ||||
theme="popover" | ||||
arrow | ||||
> | ||||
<template #trigger> | ||||
<span class="select-wrapper"> | ||||
<input | ||||
class=" | ||||
bg-transparent | ||||
cursor-pointer | ||||
flex flex-1 | ||||
py-2 | ||||
px-4 | ||||
" | ||||
:placeholder="$t('team.permissions')" | ||||
:name="'value' + index" | ||||
:value=" | ||||
typeof member.value === 'string' | ||||
? member.value | ||||
: JSON.stringify(member.value) | ||||
" | ||||
readonly | ||||
/> | ||||
</span> | ||||
</template> | ||||
<SmartItem | ||||
label="OWNER" | ||||
@click.native="updateNewMemberRole(index, 'OWNER')" | ||||
/> | ||||
<SmartItem | ||||
label="EDITOR" | ||||
@click.native="updateNewMemberRole(index, 'EDITOR')" | ||||
/> | ||||
<SmartItem | ||||
label="VIEWER" | ||||
@click.native="updateNewMemberRole(index, 'VIEWER')" | ||||
/> | ||||
</tippy> | ||||
</span> | ||||
<div class="flex"> | ||||
<ButtonSecondary | ||||
id="member" | ||||
v-tippy="{ theme: 'tooltip' }" | ||||
:title="$t('action.remove')" | ||||
icon="remove_circle_outline" | ||||
color="red" | ||||
@click.native="removeTeamMember(index)" | ||||
/> | ||||
</div> | ||||
</div> | ||||
<div | <div | |||
v-if="members.length === 0 && newMembers.length === 0" | v-if="teamDetails.data.right.team.teamMembers === 0" | |||
class=" | class=" | |||
flex flex-col | flex flex-col | |||
text-secondaryLight | text-secondaryLight | |||
p-4 | p-4 | |||
items-center | items-center | |||
justify-center | justify-center | |||
" | " | |||
> | > | |||
<i class="opacity-75 pb-2 material-icons">layers</i> | <img | |||
:src="`/images/states/${$colorMode.value}/add_group.svg`" | ||||
loading="lazy" | ||||
class=" | ||||
flex-col | ||||
my-4 | ||||
object-contain object-center | ||||
h-16 | ||||
w-16 | ||||
inline-flex | ||||
" | ||||
/> | ||||
<span class="text-center pb-4"> | <span class="text-center pb-4"> | |||
{{ $t("empty.members") }} | {{ $t("empty.members") }} | |||
</span> | </span> | |||
<ButtonSecondary | <ButtonSecondary | |||
:label="$t('add.new')" | svg="user-plus" | |||
filled | :label="$t('team.invite')" | |||
@click.native="addTeamMember" | @click.native=" | |||
() => { | ||||
emit('invite-team') | ||||
} | ||||
" | ||||
/> | /> | |||
</div> | </div> | |||
<div v-else> | ||||
<div | ||||
v-for="(member, index) in membersList" | ||||
:key="`member-${index}`" | ||||
class="divide-x divide-dividerLight flex" | ||||
> | ||||
<input | ||||
class="bg-transparent flex flex-1 py-2 px-4" | ||||
:placeholder="$t('team.email')" | ||||
:name="'param' + index" | ||||
:value="member.email" | ||||
readonly | ||||
/> | ||||
<span> | ||||
<tippy | ||||
ref="memberOptions" | ||||
interactive | ||||
trigger="click" | ||||
theme="popover" | ||||
arrow | ||||
> | ||||
<template #trigger> | ||||
<span class="select-wrapper"> | ||||
<input | ||||
class=" | ||||
bg-transparent | ||||
cursor-pointer | ||||
flex flex-1 | ||||
py-2 | ||||
px-4 | ||||
" | ||||
:placeholder="$t('team.permissions')" | ||||
:name="'value' + index" | ||||
:value=" | ||||
typeof member.role === 'string' | ||||
? member.role | ||||
: JSON.stringify(member.role) | ||||
" | ||||
readonly | ||||
/> | ||||
</span> | ||||
</template> | ||||
<SmartItem | ||||
label="OWNER" | ||||
@click.native=" | ||||
() => { | ||||
updateMemberRole(member.userID, 'OWNER') | ||||
memberOptions[index].tippy().hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
label="EDITOR" | ||||
@click.native=" | ||||
() => { | ||||
updateMemberRole(member.userID, 'EDITOR') | ||||
memberOptions[index].tippy().hide() | ||||
} | ||||
" | ||||
/> | ||||
<SmartItem | ||||
label="VIEWER" | ||||
@click.native=" | ||||
() => { | ||||
updateMemberRole(member.userID, 'VIEWER') | ||||
memberOptions[index].tippy().hide() | ||||
} | ||||
" | ||||
/> | ||||
</tippy> | ||||
</span> | ||||
<div class="flex"> | ||||
<ButtonSecondary | ||||
id="member" | ||||
v-tippy="{ theme: 'tooltip' }" | ||||
:title="$t('action.remove')" | ||||
svg="trash" | ||||
color="red" | ||||
@click.native="removeExistingTeamMember(member.userID)" | ||||
/> | ||||
</div> | ||||
</div> | ||||
</div> | ||||
</div> | ||||
<div | ||||
v-if="!teamDetails.loading && E.isLeft(teamDetails.data)" | ||||
class="flex flex-col items-center" | ||||
> | ||||
<i class="mb-4 material-icons">help_outline</i> | ||||
{{ $t("error.something_went_wrong") }} | ||||
</div> | </div> | |||
</div> | </div> | |||
</template> | </template> | |||
<template #footer> | <template #footer> | |||
<span> | <span> | |||
<ButtonPrimary :label="$t('action.save')" @click.native="saveTeam" /> | <ButtonPrimary :label="$t('action.save')" @click.native="saveTeam" /> | |||
<ButtonSecondary | <ButtonSecondary | |||
:label="$t('action.cancel')" | :label="$t('action.cancel')" | |||
@click.native="hideModal" | @click.native="hideModal" | |||
/> | /> | |||
</span> | </span> | |||
</template> | </template> | |||
</SmartModal> | </SmartModal> | |||
</template> | </template> | |||
<script> | <script setup lang="ts"> | |||
import cloneDeep from "lodash/cloneDeep" | import { | |||
import { defineComponent } from "@nuxtjs/composition-api" | computed, | |||
import * as teamUtils from "~/helpers/teams/utils" | ref, | |||
import TeamMemberAdapter from "~/helpers/teams/TeamMemberAdapter" | toRef, | |||
useContext, | ||||
export default defineComponent({ | watch, | |||
props: { | } from "@nuxtjs/composition-api" | |||
show: Boolean, | import * as E from "fp-ts/Either" | |||
editingTeam: { type: Object, default: () => {} }, | import { | |||
editingteamID: { type: String, default: null }, | GetTeamDocument, | |||
GetTeamQuery, | ||||
GetTeamQueryVariables, | ||||
TeamMemberAddedDocument, | ||||
TeamMemberRemovedDocument, | ||||
TeamMemberRole, | ||||
TeamMemberUpdatedDocument, | ||||
} from "~/helpers/backend/graphql" | ||||
import { | ||||
removeTeamMember, | ||||
renameTeam, | ||||
updateTeamMemberRole, | ||||
} from "~/helpers/backend/mutations/Team" | ||||
import { TeamNameCodec } from "~/helpers/backend/types/TeamName" | ||||
import { useGQLQuery } from "~/helpers/backend/GQLClient" | ||||
const emit = defineEmits<{ | ||||
(e: "hide-modal"): void | ||||
}>() | ||||
const memberOptions = ref<any | null>(null) | ||||
const props = defineProps<{ | ||||
show: boolean | ||||
editingTeam: { | ||||
name: string | ||||
} | ||||
editingTeamID: string | ||||
}>() | ||||
const { | ||||
$toast, | ||||
app: { i18n }, | ||||
} = useContext() | ||||
const t = i18n.t.bind(i18n) | ||||
const name = toRef(props.editingTeam, "name") | ||||
watch( | ||||
() => props.editingTeam.name, | ||||
(newName: string) => { | ||||
name.value = newName | ||||
} | ||||
) | ||||
watch( | ||||
() => props.editingTeamID, | ||||
(teamID: string) => { | ||||
teamDetails.execute({ teamID }) | ||||
} | ||||
) | ||||
const teamDetails = useGQLQuery<GetTeamQuery, GetTeamQueryVariables, "">({ | ||||
query: GetTeamDocument, | ||||
variables: { | ||||
teamID: props.editingTeamID, | ||||
}, | }, | |||
data() { | defer: true, | |||
return { | updateSubs: computed(() => { | |||
rename: null, | if (props.editingTeamID) { | |||
members: [], | return [ | |||
newMembers: [], | { | |||
membersAdapter: new TeamMemberAdapter(null), | key: 1, | |||
query: TeamMemberAddedDocument, | ||||
variables: { | ||||
teamID: props.editingTeamID, | ||||
}, | ||||
}, | ||||
{ | ||||
key: 2, | ||||
query: TeamMemberUpdatedDocument, | ||||
variables: { | ||||
teamID: props.editingTeamID, | ||||
}, | ||||
}, | ||||
{ | ||||
key: 3, | ||||
query: TeamMemberRemovedDocument, | ||||
variables: { | ||||
teamID: props.editingTeamID, | ||||
}, | ||||
}, | ||||
] | ||||
} else return [] | ||||
}), | ||||
}) | ||||
const roleUpdates = ref< | ||||
{ | ||||
userID: string | ||||
role: TeamMemberRole | ||||
}[] | ||||
>([]) | ||||
watch( | ||||
() => teamDetails, | ||||
() => { | ||||
if (teamDetails.loading) return | ||||
const data = teamDetails.data | ||||
if (E.isRight(data)) { | ||||
const members = data.right.team?.teamMembers ?? [] | ||||
// Remove deleted members | ||||
roleUpdates.value = roleUpdates.value.filter( | ||||
(update) => | ||||
members.findIndex((y) => y.user.uid === update.userID) !== -1 | ||||
) | ||||
} | } | |||
}, | } | |||
computed: { | ) | |||
editingTeamCopy() { | ||||
return this.editingTeam | const updateMemberRole = (userID: string, role: TeamMemberRole) => { | |||
}, | const updateIndex = roleUpdates.value.findIndex( | |||
name: { | (item) => item.userID === userID | |||
get() { | ) | |||
return this.editingTeam.name | if (updateIndex !== -1) { | |||
}, | // Role Update exists | |||
set(name) { | roleUpdates.value[updateIndex].role = role | |||
this.rename = name | } else { | |||
}, | // Role Update does not exist | |||
}, | roleUpdates.value.push({ | |||
}, | userID, | |||
watch: { | role, | |||
editingteamID(teamID) { | ||||
this.membersAdapter.changeTeamID(teamID) | ||||
}, | ||||
}, | ||||
mounted() { | ||||
this.membersAdapter.members$.subscribe((list) => { | ||||
this.members = cloneDeep(list) | ||||
}) | }) | |||
}, | } | |||
methods: { | } | |||
updateMemberRole(id, role) { | ||||
this.members[id].role = role | const membersList = computed(() => { | |||
this.$refs[`memberOptions-${id}`][0].tippy().hide() | if (teamDetails.loading) return [] | |||
}, | ||||
updateNewMemberRole(id, role) { | const data = teamDetails.data | |||
this.newMembers[id].value = role | ||||
this.$refs[`newMemberOptions-${id}`][0].tippy().hide() | if (E.isLeft(data)) return [] | |||
}, | ||||
addTeamMember() { | if (E.isRight(data)) { | |||
const member = { key: "", value: "" } | const members = (data.right.team?.teamMembers ?? []).map((member) => { | |||
this.newMembers.push(member) | const updatedRole = roleUpdates.value.find( | |||
}, | (update) => update.userID === member.user.uid | |||
removeExistingTeamMember(userID) { | ) | |||
teamUtils | ||||
.removeTeamMember(this.$apollo, userID, this.editingteamID) | return { | |||
.then(() => { | userID: member.user.uid, | |||
this.$toast.success(this.$t("team.member_removed"), { | email: member.user.email!, | |||
icon: "done", | role: updatedRole?.role ?? member.role, | |||
}) | ||||
this.hideModal() | ||||
}) | ||||
.catch((e) => { | ||||
this.$toast.error(this.$t("error.something_went_wrong"), { | ||||
icon: "error_outline", | ||||
}) | ||||
console.error(e) | ||||
}) | ||||
}, | ||||
removeTeamMember(index) { | ||||
this.newMembers.splice(index, 1) | ||||
}, | ||||
validateEmail(emailID) { | ||||
if ( | ||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.t | ||||
est( | ||||
emailID | ||||
) | ||||
) { | ||||
return true | ||||
} | } | |||
return false | }) | |||
}, | ||||
saveTeam() { | return members | |||
if ( | } | |||
this.$data.rename !== null && | ||||
this.$data.rename.replace(/\s/g, "").length < 6 | return [] | |||
) { | }) | |||
this.$toast.error(this.$t("team.name_length_insufficient"), { | ||||
icon: "error_outline", | const removeExistingTeamMember = async (userID: string) => { | |||
const removeTeamMemberResult = await removeTeamMember( | ||||
userID, | ||||
props.editingTeamID | ||||
)() | ||||
if (E.isLeft(removeTeamMemberResult)) { | ||||
$toast.error(t("error.something_went_wrong"), { | ||||
icon: "error", | ||||
}) | ||||
} else { | ||||
$toast.success(t("team.member_removed"), { | ||||
icon: "done", | ||||
}) | ||||
} | ||||
} | ||||
const saveTeam = async () => { | ||||
if (name.value !== "") { | ||||
if (TeamNameCodec.is(name.value)) { | ||||
const updateTeamNameResult = await renameTeam( | ||||
props.editingTeamID, | ||||
name.value | ||||
)() | ||||
if (E.isLeft(updateTeamNameResult)) { | ||||
$toast.error(t("error.something_went_wrong"), { | ||||
icon: "error", | ||||
}) | ||||
} else { | ||||
roleUpdates.value.forEach(async (update) => { | ||||
const updateMemberRoleResult = await updateTeamMemberRole( | ||||
update.userID, | ||||
props.editingTeamID, | ||||
update.role | ||||
)() | ||||
if (E.isLeft(updateMemberRoleResult)) { | ||||
$toast.error(t("error.something_went_wrong"), { | ||||
icon: "error", | ||||
}) | ||||
console.error(updateMemberRoleResult.left.error) | ||||
} | ||||
}) | }) | |||
return | ||||
} | } | |||
let invalidEmail = false | hideModal() | |||
this.$data.newMembers.forEach((element) => { | $toast.success(t("team.saved"), { | |||
if (!this.validateEmail(element.key)) { | icon: "done", | |||
this.$toast.error(this.$t("team.invalid_email_format"), { | ||||
icon: "error_outline", | ||||
}) | ||||
invalidEmail = true | ||||
} | ||||
}) | }) | |||
if (invalidEmail) return | } else { | |||
let invalidPermission = false | return $toast.error(t("team.name_length_insufficient"), { | |||
this.$data.newMembers.forEach((element) => { | icon: "error_outline", | |||
if (!element.value) { | ||||
this.$toast.error(this.$t("invalid_member_permission"), { | ||||
icon: "error_outline", | ||||
}) | ||||
invalidPermission = true | ||||
} | ||||
}) | }) | |||
if (invalidPermission) return | } | |||
this.$data.newMembers.forEach((element) => { | } else { | |||
// Call to the graphql mutation | return $toast.error(t("empty.team_name"), { | |||
teamUtils | icon: "error_outline", | |||
.addTeamMemberByEmail( | }) | |||
this.$apollo, | } | |||
element.value, | } | |||
element.key, | ||||
this.editingteamID | const hideModal = () => { | |||
) | emit("hide-modal") | |||
.then(() => { | } | |||
this.$toast.success(this.$t("team.saved"), { | ||||
icon: "done", | ||||
}) | ||||
}) | ||||
.catch((e) => { | ||||
this.$toast.error(e, { | ||||
icon: "error_outline", | ||||
}) | ||||
console.error(e) | ||||
}) | ||||
}) | ||||
this.members.forEach((element) => { | ||||
teamUtils | ||||
.updateTeamMemberRole( | ||||
this.$apollo, | ||||
element.user.uid, | ||||
element.role, | ||||
this.editingteamID | ||||
) | ||||
.then(() => { | ||||
this.$toast.success(this.$t("team.member_role_updated"), { | ||||
icon: "done", | ||||
}) | ||||
}) | ||||
.catch((e) => { | ||||
this.$toast.error(e, { | ||||
icon: "error_outline", | ||||
}) | ||||
console.error(e) | ||||
}) | ||||
}) | ||||
if (this.$data.rename !== null) { | ||||
const newName = | ||||
this.name === this.$data.rename ? this.name : this.$data.rename | ||||
if (!/\S/.test(newName)) | ||||
return this.$toast.error(this.$t("empty.team_name"), { | ||||
icon: "error_outline", | ||||
}) | ||||
// Call to the graphql mutation | ||||
if (this.name !== this.rename) | ||||
teamUtils | ||||
.renameTeam(this.$apollo, newName, this.editingteamID) | ||||
.then(() => { | ||||
this.$toast.success(this.$t("team.saved"), { | ||||
icon: "done", | ||||
}) | ||||
}) | ||||
.catch((e) => { | ||||
this.$toast.error(this.$t("error.something_went_wrong"), { | ||||
icon: "error_outline", | ||||
}) | ||||
console.error(e) | ||||
}) | ||||
} | ||||
this.hideModal() | ||||
}, | ||||
hideModal() { | ||||
this.rename = null | ||||
this.newMembers = [] | ||||
this.$emit("hide-modal") | ||||
}, | ||||
}, | ||||
}) | ||||
</script> | </script> | |||
End of changes. 17 change blocks. | ||||
323 lines changed or deleted | 353 lines changed or added |