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. 19
      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. 107
      packages/nc-gui/components/account/Profile.vue
  11. 163
      packages/nc-gui/components/account/ResetPassword.vue
  12. 43
      packages/nc-gui/components/account/SignupSettings.vue
  13. 353
      packages/nc-gui/components/account/Token.vue
  14. 375
      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. 222
      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. 72
      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. 37
      packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue
  37. 4
      packages/nc-gui/components/smartsheet/toolbar/Reload.vue
  38. 29
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  39. 253
      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. 679
      packages/nc-gui/components/workspace/AuditLogs.vue
  45. 8
      packages/nc-gui/components/workspace/CollaboratorsList.vue
  46. 76
      packages/nc-gui/components/workspace/View.vue
  47. 18
      packages/nc-gui/components/workspace/integrations/view.vue
  48. 54
      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;
}

19
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>
<div> <template #icon>
<LazyDashboardSettingsAppStore /> <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>
<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>

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

@ -61,57 +61,68 @@ 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>
<div class="mt-5 flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-y-2"> <GeneralIcon class="flex-none !h-5 !w-5" icon="user" />
<div class="flex font-medium text-base" data-rec="true">{{ $t('labels.accountDetails') }}</div> </template>
<div class="flex text-gray-500" data-rec="true">{{ $t('labels.controlAppearance') }}</div> <template #title>
<div class="flex flex-row mt-4"> <span data-rec="true">
<div class="flex h-20 mt-1.5"> {{ $t('labels.profile') }}
<GeneralUserIcon size="xlarge" :email="user?.email" :name="user?.display_name" /> </span>
</div> </template>
<div class="flex w-10"></div> </NcPageHeader>
<a-form <div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
ref="formValidator" <div class="flex flex-col w-150 mx-auto">
layout="vertical" <div class="mt-5 flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-y-2">
no-style <div class="flex font-medium text-base" data-rec="true">{{ $t('labels.accountDetails') }}</div>
:model="form" <div class="flex text-gray-500" data-rec="true">{{ $t('labels.controlAppearance') }}</div>
class="flex flex-col w-full" <div class="flex flex-row mt-4">
@finish="onSubmit" <div class="flex h-20 mt-1.5">
@validate="onValidate" <GeneralUserIcon size="xlarge" :email="user?.email" :name="user?.display_name" />
> </div>
<div class="text-gray-800 mb-1.5" data-rec="true">{{ $t('general.name') }}</div> <div class="flex w-10"></div>
<a-form-item name="title" :rules="formRules.title"> <a-form
ref="formValidator"
layout="vertical"
no-style
:model="form"
class="flex flex-col w-full"
@finish="onSubmit"
@validate="onValidate"
>
<div class="text-gray-800 mb-1.5" data-rec="true">{{ $t('general.name') }}</div>
<a-form-item name="title" :rules="formRules.title">
<a-input
v-model:value="form.title"
class="w-full !rounded-md !py-1.5"
:placeholder="$t('general.name')"
data-testid="nc-account-settings-rename-input"
/>
</a-form-item>
<div class="text-gray-800 mb-1.5" data-rec="true">{{ $t('labels.accountEmailID') }}</div>
<a-input <a-input
v-model:value="form.title" v-model:value="email"
class="w-full !rounded-md !py-1.5" class="w-full !rounded-md !py-1.5"
:placeholder="$t('general.name')" :placeholder="$t('labels.email')"
data-testid="nc-account-settings-rename-input" disabled
data-testid="nc-account-settings-email-input"
/> />
</a-form-item> <div class="flex flex-row w-full justify-end mt-8" data-rec="true">
<div class="text-gray-800 mb-1.5" data-rec="true">{{ $t('labels.accountEmailID') }}</div> <NcButton
<a-input type="primary"
v-model:value="email" html-type="submit"
class="w-full !rounded-md !py-1.5" :disabled="isErrored || (form?.title && form?.title === user?.display_name)"
:placeholder="$t('labels.email')" :loading="isTitleUpdating"
disabled data-testid="nc-account-settings-save"
data-testid="nc-account-settings-email-input" @click="onSubmit"
/> >
<div class="flex flex-row w-full justify-end mt-8" data-rec="true"> <template #loading> {{ $t('general.saving') }} </template>
<NcButton {{ $t('general.save') }}
type="primary" </NcButton>
html-type="submit" </div>
:disabled="isErrored || (form?.title && form?.title === user?.display_name)" </a-form>
:loading="isTitleUpdating" </div>
data-testid="nc-account-settings-save"
@click="onSubmit"
>
<template #loading> {{ $t('general.saving') }} </template>
{{ $t('general.save') }}
</NcButton>
</div>
</a-form>
</div> </div>
</div> </div>
</div> </div>

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

@ -57,84 +57,97 @@ 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>
<a-form <template #icon>
ref="formValidator" <GeneralIcon icon="passwordChange" class="flex-none text-gray-700 text-[20px] h-5 w-5" />
data-testid="nc-user-settings-form" </template>
layout="vertical" <template #title>
class="change-password lg:max-w-3/4 w-full" <span data-rec="true">
no-style {{ $t('activity.changePwd') }}
:model="form" </span>
@finish="passwordChange" </template>
> </NcPageHeader>
<Transition name="layout"> <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 v-if="error" class="mx-auto mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1"> <div class="mx-auto relative flex flex-col justify-start gap-2 w-full md:(bg-white) max-w-[900px]">
<div data-testid="nc-user-settings-form__error" class="flex items-center gap-2 justify-center" data-rec="true"> <a-form
<MaterialSymbolsWarning /> ref="formValidator"
{{ error }} data-testid="nc-user-settings-form"
</div> layout="vertical"
</div> class="change-password lg:max-w-3/4 w-full"
</Transition> no-style
:model="form"
<a-form-item @finish="passwordChange"
:label="$t('placeholder.password.current')"
data-rec="true"
name="currentPassword"
:rules="formRules.currentPassword"
>
<a-input-password
v-model:value="form.currentPassword"
data-testid="nc-user-settings-form__current-password"
size="large"
class="password"
:placeholder="$t('placeholder.password.current')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.new')" data-rec="true" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
data-testid="nc-user-settings-form__new-password"
size="large"
class="password"
:placeholder="$t('placeholder.password.new')"
@focus="resetError"
/>
</a-form-item>
<a-form-item
:label="$t('placeholder.password.confirm')"
data-rec="true"
name="passwordRepeat"
:rules="formRules.passwordRepeat"
>
<a-input-password
v-model:value="form.passwordRepeat"
data-testid="nc-user-settings-form__new-password-repeat"
size="large"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@focus="resetError"
/>
</a-form-item>
<div class="text-right">
<a-button
size="middle"
data-testid="nc-user-settings-form__submit"
class="!rounded-md !h-[2.5rem]"
type="primary"
html-type="submit"
> >
<div class="flex justify-center items-center gap-2" data-rec="true"> <Transition name="layout">
<component :is="iconMap.passwordChange" /> <div v-if="error" class="mx-auto mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
{{ $t('activity.changePwd') }} <div data-testid="nc-user-settings-form__error" class="flex items-center gap-2 justify-center" data-rec="true">
<MaterialSymbolsWarning />
{{ error }}
</div>
</div>
</Transition>
<a-form-item
:label="$t('placeholder.password.current')"
data-rec="true"
name="currentPassword"
:rules="formRules.currentPassword"
>
<a-input-password
v-model:value="form.currentPassword"
data-testid="nc-user-settings-form__current-password"
size="large"
class="password"
:placeholder="$t('placeholder.password.current')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.new')" data-rec="true" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
data-testid="nc-user-settings-form__new-password"
size="large"
class="password"
:placeholder="$t('placeholder.password.new')"
@focus="resetError"
/>
</a-form-item>
<a-form-item
:label="$t('placeholder.password.confirm')"
data-rec="true"
name="passwordRepeat"
:rules="formRules.passwordRepeat"
>
<a-input-password
v-model:value="form.passwordRepeat"
data-testid="nc-user-settings-form__new-password-repeat"
size="large"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@focus="resetError"
/>
</a-form-item>
<div class="text-right">
<a-button
size="middle"
data-testid="nc-user-settings-form__submit"
class="!rounded-md !h-[2.5rem]"
type="primary"
html-type="submit"
>
<div class="flex justify-center items-center gap-2" data-rec="true">
<component :is="iconMap.passwordChange" />
{{ $t('activity.changePwd') }}
</div>
</a-button>
</div> </div>
</a-button> </a-form>
</div> </div>
</a-form> </div>
</div> </div>
</template> </template>

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

@ -29,21 +29,34 @@ 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>
<div class="flex items-center gap-2"> <template #icon>
<a-form-item> <GeneralIcon icon="settings" class="flex-none text-[20px] text-gray-700 h-5 w-5" />
<a-checkbox </template>
v-model:checked="settings.invite_only_signup" <template #title>
v-e="['c:account:enable-signup']" <span data-rec="true">
class="nc-checkbox nc-invite-only-signup-checkbox !mt-6" {{ $t('activity.settings') }}
name="virtual" </span>
@change="saveSettings" </template>
/> </NcPageHeader>
</a-form-item> <div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<span data-rec="true"> <div class="flex flex-col items-center">
{{ $t('labels.inviteOnlySignup') }} <div class="flex items-center gap-2">
</span> <a-form-item>
<a-checkbox
v-model:checked="settings.invite_only_signup"
v-e="['c:account:enable-signup']"
class="nc-checkbox nc-invite-only-signup-checkbox !mt-6"
name="virtual"
@change="saveSettings"
/>
</a-form-item>
<span data-rec="true">
{{ $t('labels.inviteOnlySignup') }}
</span>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>

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

@ -224,189 +224,212 @@ 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" />
<NcTooltip v-if="tokens.length" :disabled="!(isEeUI && tokens.length)"> </template>
<template #title>{{ $t('labels.tokenLimit') }}</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)">
<template #title>{{ $t('labels.tokenLimit') }}</template>
<NcButton
:disabled="showNewTokenModal || (isEeUI && tokens.length)"
class="!rounded-md"
data-testid="nc-token-create"
size="middle"
type="primary"
tooltip="bottom"
@click="showNewTokenModal = true"
>
<span class="hidden md:block" data-rec="true">
{{ $t('title.addNewToken') }}
</span>
<span class="flex items-center justify-center md:hidden" data-rec="true">
<component :is="iconMap.plus" />
</span>
</NcButton>
</NcTooltip>
</div>
<span data-rec="true">{{ $t('msg.apiTokenCreate') }}</span>
<div v-if="!isLoadingAllTokens && (tokens.length || showNewTokenModal)" class="mt-6 h-full max-h-[calc(100%-80px)]">
<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">
<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">{{
$t('title.creator')
}}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-3/9 text-start" data-rec="true">{{
$t('labels.token')
}}</span>
<span class="py-3.5 pl-19 text-gray-500 font-medium text-3.5 w-2/9 text-start" data-rec="true">{{
$t('labels.actions')
}}</span>
</div>
<div class="nc-scrollbar-md !overflow-y-auto flex flex-col h-[calc(100%-52px)]">
<div v-if="showNewTokenModal">
<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="{
'rounded-b-md': !tokens.length,
}"
>
<div class="flex w-full">
<a-input
:ref="selectInputOnMount"
v-model:value="selectedTokenData.description"
:default-value="defaultTokenName"
type="text"
class="!rounded-lg !py-1"
placeholder="Token Name"
data-testid="nc-token-input"
:disabled="isLoading"
@press-enter="generateToken"
/>
<span v-if="!isValidTokenName" class="text-red-500 text-xs font-light mt-1.5 ml-1" data-rec="true"
>{{ errorMessage }}
</span>
</div>
<div class="flex gap-2 justify-start">
<NcButton v-if="!isLoading" type="secondary" size="small" @click="handleCancel">
{{ $t('general.cancel') }}
</NcButton>
<NcButton
type="primary"
size="sm"
:loading="isLoading"
data-testid="nc-token-save-btn"
@click="generateToken"
>
{{ $t('general.save') }}
</NcButton>
</div>
</div>
</div>
<div
v-if="!tokens.length && !showNewTokenModal"
class="border-l-1 border-r-1 border-b-1 rounded-b-md justify-center flex items-center"
>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noToken')" />
</div>
<div
v-for="el of tokens"
:key="el.id"
data-testid="nc-token-list"
class="flex pl-5 py-3 justify-between token items-center border-l-1 border-r-1 border-b-1"
>
<span class="text-black font-bold text-3.5 text-start w-2/9">
<GeneralTruncateText placement="top" :length="20">
{{ el.description }}
</GeneralTruncateText>
</span>
<span class="text-gray-500 font-medium text-3.5 text-start w-2/9">
<GeneralTruncateText placement="top" :length="20">
{{ el.created_by }}
</GeneralTruncateText>
</span>
<span class="text-gray-500 font-medium text-3.5 text-start w-3/9">
<GeneralTruncateText v-if="el.token === selectedToken.id && selectedToken.isShow" placement="top" :length="29">
{{ el.token }}
</GeneralTruncateText>
<span v-else>************************************</span>
</span>
<!-- ACTIONS -->
<div class="flex justify-end items-center gap-3 pr-5 text-gray-500 font-medium text-3.5 w-2/9">
<NcTooltip placement="top">
<template #title>{{ $t('labels.showOrHide') }}</template>
<component
:is="iconMap.eye"
class="nc-toggle-token-visibility hover::cursor-pointer w-h-4 mb-[1.8px]"
@click="hideOrShowToken(el.token as string)"
/>
</NcTooltip>
<NcTooltip placement="top">
<template #title>{{ $t('general.copy') }}</template>
<component
:is="iconMap.copy"
class="hover::cursor-pointer w-4 h-4 text-gray-600"
@click="copyToken(el.token)"
/>
</NcTooltip>
<NcTooltip placement="top">
<template #title>{{ $t('general.delete') }}</template>
<component
:is="iconMap.delete"
data-testid="nc-token-row-action-icon"
class="nc-delete-icon hover::cursor-pointer w-4 h-4"
@click="triggerDeleteModal(el.token as string, el.description as string)"
/>
</NcTooltip>
</div>
</div>
</div>
</div>
</div>
<div
v-else-if="!isLoadingAllTokens && !tokens.length && !showNewTokenModal"
class="max-w-[40rem] border px-3 py-6 flex flex-col items-center justify-center gap-6 text-center"
>
<img src="~assets/img/placeholder/api-tokens.png" class="!w-[22rem] flex-none" />
<div class="text-2xl text-gray-800 font-bold">{{ $t('placeholder.noTokenCreated') }}</div>
<div class="text-sm text-gray-700">
{{ $t('placeholder.noTokenCreatedLabel') }}
</div>
<NcButton <NcButton
:disabled="showNewTokenModal || (isEeUI && tokens.length)" class="!rounded-lg !py-3 !h-10"
class="!rounded-md"
data-testid="nc-token-create" data-testid="nc-token-create"
size="middle"
type="primary" type="primary"
tooltip="bottom"
@click="showNewTokenModal = true" @click="showNewTokenModal = true"
> >
<span class="hidden md:block" data-rec="true"> <span class="hidden md:block" data-rec="true">
{{ $t('title.addNewToken') }} {{ $t('title.createNewToken') }}
</span> </span>
<span class="flex items-center justify-center md:hidden" data-rec="true"> <span class="flex items-center justify-center md:hidden" data-rec="true">
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
</span> </span>
</NcButton> </NcButton>
</NcTooltip> </div>
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-5">
<a-pagination
v-model:current="currentPage"
:total="pagination.total"
show-less-items
@change="loadTokens(currentPage, currentLimit)"
/>
</div>
</div> </div>
<span data-rec="true">{{ $t('msg.apiTokenCreate') }}</span>
<div v-if="!isLoadingAllTokens && (tokens.length || showNewTokenModal)" class="mt-5 h-[calc(100%-13rem)]"> <GeneralDeleteModal
<div class="h-full w-full !overflow-hidden rounded-md"> v-model:visible="isModalOpen"
<div class="flex w-full pl-5 bg-gray-50 border-1 rounded-t-md"> :entity-name="$t('labels.token')"
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9" data-rec="true">{{ $t('title.tokenName') }}</span> :on-delete="() => deleteToken(tokenToCopy)"
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9 text-start" data-rec="true">{{ >
$t('title.creator') <template #entity-preview>
}}</span> <span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-3/9 text-start" data-rec="true">{{ <div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
$t('labels.token') <GeneralIcon icon="key" class="nc-view-icon"></GeneralIcon>
}}</span>
<span class="py-3.5 pl-19 text-gray-500 font-medium text-3.5 w-2/9 text-start" data-rec="true">{{
$t('labels.actions')
}}</span>
</div>
<div class="nc-scrollbar-md !overflow-y-auto flex flex-col h-[calc(100%-5rem)]">
<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="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:class="{ :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
'rounded-b-md': !tokens.length,
}"
> >
<div class="flex w-full"> {{ tokenDesc }}
<a-input
:ref="selectInputOnMount"
v-model:value="selectedTokenData.description"
:default-value="defaultTokenName"
type="text"
class="!rounded-lg !py-1"
placeholder="Token Name"
data-testid="nc-token-input"
:disabled="isLoading"
@press-enter="generateToken"
/>
<span v-if="!isValidTokenName" class="text-red-500 text-xs font-light mt-1.5 ml-1" data-rec="true"
>{{ errorMessage }}
</span>
</div>
<div class="flex gap-2 justify-start">
<NcButton v-if="!isLoading" type="secondary" size="small" @click="handleCancel">
{{ $t('general.cancel') }}
</NcButton>
<NcButton type="primary" size="sm" :loading="isLoading" data-testid="nc-token-save-btn" @click="generateToken">
{{ $t('general.save') }}
</NcButton>
</div>
</div>
</div>
<div
v-if="!tokens.length && !showNewTokenModal"
class="border-l-1 border-r-1 border-b-1 rounded-b-md justify-center flex items-center"
>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noToken')" />
</div>
<div
v-for="el of tokens"
:key="el.id"
data-testid="nc-token-list"
class="flex pl-5 py-3 justify-between token items-center border-l-1 border-r-1 border-b-1"
>
<span class="text-black font-bold text-3.5 text-start w-2/9">
<GeneralTruncateText placement="top" :length="20">
{{ el.description }}
</GeneralTruncateText>
</span>
<span class="text-gray-500 font-medium text-3.5 text-start w-2/9">
<GeneralTruncateText placement="top" :length="20">
{{ el.created_by }}
</GeneralTruncateText>
</span>
<span class="text-gray-500 font-medium text-3.5 text-start w-3/9">
<GeneralTruncateText v-if="el.token === selectedToken.id && selectedToken.isShow" placement="top" :length="29">
{{ el.token }}
</GeneralTruncateText>
<span v-else>************************************</span>
</span>
<!-- ACTIONS -->
<div class="flex justify-end items-center gap-3 pr-5 text-gray-500 font-medium text-3.5 w-2/9">
<NcTooltip placement="top">
<template #title>{{ $t('labels.showOrHide') }}</template>
<component
:is="iconMap.eye"
class="nc-toggle-token-visibility hover::cursor-pointer w-h-4 mb-[1.8px]"
@click="hideOrShowToken(el.token as string)"
/>
</NcTooltip>
<NcTooltip placement="top">
<template #title>{{ $t('general.copy') }}</template>
<component
:is="iconMap.copy"
class="hover::cursor-pointer w-4 h-4 text-gray-600"
@click="copyToken(el.token)"
/>
</NcTooltip>
<NcTooltip placement="top">
<template #title>{{ $t('general.delete') }}</template>
<component
:is="iconMap.delete"
data-testid="nc-token-row-action-icon"
class="nc-delete-icon hover::cursor-pointer w-4 h-4"
@click="triggerDeleteModal(el.token as string, el.description as string)"
/>
</NcTooltip>
</div> </div>
</div> </div>
</div>
</div>
</div>
<div
v-else-if="!isLoadingAllTokens && !tokens.length && !showNewTokenModal"
class="max-w-[40rem] border px-3 py-6 flex flex-col items-center justify-center gap-6 text-center"
>
<img src="~assets/img/placeholder/api-tokens.png" class="!w-[22rem] flex-none" />
<div class="text-2xl text-gray-800 font-bold">{{ $t('placeholder.noTokenCreated') }}</div>
<div class="text-sm text-gray-700">
{{ $t('placeholder.noTokenCreatedLabel') }}
</div>
<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">
{{ $t('title.createNewToken') }}
</span>
<span class="flex items-center justify-center md:hidden" data-rec="true">
<component :is="iconMap.plus" />
</span> </span>
</NcButton> </template>
</div> </GeneralDeleteModal>
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-5">
<a-pagination
v-model:current="currentPage"
:total="pagination.total"
show-less-items
@change="loadTokens(currentPage, currentLimit)"
/>
</div>
</div> </div>
<GeneralDeleteModal
v-model:visible="isModalOpen"
:entity-name="$t('labels.token')"
:on-delete="() => deleteToken(tokenToCopy)"
>
<template #entity-preview>
<span>
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralIcon icon="key" class="nc-view-icon"></GeneralIcon>
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ tokenDesc }}
</div>
</div>
</span>
</template>
</GeneralDeleteModal>
</div> </div>
</template> </template>

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

@ -211,198 +211,211 @@ 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">
<div class="max-w-195 mx-auto h-full"> <NcPageHeader>
<div class="text-2xl text-left font-weight-bold mb-4" data-rec="true">{{ $t('title.userMgmt') }}</div> <template #icon>
<div class="py-2 flex gap-4 items-center justify-between"> <GeneralIcon icon="users" class="flex-none text-gray-700 h-5 w-5" />
<a-input </template>
v-model:value="searchText" <template #title>
class="!max-w-90 !rounded-md" <span data-rec="true">
:placeholder="$t('title.searchMembers')" {{ $t('title.userManagement') }}
@change="loadUsers()" </span>
> </template>
<template #prefix> </NcPageHeader>
<PhMagnifyingGlassBold class="!h-3.5 text-gray-500" /> <div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
</template> <div class="h-full">
</a-input> <div class="max-w-195 mx-auto h-full">
<div class="flex gap-3 items-center justify-center"> <div class="flex gap-4 items-center justify-between">
<component :is="iconMap.reload" class="cursor-pointer" @click="loadUsers(currentPage, currentLimit)" /> <a-input
<NcButton data-testid="nc-super-user-invite" size="small" type="primary" @click="openInviteModal"> v-model:value="searchText"
<div class="flex items-center gap-1" data-rec="true"> class="!max-w-90 !rounded-md"
<component :is="iconMap.plus" /> :placeholder="$t('title.searchMembers')"
{{ $t('activity.inviteUser') }} @change="loadUsers()"
</div>
</NcButton>
</div>
</div>
<NcTable
v-model:order-by="orderBy"
:columns="columns"
:data="sortedUsers"
:is-data-loading="isLoading"
class="h-[calc(100%-140px)] max-w-250 mt-4"
>
<template #bodyCell="{ column, record: el }">
<div v-if="column.key === 'email'" class="w-full">
<NcTooltip v-if="el.display_name" class="truncate max-w-full">
<template #title>
{{ el.email }}
</template>
{{ el.display_name }}
</NcTooltip>
<NcTooltip v-else class="truncate max-w-full" show-on-truncate-only>
<template #title>
{{ el.email }}
</template>
{{ el.email }}
</NcTooltip>
</div>
<template v-if="column.key === 'role'">
<div v-if="el?.roles?.includes('super')" class="font-weight-bold" data-rec="true">
{{ $t('labels.superAdmin') }}
</div>
<NcSelect
v-else-if="el.id !== loggedInUser?.id"
v-model:value="el.roles"
class="w-55 nc-user-roles"
:dropdown-match-select-width="false"
dropdown-class-name="max-w-64"
@change="updateRole(el.id, el.roles as string)"
> >
<a-select-option <template #prefix>
class="nc-users-list-role-option" <PhMagnifyingGlassBold class="!h-3.5 text-gray-500" />
:value="OrgUserRoles.CREATOR" </template>
:label="$t(`objects.roleType.orgLevelCreator`)" </a-input>
> <div class="flex gap-3 items-center justify-center">
<div class="w-full"> <component :is="iconMap.reload" class="cursor-pointer" @click="loadUsers(currentPage, currentLimit)" />
<div class="flex items-center gap-1 justify-between"> <NcButton data-testid="nc-super-user-invite" size="small" type="primary" @click="openInviteModal">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelCreator`) }}</div> <div class="flex items-center gap-1" data-rec="true">
<GeneralIcon <component :is="iconMap.plus" />
v-if="el?.roles === OrgUserRoles.CREATOR" {{ $t('activity.inviteUser') }}
id="nc-selected-item-icon"
icon="check"
class="flex-none w-4 h-4 text-primary"
/>
</div>
<div class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgCreator') }}
</div>
</div>
</a-select-option>
<a-select-option
class="nc-users-list-role-option"
:value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)"
>
<div class="w-full">
<div class="flex items-center gap-1 justify-between">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<GeneralIcon
v-if="el.roles === OrgUserRoles.VIEWER"
id="nc-selected-item-icon"
icon="check"
class="flex-none w-4 h-4 text-primary"
/>
</div>
<div class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgViewer') }}
</div>
</div> </div>
</a-select-option> </NcButton>
</NcSelect>
<div v-else class="font-weight-bold" data-rec="true">
{{ $t(`objects.roleType.orgLevelCreator`) }}
</div> </div>
</template> </div>
<div <NcTable
v-if="column.key === 'action'" v-model:order-by="orderBy"
class="flex items-center gap-2" :columns="columns"
:class="{ :data="sortedUsers"
'opacity-0 pointer-events-none': el.roles?.includes('super'), :is-data-loading="isLoading"
}" class="h-[calc(100%-58px)] max-w-250 mt-6"
> >
<NcDropdown :trigger="['click']"> <template #bodyCell="{ column, record: el }">
<NcButton size="xsmall" type="ghost"> <div v-if="column.key === 'email'" class="w-full">
<MdiDotsVertical <NcTooltip v-if="el.display_name" class="truncate max-w-full">
class="text-gray-600 h-5.5 w-5.5 rounded outline-0 p-0.5 nc-workspace-menu transform transition-transform !text-gray-400 cursor-pointer hover:(!text-gray-500 bg-gray-100)" <template #title>
/> {{ el.email }}
</NcButton>
<template #overlay>
<NcMenu>
<template v-if="!el.roles?.includes('super')">
<!-- Resend invite Email -->
<NcMenuItem @click="resendInvite(el)">
<component :is="iconMap.email" class="flex text-gray-600" />
<div data-rec="true">{{ $t('activity.resendInvite') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyInviteUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-600" />
<div data-rec="true">{{ $t('activity.copyInviteURL') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyPasswordResetUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-600" />
<div>{{ $t('activity.copyPasswordResetURL') }}</div>
</NcMenuItem>
</template> </template>
<template v-if="el.id !== loggedInUser?.id"> {{ el.display_name }}
<NcDivider v-if="!el.roles?.includes('super')" /> </NcTooltip>
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="openDeleteModal(el)">
<MaterialSymbolsDeleteOutlineRounded /> <NcTooltip v-else class="truncate max-w-full" show-on-truncate-only>
{{ $t('general.remove') }} {{ $t('objects.user') }} <template #title>
</NcMenuItem> {{ el.email }}
</template> </template>
</NcMenu> {{ el.email }}
</NcTooltip>
</div>
<template v-if="column.key === 'role'">
<div v-if="el?.roles?.includes('super')" class="font-weight-bold" data-rec="true">
{{ $t('labels.superAdmin') }}
</div>
<NcSelect
v-else-if="el.id !== loggedInUser?.id"
v-model:value="el.roles"
class="w-55 nc-user-roles"
:dropdown-match-select-width="false"
dropdown-class-name="max-w-64"
@change="updateRole(el.id, el.roles as string)"
>
<a-select-option
class="nc-users-list-role-option"
:value="OrgUserRoles.CREATOR"
:label="$t(`objects.roleType.orgLevelCreator`)"
>
<div class="w-full">
<div class="flex items-center gap-1 justify-between">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<GeneralIcon
v-if="el?.roles === OrgUserRoles.CREATOR"
id="nc-selected-item-icon"
icon="check"
class="flex-none w-4 h-4 text-primary"
/>
</div>
<div class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgCreator') }}
</div>
</div>
</a-select-option>
<a-select-option
class="nc-users-list-role-option"
:value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)"
>
<div class="w-full">
<div class="flex items-center gap-1 justify-between">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<GeneralIcon
v-if="el.roles === OrgUserRoles.VIEWER"
id="nc-selected-item-icon"
icon="check"
class="flex-none w-4 h-4 text-primary"
/>
</div>
<div class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgViewer') }}
</div>
</div>
</a-select-option>
</NcSelect>
<div v-else class="font-weight-bold" data-rec="true">
{{ $t(`objects.roleType.orgLevelCreator`) }}
</div>
</template> </template>
</NcDropdown>
</div>
</template>
<template #extraRow>
<div
v-if="pagination.total === 1 && sortedUsers.length === 1"
class="w-full pt-12 pb-4 px-2 flex flex-col items-center gap-6 text-center"
>
<div class="text-2xl text-gray-800 font-bold">
{{ $t('placeholder.inviteYourTeam') }}
</div>
<div class="text-sm text-gray-700">
{{ $t('placeholder.inviteYourTeamLabel') }}
</div>
<img src="~assets/img/placeholder/invite-team.png" class="!w-[30rem] flex-none" />
</div>
</template>
<template #tableFooter>
<div v-if="pagination.total > 10" class="px-4 py-2 flex items-center justify-center">
<a-pagination
v-model:current="currentPage"
:total="pagination.total"
show-less-items
@change="loadUsers(currentPage, currentLimit)"
/>
</div>
</template>
</NcTable>
<GeneralDeleteModal v-model:visible="isOpen" entity-name="User" :on-delete="() => deleteUser()">
<template #entity-preview>
<span>
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralIcon icon="account" class="nc-view-icon"></GeneralIcon>
<div <div
class="text-ellipsis overflow-hidden select-none w-full pl-1.75" v-if="column.key === 'action'"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" class="flex items-center gap-2"
:class="{
'opacity-0 pointer-events-none': el.roles?.includes('super'),
}"
>
<NcDropdown :trigger="['click']">
<NcButton size="xsmall" type="ghost">
<MdiDotsVertical
class="text-gray-600 h-5.5 w-5.5 rounded outline-0 p-0.5 nc-workspace-menu transform transition-transform !text-gray-400 cursor-pointer hover:(!text-gray-500 bg-gray-100)"
/>
</NcButton>
<template #overlay>
<NcMenu>
<template v-if="!el.roles?.includes('super')">
<!-- Resend invite Email -->
<NcMenuItem @click="resendInvite(el)">
<component :is="iconMap.email" class="flex text-gray-600" />
<div data-rec="true">{{ $t('activity.resendInvite') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyInviteUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-600" />
<div data-rec="true">{{ $t('activity.copyInviteURL') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyPasswordResetUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-600" />
<div>{{ $t('activity.copyPasswordResetURL') }}</div>
</NcMenuItem>
</template>
<template v-if="el.id !== loggedInUser?.id">
<NcDivider v-if="!el.roles?.includes('super')" />
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="openDeleteModal(el)">
<MaterialSymbolsDeleteOutlineRounded />
{{ $t('general.remove') }} {{ $t('objects.user') }}
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>
</div>
</template>
<template #extraRow>
<div
v-if="pagination.total === 1 && sortedUsers.length === 1"
class="w-full pt-12 pb-4 px-2 flex flex-col items-center gap-6 text-center"
> >
{{ deleteModalInfo?.email }} <div class="text-2xl text-gray-800 font-bold">
{{ $t('placeholder.inviteYourTeam') }}
</div>
<div class="text-sm text-gray-700">
{{ $t('placeholder.inviteYourTeamLabel') }}
</div>
<img src="~assets/img/placeholder/invite-team.png" class="!w-[30rem] flex-none" />
</div> </div>
</div> </template>
</span>
</template> <template #tableFooter>
</GeneralDeleteModal> <div v-if="pagination.total > 10" class="px-4 py-2 flex items-center justify-center">
<a-pagination
v-model:current="currentPage"
:total="pagination.total"
show-less-items
@change="loadUsers(currentPage, currentLimit)"
/>
</div>
</template>
</NcTable>
<GeneralDeleteModal v-model:visible="isOpen" entity-name="User" :on-delete="() => deleteUser()">
<template #entity-preview>
<span>
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralIcon icon="account" class="nc-view-icon"></GeneralIcon>
<div
class="text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ deleteModalInfo?.email }}
</div>
</div>
</span>
</template>
</GeneralDeleteModal>
<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>

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>

222
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,129 +255,149 @@ 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">
<GeneralBaseIconColorPicker readonly />
<span class="text-base">
{{ currentBase?.title }} {{ currentBase?.title }}
</span> </div>
</div> </div>
</div> <NcPageHeader>
<template #icon>
<div v-else class="w-full flex justify-between items-center max-w-350 gap-3"> <div class="nc-page-header-icon flex justify-center items-center h-5 w-5">
<a-input <GeneralBaseIconColorPicker readonly />
v-model:value="userSearchText" </div>
:placeholder="$t('title.searchMembers')"
:disabled="isLoading"
allow-clear
class="nc-input-border-on-value !max-w-90 !h-8 !px-3 !py-1 !rounded-lg"
>
<template #prefix>
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500 group-hover:text-black" />
</template> </template>
</a-input> <template #title>
<span data-rec="true" class="capitalize">
<NcButton :disabled="isLoading" size="small" @click="isInviteModalVisible = true"> {{ currentBase?.title }}
<div class="flex items-center gap-1"> </span>
<component :is="iconMap.plus" class="w-4 h-4" /> </template>
{{ $t('activity.addMembers') }} </NcPageHeader>
</div>
</NcButton>
</div> </div>
<NcTable <div
v-model:order-by="orderBy" class="h-full flex flex-col items-center gap-6 px-6 pt-6"
:is-data-loading="isLoading" :class="{
:columns="columns" 'border-t-1 border-gray-200': isAdminPanel,
:data="sortedCollaborators" }"
:bordered="false"
:custom-row="customRow"
class="flex-1 nc-collaborators-list max-w-350"
> >
<template #emptyText> <div v-if="!isAdminPanel" class="w-full flex justify-between items-center max-w-350 gap-3">
<a-empty :description="$t('title.noMembersFound')" /> <a-input
</template> v-model:value="userSearchText"
:placeholder="$t('title.searchMembers')"
:disabled="isLoading"
allow-clear
class="nc-input-border-on-value !max-w-90 !h-8 !px-3 !py-1 !rounded-lg"
>
<template #prefix>
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500 group-hover:text-black" />
</template>
</a-input>
<template #headerCell="{ column }"> <NcButton :disabled="isLoading" size="small" @click="isInviteModalVisible = true">
<template v-if="column.key === 'select'"> <div class="flex items-center gap-1">
<NcCheckbox v-model:checked="selectAll" :disabled="!sortedCollaborators.length" /> <component :is="iconMap.plus" class="w-4 h-4" />
</template> {{ $t('activity.addMembers') }}
<template v-else> </div>
{{ column.title }} </NcButton>
</div>
<NcTable
v-model:order-by="orderBy"
:is-data-loading="isLoading"
:columns="columns"
:data="sortedCollaborators"
:bordered="false"
:custom-row="customRow"
class="flex-1 nc-collaborators-list max-w-350"
>
<template #emptyText>
<a-empty :description="$t('title.noMembersFound')" />
</template> </template>
</template>
<template #bodyCell="{ column, record }"> <template #headerCell="{ column }">
<template v-if="column.key === 'select'"> <template v-if="column.key === 'select'">
<NcCheckbox v-model:checked="selected[record.id]" /> <NcCheckbox v-model:checked="selectAll" :disabled="!sortedCollaborators.length" />
</template>
<template v-else>
{{ column.title }}
</template>
</template> </template>
<div v-if="column.key === 'email'" class="w-full flex gap-3 items-center users-email-grid"> <template #bodyCell="{ column, record }">
<GeneralUserIcon size="base" :email="record.email" class="flex-none" /> <template v-if="column.key === 'select'">
<div class="flex flex-col flex-1 max-w-[calc(100%_-_44px)]"> <NcCheckbox v-model:checked="selected[record.id]" />
<div class="flex gap-3"> </template>
<NcTooltip class="truncate max-w-full text-gray-800 capitalize font-semibold" show-on-truncate-only>
<template #title> <div v-if="column.key === 'email'" class="w-full flex gap-3 items-center users-email-grid">
<GeneralUserIcon size="base" :email="record.email" class="flex-none" />
<div class="flex flex-col flex-1 max-w-[calc(100%_-_44px)]">
<div class="flex gap-3">
<NcTooltip class="truncate max-w-full text-gray-800 capitalize font-semibold" show-on-truncate-only>
<template #title>
{{ record.display_name || record.email.slice(0, record.email.indexOf('@')) }}
</template>
{{ record.display_name || record.email.slice(0, record.email.indexOf('@')) }} {{ record.display_name || record.email.slice(0, record.email.indexOf('@')) }}
</NcTooltip>
</div>
<NcTooltip class="truncate max-w-full text-xs text-gray-600" show-on-truncate-only>
<template #title>
{{ record.email }}
</template> </template>
{{ record.display_name || record.email.slice(0, record.email.indexOf('@')) }} {{ record.email }}
</NcTooltip> </NcTooltip>
</div> </div>
<NcTooltip class="truncate max-w-full text-xs text-gray-600" show-on-truncate-only> </div>
<div v-if="column.key === 'role'">
<template v-if="accessibleRoles.includes(record.roles)">
<RolesSelector
:role="record.roles"
:roles="accessibleRoles"
:inherit="
isEeUI && record.workspace_roles && WorkspaceRolesToProjectRoles[record.workspace_roles]
? WorkspaceRolesToProjectRoles[record.workspace_roles]
: null
"
show-inherit
:description="false"
:on-role-change="(role) => updateCollaborator(record, role as ProjectRoles)"
/>
</template>
<template v-else>
<RolesBadge :border="false" :role="record.roles" />
</template>
</div>
<div v-if="column.key === 'created_at'">
<NcTooltip class="max-w-full">
<template #title> <template #title>
{{ record.email }} {{ parseStringDateTime(record.created_at) }}
</template> </template>
{{ record.email }} <span>
{{ timeAgo(record.created_at) }}
</span>
</NcTooltip> </NcTooltip>
</div> </div>
</div> </template>
<div v-if="column.key === 'role'"> </NcTable>
<template v-if="accessibleRoles.includes(record.roles)"> </div>
<RolesSelector
:role="record.roles"
:roles="accessibleRoles"
:inherit="
isEeUI && record.workspace_roles && WorkspaceRolesToProjectRoles[record.workspace_roles]
? WorkspaceRolesToProjectRoles[record.workspace_roles]
: null
"
show-inherit
:description="false"
:on-role-change="(role) => updateCollaborator(record, role as ProjectRoles)"
/>
</template>
<template v-else>
<RolesBadge :border="false" :role="record.roles" />
</template>
</div>
<div v-if="column.key === 'created_at'">
<NcTooltip class="max-w-full">
<template #title>
{{ parseStringDateTime(record.created_at) }}
</template>
<span>
{{ timeAgo(record.created_at) }}
</span>
</NcTooltip>
</div>
</template>
</NcTable>
<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" />

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

@ -15,52 +15,70 @@ 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>
<GeneralOpenLeftSidebarBtn /> <div
<LazySmartsheetToolbarViewInfo v-if="!isPublic" /> class="flex items-center gap-3 min-w-[300px]"
:style="{
width: topbarBreadcrumbItemWidth,
}"
>
<GeneralOpenLeftSidebarBtn />
<LazySmartsheetToolbarViewInfo v-if="!isPublic" />
</div>
<div v-if="!isSharedBase && !isMobileMode"> <div v-if="!isSharedBase && !isMobileMode">
<SmartsheetTopbarSelectMode /> <SmartsheetTopbarSelectMode />
</div> </div>
<div class="flex-1" />
<GeneralApiLoader v-if="!isMobileMode" /> <div class="flex items-center justify-end gap-3 flex-1">
<GeneralApiLoader v-if="!isMobileMode" />
<div <div
v-if="extensionsEgg" v-if="extensionsEgg"
class="flex items-center px-2 py-1 border-1 rounded-lg h-8 xs:(h-10 ml-0) ml-1 border-gray-200 cursor-pointer font-weight-600 text-sm select-none" class="flex items-center px-2 py-1 border-1 rounded-lg h-8 xs:(h-10 ml-0) ml-1 border-gray-200 cursor-pointer font-weight-600 text-sm select-none"
:class="{ 'bg-brand-50 text-brand-500': isPanelExpanded }" :class="{ 'bg-brand-50 text-brand-500': isPanelExpanded }"
@click="toggleExtensionPanel" @click="toggleExtensionPanel"
>
<GeneralIcon icon="puzzle" class="w-4 h-4" :class="{ 'border-l-1 border-transparent': isPanelExpanded }" />
<span
class="overflow-hidden trasition-all duration-200"
:class="{ 'w-[0px] invisible': isPanelExpanded, 'ml-2 w-[74px]': !isPanelExpanded }"
> >
Extensions <GeneralIcon icon="puzzle" class="w-4 h-4" :class="{ 'border-l-1 border-transparent': isPanelExpanded }" />
</span> <span
</div> class="overflow-hidden trasition-all duration-200"
<div v-else-if="!extensionsEgg" class="w-[15px] h-[15px] cursor-pointer" @dblclick="onEggClick" /> :class="{ 'w-[0px] invisible': isPanelExpanded, 'ml-2 w-[74px]': !isPanelExpanded }"
>
Extensions
</span>
</div>
<div v-else-if="!extensionsEgg" class="w-[15px] h-[15px] cursor-pointer" @dblclick="onEggClick" />
<LazyGeneralShareProject <LazyGeneralShareProject
v-if="(isForm || isGrid || isKanban || isGallery || isMap || isCalendar) && !isPublic && !isMobileMode" v-if="(isForm || isGrid || isKanban || isGallery || isMap || isCalendar) && !isPublic && !isMobileMode"
is-view-toolbar is-view-toolbar
/> />
<LazyGeneralLanguage <LazyGeneralLanguage
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">

37
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, <div class="flex items-center gap-0.5">
'text-gray-800 font-medium': !activeView?.is_default, <GeneralIcon icon="threeDotVertical" class="!h-4 !w-4" />
}" </div>
> </NcButton>
<NcTooltip class="truncate xs:pl-1.25 flex-1 text-inherit" show-on-truncate-only>
<template #title>{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }} </template>
<span
:class="{
'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>

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

@ -174,17 +174,8 @@ const onDelete = async () => {
<template v-if="!view?.is_default && isUIAllowed('viewCreateOrEdit')"> <template v-if="!view?.is_default && isUIAllowed('viewCreateOrEdit')">
<NcDivider /> <NcDivider />
<NcMenuItem v-if="lockType !== LockType.Locked" @click="onRenameMenuClick"> <template v-if="inSidebar">
<GeneralIcon icon="rename" /> <NcMenuItem v-if="lockType !== LockType.Locked" @click="onRenameMenuClick">
{{
$t('general.renameEntity', {
entity: view.type !== ViewTypes.FORM ? $t('objects.view').toLowerCase() : $t('objects.viewType.form').toLowerCase(),
})
}}
</NcMenuItem>
<NcTooltip v-else>
<template #title> {{ $t('msg.info.disabledAsViewLocked') }} </template>
<NcMenuItem class="!cursor-not-allowed !text-gray-400">
<GeneralIcon icon="rename" /> <GeneralIcon icon="rename" />
{{ {{
$t('general.renameEntity', { $t('general.renameEntity', {
@ -192,7 +183,19 @@ const onDelete = async () => {
}) })
}} }}
</NcMenuItem> </NcMenuItem>
</NcTooltip> <NcTooltip v-else>
<template #title> {{ $t('msg.info.disabledAsViewLocked') }} </template>
<NcMenuItem class="!cursor-not-allowed !text-gray-400">
<GeneralIcon icon="rename" />
{{
$t('general.renameEntity', {
entity:
view.type !== ViewTypes.FORM ? $t('objects.view').toLowerCase() : $t('objects.viewType.form').toLowerCase(),
})
}}
</NcMenuItem>
</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>

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

@ -1,137 +1,180 @@
<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 }">
:class="{ <div
'!max-w-none': isSharedBase && !isMobileMode, 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"
'!text-gray-500': activeTable,
'!text-gray-700': !activeTable,
}"
:to="openedBaseUrl"
>
<NcTooltip class="!text-inherit">
<template #title>
<span class="capitalize">
{{ base?.title }}
</span>
</template>
<div class="flex flex-row items-center gap-x-1.5">
<GeneralProjectIcon
:type="base?.type"
class="!grayscale min-w-4"
:style="{
filter: 'grayscale(100%) brightness(115%)',
}"
/>
<div
class="hidden !2xl:(flex truncate ml-1)"
:class="{
'!flex': isSharedBase && !isMobileMode,
}"
>
<span class="truncate !text-inherit capitalize">
{{ base?.title }}
</span>
</div>
</div>
</NcTooltip>
</NuxtLink>
<div class="px-1.75 text-gray-500">></div>
</template>
<template v-if="!(isMobileMode && !activeView?.is_default)">
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeTable?.meta?.icon" readonly size="xsmall">
<template #default>
<MdiTable
class="min-w-5"
:class="{ :class="{
'!text-gray-500': !isMobileMode, '!max-w-none': isSharedBase && !isMobileMode,
'!text-gray-700': isMobileMode, '': !isMobileMode && isLeftSidebarOpen,
}" }"
/> >
<NcTooltip :disabled="isSharedBase || isOpen">
<template #title>
<span class="capitalize">
{{ base?.title }}
</span>
</template>
<GeneralProjectIcon
:type="base?.type"
class="!grayscale min-w-4"
:style="{
filter: 'grayscale(100%) brightness(115%)',
}"
/>
</NcTooltip>
<template v-if="isSharedBase">
<NcTooltip
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',
}"
>
{{ base?.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 }"
/>
</template>
</div>
</template> </template>
</LazyGeneralEmojiPicker> </SmartsheetTopbarProjectListDropdown>
<div <GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
v-if="activeTable" </template>
:class="{ <template v-if="!(isMobileMode && !activeView?.is_default)">
'max-w-1/2': isMobileMode || activeView?.is_default, <SmartsheetTopbarTableListDropdown v-if="activeTable">
'max-w-20/100': !isSharedBase && !isMobileMode && !activeView?.is_default, <template #default="{ isOpen }">
'max-w-none': isSharedBase && !isMobileMode, <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"
>
<NcTooltip class="truncate nc-active-table-title max-w-full" show-on-truncate-only>
<template #title>
{{ activeTable?.title }}
</template>
<span
class="text-ellipsis overflow-hidden text-gray-500 xs:ml-2"
:class="{ :class="{
'text-gray-500': !isMobileMode, 'max-w-full': isMobileMode,
'text-gray-800 font-medium': isMobileMode || activeView?.is_default, 'max-w-1/2': activeView?.is_default,
}" 'max-w-1/4': !isSharedBase && !isMobileMode && !activeView?.is_default,
:style="{ 'max-w-none': isSharedBase && !isMobileMode,
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}" }"
> >
<template v-if="activeView?.is_default"> <LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeTable?.meta?.icon" readonly size="xsmall" class="mr-1">
{{ activeTable?.title }} <template #default>
</template> <GeneralIcon
<NuxtLink icon="table"
v-else class="min-w-5"
class="!text-inherit !underline-transparent !hover:(text-black underline-gray-600)" :class="{
:to="tableUrl({ table: activeTable, completeUrl: true, isSharedBase })" '!text-gray-500': !isMobileMode,
> '!text-gray-700': isMobileMode,
{{ activeTable?.title }} }"
</NuxtLink> />
</span> </template>
</NcTooltip> </LazyGeneralEmojiPicker>
</div>
<NcTooltip class="truncate nc-active-table-title max-w-full !leading-5" show-on-truncate-only :disabled="isOpen">
<template #title>
{{ activeTable?.title }}
</template>
<span
class="text-ellipsis"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}"
>
{{ activeTable?.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>
</SmartsheetTopbarTableListDropdown>
</template> </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 /> -->
<template #default>
<GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" /> <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>
<GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" />
</template>
</LazyGeneralEmojiPicker>
<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> </template>
</LazyGeneralEmojiPicker> </SmartsheetTopbarViewListDropdown>
<SmartsheetToolbarOpenedViewAction /> <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>

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

@ -198,388 +198,403 @@ 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 }">
<div v-if="!appInfo.auditEnabled" class="text-red-500">Audit logs are currently disabled by administrators.</div> <NcPageHeader v-if="!baseId">
<template #icon>
<div class="flex flex-col" :class="{ 'gap-6': !baseId, 'gap-4': baseId }"> <GeneralIcon icon="audit" class="flex-none text-gray-700 h-5 w-5" />
<div class="flex flex-row items-center gap-3 justify-between"> </template>
<div <template #title>
:class="{ <span data-rec="true">
'flex-1 max-w-[75%]': baseId, {{ $t('title.auditLogs') }}
}" </span>
> </template>
<h6 </NcPageHeader>
class="font-semibold text-gray-900 !my-0 flex items-center gap-1" <div
:style="{ class="flex flex-col"
'word-break': 'keep-all', :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 class="flex flex-col" :class="{ 'gap-6': !baseId, 'gap-4': baseId }">
<div class="flex flex-row items-center gap-3 justify-between">
<div
:class="{ :class="{
'text-xl': baseId, 'flex-1 max-w-[75%]': baseId,
'text-2xl': !baseId,
}" }"
> >
<span class="keep-word min-w-[100px]"> {{ $t('title.auditLogs') }} </span> <h6
<NcTooltip
v-if="baseId" v-if="baseId"
class="max-w-[80%] truncate" class="font-semibold text-gray-900 !my-0 flex items-center gap-1"
:style="{
'word-break': 'keep-all',
}"
:class="{ :class="{
'!leading-7': baseId, 'text-xl': baseId,
'!leading-8': !baseId, 'text-2xl': !baseId,
}" }"
show-on-truncate-only
placement="bottom"
> >
<template #title> <span class="keep-word min-w-[100px]"> {{ $t('title.auditLogs') }} </span>
{{ bases.get(baseId)?.title }} <NcTooltip
</template> class="max-w-[80%] truncate"
: {{ bases.get(baseId)?.title }} :class="{
</NcTooltip> '!leading-7': baseId,
</h6> '!leading-8': !baseId,
</div> }"
<div v-if="appInfo.auditEnabled" class="flex items-center gap-3 justify-end flex-wrap"> show-on-truncate-only
<div class="flex items-center gap-3"> placement="bottom"
<NcButton type="text" size="small" :disabled="isLoading" @click="loadAudits()"> >
<!-- Refresh --> <template #title>
<div class="flex items-center gap-2"> {{ bases.get(baseId)?.title }}
{{ $t('general.refresh') }} </template>
: {{ bases.get(baseId)?.title }}
<component :is="iconMap.refresh" class="h-3.5 w-3.5" :class="{ 'animate-infinite animate-spin': isLoading }" /> </NcTooltip>
</div> </h6>
</NcButton> </div>
<div v-if="appInfo.auditEnabled" class="flex items-center gap-3 justify-end flex-wrap">
<div class="flex items-center gap-3">
<NcButton type="text" size="small" :disabled="isLoading" @click="loadAudits()">
<!-- Refresh -->
<div class="flex items-center gap-2">
{{ $t('general.refresh') }}
<component :is="iconMap.refresh" class="h-3.5 w-3.5" :class="{ 'animate-infinite animate-spin': isLoading }" />
</div>
</NcButton>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <template v-if="appInfo.auditEnabled">
<template v-if="appInfo.auditEnabled">
<div
class="table-container relative"
:class="{
'h-[calc(100%_-_48px)] ': baseId,
'h-[calc(100%_-_56px)]': !baseId,
'bordered': bordered,
}"
>
<div <div
ref="tableWrapper" class="table-container relative"
class="nc-audit-logs-table h-full max-h-[calc(100%_-_40px)] relative nc-scrollbar-thin !overflow-auto" :class="{
'h-[calc(100%_-_48px)] ': baseId,
'h-[calc(100%_-_56px)]': !baseId,
'bordered': bordered,
}"
> >
<table class="!sticky top-0 z-5"> <div
<thead> ref="tableWrapper"
<tr> class="nc-audit-logs-table h-full max-h-[calc(100%_-_40px)] relative nc-scrollbar-thin !overflow-auto"
<th >
class="cell-user !hover:bg-gray-100 select-none cursor-pointer" <table class="!sticky top-0 z-5">
:class="{ <thead>
'cursor-not-allowed': !audits?.length, <tr>
}" <th
@click="updateOrderBy('user')" class="cell-user !hover:bg-gray-100 select-none cursor-pointer"
> :class="{
<div class="flex items-center gap-3"> 'cursor-not-allowed': !audits?.length,
<div>{{ $t('objects.user') }}</div> }"
<GeneralIcon @click="updateOrderBy('user')"
v-if="auditLogsQuery.orderBy?.user" >
icon="chevronDown" <div class="flex items-center gap-3">
class="flex-none" <div>{{ $t('objects.user') }}</div>
:class="{ <GeneralIcon
'transform rotate-180': auditLogsQuery.orderBy?.user === 'asc', v-if="auditLogsQuery.orderBy?.user"
}" icon="chevronDown"
/> class="flex-none"
<GeneralIcon v-else icon="chevronUpDown" class="flex-none" /> :class="{
</div> 'transform rotate-180': auditLogsQuery.orderBy?.user === 'asc',
</th> }"
<th />
class="cell-timestamp !hover:bg-gray-100 select-none cursor-pointer" <GeneralIcon v-else icon="chevronUpDown" class="flex-none" />
:class="{ </div>
'cursor-not-allowed': !audits?.length, </th>
}" <th
@click="updateOrderBy('created_at')" class="cell-timestamp !hover:bg-gray-100 select-none cursor-pointer"
> :class="{
<div class="flex items-center gap-3"> 'cursor-not-allowed': !audits?.length,
<div>Time</div> }"
@click="updateOrderBy('created_at')"
<GeneralIcon >
v-if="auditLogsQuery.orderBy?.created_at" <div class="flex items-center gap-3">
icon="chevronDown" <div>Time</div>
class="flex-none"
:class="{ <GeneralIcon
'transform rotate-180': auditLogsQuery.orderBy?.created_at === 'asc', v-if="auditLogsQuery.orderBy?.created_at"
}" icon="chevronDown"
/> class="flex-none"
<GeneralIcon v-else icon="chevronUpDown" class="flex-none" /> :class="{
</div> 'transform rotate-180': auditLogsQuery.orderBy?.created_at === 'asc',
</th> }"
<th class="cell-base"> />
<div>{{ $t('objects.project') }}</div> <GeneralIcon v-else icon="chevronUpDown" class="flex-none" />
</th> </div>
<th class="cell-type"> </th>
<div>{{ $t('general.type') }}</div> <th class="cell-base">
</th> <div>{{ $t('objects.project') }}</div>
<th class="cell-sub-type"> </th>
<div>{{ $t('general.subType') }}</div> <th class="cell-type">
</th> <div>{{ $t('general.type') }}</div>
<th class="cell-description"> </th>
<div>{{ $t('labels.description') }}</div> <th class="cell-sub-type">
</th> <div>{{ $t('general.subType') }}</div>
<th class="cell-ip"> </th>
<div>{{ $t('general.ipAddress') }}</div> <th class="cell-description">
</th> <div>{{ $t('labels.description') }}</div>
</tr> </th>
</thead> <th class="cell-ip">
</table> <div>{{ $t('general.ipAddress') }}</div>
<template v-if="audits?.length"> </th>
<table> </tr>
<tbody> </thead>
<tr </table>
v-for="(audit, i) of audits" <template v-if="audits?.length">
:key="i" <table>
:class="{ <tbody>
selected: selectedAudit?.id === audit.id && isRowExpanded, <tr
}" v-for="(audit, i) of audits"
@click="handleRowClick(audit)" :key="i"
> :class="{
<td class="cell-user"> selected: selectedAudit?.id === audit.id && isRowExpanded,
<div> }"
<div v-if="audit.user && collaboratorsMap.get(audit.user)?.email" class="w-full flex gap-3 items-center"> @click="handleRowClick(audit)"
<GeneralUserIcon :email="collaboratorsMap.get(audit.user)?.email" size="base" class="flex-none" /> >
<div class="flex-1 flex flex-col max-w-[calc(100%_-_44px)]"> <td class="cell-user">
<div class="w-full flex gap-3"> <div>
<NcTooltip <div v-if="audit.user && collaboratorsMap.get(audit.user)?.email" class="w-full flex gap-3 items-center">
class="text-sm !leading-5 text-gray-800 capitalize font-semibold truncate" <GeneralUserIcon :email="collaboratorsMap.get(audit.user)?.email" size="base" class="flex-none" />
show-on-truncate-only <div class="flex-1 flex flex-col max-w-[calc(100%_-_44px)]">
placement="bottom" <div class="w-full flex gap-3">
> <NcTooltip
<template #title> class="text-sm !leading-5 text-gray-800 capitalize font-semibold truncate"
show-on-truncate-only
placement="bottom"
>
<template #title>
{{
collaboratorsMap.get(audit.user)?.display_name ||
collaboratorsMap
.get(audit.user)
?.email?.slice(0, collaboratorsMap.get(audit.user)?.email.indexOf('@'))
}}
</template>
{{ {{
collaboratorsMap.get(audit.user)?.display_name || collaboratorsMap.get(audit.user)?.display_name ||
collaboratorsMap collaboratorsMap
.get(audit.user) .get(audit.user)
?.email?.slice(0, collaboratorsMap.get(audit.user)?.email.indexOf('@')) ?.email?.slice(0, collaboratorsMap.get(audit.user)?.email.indexOf('@'))
}} }}
</NcTooltip>
</div>
<NcTooltip class="text-xs !leading-4 text-gray-600 truncate" show-on-truncate-only placement="bottom">
<template #title>
{{ collaboratorsMap.get(audit.user)?.email }}
</template> </template>
{{ {{ collaboratorsMap.get(audit.user)?.email }}
collaboratorsMap.get(audit.user)?.display_name ||
collaboratorsMap
.get(audit.user)
?.email?.slice(0, collaboratorsMap.get(audit.user)?.email.indexOf('@'))
}}
</NcTooltip> </NcTooltip>
</div> </div>
<NcTooltip class="text-xs !leading-4 text-gray-600 truncate" show-on-truncate-only placement="bottom">
<template #title>
{{ collaboratorsMap.get(audit.user)?.email }}
</template>
{{ collaboratorsMap.get(audit.user)?.email }}
</NcTooltip>
</div> </div>
<template v-else>{{ audit.user }} </template>
</div> </div>
<template v-else>{{ audit.user }} </template> </td>
</div> <td class="cell-timestamp">
</td> <div>
<td class="cell-timestamp"> <NcTooltip placement="bottom">
<div> <template #title> {{ parseStringDateTime(audit.created_at, 'D MMMM YYYY HH:mm') }}</template>
<NcTooltip placement="bottom">
<template #title> {{ parseStringDateTime(audit.created_at, 'D MMMM YYYY HH:mm') }}</template>
{{ timeAgo(audit.created_at) }} {{ timeAgo(audit.created_at) }}
</NcTooltip>
</div>
</td>
<td class="cell-base">
<div>
<div v-if="audit.base_id" class="w-full">
<NcTooltip
class="truncate text-sm !leading-5 text-gray-800 font-semibold"
show-on-truncate-only
placement="bottom"
>
<template #title>
{{ bases.get(audit.base_id)?.title }}
</template>
{{ bases.get(audit.base_id)?.title }}
</NcTooltip> </NcTooltip>
<div class="text-gray-600 text-xs">ID: {{ audit.base_id }}</div>
</div> </div>
<template v-else> </td>
{{ audit.base_id }} <td class="cell-base">
</template> <div>
</div> <div v-if="audit.base_id" class="w-full">
</td> <NcTooltip
<td class="cell-type"> class="truncate text-sm !leading-5 text-gray-800 font-semibold"
<div> show-on-truncate-only
<div class="truncate bg-gray-200 px-2 py-1 rounded-lg"> placement="bottom"
<NcTooltip class="truncate" placement="bottom" show-on-truncate-only> >
<template #title> {{ auditOperationTypeLabels[audit.op_type] }}</template> <template #title>
{{ bases.get(audit.base_id)?.title }}
</template>
{{ bases.get(audit.base_id)?.title }}
</NcTooltip>
<span class="truncate"> {{ auditOperationTypeLabels[audit.op_type] }} </span> <div class="text-gray-600 text-xs">ID: {{ audit.base_id }}</div>
</NcTooltip> </div>
<template v-else>
{{ audit.base_id }}
</template>
</div> </div>
</div> </td>
</td> <td class="cell-type">
<td class="cell-sub-type"> <div>
<div> <div class="truncate bg-gray-200 px-2 py-1 rounded-lg">
<div class="truncate"> <NcTooltip class="truncate" placement="bottom" show-on-truncate-only>
<NcTooltip class="truncate" placement="bottom" show-on-truncate-only> <template #title> {{ auditOperationTypeLabels[audit.op_type] }}</template>
<template #title> {{ auditOperationSubTypeLabels[audit.op_sub_type] }}</template>
<span class="truncate"> {{ auditOperationTypeLabels[audit.op_type] }} </span>
<span class="truncate"> {{ auditOperationSubTypeLabels[audit.op_sub_type] }} </span> </NcTooltip>
</NcTooltip> </div>
</div> </div>
</div> </td>
</td> <td class="cell-sub-type">
<td class="cell-description"> <div>
<div> <div class="truncate">
<div class="truncate"> <NcTooltip class="truncate" placement="bottom" show-on-truncate-only>
{{ audit.description }} <template #title> {{ auditOperationSubTypeLabels[audit.op_sub_type] }}</template>
<span class="truncate"> {{ auditOperationSubTypeLabels[audit.op_sub_type] }} </span>
</NcTooltip>
</div>
</div> </div>
</div> </td>
</td> <td class="cell-description">
<td class="cell-ip"> <div>
<div> <div class="truncate">
<div class="truncate"> {{ audit.description }}
{{ audit.ip }} </div>
</div> </div>
</div> </td>
</td> <td class="cell-ip">
</tr> <div>
</tbody> <div class="truncate">
</table> {{ audit.ip }}
</template> </div>
</div> </div>
<div </td>
v-show="isLoading" </tr>
class="flex items-center justify-center absolute left-0 top-0 w-full h-full z-10 pb-10 pointer-events-none" </tbody>
> </table>
<div class="flex flex-col justify-center items-center gap-2"> </template>
<GeneralLoader size="xlarge" />
<span class="text-center">{{ $t('general.loading') }}</span>
</div> </div>
</div> <div
<div v-show="isLoading"
v-if="!isLoading && !audits?.length" class="flex items-center justify-center absolute left-0 top-0 w-full h-full z-10 pb-10 pointer-events-none"
class="flex items-center justify-center absolute left-0 top-0 w-full h-full pb-10 flex items-center justify-center text-gray-500" >
> <div class="flex flex-col justify-center items-center gap-2">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" class="!my-0" /> <GeneralLoader size="xlarge" />
</div> <span class="text-center">{{ $t('general.loading') }}</span>
<div
v-if="auditPaginationData.totalRows"
class="flex flex-row justify-center items-center bg-gray-50 min-h-10"
:class="{
'pointer-events-none': isLoading,
}"
>
<div class="flex justify-between items-center w-full px-6">
<div>&nbsp;</div>
<NcPagination
v-model:current="auditPaginationData.page"
v-model:page-size="auditPaginationData.pageSize"
:total="+auditPaginationData.totalRows"
show-size-changer
:use-stored-page-size="false"
:prev-page-tooltip="`${renderAltOrOptlKey()}+←`"
:next-page-tooltip="`${renderAltOrOptlKey()}+→`"
:first-page-tooltip="`${renderAltOrOptlKey()}+↓`"
:last-page-tooltip="`${renderAltOrOptlKey()}+↑`"
@update:current="loadAudits(undefined, undefined, false)"
@update:page-size="loadAudits(currentPage, $event, false)"
/>
<div class="text-gray-500 text-xs">
{{ auditPaginationData.totalRows }} {{ auditPaginationData.totalRows === 1 ? 'record' : 'records' }}
</div> </div>
</div> </div>
</div> <div
</div> v-if="!isLoading && !audits?.length"
<NcModal v-model:visible="isRowExpanded" size="medium" :show-separator="false" @keydown.esc="isRowExpanded = false"> class="flex items-center justify-center absolute left-0 top-0 w-full h-full pb-10 flex items-center justify-center text-gray-500"
<template #header> >
<div class="flex items-center justify-between gap-x-2 w-full"> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" class="!my-0" />
<div class="flex-1 text-base font-weight-700 text-gray-900">Audit Details</div> </div>
<div class="flex items-center gap-2"> <div
<NcTooltip placement="bottom" class="text-gray-600 text-small leading-[18px]"> v-if="auditPaginationData.totalRows"
<template #title> {{ parseStringDateTime(selectedAudit.created_at, 'D MMMM YYYY HH:mm') }}</template> class="flex flex-row justify-center items-center bg-gray-50 min-h-10"
:class="{
{{ timeAgo(selectedAudit.created_at) }} 'pointer-events-none': isLoading,
</NcTooltip> }"
>
<div class="flex justify-between items-center w-full px-6">
<div>&nbsp;</div>
<NcPagination
v-model:current="auditPaginationData.page"
v-model:page-size="auditPaginationData.pageSize"
:total="+auditPaginationData.totalRows"
show-size-changer
:use-stored-page-size="false"
:prev-page-tooltip="`${renderAltOrOptlKey()}+←`"
:next-page-tooltip="`${renderAltOrOptlKey()}+→`"
:first-page-tooltip="`${renderAltOrOptlKey()}+↓`"
:last-page-tooltip="`${renderAltOrOptlKey()}+↑`"
@update:current="loadAudits(undefined, undefined, false)"
@update:page-size="loadAudits(currentPage, $event, false)"
/>
<div class="text-gray-500 text-xs">
{{ auditPaginationData.totalRows }} {{ auditPaginationData.totalRows === 1 ? 'record' : 'records' }}
</div>
</div> </div>
</div> </div>
</template> </div>
<div v-if="selectedAudit" class="nc-expanded-audit flex flex-col gap-4"> <NcModal v-model:visible="isRowExpanded" size="medium" :show-separator="false" @keydown.esc="isRowExpanded = false">
<div class="bg-gray-50 rounded-lg border-1 border-gray-200"> <template #header>
<div class="flex"> <div class="flex items-center justify-between gap-x-2 w-full">
<div class="w-1/2 border-r border-gray-200 flex flex-col gap-2 px-4 py-3"> <div class="flex-1 text-base font-weight-700 text-gray-900">Audit Details</div>
<div class="cell-header">Performed by</div> <div class="flex items-center gap-2">
<div <NcTooltip placement="bottom" class="text-gray-600 text-small leading-[18px]">
v-if="selectedAudit?.user && collaboratorsMap.get(selectedAudit.user)?.email" <template #title> {{ parseStringDateTime(selectedAudit.created_at, 'D MMMM YYYY HH:mm') }}</template>
class="w-full flex gap-3 items-center"
>
<GeneralUserIcon :email="collaboratorsMap.get(selectedAudit.user)?.email" size="base" class="flex-none" />
<div class="flex-1 flex flex-col">
<div class="w-full flex gap-3">
<span class="text-sm text-gray-800 capitalize font-semibold">
{{
collaboratorsMap.get(selectedAudit.user)?.display_name ||
collaboratorsMap
.get(selectedAudit.user)
?.email?.slice(0, collaboratorsMap.get(selectedAudit.user)?.email.indexOf('@'))
}}
</span>
</div>
<span class="text-xs text-gray-600">
{{ collaboratorsMap.get(selectedAudit.user)?.email }}
</span>
</div>
</div>
<div v-else class="text-small leading-[18px] text-gray-600">{{ selectedAudit?.user }}</div> {{ timeAgo(selectedAudit.created_at) }}
</div> </NcTooltip>
<div class="w-1/2 flex flex-col gap-2 px-4 py-3">
<div class="cell-header">{{ $t('general.ipAddress') }}</div>
<div class="text-small leading-[18px] text-gray-600">{{ selectedAudit?.ip }}</div>
</div> </div>
</div> </div>
<div class="border-t-1 border-gray-200 flex"> </template>
<div class="w-1/2 border-r border-gray-200 flex flex-col gap-2 px-4 py-3"> <div v-if="selectedAudit" class="nc-expanded-audit flex flex-col gap-4">
<div class="cell-header">{{ $t('objects.project') }}</div> <div class="bg-gray-50 rounded-lg border-1 border-gray-200">
<div v-if="selectedAudit?.base_id && bases.get(selectedAudit?.base_id)" class="flex items-stretch gap-3"> <div class="flex">
<div class="flex items-center"> <div class="w-1/2 border-r border-gray-200 flex flex-col gap-2 px-4 py-3">
<GeneralProjectIcon <div class="cell-header">Performed by</div>
:color="bases.get(selectedAudit?.base_id)?.meta?.iconColor" <div
:type="bases.get(selectedAudit?.base_id)?.type || 'database'" v-if="selectedAudit?.user && collaboratorsMap.get(selectedAudit.user)?.email"
class="nc-view-icon w-5 h-5" class="w-full flex gap-3 items-center"
/> >
</div> <GeneralUserIcon :email="collaboratorsMap.get(selectedAudit.user)?.email" size="base" class="flex-none" />
<div> <div class="flex-1 flex flex-col">
<div class="text-sm font-weight-500 text-gray-900">{{ bases.get(selectedAudit?.base_id)?.title }}</div> <div class="w-full flex gap-3">
<div class="text-small leading-[18px] text-gray-600">{{ selectedAudit?.base_id }}</div> <span class="text-sm text-gray-800 capitalize font-semibold">
{{
collaboratorsMap.get(selectedAudit.user)?.display_name ||
collaboratorsMap
.get(selectedAudit.user)
?.email?.slice(0, collaboratorsMap.get(selectedAudit.user)?.email.indexOf('@'))
}}
</span>
</div>
<span class="text-xs text-gray-600">
{{ collaboratorsMap.get(selectedAudit.user)?.email }}
</span>
</div>
</div> </div>
<div v-else class="text-small leading-[18px] text-gray-600">{{ selectedAudit?.user }}</div>
</div>
<div class="w-1/2 flex flex-col gap-2 px-4 py-3">
<div class="cell-header">{{ $t('general.ipAddress') }}</div>
<div class="text-small leading-[18px] text-gray-600">{{ selectedAudit?.ip }}</div>
</div> </div>
<template v-else>
{{ selectedAudit.base_id }}
</template>
</div> </div>
<div class="w-1/2"> <div class="border-t-1 border-gray-200 flex">
<div class="h-1/2 border-b border-gray-200 flex items-center gap-2 px-4 py-3"> <div class="w-1/2 border-r border-gray-200 flex flex-col gap-2 px-4 py-3">
<div class="cell-header">{{ $t('general.type') }}</div> <div class="cell-header">{{ $t('objects.project') }}</div>
<div class="text-small leading-[18px] text-gray-600 bg-gray-200 px-1 rounded-md"> <div v-if="selectedAudit?.base_id && bases.get(selectedAudit?.base_id)" class="flex items-stretch gap-3">
{{ auditOperationTypeLabels[selectedAudit?.op_type] }} <div class="flex items-center">
<GeneralProjectIcon
:color="bases.get(selectedAudit?.base_id)?.meta?.iconColor"
:type="bases.get(selectedAudit?.base_id)?.type || 'database'"
class="nc-view-icon w-5 h-5"
/>
</div>
<div>
<div class="text-sm font-weight-500 text-gray-900">{{ bases.get(selectedAudit?.base_id)?.title }}</div>
<div class="text-small leading-[18px] text-gray-600">{{ selectedAudit?.base_id }}</div>
</div>
</div> </div>
<template v-else>
{{ selectedAudit.base_id }}
</template>
</div> </div>
<div class="h-1/2 flex items-center gap-2 px-4 py-3"> <div class="w-1/2">
<div class="cell-header">{{ $t('general.subType') }}</div> <div class="h-1/2 border-b border-gray-200 flex items-center gap-2 px-4 py-3">
<div class="text-small leading-[18px] text-gray-600"> <div class="cell-header">{{ $t('general.type') }}</div>
{{ auditOperationSubTypeLabels[selectedAudit?.op_sub_type] }} <div class="text-small leading-[18px] text-gray-600 bg-gray-200 px-1 rounded-md">
{{ auditOperationTypeLabels[selectedAudit?.op_type] }}
</div>
</div>
<div class="h-1/2 flex items-center gap-2 px-4 py-3">
<div class="cell-header">{{ $t('general.subType') }}</div>
<div class="text-small leading-[18px] text-gray-600">
{{ auditOperationSubTypeLabels[selectedAudit?.op_sub_type] }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="flex flex-col gap-2">
<div class="flex flex-col gap-2"> <div class="cell-header">{{ $t('labels.description') }}</div>
<div class="cell-header">{{ $t('labels.description') }}</div> <div>
<div> <pre class="!text-small !leading-[18px] !text-gray-600 mb-0">{{ selectedAudit?.description }}</pre>
<pre class="!text-small !leading-[18px] !text-gray-600 mb-0">{{ selectedAudit?.description }}</pre> </div>
</div> </div>
</div> </div>
</div> </NcModal>
</NcModal> </template>
</template> </div>
</div> </div>
</template> </template>

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"

76
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">
</h1> {{ currentWorkspace?.title }}
</div>
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
<h1 class="nc-breadcrumb-item active">
{{ $t('title.teamAndSettings') }}
</h1>
</div>
</div> </div>
<div v-else> <template v-else>
<div class="font-bold w-full !mb-5 text-2xl" data-rec="true"> <div class="nc-breadcrumb px-2">
<div class="flex items-center gap-3"> <div class="nc-breadcrumb-item">
<NuxtLink {{ org.title }}
:href="`/admin/${orgId}/workspaces`" </div>
class="!hover:(text-black underline-gray-600) flex items-center !text-black !underline-transparent ml-0.75 max-w-1/4" <GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
>
<component :is="iconMap.arrowLeft" class="text-3xl" />
<NuxtLink
:href="`/admin/${orgId}/workspaces`"
class="!hover:(text-gray-800 underline-gray-600) flex items-center !text-gray-700 !underline-transparent ml-0.75 max-w-1/4"
>
<div class="nc-breadcrumb-item">
{{ $t('labels.workspaces') }} {{ $t('labels.workspaces') }}
</NuxtLink> </div>
</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 /> {{ currentWorkspace?.title }}
<span class="text-base capitalize">
{{ currentWorkspace?.title }}
</span>
</div> </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>
</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;
} }

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

@ -47,10 +47,16 @@ 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">
</h1> {{ currentWorkspace?.title }}
</div>
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
<h1 class="nc-breadcrumb-item active">
{{ $t('general.integrations') }}
</h1>
</div>
</div> </div>
<NcTabs v-model:activeKey="activeViewTab"> <NcTabs v-model:activeKey="activeViewTab">
@ -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

54
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