mirror of
https://github.com/nocodb/nocodb.git
synced 2026-02-02 02:37:33 +00:00
fix: some api permission issue
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
team: TeamType
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
const useForm = Form.useForm
|
||||
|
||||
const { team } = toRefs(props)
|
||||
const { team, readOnly } = toRefs(props)
|
||||
|
||||
const inputEl = ref<HTMLInputElement>()
|
||||
|
||||
@@ -27,6 +28,8 @@ const { validate, validateInfos } = useForm(formState, validators)
|
||||
const updating = ref(false)
|
||||
|
||||
const updateTeam = async () => {
|
||||
if (readOnly.value) return
|
||||
|
||||
try {
|
||||
updating.value = true
|
||||
await validate()
|
||||
@@ -51,6 +54,8 @@ onMounted(() => {
|
||||
formState.description = team.value.description ?? ''
|
||||
formState.meta = parseProp(team.value.meta)
|
||||
|
||||
if (readOnly.value) return
|
||||
|
||||
nextTick(() => {
|
||||
inputEl.value?.focus()
|
||||
})
|
||||
@@ -78,6 +83,7 @@ onMounted(() => {
|
||||
hide-details
|
||||
data-testid="create-team-title-input"
|
||||
:placeholder="$t('placeholder.enterTeamName')"
|
||||
:disabled="readOnly"
|
||||
@input="updateTeamWithDebounce"
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -92,6 +98,7 @@ onMounted(() => {
|
||||
hide-details
|
||||
data-testid="create-team-description-input"
|
||||
:placeholder="$t('placeholder.enterTeamDescription')"
|
||||
:disabled="readOnly"
|
||||
@input="updateTeamWithDebounce"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
@@ -7,6 +7,7 @@ export type TeamMember = TeamMemberV3ResponseV3Type & WorkspaceUserType
|
||||
interface Props {
|
||||
team: TeamType
|
||||
tableToolbarClassName?: string
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
@@ -18,6 +19,8 @@ const emits = defineEmits<{
|
||||
|
||||
const team = useVModel(props, 'team', emits)
|
||||
|
||||
const { readOnly } = toRefs(props)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { api } = useApi()
|
||||
@@ -334,7 +337,7 @@ onMounted(() => {
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<div class="relative children:flex-none min-w-[150px] min-h-8 flex items-center justify-end">
|
||||
<div v-if="!readOnly" class="relative children:flex-none min-w-[150px] min-h-8 flex items-center justify-end">
|
||||
<div v-if="!selectedRowConfig.selectedRowCount">
|
||||
<NcButton
|
||||
size="small"
|
||||
@@ -383,7 +386,7 @@ onMounted(() => {
|
||||
<NcCheckbox
|
||||
:checked="selectedRowConfig.isAllSelected"
|
||||
:indeterminate="selectedRowConfig.isSomeSelected"
|
||||
:disabled="!teamMembers.length"
|
||||
:disabled="!teamMembers.length || readOnly"
|
||||
@update:checked="toggleSelectAll"
|
||||
/>
|
||||
</template>
|
||||
@@ -394,7 +397,7 @@ onMounted(() => {
|
||||
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'select'">
|
||||
<NcCheckbox v-model:checked="selectedRows[record.fk_user_id!]" />
|
||||
<NcCheckbox v-model:checked="selectedRows[record.fk_user_id!]" :disabled="readOnly" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'member_name'">
|
||||
<div class="w-full flex items-center gap-4 overflow-hidden">
|
||||
@@ -423,7 +426,11 @@ onMounted(() => {
|
||||
</template>
|
||||
<template #overlay>
|
||||
<NcMenu variant="medium" @click="isOpenContextMenu[record.fk_user_id!] = false">
|
||||
<NcMenuItem v-if="!isTeamOwner(record as TeamMember)" @click="handleAssignAsTeamOwner(record as TeamMember)">
|
||||
<NcMenuItem
|
||||
v-if="!isTeamOwner(record as TeamMember)"
|
||||
:disabled="readOnly"
|
||||
@click="handleAssignAsTeamOwner(record as TeamMember)"
|
||||
>
|
||||
<GeneralIcon icon="ncArrowUpCircle" class="h-4 w-4" />
|
||||
{{ $t('activity.assignAsTeamOwner') }}
|
||||
</NcMenuItem>
|
||||
@@ -431,12 +438,12 @@ onMounted(() => {
|
||||
<!-- Show leave team option only if logged in user is same as record user -->
|
||||
<NcTooltip
|
||||
v-if="record.fk_user_id === user?.id"
|
||||
:disabled="!(hasSoleTeamOwner && isTeamOwner(record as TeamMember))"
|
||||
:disabled="!(hasSoleTeamOwner && isTeamOwner(record as TeamMember)) || readOnly"
|
||||
:title="t('objects.teams.soleTeamOwnerTooltip')"
|
||||
placement="left"
|
||||
>
|
||||
<NcMenuItem
|
||||
:disabled="(hasSoleTeamOwner && isTeamOwner(record as TeamMember))"
|
||||
:disabled="(hasSoleTeamOwner && isTeamOwner(record as TeamMember)) || readOnly"
|
||||
danger
|
||||
@click="handleLeaveTeam(record as TeamType)"
|
||||
>
|
||||
@@ -447,12 +454,12 @@ onMounted(() => {
|
||||
|
||||
<NcTooltip
|
||||
v-else
|
||||
:disabled="!(hasSoleTeamOwner && isTeamOwner(record as TeamMember))"
|
||||
:disabled="!(hasSoleTeamOwner && isTeamOwner(record as TeamMember)) || readOnly"
|
||||
:title="t('objects.teams.thisIsTheOnlyTeamOwnerTooltip')"
|
||||
placement="left"
|
||||
>
|
||||
<NcMenuItem
|
||||
:disabled="(hasSoleTeamOwner && isTeamOwner(record as TeamMember))"
|
||||
:disabled="(hasSoleTeamOwner && isTeamOwner(record as TeamMember)) || readOnly"
|
||||
danger
|
||||
@click="handleRemoveMemberFromTeam([record as TeamMember])"
|
||||
>
|
||||
|
||||
@@ -17,10 +17,16 @@ const { isOpenUsingRouterPush } = toRefs(props)
|
||||
const router = useRouter()
|
||||
const route = router.currentRoute
|
||||
|
||||
const { isUIAllowed } = useRoles()
|
||||
|
||||
const workspaceStore = useWorkspace()
|
||||
|
||||
const { teamsMap, activeWorkspaceId } = storeToRefs(workspaceStore)
|
||||
|
||||
const hasEditPermission = computed(() => {
|
||||
return isUIAllowed('teamUpdate')
|
||||
})
|
||||
|
||||
const teamId = computed(() => {
|
||||
return route.value.name === 'index-typeOrId-settings' && route.value.query?.tab === 'teams'
|
||||
? (route.value.query?.teamId as string)
|
||||
@@ -90,8 +96,8 @@ const supportedDocs: SupportedDocsType[] = [
|
||||
<!-- Content -->
|
||||
<div class="flex-1 nc-modal-teams-edit-content">
|
||||
<template v-if="editTeam">
|
||||
<WorkspaceTeamsEditGeneralSection v-model:team="editTeam" />
|
||||
<WorkspaceTeamsEditMembersSection v-model:team="editTeam" @close="vVisible = false" />
|
||||
<WorkspaceTeamsEditGeneralSection v-model:team="editTeam" :read-only="!hasEditPermission" />
|
||||
<WorkspaceTeamsEditMembersSection v-model:team="editTeam" :read-only="!hasEditPermission" @close="vVisible = false" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TeamV3V3Type } from 'nocodb-sdk'
|
||||
import type { NcConfirmModalProps } from '~/components/nc/ModalConfirm.vue'
|
||||
|
||||
interface Props {
|
||||
@@ -15,13 +16,21 @@ const route = router.currentRoute
|
||||
|
||||
const { isActive } = toRefs(props)
|
||||
|
||||
const isAdminPanel = inject(IsAdminPanelInj, ref(false))
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const isAdminPanel = inject(IsAdminPanelInj, ref(false))
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const { isUIAllowed } = useRoles()
|
||||
|
||||
const hasEditPermission = computed(() => {
|
||||
return isUIAllowed('teamUpdate')
|
||||
})
|
||||
|
||||
const workspaceStore = useWorkspace()
|
||||
|
||||
const { teams, isTeamsLoading, collaboratorsMap } = storeToRefs(workspaceStore)
|
||||
const { teams, isTeamsLoading, collaboratorsMap, activeWorkspace } = storeToRefs(workspaceStore)
|
||||
|
||||
const { sorts, sortDirection, loadSorts, handleGetSortedData, saveOrUpdate: saveOrUpdateUserSort } = useUserSorts('Teams')
|
||||
|
||||
@@ -29,6 +38,10 @@ const searchQuery = ref('')
|
||||
|
||||
const isCreateTeamModalVisible = ref(false)
|
||||
|
||||
const activeWorkspaceId = computed(() => {
|
||||
return props.workspaceId || activeWorkspace.value?.id
|
||||
})
|
||||
|
||||
/**
|
||||
* Modal visibility is based on query params, and will use following method
|
||||
* Open - router.push
|
||||
@@ -103,7 +116,7 @@ const columns = [
|
||||
minWidth: 110,
|
||||
justify: 'justify-end',
|
||||
},
|
||||
] as NcTableColumnProps<TeamType>[]
|
||||
] as NcTableColumnProps<TeamV3V3Type>[]
|
||||
|
||||
const customRow = (record: Record<string, any>) => ({
|
||||
onClick: () => {
|
||||
@@ -115,8 +128,8 @@ const handleCreateTeam = () => {
|
||||
isCreateTeamModalVisible.value = true
|
||||
}
|
||||
|
||||
const hasSoleTeamOwner = (team: TeamType) => {
|
||||
return team?.owners?.length < 2
|
||||
const hasSoleTeamOwner = (team: TeamV3V3Type) => {
|
||||
return (team?.managers_count || 0) < 2
|
||||
}
|
||||
|
||||
const handleConfirm = ({
|
||||
@@ -159,7 +172,7 @@ const handleConfirm = ({
|
||||
}
|
||||
}
|
||||
|
||||
const handleLeaveTeam = (team: TeamType) => {
|
||||
const handleLeaveTeam = (team: TeamV3V3Type) => {
|
||||
handleConfirm({
|
||||
title: t('objects.teams.confirmLeaveTeamTitle'),
|
||||
content: t('objects.teams.confirmLeaveTeamSubtitle'),
|
||||
@@ -174,7 +187,7 @@ const handleLeaveTeam = (team: TeamType) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteTeam = (team: TeamType) => {
|
||||
const handleDeleteTeam = (team: TeamV3V3Type) => {
|
||||
handleConfirm({
|
||||
title: t('objects.teams.confirmDeleteTeamTitle'),
|
||||
content: t('objects.teams.confirmDeleteTeamSubtitle'),
|
||||
@@ -184,6 +197,24 @@ const handleDeleteTeam = (team: TeamType) => {
|
||||
// Todo: api call
|
||||
console.log('delete team', team)
|
||||
await ncDelay(2000)
|
||||
|
||||
try {
|
||||
await $api.internal.postOperation(
|
||||
activeWorkspaceId.value!,
|
||||
NO_SCOPE,
|
||||
{
|
||||
operation: 'teamDelete',
|
||||
},
|
||||
{
|
||||
teamId: team.id,
|
||||
},
|
||||
)
|
||||
|
||||
teams.value = teams.value.filter((t) => t.id !== team.id)
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
message.error(await extractSdkResponseErrorMsg(error))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -254,6 +285,7 @@ onMounted(async () => {
|
||||
</a-input>
|
||||
|
||||
<NcButton
|
||||
v-if="hasEditPermission"
|
||||
size="small"
|
||||
inner-class="!gap-2"
|
||||
:disabled="isTeamsLoading"
|
||||
@@ -280,13 +312,24 @@ onMounted(async () => {
|
||||
>
|
||||
<template #emptyText>
|
||||
<NcEmptyPlaceholder
|
||||
:title="$t('placeholder.youHaveNotCreatedAnyTeams')"
|
||||
:subtitle="$t('placeholder.youHaveNotCreatedAnyTeamsSubtitle')"
|
||||
:title="teams.length ? '' : $t('placeholder.youHaveNotCreatedAnyTeams')"
|
||||
:subtitle="teams.length ? $t('title.noResultsMatchedYourSearch') : $t('title.noResultsMatchedYourSearchSubtitle')"
|
||||
>
|
||||
<template #icon>
|
||||
<img src="~assets/img/placeholder/moscot-collaborators.png" alt="New Team" class="!w-[320px] flex-none" />
|
||||
<img
|
||||
v-if="!teams.length"
|
||||
src="~assets/img/placeholder/moscot-collaborators.png"
|
||||
alt="New Team"
|
||||
class="!w-[320px] flex-none"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
src="~assets/img/placeholder/no-search-result-found.png"
|
||||
alt="No search results found"
|
||||
class="!w-[320px] flex-none"
|
||||
/>
|
||||
</template>
|
||||
<template #action>
|
||||
<template v-if="hasEditPermission" #action>
|
||||
<NcButton size="small" inner-class="!gap-2" @click="handleCreateTeam">
|
||||
<template #icon>
|
||||
<GeneralIcon icon="plus" class="h-4 w-4" />
|
||||
@@ -329,27 +372,28 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<div v-if="column.key === 'action'" @click.stop>
|
||||
<NcDropdown>
|
||||
<NcDropdown placement="bottomRight">
|
||||
<NcButton size="small" type="secondary">
|
||||
<component :is="iconMap.ncMoreVertical" />
|
||||
</NcButton>
|
||||
<template #overlay>
|
||||
<NcMenu variant="medium">
|
||||
<NcMenuItem @click="handleEditTeam(record as TeamType)">
|
||||
<NcMenuItem @click="handleEditTeam(record as TeamV3V3Type)">
|
||||
<GeneralIcon icon="ncEdit" class="h-4 w-4" />
|
||||
{{ $t('general.edit') }}
|
||||
</NcMenuItem>
|
||||
<NcTooltip
|
||||
:disabled="!hasSoleTeamOwner(record as TeamType)"
|
||||
v-if="hasEditPermission"
|
||||
:disabled="!hasSoleTeamOwner(record as TeamV3V3Type) "
|
||||
:title="t('objects.teams.thisTeamHasOnlyOneOwnerTooltip')"
|
||||
placement="left"
|
||||
>
|
||||
<NcMenuItem :disabled="hasSoleTeamOwner(record as TeamType)" @click="handleLeaveTeam(record as TeamType)">
|
||||
<NcMenuItem :disabled="hasSoleTeamOwner(record as TeamV3V3Type) " @click="handleLeaveTeam(record as TeamV3V3Type)">
|
||||
<GeneralIcon icon="ncLogOut" class="h-4 w-4" />
|
||||
{{ $t('activity.leaveTeam') }}
|
||||
</NcMenuItem>
|
||||
</NcTooltip>
|
||||
<NcMenuItem danger @click="handleDeleteTeam(record as TeamType)">
|
||||
<NcMenuItem v-if="hasEditPermission" danger @click="handleDeleteTeam(record as TeamV3V3Type)">
|
||||
<GeneralIcon icon="delete" />
|
||||
{{ $t('activity.deleteTeam') }}
|
||||
</NcMenuItem>
|
||||
|
||||
@@ -103,6 +103,14 @@ const rolePermissions = {
|
||||
excelImport: true,
|
||||
nocodbImport: true,
|
||||
workspaceIntegrations: true,
|
||||
|
||||
// Teams
|
||||
teamCreate: true,
|
||||
teamUpdate: true,
|
||||
teamDelete: true,
|
||||
teamUserAdd: true,
|
||||
teamUserRemove: true,
|
||||
teamUserUpdate: true,
|
||||
},
|
||||
},
|
||||
[WorkspaceUserRoles.EDITOR]: {
|
||||
@@ -114,6 +122,10 @@ const rolePermissions = {
|
||||
[WorkspaceUserRoles.VIEWER]: {
|
||||
include: {
|
||||
workspaceCollaborators: true,
|
||||
|
||||
// Teams
|
||||
teamList: true,
|
||||
teamGet: true,
|
||||
},
|
||||
},
|
||||
[WorkspaceUserRoles.NO_ACCESS]: {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const useWorkspace = defineStore('workspaceStore', () => {
|
||||
|
||||
const ssoLoginRequiredDlg = ref(false)
|
||||
|
||||
const { loadRoles } = useRoles()
|
||||
const { loadRoles, isUIAllowed } = useRoles()
|
||||
|
||||
const { user: currentUser } = useGlobal()
|
||||
|
||||
@@ -635,6 +635,8 @@ export const useWorkspace = defineStore('workspaceStore', () => {
|
||||
const isTeamsLoading = ref(false)
|
||||
|
||||
async function loadTeams({ workspaceId }: { workspaceId: string }) {
|
||||
if (!isUIAllowed('teamList')) return
|
||||
|
||||
isTeamsLoading.value = true
|
||||
|
||||
try {
|
||||
@@ -651,6 +653,8 @@ export const useWorkspace = defineStore('workspaceStore', () => {
|
||||
}
|
||||
|
||||
async function getTeamById(workspaceId: string, teamId: string) {
|
||||
if (!isUIAllowed('teamGet')) return
|
||||
|
||||
try {
|
||||
return await $api.internal.getOperation(workspaceId, NO_SCOPE, {
|
||||
operation: 'teamGet',
|
||||
|
||||
1
packages/nocodb-sdk-v2/.gitignore
vendored
1
packages/nocodb-sdk-v2/.gitignore
vendored
@@ -11,3 +11,4 @@ dist/
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
src/lib/Api.ts
|
||||
|
||||
@@ -1745,6 +1745,11 @@ export interface TeamV3 {
|
||||
* @example 10
|
||||
*/
|
||||
members_count: number;
|
||||
/**
|
||||
* Number of team managers
|
||||
* @example 2
|
||||
*/
|
||||
managers_count?: number;
|
||||
/** Organization ID (for Cloud Enterprise) */
|
||||
fk_org_id?: string;
|
||||
/** Workspace ID (for other plans) */
|
||||
|
||||
@@ -420,6 +420,10 @@ const rolePermissions:
|
||||
workspaceInvite: true,
|
||||
workspaceUserDelete: true,
|
||||
requestUpgrade: true,
|
||||
|
||||
// Teams
|
||||
teamList: true,
|
||||
teamGet: true,
|
||||
},
|
||||
},
|
||||
[WorkspaceUserRoles.COMMENTER]: {
|
||||
|
||||
@@ -9017,6 +9017,11 @@
|
||||
"description": "Number of team members",
|
||||
"example": 10
|
||||
},
|
||||
"managers_count": {
|
||||
"type": "integer",
|
||||
"description": "Number of team managers",
|
||||
"example": 2
|
||||
},
|
||||
"fk_org_id": {
|
||||
"type": "string",
|
||||
"description": "Organization ID (for Cloud Enterprise)"
|
||||
|
||||
@@ -16,6 +16,7 @@ import { validatePayload } from '~/helpers';
|
||||
import Noco from '~/Noco';
|
||||
import { MetaTable } from '~/utils/globals';
|
||||
import { parseMetaProp } from '~/utils/modelUtils';
|
||||
import { TeamUserRoles } from 'nocodb-sdk';
|
||||
|
||||
@Injectable()
|
||||
export class TeamsV3Service {
|
||||
@@ -28,6 +29,17 @@ export class TeamsV3Service {
|
||||
return await TeamUser.countByTeam(context, teamId);
|
||||
}
|
||||
|
||||
async getTeamManagersCount(
|
||||
context: NcContext,
|
||||
teamId: string,
|
||||
): Promise<number> {
|
||||
return await TeamUser.countByTeamAndRole(
|
||||
context,
|
||||
teamId,
|
||||
TeamUserRoles.MANAGER,
|
||||
);
|
||||
}
|
||||
|
||||
async getUserById(context: NcContext, userId: string) {
|
||||
const user = await User.get(userId);
|
||||
if (!user) {
|
||||
@@ -50,10 +62,15 @@ export class TeamsV3Service {
|
||||
// Get teams with member counts using optimized query
|
||||
const teamsWithCounts = await Promise.all(
|
||||
teams.map(async (team) => {
|
||||
const membersCount = await TeamUser.countByTeam(context, team.id);
|
||||
const [membersCount, menagersCount] = await Promise.all([
|
||||
this.getTeamMembersCount(context, team.id),
|
||||
this.getTeamManagersCount(context, team.id),
|
||||
]);
|
||||
|
||||
return {
|
||||
...team,
|
||||
members_count: membersCount,
|
||||
managers_count: menagersCount,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -67,6 +84,7 @@ export class TeamsV3Service {
|
||||
icon: meta.icon || undefined,
|
||||
badge_color: meta.badge_color || undefined,
|
||||
members_count: team.members_count,
|
||||
managers_count: team.managers_count,
|
||||
created_at: team.created_at,
|
||||
updated_at: team.updated_at,
|
||||
};
|
||||
@@ -203,7 +221,10 @@ export class TeamsV3Service {
|
||||
}
|
||||
|
||||
// Get member count for the created team
|
||||
const teamUsers = await this.getTeamMembersCount(context, team.id);
|
||||
const [teamUsers, teamManagersCount] = await Promise.all([
|
||||
this.getTeamMembersCount(context, team.id),
|
||||
this.getTeamManagersCount(context, team.id),
|
||||
]);
|
||||
|
||||
// Transform to v3 response format
|
||||
const meta = parseMetaProp(team);
|
||||
@@ -214,6 +235,7 @@ export class TeamsV3Service {
|
||||
icon: meta.icon || undefined,
|
||||
badge_color: meta.badge_color || undefined,
|
||||
members_count: teamUsers,
|
||||
managers_count: teamManagersCount,
|
||||
created_at: team.created_at,
|
||||
updated_at: team.updated_at,
|
||||
};
|
||||
@@ -273,7 +295,10 @@ export class TeamsV3Service {
|
||||
const updatedTeam = await Team.update(context, param.teamId, updateData);
|
||||
|
||||
// Get member count for the updated team
|
||||
const teamUsers = await this.getTeamMembersCount(context, updatedTeam.id);
|
||||
const [teamUsers, teamManagersCount] = await Promise.all([
|
||||
this.getTeamMembersCount(context, updatedTeam.id),
|
||||
this.getTeamManagersCount(context, updatedTeam.id),
|
||||
]);
|
||||
|
||||
// Transform to v3 response format
|
||||
const meta = parseMetaProp(updatedTeam);
|
||||
@@ -284,6 +309,7 @@ export class TeamsV3Service {
|
||||
icon: meta.icon || undefined,
|
||||
badge_color: meta.badge_color || undefined,
|
||||
members_count: teamUsers,
|
||||
managers_count: teamManagersCount,
|
||||
created_at: updatedTeam.created_at,
|
||||
updated_at: updatedTeam.updated_at,
|
||||
};
|
||||
@@ -460,12 +486,12 @@ export class TeamsV3Service {
|
||||
|
||||
// If removing the last manager, prevent it
|
||||
if (teamUser.roles === 'manager') {
|
||||
const managerCount = await TeamUser.countByTeamAndRole(
|
||||
const managersCount = await this.getTeamManagersCount(
|
||||
context,
|
||||
param.teamId,
|
||||
'manager',
|
||||
);
|
||||
if (managerCount === 1) {
|
||||
|
||||
if (managersCount === 1) {
|
||||
NcError.get(context).invalidRequestBody(
|
||||
'Cannot remove the last manager',
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface TeamV3Type {
|
||||
icon?: string;
|
||||
badge_color?: string;
|
||||
members_count: number;
|
||||
managers_count: number;
|
||||
fk_org_id?: string;
|
||||
fk_workspace_id?: string;
|
||||
created_at?: string;
|
||||
@@ -16,6 +17,7 @@ export interface TeamV3ResponseType {
|
||||
icon?: string;
|
||||
badge_color?: string;
|
||||
members_count: number;
|
||||
managers_count: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ pnpm-workspace.yaml
|
||||
**/components.d.ts
|
||||
packages/nc-knex-dialects
|
||||
packages/nocodb-sdk/src/lib/Api.ts
|
||||
packages/nocodb-sdk-v2/src/lib/Api.ts
|
||||
*.sh
|
||||
packages/nc-sql-executor/**
|
||||
packages/nc-db-migrator/**
|
||||
|
||||
Reference in New Issue
Block a user