fix: some api permission issue

This commit is contained in:
Ramesh Mane
2025-10-18 16:50:30 +05:30
parent d619b8e282
commit 9160a5a2ee
13 changed files with 158 additions and 34 deletions

View File

@@ -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>

View File

@@ -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])"
>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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]: {

View File

@@ -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',

View File

@@ -11,3 +11,4 @@ dist/
.vscode/*
!.vscode/extensions.json
.idea
src/lib/Api.ts

View File

@@ -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) */

View File

@@ -420,6 +420,10 @@ const rolePermissions:
workspaceInvite: true,
workspaceUserDelete: true,
requestUpgrade: true,
// Teams
teamList: true,
teamGet: true,
},
},
[WorkspaceUserRoles.COMMENTER]: {

View File

@@ -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)"

View File

@@ -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',
);

View File

@@ -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;
}

View File

@@ -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/**