Browse Source

Merge pull request #3023 from nocodb/feat/menu

feat(gui-v2): implement project menu
pull/3056/head
navi 2 years ago committed by GitHub
parent
commit
7f0d12bd08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 97
      packages/nc-gui-v2/app.vue
  2. 8
      packages/nc-gui-v2/assets/style-v2.scss
  3. 33
      packages/nc-gui-v2/components.d.ts
  4. 9
      packages/nc-gui-v2/components/cell/attachment/index.vue
  5. 137
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  6. 45
      packages/nc-gui-v2/components/dashboard/settings/Modal.vue
  7. 2
      packages/nc-gui-v2/components/general/Language.vue
  8. 144
      packages/nc-gui-v2/components/general/MiniSidebar.vue
  9. 35
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  10. 29
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  11. 13
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  12. 20
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  13. 3
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  14. 25
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  15. 3
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/AddRow.vue
  16. 7
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue
  17. 19
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue
  18. 5
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  19. 2
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  20. 1
      packages/nc-gui-v2/composables/index.ts
  21. 4
      packages/nc-gui-v2/composables/useGlobal/state.ts
  22. 1
      packages/nc-gui-v2/composables/useGlobal/types.ts
  23. 2
      packages/nc-gui-v2/composables/useInjectionState/index.ts
  24. 58
      packages/nc-gui-v2/composables/useSidebar/index.ts
  25. 101
      packages/nc-gui-v2/layouts/base.vue
  26. 10
      packages/nc-gui-v2/layouts/default.vue
  27. 4
      packages/nc-gui-v2/nuxt.config.ts
  28. 199
      packages/nc-gui-v2/package-lock.json
  29. 2
      packages/nc-gui-v2/package.json
  30. 36
      packages/nc-gui-v2/pages/index/index.vue
  31. 40
      packages/nc-gui-v2/pages/index/user/index.vue
  32. 106
      packages/nc-gui-v2/pages/index/user/index/index.vue
  33. 219
      packages/nc-gui-v2/pages/nc/[projectId]/index.vue
  34. 21
      packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue
  35. 18
      packages/nc-gui-v2/pages/project/index/[id].vue
  36. 13
      packages/nc-gui-v2/pages/project/index/create-external.vue
  37. 22
      packages/nc-gui-v2/pages/project/index/create.vue
  38. 4
      packages/nc-gui-v2/pages/signin.vue
  39. 9
      packages/nc-gui-v2/plugins/state.ts
  40. 1
      packages/nc-gui-v2/tsconfig.json
  41. 1
      packages/nc-gui-v2/utils/viewUtils.ts
  42. 2
      packages/nc-gui-v2/windi.config.ts

97
packages/nc-gui-v2/app.vue

@ -1,96 +1,5 @@
<script lang="ts" setup>
import { navigateTo } from '#app'
import { useGlobal } from '#imports'
const state = useGlobal()
const sidebar = ref<HTMLDivElement>()
const email = computed(() => state.user.value?.email ?? '---')
const signOut = () => {
state.signOut()
navigateTo('/signin')
}
const sidebarCollapsed = computed({
get: () => !state.sidebarOpen.value,
set: (val) => (state.sidebarOpen.value = !val),
})
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value
}
</script>
<template>
<a-layout class="min-h-[100vh]">
<a-layout-header class="flex !bg-primary items-center text-white px-4 shadow-md">
<material-symbols-menu v-if="state.signedIn.value" class="text-xl cursor-pointer" @click="toggleSidebar" />
<div class="flex-1" />
<div class="ml-4 flex justify-center shrink">
<div class="flex items-center gap-2 cursor-pointer nc-noco-brand-icon" @click="navigateTo('/')">
<img width="35" src="~/assets/img/icons/512x512-trans.png" />
<span class="prose-xl">NocoDB</span>
</div>
</div>
<div class="flex-1 text-left">
<div v-show="state.isLoading.value" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }}
<mdi-reload :class="{ 'animate-infinite animate-spin': state.isLoading.value }" />
</div>
</div>
<div class="flex justify-end gap-4">
<general-language class="mr-3" />
<template v-if="state.signedIn.value">
<a-dropdown :trigger="['click']">
<mdi-dots-vertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />
<template #overlay>
<a-menu class="!py-0 nc-user-menu min-w-32 dark:(!bg-gray-800) leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" />&nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="signOut">
<mdi-logout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</div>
</a-layout-header>
<a-layout>
<a-layout-sider
v-model:collapsed="sidebarCollapsed"
width="300"
collapsed-width="0"
class="bg-white dark:!bg-gray-800 border-r-1 border-gray-200 dark:!border-gray-600 h-full"
:trigger="null"
collapsible
>
<div id="sidebar" ref="sidebar" class="w-full h-full" />
</a-layout-sider>
<NuxtPage />
</a-layout>
</a-layout>
<NuxtLayout name="base">
<NuxtPage />
</NuxtLayout>
</template>

8
packages/nc-gui-v2/assets/style-v2.scss

@ -2,7 +2,7 @@
@import 'ant-design-vue/dist/antd.min.css';
:root {
--header-height: 56px;
--header-height: 50px;
}
.ant-layout-header {
@ -18,7 +18,7 @@ main {
}
main {
@apply flex-0 w-full relative scrollbar-thin-primary;
@apply flex-0 w-full relative scrollbar-thin-dull;
overflow-x: hidden;
}
@ -75,7 +75,7 @@ html {
// menu item styling
.nc-menu-item {
@apply cursor-pointer text-xs flex align-center gap-2 px-4 py-3 relative after:(content-[''] absolute top-0 left-0 w-full h-full right 0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
@apply cursor-pointer text-xs flex items-center gap-2 px-4 py-3 after:(content-[''] absolute top-0 left-0 bottom-0 w-full h-full right-0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
}
.nc-sidebar-right-item {
@ -106,5 +106,5 @@ html {
}
.ant-modal-wrap {
@apply !scrollbar-thin-primary;
@apply !scrollbar-thin-dull;
}

33
packages/nc-gui-v2/components.d.ts vendored

@ -41,14 +41,12 @@ declare module '@vue/runtime-core' {
AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup']
AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARate: typeof import('ant-design-vue/es')['Rate']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASkeletonImage: typeof import('ant-design-vue/es')['SkeletonImage']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
@ -66,34 +64,63 @@ declare module '@vue/runtime-core' {
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
MaterialSymbolsArrowBackRounded: typeof import('~icons/material-symbols/arrow-back-rounded')['default']
MaterialSymbolsArrowForwardRounded: typeof import('~icons/material-symbols/arrow-forward-rounded')['default']
MaterialSymbolsAttachFile: typeof import('~icons/material-symbols/attach-file')['default']
MaterialSymbolsChevronLeftRounded: typeof import('~icons/material-symbols/chevron-left-rounded')['default']
MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsMenu: typeof import('~icons/material-symbols/menu')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MdiAccountGroup: typeof import('~icons/mdi/account-group')['default']
MdiApi: typeof import('~icons/mdi/api')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiAt: typeof import('~icons/mdi/at')['default']
MdiCalculator: typeof import('~icons/mdi/calculator')['default']
MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default']
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default']
MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiCloseCircle: typeof import('~icons/mdi/close-circle')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiDatabase: typeof import('~icons/mdi/database')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
MdiDiscord: typeof import('~icons/mdi/discord')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiDownload: typeof import('~icons/mdi/download')['default']
MdiDrag: typeof import('~icons/mdi/drag')['default']
MdiEmail: typeof import('~icons/mdi/email')['default']
MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default']
MdiLink: typeof import('~icons/mdi/link')['default']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiNotebookCheckOutline: typeof import('~icons/mdi/notebook-check-outline')['default']
MdiOpenInNew: typeof import('~icons/mdi/open-in-new')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiStar: typeof import('~icons/mdi/star')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
MdiStore: typeof import('~icons/mdi/store')['default']
MdiTableBorder: typeof import('~icons/mdi/table-border')['default']
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']
MdiTrashCan: typeof import('~icons/mdi/trash-can')['default']
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MdiXml: typeof import('~icons/mdi/xml')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

9
packages/nc-gui-v2/components/cell/attachment/index.vue

@ -60,7 +60,7 @@ onKeyDown('Escape', () => {
/** if possible, on mounted we try to fetch the relevant `td` cell to use as a dropzone */
onMounted(() => {
if (typeof document !== 'undefined') {
dropZoneRef.value = document.querySelector(`td[data-col="${column.id}"]`) as HTMLTableDataCellElement
dropZoneRef.value = document.querySelector(`td[data-col="${column.value.id}"]`) as HTMLTableDataCellElement
}
})
</script>
@ -100,14 +100,14 @@ onMounted(() => {
</div>
<template v-if="visibleItems.length">
<div ref="sortableRef" :class="{ dragging }" class="flex flex-wrap gap-2 p-1 scrollbar-thin-primary">
<div ref="sortableRef" :class="{ dragging }" class="flex flex-wrap gap-2 p-1 scrollbar-thin-dull">
<div
v-for="(item, i) of visibleItems"
:id="item.url"
:key="item.url || item.title"
style="flex: 1 1 50px"
:class="isImage(item.title, item.mimetype) ? '' : 'border-1 rounded'"
class="nc-attachment flex items-center justify-center"
class="nc-attachment flex items-center justify-center min-h-[50px]"
>
<a-tooltip placement="bottom">
<template #title>
@ -116,9 +116,8 @@ onMounted(() => {
<nuxt-img
v-if="isImage(item.title, item.mimetype)"
quality="75"
placeholder
width="150"
height="150"
:alt="item.title || `#${i}`"
:src="item.url || item.data"
class="ring-1 ring-gray-300 rounded"

137
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -2,10 +2,8 @@
import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { useToast } from 'vue-toastification'
import SettingsModal from './settings/SettingsModal.vue'
import { computed, useProject, useTable, useTabs, useUIPermission, watchEffect } from '#imports'
import { useProject, useTable, useTabs, watchEffect } from '#imports'
import { useNuxtApp, useRoute } from '#app'
import MdiSettingIcon from '~icons/mdi/cog'
import MdiTable from '~icons/mdi/table'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
@ -13,15 +11,17 @@ import MdiMenuDown from '~icons/mdi/chevron-down'
import MdiPlus from '~icons/mdi/plus-circle-outline'
import MdiDrag from '~icons/mdi/drag-vertical'
import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiAPIDocIcon from '~icons/mdi/open-in-new'
const { addTab } = useTabs()
const toast = useToast()
const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const route = useRoute()
const { tables, loadTables } = useProject(route.params.projectId as string)
const { closeTab } = useTabs()
const { deleteTable } = useTable()
const tablesById = $computed<Record<string, TableType>>(() =>
@ -31,12 +31,14 @@ const tablesById = $computed<Record<string, TableType>>(() =>
}, {}),
)
const settingsDlg = ref(false)
const showTableList = ref(true)
const tableCreateDlg = ref(false)
const tableDeleteDlg = ref(false)
const menuRef = $ref<HTMLLIElement>()
let key = $ref(0)
let sortable: Sortable
// todo: replace with vuedraggable
@ -103,21 +105,14 @@ const icon = (table: TableType) => {
}
}
const apiLink = computed(
() =>
// new URL(
`/api/v1/db/meta/projects/${route.params.projectId}/swagger`,
// todo: get siteUrl
// this.$store.state.project.appInfo && this.$store.state.project.appInfo.ncSiteUrl
// ),
)
const filterQuery = $ref('')
const filteredTables = $computed(() => {
return tables?.value?.filter((table) => !filterQuery || table?.title.toLowerCase()?.includes(filterQuery.toLowerCase()))
})
const contextMenuTarget = reactive<{ type?: 'table' | 'main'; value?: any }>({})
const setMenuContext = (type: 'table' | 'main', value?: any) => {
contextMenuTarget.type = type
contextMenuTarget.value = value
@ -125,16 +120,20 @@ const setMenuContext = (type: 'table' | 'main', value?: any) => {
}
const renameTableDlg = ref(false)
const renameTableMeta = ref()
const showRenameTableDlg = (table: TableType, rightClick = false) => {
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
renameTableMeta.value = table
renameTableDlg.value = true
}
const reloadTables = async () => {
$e('a:table:refresh:navdraw')
await loadTables()
}
const addTableTab = (table: TableType) => {
$e('a:table:open')
addTab({ title: table.title, id: table.id, type: table.type as any })
@ -142,8 +141,8 @@ const addTableTab = (table: TableType) => {
</script>
<template>
<div class="nc-treeview-container flex flex-column">
<div class="px-3 py-2">
<div class="nc-treeview-container flex flex-col">
<div class="px-2 py-[11.75px] border-b-1">
<a-input-search
v-model:value="filterQuery"
size="small"
@ -153,47 +152,73 @@ const addTableTab = (table: TableType) => {
</div>
<a-dropdown :trigger="['contextmenu']">
<div class="p-1 flex-1 overflow-y-auto flex flex-column scrollbar-thin-primary">
<div class="p-1 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull" style="direction: rtl">
<div
style="direction: ltr"
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@click="showTableList = !showTableList"
@contextmenu="setMenuContext('main')"
>
<MdiTable class="mr-1 text-gray-500" />
<span class="flex-grow text-bold nc-project-tree"
>{{ $t('objects.tables') }} <template v-if="tables?.length">({{ tables.length }})</template></span
>
<MdiPlus v-t="['c:table:create:navdraw']" class="text-gray-500 nc-btn-tbl-add" @click.stop="tableCreateDlg = true" />
<span class="flex-grow text-bold nc-project-tree">
{{ $t('objects.tables') }}
<template v-if="tables?.length"> ({{ tables.length }}) </template>
</span>
<MdiPlus
v-t="['c:table:create:navdraw']"
class="transform text-gray-500 hover:(text-pink-500 scale-105) nc-btn-tbl-add"
@click.stop="tableCreateDlg = true"
/>
<MdiMenuDown
class="transition-transform !duration-100 text-gray-500"
class="transition-transform !duration-100 text-gray-500 hover:text-pink-500"
:class="{ 'transform rotate-180': showTableList }"
/>
</div>
<div class="flex-1">
<div style="direction: ltr" class="flex-1">
<div class="transition-height duration-200 overflow-hidden" :class="{ 'h-100': showTableList, 'h-0': !showTableList }">
<div :key="key" ref="menuRef" class="border-none sortable-list">
<div
v-for="table in tables"
v-for="table of tables"
:key="table.id"
v-t="['a:table:open']"
:class="[{ hidden: !filteredTables?.includes(table) }, `nc-project-tree-tbl nc-project-tree-tbl-${table.title}`]"
class="!pl-1 py-1 !h-[28px] !my-0 text-sm cursor-pointer group"
:class="[
{ hidden: !filteredTables?.includes(table) },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,
route.params.title && route.params.title.includes(table.title) ? 'bg-blue-500/15' : '',
]"
class="pl-5 pr-3 py-2 text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
@click="addTableTab(table)"
>
<div class="flex align-center gap-1 h-full" @contextmenu="setMenuContext('table', table)">
<MdiDrag
:class="`transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 nc-drag-icon cursor-move nc-child-draggable-icon-${table.title}`"
/>
<component :is="icon(table)" class="text-[10px] text-gray-500" />
<div class="flex align-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto">
<MdiDrag
:class="`nc-child-draggable-icon-${table.title}`"
class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
@click.stop.prevent
/>
<component
:is="icon(table)"
:class="route.params.title && route.params.title.includes(table.title) ? 'text-pink-500' : 'text-gray-500'"
class="nc-view-icon group-hover:hidden text-xs"
/>
</div>
<div class="nc-tbl-title text-xs flex-1">{{ table.title }}</div>
<span class="nc-tbl-title text-xs flex-1 ml-2">{{ table.title }}</span>
<a-dropdown :trigger="['click']" @click.stop>
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
<a-menu class="cursor-pointer">
<a-menu-item v-t="" class="!text-xs" @click="showRenameTableDlg(table)"><div>Rename</div></a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item>
</a-menu>
</template>
@ -223,28 +248,13 @@ const addTableTab = (table: TableType) => {
</a-menu>
</template>
</a-dropdown>
<div class="w-full h-[1px] bg-gray-200" />
<a v-if="isUIAllowed('apiDocs')" v-t="['e:api-docs']" class="nc-treeview-footer-item" :href="apiLink" target="_blank">
<MdiAPIDocIcon class="mr-2" />
<span> {{ $t('title.apiDocs') }}</span>
</a>
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-treeview-footer-item nc-team-settings"
@click="settingsDlg = true"
>
<MdiSettingIcon class="mr-2" />
<span> {{ $t('title.teamAndSettings') }}</span>
</div>
<SettingsModal :show="settingsDlg" @closed="settingsDlg = false" />
<DlgTableCreate v-if="tableCreateDlg" v-model="tableCreateDlg" />
<DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" />
</div>
</template>
<style scoped>
<style lang="scss" scoped>
.nc-treeview-container {
@apply h-[calc(100vh_-_var(--header-height))];
}
@ -264,4 +274,29 @@ const addTableTab = (table: TableType) => {
:deep(.ant-input-group-addon:last-child) {
@apply top-[-0.5px];
}
.nc-treeview-container {
.ghost,
.ghost > * {
@apply !pointer-events-none;
}
&.dragging {
.nc-icon {
@apply !hidden;
}
.nc-view-icon {
@apply !block;
}
}
.ant-menu-item:not(.sortable-chosen) {
@apply color-transition hover:!bg-transparent;
}
.sortable-chosen {
@apply !bg-primary/25 text-primary;
}
}
</style>

45
packages/nc-gui-v2/components/dashboard/settings/SettingsModal.vue → packages/nc-gui-v2/components/dashboard/settings/Modal.vue

@ -10,9 +10,11 @@ import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple'
import NootbookOutline from '~icons/mdi/notebook-outline'
import { useVModel, watch } from '#imports'
interface Props {
show: boolean
modelValue: boolean
openKey?: string
}
interface SubTabGroup {
@ -30,9 +32,11 @@ interface TabGroup {
}
}
const { show } = defineProps<Props>()
const props = defineProps<Props>()
const emits = defineEmits(['closed'])
const emits = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emits)
const tabsInfo: TabGroup = {
teamAndAuth: {
@ -41,11 +45,11 @@ const tabsInfo: TabGroup = {
subTabs: {
usersManagement: {
title: 'Users Management',
body: () => UserManagement,
body: UserManagement,
},
apiTokenManagement: {
title: 'API Token Management',
body: () => ApiTokenManagement,
body: ApiTokenManagement,
},
},
},
@ -55,7 +59,7 @@ const tabsInfo: TabGroup = {
subTabs: {
new: {
title: 'Apps',
body: () => AppStore,
body: AppStore,
},
},
},
@ -65,11 +69,11 @@ const tabsInfo: TabGroup = {
subTabs: {
metaData: {
title: 'Metadata',
body: () => Metadata,
body: Metadata,
},
acl: {
title: 'UI Access Control',
body: () => UIAcl,
body: UIAcl,
},
},
},
@ -79,7 +83,7 @@ const tabsInfo: TabGroup = {
subTabs: {
audit: {
title: 'Audit',
body: () => AuditTab,
body: AuditTab,
},
},
},
@ -88,7 +92,7 @@ const tabsInfo: TabGroup = {
const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
// Array of keys of tabs which are selected. In our case will be only one.
const selectedTabKeys = $ref<string[]>([firstKeyOfObject(tabsInfo)])
let selectedTabKeys = $ref<string[]>([firstKeyOfObject(tabsInfo)])
const selectedTab = $computed(() => tabsInfo[selectedTabKeys[0]])
let selectedSubTabKeys = $ref<string[]>([firstKeyOfObject(selectedTab.subTabs)])
@ -100,18 +104,27 @@ watch(
selectedSubTabKeys = [firstKeyOfObject(tabsInfo[newTabKey].subTabs)]
},
)
watch(
() => props.openKey,
(nextOpenKey) => {
selectedTabKeys = [Object.keys(tabsInfo).find((key) => key === nextOpenKey) || firstKeyOfObject(tabsInfo)]
},
)
</script>
<template>
<a-modal :footer="null" :visible="show" width="max(90vw, 600px)" @cancel="emits('closed')">
<a-modal v-model:visible="vModel" :footer="null" width="max(90vw, 600px)" @cancel="emits('update:modelValue', false)">
<a-typography-title class="ml-4 mb-2 select-none" type="secondary" :level="5">SETTINGS</a-typography-title>
<a-layout class="mt-3 modal-body">
<a-layout class="mt-3 modal-body flex">
<!-- Side tabs -->
<a-layout-sider theme="light">
<a-menu v-model:selectedKeys="selectedTabKeys" class="h-full" mode="inline" :open-keys="[]">
<a-menu v-model:selected-keys="selectedTabKeys" class="h-full" mode="inline" :open-keys="[]">
<a-menu-item v-for="(tab, key) of tabsInfo" :key="key">
<div class="flex flex-row items-center space-x-2">
<component :is="tab.icon" class="flex" />
<div class="flex select-none">
{{ tab.title }}
</div>
@ -121,14 +134,14 @@ watch(
</a-layout-sider>
<!-- Sub Tabs -->
<a-layout-content class="h-full px-4 scrollbar-thumb-gray-500">
<a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500">
<a-menu v-model:selectedKeys="selectedSubTabKeys" :open-keys="[]" mode="horizontal">
<a-menu-item v-for="(tab, key) of selectedTab.subTabs" :key="key" class="select-none">
{{ tab.title }}
</a-menu-item>
</a-menu>
<component :is="selectedSubTab.body()" class="px-2 py-6" />
<component :is="selectedSubTab.body" class="px-2 py-6" />
</a-layout-content>
</a-layout>
</a-modal>
@ -136,6 +149,6 @@ watch(
<style scoped>
.modal-body {
@apply h-[70vh];
@apply min-h-[75vh];
}
</style>

2
packages/nc-gui-v2/components/general/Language.vue

@ -34,7 +34,7 @@ onMounted(() => {
<template>
<v-menu class="select-none">
<template #activator="{ props }">
<MaterialSymbolsTranslate class="md:text-xl cursor-pointer nc-menu-translate" @click="props.onClick" />
<MaterialSymbolsTranslate v-bind="$attrs" class="md:text-xl cursor-pointer nc-menu-translate" @click="props.onClick" />
</template>
<v-list class="scrollbar min-w-50 max-h-90vh overflow-auto !py-0 dark:(!bg-gray-800 !text-white)">

144
packages/nc-gui-v2/components/general/MiniSidebar.vue

@ -0,0 +1,144 @@
<script lang="ts" setup>
import { breakpointsTailwind } from '@vueuse/core'
import { navigateTo } from '#app'
import { computed, useBreakpoints, useGlobal, useProject, useRoute, useSidebar } from '#imports'
/** get current breakpoints (for enabling sidebar) */
const breakpoints = useBreakpoints(breakpointsTailwind)
const { signOut, signedIn, isLoading, user } = useGlobal()
const { isOpen } = useSidebar({ isOpen: true })
const { project } = useProject()
const route = useRoute()
const email = computed(() => user.value?.email ?? '---')
const logout = () => {
signOut()
navigateTo('/signin')
}
</script>
<template>
<a-layout-sider
:collapsed="isOpen"
width="50"
collapsed-width="0"
class="nc-mini-sidebar !bg-primary h-full"
:trigger="null"
collapsible
theme="light"
>
<a-dropdown placement="bottom" :trigger="['click']">
<div class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105">
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<template v-if="signedIn" #overlay>
<a-menu class="ml-2 !py-0 min-w-32 leading-8 !rounded">
<a-menu-item-group title="User Settings">
<a-menu-item key="email" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" />
&nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item key="signout" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout">
<MdiLogout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu-item-group>
</a-menu>
</template>
</a-dropdown>
<div id="sidebar" ref="sidebar" class="text-white flex-auto flex flex-col items-center w-full">
<a-dropdown :trigger="['contextmenu']" placement="right">
<div :class="[route.name === 'index' ? 'active' : '']" class="nc-mini-sidebar-item" @click="navigateTo('/')">
<MdiFolder class="cursor-pointer transform hover:scale-105 text-2xl" />
</div>
<template #overlay>
<a-menu class="mt-6 select-none !py-0 min-w-32 leading-8 !rounded">
<a-menu-item-group>
<template #title>
<span class="cursor-pointer prose-sm text-gray-500 hover:text-primary" @click="navigateTo('/')">
{{ $t('objects.projects') }}
</span>
</template>
<a-menu-item class="active:(ring ring-pink-500)">
<div
v-t="['c:project:create:xcdb']"
class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create')"
>
<MdiPlus class="text-lg group-hover:text-pink-500" />
{{ $t('activity.createProject') }}
</div>
</a-menu-item>
<a-menu-item class="rounded-b active:(ring ring-pink-500)">
<div
v-t="['c:project:create:extdb']"
class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create-external')"
>
<MdiDatabaseOutline class="text-lg group-hover:text-pink-500" />
<div v-html="$t('activity.createProjectExtended.extDB')" />
</div>
</a-menu-item>
</a-menu-item-group>
</a-menu>
</template>
</a-dropdown>
<a-tooltip placement="right">
<template v-if="project" #title>{{ project.title }}</template>
<div
:class="[route.name.includes('nc-projectId') ? 'active' : 'pointer-events-none !text-gray-400']"
class="nc-mini-sidebar-item"
@click="navigateTo(`/nc/${route.params.projectId}`)"
>
<MdiDatabase class="cursor-pointer transform hover:scale-105 text-2xl" />
</div>
</a-tooltip>
</div>
</a-layout-sider>
</template>
<style lang="scss" scoped>
.nc-mini-sidebar {
:deep(.ant-layout-sider-children) {
@apply flex flex-col items-center;
}
.nc-mini-sidebar-item {
@apply flex w-full justify-center items-center h-12 group p-2;
&.active {
@apply bg-pink-500 border-t-1 border-b-1;
}
}
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
</style>

35
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -251,7 +251,7 @@ const onNavigate = (dir: NavigateDir) => {
<template>
<div class="flex flex-col h-100 min-h-0 w-100">
<div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-primary">
<div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull">
<a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']">
<table ref="smartTable" class="xc-row-table nc-grid backgroundColorDefault" @contextmenu.prevent="contextMenu = true">
<thead>
@ -295,22 +295,23 @@ const onNavigate = (dir: NavigateDir) => {
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in data" :key="rowIndex" class="nc-grid-row group">
<td key="row-index" class="caption nc-grid-cell">
<div class="align-center flex w-[80px]">
<tr v-for="(row, rowIndex) of data" :key="rowIndex" class="nc-grid-row">
<td key="row-index" class="caption nc-grid-cell group">
<div class="flex items-center w-[80px]">
<div class="group-hover:hidden" :class="{ hidden: row.rowMeta.selected }">{{ rowIndex + 1 }}</div>
<div
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="group-hover:flex w-full align-center"
class="group-hover:flex w-full items-center justify-between p-1"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
<span class="flex-1" />
<MdiArrowExpand class="text-sm text-pink hidden group-hover:inline-block" />
<div class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:bg-primary/10">
<MdiArrowExpand class="select-none transform hover:(text-pink-500 scale-120)" />
</div>
</div>
</div>
</td>
<td
v-for="(columnObj, colIndex) in fields"
v-for="(columnObj, colIndex) of fields"
:key="rowIndex + columnObj.title"
class="cell pointer nc-grid-cell"
:class="{
@ -353,17 +354,13 @@ const onNavigate = (dir: NavigateDir) => {
class="text-left pointer nc-grid-add-new-cell"
@click="addEmptyRow()"
>
<a-tooltip top left>
<div class="w-full flex align-center">
<MdiPlus class="text-pint-500 text-xs" />
<span class="ml-1 caption grey--text">
{{ $t('activity.addRow') }}
</span>
</div>
<template #title>
<span class="caption"> Add new row</span>
</template>
</a-tooltip>
<div class="px-2 w-full flex items-center">
<MdiPlus class="text-pint-500 text-xs" />
<span class="ml-1">
{{ $t('activity.addRow') }}
</span>
</div>
</td>
</tr>
</tbody>

29
packages/nc-gui-v2/components/smartsheet/Pagination.vue

@ -15,37 +15,10 @@ const page = computed({
get: () => paginatedData?.value?.page ?? 1,
set: (p) => changePage?.(p),
})
/*
export default {
name: 'Pagination',
props: {
count: [Number, String],
value: [Number, String],
size: [Number, String],
},
data: () => ({
page: 1,
}),
watch: {
value(v) {
this.page = v
},
count(c) {
const page = Math.max(1, Math.min(this.page, Math.ceil(c / this.size)))
if (this.value !== page) {
this.$emit('input', page)
}
},
},
mounted() {
this.page = this.value
},
}
*/
</script>
<template>
<div class="flex items-center mb-2">
<div class="flex items-center">
<span v-if="count !== null && count !== Infinity" class="caption ml-2"> {{ count }} record{{ count !== 1 ? 's' : '' }} </span>
<div class="flex-1" />

13
packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue

@ -2,11 +2,6 @@
import { ViewTypes } from 'nocodb-sdk'
import { ref, useNuxtApp } from '#imports'
import { viewIcons } from '~/utils'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiXml from '~icons/mdi/xml'
import MdiHook from '~icons/mdi/hook'
import MdiHeartsCard from '~icons/mdi/cards-heart'
import MdiShieldLockOutline from '~icons/mdi/shield-lock-outline'
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void
@ -53,7 +48,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" />
<MdiPlus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
@ -71,7 +66,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" />
<MdiPlus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
@ -89,7 +84,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" />
<MdiPlus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
@ -137,7 +132,7 @@ function onOpenModal(type: ViewTypes, title = '') {
class="group flex items-center gap-2 w-full mx-3 px-4 py-2 rounded-l !bg-primary text-white transform translate-x-4 hover:(translate-x-0 shadow-lg !opacity-100) transition duration-150 ease"
@click.stop
>
<MdiHeartsCard class="text-red-500" />
<MdiCardsHeart class="text-red-500" />
{{ $t('activity.sponsorUs') }}
</a>
</template>

20
packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue

@ -6,7 +6,7 @@ import { notification } from 'ant-design-vue'
import type { Ref } from 'vue'
import Sortable from 'sortablejs'
import RenameableMenuItem from './RenameableMenuItem.vue'
import { inject, onMounted, ref, useApi, useRouter, watch } from '#imports'
import { inject, onMounted, ref, useApi, useRoute, useRouter, watch } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
import { ActiveViewInj, ViewListInj } from '~/context'
@ -26,6 +26,8 @@ const { api } = useApi()
const router = useRouter()
const route = useRoute()
/** Selected view(s) for menu */
const selected = ref<string[]>([])
@ -119,7 +121,6 @@ async function onSortEnd(evt: SortableEvent) {
let sortable: Sortable
// todo: replace with vuedraggable
const initSortable = (el: HTMLElement) => {
if (sortable) sortable.destroy()
@ -193,7 +194,10 @@ function onDeleted() {
:key="view.id"
:view="view"
class="transition-all ease-in duration-300"
:class="[isMarked === view.id ? 'bg-gray-200' : '']"
:class="[
isMarked === view.id ? 'bg-gray-200' : '',
route.params.viewTitle && route.params.viewTitle.includes(view.title) ? 'active' : '',
]"
@change-view="changeView"
@open-modal="$emit('openModal', $event)"
@delete="onDelete"
@ -206,7 +210,7 @@ function onDeleted() {
<style lang="scss">
.nc-views-menu {
@apply flex-1 max-h-[20vh] overflow-y-scroll scrollbar-thin-primary;
@apply flex-1 max-h-[30vh] overflow-y-scroll scrollbar-thin-dull;
.ghost,
.ghost > * {
@ -230,5 +234,13 @@ function onDeleted() {
.sortable-chosen {
@apply !bg-primary/25 text-primary;
}
.active {
@apply bg-blue-500/15;
.nc-icon {
@apply !text-pink-500;
}
}
}
</style>

3
packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -2,9 +2,6 @@
import type { ViewTypes } from 'nocodb-sdk'
import { viewIcons } from '~/utils'
import { useDebounceFn, useNuxtApp, useVModel } from '#imports'
import MdiTrashCan from '~icons/mdi/trash-can'
import MdiContentCopy from '~icons/mdi/content-copy'
import MdiDrag from '~icons/mdi/drag-vertical'
interface Props {
view: Record<string, any>

25
packages/nc-gui-v2/components/smartsheet/sidebar/index.vue

@ -5,8 +5,6 @@ import MenuBottom from './MenuBottom.vue'
import Toolbar from './toolbar/index.vue'
import { computed, inject, provide, ref, useApi, useRoute, useViews, watch } from '#imports'
import { ActiveViewInj, MetaInj, RightSidebarInj, ViewListInj } from '~/context'
import MdiXml from '~icons/mdi/xml'
import MdiHook from '~icons/mdi/hook'
const meta = inject(MetaInj, ref())
@ -73,10 +71,29 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
collapsiple
collapsed-width="50"
width="250"
class="shadow !mt-[-9px]"
style="height: calc(100% + 9px)"
class="relative shadow-md h-full"
theme="light"
>
<a-tooltip placement="left">
<template #title> Toggle sidebar </template>
<div
class="group color-transition cursor-pointer hover:ring active:ring-pink-500 z-1 flex items-center p-[1px] absolute top-1/2 left-[-1rem] shadow bg-gray-100 rounded-full"
>
<MaterialSymbolsChevronRightRounded
v-if="sidebarOpen"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
@click="sidebarOpen = false"
/>
<MaterialSymbolsChevronLeftRounded
v-else
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
@click="sidebarOpen = true"
/>
</div>
</a-tooltip>
<Toolbar v-if="sidebarOpen" class="flex items-center py-3 px-3 justify-between border-b-1" />
<Toolbar v-else class="py-3 px-2 max-w-[50px] flex !flex-col-reverse gap-4 items-center mt-[-1px]">

3
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/AddRow.vue

@ -1,5 +1,4 @@
<script setup lang="ts">
import MdiAddIcon from '~icons/mdi/plus-outline'
import { inject, ref } from '#imports'
import { RightSidebarInj } from '~/context'
@ -13,7 +12,7 @@ const sidebarOpen = inject(RightSidebarInj, ref(true))
<template #title> {{ $t('activity.addRow') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-primary/75 group">
<MdiAddIcon class="group-hover:(!text-white)" @click="emits('addRow')" />
<MdiPlusOutline class="group-hover:(!text-white)" @click="emits('addRow')" />
</div>
</a-tooltip>
</template>

7
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue

@ -5,7 +5,6 @@ import { extractSdkResponseErrorMsg } from '~/utils'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
import MdiAccountIcon from '~icons/mdi/account'
import MdiAccountGroupIcon from '~icons/mdi/account-group'
import MdiCheckIcon from '~icons/mdi/check-bold'
enum LockType {
Personal = 'personal',
@ -59,7 +58,7 @@ const Icon = computed(() => {
<div>
<div class="nc-menu-item" @click="changeLockType(LockType.Collaborative)">
<div>
<MdiCheckIcon v-if="!view?.lock_type || view?.lock_type === LockType.Collaborative" />
<MdiCheck v-if="!view?.lock_type || view?.lock_type === LockType.Collaborative" />
<span v-else />
<div>
<MdiAccountGroupIcon />
@ -70,7 +69,7 @@ const Icon = computed(() => {
</div>
<div class="nc-menu-item" @click="changeLockType(LockType.Locked)">
<div>
<MdiCheckIcon v-if="view.lock_type === LockType.Locked" />
<MdiCheck v-if="view.lock_type === LockType.Locked" />
<span v-else />
<div>
<MdiLockOutlineIcon />
@ -81,7 +80,7 @@ const Icon = computed(() => {
</div>
<div class="nc-menu-item" @click="changeLockType(LockType.Personal)">
<div>
<MdiCheckIcon v-if="view.lock_type === LockType.Personal" />
<MdiCheck v-if="view.lock_type === LockType.Personal" />
<span v-else />
<div>
<MdiAccountIcon />

19
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue

@ -1,19 +0,0 @@
<script setup lang="ts">
import MdiUnfoldMoreVertical from '~icons/mdi/unfold-more-vertical'
import MdiUnfoldLessVertical from '~icons/mdi/unfold-less-vertical'
import { inject, ref } from '#imports'
import { RightSidebarInj } from '~/context'
const sidebarOpen = inject(RightSidebarInj, ref(false))
</script>
<template>
<a-tooltip :placement="sidebarOpen ? 'bottomRight' : 'left'">
<template #title> {{ $t('tooltip.toggleNavDraw') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-pink-500 group">
<MdiUnfoldLessVertical v-if="sidebarOpen" class="group-hover:(!text-white)" @click="sidebarOpen = false" />
<MdiUnfoldMoreVertical v-else class="group-hover:(!text-white)" @click="sidebarOpen = true" />
</div>
</a-tooltip>
</template>

5
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue

@ -3,7 +3,6 @@ import AddRow from './AddRow.vue'
import DeleteTable from './DeleteTable.vue'
import LockMenu from './LockMenu.vue'
import Reload from './Reload.vue'
import ToggleDrawer from './ToggleDrawer.vue'
</script>
<template>
@ -24,10 +23,6 @@ import ToggleDrawer from './ToggleDrawer.vue'
<DeleteTable />
<div class="dot" />
<ToggleDrawer />
<slot name="end" />
</div>
</template>

2
packages/nc-gui-v2/components/tabs/Smartsheet.vue

@ -60,7 +60,7 @@ watch(tabMeta, async (newTabMeta, oldTabMeta) => {
</div>
</div>
<teleport to="#sidebar-right">
<teleport to="#content">
<SmartsheetSidebar />
</teleport>
</template>

1
packages/nc-gui-v2/composables/index.ts

@ -1,6 +1,7 @@
export * from './useApi'
export * from './useGlobal'
export * from './useInjectionState'
export * from './useSidebar'
export * from './useUIPermission'
export * from './useAttachment'
export * from './useColors'

4
packages/nc-gui-v2/composables/useGlobal/state.ts

@ -74,9 +74,6 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
/** reactive token payload */
const { payload } = useJwt<JwtPayload & User>(token)
/** is sidebar open */
const sidebarOpen = ref(false)
/** currently running requests */
const runningRequests = useCounter()
@ -88,7 +85,6 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
storage,
token,
jwtPayload: payload,
sidebarOpen,
timestamp,
runningRequests,
error,

1
packages/nc-gui-v2/composables/useGlobal/types.ts

@ -24,7 +24,6 @@ export type State = ToRefs<Omit<StoredState, 'token'>> & {
storage: Ref<StoredState>
token: WritableComputedRef<StoredState['token']>
jwtPayload: ComputedRef<(JwtPayload & User) | null>
sidebarOpen: Ref<boolean>
timestamp: Ref<number>
runningRequests: ReturnType<typeof useCounter>
error: Ref<any>

2
packages/nc-gui-v2/composables/useInjectionState/index.ts

@ -14,7 +14,7 @@ export function useInjectionState<Arguments extends any[], Return>(
return providedState
}
const useInjectedState = () => inject(key)
const useInjectedState = () => inject(key, undefined)
return [useProvidingState, useInjectedState]
}

58
packages/nc-gui-v2/composables/useSidebar/index.ts

@ -0,0 +1,58 @@
import { useInjectionState, useToggle, watch } from '#imports'
interface UseSidebarProps {
hasSidebar?: boolean
isOpen?: boolean
}
/**
* Injection state for sidebars
*
* Use `provideSidebar` to provide the injection state on current component level (will affect all children injections)
* Use `useSidebar` to use the injection state on current component level
*
* If `provideSidebar` is not called explicitly, `useSidebar` will trigger the provider if no injection state can be found
*/
const [setup, use] = useInjectionState((props: UseSidebarProps = {}) => {
const [isOpen, toggle] = useToggle(props.isOpen ?? false)
const [hasSidebar, toggleHasSidebar] = useToggle(props.hasSidebar ?? true)
watch(
hasSidebar,
(nextHasSidebar) => {
if (!nextHasSidebar) toggle(false)
},
{ immediate: true },
)
watch(
isOpen,
(nextIsOpen) => {
if (nextIsOpen && !hasSidebar.value) toggleHasSidebar(true)
},
{ immediate: true },
)
return {
isOpen,
toggle,
hasSidebar,
toggleHasSidebar,
}
}, 'useSidebar')
export const provideSidebar = setup
export function useSidebar(props: UseSidebarProps = {}) {
const state = use()
if (!state) {
return setup(props)
} else {
// set state if props were passed
if (typeof props.isOpen !== 'undefined') state.isOpen.value = props.isOpen
if (typeof props.hasSidebar !== 'undefined') state.hasSidebar.value = props.hasSidebar
}
return state
}

101
packages/nc-gui-v2/layouts/base.vue

@ -0,0 +1,101 @@
<script lang="ts" setup>
import { breakpointsTailwind } from '@vueuse/core'
import { navigateTo } from '#app'
import { computed, useBreakpoints, useGlobal, useProject, useRoute } from '#imports'
/** get current breakpoints (for enabling sidebar) */
const breakpoints = useBreakpoints(breakpointsTailwind)
const { signOut, signedIn, isLoading, user } = useGlobal()
const { project } = useProject()
const route = useRoute()
const email = computed(() => user.value?.email ?? '---')
const logout = () => {
signOut()
navigateTo('/signin')
}
</script>
<template>
<a-layout id="nc-app" has-sider>
<div id="nc-sidebar-left" />
<a-layout class="!flex-col">
<a-layout-header class="flex !bg-primary items-center text-white pl-1 pr-4 shadow-lg">
<div
v-if="route.name === 'index'"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105"
@click="navigateTo('/')"
>
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<div class="flex justify-center">
<div v-show="isLoading" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
</div>
</div>
<div class="flex-1" />
<a-tooltip placement="left">
<template #title> Switch language </template>
<div class="flex pr-4 items-center">
<GeneralLanguage class="cursor-pointer text-2xl" />
</div>
</a-tooltip>
<template v-if="signedIn">
<a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />
<template #overlay>
<a-menu class="!py-0 nc-user-menu dark:(!bg-gray-800) leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" />&nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout">
<MdiLogout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-layout-header>
<div class="w-full" style="height: calc(100% - var(--header-height))">
<slot />
</div>
</a-layout>
</a-layout>
</template>
<style lang="scss" scoped>
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
</style>

10
packages/nc-gui-v2/layouts/default.vue

@ -18,11 +18,13 @@ export default {
</script>
<template>
<a-layout-content class="pl-2 pt-2">
<teleport v-if="$slots.sidebar" to="#sidebar">
<div class="w-full h-full">
<teleport v-if="$slots.sidebar" to="#nc-sidebar-left">
<slot name="sidebar" />
</teleport>
<slot />
</a-layout-content>
<a-layout-content>
<slot />
</a-layout-content>
</div>
</template>

4
packages/nc-gui-v2/nuxt.config.ts

@ -101,4 +101,8 @@ export default defineNuxtConfig({
},
},
},
image: {
dir: 'assets/',
},
})

199
packages/nc-gui-v2/package-lock.json generated

@ -55,7 +55,7 @@
"happy-dom": "^6.0.3",
"less": "^4.1.3",
"nuxt": "3.0.0-rc.4",
"nuxt-windicss": "^2.4.2",
"nuxt-windicss": "^2.5.0",
"prettier": "^2.7.1",
"sass": "^1.53.0",
"unplugin-icons": "^0.14.7",
@ -3264,9 +3264,9 @@
}
},
"node_modules/@windicss/config": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.8.6.tgz",
"integrity": "sha512-WVS41qUJtd44g2iWzTAE8tpgk8gD0yAr1RwwaWi7FAECKm3LVNMLOoToum9R/QKFE2n64EUVJpIvSUNby8rlhg==",
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.8.7.tgz",
"integrity": "sha512-8n+/Y36j5L3rw2tgMdLjeGRuNV7VYfKoHoraLK6Bk9OJ1MTPd5vv7pekof/uOPWVV7WWjVeZ6CTO8SDbDDW3iw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
@ -3296,13 +3296,13 @@
"dev": true
},
"node_modules/@windicss/plugin-utils": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.8.6.tgz",
"integrity": "sha512-YY6EcUsgkosaQkIseFiIoHfU1H5boOAs/l74QWLI6ryNeHLMq2e04QVsFz+Rt+U8b8PRNxXPC8ADbxE05X7I7g==",
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.8.7.tgz",
"integrity": "sha512-dfj95olNZyGFDPFMBvE5oq8hA5f0ooUJZjVdWlthS4ek4W1/xNOHDxB6ygWR8LE9zCOXZykApjt1LOhy9Ky2QA==",
"dev": true,
"dependencies": {
"@antfu/utils": "^0.5.2",
"@windicss/config": "1.8.6",
"@windicss/config": "1.8.7",
"debug": "^4.3.4",
"fast-glob": "^3.2.11",
"magic-string": "^0.26.2",
@ -7640,15 +7640,15 @@
}
},
"node_modules/h3": {
"version": "0.7.10",
"resolved": "https://registry.npmjs.org/h3/-/h3-0.7.10.tgz",
"integrity": "sha512-HcUQxlOocG2uf6PQLu3Bxk0KSXcTuxSF6kRclFBDO7y74Ml0wCwTvmoN5zmrzoxbNHecDNUrcGr8qSNGcOqAHQ==",
"version": "0.7.13",
"resolved": "https://registry.npmjs.org/h3/-/h3-0.7.13.tgz",
"integrity": "sha512-3rs+iokAwin4GnToWc+4JeASAYPujojMWicaTgv5WMucED94cFVVGctlk7N6iJfQ7SZ2N5CtmQ2C3L2aDFpd2w==",
"dev": true,
"dependencies": {
"cookie-es": "^0.5.0",
"destr": "^1.1.1",
"radix3": "^0.1.2",
"ufo": "^0.8.4"
"ufo": "^0.8.5"
}
},
"node_modules/happy-dom": {
@ -9085,9 +9085,9 @@
}
},
"node_modules/lilconfig": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz",
"integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz",
"integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==",
"dev": true,
"engines": {
"node": ">=10"
@ -10144,54 +10144,26 @@
}
},
"node_modules/nuxt-windicss": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/nuxt-windicss/-/nuxt-windicss-2.4.2.tgz",
"integrity": "sha512-coHvXGw4CqkRqG47aJqNKbQKMO17ZGXFVyeOKGaRnvsS4FcdfKtYKukpDgJm0sCIG57z8CljfX3nBy71+vu3mg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/nuxt-windicss/-/nuxt-windicss-2.5.0.tgz",
"integrity": "sha512-h5Wh1Pj5OrYvbQmfarOxi6I366xyPxYVlUIZhu5OPGn0uUaoQIOl5ASR1QD3eR/BH3kdpM/r8wyjikDoW7oSbg==",
"dev": true,
"dependencies": {
"@nuxt/kit": "3.0.0-rc.3",
"@windicss/plugin-utils": "^1.8.4",
"@nuxt/kit": "3.0.0-rc.6",
"@windicss/config": "^1.8.7",
"@windicss/plugin-utils": "^1.8.7",
"consola": "^2.15.3",
"defu": "^6.0.0",
"fs-extra": "^10.1.0",
"h3": "^0.7.8",
"listhen": "^0.2.11",
"pathe": "^0.3.0",
"h3": "^0.7.12",
"listhen": "^0.2.13",
"pathe": "^0.3.2",
"read-cache": "^1.0.0",
"sirv": "^2.0.2",
"vite-plugin-windicss": "^1.8.4",
"windicss": "^3.5.4",
"vite-plugin-windicss": "^1.8.7",
"windicss": "^3.5.6",
"windicss-analysis": "^0.3.5",
"windicss-webpack-plugin": "^1.7.3"
}
},
"node_modules/nuxt-windicss/node_modules/@nuxt/kit": {
"version": "3.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.0.0-rc.3.tgz",
"integrity": "sha512-aD993HKXZ76cwxkM2LCwWRQaOI3RjLCg/kj+8ZS6qN4VrpwrZp4xM59YYIYs3/Vze4OG3D4+0gr0u5zdgf8v8g==",
"dev": true,
"dependencies": {
"@nuxt/schema": "^3.0.0-rc.3",
"c12": "^0.2.7",
"consola": "^2.15.3",
"defu": "^6.0.0",
"globby": "^13.1.1",
"hash-sum": "^2.0.0",
"ignore": "^5.2.0",
"jiti": "^1.13.0",
"knitwork": "^0.1.1",
"lodash.template": "^4.5.0",
"mlly": "^0.5.2",
"pathe": "^0.3.0",
"pkg-types": "^0.3.2",
"scule": "^0.2.1",
"semver": "^7.3.7",
"unctx": "^1.1.4",
"unimport": "^0.1.9",
"untyped": "^0.4.4"
},
"engines": {
"node": "^14.16.0 || ^16.11.0 || ^17.0.0 || ^18.0.0"
"windicss-webpack-plugin": "^1.7.5"
}
},
"node_modules/nuxt/node_modules/unimport": {
@ -14004,12 +13976,12 @@
}
},
"node_modules/vite-plugin-windicss": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.8.6.tgz",
"integrity": "sha512-D4G4qmumgklPiPrq/ZALqq8Mby6krskFVShbmb5c+0VCSsLUN96qyFRTwi81rNIHwFvlbpqflgh+BpUM/9VjQg==",
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.8.7.tgz",
"integrity": "sha512-/zwQ8+RV+MSkbG0IGqsEma6r2R01NzN/aNpNjJD7VVAkxAptNznqDXOObFTskkWfZ+9m6KJZCOuCPgAFtQIzEA==",
"dev": true,
"dependencies": {
"@windicss/plugin-utils": "1.8.6",
"@windicss/plugin-utils": "1.8.7",
"debug": "^4.3.4",
"kolorist": "^1.5.1",
"windicss": "^3.5.6"
@ -14018,7 +13990,7 @@
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vite": "^2.0.1"
"vite": "^2.0.1 || ^3.0.0"
}
},
"node_modules/vitest": {
@ -14580,19 +14552,19 @@
}
},
"node_modules/windicss-webpack-plugin": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/windicss-webpack-plugin/-/windicss-webpack-plugin-1.7.3.tgz",
"integrity": "sha512-vESTMEUqzNlvOmnOCCxeqDkj1q2u69FfKMQHyTF9Y9tLJbWttEHgt+qSJZ3cYndUHRxbz06INanEyL08dQr79A==",
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/windicss-webpack-plugin/-/windicss-webpack-plugin-1.7.5.tgz",
"integrity": "sha512-+DwZwFcjgYrD/UU1UEs1TOTo4ijAwRkXEgJMttPd05NSC3ULvguvNKL5kNxrCTYs4OMJn68qbCfkjpI0mir4cQ==",
"dev": true,
"dependencies": {
"@windicss/plugin-utils": "^1.8.4",
"@windicss/plugin-utils": "^1.8.7",
"debug": "^4.3.4",
"get-port": "^6.1.2",
"loader-utils": "^2.0.0",
"lodash": "^4.17.21",
"pathe": "^0.2.0",
"webpack-virtual-modules": "^0.4.3",
"windicss": "^3.5.4"
"windicss": "^3.5.6"
}
},
"node_modules/windicss-webpack-plugin/node_modules/pathe": {
@ -17300,9 +17272,9 @@
}
},
"@windicss/config": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.8.6.tgz",
"integrity": "sha512-WVS41qUJtd44g2iWzTAE8tpgk8gD0yAr1RwwaWi7FAECKm3LVNMLOoToum9R/QKFE2n64EUVJpIvSUNby8rlhg==",
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.8.7.tgz",
"integrity": "sha512-8n+/Y36j5L3rw2tgMdLjeGRuNV7VYfKoHoraLK6Bk9OJ1MTPd5vv7pekof/uOPWVV7WWjVeZ6CTO8SDbDDW3iw==",
"dev": true,
"requires": {
"debug": "^4.3.4",
@ -17329,13 +17301,13 @@
"dev": true
},
"@windicss/plugin-utils": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.8.6.tgz",
"integrity": "sha512-YY6EcUsgkosaQkIseFiIoHfU1H5boOAs/l74QWLI6ryNeHLMq2e04QVsFz+Rt+U8b8PRNxXPC8ADbxE05X7I7g==",
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.8.7.tgz",
"integrity": "sha512-dfj95olNZyGFDPFMBvE5oq8hA5f0ooUJZjVdWlthS4ek4W1/xNOHDxB6ygWR8LE9zCOXZykApjt1LOhy9Ky2QA==",
"dev": true,
"requires": {
"@antfu/utils": "^0.5.2",
"@windicss/config": "1.8.6",
"@windicss/config": "1.8.7",
"debug": "^4.3.4",
"fast-glob": "^3.2.11",
"magic-string": "^0.26.2",
@ -20489,15 +20461,15 @@
}
},
"h3": {
"version": "0.7.10",
"resolved": "https://registry.npmjs.org/h3/-/h3-0.7.10.tgz",
"integrity": "sha512-HcUQxlOocG2uf6PQLu3Bxk0KSXcTuxSF6kRclFBDO7y74Ml0wCwTvmoN5zmrzoxbNHecDNUrcGr8qSNGcOqAHQ==",
"version": "0.7.13",
"resolved": "https://registry.npmjs.org/h3/-/h3-0.7.13.tgz",
"integrity": "sha512-3rs+iokAwin4GnToWc+4JeASAYPujojMWicaTgv5WMucED94cFVVGctlk7N6iJfQ7SZ2N5CtmQ2C3L2aDFpd2w==",
"dev": true,
"requires": {
"cookie-es": "^0.5.0",
"destr": "^1.1.1",
"radix3": "^0.1.2",
"ufo": "^0.8.4"
"ufo": "^0.8.5"
}
},
"happy-dom": {
@ -21557,9 +21529,9 @@
}
},
"lilconfig": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz",
"integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz",
"integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==",
"dev": true
},
"lines-and-columns": {
@ -22461,53 +22433,26 @@
}
},
"nuxt-windicss": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/nuxt-windicss/-/nuxt-windicss-2.4.2.tgz",
"integrity": "sha512-coHvXGw4CqkRqG47aJqNKbQKMO17ZGXFVyeOKGaRnvsS4FcdfKtYKukpDgJm0sCIG57z8CljfX3nBy71+vu3mg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/nuxt-windicss/-/nuxt-windicss-2.5.0.tgz",
"integrity": "sha512-h5Wh1Pj5OrYvbQmfarOxi6I366xyPxYVlUIZhu5OPGn0uUaoQIOl5ASR1QD3eR/BH3kdpM/r8wyjikDoW7oSbg==",
"dev": true,
"requires": {
"@nuxt/kit": "3.0.0-rc.3",
"@windicss/plugin-utils": "^1.8.4",
"@nuxt/kit": "3.0.0-rc.6",
"@windicss/config": "^1.8.7",
"@windicss/plugin-utils": "^1.8.7",
"consola": "^2.15.3",
"defu": "^6.0.0",
"fs-extra": "^10.1.0",
"h3": "^0.7.8",
"listhen": "^0.2.11",
"pathe": "^0.3.0",
"h3": "^0.7.12",
"listhen": "^0.2.13",
"pathe": "^0.3.2",
"read-cache": "^1.0.0",
"sirv": "^2.0.2",
"vite-plugin-windicss": "^1.8.4",
"windicss": "^3.5.4",
"vite-plugin-windicss": "^1.8.7",
"windicss": "^3.5.6",
"windicss-analysis": "^0.3.5",
"windicss-webpack-plugin": "^1.7.3"
},
"dependencies": {
"@nuxt/kit": {
"version": "3.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.0.0-rc.3.tgz",
"integrity": "sha512-aD993HKXZ76cwxkM2LCwWRQaOI3RjLCg/kj+8ZS6qN4VrpwrZp4xM59YYIYs3/Vze4OG3D4+0gr0u5zdgf8v8g==",
"dev": true,
"requires": {
"@nuxt/schema": "^3.0.0-rc.3",
"c12": "^0.2.7",
"consola": "^2.15.3",
"defu": "^6.0.0",
"globby": "^13.1.1",
"hash-sum": "^2.0.0",
"ignore": "^5.2.0",
"jiti": "^1.13.0",
"knitwork": "^0.1.1",
"lodash.template": "^4.5.0",
"mlly": "^0.5.2",
"pathe": "^0.3.0",
"pkg-types": "^0.3.2",
"scule": "^0.2.1",
"semver": "^7.3.7",
"unctx": "^1.1.4",
"unimport": "^0.1.9",
"untyped": "^0.4.4"
}
}
"windicss-webpack-plugin": "^1.7.5"
}
},
"nwsapi": {
@ -25248,12 +25193,12 @@
}
},
"vite-plugin-windicss": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.8.6.tgz",
"integrity": "sha512-D4G4qmumgklPiPrq/ZALqq8Mby6krskFVShbmb5c+0VCSsLUN96qyFRTwi81rNIHwFvlbpqflgh+BpUM/9VjQg==",
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.8.7.tgz",
"integrity": "sha512-/zwQ8+RV+MSkbG0IGqsEma6r2R01NzN/aNpNjJD7VVAkxAptNznqDXOObFTskkWfZ+9m6KJZCOuCPgAFtQIzEA==",
"dev": true,
"requires": {
"@windicss/plugin-utils": "1.8.6",
"@windicss/plugin-utils": "1.8.7",
"debug": "^4.3.4",
"kolorist": "^1.5.1",
"windicss": "^3.5.6"
@ -25656,19 +25601,19 @@
}
},
"windicss-webpack-plugin": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/windicss-webpack-plugin/-/windicss-webpack-plugin-1.7.3.tgz",
"integrity": "sha512-vESTMEUqzNlvOmnOCCxeqDkj1q2u69FfKMQHyTF9Y9tLJbWttEHgt+qSJZ3cYndUHRxbz06INanEyL08dQr79A==",
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/windicss-webpack-plugin/-/windicss-webpack-plugin-1.7.5.tgz",
"integrity": "sha512-+DwZwFcjgYrD/UU1UEs1TOTo4ijAwRkXEgJMttPd05NSC3ULvguvNKL5kNxrCTYs4OMJn68qbCfkjpI0mir4cQ==",
"dev": true,
"requires": {
"@windicss/plugin-utils": "^1.8.4",
"@windicss/plugin-utils": "^1.8.7",
"debug": "^4.3.4",
"get-port": "^6.1.2",
"loader-utils": "^2.0.0",
"lodash": "^4.17.21",
"pathe": "^0.2.0",
"webpack-virtual-modules": "^0.4.3",
"windicss": "^3.5.4"
"windicss": "^3.5.6"
},
"dependencies": {
"pathe": {

2
packages/nc-gui-v2/package.json

@ -61,7 +61,7 @@
"happy-dom": "^6.0.3",
"less": "^4.1.3",
"nuxt": "3.0.0-rc.4",
"nuxt-windicss": "^2.4.2",
"nuxt-windicss": "^2.5.0",
"prettier": "^2.7.1",
"sass": "^1.53.0",
"unplugin-icons": "^0.14.7",

36
packages/nc-gui-v2/pages/index/index.vue

@ -3,7 +3,7 @@ import { Modal } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { navigateTo } from '#app'
import { computed, onMounted } from '#imports'
import { computed, onMounted, ref, useApi, useNuxtApp, useSidebar } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
import MdiDeleteOutline from '~icons/mdi/delete-outline'
import MdiEditOutline from '~icons/mdi/edit-outline'
@ -12,30 +12,31 @@ import MdiMenuDown from '~icons/mdi/menu-down'
import MdiPlus from '~icons/mdi/plus'
import MdiDatabaseOutline from '~icons/mdi/database-outline'
const { $api, $state, $e } = useNuxtApp()
const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
useSidebar({ hasSidebar: true, isOpen: true })
const toast = useToast()
const filterQuery = ref('')
const loading = ref(true)
const projects = ref<ProjectType[]>()
const loadProjects = async () => {
loading.value = true
const response = await $api.project.list({})
const response = await api.project.list({})
projects.value = response.list
loading.value = false
}
const filteredProjects = computed(() => {
return (
const filteredProjects = computed(
() =>
projects.value?.filter(
(project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()),
) ?? []
)
})
) ?? [],
)
const deleteProject = (project: ProjectType) => {
$e('c:project:delete')
Modal.confirm({
title: `Do you want to delete '${project.title}' project?`,
okText: 'Yes',
@ -45,9 +46,9 @@ const deleteProject = (project: ProjectType) => {
try {
$e('c:project:delete')
await $api.project.delete(project.id as string)
projects.value?.splice(projects.value.indexOf(project), 1)
} catch (e) {
toast.error(await extractSdkResponseErrorMsg(e))
return projects.value?.splice(projects.value.indexOf(project), 1)
} catch (e: any) {
return toast.error(await extractSdkResponseErrorMsg(e))
}
},
})
@ -56,9 +57,6 @@ const deleteProject = (project: ProjectType) => {
onMounted(() => {
loadProjects()
})
// hide sidebar
$state.sidebarOpen.value = false
</script>
<template>
@ -114,7 +112,7 @@ $state.sidebarOpen.value = false
</a-dropdown>
</div>
<div v-if="loading">
<div v-if="isLoading">
<a-skeleton />
</div>

40
packages/nc-gui-v2/pages/index/user/index.vue

@ -6,50 +6,10 @@ import MdiFolderOutline from '~icons/mdi/folder-outline'
const { $api } = useNuxtApp()
const route = useRoute()
const navDrawerOptions = [
{
title: 'My NocoDB',
route: '/',
icon: MdiFolderOutline,
},
{
title: 'Settings',
route: '/user',
icon: MdiAccountCog,
},
]
const selectedKey = computed(() => [navDrawerOptions.findIndex((opt) => opt.route === route.path)])
</script>
<template>
<NuxtLayout>
<template #sidebar>
<div class="flex flex-col h-full">
<a-menu :selected-keys="selectedKey" class="pr-4 dark:bg-gray-800 dark:text-white flex-1 border-0">
<a-menu-item
v-for="(option, index) in navDrawerOptions"
:key="index"
class="!rounded-r-lg"
@click="navigateTo(option.route)"
>
<div class="flex items-center gap-4">
<component :is="option.icon" />
<span class="font-semibold">
{{ option.title }}
</span>
</div>
</a-menu-item>
</a-menu>
<general-social />
<general-sponsors :nav="true" />
</div>
</template>
<NuxtPage />
</NuxtLayout>
</template>

106
packages/nc-gui-v2/pages/index/user/index/index.vue

@ -1,13 +1,12 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { useNuxtApp } from '#app'
import { reactive, ref } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
import { reactive, ref, useApi } from '#imports'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import MdiKeyChange from '~icons/mdi/key-change'
const { $api, $state } = useNuxtApp()
const { api, isLoading } = useApi()
const { t } = useI18n()
@ -52,7 +51,7 @@ const passwordChange = async () => {
error = null
try {
const { msg } = await $api.auth.passwordChange({
const { msg } = await api.auth.passwordChange({
currentPassword: form.currentPassword,
newPassword: form.password,
})
@ -70,60 +69,61 @@ const resetError = () => {
</script>
<template>
<a-form ref="formValidator" layout="vertical" :model="form" class="change-password h-full w-full" @finish="passwordChange">
<div
class="md:relative flex flex-col gap-2 w-full h-full p-8 lg:(max-w-1/2)"
:class="{ 'mx-auto': !$state.sidebarOpen.value }"
>
<h1 class="prose-2xl font-bold mb-4">{{ $t('activity.changePwd') }}</h1>
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div class="flex items-center gap-2 justify-center"><MaterialSymbolsWarning /> {{ error }}</div>
<div class="mt-4 w-1/2 mx-auto">
<a-form ref="formValidator" layout="vertical" :model="form" class="change-password" @finish="passwordChange">
<div class="md:relative flex flex-col gap-2 w-full h-full p-8 w-full">
<h1 class="prose-2xl font-bold mb-4">{{ $t('activity.changePwd') }}</h1>
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div class="flex items-center gap-2 justify-center"><MaterialSymbolsWarning /> {{ error }}</div>
</div>
</Transition>
<a-form-item :label="$t('placeholder.password.current')" name="currentPassword" :rules="formRules.currentPassword">
<a-input-password
v-model:value="form.currentPassword"
size="large"
class="password"
:placeholder="$t('placeholder.password.current')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.new')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
size="large"
class="password"
:placeholder="$t('placeholder.password.new')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.confirm')" name="passwordRepeat" :rules="formRules.passwordRepeat">
<a-input-password
v-model:value="form.passwordRepeat"
size="large"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@focus="resetError"
/>
</a-form-item>
<div class="flex flex-wrap gap-4 items-center mt-4 md:justify-between w-full">
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiKeyChange /> {{ $t('activity.changePwd') }}</span>
</button>
</div>
</Transition>
<a-form-item :label="$t('placeholder.password.current')" name="currentPassword" :rules="formRules.currentPassword">
<a-input-password
v-model:value="form.currentPassword"
size="large"
class="password"
:placeholder="$t('placeholder.password.current')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.new')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
size="large"
class="password"
:placeholder="$t('placeholder.password.new')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.confirm')" name="passwordRepeat" :rules="formRules.passwordRepeat">
<a-input-password
v-model:value="form.passwordRepeat"
size="large"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@focus="resetError"
/>
</a-form-item>
<div class="flex flex-wrap gap-4 items-center mt-4 md:justify-between w-full">
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiKeyChange /> {{ $t('activity.changePwd') }}</span>
</button>
</div>
</div>
</a-form>
</a-form>
</div>
</template>
<style lang="scss">
.change-password {
@apply border-1 shadow-md rounded;
.ant-input-affix-wrapper,
.ant-input {
@apply dark:(!bg-gray-700 !text-white) !appearance-none my-1 border-1 border-solid border-primary/50 rounded;

219
packages/nc-gui-v2/pages/nc/[projectId]/index.vue

@ -1,29 +1,234 @@
<script setup lang="ts">
import { useTabs } from '#imports'
import { navigateTo, provideSidebar, ref, useProject, useRoute, useSidebar, useTabs, useUIPermission } from '#imports'
import { TabType } from '~/composables'
import { openLink } from '~/utils'
const route = useRoute()
const { loadProject, loadTables } = useProject(route.params.projectId as string)
const { project, loadProject, loadTables } = useProject(route.params.projectId as string)
const { addTab, clearTabs } = useTabs()
const { $state } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
// set old sidebar state
useSidebar({ isOpen: true })
// create a new sidebar state
const { isOpen, toggle } = provideSidebar({ isOpen: true })
const dialogOpen = ref(false)
const openDialogKey = ref<string>()
clearTabs()
if (!route.params.type) {
addTab({ type: TabType.AUTH, title: 'Team & Auth' })
}
function toggleDialog(value?: boolean, key?: string) {
dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key
}
await loadProject(route.params.projectId as string)
await loadTables()
$state.sidebarOpen.value = true
await loadTables()
</script>
<template>
<NuxtLayout>
<NuxtLayout id="content" class="flex">
<template #sidebar>
<DashboardTreeView />
<a-layout-sider
:collapsed="!isOpen"
width="250"
collapsed-width="50"
class="relative shadow-md h-full z-1"
:trigger="null"
collapsible
theme="light"
>
<div style="height: var(--header-height)" class="flex items-center !bg-primary text-white px-1 gap-2">
<div
v-if="isOpen"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105"
@click="navigateTo('/')"
>
<img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<a-dropdown :trigger="['click']">
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"
class="group cursor-pointer flex gap-4 items-center"
>
<template v-if="isOpen">
<div class="text-xl font-semibold truncate">{{ project.title }}</div>
<MdiChevronDown class="min-w-[28.5px] group-hover:text-pink-500 text-2xl" />
</template>
<template v-else>
<MdiFolder class="text-primary cursor-pointer transform hover:scale-105 text-2xl" />
</template>
</div>
<template #overlay>
<a-menu class="ml-6 !w-[300px] !text-sm !p-0 !rounded">
<a-menu-item-group>
<template #title>
<div class="group select-none flex items-center gap-4 py-1">
<MdiFolder class="group-hover:text-pink-500 text-xl" />
<div class="flex flex-col">
<div class="text-lg group-hover:(!text-primary) font-semibold truncate">{{ project.title }}</div>
<div class="text-xs group-hover:text-pink-500 truncate font-italic">{{ project.id }}</div>
</div>
</div>
</template>
<a-menu-item key="copy">
<div class="nc-project-menu-item group">
<MdiContentCopy class="group-hover:text-pink-500" />
Copy Project Info
</div>
</a-menu-item>
<a-menu-item key="api">
<div
v-if="isUIAllowed('apiDocs')"
v-t="['e:api-docs']"
class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`)"
>
<MdiApi class="group-hover:text-pink-500" />
Swagger: Rest APIs
</div>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="teamAndAuth">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')"
>
<MdiAccountGroup class="group-hover:text-pink-500" />
Team & Auth
</div>
</a-menu-item>
<a-menu-item key="appStore">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'appStore')"
>
<MdiStore class="group-hover:text-pink-500" />
App Store
</div>
</a-menu-item>
<a-menu-item key="metaData">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'metaData')"
>
<MdiTableBorder class="group-hover:text-pink-500" />
Project Metadata
</div>
</a-menu-item>
<a-menu-item key="audit">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'audit')"
>
<MdiNotebookCheckOutline class="group-hover:text-pink-500" />
Audit
</div>
</a-menu-item>
<a-menu-divider />
<a-sub-menu key="preview-as">
<template #title>
<div class="nc-project-menu-item group">
<MdiContentCopy class="group-hover:text-pink-500" />
Preview Project As
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-menu-item> Foo </a-menu-item>
</a-sub-menu>
</a-menu-item-group>
</a-menu>
</template>
</a-dropdown>
</div>
<a-tooltip placement="right">
<template #title> Toggle table list </template>
<div
class="group color-transition cursor-pointer hover:ring active:ring-pink-500 z-1 flex items-center absolute top-1/2 right-[-0.75rem] shadow bg-gray-100 rounded-full"
>
<MaterialSymbolsChevronLeftRounded
v-if="isOpen"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
@click="toggle(false)"
/>
<MaterialSymbolsChevronRightRounded
v-else
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
@click="toggle(true)"
/>
</div>
</a-tooltip>
<DashboardTreeView v-show="isOpen" />
</a-layout-sider>
</template>
<dashboard-settings-modal v-model="dialogOpen" :open-key="openDialogKey" />
<NuxtPage />
</NuxtLayout>
</template>
<style lang="scss" scoped>
.nc-project-menu-item {
@apply cursor-pointer flex items-center gap-2 py-2 hover:text-primary after:(content-[''] absolute top-0 left-0 bottom-0 right-0 w-full h-full bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
</style>

21
packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue

@ -34,9 +34,9 @@ function openQuickImportDialog(type: string) {
</script>
<template>
<div class="flex w-full h-full">
<div class="nc-container flex flex-col">
<div>
<div class="h-full w-full nc-container pt-[9px]">
<div class="h-full w-full flex flex-col">
<div class="px-2">
<a-tabs v-model:activeKey="activeTabIndex" type="editable-card" @edit="closeTab">
<a-tab-pane v-for="(tab, i) in tabs" :key="i" :tab="tab.title" />
@ -129,23 +129,18 @@ function openQuickImportDialog(type: string) {
</a-tabs>
</div>
<div class="flex-1 min-h-0">
<NuxtPage />
</div>
<DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" />
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" :import-type="importType" />
<DlgAirtableImport v-if="airtableImportDialog" v-model="airtableImportDialog" />
<NuxtPage class="px-4 pt-2" />
</div>
<div id="sidebar-right" class="h-full" />
<DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" />
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" :import-type="importType" />
<DlgAirtableImport v-if="airtableImportDialog" v-model="airtableImportDialog" />
</div>
</template>
<style scoped>
.nc-container {
height: calc(100vh - var(--header-height) - 8px);
@apply overflow-hidden;
height: calc(100% - var(--header-height));
flex: 1 1 100%;
}

18
packages/nc-gui-v2/pages/project/index/[id].vue

@ -8,11 +8,14 @@ import { navigateTo, useNuxtApp, useRoute } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { projectTitleValidator } from '~/utils/validation'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
import { nextTick, reactive, useSidebar } from '#imports'
const loading = ref(false)
const { api, isLoading } = useApi()
useSidebar({ hasSidebar: false })
const { $api, $state } = useNuxtApp()
const toast = useToast()
const route = useRoute()
const nameValidationRules = [
@ -35,27 +38,24 @@ const getProject = async () => {
toast.error(await extractSdkResponseErrorMsg(e))
}
}
const renameProject = async () => {
loading.value = true
try {
await $api.project.update(route.params.id as string, formState)
await api.project.update(route.params.id as string, formState)
navigateTo(`/nc/${route.params.id}`)
} catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e))
}
loading.value = false
}
const form = ref<typeof Form>()
// hide sidebar
$state.sidebarOpen.value = false
// select and focus title field on load
onMounted(async () => {
await getProject()
nextTick(() => {
await nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')

13
packages/nc-gui-v2/pages/project/index/create-external.vue

@ -3,7 +3,7 @@ import { onMounted } from '@vue/runtime-core'
import { Form, Modal } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { useToast } from 'vue-toastification'
import { ref } from '#imports'
import { computed, ref, useSidebar, watch } from '#imports'
import { navigateTo, useNuxtApp } from '#app'
import { ClientType } from '~/lib'
import type { ProjectCreateForm } from '~/utils'
@ -23,8 +23,12 @@ const useForm = Form.useForm
const loading = ref(false)
const testSuccess = ref(false)
const { $api, $e, $state } = useNuxtApp()
const { $api, $e } = useNuxtApp()
useSidebar({ hasSidebar: false })
const toast = useToast()
const { t } = useI18n()
const formState = $ref<ProjectCreateForm>({
@ -66,7 +70,7 @@ const validators = computed(() => {
}
})
const { resetFields, validate, validateInfos } = useForm(formState, validators)
const { validate, validateInfos } = useForm(formState, validators)
const onClientChange = () => {
formState.dataSource = { ...getDefaultConnectionConfig(formState.dataSource.client) }
@ -192,9 +196,6 @@ const testConnection = async () => {
}
}
// hide sidebar
$state.sidebarOpen.value = false
// reset test status on config change
watch(
() => formState.dataSource,

22
packages/nc-gui-v2/pages/project/index/create.vue

@ -2,17 +2,22 @@
import { onMounted, onUpdated } from '@vue/runtime-core'
import type { Form } from 'ant-design-vue'
import { useToast } from 'vue-toastification'
import { nextTick, ref } from '#imports'
import { nextTick, reactive, ref, useApi, useSidebar } from '#imports'
import { navigateTo, useNuxtApp } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { projectTitleValidator } from '~/utils/validation'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
const name = ref('')
const loading = ref(false)
const valid = ref(false)
const { $api, $state, $e } = useNuxtApp()
const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
useSidebar({ hasSidebar: false })
const toast = useToast()
const nameValidationRules = [
@ -29,9 +34,8 @@ const formState = reactive({
const createProject = async () => {
$e('a:project:create:xcdb')
loading.value = true
try {
const result = await $api.project.create({
const result = await api.project.create({
title: formState.title,
})
@ -39,17 +43,13 @@ const createProject = async () => {
} catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e))
}
loading.value = false
}
const form = ref<typeof Form>()
// hide sidebar
$state.sidebarOpen.value = false
// select and focus title field on load
onMounted(async () => {
nextTick(() => {
await nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
@ -61,7 +61,7 @@ onMounted(async () => {
</script>
<template>
<a-card class="w-[500px] mx-auto !mt-100px shadow-md">
<a-card :loading="isLoading" class="w-[500px] mx-auto !mt-100px shadow-md">
<h3 class="text-3xl text-center font-semibold mb-2">{{ $t('activity.createProject') }}</h3>
<a-form ref="form" :model="formState" name="basic" layout="vertical" autocomplete="off" @finish="createProject">

4
packages/nc-gui-v2/pages/signin.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { RuleObject } from 'ant-design-vue/es/form'
import { definePageMeta } from '#imports'
import { definePageMeta, useSidebar } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { navigateTo, useNuxtApp } from '#app'
import { isEmail } from '~/utils/validation'
@ -12,6 +12,8 @@ const { $api, $state } = $(useNuxtApp())
const { t } = useI18n()
useSidebar({ hasSidebar: false })
definePageMeta({
requiresAuth: false,
title: 'title.headLogin',

9
packages/nc-gui-v2/plugins/state.ts

@ -1,6 +1,5 @@
import { breakpointsTailwind } from '@vueuse/core'
import { defineNuxtPlugin } from '#app'
import { useBreakpoints, useDark, useGlobal, watch } from '#imports'
import { useDark, useGlobal, watch } from '#imports'
/**
* Initialize global state and watches for changes
@ -19,9 +18,6 @@ export default defineNuxtPlugin((nuxtApp) => {
const darkMode = useDark()
/** get current breakpoints (for enabling sidebar) */
const breakpoints = useBreakpoints(breakpointsTailwind)
/** set i18n locale to stored language */
nuxtApp.vueApp.i18n.locale.value = state.lang.value
@ -33,7 +29,4 @@ export default defineNuxtPlugin((nuxtApp) => {
},
{ immediate: true },
)
/** is initial sidebar open */
state.sidebarOpen.value = state.signedIn.value && breakpoints.greater('md').value
})

1
packages/nc-gui-v2/tsconfig.json

@ -11,6 +11,7 @@
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": [
"@nuxt/types",
"@intlify/vite-plugin-vue-i18n/client",
"vue-i18n",
"unplugin-icons/types/vue",

1
packages/nc-gui-v2/utils/viewUtils.ts

@ -9,7 +9,6 @@ import MdiEyeIcon from '~icons/mdi/eye-circle-outline'
export const viewIcons = {
[ViewTypes.GRID]: { icon: MdiGridIcon, color: 'blue' },
// [ViewTypes.GRID]: { icon: "mdi-grid-large", color: "blue" },
[ViewTypes.FORM]: { icon: MdiFormIcon, color: 'pink' },
calendar: { icon: MdiCalendarIcon, color: 'purple' },
[ViewTypes.GALLERY]: { icon: MdiGalleryIcon, color: 'orange' },

2
packages/nc-gui-v2/windi.config.ts

@ -44,6 +44,8 @@ export default defineConfig({
'color-transition': 'transition-color duration-100 ease-in',
'scrollbar-thin-primary':
'scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-primary scrollbar-track-white dark:(!scrollbar-track-black)',
'scrollbar-thin-dull':
'scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-gray-300 scrollbar-track-white dark:(!scrollbar-track-black)',
},
theme: {

Loading…
Cancel
Save