refactor: consolidated admin dashboard improvements (#3790)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
committed by
GitHub
parent
aab76f1358
commit
3d6adcc39d
@@ -72,13 +72,14 @@ onMounted(async () => {
|
||||
success = await resetInfraConfigs(resetInfraConfigsMutation);
|
||||
if (!success) return;
|
||||
} else {
|
||||
const infraResult = await updateInfraConfigs(updateInfraConfigsMutation);
|
||||
|
||||
if (!infraResult) return;
|
||||
|
||||
const authResult = await updateAuthProvider(
|
||||
updateAllowedAuthProviderMutation
|
||||
);
|
||||
const infraResult = await updateInfraConfigs(updateInfraConfigsMutation);
|
||||
|
||||
success = authResult && infraResult;
|
||||
if (!success) return;
|
||||
if (!authResult) return;
|
||||
}
|
||||
|
||||
restart.value = true;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('teams.create_team')"
|
||||
@close="$emit('hide-modal')"
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-4 relative">
|
||||
@@ -16,12 +16,16 @@
|
||||
class="flex-1 !flex"
|
||||
:source="allUsersEmail"
|
||||
:spellcheck="true"
|
||||
placeholder=""
|
||||
:placeholder="t('teams.email')"
|
||||
@input="(email: string) => getOwnerEmail(email)"
|
||||
/>
|
||||
</div>
|
||||
<label for="teamName"> {{ t('teams.name') }} </label>
|
||||
<HoppSmartInput v-model="teamName" placeholder="" class="!my-2" />
|
||||
<HoppSmartInput
|
||||
v-model="teamName"
|
||||
:placeholder="t('teams.name')"
|
||||
class="!my-2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
@@ -44,11 +48,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect } from 'vue';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -82,11 +85,11 @@ const getOwnerEmail = (email: string) => (ownerEmail.value = email);
|
||||
|
||||
const createTeam = () => {
|
||||
if (teamName.value.trim() === '') {
|
||||
toast.error(`${t('teams.valid_name')}`);
|
||||
toast.error(t('teams.valid_name'));
|
||||
return;
|
||||
}
|
||||
if (ownerEmail.value.trim() === '') {
|
||||
toast.error(`${t('teams.valid_owner_email')}`);
|
||||
toast.error(t('teams.valid_owner_email'));
|
||||
return;
|
||||
}
|
||||
emit('create-team', teamName.value, ownerEmail.value);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div v-if="team" class="flex flex-col">
|
||||
<div class="flex flex-col space-y-8">
|
||||
<div v-if="team.id" class="flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<label class="text-accentContrast" for="username"
|
||||
>{{ t('teams.id') }}
|
||||
</label>
|
||||
@@ -10,33 +10,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="teamName" class="flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<label class="text-accentContrast" for="teamname"
|
||||
>{{ t('teams.name') }}
|
||||
</label>
|
||||
<div
|
||||
class="flex bg-divider rounded-md items-stretch flex-1 border border-divider"
|
||||
:class="{
|
||||
'!border-accent': showRenameInput,
|
||||
'!border-accent': isTeamNameBeingEdited,
|
||||
}"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="newTeamName"
|
||||
v-model="updatedTeamName"
|
||||
styles="bg-transparent flex-1 rounded-md !rounded-r-none disabled:select-none border-r-0 disabled:cursor-default disabled:opacity-50"
|
||||
placeholder="Team Name"
|
||||
:disabled="!showRenameInput"
|
||||
:disabled="!isTeamNameBeingEdited"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonPrimary
|
||||
class="!rounded-l-none"
|
||||
filled
|
||||
:icon="showRenameInput ? IconSave : IconEdit"
|
||||
:icon="isTeamNameBeingEdited ? IconSave : IconEdit"
|
||||
:label="
|
||||
showRenameInput
|
||||
isTeamNameBeingEdited
|
||||
? `${t('teams.rename')}`
|
||||
: `${t('teams.edit')}`
|
||||
"
|
||||
@click="handleNameEdit()"
|
||||
@click="handleTeamNameEdit"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
@@ -58,7 +58,7 @@
|
||||
class="!bg-red-600 !hover:opacity-80"
|
||||
filled
|
||||
:label="t('teams.delete_team')"
|
||||
@click="team && $emit('delete-team', team.id)"
|
||||
@click="emit('delete-team', team.id)"
|
||||
:icon="IconTrash"
|
||||
/>
|
||||
</div>
|
||||
@@ -66,45 +66,89 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { TeamInfoQuery } from '~/helpers/backend/graphql';
|
||||
import { RenameTeamDocument, TeamInfoQuery } from '~/helpers/backend/graphql';
|
||||
import IconEdit from '~icons/lucide/edit';
|
||||
import IconSave from '~icons/lucide/save';
|
||||
import IconTrash from '~icons/lucide/trash-2';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const props = defineProps<{
|
||||
team: TeamInfoQuery['infra']['teamInfo'];
|
||||
teamName: string;
|
||||
showRenameInput: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'delete-team', teamID: string): void;
|
||||
(event: 'rename-team', teamName: string): void;
|
||||
(event: 'update:showRenameInput', showRenameInput: boolean): void;
|
||||
(event: 'update:team', team: TeamInfoQuery['infra']['teamInfo']): void;
|
||||
(event: 'delete-team', teamId: string): void;
|
||||
}>();
|
||||
|
||||
const newTeamName = ref(props.teamName);
|
||||
const team = useVModel(props, 'team', emit);
|
||||
|
||||
const handleNameEdit = () => {
|
||||
if (props.showRenameInput) {
|
||||
renameTeam();
|
||||
// Contains the actual team name
|
||||
const teamName = computed({
|
||||
get: () => team.value.name,
|
||||
set: (value) => {
|
||||
team.value.name = value;
|
||||
},
|
||||
});
|
||||
|
||||
// Contains the stored team name from the actual team name before being edited
|
||||
const currentTeamName = ref('');
|
||||
|
||||
// Contains the team name that is being edited
|
||||
const updatedTeamName = computed({
|
||||
get: () => currentTeamName.value,
|
||||
set: (value) => {
|
||||
currentTeamName.value = value;
|
||||
},
|
||||
});
|
||||
|
||||
// Set the current team name to the actual team name
|
||||
onMounted(() => {
|
||||
currentTeamName.value = teamName.value;
|
||||
});
|
||||
|
||||
// Rename the team name
|
||||
const isTeamNameBeingEdited = ref(false);
|
||||
const teamRename = useMutation(RenameTeamDocument);
|
||||
|
||||
const handleTeamNameEdit = () => {
|
||||
if (isTeamNameBeingEdited.value) {
|
||||
// If the team name is not changed, then return control
|
||||
if (teamName.value !== updatedTeamName.value) {
|
||||
renameTeamName();
|
||||
} else isTeamNameBeingEdited.value = false;
|
||||
} else {
|
||||
emit('update:showRenameInput', true);
|
||||
isTeamNameBeingEdited.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const renameTeam = () => {
|
||||
if (newTeamName.value.trim() === '') {
|
||||
toast.error(`${t('teams.empty_name')}`);
|
||||
const renameTeamName = async () => {
|
||||
if (updatedTeamName.value.trim() === '') {
|
||||
toast.error(t('teams.empty_name'));
|
||||
return;
|
||||
}
|
||||
emit('rename-team', newTeamName.value);
|
||||
|
||||
if (updatedTeamName.value.length < 6) {
|
||||
toast.error(t('state.team_name_too_short'));
|
||||
return;
|
||||
}
|
||||
|
||||
const variables = { uid: team.value.id, name: updatedTeamName.value };
|
||||
const result = await teamRename.executeMutation(variables);
|
||||
|
||||
if (result.error) {
|
||||
toast.error(t('state.rename_team_failure'));
|
||||
} else {
|
||||
isTeamNameBeingEdited.value = false;
|
||||
toast.success(t('state.rename_team_success'));
|
||||
teamName.value = updatedTeamName.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<template>
|
||||
<HoppSmartModal v-if="show" dialog title="Add Member" @close="hideModal">
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('teams.add_member')"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="addingUserToTeam" class="flex items-center justify-center p-4">
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
<div v-else class="flex flex-col">
|
||||
<div class="flex items-center justify-between flex-1 pt-4">
|
||||
<label for="memberList" class="p-4"> Add members </label>
|
||||
<label for="memberList" class="p-4">
|
||||
{{ t('teams.add_members') }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
label="Add new"
|
||||
:label="t('teams.add_new')"
|
||||
filled
|
||||
@click="addNewMember"
|
||||
/>
|
||||
@@ -23,8 +30,8 @@
|
||||
class="flex divide-x divide-dividerLight"
|
||||
>
|
||||
<HoppSmartAutoComplete
|
||||
v-model="member.key"
|
||||
placeholder="Email"
|
||||
:value="member.key"
|
||||
:placeholder="t('state.email')"
|
||||
:source="allUsersEmail"
|
||||
:name="'member' + index"
|
||||
:spellcheck="true"
|
||||
@@ -44,7 +51,7 @@
|
||||
<HoppSmartSelectWrapper>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-2 bg-transparent cursor-pointer"
|
||||
placeholder="Permissions"
|
||||
:placeholder="t('teams.permissions')"
|
||||
:name="'value' + index"
|
||||
:value="member.value"
|
||||
readonly
|
||||
@@ -58,7 +65,7 @@
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
label="OWNER"
|
||||
:label="t('teams.owner')"
|
||||
:icon="
|
||||
member.value === 'OWNER' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -71,7 +78,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="EDITOR"
|
||||
:label="t('teams.editor')"
|
||||
:icon="
|
||||
member.value === 'EDITOR' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -84,7 +91,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="VIEWER"
|
||||
:label="t('teams.viewer')"
|
||||
:icon="
|
||||
member.value === 'VIEWER' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -104,7 +111,7 @@
|
||||
<HoppButtonSecondary
|
||||
id="member"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
title="Remove"
|
||||
:title="t('teams.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@click="removeNewMember(index)"
|
||||
@@ -113,13 +120,13 @@
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="newMembersList.length === 0"
|
||||
:src="`/images/states/dark/add_group.svg`"
|
||||
alt="No invites"
|
||||
text="No invites"
|
||||
:src="addGroupImagePath"
|
||||
:alt="t('teams.no_members')"
|
||||
:text="t('teams.no_members')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
label="Add new"
|
||||
:label="t('teams.add_new')"
|
||||
filled
|
||||
@click="addNewMember"
|
||||
/>
|
||||
@@ -136,11 +143,12 @@
|
||||
<icon-lucide-help-circle
|
||||
class="mr-2 text-secondaryLight svg-icons"
|
||||
/>
|
||||
Roles
|
||||
|
||||
{{ t('teams.roles') }}
|
||||
</span>
|
||||
<p>
|
||||
<span class="text-secondaryLight">
|
||||
Roles are used to control access to the shared collections.
|
||||
{{ t('teams.roles_description') }}
|
||||
</span>
|
||||
</p>
|
||||
<ul class="mt-4 space-y-4">
|
||||
@@ -148,31 +156,30 @@
|
||||
<span
|
||||
class="w-1/4 font-semibold uppercase truncate text-secondaryDark max-w-[4rem]"
|
||||
>
|
||||
Owner
|
||||
{{ t('teams.owner') }}
|
||||
</span>
|
||||
<span class="flex flex-1">
|
||||
Owners can add, edit, and delete requests, collections and team
|
||||
members.
|
||||
{{ t('teams.owner_description') }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<span
|
||||
class="w-1/4 font-semibold uppercase truncate text-secondaryDark max-w-[4rem]"
|
||||
>
|
||||
Editor
|
||||
{{ t('teams.editor') }}
|
||||
</span>
|
||||
<span class="flex flex-1">
|
||||
Editors can add, edit, and delete requests.
|
||||
{{ t('teams.editor_description') }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<span
|
||||
class="w-1/4 font-semibold uppercase truncate text-secondaryDark max-w-[4rem]"
|
||||
>
|
||||
Viewer
|
||||
{{ t('teams.viewer') }}
|
||||
</span>
|
||||
<span class="flex flex-1">
|
||||
Viewers can only view and use requests.
|
||||
{{ t('teams.viewer_description') }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -183,41 +190,61 @@
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
label="Add Member"
|
||||
:label="t('teams.add_member')"
|
||||
outline
|
||||
@click="addUserasTeamMember"
|
||||
/>
|
||||
<HoppButtonSecondary label="Cancel" outline filled @click="hideModal" />
|
||||
<HoppButtonSecondary
|
||||
:label="t('teams.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useMutation, useQuery } from '@urql/vue';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { Email, EmailCodec } from '~/helpers/Email';
|
||||
import IconCircle from '~icons/lucide/circle';
|
||||
import IconCircleDot from '~icons/lucide/circle-dot';
|
||||
import IconPlus from '~icons/lucide/plus';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import {
|
||||
AddUserToTeamByAdminDocument,
|
||||
TeamMemberRole,
|
||||
MetricsDocument,
|
||||
TeamMemberRole,
|
||||
UsersListDocument,
|
||||
} from '../../helpers/backend/graphql';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useMutation, useQuery } from '@urql/vue';
|
||||
import { Email, EmailCodec } from '~/helpers/Email';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import IconPlus from '~icons/lucide/plus';
|
||||
import IconCircleDot from '~icons/lucide/circle-dot';
|
||||
import IconCircle from '~icons/lucide/circle';
|
||||
import { computed } from 'vue';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
const tippyActions = ref<any | null>(null);
|
||||
|
||||
// Get Users List
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
editingTeamID: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'hide-modal'): void;
|
||||
(e: 'member'): void;
|
||||
}>();
|
||||
|
||||
const addGroupImagePath = `${
|
||||
import.meta.env.VITE_ADMIN_URL
|
||||
}/assets/images/add_group.svg`;
|
||||
|
||||
// Get Users List to extract email ids of all users
|
||||
const { data } = useQuery({ query: MetricsDocument });
|
||||
const usersPerPage = computed(() => data.value?.infra.usersCount || 10000);
|
||||
|
||||
@@ -231,21 +258,6 @@ const { list: usersList } = usePagedQuery(
|
||||
|
||||
const allUsersEmail = computed(() => usersList.value.map((user) => user.email));
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null);
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
editingTeamID: { type: String, default: null },
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'hide-modal'): void;
|
||||
(e: 'member'): void;
|
||||
}>();
|
||||
|
||||
const newMembersList = ref<Array<{ key: string; value: TeamMemberRole }>>([
|
||||
{
|
||||
key: '',
|
||||
@@ -264,12 +276,13 @@ const updateNewMemberRole = (index: number, role: TeamMemberRole) => {
|
||||
newMembersList.value[index].value = role;
|
||||
};
|
||||
|
||||
const removeNewMember = (id: number) => {
|
||||
newMembersList.value.splice(id, 1);
|
||||
const removeNewMember = (index: number) => {
|
||||
newMembersList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const addingUserToTeam = ref<boolean>(false);
|
||||
|
||||
// Checks if the member invites are of valid email format and then adds the users to the team
|
||||
const addUserasTeamMember = async () => {
|
||||
addingUserToTeam.value = true;
|
||||
|
||||
@@ -293,7 +306,7 @@ const addUserasTeamMember = async () => {
|
||||
|
||||
if (O.isNone(validationResult)) {
|
||||
// Error handling for no validation
|
||||
toast.error(`${t('users.invalid_user')}`);
|
||||
toast.error(t('users.invalid_user'));
|
||||
addingUserToTeam.value = false;
|
||||
return;
|
||||
}
|
||||
@@ -320,20 +333,18 @@ const addUserToTeam = async (
|
||||
) => {
|
||||
const variables = { userEmail: email, role: userRole, teamID: teamID };
|
||||
|
||||
const res = await addUserToTeamMutation
|
||||
.executeMutation(variables)
|
||||
.then((result) => {
|
||||
if (result.error) {
|
||||
if (result.error.toString() == '[GraphQL] user/not_found') {
|
||||
toast.error(`${t('state.user_not_found')}`);
|
||||
} else {
|
||||
toast.error(`${t('state.add_user_failure')}`);
|
||||
}
|
||||
} else {
|
||||
toast.success(`${t('state.add_user_success')}`);
|
||||
emit('member');
|
||||
}
|
||||
});
|
||||
return res;
|
||||
const result = await addUserToTeamMutation.executeMutation(variables);
|
||||
|
||||
if (result.error) {
|
||||
if (result.error.toString() == '[GraphQL] user/not_found') {
|
||||
toast.error(t('state.user_not_found'));
|
||||
} else {
|
||||
toast.error(t('state.add_user_failure'));
|
||||
}
|
||||
} else {
|
||||
toast.success(t('state.add_user_success'));
|
||||
emit('member');
|
||||
}
|
||||
return result;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="border rounded border-divider my-8">
|
||||
<HoppSmartPlaceholder
|
||||
v-if="team?.teamMembers?.length === 0"
|
||||
text="No members in this team. Add members to this team to collaborate"
|
||||
:text="t('teams.no_members')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
@@ -35,7 +35,7 @@
|
||||
>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-3 bg-transparent"
|
||||
placeholder="Email"
|
||||
:placeholder="t('teams.email_title')"
|
||||
:name="'param' + index"
|
||||
:value="member.email"
|
||||
readonly
|
||||
@@ -50,7 +50,7 @@
|
||||
<span class="relative">
|
||||
<input
|
||||
class="flex flex-1 px-4 py-3 bg-transparent cursor-pointer"
|
||||
placeholder="Permissions"
|
||||
:placeholder="t('teams.permissions')"
|
||||
:name="'value' + index"
|
||||
:value="member.role"
|
||||
readonly
|
||||
@@ -69,7 +69,7 @@
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
label="OWNER"
|
||||
:label="t('teams.owner')"
|
||||
:icon="
|
||||
member.role === 'OWNER' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -82,7 +82,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="EDITOR"
|
||||
:label="t('teams.editor')"
|
||||
:icon="
|
||||
member.role === 'EDITOR' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -98,7 +98,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="VIEWER"
|
||||
:label="t('teams.viewer')"
|
||||
:icon="
|
||||
member.role === 'VIEWER' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -131,7 +131,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!fetching && !team" class="flex flex-col items-center">
|
||||
<div v-if="!team" class="flex flex-col items-center">
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ t('teams.error') }}
|
||||
</div>
|
||||
@@ -139,13 +139,15 @@
|
||||
|
||||
<div class="flex">
|
||||
<HoppButtonPrimary
|
||||
:label="t('teams.save')"
|
||||
v-if="areRolesUpdated"
|
||||
:label="t('teams.save_changes')"
|
||||
outline
|
||||
@click="saveUpdatedTeam"
|
||||
/>
|
||||
</div>
|
||||
<TeamsInvite
|
||||
:show="showInvite"
|
||||
:team="team"
|
||||
:editingTeamID="route.params.id.toString()"
|
||||
@member="updateMembers"
|
||||
@hide-modal="
|
||||
@@ -158,102 +160,101 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 IconChevronDown from '~icons/lucide/chevron-down';
|
||||
import { useClientHandle, useMutation } from '@urql/vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useClientHandler } from '~/composables/useClientHandler';
|
||||
import IconChevronDown from '~icons/lucide/chevron-down';
|
||||
import IconCircle from '~icons/lucide/circle';
|
||||
import IconCircleDot from '~icons/lucide/circle-dot';
|
||||
import IconUserMinus from '~icons/lucide/user-minus';
|
||||
import IconUserPlus from '~icons/lucide/user-plus';
|
||||
import {
|
||||
ChangeUserRoleInTeamByAdminDocument,
|
||||
TeamInfoDocument,
|
||||
TeamMemberRole,
|
||||
RemoveUserFromTeamByAdminDocument,
|
||||
TeamInfoDocument,
|
||||
TeamInfoQuery,
|
||||
TeamMemberRole,
|
||||
} from '../../helpers/backend/graphql';
|
||||
import { HoppButtonPrimary, HoppButtonSecondary } from '@hoppscotch/ui';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
const route = useRoute();
|
||||
|
||||
const props = defineProps<{
|
||||
team: TeamInfoQuery['infra']['teamInfo'];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update-team'): void;
|
||||
(event: 'update:team', team: TeamInfoQuery['infra']['teamInfo']): void;
|
||||
}>();
|
||||
|
||||
const teamDetails = useVModel(props, 'team', emit);
|
||||
|
||||
// Used to Invoke the Invite Members Modal
|
||||
const showInvite = ref(false);
|
||||
|
||||
// Get Team Details
|
||||
const team = ref<TeamInfoQuery['infra']['teamInfo'] | undefined>();
|
||||
const fetching = ref(true);
|
||||
const route = useRoute();
|
||||
const { client } = useClientHandle();
|
||||
|
||||
const getTeamInfo = async () => {
|
||||
fetching.value = true;
|
||||
const result = await client
|
||||
.query(TeamInfoDocument, { teamID: route.params.id.toString() })
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
return toast.error(`${t('teams.load_info_error')}`);
|
||||
const { fetchData: getTeamInfo, data: teamInfo } = useClientHandler(
|
||||
TeamInfoDocument,
|
||||
{
|
||||
teamID: route.params.id.toString(),
|
||||
}
|
||||
if (result.data?.infra.teamInfo) {
|
||||
team.value = result.data.infra.teamInfo;
|
||||
}
|
||||
fetching.value = false;
|
||||
};
|
||||
);
|
||||
|
||||
onMounted(async () => await getTeamInfo());
|
||||
onUnmounted(() => emit('update-team'));
|
||||
onMounted(async () => {
|
||||
await getTeamInfo();
|
||||
});
|
||||
|
||||
const team = computed(() => teamInfo.value?.infra.teamInfo);
|
||||
|
||||
// Update members tab after a change in the members list or member roles
|
||||
const updateMembers = () => {
|
||||
getTeamInfo();
|
||||
emit('update-team');
|
||||
const updateMembers = async () => {
|
||||
if (!team.value) return;
|
||||
await getTeamInfo();
|
||||
teamDetails.value = team.value;
|
||||
};
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null);
|
||||
|
||||
const roleUpdates = ref<
|
||||
// Roles of the members in the team
|
||||
const currentMemberRoles = ref<
|
||||
{
|
||||
userID: string;
|
||||
role: TeamMemberRole;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
watch(
|
||||
() => team.value,
|
||||
(teamDetails) => {
|
||||
const members = teamDetails?.teamMembers ?? [];
|
||||
// Roles of the members in the team after the updates but before saving
|
||||
const updatedMemberRoles = ref<
|
||||
{
|
||||
userID: string;
|
||||
role: TeamMemberRole;
|
||||
}[]
|
||||
>(cloneDeep(currentMemberRoles.value));
|
||||
|
||||
// Remove deleted members
|
||||
roleUpdates.value = roleUpdates.value.filter(
|
||||
(update) =>
|
||||
members.findIndex(
|
||||
(y: { user: { uid: string } }) => y.user.uid === update.userID
|
||||
) !== -1
|
||||
);
|
||||
}
|
||||
// Check if the roles of the members have been updated
|
||||
const areRolesUpdated = computed(() =>
|
||||
currentMemberRoles.value && updatedMemberRoles.value
|
||||
? !isEqual(currentMemberRoles.value, updatedMemberRoles.value)
|
||||
: false
|
||||
);
|
||||
|
||||
// Update the role of the member selected in the UI
|
||||
const updateMemberRole = (userID: string, role: TeamMemberRole) => {
|
||||
const updateIndex = roleUpdates.value.findIndex(
|
||||
const updateIndex = updatedMemberRoles.value.findIndex(
|
||||
(item) => item.userID === userID
|
||||
);
|
||||
if (updateIndex !== -1) {
|
||||
// Role Update exists
|
||||
roleUpdates.value[updateIndex].role = role;
|
||||
updatedMemberRoles.value[updateIndex].role = role;
|
||||
} else {
|
||||
// Role Update does not exist
|
||||
roleUpdates.value.push({
|
||||
updatedMemberRoles.value.push({
|
||||
userID,
|
||||
role,
|
||||
});
|
||||
@@ -264,7 +265,7 @@ const updateMemberRole = (userID: string, role: TeamMemberRole) => {
|
||||
const membersList = computed(() => {
|
||||
if (!team.value) return [];
|
||||
const members = (team.value.teamMembers ?? []).map((member) => {
|
||||
const updatedRole = roleUpdates.value.find(
|
||||
const updatedRole = updatedMemberRoles.value.find(
|
||||
(update) => update.userID === member.user.uid
|
||||
);
|
||||
|
||||
@@ -299,19 +300,31 @@ const isLoading = ref(false);
|
||||
|
||||
const saveUpdatedTeam = async () => {
|
||||
isLoading.value = true;
|
||||
roleUpdates.value.forEach(async (update) => {
|
||||
|
||||
const isOwnerPresent = membersList.value.some(
|
||||
(member) => member.role === TeamMemberRole.Owner
|
||||
);
|
||||
|
||||
if (!isOwnerPresent) {
|
||||
toast.error(t('state.owner_not_present'));
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
updatedMemberRoles.value.forEach(async (update) => {
|
||||
if (!team.value) return;
|
||||
|
||||
const updateMemberRoleResult = await changeUserRoleInTeam(
|
||||
update.userID,
|
||||
team.value.id,
|
||||
update.role
|
||||
);
|
||||
if (updateMemberRoleResult.error) {
|
||||
toast.error(`${t('state.role_update_failed')}`);
|
||||
roleUpdates.value = [];
|
||||
toast.error(t('state.role_update_failed'));
|
||||
} else {
|
||||
toast.success(`${t('state.role_update_success')}`);
|
||||
roleUpdates.value = [];
|
||||
toast.success(t('state.role_update_success'));
|
||||
currentMemberRoles.value = updatedMemberRoles.value;
|
||||
updatedMemberRoles.value = cloneDeep(currentMemberRoles.value);
|
||||
}
|
||||
isLoading.value = false;
|
||||
});
|
||||
@@ -340,14 +353,14 @@ const removeExistingTeamMember = async (userID: string, index: number) => {
|
||||
team.value.id
|
||||
)();
|
||||
if (removeTeamMemberResult.error) {
|
||||
toast.error(`${t('state.remove_member_failure')}`);
|
||||
toast.error(t('state.remove_member_failure'));
|
||||
} else {
|
||||
team.value.teamMembers = team.value.teamMembers?.filter(
|
||||
(member: any) => member.user.uid !== userID
|
||||
);
|
||||
toast.success(`${t('state.remove_member_success')}`);
|
||||
teamDetails.value = team.value;
|
||||
toast.success(t('state.remove_member_success'));
|
||||
}
|
||||
isLoadingIndex.value = null;
|
||||
emit('update-team');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,103 +1,76 @@
|
||||
<template>
|
||||
<div class="border rounded divide-y divide-dividerLight border-divider my-8">
|
||||
<div v-if="fetching" class="flex items-center justify-center p-4">
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="team" class="divide-y divide-dividerLight">
|
||||
<div
|
||||
v-for="(invitee, index) in pendingInvites"
|
||||
:key="`invitee-${index}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
>
|
||||
<input
|
||||
v-if="invitee"
|
||||
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
|
||||
placeholder="Email"
|
||||
:name="'param' + index"
|
||||
:value="invitee.inviteeEmail"
|
||||
readonly
|
||||
<HoppSmartPlaceholder
|
||||
v-if="team && pendingInvites?.length === 0"
|
||||
text="No pending invites"
|
||||
/>
|
||||
|
||||
<div v-else class="divide-y divide-dividerLight">
|
||||
<div
|
||||
v-for="(invitee, index) in pendingInvites"
|
||||
:key="`invitee-${index}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
>
|
||||
<input
|
||||
v-if="invitee"
|
||||
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
|
||||
:placeholder="t('teams.email_title')"
|
||||
:name="'param' + index"
|
||||
:value="invitee.inviteeEmail"
|
||||
readonly
|
||||
/>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
|
||||
:placeholder="t('teams.permission')"
|
||||
:name="'value' + index"
|
||||
:value="invitee.inviteeRole"
|
||||
readonly
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('teams.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
:loading="isLoadingIndex === index"
|
||||
@click="removeInvitee(invitee.id, index)"
|
||||
/>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
|
||||
placeholder="Permissions"
|
||||
:name="'value' + index"
|
||||
:value="invitee.inviteeRole"
|
||||
readonly
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('teams.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
:loading="isLoadingIndex === index"
|
||||
@click="removeInvitee(invitee.id, index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="team && pendingInvites?.length === 0"
|
||||
text="No pending invites"
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="!fetching && error" class="flex flex-col items-center p-4">
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ t('error.something_went_wrong') }}
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import { useMutation, useClientHandle } from '@urql/vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import {
|
||||
RevokeTeamInvitationDocument,
|
||||
TeamInfoDocument,
|
||||
TeamInfoQuery,
|
||||
} from '~/helpers/backend/graphql';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// Get details of the team
|
||||
const fetching = ref(true);
|
||||
const error = ref(false);
|
||||
const { client } = useClientHandle();
|
||||
const route = useRoute();
|
||||
const team = ref<TeamInfoQuery['infra']['teamInfo'] | undefined>();
|
||||
const pendingInvites = ref<
|
||||
TeamInfoQuery['infra']['teamInfo']['teamInvitations'] | undefined
|
||||
>();
|
||||
const props = defineProps<{
|
||||
team: TeamInfoQuery['infra']['teamInfo'];
|
||||
}>();
|
||||
|
||||
const getTeamInfo = async () => {
|
||||
fetching.value = true;
|
||||
const result = await client
|
||||
.query(TeamInfoDocument, { teamID: route.params.id.toString() })
|
||||
.toPromise();
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:team', team: TeamInfoQuery['infra']['teamInfo']): void;
|
||||
}>();
|
||||
|
||||
if (result.error) {
|
||||
error.value = true;
|
||||
return toast.error(`${t('teams.load_info_error')}`);
|
||||
}
|
||||
|
||||
if (result.data?.infra.teamInfo) {
|
||||
team.value = result.data.infra.teamInfo;
|
||||
pendingInvites.value = team.value.teamInvitations;
|
||||
}
|
||||
fetching.value = false;
|
||||
};
|
||||
|
||||
onMounted(async () => await getTeamInfo());
|
||||
const team = useVModel(props, 'team', emit);
|
||||
const pendingInvites = computed({
|
||||
get: () => team.value?.teamInvitations,
|
||||
set: (value) => {
|
||||
team.value.teamInvitations = value;
|
||||
},
|
||||
});
|
||||
|
||||
// Remove Invitation
|
||||
const isLoadingIndex = ref<null | number>(null);
|
||||
@@ -110,7 +83,7 @@ const removeInvitee = async (id: string, index: number) => {
|
||||
isLoadingIndex.value = index;
|
||||
const result = await revokeTeamInvitation(id);
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.remove_invitee_failure')}`);
|
||||
toast.error(t('state.remove_invitee_failure'));
|
||||
} else {
|
||||
if (pendingInvites.value) {
|
||||
pendingInvites.value = pendingInvites.value.filter(
|
||||
@@ -118,7 +91,7 @@ const removeInvitee = async (id: string, index: number) => {
|
||||
return invite.id !== id;
|
||||
}
|
||||
);
|
||||
toast.success(`${t('state.remove_invitee_success')}`);
|
||||
toast.success(t('state.remove_invitee_success'));
|
||||
}
|
||||
}
|
||||
isLoadingIndex.value = null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="rounded-md">
|
||||
<div class="grid gap-6 mt-4">
|
||||
<div class="grid gap-6">
|
||||
<div
|
||||
class="relative"
|
||||
:class="
|
||||
@@ -71,18 +71,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
|
||||
import { UserInfoQuery } from '~/helpers/backend/graphql';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import IconUserCheck from '~icons/lucide/user-check';
|
||||
import IconUserMinus from '~icons/lucide/user-minus';
|
||||
|
||||
import { UserInfoQuery } from '~/helpers/backend/graphql';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('users.invite_user')"
|
||||
@close="$emit('hide-modal')"
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
@@ -16,7 +16,6 @@
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('users.send_invite')"
|
||||
:loading="loadingState"
|
||||
@click="sendInvite"
|
||||
/>
|
||||
<HoppButtonSecondary label="Cancel" outline filled @click="hideModal" />
|
||||
@@ -27,21 +26,18 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
loadingState: boolean;
|
||||
}>(),
|
||||
{
|
||||
show: false,
|
||||
loadingState: false,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -54,7 +50,7 @@ const email = ref('');
|
||||
|
||||
const sendInvite = () => {
|
||||
if (email.value.trim() === '') {
|
||||
toast.error(`${t('users.valid_email')}`);
|
||||
toast.error(t('users.valid_email'));
|
||||
return;
|
||||
}
|
||||
emit('send-invite', email.value);
|
||||
|
||||
@@ -1,116 +1,116 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="px-4">
|
||||
<div v-if="fetching" class="flex justify-center">
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
|
||||
<div v-else-if="error">{{ t('shared_requests.load_list_error') }}</div>
|
||||
|
||||
<div v-else-if="sharedRequests.length === 0" class="ml-3 mt-5 text-lg">
|
||||
<div v-else-if="sharedRequests.length === 0" class="mt-5">
|
||||
{{ t('users.no_shared_requests') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-10">
|
||||
<HoppSmartTable :list="sharedRequests">
|
||||
<template #head>
|
||||
<tr
|
||||
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
|
||||
>
|
||||
<th class="px-6 py-2">{{ t('shared_requests.id') }}</th>
|
||||
<th class="px-6 py-2 w-30">{{ t('shared_requests.url') }}</th>
|
||||
<th class="px-6 py-2">{{ t('shared_requests.created_on') }}</th>
|
||||
<!-- Empty Heading for the Action Button -->
|
||||
<th class="px-6 py-2 text-center">Actions</th>
|
||||
</tr>
|
||||
</template>
|
||||
<template #body="{ list: sharedRequests }">
|
||||
<tr
|
||||
v-for="request in sharedRequests"
|
||||
:key="request.id"
|
||||
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
|
||||
>
|
||||
<td class="flex py-4 px-7 max-w-50">
|
||||
<span class="truncate">
|
||||
{{ request.id }}
|
||||
</span>
|
||||
</td>
|
||||
<HoppSmartTable v-else class="mt-8" :list="sharedRequests">
|
||||
<template #head>
|
||||
<tr
|
||||
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
|
||||
>
|
||||
<th class="px-6 py-2">{{ t('shared_requests.id') }}</th>
|
||||
<th class="px-6 py-2 w-30">{{ t('shared_requests.url') }}</th>
|
||||
<th class="px-6 py-2">{{ t('shared_requests.created_on') }}</th>
|
||||
<!-- Empty Heading for the Action Button -->
|
||||
<th class="px-6 py-2 text-center">
|
||||
{{ t('shared_requests.action') }}
|
||||
</th>
|
||||
</tr>
|
||||
</template>
|
||||
<template #body="{ list: sharedRequests }">
|
||||
<tr
|
||||
v-for="request in sharedRequests"
|
||||
:key="request.id"
|
||||
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
|
||||
>
|
||||
<td class="flex py-4 px-7 max-w-50">
|
||||
<span class="truncate">
|
||||
{{ request.id }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="py-4 px-7 w-96">
|
||||
{{ sharedRequestURL(request.request) }}
|
||||
</td>
|
||||
<td class="py-4 px-7 w-96">
|
||||
{{ sharedRequestURL(request.request) }}
|
||||
</td>
|
||||
|
||||
<td class="py-2 px-7">
|
||||
{{ getCreatedDate(request.createdOn) }}
|
||||
<div class="text-gray-400 text-tiny">
|
||||
{{ getCreatedTime(request.createdOn) }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 px-7">
|
||||
{{ getCreatedDate(request.createdOn) }}
|
||||
<div class="text-gray-400 text-tiny">
|
||||
{{ getCreatedTime(request.createdOn) }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="flex justify-center">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('shared_requests.open_request')"
|
||||
:to="`${shortcodeBaseURL}/r/${request.id}`"
|
||||
:blank="true"
|
||||
:icon="IconExternalLink"
|
||||
class="px-3 text-emerald-500 hover:text-accent"
|
||||
/>
|
||||
<td class="flex justify-center">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('shared_requests.open_request')"
|
||||
:to="`${shortcodeBaseURL}/r/${request.id}`"
|
||||
:blank="true"
|
||||
:icon="IconExternalLink"
|
||||
class="px-3 text-emerald-500 hover:text-accent"
|
||||
/>
|
||||
|
||||
<UiAutoResetIcon
|
||||
:title="t('shared_requests.copy')"
|
||||
:icon="{ default: IconCopy, temporary: IconCheck }"
|
||||
@click="copySharedRequest(request.id)"
|
||||
/>
|
||||
<UiAutoResetIcon
|
||||
:title="t('shared_requests.copy')"
|
||||
:icon="{ default: IconCopy, temporary: IconCheck }"
|
||||
@click="copySharedRequest(request.id)"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('shared_requests.delete')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
class="px-3"
|
||||
@click="deleteSharedRequest(request.id)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</HoppSmartTable>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('shared_requests.delete')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
class="px-3"
|
||||
@click="deleteSharedRequest(request.id)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</HoppSmartTable>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div
|
||||
v-if="hasNextPage && sharedRequests.length >= sharedRequestsPerPage"
|
||||
class="flex items-center w-28 px-3 py-2 mt-5 mx-auto font-semibold text-secondaryDark bg-divider hover:bg-dividerDark rounded-3xl cursor-pointer"
|
||||
@click="fetchNextSharedRequests"
|
||||
>
|
||||
<span class="mr-2">{{ t('shared_requests.show_more') }}</span>
|
||||
<icon-lucide-chevron-down />
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div
|
||||
v-if="hasNextPage && sharedRequests.length >= sharedRequestsPerPage"
|
||||
class="flex items-center w-28 px-3 py-2 mt-5 mx-auto font-semibold text-secondaryDark bg-divider hover:bg-dividerDark rounded-3xl cursor-pointer"
|
||||
@click="fetchNextSharedRequests"
|
||||
>
|
||||
<span class="mr-2">{{ t('shared_requests.show_more') }}</span>
|
||||
<icon-lucide-chevron-down />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmDeletion"
|
||||
:title="t('shared_requests.confirm_request_deletion')"
|
||||
@hide-modal="confirmDeletion = false"
|
||||
@resolve="deleteSharedRequestMutation(deleteSharedRequestID)"
|
||||
/>
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmDeletion"
|
||||
:title="t('shared_requests.confirm_request_deletion')"
|
||||
@hide-modal="confirmDeletion = false"
|
||||
@resolve="deleteSharedRequestMutation(deleteSharedRequestID)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { format } from 'date-fns';
|
||||
import { ref } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import {
|
||||
SharedRequestsDocument,
|
||||
RevokeShortcodeByAdminDocument,
|
||||
} from '../../helpers/backend/graphql';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { copyToClipboard } from '~/helpers/utils/clipboard';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import IconCopy from '~icons/lucide/copy';
|
||||
import IconCheck from '~icons/lucide/check';
|
||||
import IconCopy from '~icons/lucide/copy';
|
||||
import IconExternalLink from '~icons/lucide/external-link';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import {
|
||||
RevokeShortcodeByAdminDocument,
|
||||
SharedRequestsDocument,
|
||||
} from '~/helpers/backend/graphql';
|
||||
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
@@ -154,7 +154,7 @@ const shortcodeBaseURL =
|
||||
// Copy Shared Request to Clipboard
|
||||
const copySharedRequest = (requestID: string) => {
|
||||
copyToClipboard(`${shortcodeBaseURL}/r/${requestID}`);
|
||||
toast.success(`${t('state.copied_to_clipboard')}`);
|
||||
toast.success(t('state.copied_to_clipboard'));
|
||||
};
|
||||
|
||||
// Shared Request Deletion
|
||||
@@ -170,19 +170,19 @@ const deleteSharedRequest = (id: string) => {
|
||||
const deleteSharedRequestMutation = async (id: string | null) => {
|
||||
if (!id) {
|
||||
confirmDeletion.value = false;
|
||||
toast.error(`${t('state.delete_request_failure')}`);
|
||||
toast.error(t('state.delete_request_failure'));
|
||||
return;
|
||||
}
|
||||
const variables = { codeID: id };
|
||||
await sharedRequestDeletion.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.delete_request_failure')}`);
|
||||
toast.error(t('state.delete_request_failure'));
|
||||
} else {
|
||||
sharedRequests.value = sharedRequests.value.filter(
|
||||
(request) => request.id !== id
|
||||
);
|
||||
refetch();
|
||||
toast.success(`${t('state.delete_request_success')}`);
|
||||
toast.success(t('state.delete_request_success'));
|
||||
}
|
||||
});
|
||||
confirmDeletion.value = false;
|
||||
|
||||
Reference in New Issue
Block a user