Browse Source

Nc feat: header revamp (#9204)

* fix(nc-gui): update topbar breadcrumb divider

* feat(nc-gui): custom list component setup

* fix(nc-gui): update reload view data tooltip

* feat(nc-gui): custom list component

* feat(nc-gui): add table list menu

* fix(nc-gui): small changes

* fix(nc-gui): add bases list dropdown

* fix(nc-gui): show chevron icon in mobile view

* feat(nc-gui): add view list dropdown in topbar

* fix(nc-gui): auto scroll selected list option on open dropdown

* feat(nc-gui): add typedocs for each fun from custom list component

* chore(nc-gui): add typedocs for new functions

* fix(nc-gui): view search issue on default view

* fix(nc-gui): reset selected option hover state on search input

* fix(nc-gui): font weight issue

* fix(nc-gui): show reload data topbar option

* fix(nc-gui): change view action menu position

* fix(nc-gui): font weight issue

* feat(nc-gui): create new table/view from topbar

* fix(nc-gui): update other page headers

* fix(nc-gui): project view header

* fix(nc-gui): update admin panel workspaces page header

* fix(nc-gui): admin panel base/workspace user page header

* fix(nc-gui): admin panel scroll issue

* fix(nc-gui): update project home page

* fix(nc-gui): table list scroll issue

* chore(nc-gui): lint

* fix(nc-gui): reset breadcrumb btn hover state on open dropdown

* fix(nc-gui): review changes

* fix(nc-gui): use slash icon instead of text

* fix(nc-gui): pr review changes

* fix(nc-gui): details tab height issue

* fix(nc-gui): add user account pages breadcrumb

* fix(nc-gui): hide rename view option

* fix(nc-gui): disable scrollIntoView on base rename

* fix(nc-gui): on rename view select text

* fix(nc-gui): user menu overflow issue if sidebar baselist is scrollable

* feat(nc-gui): use virtual scrolling for NcList component

* fix(nc-gui): reduce chevron icon opacity

* chore(nc-gui): lint

* fix(nc-gui): ai review changes

* fix(nc-gui): view rename input focus issue

* fix(nc-gui): topbar width issue

* fix(nc-gui): udpate toolbar height

* fix(nc-gui): update chevron icon from breadcrumb

* fix(nc-gui): update breadcrumb icon size

* fix(nc-gui): add min width for breadcrumb

* fix(nc-gui): add topbar bottom border

* fix(nc-gui): details tab heigth and alignment issue

* fix(nc-gui): hide basename and show only icon

* fix(nc-gui): update NcList component

* fix(nc-gui): update admin panel header

* fix(nc-gui): add header in account settings pages

* fix(nc-gui): add account pages header oss

* fix(nc-gui): udpate max width

* chore(nc-gui): lint

* fix(nc-gui(: reduce topbar top padding

* fix(nc-gui): typo error

* fix(nc-gui): review changes

* fix(nc-gui): review changes

* fix(nc-gui): slash icon conflict

* fix(nc-gui): review changes

* fix(nc-gui): remove chevron icon & add list wrapper div to control height

* fix(nc-gui): ncList keyboard navigation issue

* chore(nc-gui): lint
pull/9230/head
Ramesh Mane 4 months ago committed by GitHub
parent
commit
57c36d9d71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      packages/nc-gui/assets/nc-icons/chevron-down-small.svg
  2. 5
      packages/nc-gui/assets/nc-icons/chevron-up-down-small.svg
  3. 3
      packages/nc-gui/assets/nc-icons/chevron-up-small.svg
  4. 3
      packages/nc-gui/assets/nc-icons/home1.svg
  5. 10
      packages/nc-gui/assets/nc-icons/slash1.svg
  6. 27
      packages/nc-gui/assets/style.scss
  7. 15
      packages/nc-gui/components/account/AppStore.vue
  8. 112
      packages/nc-gui/components/account/Breadcrumb.vue
  9. 5
      packages/nc-gui/components/account/Integration.vue
  10. 17
      packages/nc-gui/components/account/Profile.vue
  11. 17
      packages/nc-gui/components/account/ResetPassword.vue
  12. 17
      packages/nc-gui/components/account/SignupSettings.vue
  13. 41
      packages/nc-gui/components/account/Token.vue
  14. 21
      packages/nc-gui/components/account/UserList.vue
  15. 2
      packages/nc-gui/components/dashboard/Sidebar.vue
  16. 21
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  17. 17
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  18. 22
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  19. 6
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  20. 6
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  21. 2
      packages/nc-gui/components/general/ApiLoader.vue
  22. 2
      packages/nc-gui/components/general/OpenLeftSidebarBtn.vue
  23. 2
      packages/nc-gui/components/general/ShareProject.vue
  24. 2
      packages/nc-gui/components/general/language/index.vue
  25. 338
      packages/nc-gui/components/nc/List.vue
  26. 36
      packages/nc-gui/components/nc/PageHeader.vue
  27. 54
      packages/nc-gui/components/project/AccessSettings.vue
  28. 8
      packages/nc-gui/components/project/AllTables.vue
  29. 22
      packages/nc-gui/components/project/View.vue
  30. 11
      packages/nc-gui/components/smartsheet/Details.vue
  31. 4
      packages/nc-gui/components/smartsheet/Form.vue
  32. 5
      packages/nc-gui/components/smartsheet/Toolbar.vue
  33. 22
      packages/nc-gui/components/smartsheet/Topbar.vue
  34. 2
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  35. 2
      packages/nc-gui/components/smartsheet/toolbar/LockType.vue
  36. 35
      packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue
  37. 4
      packages/nc-gui/components/smartsheet/toolbar/Reload.vue
  38. 7
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  39. 167
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  40. 80
      packages/nc-gui/components/smartsheet/topbar/ProjectListDropdown.vue
  41. 12
      packages/nc-gui/components/smartsheet/topbar/SelectMode.vue
  42. 159
      packages/nc-gui/components/smartsheet/topbar/TableListDropdown.vue
  43. 303
      packages/nc-gui/components/smartsheet/topbar/ViewListDropdown.vue
  44. 19
      packages/nc-gui/components/workspace/AuditLogs.vue
  45. 8
      packages/nc-gui/components/workspace/CollaboratorsList.vue
  46. 64
      packages/nc-gui/components/workspace/View.vue
  47. 16
      packages/nc-gui/components/workspace/integrations/view.vue
  48. 46
      packages/nc-gui/pages/account/index.vue
  49. 4
      packages/nc-gui/pages/account/index/[page].vue
  50. 2
      packages/nc-gui/pages/account/index/users/[[nestedPage]].vue
  51. 3
      packages/nc-gui/store/config.ts
  52. 12
      packages/nc-gui/utils/iconUtils.ts

3
packages/nc-gui/assets/nc-icons/chevron-down-small.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M3.75 4.5L6 6.75L8.25 4.5" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 234 B

5
packages/nc-gui/assets/nc-icons/chevron-up-down-small.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M11 6L8 3L5 6" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 10L8 13L11 10" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 346 B

3
packages/nc-gui/assets/nc-icons/chevron-up-small.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M8.25 6.75L6 4.5L3.75 6.75" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

3
packages/nc-gui/assets/nc-icons/home1.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 1.6665L2.5 7.49984V16.6665C2.5 17.1085 2.67559 17.5325 2.98816 17.845C3.30072 18.1576 3.72464 18.3332 4.16667 18.3332L7.5 18.3332V12.4999H12.5V18.3332L15.8333 18.3332C16.2754 18.3332 16.6993 18.1576 17.0118 17.845C17.3244 17.5325 17.5 17.1085 17.5 16.6665V7.49984L10 1.6665Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

10
packages/nc-gui/assets/nc-icons/slash1.svg

@ -0,0 +1,10 @@
<svg width="6" height="16" viewBox="0 0 6 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_242_24839)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.01007 0.499643C5.36572 0.594938 5.57677 0.960496 5.48148 1.31614L1.80722 15.0287C1.71192 15.3843 1.34637 15.5954 0.990723 15.5001C0.635078 15.4048 0.424023 15.0392 0.519318 14.6836L4.19358 0.971048C4.28887 0.615404 4.65443 0.404349 5.01007 0.499643Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_242_24839">
<rect width="6" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 571 B

27
packages/nc-gui/assets/style.scss

@ -25,8 +25,8 @@ body {
} }
:root { :root {
--toolbar-height: 2.25rem; --toolbar-height: 2.75rem;
--topbar-height: 3.1rem; --topbar-height: 3rem;
--sidebar-bottom-height: 8.5rem; --sidebar-bottom-height: 8.5rem;
--new-header-height: 3.5rem; --new-header-height: 3.5rem;
--tw-text-opacity: 1; --tw-text-opacity: 1;
@ -935,3 +935,26 @@ svg.nc-virtual-cell-icon {
.nc-edit-or-add-provider-wrapper { .nc-edit-or-add-provider-wrapper {
@apply overflow-auto max-h-[max(80vh,500px)] min-w-[384px] rounded-xl shadow-lg shadow-gray-300; @apply overflow-auto max-h-[max(80vh,500px)] min-w-[384px] rounded-xl shadow-lg shadow-gray-300;
} }
.nc-breadcrumb {
@apply text-sm text-gray-700 font-weight-500 flex items-center;
&:not(.nc-no-negative-margin) {
@apply -ml-1;
}
.nc-breadcrumb-item {
@apply h-8 px-2 flex items-center gap-2 my-0;
&.nc-clickable {
@apply cursor-pointer select-none rounded-lg hover:(bg-gray-100 text-gray-900);
}
&.active {
@apply font-bold text-gray-800;
}
}
}
.nc-breadcrumb-divider {
@apply flex-none text-gray-500 !stroke-transparent;
}

15
packages/nc-gui/components/account/AppStore.vue

@ -1,8 +1,19 @@
<template> <template>
<div class="h-full overflow-y-auto scrollbar-thin-dull pt-2 px-5"> <div class="flex flex-col">
<div class="text-xl mt-4 mb-8 text-left font-weight-bold">{{ $t('title.appStore') }}</div> <NcPageHeader>
<template #icon>
<GeneralIcon icon="appStore" class="flex-none text-gray-700 text-[20px] h-5 w-5" />
</template>
<template #title>
<span data-rec="true">
{{ $t('title.appStore') }}
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div> <div>
<LazyDashboardSettingsAppStore /> <LazyDashboardSettingsAppStore />
</div> </div>
</div> </div>
</div>
</template> </template>

112
packages/nc-gui/components/account/Breadcrumb.vue

@ -0,0 +1,112 @@
<script lang="ts" setup>
const route = useRoute()
interface BreadcrumbType {
title: string
active?: boolean
}
const { t } = useI18n()
const breadcrumb = computed<BreadcrumbType[]>(() => {
const payload: BreadcrumbType[] = [
{
title: 'Account',
},
]
switch (route.params.page) {
case 'profile': {
payload.push({
title: t('labels.profile'),
active: true,
})
break
}
case 'tokens': {
payload.push({
title: t('title.tokens'),
active: true,
})
break
}
case 'audit': {
payload.push({
title: t('title.auditLogs'),
active: true,
})
break
}
case 'apps': {
payload.push({
title: t('title.appStore'),
active: true,
})
break
}
}
switch (route.params.nestedPage) {
case 'password-reset': {
payload.push(
...[
{
title: t('objects.users'),
},
{
title: t('title.resetPasswordMenu'),
active: true,
},
],
)
break
}
case 'settings': {
payload.push(
...[
{
title: t('objects.users'),
},
{
title: t('activity.settings'),
active: true,
},
],
)
break
}
}
if ((route.params.page === undefined && route.params.nestedPage === '') || route.params.nestedPage === 'list') {
payload.push(
...[
{
title: t('objects.users'),
},
{
title: t('title.userManagement'),
active: true,
},
],
)
}
return payload
})
</script>
<template>
<div class="nc-breadcrumb">
<template v-for="(item, i) of breadcrumb" :key="i">
<div
class="nc-breadcrumb-item"
:class="{
active: item.active,
}"
>
{{ item.title }}
</div>
<GeneralIcon v-if="i !== breadcrumb.length - 1" icon="ncSlash1" class="nc-breadcrumb-divider" />
</template>
</div>
</template>

5
packages/nc-gui/components/account/Integration.vue

@ -1,5 +0,0 @@
<script lang="ts" setup></script>
<template>
<WorkspaceIntegrationsView />
</template>

17
packages/nc-gui/components/account/Profile.vue

@ -61,9 +61,19 @@ const onValidate = async (_: any, valid: boolean) => {
</script> </script>
<template> <template>
<div class="flex flex-col items-center"> <div class="flex flex-col">
<div class="flex flex-col w-150"> <NcPageHeader>
<div class="flex font-bold text-xl" data-rec="true">{{ $t('labels.profile') }}</div> <template #icon>
<GeneralIcon class="flex-none !h-5 !w-5" icon="user" />
</template>
<template #title>
<span data-rec="true">
{{ $t('labels.profile') }}
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="flex flex-col w-150 mx-auto">
<div class="mt-5 flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-y-2"> <div class="mt-5 flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-y-2">
<div class="flex font-medium text-base" data-rec="true">{{ $t('labels.accountDetails') }}</div> <div class="flex font-medium text-base" data-rec="true">{{ $t('labels.accountDetails') }}</div>
<div class="flex text-gray-500" data-rec="true">{{ $t('labels.controlAppearance') }}</div> <div class="flex text-gray-500" data-rec="true">{{ $t('labels.controlAppearance') }}</div>
@ -116,4 +126,5 @@ const onValidate = async (_: any, valid: boolean) => {
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>

17
packages/nc-gui/components/account/ResetPassword.vue

@ -57,8 +57,19 @@ const resetError = () => {
</script> </script>
<template> <template>
<div class="mx-auto relative flex flex-col justify-start gap-2 w-full px-8 md:(bg-white) max-w-[900px]"> <div class="flex flex-col">
<div class="text-xl my-4 text-left font-weight-bold">{{ $t('activity.changePwd') }}</div> <NcPageHeader>
<template #icon>
<GeneralIcon icon="passwordChange" class="flex-none text-gray-700 text-[20px] h-5 w-5" />
</template>
<template #title>
<span data-rec="true">
{{ $t('activity.changePwd') }}
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="mx-auto relative flex flex-col justify-start gap-2 w-full md:(bg-white) max-w-[900px]">
<a-form <a-form
ref="formValidator" ref="formValidator"
data-testid="nc-user-settings-form" data-testid="nc-user-settings-form"
@ -136,6 +147,8 @@ const resetError = () => {
</div> </div>
</a-form> </a-form>
</div> </div>
</div>
</div>
</template> </template>
<style lang="scss"> <style lang="scss">

17
packages/nc-gui/components/account/SignupSettings.vue

@ -29,8 +29,19 @@ loadSettings()
</script> </script>
<template> <template>
<div class="p-7 flex flex-col items-center"> <div class="flex flex-col">
<h1 class="text-2xl mt-4 mb-5 pl-3.5 font-bold">{{ t('activity.settings') }}</h1> <NcPageHeader>
<template #icon>
<GeneralIcon icon="settings" class="flex-none text-[20px] text-gray-700 h-5 w-5" />
</template>
<template #title>
<span data-rec="true">
{{ $t('activity.settings') }}
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="flex flex-col items-center">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a-form-item> <a-form-item>
<a-checkbox <a-checkbox
@ -46,6 +57,8 @@ loadSettings()
</span> </span>
</div> </div>
</div> </div>
</div>
</div>
</template> </template>
<style scoped> <style scoped>

41
packages/nc-gui/components/account/Token.vue

@ -224,10 +224,21 @@ const handleCancel = () => {
</script> </script>
<template> <template>
<div class="h-full pt-2"> <div class="flex flex-col">
<div class="max-w-202 mx-auto px-4 h-full" data-testid="nc-token-list"> <NcPageHeader>
<div class="py-2 flex gap-4 items-baseline justify-between"> <template #icon>
<h6 class="text-2xl text-left font-bold" data-rec="true">{{ $t('title.apiTokens') }}</h6> <MdiShieldKeyOutline class="flex-none text-gray-700 h-5 w-5" />
</template>
<template #title>
<span data-rec="true">
{{ $t('title.tokens') }}
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="max-w-202 mx-auto h-full w-full" data-testid="nc-token-list">
<div class="flex gap-4 items-baseline justify-between">
<h6 class="text-xl text-left font-bold my-0" data-rec="true">{{ $t('title.apiTokens') }}</h6>
<NcTooltip v-if="tokens.length" :disabled="!(isEeUI && tokens.length)"> <NcTooltip v-if="tokens.length" :disabled="!(isEeUI && tokens.length)">
<template #title>{{ $t('labels.tokenLimit') }}</template> <template #title>{{ $t('labels.tokenLimit') }}</template>
<NcButton <NcButton
@ -249,8 +260,8 @@ const handleCancel = () => {
</NcTooltip> </NcTooltip>
</div> </div>
<span data-rec="true">{{ $t('msg.apiTokenCreate') }}</span> <span data-rec="true">{{ $t('msg.apiTokenCreate') }}</span>
<div v-if="!isLoadingAllTokens && (tokens.length || showNewTokenModal)" class="mt-5 h-[calc(100%-13rem)]"> <div v-if="!isLoadingAllTokens && (tokens.length || showNewTokenModal)" class="mt-6 h-full max-h-[calc(100%-80px)]">
<div class="h-full w-full !overflow-hidden rounded-md"> <div class="h-full w-full overflow-y-auto rounded-md">
<div class="flex w-full pl-5 bg-gray-50 border-1 rounded-t-md"> <div class="flex w-full pl-5 bg-gray-50 border-1 rounded-t-md">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9" data-rec="true">{{ $t('title.tokenName') }}</span> <span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9" data-rec="true">{{ $t('title.tokenName') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9 text-start" data-rec="true">{{ <span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9 text-start" data-rec="true">{{
@ -263,7 +274,7 @@ const handleCancel = () => {
$t('labels.actions') $t('labels.actions')
}}</span> }}</span>
</div> </div>
<div class="nc-scrollbar-md !overflow-y-auto flex flex-col h-[calc(100%-5rem)]"> <div class="nc-scrollbar-md !overflow-y-auto flex flex-col h-[calc(100%-52px)]">
<div v-if="showNewTokenModal"> <div v-if="showNewTokenModal">
<div <div
class="flex gap-5 px-3 py-2.5 text-gray-500 font-medium text-3.5 w-full nc-token-generate border-b-1 border-l-1 border-r-1" class="flex gap-5 px-3 py-2.5 text-gray-500 font-medium text-3.5 w-full nc-token-generate border-b-1 border-l-1 border-r-1"
@ -291,7 +302,13 @@ const handleCancel = () => {
<NcButton v-if="!isLoading" type="secondary" size="small" @click="handleCancel"> <NcButton v-if="!isLoading" type="secondary" size="small" @click="handleCancel">
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</NcButton> </NcButton>
<NcButton type="primary" size="sm" :loading="isLoading" data-testid="nc-token-save-btn" @click="generateToken"> <NcButton
type="primary"
size="sm"
:loading="isLoading"
data-testid="nc-token-save-btn"
@click="generateToken"
>
{{ $t('general.save') }} {{ $t('general.save') }}
</NcButton> </NcButton>
</div> </div>
@ -368,7 +385,12 @@ const handleCancel = () => {
<div class="text-sm text-gray-700"> <div class="text-sm text-gray-700">
{{ $t('placeholder.noTokenCreatedLabel') }} {{ $t('placeholder.noTokenCreatedLabel') }}
</div> </div>
<NcButton class="!rounded-lg !py-3 !h-10" data-testid="nc-token-create" type="primary" @click="showNewTokenModal = true"> <NcButton
class="!rounded-lg !py-3 !h-10"
data-testid="nc-token-create"
type="primary"
@click="showNewTokenModal = true"
>
<span class="hidden md:block" data-rec="true"> <span class="hidden md:block" data-rec="true">
{{ $t('title.createNewToken') }} {{ $t('title.createNewToken') }}
</span> </span>
@ -408,6 +430,7 @@ const handleCancel = () => {
</template> </template>
</GeneralDeleteModal> </GeneralDeleteModal>
</div> </div>
</div>
</template> </template>
<style> <style>

21
packages/nc-gui/components/account/UserList.vue

@ -211,10 +211,21 @@ const columns = [
</script> </script>
<template> <template>
<div data-testid="nc-super-user-list" class="h-full px-6"> <div class="flex flex-col" data-testid="nc-super-user-list">
<NcPageHeader>
<template #icon>
<GeneralIcon icon="users" class="flex-none text-gray-700 h-5 w-5" />
</template>
<template #title>
<span data-rec="true">
{{ $t('title.userManagement') }}
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="h-full">
<div class="max-w-195 mx-auto h-full"> <div class="max-w-195 mx-auto h-full">
<div class="text-2xl text-left font-weight-bold mb-4" data-rec="true">{{ $t('title.userMgmt') }}</div> <div class="flex gap-4 items-center justify-between">
<div class="py-2 flex gap-4 items-center justify-between">
<a-input <a-input
v-model:value="searchText" v-model:value="searchText"
class="!max-w-90 !rounded-md" class="!max-w-90 !rounded-md"
@ -240,7 +251,7 @@ const columns = [
:columns="columns" :columns="columns"
:data="sortedUsers" :data="sortedUsers"
:is-data-loading="isLoading" :is-data-loading="isLoading"
class="h-[calc(100%-140px)] max-w-250 mt-4" class="h-[calc(100%-58px)] max-w-250 mt-6"
> >
<template #bodyCell="{ column, record: el }"> <template #bodyCell="{ column, record: el }">
<div v-if="column.key === 'email'" class="w-full"> <div v-if="column.key === 'email'" class="w-full">
@ -405,6 +416,8 @@ const columns = [
<LazyAccountUsersModal :key="userMadalKey" :show="showUserModal" @closed="showUserModal = false" @reload="loadUsers" /> <LazyAccountUsersModal :key="userMadalKey" :show="showUserModal" @closed="showUserModal = false" @reload="loadUsers" />
</div> </div>
</div> </div>
</div>
</div>
</template> </template>
<style scoped> <style scoped>

2
packages/nc-gui/components/dashboard/Sidebar.vue

@ -56,7 +56,7 @@ onUnmounted(() => {
> >
<DashboardTreeView v-if="!isWorkspaceLoading" /> <DashboardTreeView v-if="!isWorkspaceLoading" />
</div> </div>
<div v-if="!isSharedBase" class="overflow-auto"> <div v-if="!isSharedBase" class="flex-none overflow-auto">
<GeneralGift v-if="!isEeUI" /> <GeneralGift v-if="!isEeUI" />
<div class="border-t-1 w-full"></div> <div class="border-t-1 w-full"></div>
<DashboardSidebarBeforeUserInfo /> <DashboardSidebarBeforeUserInfo />

21
packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue

@ -32,6 +32,27 @@ const overlayClassName = computed(() => {
return 'nc-view-create-dropdown' return 'nc-view-create-dropdown'
}) })
/**
* Opens a modal for creating or editing a view.
*
* @param options - The options for opening the modal.
* @param options.title - The title of the modal. Default is an empty string.
* @param options.type - The type of view to create or edit.
* @param options.copyViewId - The ID of the view to copy, if creating a copy.
* @param options.groupingFieldColumnId - The ID of the column to use for grouping, if applicable.
* @param options.calendarRange - The date range for calendar views.
* @param options.coverImageColumnId - The ID of the column to use for cover images, if applicable.
*
* @returns A Promise that resolves when the modal operation is complete.
*
* @remarks
* This function opens a modal dialog for creating or editing a view.
* It handles the dialog state, view creation, and navigation to the newly created view.
* After creating a view, it refreshes the command palette and reloads the views.
*
* @see {@link packages/nc-gui/components/smartsheet/topbar/ViewListDropdown.vue} for a similar implementation of view creation dialog.
* If this function is updated, consider updating the other implementations as well.
*/
async function onOpenModal({ async function onOpenModal({
title = '', title = '',
type, type,

17
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -112,7 +112,7 @@ const enableEditMode = () => {
nextTick(() => { nextTick(() => {
input.value?.focus() input.value?.focus()
input.value?.select() input.value?.select()
input.value?.scrollIntoView() // input.value?.scrollIntoView()
}) })
} }
@ -128,7 +128,7 @@ const enableEditModeForSource = (sourceId: string) => {
if (!input) return if (!input) return
input?.focus() input?.focus()
input?.select() input?.select()
input?.scrollIntoView() // input?.scrollIntoView()
}) })
} }
@ -225,6 +225,19 @@ const setColor = async (color: string, base: BaseType) => {
} }
} }
/**
* Opens a dialog to create a new table.
*
* @returns {void}
*
* @remarks
* This function is triggered when the user initiates the table creation process.
* It opens a dialog for table creation, handles the dialog closure,
* and potentially scrolls to the newly created table.
*
* @see {@link packages/nc-gui/components/smartsheet/topbar/TableListDropdown.vue} for a similar implementation
* of table creation dialog. If this function is updated, consider updating the other implementation as well.
*/
function openTableCreateDialog(sourceIndex?: number | undefined) { function openTableCreateDialog(sourceIndex?: number | undefined) {
const isOpen = ref(true) const isOpen = ref(true)
let sourceId = base.value!.sources?.[0].id let sourceId = base.value!.sources?.[0].id

22
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import type { TableType, ViewType, ViewTypes } from 'nocodb-sdk' import type { TableType, ViewType, ViewTypes } from 'nocodb-sdk'
import type { WritableComputedRef } from '@vue/reactivity' import type { WritableComputedRef } from '@vue/reactivity'
import { isDefaultBase as _isDefaultBase } from '#imports' import { isDefaultBase as _isDefaultBase } from '#imports'
@ -64,6 +63,8 @@ const isDefaultBase = computed(() => {
return _isDefaultBase(source) return _isDefaultBase(source)
}) })
const input = ref<HTMLInputElement>()
const isDropdownOpen = ref(false) const isDropdownOpen = ref(false)
const isEditing = ref(false) const isEditing = ref(false)
@ -92,6 +93,13 @@ const handleOnClick = () => {
} }
} }
const focusInput = () => {
setTimeout(() => {
input.value?.focus()
input.value?.select()
})
}
/** Enable editing view name on dbl click */ /** Enable editing view name on dbl click */
function onDblClick() { function onDblClick() {
if (isMobileMode.value) return if (isMobileMode.value) return
@ -101,6 +109,10 @@ function onDblClick() {
isEditing.value = true isEditing.value = true
_title.value = vModel.value.title _title.value = vModel.value.title
$e('c:view:rename', { view: vModel.value?.type }) $e('c:view:rename', { view: vModel.value?.type })
nextTick(() => {
focusInput()
})
} }
} }
@ -142,11 +154,13 @@ const onRenameMenuClick = () => {
isEditing.value = true isEditing.value = true
_title.value = vModel.value.title _title.value = vModel.value.title
$e('c:view:rename', { view: vModel.value?.type }) $e('c:view:rename', { view: vModel.value?.type })
nextTick(() => {
focusInput()
})
} }
} }
const focusInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
/** Rename a view */ /** Rename a view */
async function onRename() { async function onRename() {
isDropdownOpen.value = false isDropdownOpen.value = false
@ -244,7 +258,7 @@ watch(isDropdownOpen, async () => {
<a-input <a-input
v-if="isEditing" v-if="isEditing"
:ref="focusInput" ref="input"
v-model:value="_title" v-model:value="_title"
class="!bg-transparent !border-0 !ring-0 !outline-transparent !border-transparent !pl-0 !flex-1 mr-4" class="!bg-transparent !border-0 !ring-0 !outline-transparent !border-transparent !pl-0 !flex-1 mr-4"
:class="{ :class="{

6
packages/nc-gui/components/dashboard/settings/AppStore.vue

@ -106,11 +106,11 @@ onMounted(async () => {
</div> </div>
</a-modal> </a-modal>
<div class="flex flex-wrap mt-4 w-full gap-5 mb-10"> <div class="flex flex-wrap w-full gap-5">
<a-card <a-card
v-for="(app, i) in apps" v-for="(app, i) in apps"
:key="i" :key="i"
class="sm:w-100 md:w-138.1" class="sm:w-100 md:w-130"
:class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`" :class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`"
> >
<div class="install-btn flex flex-row justify-end space-x-1"> <div class="install-btn flex flex-row justify-end space-x-1">
@ -137,7 +137,7 @@ onMounted(async () => {
</div> </div>
<div class="flex flex-row space-x-2 items-center justify-start w-full"> <div class="flex flex-row space-x-2 items-center justify-start w-full">
<div class="flex w-20 pl-3"> <div class="flex w-[68px]">
<img <img
v-if="app.title !== 'SMTP'" v-if="app.title !== 'SMTP'"
class="avatar" class="avatar"

6
packages/nc-gui/components/dashboard/settings/DataSources.vue

@ -294,8 +294,8 @@ const handleClickRow = (source: SourceType, tab?: string) => {
</script> </script>
<template> <template>
<div class="flex flex-col h-full" data-testid="nc-settings-datasources-tab"> <div class="flex flex-col h-full p-6" data-testid="nc-settings-datasources-tab">
<div class="px-1 pt-3 mb-6 flex items-center justify-between gap-3"> <div class="mb-6 flex items-center justify-between gap-3">
<a-input <a-input
v-model:value="searchQuery" v-model:value="searchQuery"
type="text" type="text"
@ -429,7 +429,7 @@ const handleClickRow = (source: SourceType, tab?: string) => {
@source-created="loadBases(true)" @source-created="loadBases(true)"
/> />
</template> </template>
<div v-else class="ds-table overflow-y-auto nc-scrollbar-thin relative max-h-full mx-1 mb-4"> <div v-else class="ds-table overflow-y-auto nc-scrollbar-thin relative max-h-full mb-4">
<div class="ds-table-head sticky top-0 bg-white z-10"> <div class="ds-table-head sticky top-0 bg-white z-10">
<div class="ds-table-row !border-0"> <div class="ds-table-row !border-0">
<div class="ds-table-col ds-table-enabled cursor-pointer">{{ $t('general.visibility') }}</div> <div class="ds-table-col ds-table-enabled cursor-pointer">{{ $t('general.visibility') }}</div>

2
packages/nc-gui/components/general/ApiLoader.vue

@ -4,7 +4,7 @@ const { isLoading } = useGlobal()
<template> <template>
<div class="flex justify-center self-center min-w-6"> <div class="flex justify-center self-center min-w-6">
<div v-if="isLoading" class="flex items-center gap-2 ml-3 text-gray-200 text-xs" data-testid="nc-loading"> <div v-if="isLoading" class="flex items-center gap-2 text-gray-200 text-xs" data-testid="nc-loading">
<component :is="iconMap.loading" class="animate-infinite animate-spin" /> <component :is="iconMap.loading" class="animate-infinite animate-spin" />
</div> </div>
</div> </div>

2
packages/nc-gui/components/general/OpenLeftSidebarBtn.vue

@ -11,7 +11,7 @@ const onClick = () => {
</script> </script>
<template> <template>
<div v-e="['c:leftSidebar:hideToggle']"> <div v-if="isMobileMode || !isLeftSidebarOpen" v-e="['c:leftSidebar:hideToggle']">
<NcTooltip <NcTooltip
placement="topLeft" placement="topLeft"
hide-on-click hide-on-click

2
packages/nc-gui/components/general/ShareProject.vue

@ -45,7 +45,7 @@ const copySharedBase = async () => {
<template> <template>
<div <div
v-if="!isSharedBase && isUIAllowed('baseShare') && visibility !== 'hidden' && (activeTable || base)" v-if="!isSharedBase && isUIAllowed('baseShare') && visibility !== 'hidden' && (activeTable || base)"
class="nc-share-base-button flex flex-col justify-center h-full" class="nc-share-base-button flex flex-col justify-center"
data-testid="share-base-button" data-testid="share-base-button"
:data-sharetype="visibility" :data-sharetype="visibility"
> >

2
packages/nc-gui/components/general/language/index.vue

@ -5,7 +5,7 @@
overlay-class-name="nc-dropdown-menu-translate" overlay-class-name="nc-dropdown-menu-translate"
> >
<div v-bind="$attrs" class="flex items-center justify-center"> <div v-bind="$attrs" class="flex items-center justify-center">
<MaterialSymbolsTranslate class="md:text-xl nc-menu-translate" /> <MaterialSymbolsTranslate class="text-base nc-menu-translate" />
</div> </div>
<template #overlay> <template #overlay>

338
packages/nc-gui/components/nc/List.vue

@ -0,0 +1,338 @@
<script lang="ts" setup>
import { useVirtualList } from '@vueuse/core'
export type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight'
export type RawValueType = string | number
interface ListItem {
value?: RawValueType
label?: string
[key: string]: any
}
/**
* Props interface for the List component
*/
interface Props {
/** The currently selected value */
value: RawValueType
/** The list of items to display */
list: ListItem[]
/**
* The key to use for accessing the value from a list item
* @default 'value'
*/
optionValueKey?: string
/**
* The key to use for accessing the label from a list item
* @default 'label'
*/
optionLabelKey?: string
/** Whether the list is open or closed */
open?: boolean
/** Whether to close the list after an item is selected */
closeOnSelect?: boolean
/** Placeholder text for the search input */
searchInputPlaceholder?: string
/** Whether to show the currently selected option */
showSelectedOption?: boolean
/** Custom filter function for list items */
filterOption?: (input: string, option: ListItem, index: Number) => boolean
}
interface Emits {
(e: 'update:value', value: RawValueType): void
(e: 'update:open', open: boolean): void
(e: 'change', option: ListItem): void
}
const props = withDefaults(defineProps<Props>(), {
open: false,
closeOnSelect: true,
showSelectedOption: true,
optionValueKey: 'value',
optionLabelKey: 'label',
})
const emits = defineEmits<Emits>()
const vModel = useVModel(props, 'value', emits)
const vOpen = useVModel(props, 'open', emits)
const { optionValueKey, optionLabelKey } = props
const { closeOnSelect, showSelectedOption } = toRefs(props)
const listRef = ref<HTMLDivElement>()
const searchQuery = ref('')
const inputRef = ref()
const activeOptionIndex = ref(-1)
const showHoverEffectOnSelectedOption = ref(true)
const isSearchEnabled = computed(() => props.list.length > 4)
/**
* Computed property that filters the list of options based on the search query.
* If a custom filter function is provided via props.filterOption, it will be used instead of the default filtering logic.
*
* @returns Filtered list of options
*
* @typeparam ListItem - The type of items in the list
*/
const list = computed(() => {
const query = searchQuery.value.toLowerCase()
return props.list.filter((item, i) => {
if (props?.filterOption) {
return props.filterOption(query, item, i)
} else {
return item[optionLabelKey]?.toLowerCase()?.includes(query)
}
})
})
const {
list: virtualList,
containerProps,
wrapperProps,
scrollTo,
} = useVirtualList(list, {
itemHeight: 38,
})
/**
* Resets the hover effect on the selected option
* @param clearActiveOption - Whether to clear the active option index
*/
const handleResetHoverEffect = (clearActiveOption = false, newActiveIndex?: number) => {
if (clearActiveOption && showHoverEffectOnSelectedOption.value) {
activeOptionIndex.value = -1
}
if (newActiveIndex !== undefined) {
activeOptionIndex.value = newActiveIndex
}
if (!showHoverEffectOnSelectedOption.value) return
showHoverEffectOnSelectedOption.value = false
}
/**
* Handles the selection of an option from the list
*
* @param option - The selected list item
*
* This function is responsible for handling the selection of an option from the list.
* It updates the model value, emits a change event, and optionally closes the dropdown.
*/
const handleSelectOption = (option: ListItem) => {
if (!option?.[optionValueKey]) return
vModel.value = option[optionValueKey] as RawValueType
emits('change', option)
if (closeOnSelect.value) {
vOpen.value = false
}
}
/**
* Automatically scrolls to the active option in the list
*/
const handleAutoScrollOption = (useDelay = false) => {
if (activeOptionIndex.value === -1) return
if (!useDelay) {
scrollTo(activeOptionIndex.value)
return
}
setTimeout(() => {
scrollTo(activeOptionIndex.value)
}, 150)
}
const onArrowDown = () => {
handleResetHoverEffect()
if (activeOptionIndex.value === list.value.length - 1) return
activeOptionIndex.value = Math.min(activeOptionIndex.value + 1, list.value.length - 1)
handleAutoScrollOption()
}
const onArrowUp = () => {
handleResetHoverEffect()
if (activeOptionIndex.value === 0) return
activeOptionIndex.value = Math.max(activeOptionIndex.value - 1, 0)
handleAutoScrollOption()
}
const handleKeydownEnter = () => {
if (list.value[activeOptionIndex.value]) {
handleSelectOption(list.value[activeOptionIndex.value])
} else if (list.value[0]) {
handleSelectOption(list.value[activeOptionIndex.value])
}
}
/**
* Focuses the input box when the list is opened
*/
const focusInputBox = () => {
if (!vOpen.value) return
setTimeout(() => {
inputRef.value?.focus()
}, 100)
}
/**
* Focuses the list wrapper when the list is opened
*
* This function is called when the list is opened and search is not enabled.
* It sets a timeout to focus the list wrapper element after a short delay.
* This allows for proper rendering and improves accessibility.
*/
const focusListWrapper = () => {
if (!vOpen.value || isSearchEnabled.value) return
setTimeout(() => {
listRef.value?.focus()
}, 100)
}
watch(
vOpen,
() => {
if (!vOpen.value) return
searchQuery.value = ''
showHoverEffectOnSelectedOption.value = true
if (vModel.value) {
activeOptionIndex.value = list.value.findIndex((o) => o?.[optionValueKey] === vModel.value)
nextTick(() => {
handleAutoScrollOption(true)
})
} else {
activeOptionIndex.value = -1
}
if (isSearchEnabled.value) {
focusInputBox()
} else {
focusListWrapper()
}
},
{
immediate: true,
},
)
</script>
<template>
<div
ref="listRef"
tabindex="0"
class="flex flex-col pt-2 w-64 !focus:(shadow-none outline-none)"
@keydown.arrow-down.prevent="onArrowDown"
@keydown.arrow-up.prevent="onArrowUp"
@keydown.enter.prevent="handleSelectOption(list[activeOptionIndex])"
>
<template v-if="isSearchEnabled">
<div class="w-full px-2" @click.stop>
<a-input
ref="inputRef"
v-model:value="searchQuery"
:placeholder="searchInputPlaceholder || $t('placeholder.searchFields')"
class="nc-toolbar-dropdown-search-field-input !pl-2 !pr-1.5"
allow-clear
:bordered="false"
@keydown.enter.stop="handleKeydownEnter"
@change="handleResetHoverEffect(false, 0)"
>
<template #prefix> <GeneralIcon icon="search" class="nc-search-icon h-3.5 w-3.5 mr-1" /> </template
></a-input>
</div>
<NcDivider />
</template>
<slot name="listHeader"></slot>
<div class="nc-list-wrapper">
<template v-if="list.length">
<div class="h-auto !max-h-[247px]">
<div
v-bind="containerProps"
class="nc-list !h-auto w-full nc-scrollbar-thin px-2 pb-2"
:style="{
maxHeight: '247px !important',
}"
>
<div v-bind="wrapperProps">
<div
v-for="{ data: option, index: idx } in virtualList"
:key="idx"
class="flex items-center gap-2 w-full py-2 px-2 hover:bg-gray-100 cursor-pointer rounded-md"
:class="[
`nc-list-option-${idx}`,
{
'nc-list-option-selected': option[optionValueKey] === vModel,
'bg-gray-100 ': showHoverEffectOnSelectedOption && option[optionValueKey] === vModel,
'bg-gray-100 nc-list-option-active': activeOptionIndex === idx,
},
]"
@mouseover="handleResetHoverEffect(true)"
@click="handleSelectOption(option)"
>
<slot name="listItem" :option="option" :index="idx">
<NcTooltip class="truncate flex-1" show-on-truncate-only>
<template #title>
{{ option[optionLabelKey] }}
</template>
{{ option[optionLabelKey] }}
</NcTooltip>
<GeneralIcon
v-if="showSelectedOption && option[optionValueKey] === vModel"
id="nc-selected-item-icon"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</slot>
</div>
</div>
</div>
</div>
</template>
<template v-if="!list.length">
<slot name="emptyState">
<div class="h-full text-center flex items-center justify-center gap-3 mt-4">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" class="!my-0" />
</div>
</slot>
</template>
</div>
<slot name="listFooter"></slot>
</div>
</template>
<style lang="scss" scoped>
:deep(.nc-toolbar-dropdown-search-field-input) {
&.ant-input-affix-wrapper-focused {
.ant-input-prefix svg {
@apply text-brand-500;
}
}
.ant-input {
@apply placeholder-gray-500;
}
}
</style>

36
packages/nc-gui/components/nc/PageHeader.vue

@ -0,0 +1,36 @@
<script lang="ts" setup></script>
<template>
<div class="nc-page-header">
<div class="flex-1 flex items-start gap-3">
<div v-if="$slots.icon" class="h-7 flex items-center children:flex-none">
<slot name="icon"></slot>
</div>
<div class="flex flex-col gap-3">
<h1 class="nc-page-header-title truncate">
<slot name="title"></slot>
</h1>
<p v-if="$slots.subtitle" class="nc-page-header-subtitle">
<slot name="subtitle"></slot>
</p>
</div>
</div>
<div v-if="$slots.action">
<slot name="action"></slot>
</div>
</div>
</template>
<style lang="scss" scoped>
.nc-page-header {
@apply h-12 flex items-center gap-3 px-3 py-2;
.nc-page-header-title {
@apply text-xl font-semibold text-gray-800 my-0;
}
.nc-page-header-subtitle {
@apply text-sm font-weight-500 text-gray-700;
}
}
</style>

54
packages/nc-gui/components/project/AccessSettings.vue

@ -17,7 +17,7 @@ const { sorts, sortDirection, loadSorts, handleGetSortedData, saveOrUpdate: save
const isSuper = computed(() => orgRoles.value?.[OrgUserRoles.SUPER_ADMIN]) const isSuper = computed(() => orgRoles.value?.[OrgUserRoles.SUPER_ADMIN])
const orgStore = useOrg() const orgStore = useOrg()
const { orgId } = storeToRefs(orgStore) const { orgId, org } = storeToRefs(orgStore)
const isAdminPanel = inject(IsAdminPanelInj, ref(false)) const isAdminPanel = inject(IsAdminPanelInj, ref(false))
@ -255,32 +255,51 @@ const customRow = (record: Record<string, any>) => ({
<template> <template>
<div <div
class="nc-collaborator-table-container nc-access-settings-view flex flex-col"
:class="{ :class="{
'px-6': isAdminPanel, 'h-[calc(100vh_-_100px)]': !isAdminPanel,
'px-1': !isAdminPanel,
}" }"
class="nc-collaborator-table-container pt-6 nc-access-settings-view h-[calc(100vh-8rem)] flex flex-col gap-6"
> >
<div v-if="isAdminPanel" class="font-bold w-full text-2xl" data-rec="true"> <div v-if="isAdminPanel">
<div class="flex items-center gap-3"> <div class="nc-breadcrumb px-2">
<div class="nc-breadcrumb-item">
{{ org.title }}
</div>
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
<NuxtLink <NuxtLink
:href="`/admin/${orgId}/bases`" :href="`/admin/${orgId}/bases`"
class="!hover:(text-black underline-gray-600) flex items-center !text-black !underline-transparent ml-0.75 max-w-1/4" class="!hover:(text-gray-800 underline-gray-600) flex items-center !text-gray-700 !underline-transparent ml-0.75 max-w-1/4"
> >
<component :is="iconMap.arrowLeft" class="text-3xl" /> <div class="nc-breadcrumb-item">
{{ $t('objects.projects') }} {{ $t('objects.projects') }}
</div>
</NuxtLink> </NuxtLink>
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
<span class="text-2xl"> / </span> <div class="nc-breadcrumb-item active truncate capitalize">
{{ currentBase?.title }}
</div>
</div>
<NcPageHeader>
<template #icon>
<div class="nc-page-header-icon flex justify-center items-center h-5 w-5">
<GeneralBaseIconColorPicker readonly /> <GeneralBaseIconColorPicker readonly />
<span class="text-base"> </div>
</template>
<template #title>
<span data-rec="true" class="capitalize">
{{ currentBase?.title }} {{ currentBase?.title }}
</span> </span>
</div> </template>
</NcPageHeader>
</div> </div>
<div v-else class="w-full flex justify-between items-center max-w-350 gap-3"> <div
class="h-full flex flex-col items-center gap-6 px-6 pt-6"
:class="{
'border-t-1 border-gray-200': isAdminPanel,
}"
>
<div v-if="!isAdminPanel" class="w-full flex justify-between items-center max-w-350 gap-3">
<a-input <a-input
v-model:value="userSearchText" v-model:value="userSearchText"
:placeholder="$t('title.searchMembers')" :placeholder="$t('title.searchMembers')"
@ -378,6 +397,7 @@ const customRow = (record: Record<string, any>) => ({
</div> </div>
</template> </template>
</NcTable> </NcTable>
</div>
<LazyDlgInviteDlg v-model:model-value="isInviteModalVisible" :base-id="currentBase?.id" type="base" /> <LazyDlgInviteDlg v-model:model-value="isInviteModalVisible" :base-id="currentBase?.id" type="base" />
</div> </div>
@ -395,4 +415,10 @@ const customRow = (record: Record<string, any>) => ({
:deep(.nc-collaborator-role-select .ant-select-selector) { :deep(.nc-collaborator-role-select .ant-select-selector) {
@apply !rounded; @apply !rounded;
} }
.nc-page-header-icon {
:deep(svg) {
@apply h-4.5 w-4.5;
}
}
</style> </style>

8
packages/nc-gui/components/project/AllTables.vue

@ -111,9 +111,9 @@ const onCreateBaseClick = () => {
</script> </script>
<template> <template>
<div class="nc-all-tables-view"> <div class="nc-all-tables-view px-6 pt-6">
<div <div
class="flex flex-row gap-x-6 pt-6 pb-2 overflow-x-auto nc-scrollbar-thin" class="flex flex-row gap-x-6 pb-2 overflow-x-auto nc-scrollbar-thin"
:class="{ :class="{
'pointer-events-none': base?.isLoading, 'pointer-events-none': base?.isLoading,
}" }"
@ -199,7 +199,7 @@ const onCreateBaseClick = () => {
v-else-if="activeTables.length" v-else-if="activeTables.length"
class="flex mt-4" class="flex mt-4"
:style="{ :style="{
height: 'calc(100vh - var(--topbar-height) - 15.2rem)', height: 'calc(100vh - var(--topbar-height) - 218px)',
}" }"
> >
<NcTable <NcTable
@ -219,7 +219,7 @@ const onCreateBaseClick = () => {
class="w-full flex items-center gap-3 max-w-full text-gray-800 font-semibold" class="w-full flex items-center gap-3 max-w-full text-gray-800 font-semibold"
data-testid="proj-view-list__item-title" data-testid="proj-view-list__item-title"
> >
<div class="min-w-5 flex items-center justify-center"> <div class="min-w-6 flex items-center justify-center">
<GeneralTableIcon :meta="record" class="flex-none text-gray-600" /> <GeneralTableIcon :meta="record" class="flex-none text-gray-600" />
</div> </div>
<NcTooltip class="truncate max-w-[calc(100%_-_28px)]" show-on-truncate-only> <NcTooltip class="truncate max-w-[calc(100%_-_28px)]" show-on-truncate-only>

22
packages/nc-gui/components/project/View.vue

@ -112,14 +112,14 @@ watch(
<div class="h-full nc-base-view"> <div class="h-full nc-base-view">
<div <div
v-if="!isAdminPanel" v-if="!isAdminPanel"
class="flex flex-row pl-2 pr-2 gap-1 border-b-1 border-gray-200 justify-between w-full" class="flex flex-row px-2 py-2 gap-1 justify-between w-full border-b-1 border-gray-200"
:class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--topbar-height)]': !isMobileMode }" :class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--topbar-height)]': !isMobileMode }"
> >
<div class="flex flex-row items-center gap-x-3"> <div class="flex flex-row items-center gap-x-3">
<GeneralOpenLeftSidebarBtn /> <GeneralOpenLeftSidebarBtn />
<div class="flex flex-row items-center h-full gap-x-2.5"> <div class="flex flex-row items-center h-full gap-x-2 px-2">
<GeneralProjectIcon :color="parseProp(currentBase?.meta).iconColor" :type="currentBase?.type" /> <GeneralProjectIcon :color="parseProp(currentBase?.meta).iconColor" :type="currentBase?.type" />
<NcTooltip class="flex font-medium text-sm capitalize truncate max-w-150" show-on-truncate-only> <NcTooltip class="flex font-bold text-sm capitalize truncate max-w-150 text-gray-800" show-on-truncate-only>
<template #title> {{ currentBase?.title }}</template> <template #title> {{ currentBase?.title }}</template>
<span class="truncate"> <span class="truncate">
{{ currentBase?.title }} {{ currentBase?.title }}
@ -130,12 +130,15 @@ watch(
<LazyGeneralShareProject /> <LazyGeneralShareProject />
</div> </div>
<div <div
class="flex mx-12 my-8 nc-base-view-tab" class="flex nc-base-view-tab container"
:style="{ :style="{
height: 'calc(100% - var(--topbar-height))', height: 'calc(100% - var(--topbar-height))',
}" }"
> >
<a-tabs v-model:activeKey="projectPageTab" class="w-full"> <a-tabs v-model:activeKey="projectPageTab" class="w-full">
<template #leftExtra>
<div class="w-3"></div>
</template>
<a-tab-pane v-if="!isAdminPanel" key="allTable"> <a-tab-pane v-if="!isAdminPanel" key="allTable">
<template #tab> <template #tab>
<div class="tab-title" data-testid="proj-view-tab__all-tables"> <div class="tab-title" data-testid="proj-view-tab__all-tables">
@ -193,11 +196,7 @@ watch(
</div> </div>
</div> </div>
</template> </template>
<DashboardSettingsDataSources <DashboardSettingsDataSources v-model:state="baseSettingsState" :base-id="base.id" class="max-h-full" />
v-model:state="baseSettingsState"
:base-id="base.id"
class="max-h-[calc(100%_-_36px)] pt-3"
/>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
@ -213,7 +212,10 @@ watch(
} }
.tab-title { .tab-title {
@apply flex flex-row items-center gap-x-2 px-2; @apply flex flex-row items-center gap-x-2 px-2 py-[1px];
}
:deep(.ant-tabs-tab) {
@apply pt-2 pb-3;
} }
:deep(.ant-tabs-tab .tab-title) { :deep(.ant-tabs-tab .tab-title) {
@apply text-gray-500; @apply text-gray-500;

11
packages/nc-gui/components/smartsheet/Details.vue

@ -104,23 +104,28 @@ watch(openedSubTab, () => {
} }
:deep(.ant-tabs-nav) { :deep(.ant-tabs-nav) {
min-height: calc(var(--topbar-height) - 1.75px); min-height: calc(var(--toolbar-height) - 1px);
}
:deep(.ant-tabs-tab) {
@apply pt-2 pb-3;
} }
</style> </style>
<style lang="scss"> <style lang="scss">
.nc-details-tab.nc-tabs.centered { .nc-details-tab.nc-tabs.centered {
> .ant-tabs-nav { > .ant-tabs-nav {
@apply px-3;
.ant-tabs-nav-wrap { .ant-tabs-nav-wrap {
@apply absolute mx-auto -left-9.5; @apply absolute mx-auto;
} }
} }
} }
.nc-details-tab-left-sidebar-close > .nc-details-tab.nc-tabs.centered { .nc-details-tab-left-sidebar-close > .nc-details-tab.nc-tabs.centered {
> .ant-tabs-nav { > .ant-tabs-nav {
@apply px-3;
.ant-tabs-nav-wrap { .ant-tabs-nav-wrap {
@apply absolute mx-auto left-0; @apply absolute mx-auto;
} }
} }
} }

4
packages/nc-gui/components/smartsheet/Form.vue

@ -1254,7 +1254,7 @@ useEventListener(
:trigger="['click']" :trigger="['click']"
overlay-class-name="nc-dropdown-form-edit-column" overlay-class-name="nc-dropdown-form-edit-column"
:disabled="!isUIAllowed('fieldEdit')" :disabled="!isUIAllowed('fieldEdit')"
@visibleChange="onVisibilityChange('showEditColumn')" @visible-change="onVisibilityChange('showEditColumn')"
> >
<NcButton type="secondary" size="small" class="nc-form-add-field" data-testid="nc-form-add-field"> <NcButton type="secondary" size="small" class="nc-form-add-field" data-testid="nc-form-add-field">
{{ $t('general.edit') }} {{ $t('objects.field') }} {{ $t('general.edit') }} {{ $t('objects.field') }}
@ -1340,7 +1340,7 @@ useEventListener(
v-model:visible="dropdownStates.showAddColumn" v-model:visible="dropdownStates.showAddColumn"
:trigger="['click']" :trigger="['click']"
overlay-class-name="nc-dropdown-form-add-column" overlay-class-name="nc-dropdown-form-add-column"
@visibleChange="onVisibilityChange('showAddColumn')" @visible-change="onVisibilityChange('showAddColumn')"
> >
<NcButton type="secondary" size="small" class="nc-form-add-field" data-testid="nc-form-add-field"> <NcButton type="secondary" size="small" class="nc-form-add-field" data-testid="nc-form-add-field">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">

5
packages/nc-gui/components/smartsheet/Toolbar.vue

@ -34,7 +34,7 @@ provide(IsToolbarIconMode, isToolbarIconMode)
:class="{ :class="{
'px-4': isMobileMode, 'px-4': isMobileMode,
}" }"
class="nc-table-toolbar relative px-3 flex gap-2 items-center border-b border-gray-200 overflow-hidden xs:(min-h-14) min-h-9 max-h-9 z-7" class="nc-table-toolbar relative px-3 flex gap-2 items-center border-b border-gray-200 overflow-hidden xs:(min-h-14) min-h-[var(--toolbar-height)] max-h-[var(--toolbar-height)] z-7"
> >
<template v-if="isViewsLoading"> <template v-if="isViewsLoading">
<a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" /> <a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" />
@ -63,6 +63,8 @@ provide(IsToolbarIconMode, isToolbarIconMode)
<LazySmartsheetToolbarGroupByMenu v-if="isGrid && !isLocalMode" /> <LazySmartsheetToolbarGroupByMenu v-if="isGrid && !isLocalMode" />
<LazySmartsheetToolbarSortListMenu v-if="isGrid || isGallery || isKanban" /> <LazySmartsheetToolbarSortListMenu v-if="isGrid || isGallery || isKanban" />
<LazySmartsheetToolbarOpenedViewAction v-if="isCalendar" />
</div> </div>
<LazySmartsheetToolbarCalendarMode v-if="isCalendar && isTab" :tab="isTab" /> <LazySmartsheetToolbarCalendarMode v-if="isCalendar && isTab" :tab="isTab" />
@ -70,6 +72,7 @@ provide(IsToolbarIconMode, isToolbarIconMode)
<template v-if="!isMobileMode"> <template v-if="!isMobileMode">
<LazySmartsheetToolbarRowHeight v-if="isGrid" /> <LazySmartsheetToolbarRowHeight v-if="isGrid" />
<LazySmartsheetToolbarOpenedViewAction v-if="!isCalendar" />
<!-- <LazySmartsheetToolbarQrScannerButton v-if="isMobileMode && (isGrid || isKanban || isGallery)" /> --> <!-- <LazySmartsheetToolbarQrScannerButton v-if="isMobileMode && (isGrid || isKanban || isGallery)" /> -->
<div class="flex-1" /> <div class="flex-1" />

22
packages/nc-gui/components/smartsheet/Topbar.vue

@ -15,25 +15,42 @@ const { appInfo } = useGlobal()
const { toggleExtensionPanel, isPanelExpanded, extensionsEgg, onEggClick } = useExtensions() const { toggleExtensionPanel, isPanelExpanded, extensionsEgg, onEggClick } = useExtensions()
const isSharedBase = computed(() => route.value.params.typeOrId === 'base') const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
const topbarBreadcrumbItemWidth = computed(() => {
if (!isSharedBase.value && !isMobileMode.value) {
return 'calc(\(100% - 167px - 24px\) / 2)'
} else if (isMobileMode.value) {
return 'calc(75% - 12px)'
} else {
return 'calc(\(100% - 12px\) / 2)'
}
})
</script> </script>
<template> <template>
<div <div
class="nc-table-topbar h-20 py-1 flex gap-2 items-center border-b border-gray-200 overflow-hidden relative max-h-[var(--topbar-height)] min-h-[var(--topbar-height)] md:(pr-2 pl-2) xs:(px-1)" class="nc-table-topbar py-2 border-b-1 border-gray-200 flex gap-3 items-center justify-between overflow-hidden relative h-[var(--topbar-height)] max-h-[var(--topbar-height)] min-h-[var(--topbar-height)] md:(px-2) xs:(px-1)"
style="z-index: 7" style="z-index: 7"
> >
<template v-if="isViewsLoading"> <template v-if="isViewsLoading">
<a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" /> <a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" />
</template> </template>
<template v-else> <template v-else>
<div
class="flex items-center gap-3 min-w-[300px]"
:style="{
width: topbarBreadcrumbItemWidth,
}"
>
<GeneralOpenLeftSidebarBtn /> <GeneralOpenLeftSidebarBtn />
<LazySmartsheetToolbarViewInfo v-if="!isPublic" /> <LazySmartsheetToolbarViewInfo v-if="!isPublic" />
</div>
<div v-if="!isSharedBase && !isMobileMode"> <div v-if="!isSharedBase && !isMobileMode">
<SmartsheetTopbarSelectMode /> <SmartsheetTopbarSelectMode />
</div> </div>
<div class="flex-1" />
<div class="flex items-center justify-end gap-3 flex-1">
<GeneralApiLoader v-if="!isMobileMode" /> <GeneralApiLoader v-if="!isMobileMode" />
<div <div
@ -61,6 +78,7 @@ const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
v-if="isSharedBase && !appInfo.ee" v-if="isSharedBase && !appInfo.ee"
class="cursor-pointer text-lg hover:(text-black bg-gray-200) mr-0 p-1.5 rounded-md" class="cursor-pointer text-lg hover:(text-black bg-gray-200) mr-0 p-1.5 rounded-md"
/> />
</div>
</template> </template>
</div> </div>
</template> </template>

2
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -241,7 +241,7 @@ const onClick = (e: Event) => {
:trigger="['click']" :trigger="['click']"
:placement="isExpandedForm && !isExpandedBulkUpdateForm ? 'bottomLeft' : 'bottomRight'" :placement="isExpandedForm && !isExpandedBulkUpdateForm ? 'bottomLeft' : 'bottomRight'"
:overlay-class-name="`nc-dropdown-edit-column ${editColumnDropdown ? 'active' : ''}`" :overlay-class-name="`nc-dropdown-edit-column ${editColumnDropdown ? 'active' : ''}`"
@visibleChange="onVisibleChange" @visible-change="onVisibleChange"
> >
<div v-if="isExpandedForm && !isExpandedBulkUpdateForm" class="h-[1px]" @dblclick.stop>&nbsp;</div> <div v-if="isExpandedForm && !isExpandedBulkUpdateForm" class="h-[1px]" @dblclick.stop>&nbsp;</div>
<div v-else /> <div v-else />

2
packages/nc-gui/components/smartsheet/toolbar/LockType.vue

@ -29,7 +29,7 @@ const selectedView = inject(ActiveViewInj)
</script> </script>
<template> <template>
<div class="nc-locked-menu-item !px-1 text-gray-800" @click="emit('select', type)"> <div class="nc-locked-menu-item !px-1 text-gray-800 max-w-[312px]" @click="emit('select', type)">
<div :class="{ 'show-tick': !hideTick }"> <div :class="{ 'show-tick': !hideTick }">
<div class="flex flex-col gap-y-1"> <div class="flex flex-col gap-y-1">
<div class="flex items-center gap-2 flex-grow"> <div class="flex items-center gap-2 flex-grow">

35
packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue

@ -11,7 +11,7 @@ const { $api } = useNuxtApp()
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
const { activeView, views, openedViewsTab, viewsByTable } = storeToRefs(useViewsStore()) const { activeView, views, viewsByTable } = storeToRefs(useViewsStore())
const { loadViews, removeFromRecentViews } = useViewsStore() const { loadViews, removeFromRecentViews } = useViewsStore()
const { navigateToTable } = useTablesStore() const { navigateToTable } = useTablesStore()
@ -171,28 +171,17 @@ function openDeleteDialog() {
class="!xs:pointer-events-none nc-actions-menu-btn nc-view-context-btn" class="!xs:pointer-events-none nc-actions-menu-btn nc-view-context-btn"
overlay-class-name="nc-dropdown-actions-menu" overlay-class-name="nc-dropdown-actions-menu"
> >
<div <div>
v-e="['c:breadcrumb:view-actions']" <NcButton
class="truncate nc-active-view-title flex gap-1 items-center !hover:(bg-gray-100 text-gray-800) ml-1 pl-1 pr-0.25 rounded-md py-1 cursor-pointer" v-e="['c:toolbar:view-actions']"
:class="{ class="nc-view-action-menu-btn nc-toolbar-btn !border-0 !h-7 !px-1.5 !min-w-7"
'max-w-2/5': !isSharedBase && !isMobileMode && activeView?.is_default, size="small"
'max-w-3/5': !isSharedBase && !isMobileMode && !activeView?.is_default, type="secondary"
'max-w-1/2': isMobileMode,
'text-gray-500': activeView?.is_default,
'text-gray-800 font-medium': !activeView?.is_default,
}"
> >
<NcTooltip class="truncate xs:pl-1.25 flex-1 text-inherit" show-on-truncate-only> <div class="flex items-center gap-0.5">
<template #title>{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }} </template> <GeneralIcon icon="threeDotVertical" class="!h-4 !w-4" />
<span </div>
:class="{ </NcButton>
'max-w-28/100': !isMobileMode,
}"
>
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span>
</NcTooltip>
<GeneralIcon icon="chevronDown" class="!text-gray-500 mt-0.5" />
</div> </div>
<template #overlay> <template #overlay>
<SmartsheetToolbarViewActionMenu <SmartsheetToolbarViewActionMenu
@ -204,6 +193,4 @@ function openDeleteDialog() {
/> />
</template> </template>
</NcDropdown> </NcDropdown>
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view' && !isMobileMode && !isRenaming" />
</template> </template>

4
packages/nc-gui/components/smartsheet/toolbar/Reload.vue

@ -25,7 +25,7 @@ watch(isReloading, () => {
</script> </script>
<template> <template>
<a-tooltip placement="bottom"> <NcTooltip placement="bottom">
<template #title> {{ $t('general.reload') }} </template> <template #title> {{ $t('general.reload') }} </template>
<div <div
@ -38,5 +38,5 @@ watch(isReloading, () => {
@click="onClick" @click="onClick"
/> />
</div> </div>
</a-tooltip> </NcTooltip>
</template> </template>

7
packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue

@ -174,6 +174,7 @@ const onDelete = async () => {
<template v-if="!view?.is_default && isUIAllowed('viewCreateOrEdit')"> <template v-if="!view?.is_default && isUIAllowed('viewCreateOrEdit')">
<NcDivider /> <NcDivider />
<template v-if="inSidebar">
<NcMenuItem v-if="lockType !== LockType.Locked" @click="onRenameMenuClick"> <NcMenuItem v-if="lockType !== LockType.Locked" @click="onRenameMenuClick">
<GeneralIcon icon="rename" /> <GeneralIcon icon="rename" />
{{ {{
@ -188,11 +189,13 @@ const onDelete = async () => {
<GeneralIcon icon="rename" /> <GeneralIcon icon="rename" />
{{ {{
$t('general.renameEntity', { $t('general.renameEntity', {
entity: view.type !== ViewTypes.FORM ? $t('objects.view').toLowerCase() : $t('objects.viewType.form').toLowerCase(), entity:
view.type !== ViewTypes.FORM ? $t('objects.view').toLowerCase() : $t('objects.viewType.form').toLowerCase(),
}) })
}} }}
</NcMenuItem> </NcMenuItem>
</NcTooltip> </NcTooltip>
</template>
<NcMenuItem @click="onDuplicate"> <NcMenuItem @click="onDuplicate">
<GeneralIcon class="nc-view-copy-icon" icon="duplicate" /> <GeneralIcon class="nc-view-copy-icon" icon="duplicate" />
{{ {{
@ -296,7 +299,7 @@ const onDelete = async () => {
<template #expandIcon></template> <template #expandIcon></template>
<div class="flex py-3 px-4 font-bold uppercase text-xs text-gray-500">{{ $t('labels.viewMode') }}</div> <div class="flex py-3 px-4 font-bold uppercase text-xs text-gray-500">{{ $t('labels.viewMode') }}</div>
<a-menu-item class="!mx-1 !py-2 !rounded-md nc-view-action-lock-subaction"> <a-menu-item class="!mx-1 !py-2 !rounded-md nc-view-action-lock-subaction max-w-[100px]">
<LazySmartsheetToolbarLockType :type="LockType.Collaborative" @click="changeLockType(LockType.Collaborative)" /> <LazySmartsheetToolbarLockType :type="LockType.Collaborative" @click="changeLockType(LockType.Collaborative)" />
</a-menu-item> </a-menu-item>

167
packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue

@ -1,54 +1,41 @@
<script setup lang="ts"> <script setup lang="ts">
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const { activeView } = storeToRefs(useViewsStore()) const { activeView, openedViewsTab } = storeToRefs(useViewsStore())
const { base, isSharedBase } = storeToRefs(useBase()) const { base, isSharedBase } = storeToRefs(useBase())
const { baseUrl } = useBase()
const { activeTable } = storeToRefs(useTablesStore()) const { activeTable } = storeToRefs(useTablesStore())
const { tableUrl } = useTablesStore()
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore()) const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const openedBaseUrl = computed(() => {
if (!base.value) return ''
return `${window.location.origin}/#${baseUrl({
id: base.value.id!,
type: 'database',
isSharedBase: isSharedBase.value,
})}`
})
</script> </script>
<template> <template>
<div <div
class="ml-0.25 flex flex-row font-medium items-center border-gray-50 transition-all duration-100" class="flex flex-row items-center border-gray-50 transition-all duration-100 select-none"
:class="{ :class="{
'min-w-36/100 max-w-36/100': !isMobileMode && isLeftSidebarOpen, 'text-base w-[calc(100%_-_52px)]': isMobileMode,
'min-w-39/100 max-w-39/100': !isMobileMode && !isLeftSidebarOpen, 'w-[calc(100%_-_44px)]': !isMobileMode && !isLeftSidebarOpen,
'w-2/3 text-base ml-1.5': isMobileMode, 'w-full': !isMobileMode && isLeftSidebarOpen,
'!max-w-3/4': isSharedBase && !isMobileMode,
}" }"
> >
<template v-if="!isMobileMode"> <template v-if="!isMobileMode">
<NuxtLink <SmartsheetTopbarProjectListDropdown v-if="activeTable">
class="!hover:(text-black underline-gray-600) !underline-transparent ml-0.75 max-w-1/4" <template #default="{ isOpen }">
<div
class="rounded-lg h-8 px-2 text-gray-700 font-weight-500 hover:(bg-gray-100 text-gray-900) flex items-center gap-1 cursor-pointer max-w-1/3"
:class="{ :class="{
'!max-w-none': isSharedBase && !isMobileMode, '!max-w-none': isSharedBase && !isMobileMode,
'!text-gray-500': activeTable, '': !isMobileMode && isLeftSidebarOpen,
'!text-gray-700': !activeTable,
}" }"
:to="openedBaseUrl"
> >
<NcTooltip class="!text-inherit"> <NcTooltip :disabled="isSharedBase || isOpen">
<template #title> <template #title>
<span class="capitalize"> <span class="capitalize">
{{ base?.title }} {{ base?.title }}
</span> </span>
</template> </template>
<div class="flex flex-row items-center gap-x-1.5">
<GeneralProjectIcon <GeneralProjectIcon
:type="base?.type" :type="base?.type"
class="!grayscale min-w-4" class="!grayscale min-w-4"
@ -56,25 +43,57 @@ const openedBaseUrl = computed(() => {
filter: 'grayscale(100%) brightness(115%)', filter: 'grayscale(100%) brightness(115%)',
}" }"
/> />
<div </NcTooltip>
class="hidden !2xl:(flex truncate ml-1)" <template v-if="isSharedBase">
:class="{ <NcTooltip
'!flex': isSharedBase && !isMobileMode, class="ml-1 truncate nc-active-base-title max-w-full !leading-5"
show-on-truncate-only
:disabled="isOpen"
>
<template #title>
<span class="capitalize">
{{ base?.title }}
</span>
</template>
<span
class="text-ellipsis capitalize"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}" }"
> >
<span class="truncate !text-inherit capitalize">
{{ base?.title }} {{ base?.title }}
</span> </span>
</div>
</div>
</NcTooltip> </NcTooltip>
</NuxtLink> <GeneralIcon
<div class="px-1.75 text-gray-500">></div> icon="chevronDown"
class="!text-current opacity-70 flex-none transform transition-transform duration-25 w-3.5 h-3.5"
:class="{ '!rotate-180': isOpen }"
/>
</template>
</div>
</template>
</SmartsheetTopbarProjectListDropdown>
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
</template> </template>
<template v-if="!(isMobileMode && !activeView?.is_default)"> <template v-if="!(isMobileMode && !activeView?.is_default)">
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeTable?.meta?.icon" readonly size="xsmall"> <SmartsheetTopbarTableListDropdown v-if="activeTable">
<template #default="{ isOpen }">
<div
class="rounded-lg h-8 px-2 text-gray-700 font-weight-500 hover:(bg-gray-100 text-gray-900) flex items-center gap-1 cursor-pointer"
:class="{
'max-w-full': isMobileMode,
'max-w-1/2': activeView?.is_default,
'max-w-1/4': !isSharedBase && !isMobileMode && !activeView?.is_default,
'max-w-none': isSharedBase && !isMobileMode,
}"
>
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeTable?.meta?.icon" readonly size="xsmall" class="mr-1">
<template #default> <template #default>
<MdiTable <GeneralIcon
icon="table"
class="min-w-5" class="min-w-5"
:class="{ :class="{
'!text-gray-500': !isMobileMode, '!text-gray-500': !isMobileMode,
@ -83,55 +102,79 @@ const openedBaseUrl = computed(() => {
/> />
</template> </template>
</LazyGeneralEmojiPicker> </LazyGeneralEmojiPicker>
<div
v-if="activeTable" <NcTooltip class="truncate nc-active-table-title max-w-full !leading-5" show-on-truncate-only :disabled="isOpen">
:class="{
'max-w-1/2': isMobileMode || activeView?.is_default,
'max-w-20/100': !isSharedBase && !isMobileMode && !activeView?.is_default,
'max-w-none': isSharedBase && !isMobileMode,
}"
>
<NcTooltip class="truncate nc-active-table-title max-w-full" show-on-truncate-only>
<template #title> <template #title>
{{ activeTable?.title }} {{ activeTable?.title }}
</template> </template>
<span <span
class="text-ellipsis overflow-hidden text-gray-500 xs:ml-2" class="text-ellipsis"
:class="{
'text-gray-500': !isMobileMode,
'text-gray-800 font-medium': isMobileMode || activeView?.is_default,
}"
:style="{ :style="{
wordBreak: 'keep-all', wordBreak: 'keep-all',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
display: 'inline', display: 'inline',
}" }"
>
<template v-if="activeView?.is_default">
{{ activeTable?.title }}
</template>
<NuxtLink
v-else
class="!text-inherit !underline-transparent !hover:(text-black underline-gray-600)"
:to="tableUrl({ table: activeTable, completeUrl: true, isSharedBase })"
> >
{{ activeTable?.title }} {{ activeTable?.title }}
</NuxtLink>
</span> </span>
</NcTooltip> </NcTooltip>
<GeneralIcon
icon="chevronDown"
class="!text-current opacity-70 flex-none transform transition-transform duration-25 w-3.5 h-3.5"
:class="{ '!rotate-180': isOpen }"
/>
</div> </div>
</template> </template>
</SmartsheetTopbarTableListDropdown>
</template>
<div v-if="!isMobileMode" class="pl-1.25 text-gray-500">></div> <GeneralIcon v-if="!isMobileMode" icon="ncSlash1" class="nc-breadcrumb-divider" />
<template v-if="!(isMobileMode && activeView?.is_default)"> <template v-if="!(isMobileMode && activeView?.is_default)">
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeView?.meta?.icon" readonly size="xsmall"> <!-- <SmartsheetToolbarOpenedViewAction /> -->
<SmartsheetTopbarViewListDropdown>
<template #default="{ isOpen }">
<div
class="rounded-lg h-8 px-2 text-gray-800 font-semibold hover:(bg-gray-100 text-gray-900) flex items-center gap-1 cursor-pointer"
:class="{
'max-w-full': isMobileMode,
'max-w-2/5': !isSharedBase && !isMobileMode && activeView?.is_default,
'max-w-1/2': !isSharedBase && !isMobileMode && !activeView?.is_default,
'max-w-none': isSharedBase && !isMobileMode,
}"
>
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeView?.meta?.icon" readonly size="xsmall" class="mr-1">
<template #default> <template #default>
<GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" /> <GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" />
</template> </template>
</LazyGeneralEmojiPicker> </LazyGeneralEmojiPicker>
<SmartsheetToolbarOpenedViewAction /> <NcTooltip class="truncate nc-active-view-title max-w-full !leading-5" show-on-truncate-only :disabled="isOpen">
<template #title>
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</template>
<span
class="text-ellipsis"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}"
>
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span>
</NcTooltip>
<GeneralIcon
icon="chevronDown"
class="!text-current opacity-70 flex-none transform transition-transform duration-25 w-3.5 h-3.5"
:class="{ '!rotate-180': isOpen }"
/>
</div>
</template>
</SmartsheetTopbarViewListDropdown>
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view' && !isMobileMode" />
</template> </template>
</div> </div>
</template> </template>

80
packages/nc-gui/components/smartsheet/topbar/ProjectListDropdown.vue

@ -0,0 +1,80 @@
<script lang="ts" setup>
const basesStore = useBases()
const { basesList } = storeToRefs(basesStore)
const baseStore = useBase()
const { base: activeBase, isSharedBase } = storeToRefs(baseStore)
const { loadProjectTables } = useTablesStore()
const isOpen = ref<boolean>(false)
/**
* Handles navigation to a selected project/base.
*
* @param base - The project/base to navigate to.
* @returns A Promise that resolves when the navigation is complete.
*
* @remarks
* This function is called when a user selects a project from the dropdown list.
* It performs the following steps:
* 1. Checks if the base has a valid ID.
* 2. Determines if the project data is already populated.
* 3. Navigates to the selected project's URL.
* 4. If the project data isn't populated, it loads the project tables.
*/
const handleNavigateToProject = async (base: NcProject) => {
if (!base?.id) return
const isProjectPopulated = basesStore.isProjectPopulated(base.id!)
await navigateTo(
baseStore.baseUrl({
id: base.id!,
type: 'database',
isSharedBase: isSharedBase.value,
}),
)
if (!isProjectPopulated) {
await loadProjectTables(base.id)
}
}
</script>
<template>
<NcDropdown v-model:visible="isOpen">
<slot name="default" :is-open="isOpen"></slot>
<template #overlay>
<LazyNcList
v-if="activeBase.id"
v-model:open="isOpen"
:value="activeBase.id"
:list="basesList"
option-value-key="id"
option-label-key="title"
search-input-placeholder="Search bases"
@change="handleNavigateToProject"
>
<template #listItem="{ option }">
<GeneralBaseIconColorPicker :type="option?.type" :model-value="parseProp(option.meta).iconColor" size="xsmall" readonly>
</GeneralBaseIconColorPicker>
<NcTooltip class="truncate flex-1" show-on-truncate-only>
<template #title>
{{ option?.title }}
</template>
{{ option?.title }}
</NcTooltip>
<GeneralIcon
v-if="option.id === activeBase.id"
id="nc-selected-item-icon"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</template>
</LazyNcList>
</template>
</NcDropdown>
</template>
<style lang="scss" scoped></style>

12
packages/nc-gui/components/smartsheet/topbar/SelectMode.vue

@ -15,7 +15,7 @@ const onClickDetails = () => {
</script> </script>
<template> <template>
<div class="flex flex-row p-1 mx-3 mt-3 mb-3 bg-gray-100 rounded-lg gap-x-0.5 nc-view-sidebar-tab"> <div class="flex flex-row p-1 bg-gray-200 rounded-lg gap-x-0.5 nc-view-sidebar-tab">
<div <div
v-e="['c:project:mode:data']" v-e="['c:project:mode:data']"
class="tab" class="tab"
@ -51,12 +51,12 @@ const onClickDetails = () => {
<style scoped> <style scoped>
.tab { .tab {
@apply flex flex-row items-center h-7.5 justify-center px-2 py-1 bg-gray-100 rounded-lg gap-x-1.5 text-gray-500 hover:text-black cursor-pointer transition-all duration-300 select-none; @apply flex flex-row items-center h-6 justify-center px-2 py-1 rounded-md gap-x-2 text-gray-600 hover:text-black cursor-pointer transition-all duration-300 select-none;
} }
.tab-icon { .tab-icon {
font-size: 1.1rem !important; font-size: 1rem !important;
@apply w-4.5; @apply w-4;
} }
.tab .tab-title { .tab .tab-title {
@apply min-w-0; @apply min-w-0;
@ -67,6 +67,8 @@ const onClickDetails = () => {
} }
.active { .active {
@apply bg-white shadow text-brand-500 hover:text-brand-500; @apply bg-white text-brand-600 hover:text-brand-600;
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.06), 0px 5px 3px -2px rgba(0, 0, 0, 0.02);
} }
</style> </style>

159
packages/nc-gui/components/smartsheet/topbar/TableListDropdown.vue

@ -0,0 +1,159 @@
<script lang="ts" setup>
import type { TableType } from 'nocodb-sdk'
const { isMobileMode } = useGlobal()
const { $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
const { base } = storeToRefs(useBase())
const { activeTable, activeTables } = storeToRefs(useTablesStore())
const { openTable } = useTablesStore()
const isOpen = ref<boolean>(false)
const activeTableSourceIndex = computed(() => base.value?.sources?.findIndex((s) => s.id === activeTable.value?.source_id) ?? -1)
const filteredTableList = computed(() => {
return activeTables.value.filter((t: TableType) => t?.source_id === activeTable.value?.source_id) || []
})
/**
* Handles navigation to a selected table.
*
* @param table - The table to navigate to.
*
* @remarks
* This function is called when a user selects a table from the dropdown list.
* It checks if the table has a valid ID and then opens the selected table.
*/
const handleNavigateToTable = (table: TableType) => {
if (table?.id) {
openTable(table)
}
}
/**
* Opens a dialog to create a new table.
*
* @returns void
*
* @remarks
* This function is triggered when the user initiates the table creation process from the topbar.
* It emits a tracking event, checks for a valid source, and opens a dialog for table creation.
* The function also handles the dialog closure and potential scrolling to the newly created table.
*
* @see {@link packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue} for a similar implementation
* of table creation dialog. If this function is updated, consider updating the other implementation as well.
*/
function openTableCreateDialog() {
$e('c:table:create:topbar')
if (activeTableSourceIndex.value === -1) return
isOpen.value = false
const isCreateTableOpen = ref(true)
const sourceId = base.value!.sources?.[activeTableSourceIndex.value].id
if (!sourceId || !base.value?.id) return
const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isCreateTableOpen,
sourceId, // || sources.value[0].id,
'baseId': base.value!.id,
'onCreate': closeDialog,
'onUpdate:modelValue': () => closeDialog(),
})
function closeDialog(table?: TableType) {
isCreateTableOpen.value = false
if (!table) return
// TODO: Better way to know when the table node dom is available
setTimeout(() => {
const newTableDom = document.querySelector(`[data-table-id="${table.id}"]`)
if (!newTableDom) return
// Scroll to the table node
newTableDom?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}, 1000)
close(1000)
}
}
</script>
<template>
<NcDropdown v-model:visible="isOpen">
<slot name="default" :is-open="isOpen"></slot>
<template #overlay>
<LazyNcList
v-model:open="isOpen"
:value="activeTable.id"
:list="filteredTableList"
option-value-key="id"
option-label-key="title"
search-input-placeholder="Search tables"
@change="handleNavigateToTable"
>
<template #listItem="{ option }">
<div>
<LazyGeneralEmojiPicker :emoji="option?.meta?.icon" readonly size="xsmall">
<template #default>
<GeneralIcon icon="table" class="min-w-4 !text-gray-500" />
</template>
</LazyGeneralEmojiPicker>
</div>
<NcTooltip class="truncate flex-1" show-on-truncate-only>
<template #title>
{{ option?.title }}
</template>
{{ option?.title }}
</NcTooltip>
<GeneralIcon
v-if="option.id === activeTable.id"
id="nc-selected-item-icon"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</template>
<template
v-if="
!isMobileMode &&
isUIAllowed('tableCreate', {
roles: base?.project_role || base?.workspace_role,
source: base?.sources?.[activeTableSourceIndex] || {},
})
"
#listFooter
>
<NcDivider class="!mt-0 !mb-2" />
<div class="px-2 mb-2" @click="openTableCreateDialog()">
<div
class="px-2 py-1.5 flex items-center justify-between gap-2 text-sm font-weight-500 !text-brand-500 hover:bg-gray-100 rounded-md cursor-pointer"
>
<div class="flex items-center gap-2">
<GeneralIcon icon="plus" />
<div>
{{
$t('general.createEntity', {
entity: $t('objects.table'),
})
}}
</div>
</div>
</div>
</div>
</template>
</LazyNcList>
</template>
</NcDropdown>
</template>
<style lang="scss" scoped></style>

303
packages/nc-gui/components/smartsheet/topbar/ViewListDropdown.vue

@ -0,0 +1,303 @@
<script lang="ts" setup>
import { type ViewType, ViewTypes } from 'nocodb-sdk'
const { isMobileMode } = useGlobal()
const { t } = useI18n()
const { $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
const { base } = storeToRefs(useBase())
const { activeTable } = storeToRefs(useTablesStore())
const viewsStore = useViewsStore()
const { activeView, views } = storeToRefs(viewsStore)
const { loadViews, navigateToView } = viewsStore
const { refreshCommandPalette } = useCommandPalette()
const isOpen = ref<boolean>(false)
const activeSource = computed(() => {
return base.value.sources?.find((s) => s.id === activeView.value?.source_id)
})
/**
* Handles navigation to a selected view.
*
* @param view - The view to navigate to.
* @returns A Promise that resolves when the navigation is complete.
*
* @remarks
* This function is called when a user selects a view from the dropdown list.
* It checks if the view has a valid ID and then navigates to the selected view.
* If the view is a form and it's already active, it performs a hard reload.
*/
const handleNavigateToView = async (view: ViewType) => {
if (!view?.id) return
await navigateToView({
view,
tableId: activeTable.value.id!,
baseId: base.value.id!,
hardReload: view.type === ViewTypes.FORM && activeView.value?.id === view.id,
doNotSwitchTab: true,
})
}
/**
* Filters the view options based on the input string.
*
* @param input - The search input string.
* @param view - The view object to be filtered.
* @returns True if the view matches the filter criteria, false otherwise.
*
* @remarks
* This function is used to filter the list of views in the dropdown.
* It checks if the input string matches either the default view title (translated) or the view's title.
* The matching is case-insensitive.
*/
const filterOption = (input: string = '', view: ViewType) => {
if (view.is_default && t('title.defaultView').toLowerCase().includes(input)) {
return true
}
return view.title?.toLowerCase()?.includes(input.toLowerCase())
}
/**
* Opens a modal for creating or editing a view.
*
* @param options - The options for opening the modal.
* @param options.title - The title of the modal. Default is an empty string.
* @param options.type - The type of view to create or edit.
* @param options.copyViewId - The ID of the view to copy, if creating a copy.
* @param options.groupingFieldColumnId - The ID of the column to use for grouping, if applicable.
* @param options.calendarRange - The date range for calendar views.
* @param options.coverImageColumnId - The ID of the column to use for cover images, if applicable.
*
* @returns A Promise that resolves when the modal operation is complete.
*
* @remarks
* This function opens a modal dialog for creating or editing a view.
* It handles the dialog state, view creation, and navigation to the newly created view.
* After creating a view, it refreshes the command palette and reloads the views.
*
* @see {@link packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue} for a similar implementation of view creation dialog.
* If this function is updated, consider updating the other implementations as well.
*/
async function onOpenModal({
title = '',
type,
copyViewId,
groupingFieldColumnId,
calendarRange,
coverImageColumnId,
}: {
title?: string
type: ViewTypes
copyViewId?: string
groupingFieldColumnId?: string
calendarRange?: Array<{
fk_from_column_id: string
fk_to_column_id: string | null
}>
coverImageColumnId?: string
}) {
isOpen.value = false
const isDlgOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgViewCreate'), {
'modelValue': isDlgOpen,
title,
type,
'tableId': activeTable.value.id,
'selectedViewId': copyViewId,
calendarRange,
groupingFieldColumnId,
coverImageColumnId,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
refreshCommandPalette()
await loadViews({
tableId: activeTable.value.id!,
force: true,
})
activeTable.value.meta = {
...(activeTable.value.meta as object),
hasNonDefaultViews: true,
}
navigateToView({
view,
tableId: activeTable.value.id!,
baseId: base.value.id!,
doNotSwitchTab: true,
})
$e('a:view:create', { view: view.type })
},
})
function closeDialog() {
isOpen.value = false
isDlgOpen.value = false
close(1000)
}
}
</script>
<template>
<NcDropdown v-if="activeView" v-model:visible="isOpen">
<slot name="default" :is-open="isOpen"></slot>
<template #overlay>
<LazyNcList
v-model:open="isOpen"
:value="activeView.id"
:list="views"
option-value-key="id"
option-label-key="title"
search-input-placeholder="Search views"
:filter-option="filterOption"
@change="handleNavigateToView"
>
<template #listItem="{ option }">
<div>
<LazyGeneralEmojiPicker :emoji="option?.meta?.icon" readonly size="xsmall">
<template #default>
<GeneralViewIcon :meta="{ type: option?.type }" class="min-w-4 text-lg flex" />
</template>
</LazyGeneralEmojiPicker>
</div>
<NcTooltip class="truncate flex-1" show-on-truncate-only>
<template #title>
{{ option?.is_default ? $t('title.defaultView') : option?.title }}
</template>
{{ option?.is_default ? $t('title.defaultView') : option?.title }}
</NcTooltip>
<GeneralIcon
v-if="option.id === activeView.id"
id="nc-selected-item-icon"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</template>
<template v-if="!isMobileMode && isUIAllowed('viewCreateOrEdit')" #listFooter>
<NcDivider class="!mt-0 !mb-2" />
<div class="overflow-hidden mb-2">
<a-menu class="nc-viewlist-menu">
<a-sub-menu popup-class-name="nc-viewlist-submenu-popup ">
<template #title>
<div class="flex items-center justify-between gap-2 text-sm font-weight-500 !text-brand-500">
<div class="flex items-center gap-2">
<GeneralIcon icon="plus" />
<div>
{{
$t('general.createEntity', {
entity: $t('objects.view'),
})
}}
</div>
</div>
<GeneralIcon icon="arrowRight" class="text-base text-gray-600 group-hover:text-gray-800" />
</div>
</template>
<template #expandIcon> </template>
<a-menu-item @click.stop="onOpenModal({ type: ViewTypes.GRID })">
<div class="nc-viewlist-submenu-popup-item" data-testid="topbar-view-create-grid">
<GeneralViewIcon :meta="{ type: ViewTypes.GRID }" />
Grid
</div>
</a-menu-item>
<a-menu-item v-if="!activeSource?.is_schema_readonly" @click="onOpenModal({ type: ViewTypes.FORM })">
<div class="nc-viewlist-submenu-popup-item" data-testid="topbar-view-create-form">
<GeneralViewIcon :meta="{ type: ViewTypes.FORM }" />
Form
</div>
</a-menu-item>
<a-menu-item @click="onOpenModal({ type: ViewTypes.GALLERY })">
<div class="nc-viewlist-submenu-popup-item" data-testid="topbar-view-create-gallery">
<GeneralViewIcon :meta="{ type: ViewTypes.GALLERY }" />
Gallery
</div>
</a-menu-item>
<a-menu-item data-testid="topbar-view-create-kanban" @click="onOpenModal({ type: ViewTypes.KANBAN })">
<div class="nc-viewlist-submenu-popup-item">
<GeneralViewIcon :meta="{ type: ViewTypes.KANBAN }" />
Kanban
</div>
</a-menu-item>
<a-menu-item data-testid="topbar-view-create-calendar" @click="onOpenModal({ type: ViewTypes.CALENDAR })">
<div class="nc-viewlist-submenu-popup-item">
<GeneralViewIcon :meta="{ type: ViewTypes.CALENDAR }" class="!w-4 !h-4" />
{{ $t('objects.viewType.calendar') }}
</div>
</a-menu-item>
</a-sub-menu>
</a-menu>
</div>
</template>
</LazyNcList>
</template>
</NcDropdown>
</template>
<style lang="scss">
.nc-viewlist-menu {
@apply !border-r-0;
.ant-menu-submenu {
@apply !mx-2;
.ant-menu-submenu-title {
@apply flex items-center gap-2 py-1.5 px-2 my-0 h-auto hover:bg-gray-100 cursor-pointer rounded-md;
.ant-menu-title-content {
@apply w-full;
}
}
}
}
.nc-viewlist-submenu-popup {
@apply !rounded-lg border-1 border-gray-50;
.ant-menu.ant-menu-sub {
@apply p-2 !rounded-lg !shadow-lg shadow-gray-200;
}
.ant-menu-item {
@apply h-auto !my-0 text-sm !leading-5 py-2 px-2 hover:!bg-gray-100 cursor-pointer rounded-md;
.ant-menu-title-content {
@apply w-full px-0;
}
.nc-viewlist-submenu-popup-item {
@apply flex items-center gap-2 !text-gray-800;
}
&.ant-menu-item-selected {
@apply bg-transparent;
}
}
}
.nc-viewlist-submenu-popup .ant-dropdown-menu.ant-dropdown-menu-sub {
@apply !rounded-lg !shadow-lg shadow-gray-200;
}
</style>

19
packages/nc-gui/components/workspace/AuditLogs.vue

@ -198,7 +198,21 @@ onKeyStroke('ArrowDown', onDown)
</script> </script>
<template> <template>
<div class="h-full flex flex-col px-1" :class="{ 'gap-6 pb-6': !baseId, 'gap-4': baseId }"> <div class="h-full flex flex-col" :class="{ 'gap-4 px-1': baseId }">
<NcPageHeader v-if="!baseId">
<template #icon>
<GeneralIcon icon="audit" class="flex-none text-gray-700 h-5 w-5" />
</template>
<template #title>
<span data-rec="true">
{{ $t('title.auditLogs') }}
</span>
</template>
</NcPageHeader>
<div
class="flex flex-col"
:class="{ 'gap-6 p-6 border-t-1 border-gray-200 h-[calc(100vh_-_100px)]': !baseId, 'gap-4 h-full': baseId }"
>
<div v-if="!appInfo.auditEnabled" class="text-red-500">Audit logs are currently disabled by administrators.</div> <div v-if="!appInfo.auditEnabled" class="text-red-500">Audit logs are currently disabled by administrators.</div>
<div class="flex flex-col" :class="{ 'gap-6': !baseId, 'gap-4': baseId }"> <div class="flex flex-col" :class="{ 'gap-6': !baseId, 'gap-4': baseId }">
@ -209,6 +223,7 @@ onKeyStroke('ArrowDown', onDown)
}" }"
> >
<h6 <h6
v-if="baseId"
class="font-semibold text-gray-900 !my-0 flex items-center gap-1" class="font-semibold text-gray-900 !my-0 flex items-center gap-1"
:style="{ :style="{
'word-break': 'keep-all', 'word-break': 'keep-all',
@ -220,7 +235,6 @@ onKeyStroke('ArrowDown', onDown)
> >
<span class="keep-word min-w-[100px]"> {{ $t('title.auditLogs') }} </span> <span class="keep-word min-w-[100px]"> {{ $t('title.auditLogs') }} </span>
<NcTooltip <NcTooltip
v-if="baseId"
class="max-w-[80%] truncate" class="max-w-[80%] truncate"
:class="{ :class="{
'!leading-7': baseId, '!leading-7': baseId,
@ -581,6 +595,7 @@ onKeyStroke('ArrowDown', onDown)
</NcModal> </NcModal>
</template> </template>
</div> </div>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

8
packages/nc-gui/components/workspace/CollaboratorsList.vue

@ -166,7 +166,13 @@ const customRow = (_record: Record<string, any>, recordIndex: number) => ({
</script> </script>
<template> <template>
<div class="nc-collaborator-table-container py-6 h-[calc(100vh-92px)] max-w-350 px-1 flex flex-col gap-6"> <div
class="nc-collaborator-table-container py-6 max-w-350 px-6 flex flex-col gap-6"
:class="{
'h-[calc(100vh-144px)]': isAdminPanel,
'h-[calc(100vh-92px)]': !isAdminPanel,
}"
>
<div class="w-full flex items-center justify-between gap-3"> <div class="w-full flex items-center justify-between gap-3">
<a-input <a-input
v-model:value="userSearchText" v-model:value="userSearchText"

64
packages/nc-gui/components/workspace/View.vue

@ -17,7 +17,7 @@ const { activeWorkspace: _activeWorkspace, workspaces } = storeToRefs(workspaceS
const { loadCollaborators, loadWorkspace } = workspaceStore const { loadCollaborators, loadWorkspace } = workspaceStore
const orgStore = useOrg() const orgStore = useOrg()
const { orgId } = storeToRefs(orgStore) const { orgId, org } = storeToRefs(orgStore)
const currentWorkspace = computedAsync(async () => { const currentWorkspace = computedAsync(async () => {
let ws let ws
@ -68,38 +68,62 @@ onMounted(() => {
</script> </script>
<template> <template>
<div v-if="currentWorkspace" class="flex w-full px-6 max-w-[97.5rem] flex-col nc-workspace-settings"> <div v-if="currentWorkspace" class="flex w-full max-w-[97.5rem] flex-col nc-workspace-settings">
<div v-if="!props.workspaceId" class="flex gap-2 items-center min-w-0 py-4"> <div v-if="!props.workspaceId" class="min-w-0 pt-2 px-2">
<h1 class="text-base capitalize font-weight-bold tracking-[0.5px] mb-0 nc-workspace-title truncate min-w-10 capitalize"> <div class="nc-breadcrumb nc-workspace-title">
<span class="text-gray-500">{{ currentWorkspace?.title }} ></span> {{ $t('title.teamAndSettings') }} <div class="nc-breadcrumb-item capitalize">
{{ currentWorkspace?.title }}
</div>
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
<h1 class="nc-breadcrumb-item active">
{{ $t('title.teamAndSettings') }}
</h1> </h1>
</div> </div>
<div v-else> </div>
<div class="font-bold w-full !mb-5 text-2xl" data-rec="true"> <template v-else>
<div class="flex items-center gap-3"> <div class="nc-breadcrumb px-2">
<div class="nc-breadcrumb-item">
{{ org.title }}
</div>
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
<NuxtLink <NuxtLink
:href="`/admin/${orgId}/workspaces`" :href="`/admin/${orgId}/workspaces`"
class="!hover:(text-black underline-gray-600) flex items-center !text-black !underline-transparent ml-0.75 max-w-1/4" class="!hover:(text-gray-800 underline-gray-600) flex items-center !text-gray-700 !underline-transparent ml-0.75 max-w-1/4"
> >
<component :is="iconMap.arrowLeft" class="text-3xl" /> <div class="nc-breadcrumb-item">
{{ $t('labels.workspaces') }} {{ $t('labels.workspaces') }}
</div>
</NuxtLink> </NuxtLink>
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
<span class="text-2xl"> / </span> <div class="nc-breadcrumb-item active truncate capitalize">
<GeneralWorkspaceIcon :workspace="currentWorkspace" hide-label />
<span class="text-base capitalize">
{{ currentWorkspace?.title }} {{ currentWorkspace?.title }}
</span>
</div> </div>
</div> </div>
<NcPageHeader>
<template #icon>
<div class="flex justify-center items-center h-6 w-6">
<GeneralWorkspaceIcon :workspace="currentWorkspace" hide-label size="small" />
</div> </div>
</template>
<template #title>
<span data-rec="true" class="capitalize">
{{ currentWorkspace?.title }}
</span>
</template>
</NcPageHeader>
</template>
<NcTabs v-model:activeKey="tab"> <NcTabs v-model:activeKey="tab">
<template #leftExtra>
<div class="w-3"></div>
</template>
<template v-if="isUIAllowed('workspaceSettings')"> <template v-if="isUIAllowed('workspaceSettings')">
<a-tab-pane key="collaborators" class="w-full"> <a-tab-pane key="collaborators" class="w-full">
<template #tab> <template #tab>
<div class="flex flex-row items-center pb-1 gap-x-1.5"> <div class="flex flex-row items-center pb-1 pt-2 gap-x-1.5">
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" /> <GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
Members Members
</div> </div>
@ -111,7 +135,7 @@ onMounted(() => {
<template v-if="isUIAllowed('workspaceManage')"> <template v-if="isUIAllowed('workspaceManage')">
<a-tab-pane key="settings" class="w-full"> <a-tab-pane key="settings" class="w-full">
<template #tab> <template #tab>
<div class="flex flex-row items-center pb-1 gap-x-1.5" data-testid="nc-workspace-settings-tab-settings"> <div class="flex flex-row items-center pb-1 pt-2 gap-x-1.5" data-testid="nc-workspace-settings-tab-settings">
<GeneralIcon icon="settings" /> <GeneralIcon icon="settings" />
Settings Settings
</div> </div>
@ -123,12 +147,12 @@ onMounted(() => {
<template v-if="isUIAllowed('workspaceAuditList') && !props.workspaceId"> <template v-if="isUIAllowed('workspaceAuditList') && !props.workspaceId">
<a-tab-pane key="audit" class="w-full"> <a-tab-pane key="audit" class="w-full">
<template #tab> <template #tab>
<div class="flex flex-row items-center pb-1 gap-x-1.5"> <div class="flex flex-row items-center pb-1 pt-2 gap-x-1.5">
<GeneralIcon icon="audit" class="!h-3.5 !w-3.5" /> <GeneralIcon icon="audit" class="!h-3.5 !w-3.5" />
Audit Logs Audit Logs
</div> </div>
</template> </template>
<div class="h-[calc(100vh-92px)]"> <div class="h-[calc(100vh-92px)] px-6">
<WorkspaceAuditLogs :workspace-id="currentWorkspace.id" /> <WorkspaceAuditLogs :workspace-id="currentWorkspace.id" />
</div> </div>
</a-tab-pane> </a-tab-pane>
@ -139,7 +163,7 @@ onMounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.nc-workspace-avatar { .nc-workspace-avatar {
@apply min-w-6 h-6 rounded-[6px] flex items-center justify-center text-white font-weight-bold uppercase; @apply min-w-5 h-5 w-5 rounded-[6px] flex items-center justify-center text-white font-weight-bold uppercase;
font-size: 0.7rem; font-size: 0.7rem;
} }

16
packages/nc-gui/components/workspace/integrations/view.vue

@ -47,11 +47,17 @@ onBeforeMount(() => {
<template> <template>
<div v-if="currentWorkspace" class="flex w-full max-w-[97.5rem] flex-col nc-workspace-integrations"> <div v-if="currentWorkspace" class="flex w-full max-w-[97.5rem] flex-col nc-workspace-integrations">
<div class="flex gap-2 items-center min-w-0 py-4 px-6"> <div class="flex gap-2 items-center min-w-0 pt-2 px-2">
<h1 class="text-base capitalize font-weight-bold tracking-[0.5px] mb-0 nc-workspace-title truncate min-w-10 capitalize"> <div class="nc-breadcrumb">
<span class="text-gray-500"> {{ currentWorkspace?.title }} ></span> {{ $t('general.integrations') }} <div class="nc-breadcrumb-item capitalize">
{{ currentWorkspace?.title }}
</div>
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
<h1 class="nc-breadcrumb-item active">
{{ $t('general.integrations') }}
</h1> </h1>
</div> </div>
</div>
<NcTabs v-model:activeKey="activeViewTab"> <NcTabs v-model:activeKey="activeViewTab">
<template #leftExtra> <template #leftExtra>
@ -60,7 +66,7 @@ onBeforeMount(() => {
<template v-if="isUIAllowed('workspaceIntegrations')"> <template v-if="isUIAllowed('workspaceIntegrations')">
<a-tab-pane key="integrations" class="w-full"> <a-tab-pane key="integrations" class="w-full">
<template #tab> <template #tab>
<div class="flex flex-row items-center pb-1 gap-x-1.5" data-testid="nc-workspace-settings-tab-integrations"> <div class="flex flex-row items-center pb-1 pt-2 gap-x-1.5" data-testid="nc-workspace-settings-tab-integrations">
<GeneralIcon icon="integration" /> <GeneralIcon icon="integration" />
{{ $t('general.integrations') }} {{ $t('general.integrations') }}
</div> </div>
@ -73,7 +79,7 @@ onBeforeMount(() => {
<template v-if="isUIAllowed('workspaceIntegrations')"> <template v-if="isUIAllowed('workspaceIntegrations')">
<a-tab-pane key="connections" class="w-full"> <a-tab-pane key="connections" class="w-full">
<template #tab> <template #tab>
<div class="flex flex-row items-center pb-1 gap-x-1.5" data-testid="nc-workspace-settings-tab-integrations"> <div class="flex flex-row items-center pb-1 pt-2 gap-x-1.5" data-testid="nc-workspace-settings-tab-integrations">
<GeneralIcon icon="gitCommit" /> <GeneralIcon icon="gitCommit" />
{{ $t('general.connections') }} {{ $t('general.connections') }}
<div <div

46
packages/nc-gui/pages/account/index.vue

@ -29,10 +29,10 @@ const logout = async () => {
<div> <div>
<NuxtLayout name="empty"> <NuxtLayout name="empty">
<div class="mx-auto h-full"> <div class="mx-auto h-full">
<div class="h-full overflow-y-auto flex"> <div class="h-full flex">
<!-- Side tabs --> <!-- Side tabs -->
<div class="h-full bg-white nc-user-sidebar fixed"> <div class="h-full bg-white nc-user-sidebar overflow-y-auto nc-scrollbar-thin min-w-[312px]">
<NcMenu <NcMenu
v-model:openKeys="openKeys" v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys" v-model:selectedKeys="selectedKeys"
@ -40,20 +40,23 @@ const logout = async () => {
class="tabs-menu h-full" class="tabs-menu h-full"
mode="inline" mode="inline"
> >
<div <NcButton
v-if="!$route.params.baseType" v-if="!$route.params.baseType"
v-e="['c:navbar:home']" v-e="['c:navbar:home']"
type="text"
size="small"
class="transition-all duration-200 mx-2 my-2.5 cursor-pointer transform hover:bg-gray-100 nc-noco-brand-icon"
data-testid="nc-noco-brand-icon" data-testid="nc-noco-brand-icon"
class="transition-all duration-200 px-2 mx-2 mt-1.5 cursor-pointer transform hover:bg-gray-100 my-1 nc-noco-brand-icon h-8 rounded-md min-w-60"
@click="navigateTo('/')" @click="navigateTo('/')"
> >
<div class="flex flex-row gap-x-2 items-center h-8.5"> <div class="flex flex-row gap-x-2 items-center">
<GeneralIcon icon="arrowLeft" class="-mt-0.1" /> <GeneralIcon icon="ncArrowLeft" />
<div class="flex text-sm font-medium text-gray-800">{{ $t('labels.backToWorkspace') }}</div> <div class="flex text-small leading-[18px] font-semibold">{{ $t('labels.back') }}</div>
</div>
</div> </div>
</NcButton>
<NcDivider class="!mt-0" />
<div class="text-sm text-gray-600 ml-4 p-2 mt-3 gray-600 font-medium">{{ $t('labels.account') }}</div> <div class="text-sm text-gray-500 font-semibold ml-4 py-1.5 mt-2">{{ $t('labels.account') }}</div>
<NcMenuItem <NcMenuItem
key="profile" key="profile"
@ -78,9 +81,9 @@ const logout = async () => {
@click="navigateTo('/account/tokens')" @click="navigateTo('/account/tokens')"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<component :is="iconMap.code" /> <MdiShieldKeyOutline />
<div class="select-none">API {{ $t('title.tokens') }}</div> <div class="select-none">{{ $t('title.tokens') }}</div>
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
@ -156,9 +159,11 @@ const logout = async () => {
<!-- Sub Tabs --> <!-- Sub Tabs -->
<div class="flex flex-col w-full pl-65"> <div class="h-full flex-1 flex flex-col overflow-y-auto nc-scrollbar-thin">
<div class="flex flex-row p-3 items-center h-14"> <div class="flex flex-row pt-2 px-2 items-center">
<div class="flex-1" /> <div class="flex-1">
<LazyAccountBreadcrumb />
</div>
<LazyGeneralReleaseInfo /> <LazyGeneralReleaseInfo />
@ -198,12 +203,12 @@ const logout = async () => {
</template> </template>
</div> </div>
<div <div
class="flex flex-col container mx-auto" class="flex flex-col w-full max-w-[97.5rem]"
:style="{ :style="{
height: 'calc(100vh - 3.5rem)', height: 'calc(100vh - 3.5rem)',
}" }"
> >
<div class="mt-2 h-full"> <div class="h-full">
<NuxtPage /> <NuxtPage />
</div> </div>
</div> </div>
@ -231,14 +236,15 @@ const logout = async () => {
:deep(.ant-menu-submenu-selected .ant-menu-submenu-arrow) { :deep(.ant-menu-submenu-selected .ant-menu-submenu-arrow) {
@apply !text-inherit; @apply !text-inherit;
} }
.tabs-menu {
:deep(.item) { :deep(.item) {
@apply select-none mx-2 !px-3 !text-sm !rounded-md !mb-1 !hover:(bg-brand-50 text-brand-500); @apply select-none mx-2 !px-3 !text-sm !rounded-md !mb-1 !hover:(bg-brand-50 text-brand-500);
width: calc(100% - 1rem); width: calc(100% - 1rem);
} }
:deep(.active) { :deep(.active) {
@apply !bg-brand-50 !text-brand-500; @apply !bg-brand-50 !text-brand-500;
}
} }
:deep(.ant-menu-submenu-title) { :deep(.ant-menu-submenu-title) {

4
packages/nc-gui/pages/account/index/[page].vue

@ -5,9 +5,7 @@ const { appInfo } = useGlobal()
<template> <template>
<div> <div>
<AccountToken v-if="$route.params.page === 'tokens'" /> <AccountToken v-if="$route.params.page === 'tokens'" />
<div v-else-if="$route.params.page === 'audit'" class="h-[calc(100vh_-_4rem)] w-full px-6"> <WorkspaceAuditLogs v-else-if="$route.params.page === 'audit'" />
<WorkspaceAuditLogs />
</div>
<AccountProfile v-else-if="$route.params.page === 'profile'" /> <AccountProfile v-else-if="$route.params.page === 'profile'" />
<AccountAppStore v-else-if="$route.params.page === 'apps' && !appInfo.isCloud" /> <AccountAppStore v-else-if="$route.params.page === 'apps' && !appInfo.isCloud" />
<span v-else></span> <span v-else></span>

2
packages/nc-gui/pages/account/index/users/[[nestedPage]].vue

@ -3,7 +3,7 @@ const { isUIAllowed } = useRoles()
</script> </script>
<template> <template>
<div class="h-full overflow-y-auto scrollbar-thin-dull pt-2"> <div class="h-full">
<template <template
v-if=" v-if="
$route.params.nestedPage === 'password-reset' || $route.params.nestedPage === 'password-reset' ||

3
packages/nc-gui/store/config.ts

@ -1,3 +1,4 @@
import { ViewTypes } from 'nocodb-sdk'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { MAX_WIDTH_FOR_MOBILE_MODE } from '~/lib/constants' import { MAX_WIDTH_FOR_MOBILE_MODE } from '~/lib/constants'
@ -33,7 +34,7 @@ export const useConfigStore = defineStore('configStore', () => {
globalIsMobile.value = isMobileMode.value globalIsMobile.value = isMobileMode.value
// Change --topbar-height css variable // Change --topbar-height css variable
document.documentElement.style.setProperty('--topbar-height', isMobileMode.value ? '3.25rem' : '3.1rem') document.documentElement.style.setProperty('--topbar-height', isMobileMode.value ? '3.875rem' : '3rem')
// Set .mobile-mode class on body // Set .mobile-mode class on body
if (isMobileMode.value) { if (isMobileMode.value) {

12
packages/nc-gui/utils/iconUtils.ts

@ -150,6 +150,7 @@ import NcStrike from '~icons/nc-icons/strike-through'
import NcCrop from '~icons/nc-icons/crop' import NcCrop from '~icons/nc-icons/crop'
import NcControlPanel from '~icons/nc-icons/control-panel' import NcControlPanel from '~icons/nc-icons/control-panel'
import NcHome from '~icons/nc-icons/home' import NcHome from '~icons/nc-icons/home'
import NcHome1 from '~icons/nc-icons/home1'
import NcWorkspace from '~icons/nc-icons/workspace' import NcWorkspace from '~icons/nc-icons/workspace'
import NcCellBarcode from '~icons/nc-icons/cell-barcode' import NcCellBarcode from '~icons/nc-icons/cell-barcode'
@ -528,6 +529,12 @@ import NcAlignLeftIcon from '~icons/nc-icons-v2/align-left.svg'
import NcHeartIcon from '~icons/nc-icons-v2/heart.svg' import NcHeartIcon from '~icons/nc-icons-v2/heart.svg'
import NcTrendingUpIcon from '~icons/nc-icons-v2/trending-up.svg' import NcTrendingUpIcon from '~icons/nc-icons-v2/trending-up.svg'
import NcSlash1 from '~icons/nc-icons/slash1'
import NcChevronUpSmall from '~icons/nc-icons/chevron-up-small'
import NcChevronDownSmall from '~icons/nc-icons/chevron-down-small'
import NcChevronUpDownSmall from '~icons/nc-icons/chevron-up-down-small'
// keep it for reference // keep it for reference
// todo: remove it after all icons are migrated // todo: remove it after all icons are migrated
/* export const iconMapOld = { /* export const iconMapOld = {
@ -679,10 +686,12 @@ export const iconMap = {
strike: NcStrike, strike: NcStrike,
atSign: NcAtSign, atSign: NcAtSign,
slash: NcSlash, slash: NcSlash,
ncSlash1: NcSlash1,
arrowUpRight: NcArrowUpRight, arrowUpRight: NcArrowUpRight,
ncWorkspace: NcWorkspace, ncWorkspace: NcWorkspace,
controlPanel: NcControlPanel, controlPanel: NcControlPanel,
home: NcHome, home: NcHome,
home1: NcHome1,
cellBarcode: NcCellBarcode, cellBarcode: NcCellBarcode,
cellCheckbox: NcCellCheckbox, cellCheckbox: NcCellCheckbox,
cellDate: NcCellDate, cellDate: NcCellDate,
@ -1275,6 +1284,9 @@ export const iconMap = {
ncAlignLeft: NcAlignLeftIcon, ncAlignLeft: NcAlignLeftIcon,
ncHeart: NcHeartIcon, ncHeart: NcHeartIcon,
ncTrendingUp: NcTrendingUpIcon, ncTrendingUp: NcTrendingUpIcon,
chevronUpSmall: NcChevronUpSmall,
chevronDownSmall: NcChevronDownSmall,
chevronUpDownSmall: NcChevronUpDownSmall,
} }
export const getMdiIcon = (type: string): any => { export const getMdiIcon = (type: string): any => {

Loading…
Cancel
Save