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 isProfileUpdating = ref(false)
const copyBtnRef = ref()
const form = ref<{
title: string
email: string
@@ -184,7 +186,26 @@ const onCancel = () => {
<div class="flex-1 flex flex-col gap-4">
<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-input
v-model:value="form.title"

View File

@@ -328,14 +328,8 @@ const userRoleOptions = [
{{ $t(`objects.roleType.orgLevelCreator`) }}
</div>
</template>
<div
v-if="column.key === 'action'"
class="flex items-center gap-2"
:class="{
'opacity-0 pointer-events-none': el.roles?.includes('super'),
}"
>
<NcDropdown :trigger="['click']">
<div v-if="column.key === 'action'" class="flex items-center gap-2">
<NcDropdown :trigger="['click']" placement="bottomRight">
<NcButton size="xsmall" type="ghost">
<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)"
@@ -344,7 +338,19 @@ const userRoleOptions = [
<template #overlay>
<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')">
<NcDivider />
<!-- Resend invite Email -->
<NcMenuItem @click="resendInvite(el)">
<component :is="iconMap.email" class="flex text-gray-600" />
@@ -358,13 +364,13 @@ const userRoleOptions = [
<component :is="iconMap.copy" class="flex text-gray-600" />
<div>{{ $t('activity.copyPasswordResetURL') }}</div>
</NcMenuItem>
</template>
<template v-if="el.id !== loggedInUser?.id">
<NcDivider v-if="!el.roles?.includes('super')" />
<NcMenuItem data-rec="true" danger @click="openDeleteModal(el)">
<MaterialSymbolsDeleteOutlineRounded />
{{ $t('general.remove') }} {{ $t('objects.user') }}
</NcMenuItem>
<template v-if="el.id !== loggedInUser?.id">
<NcDivider v-if="!el.roles?.includes('super')" />
<NcMenuItem data-rec="true" danger @click="openDeleteModal(el)">
<MaterialSymbolsDeleteOutlineRounded />
{{ $t('general.remove') }} {{ $t('objects.user') }}
</NcMenuItem>
</template>
</template>
</NcMenu>
</template>

View File

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

View File

@@ -283,9 +283,16 @@ const columns = [
{
key: 'created_at',
title: t('title.dateJoined'),
basis: '25%',
basis: '20%',
minWidth: 200,
},
{
key: 'action',
title: t('labels.actions'),
width: 110,
minWidth: 110,
justify: 'justify-end',
},
] as NcTableColumnProps[]
const customRow = (record: Record<string, any>) => ({
@@ -534,6 +541,26 @@ onBeforeUnmount(() => {
</span>
</NcTooltip>
</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>
</NcTable>
</div>

View File

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

View File

@@ -8,6 +8,8 @@ const { t } = useI18n()
const isErrored = ref(false)
const isProfileUpdating = ref(false)
const copyBtnRef = ref()
const form = ref<{
title: string
email: string
@@ -185,7 +187,26 @@ const onCancel = () => {
<div class="flex-1 flex flex-col gap-4">
<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-input
v-model:value="form.title"

View File

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

View File

@@ -11,7 +11,7 @@ const { loadWorkspaces } = workspaceStore
const { appInfo } = useGlobal()
const { leftSidebarState, isLeftSidebarOpen, nonHiddenLeftSidebarWidth: leftSidebarWidth } = storeToRefs(useSidebarStore())
const { leftSidebarState, isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const viewportWidth = ref(window.innerWidth)
const { navigateToTable } = useTablesStore()
@@ -22,6 +22,8 @@ const { navigateToProject, isMobileMode } = useGlobal()
const isWorkspaceDropdownOpen = ref(false)
const copyBtnRef = ref()
watch(isLeftSidebarOpen, () => {
isWorkspaceDropdownOpen.value = false
})
@@ -126,7 +128,7 @@ const onWorkspaceCreateClick = () => {
v-model:visible="isWorkspaceDropdownOpen"
class="h-full min-w-0 rounded-lg"
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
v-e="['c:workspace:menu']"
@@ -163,13 +165,9 @@ const onWorkspaceCreateClick = () => {
</div>
<template #overlay>
<NcMenu
class="nc-workspace-dropdown-inner"
:style="`width: ${leftSidebarWidth - 4}px`"
@click="isWorkspaceDropdownOpen = false"
>
<NcMenu class="nc-workspace-dropdown-inner min-w-[332px] max-w-[360px]" @click="isWorkspaceDropdownOpen = false">
<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" />
<div class="flex-1 flex flex-col gap-y-0 max-w-[calc(100%-5.6rem)]">
<div
@@ -191,18 +189,34 @@ const onWorkspaceCreateClick = () => {
</template>
</template>
<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' }}
</div>
<template v-if="workspaceUserCount !== undefined">
<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 > 1 ? $t('labels.members').toLowerCase() : $t('objects.member').toLowerCase() }}
</div>
</template>
</template>
</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>
<NcTooltip v-if="activeWorkspace.roles === WorkspaceUserRoles.OWNER" class="!z-1" placement="bottomRight">
<template #title>
@@ -220,14 +234,13 @@ const onWorkspaceCreateClick = () => {
:list="otherWorkspaces"
height="auto"
: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 }">
<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
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"
:style="`width: ${leftSidebarWidth + 26}px`"
>
<div class="flex flex-row w-[calc(100%-2rem)] truncate items-center gap-2">
<GeneralWorkspaceIcon :workspace="workspace" size="medium" />

View File

@@ -29,6 +29,8 @@ const { showInfoModal } = useNcConfirmModal()
const leavedWsUserId = ref('')
const copyBtnRef = ref()
const formValidator = ref()
const isErrored = ref(false)
const isWorkspaceUpdating = ref(false)
@@ -367,7 +369,25 @@ const onCancel = () => {
</GeneralIconSelector>
</div>
<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-input
v-model:value="form.title"

View File

@@ -1223,6 +1223,9 @@
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
"workspaceId": "WORKSPACE ID: {workspaceId}",
"userIdColon": "USER ID: {userId}",
"sourceIdColon": "SOURCE ID: {sourceId}",
"tableIdColon": "TABLE ID: {tableId}",
"baseIdColon": "BASE ID: {baseId}",
"scriptIdColon": "SCRIPT ID: {scriptId}",
@@ -1267,6 +1270,9 @@
"clickToDownload": "Click to download",
"forRole": "for role",
"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",
"clickToCopyBaseID": "Click to copy Base ID",
"clickToCopyScriptID": "Click to copy Script ID",