mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 04:02:15 +01:00
add member merge frontend modal
This commit is contained in:
committed by
Constantin Graf
parent
ab263e725f
commit
e5ec11af44
@@ -138,6 +138,8 @@ class MemberController extends Controller
|
|||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
|
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
|
||||||
* @throws \Throwable
|
* @throws \Throwable
|
||||||
|
*
|
||||||
|
* @operationId mergeMember
|
||||||
*/
|
*/
|
||||||
public function mergeInto(Organization $organization, Member $member, MemberMergeIntoRequest $request): JsonResponse
|
public function mergeInto(Organization $organization, Member $member, MemberMergeIntoRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'members:invite-placeholder',
|
'members:invite-placeholder',
|
||||||
'members:change-ownership',
|
'members:change-ownership',
|
||||||
'members:make-placeholder',
|
'members:make-placeholder',
|
||||||
|
'members:merge-into',
|
||||||
'members:update',
|
'members:update',
|
||||||
'members:delete',
|
'members:delete',
|
||||||
'billing',
|
'billing',
|
||||||
@@ -174,6 +175,7 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'members:view',
|
'members:view',
|
||||||
'members:update',
|
'members:update',
|
||||||
'members:invite-placeholder',
|
'members:invite-placeholder',
|
||||||
|
'members:merge-into',
|
||||||
'reports:view',
|
'reports:view',
|
||||||
'reports:create',
|
'reports:create',
|
||||||
'reports:update',
|
'reports:update',
|
||||||
|
|||||||
109
resources/js/Components/Common/Member/MemberMergeModal.vue
Normal file
109
resources/js/Components/Common/Member/MemberMergeModal.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
|
import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import {api, type Member} from '@/packages/api/src';
|
||||||
|
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||||
|
import MemberCombobox from "@/Components/Common/Member/MemberCombobox.vue";
|
||||||
|
import {UserIcon, ArrowRightIcon} from "@heroicons/vue/24/solid";
|
||||||
|
import {Badge} from "@/packages/ui/src";
|
||||||
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
|
import {getCurrentOrganizationId} from "@/utils/useUser";
|
||||||
|
import {useNotificationsStore} from "@/utils/notification";
|
||||||
|
const { handleApiRequestNotifications, addNotification } = useNotificationsStore();
|
||||||
|
|
||||||
|
const show = defineModel('show', { default: false });
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
member: Member;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const newMember = ref<string>('');
|
||||||
|
|
||||||
|
const mergeMember = useMutation({
|
||||||
|
mutationFn: async (newMemberId: string) => {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (organizationId === null) {
|
||||||
|
throw new Error('No current organization id - create report');
|
||||||
|
}
|
||||||
|
return await api.mergeMember({
|
||||||
|
memberId: newMemberId,
|
||||||
|
}, {
|
||||||
|
params: {
|
||||||
|
organization: organizationId,
|
||||||
|
member: props.member.id
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
const newMemberId = newMember.value;
|
||||||
|
if(newMemberId !== ''){
|
||||||
|
saving.value = true;
|
||||||
|
await handleApiRequestNotifications(
|
||||||
|
() =>
|
||||||
|
mergeMember.mutateAsync(newMemberId),
|
||||||
|
'Members successfully merged!',
|
||||||
|
'There was an error merging the members.',
|
||||||
|
() => {
|
||||||
|
show.value = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
addNotification(
|
||||||
|
'error',
|
||||||
|
'Please select a member to merge into.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<span> Merge Member </span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<p>Merging the user <strong>{{ member.name }} </strong> into another one will transfer all time entries to the new user. <strong>This cannot be reverted!</strong></p>
|
||||||
|
<div class="py-5 flex flex-col md:flex-row gap-6 items-center">
|
||||||
|
<div class="flex-1">
|
||||||
|
<Badge class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary font-normal cursor py-1.5">
|
||||||
|
<UserIcon class="relative z-10 w-4 text-muted"></UserIcon>
|
||||||
|
<div class="flex-1 font-medium truncate">
|
||||||
|
{{ member.name }}
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ArrowRightIcon class="relative z-10 w-4 text-muted"></ArrowRightIcon>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<MemberCombobox
|
||||||
|
v-model="newMember"
|
||||||
|
></MemberCombobox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
||||||
|
|
||||||
|
<PrimaryButton
|
||||||
|
class="ms-3"
|
||||||
|
:class="{ 'opacity-25': saving }"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="submit()">
|
||||||
|
Merge Member
|
||||||
|
</PrimaryButton>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid';
|
import { TrashIcon, PencilSquareIcon, ArrowDownOnSquareStackIcon } from '@heroicons/vue/20/solid';
|
||||||
import type { Member } from '@/packages/api/src';
|
import type { Member } from '@/packages/api/src';
|
||||||
import { canDeleteMembers, canUpdateMembers } from '@/utils/permissions';
|
import {canDeleteMembers, canMergeMembers, canUpdateMembers} from '@/utils/permissions';
|
||||||
import MoreOptionsDropdown from '@/packages/ui/src/MoreOptionsDropdown.vue';
|
import MoreOptionsDropdown from '@/packages/ui/src/MoreOptionsDropdown.vue';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
delete: [];
|
delete: [];
|
||||||
edit: [];
|
edit: [];
|
||||||
|
merge: [];
|
||||||
}>();
|
}>();
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
member: Member;
|
member: Member;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -36,6 +38,15 @@ const props = defineProps<{
|
|||||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="props.member.role === 'placeholder' && canMergeMembers()"
|
||||||
|
:aria-label="'Merge Member ' + props.member.name"
|
||||||
|
data-testid="member_merge"
|
||||||
|
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||||
|
@click="emit('merge')">
|
||||||
|
<ArrowDownOnSquareStackIcon class="w-5 text-icon-active"></ArrowDownOnSquareStackIcon>
|
||||||
|
<span>Merge</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</MoreOptionsDropdown>
|
</MoreOptionsDropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,16 +10,18 @@ import { getCurrentOrganizationId } from '@/utils/useUser';
|
|||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
import { canInvitePlaceholderMembers } from '@/utils/permissions';
|
import { canInvitePlaceholderMembers } from '@/utils/permissions';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
import { useMembersStore } from '@/utils/useMembers';
|
||||||
import { ref } from 'vue';
|
import {computed, ref} from 'vue';
|
||||||
import MemberEditModal from '@/Components/Common/Member/MemberEditModal.vue';
|
import MemberEditModal from '@/Components/Common/Member/MemberEditModal.vue';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
|
import MemberMergeModal from "@/Components/Common/Member/MemberMergeModal.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
member: Member;
|
member: Member;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const showEditMemberModal = ref(false);
|
const showEditMemberModal = ref(false);
|
||||||
|
const showMergeMemberModal = ref(false);
|
||||||
|
|
||||||
function removeMember() {
|
function removeMember() {
|
||||||
useMembersStore().removeMember(props.member.id);
|
useMembersStore().removeMember(props.member.id);
|
||||||
@@ -45,6 +47,11 @@ async function invitePlaceholder(id: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userHasValidMailAddress = computed(() => {
|
||||||
|
return !props.member.email.endsWith('@solidtime-import.test');
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -87,7 +94,8 @@ async function invitePlaceholder(id: string) {
|
|||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
v-if="
|
v-if="
|
||||||
member.is_placeholder === true &&
|
member.is_placeholder === true &&
|
||||||
canInvitePlaceholderMembers()
|
canInvitePlaceholderMembers() &&
|
||||||
|
userHasValidMailAddress
|
||||||
"
|
"
|
||||||
size="small"
|
size="small"
|
||||||
@click="invitePlaceholder(member.id)"
|
@click="invitePlaceholder(member.id)"
|
||||||
@@ -96,11 +104,14 @@ async function invitePlaceholder(id: string) {
|
|||||||
<MemberMoreOptionsDropdown
|
<MemberMoreOptionsDropdown
|
||||||
:member="member"
|
:member="member"
|
||||||
@edit="showEditMemberModal = true"
|
@edit="showEditMemberModal = true"
|
||||||
@delete="removeMember"></MemberMoreOptionsDropdown>
|
@delete="removeMember"
|
||||||
|
@merge="showMergeMemberModal = true"
|
||||||
|
></MemberMoreOptionsDropdown>
|
||||||
</div>
|
</div>
|
||||||
<MemberEditModal
|
<MemberEditModal
|
||||||
v-model:show="showEditMemberModal"
|
v-model:show="showEditMemberModal"
|
||||||
:member="member"></MemberEditModal>
|
:member="member"></MemberEditModal>
|
||||||
|
<MemberMergeModal v-model:show="showMergeMemberModal" :member="member"></MemberMergeModal>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ async function createApiToken(){
|
|||||||
(response) => {
|
(response) => {
|
||||||
createApiTokenForm.name = '';
|
createApiTokenForm.name = '';
|
||||||
displayingToken.value = true;
|
displayingToken.value = true;
|
||||||
// @ts-expect-error temporary fix until openapi docs type is fixed
|
|
||||||
newToken.value = response.data.access_token;
|
newToken.value = response.data.access_token;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -117,7 +116,7 @@ const deleteApiTokenMutation = useMutation({
|
|||||||
mutationFn: async (apiTokenId: string) => {
|
mutationFn: async (apiTokenId: string) => {
|
||||||
return await api.deleteApiToken(undefined, {
|
return await api.deleteApiToken(undefined, {
|
||||||
params: {
|
params: {
|
||||||
apiTokenId: apiTokenId,
|
apiToken: apiTokenId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -130,7 +129,7 @@ const revokeApiTokenMutation = useMutation({
|
|||||||
mutationFn: async (apiTokenId: string) => {
|
mutationFn: async (apiTokenId: string) => {
|
||||||
return await api.revokeApiToken(undefined, {
|
return await api.revokeApiToken(undefined, {
|
||||||
params: {
|
params: {
|
||||||
apiTokenId: apiTokenId,
|
apiToken: apiTokenId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ const ApiTokenResource = z
|
|||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
revoked: z.string(),
|
revoked: z.boolean(),
|
||||||
scopes: z.string(),
|
scopes: z.array(z.string()),
|
||||||
created_at: z.union([z.string(), z.null()]),
|
created_at: z.string(),
|
||||||
expires_at: z.union([z.string(), z.null()]),
|
expires_at: z.union([z.string(), z.null()]),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
@@ -15,7 +15,17 @@ const ApiTokenCollection = z.array(ApiTokenResource);
|
|||||||
const ApiTokenStoreRequest = z
|
const ApiTokenStoreRequest = z
|
||||||
.object({ name: z.string().min(1).max(255) })
|
.object({ name: z.string().min(1).max(255) })
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const ApiTokenWithAccessTokenResource = z.string();
|
const ApiTokenWithAccessTokenResource = z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
revoked: z.boolean(),
|
||||||
|
scopes: z.array(z.string()),
|
||||||
|
created_at: z.string(),
|
||||||
|
expires_at: z.union([z.string(), z.null()]),
|
||||||
|
access_token: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
const ClientResource = z
|
const ClientResource = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -63,6 +73,10 @@ const MemberUpdateRequest = z
|
|||||||
.object({ role: Role, billable_rate: z.union([z.number(), z.null()]) })
|
.object({ role: Role, billable_rate: z.union([z.number(), z.null()]) })
|
||||||
.partial()
|
.partial()
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
const MemberMergeIntoRequest = z
|
||||||
|
.object({ member_id: z.string() })
|
||||||
|
.partial()
|
||||||
|
.passthrough();
|
||||||
const OrganizationResource = z
|
const OrganizationResource = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -80,6 +94,28 @@ const OrganizationUpdateRequest = z
|
|||||||
employees_can_see_billable_rates: z.boolean().optional(),
|
employees_can_see_billable_rates: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
const VersionRequest = z
|
||||||
|
.object({
|
||||||
|
version: z.string().max(255),
|
||||||
|
build: z.string().max(255),
|
||||||
|
url: z.string().max(255),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
const TelemetryRequest = z
|
||||||
|
.object({
|
||||||
|
version: z.string().max(255),
|
||||||
|
build: z.string().max(255),
|
||||||
|
url: z.string().max(255).url(),
|
||||||
|
user_count: z.number().int(),
|
||||||
|
organization_count: z.number().int(),
|
||||||
|
audit_count: z.number().int(),
|
||||||
|
project_count: z.number().int(),
|
||||||
|
project_member_count: z.number().int(),
|
||||||
|
client_count: z.number().int(),
|
||||||
|
task_count: z.number().int(),
|
||||||
|
time_entry_count: z.number().int(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
const ProjectResource = z
|
const ProjectResource = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -486,8 +522,11 @@ export const schemas = {
|
|||||||
MemberResource,
|
MemberResource,
|
||||||
Role,
|
Role,
|
||||||
MemberUpdateRequest,
|
MemberUpdateRequest,
|
||||||
|
MemberMergeIntoRequest,
|
||||||
OrganizationResource,
|
OrganizationResource,
|
||||||
OrganizationUpdateRequest,
|
OrganizationUpdateRequest,
|
||||||
|
VersionRequest,
|
||||||
|
TelemetryRequest,
|
||||||
ProjectResource,
|
ProjectResource,
|
||||||
ProjectStoreRequest,
|
ProjectStoreRequest,
|
||||||
ProjectUpdateRequest,
|
ProjectUpdateRequest,
|
||||||
@@ -1160,6 +1199,71 @@ const endpoints = makeApi([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/v1/organizations/:organization/member/:member/merge-into',
|
||||||
|
alias: 'mergeMember',
|
||||||
|
requestFormat: 'json',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'body',
|
||||||
|
type: 'Body',
|
||||||
|
schema: z
|
||||||
|
.object({ member_id: z.string() })
|
||||||
|
.partial()
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'organization',
|
||||||
|
type: 'Path',
|
||||||
|
schema: z.string(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'member',
|
||||||
|
type: 'Path',
|
||||||
|
schema: z.string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response: z.null(),
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
description: `API exception`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
error: z.boolean(),
|
||||||
|
key: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
description: `Unauthenticated`,
|
||||||
|
schema: z.object({ message: z.string() }).passthrough(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
description: `Authorization error`,
|
||||||
|
schema: z.object({ message: z.string() }).passthrough(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
description: `Not found`,
|
||||||
|
schema: z.object({ message: z.string() }).passthrough(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 422,
|
||||||
|
description: `Validation error`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
message: z.string(),
|
||||||
|
errors: z.record(z.array(z.string())),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/v1/organizations/:organization/members',
|
path: '/v1/organizations/:organization/members',
|
||||||
@@ -3394,6 +3498,58 @@ If the group parameters are all set to `null` or are all missing, the
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/v1/ping/telemetry',
|
||||||
|
alias: 'v1.ping.telemetry',
|
||||||
|
requestFormat: 'json',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'body',
|
||||||
|
type: 'Body',
|
||||||
|
schema: TelemetryRequest,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response: z.object({ success: z.boolean() }).passthrough(),
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
status: 422,
|
||||||
|
description: `Validation error`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
message: z.string(),
|
||||||
|
errors: z.record(z.array(z.string())),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/v1/ping/version',
|
||||||
|
alias: 'v1.ping.version',
|
||||||
|
requestFormat: 'json',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'body',
|
||||||
|
type: 'Body',
|
||||||
|
schema: VersionRequest,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response: z.object({ version: z.string() }).passthrough(),
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
status: 422,
|
||||||
|
description: `Validation error`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
message: z.string(),
|
||||||
|
errors: z.record(z.array(z.string())),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/v1/public/reports',
|
path: '/v1/public/reports',
|
||||||
@@ -3471,6 +3627,17 @@ Please note that the access token is only shown in this response and cannot be r
|
|||||||
.object({ data: ApiTokenWithAccessTokenResource })
|
.object({ data: ApiTokenWithAccessTokenResource })
|
||||||
.passthrough(),
|
.passthrough(),
|
||||||
errors: [
|
errors: [
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
description: `API exception`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
error: z.boolean(),
|
||||||
|
key: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 401,
|
status: 401,
|
||||||
description: `Unauthenticated`,
|
description: `Unauthenticated`,
|
||||||
@@ -3495,18 +3662,29 @@ Please note that the access token is only shown in this response and cannot be r
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
path: '/v1/users/me/api-tokens/:apiTokenId',
|
path: '/v1/users/me/api-tokens/:apiToken',
|
||||||
alias: 'deleteApiToken',
|
alias: 'deleteApiToken',
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
{
|
||||||
name: 'apiTokenId',
|
name: 'apiToken',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
errors: [
|
errors: [
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
description: `API exception`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
error: z.boolean(),
|
||||||
|
key: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 401,
|
status: 401,
|
||||||
description: `Unauthenticated`,
|
description: `Unauthenticated`,
|
||||||
@@ -3517,22 +3695,38 @@ Please note that the access token is only shown in this response and cannot be r
|
|||||||
description: `Authorization error`,
|
description: `Authorization error`,
|
||||||
schema: z.object({ message: z.string() }).passthrough(),
|
schema: z.object({ message: z.string() }).passthrough(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
description: `Not found`,
|
||||||
|
schema: z.object({ message: z.string() }).passthrough(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/v1/users/me/api-tokens/:apiTokenId/revoke',
|
path: '/v1/users/me/api-tokens/:apiToken/revoke',
|
||||||
alias: 'revokeApiToken',
|
alias: 'revokeApiToken',
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
{
|
||||||
name: 'apiTokenId',
|
name: 'apiToken',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
errors: [
|
errors: [
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
description: `API exception`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
error: z.boolean(),
|
||||||
|
key: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 401,
|
status: 401,
|
||||||
description: `Unauthenticated`,
|
description: `Unauthenticated`,
|
||||||
@@ -3543,6 +3737,11 @@ Please note that the access token is only shown in this response and cannot be r
|
|||||||
description: `Authorization error`,
|
description: `Authorization error`,
|
||||||
schema: z.object({ message: z.string() }).passthrough(),
|
schema: z.object({ message: z.string() }).passthrough(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
description: `Not found`,
|
||||||
|
schema: z.object({ message: z.string() }).passthrough(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ export function canDeleteMembers() {
|
|||||||
return currentUserHasPermission('members:delete');
|
return currentUserHasPermission('members:delete');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canMergeMembers() {
|
||||||
|
return currentUserHasPermission('members:merge-into');
|
||||||
|
}
|
||||||
|
|
||||||
export function canInvitePlaceholderMembers() {
|
export function canInvitePlaceholderMembers() {
|
||||||
return currentUserHasPermission('members:invite-placeholder');
|
return currentUserHasPermission('members:invite-placeholder');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user