Merge pull request #7156 from nocodb/feat/copy-ws-id-support

Feat: Provide a way to copy workspace id
This commit is contained in:
Ramesh Mane
2025-10-30 14:10:55 +05:30
committed by GitHub
11 changed files with 261 additions and 75 deletions

View File

@@ -8,6 +8,8 @@ const { t } = useI18n()
const isErrored = ref(false) const isErrored = ref(false)
const isProfileUpdating = ref(false) const isProfileUpdating = ref(false)
const copyBtnRef = ref()
const form = ref<{ const form = ref<{
title: string title: string
email: string email: string
@@ -184,7 +186,26 @@ const onCancel = () => {
<div class="flex-1 flex flex-col gap-4"> <div class="flex-1 flex flex-col gap-4">
<div> <div>
<div class="text-gray-800 mb-2" data-rec="true">{{ $t('general.name') }}</div> <div class="flex items-center gap-1.5 justify-between mb-2">
<div class="text-nc-content-gray" data-rec="true">{{ $t('general.name') }}</div>
<NcTooltip v-if="user" :title="$t('labels.clickToCopyUserID')" placement="top" hide-on-click class="flex">
<div
data-rec="true"
class="flex items-center gap-1.5 text-bodyDefaultSm text-nc-content-gray-subtle2 cursor-pointer"
@click="copyBtnRef?.copyContent()"
>
{{ $t('labels.userIdColon', { userId: user?.id }) }}
<GeneralCopyButton
ref="copyBtnRef"
type="text"
size="xxsmall"
:content="user?.id"
:show-toast="false"
/>
</div>
</NcTooltip>
</div>
<a-form-item name="title" :rules="formRules.title" class="!my-0"> <a-form-item name="title" :rules="formRules.title" class="!my-0">
<a-input <a-input
v-model:value="form.title" v-model:value="form.title"

View File

@@ -328,14 +328,8 @@ const userRoleOptions = [
{{ $t(`objects.roleType.orgLevelCreator`) }} {{ $t(`objects.roleType.orgLevelCreator`) }}
</div> </div>
</template> </template>
<div <div v-if="column.key === 'action'" class="flex items-center gap-2">
v-if="column.key === 'action'" <NcDropdown :trigger="['click']" placement="bottomRight">
class="flex items-center gap-2"
:class="{
'opacity-0 pointer-events-none': el.roles?.includes('super'),
}"
>
<NcDropdown :trigger="['click']">
<NcButton size="xsmall" type="ghost"> <NcButton size="xsmall" type="ghost">
<MdiDotsVertical <MdiDotsVertical
class="text-gray-600 h-5.5 w-5.5 rounded outline-0 p-0.5 nc-workspace-menu transform transition-transform !text-gray-400 cursor-pointer hover:(!text-gray-500 bg-gray-100)" class="text-gray-600 h-5.5 w-5.5 rounded outline-0 p-0.5 nc-workspace-menu transform transition-transform !text-gray-400 cursor-pointer hover:(!text-gray-500 bg-gray-100)"
@@ -344,7 +338,19 @@ const userRoleOptions = [
<template #overlay> <template #overlay>
<NcMenu variant="small"> <NcMenu variant="small">
<NcMenuItemCopyId
:id="el.id"
:tooltip="$t('labels.clickToCopyUserID')"
:label="
$t('labels.userIdColon', {
userId: el.id,
})
"
/>
<template v-if="!el.roles?.includes('super')"> <template v-if="!el.roles?.includes('super')">
<NcDivider />
<!-- Resend invite Email --> <!-- Resend invite Email -->
<NcMenuItem @click="resendInvite(el)"> <NcMenuItem @click="resendInvite(el)">
<component :is="iconMap.email" class="flex text-gray-600" /> <component :is="iconMap.email" class="flex text-gray-600" />
@@ -358,13 +364,13 @@ const userRoleOptions = [
<component :is="iconMap.copy" class="flex text-gray-600" /> <component :is="iconMap.copy" class="flex text-gray-600" />
<div>{{ $t('activity.copyPasswordResetURL') }}</div> <div>{{ $t('activity.copyPasswordResetURL') }}</div>
</NcMenuItem> </NcMenuItem>
</template> <template v-if="el.id !== loggedInUser?.id">
<template v-if="el.id !== loggedInUser?.id"> <NcDivider v-if="!el.roles?.includes('super')" />
<NcDivider v-if="!el.roles?.includes('super')" /> <NcMenuItem data-rec="true" danger @click="openDeleteModal(el)">
<NcMenuItem data-rec="true" danger @click="openDeleteModal(el)"> <MaterialSymbolsDeleteOutlineRounded />
<MaterialSymbolsDeleteOutlineRounded /> {{ $t('general.remove') }} {{ $t('objects.user') }}
{{ $t('general.remove') }} {{ $t('objects.user') }} </NcMenuItem>
</NcMenuItem> </template>
</template> </template>
</NcMenu> </NcMenu>
</template> </template>

View File

@@ -372,6 +372,18 @@ onKeyStroke('Escape', () => {
variant="small" variant="small"
@click="isBasesOptionsOpen[source!.id!] = false" @click="isBasesOptionsOpen[source!.id!] = false"
> >
<NcMenuItemCopyId
:id="source.id"
:tooltip="$t('labels.clickToCopySourceID')"
:label="
$t('labels.sourceIdColon', {
sourceId: source.id,
})
"
@click.stop
/>
<NcDivider />
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('baseRename')" v-if="isUIAllowed('baseRename')"
data-testid="nc-sidebar-source-rename" data-testid="nc-sidebar-source-rename"

View File

@@ -494,16 +494,36 @@ const handleClickRow = (source: SourceType, tab?: string) => {
<div class="flex items-center gap-1">-</div> <div class="flex items-center gap-1">-</div>
</div> </div>
<div class="ds-table-col ds-table-actions"> <div class="ds-table-col justify-end gap-x-1 ds-table-actions" @click.stop>
<NcButton <div class="flex justify-end">
v-if="!sources[0].is_meta && !sources[0].is_local" <NcDropdown placement="bottomRight">
size="small" <NcButton size="small" type="secondary">
class="nc-action-btn nc-edit-base cursor-pointer outline-0 !w-8 !px-1 !rounded-lg" <GeneralIcon icon="threeDotVertical" />
type="text" </NcButton>
@click.stop="baseAction(sources[0].id, DataSourcesSubTab.Edit)" <template #overlay>
> <NcMenu variant="small">
<GeneralIcon icon="edit" class="text-gray-600" /> <NcMenuItemCopyId
</NcButton> :id="sources[0].id"
:tooltip="$t('labels.clickToCopySourceID')"
:label="
$t('labels.sourceIdColon', {
sourceId: sources[0].id,
})
"
/>
<template v-if="!sources[0].is_meta && !sources[0].is_local">
<NcDivider />
<NcMenuItem @click="baseAction(sources[0].id, DataSourcesSubTab.Edit)">
<GeneralIcon icon="edit" />
<span>{{ $t('general.edit') }}</span>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -564,22 +584,36 @@ const handleClickRow = (source: SourceType, tab?: string) => {
</div> </div>
<div class="ds-table-col justify-end gap-x-1 ds-table-actions" @click.stop> <div class="ds-table-col justify-end gap-x-1 ds-table-actions" @click.stop>
<div class="flex justify-end"> <div class="flex justify-end">
<NcDropdown v-if="!source.is_meta && !source.is_local" placement="bottomRight"> <NcDropdown placement="bottomRight">
<NcButton size="small" type="secondary"> <NcButton size="small" type="secondary">
<GeneralIcon icon="threeDotVertical" /> <GeneralIcon icon="threeDotVertical" />
</NcButton> </NcButton>
<template #overlay> <template #overlay>
<NcMenu variant="small"> <NcMenu variant="small">
<NcMenuItem @click="handleClickRow(source, 'edit')"> <NcMenuItemCopyId
<GeneralIcon icon="edit" /> :id="source.id"
<span>{{ $t('general.edit') }}</span> :tooltip="$t('labels.clickToCopySourceID')"
</NcMenuItem> :label="
$t('labels.sourceIdColon', {
sourceId: source.id,
})
"
/>
<NcDivider /> <template v-if="!source.is_meta && !source.is_local">
<NcMenuItem danger @click.stop="openDeleteBase(source)"> <NcDivider />
<GeneralIcon icon="delete" />
{{ $t('general.remove') }} <NcMenuItem @click="handleClickRow(source, 'edit')">
</NcMenuItem> <GeneralIcon icon="edit" />
<span>{{ $t('general.edit') }}</span>
</NcMenuItem>
<NcDivider />
<NcMenuItem danger @click.stop="openDeleteBase(source)">
<GeneralIcon icon="delete" />
{{ $t('general.remove') }}
</NcMenuItem>
</template>
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>

View File

@@ -283,9 +283,16 @@ const columns = [
{ {
key: 'created_at', key: 'created_at',
title: t('title.dateJoined'), title: t('title.dateJoined'),
basis: '25%', basis: '20%',
minWidth: 200, minWidth: 200,
}, },
{
key: 'action',
title: t('labels.actions'),
width: 110,
minWidth: 110,
justify: 'justify-end',
},
] as NcTableColumnProps[] ] as NcTableColumnProps[]
const customRow = (record: Record<string, any>) => ({ const customRow = (record: Record<string, any>) => ({
@@ -534,6 +541,26 @@ onBeforeUnmount(() => {
</span> </span>
</NcTooltip> </NcTooltip>
</div> </div>
<div v-if="column.key === 'action'">
<NcDropdown placement="bottomRight">
<NcButton size="small" type="secondary">
<component :is="iconMap.ncMoreVertical" />
</NcButton>
<template #overlay>
<NcMenu variant="small">
<NcMenuItemCopyId
:id="record.id"
:tooltip="$t('labels.clickToCopyUserID')"
:label="
$t('labels.userIdColon', {
userId: record.id,
})
"
/>
</NcMenu>
</template>
</NcDropdown>
</div>
</template> </template>
</NcTable> </NcTable>
</div> </div>

View File

@@ -554,36 +554,50 @@ watch(inviteDlg, (newVal) => {
</div> </div>
<div v-if="column.key === 'action'"> <div v-if="column.key === 'action'">
<NcDropdown v-if="isOwnerOrCreator || record.id === user.id"> <NcDropdown placement="bottomRight">
<NcButton size="small" type="secondary"> <NcButton size="small" type="secondary">
<component :is="iconMap.ncMoreVertical" /> <component :is="iconMap.ncMoreVertical" />
</NcButton> </NcButton>
<template #overlay> <template #overlay>
<NcMenu variant="small"> <NcMenu variant="small">
<template v-if="isAdminPanel"> <NcMenuItemCopyId
<NcMenuItem data-testid="nc-admin-org-user-delete"> :id="record.id"
<GeneralIcon icon="signout" /> :tooltip="$t('labels.clickToCopyUserID')"
<span>{{ $t('labels.signOutUser') }}</span> :label="
</NcMenuItem> $t('labels.userIdColon', {
userId: record.id,
})
"
/>
<template v-if="isOwnerOrCreator || record.id === user.id">
<NcDivider /> <NcDivider />
</template>
<NcTooltip :disabled="!isOnlyOneOwner || record.roles !== WorkspaceUserRoles.OWNER"> <template v-if="isAdminPanel">
<template #title> <NcMenuItem data-testid="nc-admin-org-user-delete">
{{ $t('tooltip.leaveWorkspace') }} <GeneralIcon icon="signout" />
<span>{{ $t('labels.signOutUser') }}</span>
</NcMenuItem>
<NcDivider />
</template> </template>
<NcMenuItem <NcTooltip :disabled="!isOnlyOneOwner || record.roles !== WorkspaceUserRoles.OWNER">
:disabled="!isDeleteOrUpdateAllowed(record)" <template #title>
danger {{ $t('tooltip.leaveWorkspace') }}
@click="removeCollaborator(record.id, currentWorkspace?.id)" </template>
> <NcMenuItem
<div v-if="removingCollaboratorMap[record.id]" class="h-4 w-4 flex items-center justify-center"> :disabled="!isDeleteOrUpdateAllowed(record)"
<GeneralLoader class="!flex-none !text-current" /> danger
</div> @click="removeCollaborator(record.id, currentWorkspace?.id)"
<GeneralIcon v-else icon="delete" /> >
{{ record.id === user.id ? t('activity.leaveWorkspace') : t('activity.removeMember') }} <div v-if="removingCollaboratorMap[record.id]" class="h-4 w-4 flex items-center justify-center">
</NcMenuItem> <GeneralLoader class="!flex-none !text-current" />
</NcTooltip> </div>
<GeneralIcon v-else icon="delete" />
{{ record.id === user.id ? t('activity.leaveWorkspace') : t('activity.removeMember') }}
</NcMenuItem>
</NcTooltip>
</template>
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>

View File

@@ -8,6 +8,8 @@ const { t } = useI18n()
const isErrored = ref(false) const isErrored = ref(false)
const isProfileUpdating = ref(false) const isProfileUpdating = ref(false)
const copyBtnRef = ref()
const form = ref<{ const form = ref<{
title: string title: string
email: string email: string
@@ -185,7 +187,26 @@ const onCancel = () => {
<div class="flex-1 flex flex-col gap-4"> <div class="flex-1 flex flex-col gap-4">
<div> <div>
<div class="text-nc-content-gray mb-2" data-rec="true">{{ $t('general.name') }}</div> <div class="flex items-center gap-1.5 justify-between mb-2">
<div class="text-nc-content-gray" data-rec="true">{{ $t('general.name') }}</div>
<NcTooltip v-if="user" :title="$t('labels.clickToCopyUserID')" placement="top" hide-on-click class="flex">
<div
data-rec="true"
class="flex items-center gap-1.5 text-bodyDefaultSm text-nc-content-gray-subtle2 cursor-pointer"
@click="copyBtnRef?.copyContent()"
>
{{ $t('labels.userIdColon', { userId: user?.id }) }}
<GeneralCopyButton
ref="copyBtnRef"
type="text"
size="xxsmall"
:content="user?.id"
:show-toast="false"
/>
</div>
</NcTooltip>
</div>
<a-form-item name="title" :rules="formRules.title" class="!my-0"> <a-form-item name="title" :rules="formRules.title" class="!my-0">
<a-input <a-input
v-model:value="form.title" v-model:value="form.title"

View File

@@ -337,6 +337,18 @@ onMounted(() => {
variant="small" variant="small"
@click="isBasesOptionsOpen[source!.id!] = false" @click="isBasesOptionsOpen[source!.id!] = false"
> >
<NcMenuItemCopyId
:id="source.id"
:tooltip="$t('labels.clickToCopySourceID')"
:label="
$t('labels.sourceIdColon', {
sourceId: source.id,
})
"
@click.stop
/>
<NcDivider />
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('baseRename')" v-if="isUIAllowed('baseRename')"
data-testid="nc-sidebar-source-rename" data-testid="nc-sidebar-source-rename"

View File

@@ -11,7 +11,7 @@ const { loadWorkspaces } = workspaceStore
const { appInfo } = useGlobal() const { appInfo } = useGlobal()
const { leftSidebarState, isLeftSidebarOpen, nonHiddenLeftSidebarWidth: leftSidebarWidth } = storeToRefs(useSidebarStore()) const { leftSidebarState, isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const viewportWidth = ref(window.innerWidth) const viewportWidth = ref(window.innerWidth)
const { navigateToTable } = useTablesStore() const { navigateToTable } = useTablesStore()
@@ -22,6 +22,8 @@ const { navigateToProject, isMobileMode } = useGlobal()
const isWorkspaceDropdownOpen = ref(false) const isWorkspaceDropdownOpen = ref(false)
const copyBtnRef = ref()
watch(isLeftSidebarOpen, () => { watch(isLeftSidebarOpen, () => {
isWorkspaceDropdownOpen.value = false isWorkspaceDropdownOpen.value = false
}) })
@@ -126,7 +128,7 @@ const onWorkspaceCreateClick = () => {
v-model:visible="isWorkspaceDropdownOpen" v-model:visible="isWorkspaceDropdownOpen"
class="h-full min-w-0 rounded-lg" class="h-full min-w-0 rounded-lg"
placement="bottomLeft" placement="bottomLeft"
:overlay-class-name="`nc-dropdown-workspace-menu !overflow-hidden ${isMiniSidebar ? '!left-1' : ''}`" :overlay-class-name="`nc-dropdown-workspace-menu !overflow-hidden ${isMiniSidebar ? '!left-1' : ''}`"
> >
<div <div
v-e="['c:workspace:menu']" v-e="['c:workspace:menu']"
@@ -163,13 +165,9 @@ const onWorkspaceCreateClick = () => {
</div> </div>
<template #overlay> <template #overlay>
<NcMenu <NcMenu class="nc-workspace-dropdown-inner min-w-[332px] max-w-[360px]" @click="isWorkspaceDropdownOpen = false">
class="nc-workspace-dropdown-inner"
:style="`width: ${leftSidebarWidth - 4}px`"
@click="isWorkspaceDropdownOpen = false"
>
<a-menu-item-group class="!border-t-0 w-full"> <a-menu-item-group class="!border-t-0 w-full">
<div class="flex gap-x-3 min-w-0 pl-4 pr-3 w-full py-3 items-center"> <div class="flex gap-x-3 min-w-0 pl-4 pr-3 w-full py-1 items-center">
<GeneralWorkspaceIcon :workspace="activeWorkspace" size="large" /> <GeneralWorkspaceIcon :workspace="activeWorkspace" size="large" />
<div class="flex-1 flex flex-col gap-y-0 max-w-[calc(100%-5.6rem)]"> <div class="flex-1 flex flex-col gap-y-0 max-w-[calc(100%-5.6rem)]">
<div <div
@@ -191,18 +189,34 @@ const onWorkspaceCreateClick = () => {
</template> </template>
</template> </template>
<template v-else> <template v-else>
<div class="nc-workspace-dropdown-active-workspace-info"> <div class="nc-workspace-dropdown-active-workspace-info truncate">
{{ activeWorkspace.payment?.plan?.title || 'Free Plan' }} {{ activeWorkspace.payment?.plan?.title || 'Free Plan' }}
</div> </div>
<template v-if="workspaceUserCount !== undefined"> <template v-if="workspaceUserCount !== undefined">
<div class="nc-workspace-dropdown-active-workspace-info">-</div> <div class="nc-workspace-dropdown-active-workspace-info">-</div>
<div class="nc-workspace-dropdown-active-workspace-info"> <div class="nc-workspace-dropdown-active-workspace-info truncate">
{{ workspaceUserCount }} {{ workspaceUserCount }}
{{ workspaceUserCount > 1 ? $t('labels.members').toLowerCase() : $t('objects.member').toLowerCase() }} {{ workspaceUserCount > 1 ? $t('labels.members').toLowerCase() : $t('objects.member').toLowerCase() }}
</div> </div>
</template> </template>
</template> </template>
</div> </div>
<NcTooltip :title="$t('labels.clickToCopyWorkspaceID')" placement="top" hide-on-click class="flex">
<div
class="flex items-center gap-1.5 nc-workspace-dropdown-active-workspace-info cursor-pointer"
@click="copyBtnRef?.copyContent()"
>
{{ $t('labels.workspaceId', { workspaceId: activeWorkspace.id }) }}
<GeneralCopyButton
ref="copyBtnRef"
type="text"
size="xxsmall"
:content="activeWorkspace.id"
:show-toast="false"
/>
</div>
</NcTooltip>
</div> </div>
<NcTooltip v-if="activeWorkspace.roles === WorkspaceUserRoles.OWNER" class="!z-1" placement="bottomRight"> <NcTooltip v-if="activeWorkspace.roles === WorkspaceUserRoles.OWNER" class="!z-1" placement="bottomRight">
<template #title> <template #title>
@@ -220,14 +234,13 @@ const onWorkspaceCreateClick = () => {
:list="otherWorkspaces" :list="otherWorkspaces"
height="auto" height="auto"
:options="{ itemHeight: 40 }" :options="{ itemHeight: 40 }"
class="my-1 max-h-300px nc-scrollbar-md" class="my-1 max-h-[min(60vh,600px)] nc-scrollbar-md w-full"
> >
<template #default="{ data: workspace }"> <template #default="{ data: workspace }">
<NcMenuItem :key="workspace.id!" class="!h-[40px]" @click="switchWorkspace(workspace.id!)"> <NcMenuItem :key="workspace.id!" class="!h-[40px]" inner-class="w-full" @click="switchWorkspace(workspace.id!)">
<div <div
class="nc-workspace-menu-item group capitalize max-w-[calc(100%-3.5rem)] flex items-center" class="nc-workspace-menu-item group capitalize flex items-center w-full max-w-full"
data-testid="nc-workspace-list" data-testid="nc-workspace-list"
:style="`width: ${leftSidebarWidth + 26}px`"
> >
<div class="flex flex-row w-[calc(100%-2rem)] truncate items-center gap-2"> <div class="flex flex-row w-[calc(100%-2rem)] truncate items-center gap-2">
<GeneralWorkspaceIcon :workspace="workspace" size="medium" /> <GeneralWorkspaceIcon :workspace="workspace" size="medium" />

View File

@@ -29,6 +29,8 @@ const { showInfoModal } = useNcConfirmModal()
const leavedWsUserId = ref('') const leavedWsUserId = ref('')
const copyBtnRef = ref()
const formValidator = ref() const formValidator = ref()
const isErrored = ref(false) const isErrored = ref(false)
const isWorkspaceUpdating = ref(false) const isWorkspaceUpdating = ref(false)
@@ -367,7 +369,25 @@ const onCancel = () => {
</GeneralIconSelector> </GeneralIconSelector>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class="text-sm text-nc-content-gray-subtle2">{{ $t('general.name') }}</div> <div class="flex items-center gap-1.5 justify-between">
<div class="text-sm text-nc-content-gray-subtle2">{{ $t('general.name') }}</div>
<NcTooltip :title="$t('labels.clickToCopyWorkspaceID')" placement="top" hide-on-click class="flex">
<div
class="flex items-center gap-1.5 text-bodyDefaultSm text-nc-content-gray-subtle2 cursor-pointer"
@click="copyBtnRef?.copyContent()"
>
{{ $t('labels.workspaceId', { workspaceId: currentWorkspace.id }) }}
<GeneralCopyButton
ref="copyBtnRef"
type="text"
size="xxsmall"
:content="currentWorkspace.id"
:show-toast="false"
/>
</div>
</NcTooltip>
</div>
<a-form-item name="title" :rules="formRules.title" class="!mt-2 !mb-0"> <a-form-item name="title" :rules="formRules.title" class="!mt-2 !mb-0">
<a-input <a-input
v-model:value="form.title" v-model:value="form.title"

View File

@@ -1223,6 +1223,9 @@
"noToken": "No Token", "noToken": "No Token",
"tokenLimit": "Only one token per user is allowed", "tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached", "duplicateAttachment": "File with name {filename} already attached",
"workspaceId": "WORKSPACE ID: {workspaceId}",
"userIdColon": "USER ID: {userId}",
"sourceIdColon": "SOURCE ID: {sourceId}",
"tableIdColon": "TABLE ID: {tableId}", "tableIdColon": "TABLE ID: {tableId}",
"baseIdColon": "BASE ID: {baseId}", "baseIdColon": "BASE ID: {baseId}",
"scriptIdColon": "SCRIPT ID: {scriptId}", "scriptIdColon": "SCRIPT ID: {scriptId}",
@@ -1267,6 +1270,9 @@
"clickToDownload": "Click to download", "clickToDownload": "Click to download",
"forRole": "for role", "forRole": "for role",
"clickToCopy": "Click to copy", "clickToCopy": "Click to copy",
"clickToCopyWorkspaceID": "Click to copy Workspace ID",
"clickToCopyUserID": "Click to copy User ID",
"clickToCopySourceID": "Click to copy Source ID",
"clickToCopyTableID": "Click to copy Table ID", "clickToCopyTableID": "Click to copy Table ID",
"clickToCopyBaseID": "Click to copy Base ID", "clickToCopyBaseID": "Click to copy Base ID",
"clickToCopyScriptID": "Click to copy Script ID", "clickToCopyScriptID": "Click to copy Script ID",