feat(sh-admin): introducing advanced SMTP configurations and invite links to dashboard (#4087)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
b851d3003c
commit
1d1462df69
@@ -33,12 +33,21 @@
|
|||||||
},
|
},
|
||||||
"load_error": "Unable to load server configurations",
|
"load_error": "Unable to load server configurations",
|
||||||
"mail_configs": {
|
"mail_configs": {
|
||||||
"description": " Configure the smtp configurations",
|
|
||||||
"enable": "Email based authentication",
|
|
||||||
"smtp_url": "MAILER SMTP URL",
|
|
||||||
"address_from": "MAILER FROM ADDRESS",
|
"address_from": "MAILER FROM ADDRESS",
|
||||||
|
"custom_smtp_configs": "Use Custom SMTP Configurations",
|
||||||
|
"description": " Configure the smtp configurations",
|
||||||
|
"enable_email_auth": "Enable Email based authentication",
|
||||||
|
"enable_smtp": "Enable SMTP",
|
||||||
|
"host": "MAILER HOST",
|
||||||
|
"password": "MAILER PASSWORD",
|
||||||
|
"port": "MAILER PORT",
|
||||||
|
"secure": "MAILER SECURE",
|
||||||
|
"smtp_url": "MAILER SMTP URL",
|
||||||
|
"tls_reject_unauthorized": "TLS REJECT UNAUTHORIZED",
|
||||||
"title": "SMTP Configurations",
|
"title": "SMTP Configurations",
|
||||||
"update_failure": "Failed to update smtp configurations!!"
|
"toggle_failure": "Failed to toggle smtp!!",
|
||||||
|
"update_failure": "Failed to update smtp configurations!!",
|
||||||
|
"user": "MAILER USER"
|
||||||
},
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
"confirm_reset": "Hoppscotch server must restart to reflect the new changes. Confirm the reset of server configurations?",
|
"confirm_reset": "Hoppscotch server must restart to reflect the new changes. Confirm the reset of server configurations?",
|
||||||
@@ -210,6 +219,7 @@
|
|||||||
"admin_id": "Admin ID",
|
"admin_id": "Admin ID",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"confirm_team_deletion": "Confirm deletion of the workspace?",
|
"confirm_team_deletion": "Confirm deletion of the workspace?",
|
||||||
|
"copy": "Copy",
|
||||||
"create_team": "Create Workspace",
|
"create_team": "Create Workspace",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"delete_team": "Delete Workspace",
|
"delete_team": "Delete Workspace",
|
||||||
@@ -262,6 +272,7 @@
|
|||||||
"admin_id": "Admin ID",
|
"admin_id": "Admin ID",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"created_on": "Created On",
|
"created_on": "Created On",
|
||||||
|
"copy_link": "Copy Link",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete_user": "Delete User",
|
"delete_user": "Delete User",
|
||||||
|
|||||||
73
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
73
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
@@ -1,46 +1,45 @@
|
|||||||
// generated by unplugin-vue-components
|
// generated by unplugin-vue-components
|
||||||
// We suggest you to commit this file into source control
|
// We suggest you to commit this file into source control
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
import '@vue/runtime-core'
|
import '@vue/runtime-core';
|
||||||
|
|
||||||
export {}
|
export {};
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
AppHeader: typeof import('./components/app/Header.vue')['default'];
|
||||||
AppLogin: typeof import('./components/app/Login.vue')['default']
|
AppLogin: typeof import('./components/app/Login.vue')['default'];
|
||||||
AppLogout: typeof import('./components/app/Logout.vue')['default']
|
AppLogout: typeof import('./components/app/Logout.vue')['default'];
|
||||||
AppModal: typeof import('./components/app/Modal.vue')['default']
|
AppModal: typeof import('./components/app/Modal.vue')['default'];
|
||||||
AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
|
AppSidebar: typeof import('./components/app/Sidebar.vue')['default'];
|
||||||
AppToast: typeof import('./components/app/Toast.vue')['default']
|
AppToast: typeof import('./components/app/Toast.vue')['default'];
|
||||||
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']
|
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'];
|
||||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'];
|
||||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'];
|
||||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'];
|
||||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'];
|
||||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'];
|
||||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'];
|
||||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'];
|
||||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'];
|
||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'];
|
||||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
IconLucideInbox: typeof import('~icons/lucide/inbox')['default'];
|
||||||
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default']
|
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default'];
|
||||||
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default']
|
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default'];
|
||||||
SettingsDataSharing: typeof import('./components/settings/DataSharing.vue')['default']
|
SettingsDataSharing: typeof import('./components/settings/DataSharing.vue')['default'];
|
||||||
SettingsReset: typeof import('./components/settings/Reset.vue')['default']
|
SettingsReset: typeof import('./components/settings/Reset.vue')['default'];
|
||||||
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default']
|
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default'];
|
||||||
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default']
|
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default'];
|
||||||
SetupDataSharingAndNewsletter: typeof import('./components/setup/DataSharingAndNewsletter.vue')['default']
|
SetupDataSharingAndNewsletter: typeof import('./components/setup/DataSharingAndNewsletter.vue')['default'];
|
||||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
|
TeamsAdd: typeof import('./components/teams/Add.vue')['default'];
|
||||||
TeamsDetails: typeof import('./components/teams/Details.vue')['default']
|
TeamsDetails: typeof import('./components/teams/Details.vue')['default'];
|
||||||
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
|
TeamsInvite: typeof import('./components/teams/Invite.vue')['default'];
|
||||||
TeamsMembers: typeof import('./components/teams/Members.vue')['default']
|
TeamsMembers: typeof import('./components/teams/Members.vue')['default'];
|
||||||
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default']
|
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default'];
|
||||||
Tippy: typeof import('vue-tippy')['Tippy']
|
Tippy: typeof import('vue-tippy')['Tippy'];
|
||||||
UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default']
|
UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default'];
|
||||||
UsersDetails: typeof import('./components/users/Details.vue')['default']
|
UsersDetails: typeof import('./components/users/Details.vue')['default'];
|
||||||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
|
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'];
|
||||||
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default']
|
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default'];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
EnableAndDisableSsoDocument,
|
EnableAndDisableSsoDocument,
|
||||||
ResetInfraConfigsDocument,
|
ResetInfraConfigsDocument,
|
||||||
ToggleAnalyticsCollectionDocument,
|
ToggleAnalyticsCollectionDocument,
|
||||||
|
ToggleSmtpDocument,
|
||||||
UpdateInfraConfigsDocument,
|
UpdateInfraConfigsDocument,
|
||||||
} from '~/helpers/backend/graphql';
|
} from '~/helpers/backend/graphql';
|
||||||
import { ServerConfigs } from '~/helpers/configs';
|
import { ServerConfigs } from '~/helpers/configs';
|
||||||
@@ -52,6 +53,7 @@ const updateAllowedAuthProviderMutation = useMutation(
|
|||||||
const toggleDataSharingMutation = useMutation(
|
const toggleDataSharingMutation = useMutation(
|
||||||
ToggleAnalyticsCollectionDocument
|
ToggleAnalyticsCollectionDocument
|
||||||
);
|
);
|
||||||
|
const toggleSMTPMutation = useMutation(ToggleSmtpDocument);
|
||||||
|
|
||||||
// Mutation handlers
|
// Mutation handlers
|
||||||
const {
|
const {
|
||||||
@@ -59,6 +61,7 @@ const {
|
|||||||
updateAuthProvider,
|
updateAuthProvider,
|
||||||
resetInfraConfigs,
|
resetInfraConfigs,
|
||||||
updateDataSharingConfigs,
|
updateDataSharingConfigs,
|
||||||
|
toggleSMTPConfigs,
|
||||||
} = useConfigHandler(props.workingConfigs);
|
} = useConfigHandler(props.workingConfigs);
|
||||||
|
|
||||||
// Call relevant mutations on component mount and initiate server restart
|
// Call relevant mutations on component mount and initiate server restart
|
||||||
@@ -111,6 +114,12 @@ onMounted(async () => {
|
|||||||
if (!dataSharingResult) {
|
if (!dataSharingResult) {
|
||||||
return triggerComponentUnMount();
|
return triggerComponentUnMount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const smtpResult = await toggleSMTPConfigs(toggleSMTPMutation);
|
||||||
|
|
||||||
|
if (!smtpResult) {
|
||||||
|
return triggerComponentUnMount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
restart.value = true;
|
restart.value = true;
|
||||||
|
|||||||
@@ -22,30 +22,66 @@
|
|||||||
:on="smtpConfigs.enabled"
|
:on="smtpConfigs.enabled"
|
||||||
@change="smtpConfigs.enabled = !smtpConfigs.enabled"
|
@change="smtpConfigs.enabled = !smtpConfigs.enabled"
|
||||||
>
|
>
|
||||||
{{ t('configs.mail_configs.enable') }}
|
{{ t('configs.mail_configs.enable_smtp') }}
|
||||||
</HoppSmartToggle>
|
</HoppSmartToggle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="smtpConfigs.enabled" class="ml-12">
|
<div v-if="smtpConfigs.enabled" class="ml-12">
|
||||||
|
<div class="flex flex-col items-start gap-5">
|
||||||
|
<HoppSmartCheckbox
|
||||||
|
:on="smtpConfigs.fields.email_auth"
|
||||||
|
@change="
|
||||||
|
smtpConfigs.fields.email_auth = !smtpConfigs.fields.email_auth
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ t('configs.mail_configs.enable_email_auth') }}
|
||||||
|
</HoppSmartCheckbox>
|
||||||
|
|
||||||
|
<HoppSmartCheckbox
|
||||||
|
:on="smtpConfigs.fields.mailer_use_custom_configs"
|
||||||
|
:title="t('configs.mail_configs.custom_smtp_configs')"
|
||||||
|
@change="
|
||||||
|
smtpConfigs.fields.mailer_use_custom_configs =
|
||||||
|
!smtpConfigs.fields.mailer_use_custom_configs
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ t('configs.mail_configs.custom_smtp_configs') }}
|
||||||
|
</HoppSmartCheckbox>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="field in smtpConfigFields"
|
v-for="field in smtpConfigFields"
|
||||||
:key="field.key"
|
:key="field.key"
|
||||||
class="mt-5"
|
class="mt-5 ml-12"
|
||||||
>
|
>
|
||||||
<label>{{ field.name }}</label>
|
<div v-if="fieldCondition(field)">
|
||||||
<span class="flex max-w-lg">
|
<div
|
||||||
<HoppSmartInput
|
v-if="isCheckboxField(field)"
|
||||||
v-model="smtpConfigs.fields[field.key]"
|
class="flex flex-col items-start gap-5"
|
||||||
:type="isMasked(field.key) ? 'password' : 'text'"
|
>
|
||||||
:autofocus="false"
|
<HoppSmartCheckbox
|
||||||
class="!my-2 !bg-primaryLight flex-1"
|
:on="Boolean(smtpConfigs.fields[field.key])"
|
||||||
/>
|
@change="toggleCheckbox(field)"
|
||||||
<HoppButtonSecondary
|
>
|
||||||
:icon="isMasked(field.key) ? IconEye : IconEyeOff"
|
{{ field.name }}
|
||||||
class="bg-primaryLight h-9 mt-2"
|
</HoppSmartCheckbox>
|
||||||
@click="toggleMask(field.key)"
|
</div>
|
||||||
/>
|
<span v-else>
|
||||||
</span>
|
<label>{{ field.name }}</label>
|
||||||
|
<span class="flex max-w-lg">
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="smtpConfigs.fields[field.key]"
|
||||||
|
:type="isMasked(field.key) ? 'password' : 'text'"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:icon="isMasked(field.key) ? IconEye : IconEyeOff"
|
||||||
|
class="bg-primaryLight h-9 mt-2"
|
||||||
|
@click="toggleMask(field.key)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,13 +127,47 @@ type Field = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const smtpConfigFields = reactive<Field[]>([
|
const smtpConfigFields = reactive<Field[]>([
|
||||||
{ name: t('configs.mail_configs.smtp_url'), key: 'mailer_smtp_url' },
|
{
|
||||||
{ name: t('configs.mail_configs.address_from'), key: 'mailer_from_address' },
|
name: t('configs.mail_configs.smtp_url'),
|
||||||
|
key: 'mailer_smtp_url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('configs.mail_configs.address_from'),
|
||||||
|
key: 'mailer_from_address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('configs.mail_configs.host'),
|
||||||
|
key: 'mailer_smtp_host',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('configs.mail_configs.port'),
|
||||||
|
key: 'mailer_smtp_port',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('configs.mail_configs.user'),
|
||||||
|
key: 'mailer_smtp_user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('configs.mail_configs.password'),
|
||||||
|
key: 'mailer_smtp_password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('configs.mail_configs.secure'),
|
||||||
|
key: 'mailer_smtp_secure',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('configs.mail_configs.tls_reject_unauthorized'),
|
||||||
|
key: 'mailer_tls_reject_unauthorized',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const maskState = reactive<Record<string, boolean>>({
|
const maskState = reactive<Record<string, boolean>>({
|
||||||
mailer_smtp_url: true,
|
mailer_smtp_url: true,
|
||||||
mailer_from_address: true,
|
mailer_from_address: true,
|
||||||
|
mailer_smtp_host: true,
|
||||||
|
mailer_smtp_port: true,
|
||||||
|
mailer_smtp_user: true,
|
||||||
|
mailer_smtp_password: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleMask = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) => {
|
const toggleMask = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) => {
|
||||||
@@ -106,4 +176,35 @@ const toggleMask = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) => {
|
|||||||
|
|
||||||
const isMasked = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) =>
|
const isMasked = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) =>
|
||||||
maskState[fieldKey];
|
maskState[fieldKey];
|
||||||
|
|
||||||
|
const fieldCondition = (field: Field) => {
|
||||||
|
const advancedFields = [
|
||||||
|
'mailer_smtp_host',
|
||||||
|
'mailer_smtp_port',
|
||||||
|
'mailer_smtp_user',
|
||||||
|
'mailer_smtp_password',
|
||||||
|
'mailer_smtp_secure',
|
||||||
|
'mailer_tls_reject_unauthorized',
|
||||||
|
];
|
||||||
|
const basicFields = ['mailer_smtp_url'];
|
||||||
|
|
||||||
|
if (field.key === 'mailer_from_address') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smtpConfigs.value.fields.mailer_use_custom_configs) {
|
||||||
|
return (
|
||||||
|
!basicFields.includes(field.key) && advancedFields.includes(field.key)
|
||||||
|
);
|
||||||
|
} else return basicFields.includes(field.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCheckboxField = (field: Field) => {
|
||||||
|
const checkboxKeys = ['mailer_smtp_secure', 'mailer_tls_reject_unauthorized'];
|
||||||
|
return checkboxKeys.includes(field.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCheckbox = (field: Field) =>
|
||||||
|
((smtpConfigs.value.fields[field.key] as boolean) =
|
||||||
|
!smtpConfigs.value.fields[field.key]);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="border rounded divide-y divide-dividerLight border-divider my-8">
|
<div
|
||||||
|
class="border rounded divide-y divide-dividerLight border-divider mx-4 my-8"
|
||||||
|
>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-if="team && pendingInvites?.length === 0"
|
v-if="team && pendingInvites?.length === 0"
|
||||||
text="No pending invites"
|
text="No pending invites"
|
||||||
@@ -26,16 +28,22 @@
|
|||||||
:value="invitee.inviteeRole"
|
:value="invitee.inviteeRole"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
<div class="flex">
|
<HoppButtonSecondary
|
||||||
<HoppButtonSecondary
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
:title="t('teams.copy')"
|
||||||
:title="t('teams.remove')"
|
:icon="IconCopy"
|
||||||
:icon="IconTrash"
|
color="white"
|
||||||
color="red"
|
:loading="isLoadingIndex === index"
|
||||||
:loading="isLoadingIndex === index"
|
@click="copyInviteLink(invitee.id)"
|
||||||
@click="removeInvitee(invitee.id, index)"
|
/>
|
||||||
/>
|
<HoppButtonSecondary
|
||||||
</div>
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('teams.remove')"
|
||||||
|
:icon="IconTrash"
|
||||||
|
color="red"
|
||||||
|
:loading="isLoadingIndex === index"
|
||||||
|
@click="removeInvitee(invitee.id, index)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,6 +59,8 @@ import {
|
|||||||
RevokeTeamInvitationDocument,
|
RevokeTeamInvitationDocument,
|
||||||
TeamInfoQuery,
|
TeamInfoQuery,
|
||||||
} from '~/helpers/backend/graphql';
|
} from '~/helpers/backend/graphql';
|
||||||
|
import { copyToClipboard } from '~/helpers/utils/clipboard';
|
||||||
|
import IconCopy from '~icons/lucide/copy';
|
||||||
import IconTrash from '~icons/lucide/trash';
|
import IconTrash from '~icons/lucide/trash';
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -96,4 +106,11 @@ const removeInvitee = async (id: string, index: number) => {
|
|||||||
}
|
}
|
||||||
isLoadingIndex.value = null;
|
isLoadingIndex.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const baseURL = import.meta.env.VITE_BASE_URL ?? '';
|
||||||
|
|
||||||
|
const copyInviteLink = (inviteID: string) => {
|
||||||
|
copyToClipboard(`${baseURL}/join-team?id=${inviteID}`);
|
||||||
|
toast.success(t('state.copied_to_clipboard'));
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
v-model="email"
|
v-model="email"
|
||||||
:label="t('users.email_address')"
|
:label="t('users.email_address')"
|
||||||
input-styles="floating-input"
|
input-styles="floating-input"
|
||||||
@submit="sendInvite"
|
@submit="emit('send-invite', email)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:label="t('users.send_invite')"
|
:label="t('users.send_invite')"
|
||||||
@click="sendInvite"
|
@click="emit('send-invite', email)"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('users.cancel')"
|
:label="t('users.cancel')"
|
||||||
@@ -25,6 +25,12 @@
|
|||||||
@click="hideModal"
|
@click="hideModal"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('users.copy_link')"
|
||||||
|
outline
|
||||||
|
filled
|
||||||
|
@click="emit('copy-invite-link', email)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartModal>
|
</HoppSmartModal>
|
||||||
</template>
|
</template>
|
||||||
@@ -32,26 +38,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useI18n } from '~/composables/i18n';
|
import { useI18n } from '~/composables/i18n';
|
||||||
import { useToast } from '~/composables/toast';
|
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'hide-modal'): void;
|
(event: 'hide-modal'): void;
|
||||||
(event: 'send-invite', email: string): void;
|
(event: 'send-invite', email: string): void;
|
||||||
|
(event: 'copy-invite-link', email: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
|
|
||||||
const sendInvite = () => {
|
|
||||||
if (email.value.trim() === '') {
|
|
||||||
toast.error(t('users.valid_email'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit('send-invite', email.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
emit('hide-modal');
|
emit('hide-modal');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { AnyVariables, UseMutationResponse } from '@urql/vue';
|
import { AnyVariables, UseMutationResponse } from '@urql/vue';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { useI18n } from '~/composables/i18n';
|
import { useI18n } from '~/composables/i18n';
|
||||||
import {
|
import {
|
||||||
AllowedAuthProvidersDocument,
|
AllowedAuthProvidersDocument,
|
||||||
@@ -14,10 +13,12 @@ import {
|
|||||||
ResetInfraConfigsMutation,
|
ResetInfraConfigsMutation,
|
||||||
ServiceStatus,
|
ServiceStatus,
|
||||||
ToggleAnalyticsCollectionMutation,
|
ToggleAnalyticsCollectionMutation,
|
||||||
|
ToggleSmtpMutation,
|
||||||
UpdateInfraConfigsMutation,
|
UpdateInfraConfigsMutation,
|
||||||
} from '~/helpers/backend/graphql';
|
} from '~/helpers/backend/graphql';
|
||||||
import {
|
import {
|
||||||
ALL_CONFIGS,
|
ALL_CONFIGS,
|
||||||
|
CUSTOM_MAIL_CONFIGS,
|
||||||
ConfigSection,
|
ConfigSection,
|
||||||
ConfigTransform,
|
ConfigTransform,
|
||||||
GITHUB_CONFIGS,
|
GITHUB_CONFIGS,
|
||||||
@@ -114,10 +115,24 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||||||
},
|
},
|
||||||
mailConfigs: {
|
mailConfigs: {
|
||||||
name: 'email',
|
name: 'email',
|
||||||
enabled: allowedAuthProviders.value.includes(AuthProvider.Email),
|
enabled: getFieldValue(InfraConfigEnum.MailerSmtpEnable) === 'true',
|
||||||
fields: {
|
fields: {
|
||||||
|
email_auth: allowedAuthProviders.value.includes(AuthProvider.Email),
|
||||||
mailer_smtp_url: getFieldValue(InfraConfigEnum.MailerSmtpUrl),
|
mailer_smtp_url: getFieldValue(InfraConfigEnum.MailerSmtpUrl),
|
||||||
mailer_from_address: getFieldValue(InfraConfigEnum.MailerAddressFrom),
|
mailer_from_address: getFieldValue(InfraConfigEnum.MailerAddressFrom),
|
||||||
|
mailer_smtp_host: getFieldValue(InfraConfigEnum.MailerSmtpHost),
|
||||||
|
mailer_smtp_port: getFieldValue(InfraConfigEnum.MailerSmtpPort),
|
||||||
|
mailer_smtp_user: getFieldValue(InfraConfigEnum.MailerSmtpUser),
|
||||||
|
mailer_smtp_password: getFieldValue(
|
||||||
|
InfraConfigEnum.MailerSmtpPassword
|
||||||
|
),
|
||||||
|
mailer_smtp_secure:
|
||||||
|
getFieldValue(InfraConfigEnum.MailerSmtpSecure) === 'true',
|
||||||
|
mailer_tls_reject_unauthorized:
|
||||||
|
getFieldValue(InfraConfigEnum.MailerTlsRejectUnauthorized) ===
|
||||||
|
'true',
|
||||||
|
mailer_use_custom_configs:
|
||||||
|
getFieldValue(InfraConfigEnum.MailerUseCustomConfigs) === 'true',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dataSharingConfigs: {
|
dataSharingConfigs: {
|
||||||
@@ -133,11 +148,19 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||||||
workingConfigs.value = cloneDeep(currentConfigs.value);
|
workingConfigs.value = cloneDeep(currentConfigs.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if custom mail config is enabled
|
||||||
|
const isCustomMailConfigEnabled =
|
||||||
|
updatedConfigs?.mailConfigs.fields.mailer_use_custom_configs;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Check if any of the config fields are empty
|
Check if any of the config fields are empty
|
||||||
*/
|
*/
|
||||||
|
const isFieldEmpty = (field: string | boolean) => {
|
||||||
const isFieldEmpty = (field: string) => field.trim() === '';
|
if (typeof field === 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return field.trim() === '';
|
||||||
|
};
|
||||||
|
|
||||||
const AreAnyConfigFieldsEmpty = (config: ServerConfigs): boolean => {
|
const AreAnyConfigFieldsEmpty = (config: ServerConfigs): boolean => {
|
||||||
const sections: Array<ConfigSection> = [
|
const sections: Array<ConfigSection> = [
|
||||||
@@ -147,12 +170,56 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||||||
config.mailConfigs,
|
config.mailConfigs,
|
||||||
];
|
];
|
||||||
|
|
||||||
return sections.some(
|
const hasSectionWithEmptyFields = sections.some((section) => {
|
||||||
(section) =>
|
if (
|
||||||
|
section.name === 'email' &&
|
||||||
|
!section.fields.mailer_use_custom_configs
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
section.enabled &&
|
||||||
|
Object.entries(section.fields).some(
|
||||||
|
([key, value]) =>
|
||||||
|
isFieldEmpty(value) &&
|
||||||
|
key !== 'mailer_smtp_host' &&
|
||||||
|
key !== 'mailer_smtp_port' &&
|
||||||
|
key !== 'mailer_smtp_user' &&
|
||||||
|
key !== 'mailer_smtp_password'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
section.enabled && Object.values(section.fields).some(isFieldEmpty)
|
section.enabled && Object.values(section.fields).some(isFieldEmpty)
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasSectionWithEmptyFields;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extract the mail config fields (excluding the custom mail config fields)
|
||||||
|
const mailConfigFields = Object.fromEntries(
|
||||||
|
Object.entries(updatedConfigs?.mailConfigs.fields ?? {}).filter(([key]) => {
|
||||||
|
if (isCustomMailConfigEnabled) {
|
||||||
|
return MAIL_CONFIGS.some(
|
||||||
|
(x) =>
|
||||||
|
x.key === key &&
|
||||||
|
key !== 'mailer_smtp_url' &&
|
||||||
|
key !== 'mailer_smtp_enabled'
|
||||||
|
);
|
||||||
|
} else
|
||||||
|
return MAIL_CONFIGS.some(
|
||||||
|
(x) => x.key === key && key !== 'mailer_smtp_enabled'
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extract the custom mail config fields
|
||||||
|
const customMailConfigFields = Object.fromEntries(
|
||||||
|
Object.entries(updatedConfigs?.mailConfigs.fields ?? {}).filter(([key]) =>
|
||||||
|
CUSTOM_MAIL_CONFIGS.some((x) => x.key === key)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Transforming the working configs back into the format required by the mutations
|
// Transforming the working configs back into the format required by the mutations
|
||||||
const transformInfraConfigs = () => {
|
const transformInfraConfigs = () => {
|
||||||
const updatedWorkingConfigs: ConfigTransform[] = [
|
const updatedWorkingConfigs: ConfigTransform[] = [
|
||||||
@@ -174,7 +241,12 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||||||
{
|
{
|
||||||
config: MAIL_CONFIGS,
|
config: MAIL_CONFIGS,
|
||||||
enabled: updatedConfigs?.mailConfigs.enabled,
|
enabled: updatedConfigs?.mailConfigs.enabled,
|
||||||
fields: updatedConfigs?.mailConfigs.fields,
|
fields: mailConfigFields,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: CUSTOM_MAIL_CONFIGS,
|
||||||
|
enabled: isCustomMailConfigEnabled,
|
||||||
|
fields: customMailConfigFields,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -182,7 +254,10 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||||||
|
|
||||||
updatedWorkingConfigs.forEach(({ config, enabled, fields }) => {
|
updatedWorkingConfigs.forEach(({ config, enabled, fields }) => {
|
||||||
config.forEach(({ name, key }) => {
|
config.forEach(({ name, key }) => {
|
||||||
if (enabled && fields) {
|
if (name === 'MAILER_SMTP_ENABLE') return;
|
||||||
|
else if (isCustomMailConfigEnabled && name === 'MAILER_SMTP_URL')
|
||||||
|
return;
|
||||||
|
else if (enabled && fields) {
|
||||||
const value =
|
const value =
|
||||||
typeof fields === 'string' ? fields : String(fields[key]);
|
typeof fields === 'string' ? fields : String(fields[key]);
|
||||||
transformedConfigs.push({ name, value });
|
transformedConfigs.push({ name, value });
|
||||||
@@ -239,7 +314,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provider: AuthProvider.Email,
|
provider: AuthProvider.Email,
|
||||||
status: updatedConfigs?.mailConfigs.enabled
|
status: updatedConfigs?.mailConfigs.fields.email_auth
|
||||||
? ServiceStatus.Enable
|
? ServiceStatus.Enable
|
||||||
: ServiceStatus.Disable,
|
: ServiceStatus.Disable,
|
||||||
},
|
},
|
||||||
@@ -281,6 +356,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||||||
'configs.reset.failure'
|
'configs.reset.failure'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Toggle Data Sharing
|
||||||
const updateDataSharingConfigs = (
|
const updateDataSharingConfigs = (
|
||||||
toggleDataSharingMutation: UseMutationResponse<ToggleAnalyticsCollectionMutation>
|
toggleDataSharingMutation: UseMutationResponse<ToggleAnalyticsCollectionMutation>
|
||||||
) =>
|
) =>
|
||||||
@@ -294,11 +370,26 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
|||||||
'configs.data_sharing.update_failure'
|
'configs.data_sharing.update_failure'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Toggle SMTP
|
||||||
|
const toggleSMTPConfigs = (
|
||||||
|
toggleSMTP: UseMutationResponse<ToggleSmtpMutation>
|
||||||
|
) =>
|
||||||
|
executeMutation(
|
||||||
|
toggleSMTP,
|
||||||
|
{
|
||||||
|
status: updatedConfigs?.mailConfigs.enabled
|
||||||
|
? ServiceStatus.Enable
|
||||||
|
: ServiceStatus.Disable,
|
||||||
|
},
|
||||||
|
'configs.mail_configs.toggle_failure'
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentConfigs,
|
currentConfigs,
|
||||||
workingConfigs,
|
workingConfigs,
|
||||||
updateAuthProvider,
|
updateAuthProvider,
|
||||||
updateDataSharingConfigs,
|
updateDataSharingConfigs,
|
||||||
|
toggleSMTPConfigs,
|
||||||
updateInfraConfigs,
|
updateInfraConfigs,
|
||||||
resetInfraConfigs,
|
resetInfraConfigs,
|
||||||
fetchingInfraConfigs,
|
fetchingInfraConfigs,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mutation ToggleSMTP($status: ServiceStatus!) {
|
||||||
|
toggleSMTP(status: $status)
|
||||||
|
}
|
||||||
@@ -41,8 +41,16 @@ export type ServerConfigs = {
|
|||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
fields: {
|
fields: {
|
||||||
|
email_auth: boolean;
|
||||||
mailer_smtp_url: string;
|
mailer_smtp_url: string;
|
||||||
mailer_from_address: string;
|
mailer_from_address: string;
|
||||||
|
mailer_smtp_host: string;
|
||||||
|
mailer_smtp_port: string;
|
||||||
|
mailer_smtp_user: string;
|
||||||
|
mailer_smtp_password: string;
|
||||||
|
mailer_smtp_secure: boolean;
|
||||||
|
mailer_tls_reject_unauthorized: boolean;
|
||||||
|
mailer_use_custom_configs: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,8 +72,9 @@ export type ConfigTransform = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigSection = {
|
export type ConfigSection = {
|
||||||
|
name: SsoAuthProviders | string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
fields: Record<string, string>;
|
fields: Record<string, string | boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
@@ -143,6 +152,41 @@ export const MAIL_CONFIGS: Config[] = [
|
|||||||
name: InfraConfigEnum.MailerAddressFrom,
|
name: InfraConfigEnum.MailerAddressFrom,
|
||||||
key: 'mailer_from_address',
|
key: 'mailer_from_address',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MailerSmtpEnable,
|
||||||
|
key: 'mailer_smtp_enabled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MailerUseCustomConfigs,
|
||||||
|
key: 'mailer_use_custom_configs',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CUSTOM_MAIL_CONFIGS: Config[] = [
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MailerSmtpHost,
|
||||||
|
key: 'mailer_smtp_host',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MailerSmtpPort,
|
||||||
|
key: 'mailer_smtp_port',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MailerSmtpUser,
|
||||||
|
key: 'mailer_smtp_user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MailerSmtpPassword,
|
||||||
|
key: 'mailer_smtp_password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MailerSmtpSecure,
|
||||||
|
key: 'mailer_smtp_secure',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MailerTlsRejectUnauthorized,
|
||||||
|
key: 'mailer_tls_reject_unauthorized',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const DATA_SHARING_CONFIGS: Omit<Config, 'key'>[] = [
|
const DATA_SHARING_CONFIGS: Omit<Config, 'key'>[] = [
|
||||||
@@ -156,5 +200,6 @@ export const ALL_CONFIGS = [
|
|||||||
MICROSOFT_CONFIGS,
|
MICROSOFT_CONFIGS,
|
||||||
GITHUB_CONFIGS,
|
GITHUB_CONFIGS,
|
||||||
MAIL_CONFIGS,
|
MAIL_CONFIGS,
|
||||||
|
CUSTOM_MAIL_CONFIGS,
|
||||||
DATA_SHARING_CONFIGS,
|
DATA_SHARING_CONFIGS,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -207,6 +207,7 @@
|
|||||||
v-if="showInviteUserModal"
|
v-if="showInviteUserModal"
|
||||||
@hide-modal="showInviteUserModal = false"
|
@hide-modal="showInviteUserModal = false"
|
||||||
@send-invite="sendInvite"
|
@send-invite="sendInvite"
|
||||||
|
@copy-invite-link="copyInviteLink"
|
||||||
/>
|
/>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmUsersToAdmin"
|
:show="confirmUsersToAdmin"
|
||||||
@@ -262,6 +263,7 @@ import {
|
|||||||
} from '~/helpers/backend/graphql';
|
} from '~/helpers/backend/graphql';
|
||||||
import { getCompiledErrorMessage } from '~/helpers/errors';
|
import { getCompiledErrorMessage } from '~/helpers/errors';
|
||||||
import { handleUserDeletion } from '~/helpers/userManagement';
|
import { handleUserDeletion } from '~/helpers/userManagement';
|
||||||
|
import { copyToClipboard } from '~/helpers/utils/clipboard';
|
||||||
import IconCheck from '~icons/lucide/check';
|
import IconCheck from '~icons/lucide/check';
|
||||||
import IconLeft from '~icons/lucide/chevron-left';
|
import IconLeft from '~icons/lucide/chevron-left';
|
||||||
import IconRight from '~icons/lucide/chevron-right';
|
import IconRight from '~icons/lucide/chevron-right';
|
||||||
@@ -451,11 +453,16 @@ const showInviteUserModal = ref(false);
|
|||||||
const sendInvitation = useMutation(InviteNewUserDocument);
|
const sendInvitation = useMutation(InviteNewUserDocument);
|
||||||
|
|
||||||
const sendInvite = async (email: string) => {
|
const sendInvite = async (email: string) => {
|
||||||
if (!email.trim()) {
|
const trimmedEmail = email.trim();
|
||||||
|
if (!trimmedEmail) {
|
||||||
toast.error(t('state.invalid_email'));
|
toast.error(t('state.invalid_email'));
|
||||||
return;
|
return false;
|
||||||
|
} else if (trimmedEmail === '') {
|
||||||
|
toast.error(t('users.valid_email'));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
const variables = { inviteeEmail: email.trim() };
|
|
||||||
|
const variables = { inviteeEmail: trimmedEmail };
|
||||||
const result = await sendInvitation.executeMutation(variables);
|
const result = await sendInvitation.executeMutation(variables);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
const { message } = result.error;
|
const { message } = result.error;
|
||||||
@@ -464,12 +471,23 @@ const sendInvite = async (email: string) => {
|
|||||||
compiledErrorMessage
|
compiledErrorMessage
|
||||||
? toast.error(t(compiledErrorMessage))
|
? toast.error(t(compiledErrorMessage))
|
||||||
: toast.error(t('state.email_failure'));
|
: toast.error(t('state.email_failure'));
|
||||||
|
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
toast.success(t('state.email_success'));
|
toast.success(t('state.email_success'));
|
||||||
showInviteUserModal.value = false;
|
showInviteUserModal.value = false;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyInviteLink = async (email: string) => {
|
||||||
|
const result = await sendInvite(email);
|
||||||
|
if (!result) return;
|
||||||
|
const baseURL = import.meta.env.VITE_BASE_URL ?? '';
|
||||||
|
copyToClipboard(baseURL);
|
||||||
|
toast.success(t('state.copied_to_clipboard'));
|
||||||
|
};
|
||||||
|
|
||||||
// Make Multiple Users Admin
|
// Make Multiple Users Admin
|
||||||
const confirmUsersToAdmin = ref(false);
|
const confirmUsersToAdmin = ref(false);
|
||||||
const usersToAdminUID = ref<string | null>(null);
|
const usersToAdminUID = ref<string | null>(null);
|
||||||
|
|||||||
@@ -6,9 +6,18 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold text-accentContrast pt-6 pb-4">
|
<div class="flex justify-between items-center">
|
||||||
{{ t('users.pending_invites') }}
|
<h3 class="text-lg font-bold text-accentContrast pt-6 pb-4">
|
||||||
</h3>
|
{{ t('users.pending_invites') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('users.copy_link')"
|
||||||
|
outline
|
||||||
|
filled
|
||||||
|
@click="copyInviteLink"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="relative py-2 overflow-x-auto">
|
<div class="relative py-2 overflow-x-auto">
|
||||||
@@ -107,6 +116,7 @@ import { computed, ref } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useI18n } from '~/composables/i18n';
|
import { useI18n } from '~/composables/i18n';
|
||||||
import { useToast } from '~/composables/toast';
|
import { useToast } from '~/composables/toast';
|
||||||
|
import { copyToClipboard } from '~/helpers/utils/clipboard';
|
||||||
import IconTrash from '~icons/lucide/trash';
|
import IconTrash from '~icons/lucide/trash';
|
||||||
import {
|
import {
|
||||||
InvitedUsersDocument,
|
InvitedUsersDocument,
|
||||||
@@ -185,4 +195,11 @@ const deleteInvitation = async (email: string | null) => {
|
|||||||
confirmDeletion.value = false;
|
confirmDeletion.value = false;
|
||||||
inviteToBeDeleted.value = null;
|
inviteToBeDeleted.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const baseURL = import.meta.env.VITE_BASE_URL ?? '';
|
||||||
|
|
||||||
|
const copyInviteLink = () => {
|
||||||
|
copyToClipboard(baseURL);
|
||||||
|
toast.success(t('state.copied_to_clipboard'));
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user