Browse Source

Merge branch 'develop' into feat/kanban-view

pull/3818/head
Wing-Kam Wong 2 years ago
parent
commit
ba0713cdc7
  1. 15
      .github/workflows/release-nocodb.yml
  2. 11
      packages/nc-gui/components.d.ts
  3. 11
      packages/nc-gui/components/cell/Checkbox.vue
  4. 5
      packages/nc-gui/components/dashboard/TreeView.vue
  5. 29
      packages/nc-gui/components/general/FullScreen.vue
  6. 2
      packages/nc-gui/components/general/MiniSidebar.vue
  7. 45
      packages/nc-gui/components/general/Tooltip.vue
  8. 11
      packages/nc-gui/components/smartsheet-column/EditOrAdd.vue
  9. 5
      packages/nc-gui/components/smartsheet/Toolbar.vue
  10. 3
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  11. 2
      packages/nc-gui/components/smartsheet/sidebar/toolbar/DeleteTable.vue
  12. 8
      packages/nc-gui/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue
  13. 3
      packages/nc-gui/components/smartsheet/sidebar/toolbar/index.vue
  14. 2
      packages/nc-gui/components/tabs/Smartsheet.vue
  15. 5
      packages/nc-gui/composables/useColumnCreateStore.ts
  16. 87
      packages/nc-gui/composables/useProject.ts
  17. 70
      packages/nc-gui/composables/useSidebar/index.ts
  18. 4
      packages/nc-gui/composables/useSmartsheetRowStore.ts
  19. 52
      packages/nc-gui/composables/useViewData.ts
  20. 4
      packages/nc-gui/layouts/default.vue
  21. 2
      packages/nc-gui/lib/types.ts
  22. 12
      packages/nc-gui/nuxt-shim.d.ts
  23. 19
      packages/nc-gui/package-lock.json
  24. 1
      packages/nc-gui/package.json
  25. 27
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  26. 58
      packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue
  27. 11
      packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue
  28. 2
      packages/nc-gui/pages/[projectType]/form/[viewId].vue
  29. 2
      packages/nc-gui/pages/index/index/[projectId].vue
  30. 2
      packages/nc-gui/pages/index/index/create-external.vue
  31. 2
      packages/nc-gui/pages/index/index/create.vue
  32. 35
      packages/nc-gui/pages/index/index/index.vue
  33. 2
      packages/nc-gui/pages/signin.vue
  34. 36
      packages/nc-gui/utils/memStorage.ts
  35. 805
      packages/nocodb-sdk/package-lock.json

15
.github/workflows/release-nocodb.yml

@ -103,24 +103,17 @@ jobs:
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# Close all issues with target tags 'Fixed' & 'Resolved'
close-fixed-issues:
# Close all issues with target tags 'Status: Ready for Next Release'
close-issues:
needs: [release-docker, process-input]
uses: ./.github/workflows/release-close-issue.yml
with:
issue_label: 'Status: Fixed'
version: ${{ needs.process-input.outputs.target_tag }}
close-resolved-issues:
needs: [close-fixed-issues, process-input]
uses: ./.github/workflows/release-close-issue.yml
with:
issue_label: 'Status: Resolved'
issue_label: 'Status: Ready for Next Release'
version: ${{ needs.process-input.outputs.target_tag }}
# Publish Docs
publish-docs:
needs: close-resolved-issues
needs: close-issues
uses: ./.github/workflows/publish-docs.yml
secrets:
GH_TOKEN: "${{ secrets.GH_TOKEN }}"

11
packages/nc-gui/components.d.ts vendored

@ -79,11 +79,15 @@ declare module '@vue/runtime-core' {
ClarityImageLine: typeof import('~icons/clarity/image-line')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
EvaEmailOutline: typeof import('~icons/eva/email-outline')['default']
'Ic:twotoneWidthFull': typeof import('~icons/ic/twotone-width-full')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
IcBaselineWidthFull: typeof import('~icons/ic/baseline-width-full')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
IcRoundSearch: typeof import('~icons/ic/round-search')['default']
IcTwotoneWidthFull: typeof import('~icons/ic/twotone-width-full')['default']
IcTwotoneWidthNormal: typeof import('~icons/ic/twotone-width-normal')['default']
LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default']
LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default']
LogosSwagger: typeof import('~icons/logos/swagger')['default']
@ -98,6 +102,9 @@ declare module '@vue/runtime-core' {
MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MaterialSymbolsWidthFull: typeof import('~icons/material-symbols/width-full')['default']
MaterialSymbolsWidthWideOutline: typeof import('~icons/material-symbols/width-wide-outline')['default']
'Mdi:arrowExpandHorizontal': typeof import('~icons/mdi/arrow-expand-horizontal')['default']
MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
MdiAccountGroup: typeof import('~icons/mdi/account-group')['default']
@ -113,6 +120,7 @@ declare module '@vue/runtime-core' {
MdiArrowCollapse: typeof import('~icons/mdi/arrow-collapse')['default']
MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowExpandHorizontal: typeof import('~icons/mdi/arrow-expand-horizontal')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiAt: typeof import('~icons/mdi/at')['default']
MdiBackburger: typeof import('~icons/mdi/backburger')['default']
@ -165,6 +173,8 @@ declare module '@vue/runtime-core' {
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFlashOutline: typeof import('~icons/mdi/flash-outline')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiFullscreen: typeof import('~icons/mdi/fullscreen')['default']
MdiFullscreenExit: typeof import('~icons/mdi/fullscreen-exit')['default']
MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiGithub: typeof import('~icons/mdi/github')['default']
@ -230,5 +240,6 @@ declare module '@vue/runtime-core' {
PhFileCsv: typeof import('~icons/ph/file-csv')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SystemUiconsExpandWidth: typeof import('~icons/system-uicons/expand-width')['default']
}
}

11
packages/nc-gui/components/cell/Checkbox.vue

@ -2,18 +2,23 @@
import { ColumnInj, IsFormInj, ReadonlyInj, getMdiIcon, inject } from '#imports'
interface Props {
modelValue?: boolean | undefined | number
// If the previous cell value was a text, the initial checkbox value is a string type
// otherwise it can be either a boolean, or a string representing a boolean, i.e '0' or '1'
modelValue?: boolean | string | '0' | '1'
}
interface Emits {
(event: 'update:modelValue', model: boolean | undefined | number): void
(event: 'update:modelValue', model: boolean): void
}
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
let vModel = $(useVModel(props, 'modelValue', emits))
let vModel = $computed({
get: () => !!props.modelValue && props.modelValue !== '0',
set: (val) => emits('update:modelValue', val),
})
const column = inject(ColumnInj)

5
packages/nc-gui/components/dashboard/TreeView.vue

@ -311,11 +311,13 @@ function openTableCreateDialog() {
{ hidden: !filteredTables?.includes(table), active: activeTable === table.title },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,
]"
class="nc-tree-item pl-5 pr-3 py-2 text-sm cursor-pointer group"
class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
@click="addTableTab(table)"
>
<GeneralTooltip wrapper-class="pl-5 pr-3 py-2" modifier-key="Alt">
<template #title>{{ table.table_name }}</template>
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto">
<MdiDrag
@ -360,6 +362,7 @@ function openTableCreateDialog() {
</template>
</a-dropdown>
</div>
</GeneralTooltip>
</div>
</div>
</div>

29
packages/nc-gui/components/general/FullScreen.vue

@ -0,0 +1,29 @@
<script setup lang="ts">
import { useSidebar } from '#imports'
const rightSidebar = useSidebar('nc-right-sidebar')
const leftSidebar = useSidebar('nc-left-sidebar')
const isSidebarsOpen = computed({
get: () => rightSidebar.isOpen.value || leftSidebar.isOpen.value,
set: (value) => {
rightSidebar.toggle(value)
leftSidebar.toggle(value)
},
})
</script>
<template>
<a-tooltip>
<!-- todo: i18n -->
<template #title> {{ isSidebarsOpen ? 'Full width': 'Exit full width' }}</template>
<div
v-e="['c:toolbar:fullscreen']"
class="nc-fullscreen-btn cursor-pointer flex align-center self-center px-2 py-2 mr-2"
@click="isSidebarsOpen = !isSidebarsOpen"
>
<IcTwotoneWidthFull v-if="isSidebarsOpen" class="text-gray-300" />
<IcTwotoneWidthNormal v-else class="text-gray-300" />
</div>
</a-tooltip>
</template>

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

@ -4,7 +4,7 @@ import { computed, useGlobal, useProject, useRoute, useSidebar } from '#imports'
const { signOut, signedIn, user } = useGlobal()
const { isOpen } = useSidebar({ isOpen: true })
const { isOpen } = useSidebar('nc-mini-sidebar', { isOpen: true })
const { project } = useProject()

45
packages/nc-gui/components/general/Tooltip.vue

@ -0,0 +1,45 @@
<script lang="ts" setup>
import { onKeyStroke } from '@vueuse/core'
interface Props {
// Key to be pressed on hover to trigger the tooltip
modifierKey?: string
wrapperClass?: string
}
const { modifierKey } = defineProps<Props>()
const showTooltip = ref(false)
const isMouseOver = ref(false)
if (modifierKey) {
onKeyStroke(modifierKey, (e) => {
e.preventDefault()
if (modifierKey && isMouseOver.value) {
showTooltip.value = true
}
})
}
watch(isMouseOver, (val) => {
if (!val) {
showTooltip.value = false
}
// Show tooltip on mouseover if no modifier key is provided
if (val && !modifierKey) {
showTooltip.value = true
}
})
</script>
<template>
<a-tooltip v-model:visible="showTooltip" :trigger="[]">
<template #title>
<slot name="title" />
</template>
<div class="w-full" :class="wrapperClass" @mouseenter="isMouseOver = true" @mouseleave="isMouseOver = false">
<slot />
</div>
</a-tooltip>
</template>

11
packages/nc-gui/components/smartsheet-column/EditOrAdd.vue

@ -110,11 +110,11 @@ onMounted(() => {
<template>
<div
class="w-[400px] max-h-[95vh] bg-gray-50 shadow-lg p-6 overflow-auto !border"
class="w-[400px] bg-gray-50 shadow p-4 overflow-auto border"
:class="{ '!w-[600px]': formState.uidt === UITypes.Formula }"
@click.stop
>
<a-form v-if="formState" v-model="formState" name="column-create-or-edit" layout="vertical">
<a-form v-if="formState" v-model="formState" no-style name="column-create-or-edit" layout="vertical">
<div class="flex flex-col gap-2">
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title">
<a-input
@ -163,6 +163,7 @@ onMounted(() => {
v-model:value="formState"
/>
</div>
<div
v-if="!isVirtualCol(formState.uidt)"
class="text-xs cursor-pointer text-grey nc-more-options mb-1 mt-4 flex items-center gap-1 justify-end"
@ -172,7 +173,8 @@ onMounted(() => {
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div>
<div class="overflow-hidden" :class="advancedOptions ? 'h-min mb-2' : 'h-0'">
<Transition name="layout" mode="out-in">
<div v-if="advancedOptions" class="overflow-hidden">
<a-checkbox
v-if="formState.meta && columnToValidate.includes(formState.uidt)"
v-model:checked="formState.meta.validate"
@ -182,8 +184,11 @@ onMounted(() => {
{{ `Accept only valid ${formState.uidt}` }}
</span>
</a-checkbox>
<SmartsheetColumnAdvancedOptions v-model:value="formState" />
</div>
</Transition>
<a-form-item>
<div class="flex justify-end gap-1 mt-4">
<a-button html-type="button" @click="emit('cancel')">

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

@ -1,5 +1,5 @@
<script setup lang="ts">
import { IsPublicInj, useSharedView, useSmartsheetStoreOrThrow } from '#imports'
import { IsPublicInj, useSharedView, useSidebar, useSmartsheetStoreOrThrow } from '#imports'
import ToggleDrawer from '~/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue'
const { isGrid, isForm, isGallery, isKanban, isSqlView } = useSmartsheetStoreOrThrow()
@ -8,7 +8,7 @@ const isPublic = inject(IsPublicInj, ref(false))
const { isUIAllowed } = useUIPermission()
const { isOpen } = useSidebar()
const { isOpen } = useSidebar('nc-right-sidebar')
const { allowCSVDownload } = useSharedView()
</script>
@ -46,6 +46,7 @@ const { allowCSVDownload } = useSharedView()
<SmartsheetToolbarAddRow v-if="isUIAllowed('dataInsert') && !isPublic && !isForm && !isSqlView" class="mx-1" />
<SmartsheetToolbarSearchData v-if="(isGrid || isGallery || isKanban) && !isPublic" class="shrink mr-2 ml-2" />
<template v-if="!isOpen && !isPublic">
<div class="border-l-1 pl-3">
<ToggleDrawer class="mr-2" />

3
packages/nc-gui/components/smartsheet/sidebar/index.vue

@ -13,6 +13,7 @@ import {
ref,
useRoute,
useRouter,
useSidebar,
useViews,
watch,
} from '#imports'
@ -34,7 +35,7 @@ const { $e } = useNuxtApp()
provide(ViewListInj, views)
/** Sidebar visible */
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar', isOpen: true })
const { isOpen } = useSidebar('nc-right-sidebar', { isOpen: true })
const sidebarCollapsed = computed(() => !isOpen.value)

2
packages/nc-gui/components/smartsheet/sidebar/toolbar/DeleteTable.vue

@ -5,7 +5,7 @@ const meta = inject(MetaInj, ref())
const { deleteTable } = useTable()
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
const { isOpen } = useSidebar('nc-right-sidebar')
</script>
<template>

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

@ -1,11 +1,15 @@
<script setup lang="ts">
/** Sidebar visible */
const { isOpen, toggle } = useSidebar({ storageKey: 'nc-right-sidebar', isOpen: true })
const { isOpen, toggle } = useSidebar('nc-right-sidebar')
const onClick = () => {
toggle(!isOpen.value)
}
</script>
<template>
<div :class="{ 'nc-active-btn': isOpen }">
<a-button size="small" class="nc-toggle-right-navbar" @click="toggle(!isOpen)">
<a-button size="small" class="nc-toggle-right-navbar" @click="onClick">
<div class="flex items-center gap-1 text-xs" :class="{ 'text-gray-500': !isOpen }">
<AntDesignMenuUnfoldOutlined v-if="isOpen" />
<AntDesignMenuFoldOutlined v-else />

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

@ -20,8 +20,11 @@ const clickCount = $ref(0)
"
>
<slot name="start" />
<ToggleDrawer />
<span></span>
<template v-if="debug">
<ExportCache />

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

@ -40,7 +40,7 @@ const openNewRecordFormHook = createEventHook<void>()
const { isGallery, isGrid, isForm, isKanban, isLocked } = useProvideSmartsheetStore(activeView, meta)
// provide the sidebar injection state
provideSidebar({ storageKey: 'nc-right-sidebar', isOpen: true })
provideSidebar('nc-right-sidebar', { useStorage: true, isOpen: true })
// todo: move to store
provide(MetaInj, meta)

5
packages/nc-gui/composables/useColumnCreateStore.ts

@ -82,8 +82,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { resetFields, validate, validateInfos } = useForm(formState, validators)
const onUidtOrIdTypeChange = () => {
const { isCurrency } = useColumn(ref(formState.value as ColumnType))
const colProp = sqlUi.value.getDataTypeForUiType(formState.value as { uidt: UITypes }, idType ?? undefined)
formState.value = {
...formState.value,
@ -111,7 +109,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
}
}
if (isCurrency.value) {
if (column.value?.uidt === UITypes.Currency) {
formState.value.dtxp = column.value.dtxp
formState.value.dtxs = column.value.dtxs
@ -119,7 +116,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
formState.value.dtxp = 19
formState.value.dtxs = 2
}
}
formState.value.altered = formState.value.altered || 2
}
@ -167,7 +163,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const addOrUpdate = async (onSuccess: () => void) => {
try {
console.log(formState, validators)
if (!(await validate())) return
} catch (e) {
console.log(e)

87
packages/nc-gui/composables/useProject.ts

@ -1,31 +1,53 @@
import type { MaybeRef } from '@vueuse/core'
import { SqlUiFactory } from 'nocodb-sdk'
import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import { useNuxtApp, useRoute } from '#app'
import type { ProjectMetaInfo } from '~/lib'
import { SqlUiFactory } from 'nocodb-sdk'
import { isString } from '@vueuse/core'
import {
USER_PROJECT_ROLES,
computed,
createEventHook,
ref,
useApi,
useGlobal,
useInjectionState,
useNuxtApp,
useRoute,
useState,
useTheme,
watch,
} from '#imports'
import type { ProjectMetaInfo, Roles } from '~/lib'
import type { ThemeConfig } from '@/composables/useTheme'
import { createEventHook, useInjectionState } from '#imports'
const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
const { $api, $e } = useNuxtApp()
const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
const route = useRoute()
const { includeM2M } = useGlobal()
const { setTheme, theme } = useTheme()
const projectLoadedHook = createEventHook<ProjectType>()
const projectId = computed(() => (_projectId ? unref(_projectId) : (route.params.projectId as string)))
const project = ref<ProjectType>({})
const tables = ref<TableType[]>([])
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const projectRoles = useState<Roles>(USER_PROJECT_ROLES, () => ({}))
const projectMetaInfo = ref<ProjectMetaInfo | undefined>()
const projectId = computed(() => (_projectId ? unref(_projectId) : (route.params.projectId as string)))
// todo: refactor path param name and variable name
const projectType = $computed(() => route.params.projectType as string)
const projectMeta = computed(() => {
const projectMeta = computed<Record<string, any>>(() => {
try {
return typeof project.value.meta === 'string' ? JSON.parse(project.value.meta) : project.value.meta
return isString(project.value.meta) ? JSON.parse(project.value.meta) : project.value.meta
} catch (e) {
return {}
}
@ -44,15 +66,15 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
async function loadProjectMetaInfo(force?: boolean) {
if (!projectMetaInfo.value || force) {
const data = await $api.project.metaGet(project.value.id!, {}, {})
projectMetaInfo.value = data
projectMetaInfo.value = await api.project.metaGet(project.value.id!, {}, {})
}
}
async function loadProjectRoles() {
projectRoles.value = {}
if (isSharedBase.value) {
const user = await $api.auth.me(
const user = await api.auth.me(
{},
{
headers: {
@ -60,33 +82,40 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
},
},
)
projectRoles.value = user.roles
} else if (project.value.id) {
const user = await $api.auth.me({ project_id: project.value.id })
const user = await api.auth.me({ project_id: project.value.id })
projectRoles.value = user.roles
}
}
async function loadTables() {
if (project.value.id) {
const tablesResponse = await $api.dbTable.list(project.value.id, {
const tablesResponse = await api.dbTable.list(project.value.id, {
includeM2M: includeM2M.value,
})
if (tablesResponse.list) tables.value = tablesResponse.list
}
}
async function loadProject() {
if (projectType === 'base') {
const baseData = await $api.public.sharedBaseGet(route.params.projectId as string)
project.value = await $api.project.read(baseData.project_id!)
async function loadProject(id?: string) {
if (id) {
project.value = await api.project.read(projectId.value)
} else if (projectType === 'base') {
const baseData = await api.public.sharedBaseGet(route.params.projectId as string)
project.value = await api.project.read(baseData.project_id!)
} else if (projectId.value) {
project.value = await $api.project.read(projectId.value)
project.value = await api.project.read(projectId.value)
} else {
return
}
await loadProjectRoles()
await loadTables()
setTheme(projectMeta.value?.theme)
projectLoadedHook.trigger(project.value)
@ -97,15 +126,13 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
return
}
if (data.meta && typeof data.meta === 'string') {
await $api.project.update(projectId.value, data)
await api.project.update(projectId.value, data)
} else {
await $api.project.update(projectId.value, { ...data, meta: JSON.stringify(data.meta) })
await api.project.update(projectId.value, { ...data, meta: JSON.stringify(data.meta) })
}
}
async function saveTheme(_theme: Partial<ThemeConfig>) {
$e('c:themes:change')
const fullTheme = {
primaryColor: theme.value.primaryColor,
accentColor: theme.value.accentColor,
@ -119,25 +146,27 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
theme: fullTheme,
},
})
setTheme(fullTheme)
$e('c:themes:change')
}
watch(
() => route.params,
(v) => {
if (!v?.projectId) {
(next) => {
if (!next.projectId) {
setTheme()
}
},
)
// TODO useProject should only called inside a project for now this doesn't work
onScopeDispose(() => {
const reset = () => {
project.value = {}
tables.value = []
projectMetaInfo.value = undefined
projectRoles.value = {}
})
}
return {
project,
@ -156,6 +185,8 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
projectMeta,
saveTheme,
projectLoadedHook: projectLoadedHook.on,
reset,
isLoading,
}
}, 'useProject')

70
packages/nc-gui/composables/useSidebar/index.ts

@ -1,10 +1,10 @@
import { useStorage } from '@vueuse/core'
import { useInjectionState, watch } from '#imports'
import { MemStorage, useInjectionState, watch } from '#imports'
interface UseSidebarProps {
hasSidebar?: boolean
isOpen?: boolean
storageKey?: string // if a storageKey is passed, use that key for localStorage
useStorage?: boolean
}
/**
@ -15,9 +15,11 @@ interface UseSidebarProps {
*
* If `provideSidebar` is not called explicitly, `useSidebar` will trigger the provider if no injection state can be found
*/
const [setup, use] = useInjectionState((props: UseSidebarProps = {}) => {
let isOpen = ref(props.isOpen ?? false)
let hasSidebar = ref(props.hasSidebar ?? true)
const [setupSidebarStore, useSidebarStore] = useInjectionState(() => new MemStorage(), 'SidebarStore')
const createSidebar = (id: string, props: UseSidebarProps = {}) => {
const isOpen = ref(props.isOpen ?? false)
const hasSidebar = ref(props.hasSidebar ?? true)
function toggle(state?: boolean) {
isOpen.value = state ?? !isOpen.value
@ -27,10 +29,10 @@ const [setup, use] = useInjectionState((props: UseSidebarProps = {}) => {
hasSidebar.value = state ?? !hasSidebar.value
}
if (props.storageKey) {
const storage = toRefs(useStorage(props.storageKey, { isOpen, hasSidebar }, localStorage, { mergeDefaults: true }).value)
isOpen = storage.isOpen
hasSidebar = storage.hasSidebar
if (props.useStorage) {
const storage = toRefs(useStorage(id, { isOpen, hasSidebar }, localStorage, { mergeDefaults: true }).value)
syncRef(isOpen, storage.isOpen)
syncRef(hasSidebar, storage.hasSidebar)
}
watch(
@ -55,20 +57,50 @@ const [setup, use] = useInjectionState((props: UseSidebarProps = {}) => {
hasSidebar,
toggleHasSidebar,
}
}, 'useSidebar')
}
export const provideSidebar = setup
const useSidebarStorage = () => {
let sidebarStorage = useSidebarStore()
export function useSidebar(props: UseSidebarProps = {}) {
const state = use()
if (!sidebarStorage) {
sidebarStorage = setupSidebarStore()
}
return sidebarStorage
}
if (!state) {
return setup(props)
export const provideSidebar = (id: string, props: UseSidebarProps = {}) => {
const sidebarStorage = useSidebarStorage()
if (!sidebarStorage.has(id)) {
const sidebar = createSidebar(id, props)
sidebarStorage.set(id, sidebar)
return sidebar
} 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
const sidebar = sidebarStorage.get(id)
if (props.isOpen !== undefined) sidebar.isOpen.value = props.isOpen
if (props.hasSidebar !== undefined) sidebar.hasSidebar.value = props.hasSidebar
return sidebar
}
}
return state
export function useSidebar(id: string, props: UseSidebarProps = {}) {
if (!id) throw new Error('useSidebar requires an id')
const sidebarStorage = useSidebarStorage()
if (sidebarStorage.has(id)) {
const sidebar = sidebarStorage.get(id)
if (props.isOpen !== undefined) sidebar.isOpen.value = props.isOpen
if (props.hasSidebar !== undefined) sidebar.hasSidebar.value = props.hasSidebar
return sidebar
} else {
return provideSidebar(id, props)
}
}

4
packages/nc-gui/composables/useSmartsheetRowStore.ts

@ -117,9 +117,9 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
NOCO,
project.value?.id as string,
meta.value?.title as string,
extractPkFromRow(ref(row).value?.row, meta.value?.columns as ColumnType[]),
extractPkFromRow(unref(row)?.row, meta.value?.columns as ColumnType[]),
)
Object.assign(ref(row).value, {
Object.assign(unref(row), {
row: record,
oldRow: { ...record },
rowMeta: {},

52
packages/nc-gui/composables/useViewData.ts

@ -1,9 +1,6 @@
import type { Api, ColumnType, FormType, GalleryType, KanbanType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { Api, ColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { ViewTypes } from 'nocodb-sdk'
import { useNuxtApp } from '#app'
import {
IsPublicInj,
NOCO,
@ -11,6 +8,8 @@ import {
extractSdkResponseErrorMsg,
getHTMLEncodedText,
useApi,
useI18n,
useNuxtApp,
useProject,
useUIPermission,
} from '#imports'
@ -40,11 +39,9 @@ export function useViewData(
const _paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 })
const aggCommentCount = ref<{ row_id: string; count: number }[]>([])
const galleryData = ref<GalleryType>()
const kanbanData = ref<KanbanType>()
const formColumnData = ref<FormType>()
const formViewData = ref<FormType>()
const formattedData = ref<Row[]>([])
const formattedKanbanData = ref<Record<string, Row[]>>()
const isPublic = inject(IsPublicInj, ref(false))
const { project, isSharedBase } = useProject()
@ -60,38 +57,6 @@ export function useViewData(
rowMeta: {},
}))
const formatKanbanData = (list: Record<string, any>[]) => {
const groupingField = 'singleSelect2'
const groupingFieldColumn = meta?.value?.columns?.filter((f) => f.title === groupingField)[0] as Record<string, any>
// TODO: sort by kanban meta
const groupingFieldColumnOptions = [
...(groupingFieldColumn?.colOptions?.options ?? []),
{ title: 'Uncategorized', order: 0 },
].sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order)
const initialAcc = groupingFieldColumnOptions.reduce((acc: any, obj: any) => {
if (!acc[obj.title]) {
acc[obj.title] = []
}
return acc
}, {})
return {
meta: groupingFieldColumnOptions,
data: list.reduce((acc: any, obj: any) => {
// TODO: grouping field
const key = obj[groupingField] === null ? 'Uncategorized' : obj[groupingField]
if (!acc[key]) {
acc[key] = []
}
acc[key].push({
row: { ...obj },
oldRow: { ...obj },
rowMeta: {},
})
return acc
}, initialAcc),
}
}
const paginationData = computed({
get: () => (isPublic.value ? sharedPaginationData.value : _paginationData.value),
set: (value) => {
@ -207,9 +172,6 @@ export function useViewData(
where: where?.value,
})
: await fetchSharedViewData()
if (viewMeta?.value.type === ViewTypes.KANBAN) {
formattedKanbanData.value = formatKanbanData(response.list)
}
formattedData.value = formatData(response.list)
paginationData.value = response.pageInfo
await loadAggCommentsCount()
@ -221,11 +183,6 @@ export function useViewData(
galleryData.value = await $api.dbView.galleryRead(viewMeta.value.id)
}
async function loadKanbanData() {
if (!viewMeta?.value?.id) return
kanbanData.value = await $api.dbView.kanbanRead(viewMeta.value.id)
}
async function insertRow(row: Record<string, any>, rowIndex = formattedData.value?.length) {
try {
const insertObj = meta?.value?.columns?.reduce((o: any, col) => {
@ -439,7 +396,6 @@ export function useViewData(
paginationData,
queryParams,
formattedData,
formattedKanbanData,
insertRow,
updateRowProperty,
changePage,
@ -460,7 +416,5 @@ export function useViewData(
loadAggCommentsCount,
removeLastEmptyRow,
removeRowIfNew,
kanbanData,
loadKanbanData,
}
}

4
packages/nc-gui/layouts/default.vue

@ -1,12 +1,12 @@
<script lang="ts" setup>
import { useTitle } from '@vueuse/core'
import { useI18n, useRoute, useSidebar } from '#imports'
import { provideSidebar, useI18n, useRoute } from '#imports'
const route = useRoute()
const { te, t } = useI18n()
const { hasSidebar } = useSidebar()
const { hasSidebar } = provideSidebar('nc-left-sidebar')
useTitle(route.meta?.title && te(route.meta.title) ? `${t(route.meta.title)} | NocoDB` : 'NocoDB')
</script>

2
packages/nc-gui/lib/types.ts

@ -31,7 +31,7 @@ export interface Field {
system?: boolean
}
export type Roles = Record<Role, boolean> | string
export type Roles = Record<Role | string, boolean> | string
export type Filter = FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string; readOnly?: boolean }

12
packages/nc-gui/nuxt-shim.d.ts vendored

@ -1,6 +1,7 @@
import type { Api as BaseAPI } from 'nocodb-sdk'
import type { UseGlobalReturn } from './composables/useGlobal/types'
import type { NocoI18n } from './lib'
import type { TabType } from './composables'
declare module '#app/nuxt' {
interface NuxtApp {
@ -28,4 +29,15 @@ declare module 'vue-router' {
hideHeader?: boolean
title?: string
}
interface RouteParams {
projectId: string
projectType: 'base' | 'nc' | string
type: TabType
title: string
viewId: string
viewTitle: string
baseId: string
token: string
}
}

19
packages/nc-gui/package-lock.json generated

@ -49,6 +49,7 @@
"@iconify-json/mi": "^1.1.2",
"@iconify-json/ph": "^1.1.2",
"@iconify-json/ri": "^1.1.3",
"@iconify-json/system-uicons": "^1.1.4",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",
@ -1151,6 +1152,15 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/system-uicons": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@iconify-json/system-uicons/-/system-uicons-1.1.4.tgz",
"integrity": "sha512-LL4gc9Fz7ZoGZzBS5fSrFnTNSRB8gXKw+sZAlIq6Msa/rRljyXoTuySIgS66dzX4QnbUC8DqXIFn5BT0/d+Cpg==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify/types": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
@ -16055,6 +16065,15 @@
"@iconify/types": "*"
}
},
"@iconify-json/system-uicons": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@iconify-json/system-uicons/-/system-uicons-1.1.4.tgz",
"integrity": "sha512-LL4gc9Fz7ZoGZzBS5fSrFnTNSRB8gXKw+sZAlIq6Msa/rRljyXoTuySIgS66dzX4QnbUC8DqXIFn5BT0/d+Cpg==",
"dev": true,
"requires": {
"@iconify/types": "*"
}
},
"@iconify/types": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",

1
packages/nc-gui/package.json

@ -58,6 +58,7 @@
"@iconify-json/mi": "^1.1.2",
"@iconify-json/ph": "^1.1.2",
"@iconify-json/ri": "^1.1.3",
"@iconify-json/system-uicons": "^1.1.4",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",

27
packages/nc-gui/pages/[projectType]/[projectId]/index.vue

@ -5,17 +5,20 @@ import {
computed,
definePageMeta,
navigateTo,
onBeforeMount,
onBeforeUnmount,
onKeyStroke,
openLink,
projectThemeColors,
provide,
provideSidebar,
ref,
useClipboard,
useGlobal,
useI18n,
useProject,
useRoute,
useRouter,
useSidebar,
useTabs,
useUIPermission,
} from '#imports'
@ -29,9 +32,11 @@ const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const { appInfo, token, signOut, signedIn, user } = useGlobal()
const { projectLoadedHook, project, isSharedBase, loadProjectMetaInfo, projectMetaInfo, saveTheme } = useProject()
const { project, isSharedBase, loadProjectMetaInfo, projectMetaInfo, saveTheme, loadProject, reset } = useProject()
const { clearTabs, addTab } = useTabs()
@ -44,7 +49,7 @@ const isLocked = ref(false)
provide('TreeViewIsLockedInj', isLocked)
// create a new sidebar state
const { isOpen, toggle } = provideSidebar({ isOpen: true })
const { isOpen, toggle } = useSidebar('nc-left-sidebar', { isOpen: true })
const dialogOpen = ref(false)
@ -114,7 +119,7 @@ const copyProjectInfo = async () => {
// Copied to clipboard
message.info(t('msg.info.copiedToClipboard'))
} catch (e: any) {
console.log(e)
console.error(e)
message.error(e.message)
}
}
@ -125,7 +130,7 @@ const copyAuthToken = async () => {
// Copied to clipboard
message.info(t('msg.info.copiedToClipboard'))
} catch (e: any) {
console.log(e)
console.error(e)
message.error(e.message)
}
}
@ -140,11 +145,21 @@ onKeyStroke(
clearTabs()
projectLoadedHook(() => {
onBeforeMount(async () => {
await loadProject()
if (!route.params.type && isUIAllowed('teamAndAuth')) {
addTab({ type: TabType.AUTH, title: t('title.teamAndAuth') })
}
/** If v1 url found navigate to corresponding new url */
const { type, name, view } = route.query
if (type && name) {
await router.replace(`/nc/${route.params.projectId}/${type}/${name}${view ? `/${view}` : ''}`)
}
})
onBeforeUnmount(reset)
</script>
<template>

58
packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue

@ -1,48 +1,17 @@
<script setup lang="ts">
import type { TabItem } from '~/composables'
import { TabType } from '~/composables'
import {
TabMetaInj,
onBeforeMount,
provide,
ref,
useGlobal,
useProject,
useRoute,
useRouter,
useSidebar,
useTabs,
} from '#imports'
import { TabMetaInj, provide, useGlobal, useProject, useSidebar, useTabs } from '#imports'
import MdiAirTableIcon from '~icons/mdi/table-large'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiAccountGroup from '~icons/mdi/account-group'
const { project, loadProject, loadTables } = useProject()
const { isLoading: isLoadingProject } = useProject()
const { tabs, activeTabIndex, activeTab, closeTab } = useTabs()
const { isLoading } = useGlobal()
const route = useRoute()
const router = useRouter()
const isReady = ref(false)
onBeforeMount(async () => {
if (!Object.keys(project.value).length) await loadProject()
/** If v1 url found navigate to corresponding new url */
const { type, name, view } = route.query
if (type && name) {
await router.replace(`/nc/${route.params.projectId}/${type}/${name}${view ? `/${view}` : ''}`)
}
await loadTables()
isReady.value = true
})
provide(TabMetaInj, activeTab)
const icon = (tab: TabItem) => {
@ -56,7 +25,7 @@ const icon = (tab: TabItem) => {
}
}
const { isOpen, toggle } = useSidebar()
const { isOpen, toggle } = useSidebar('nc-left-sidebar')
function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
if (action === 'remove') closeTab(targetKey)
@ -66,7 +35,7 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
<template>
<div class="h-full w-full nc-container">
<div class="h-full w-full flex flex-col">
<div class="flex items-end !min-h-[var(--header-height)] !bg-primary">
<div class="flex items-end !min-h-[var(--header-height)] !bg-primary nc-tab-bar">
<div
v-if="!isOpen"
class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) group nc-sidebar-add-row py-2 px-3"
@ -107,12 +76,16 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
<MdiLoading class="animate-infinite animate-spin" />
</div>
</div>
<GeneralFullScreen class="nc-fullscreen-icon" />
</div>
<div class="w-full min-h-[300px] flex-auto">
<NuxtPage v-if="isReady" />
<div v-show="!isLoadingProject" class="w-full h-full">
<NuxtPage />
</div>
<div v-else class="w-full h-full flex justify-center items-center">
<div v-show="isLoadingProject" class="w-full h-full flex justify-center items-center">
<a-spin size="large" />
</div>
</div>
@ -171,7 +144,18 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
:deep(.ant-menu-submenu::after) {
@apply !border-none;
}
:deep(.ant-tabs-tab-remove) {
@apply mt-[3px];
}
.nc-tab-bar {
:deep(.nc-fullscreen-icon) {
@apply opacity-0 transition;
}
&:hover :deep(.nc-fullscreen-icon) {
@apply opacity-100;
}
}
</style>

11
packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue

@ -3,7 +3,11 @@ import type { TabItem } from '~/composables'
import { TabMetaInj } from '#imports'
const { getMeta } = useMetas()
const { project, projectLoadedHook } = useProject()
const route = useRoute()
const loading = ref(true)
const activeTab = inject(
@ -11,9 +15,14 @@ const activeTab = inject(
computed(() => ({} as TabItem)),
)
getMeta(route.params.title as string, true).finally(() => {
if (!project.value.id) {
projectLoadedHook(async () => {
await getMeta(route.params.title as string, true)
loading.value = false
})
} else {
getMeta(route.params.title as string, true).finally(() => (loading.value = false))
}
</script>
<template>

2
packages/nc-gui/pages/[projectType]/form/[viewId].vue

@ -18,7 +18,7 @@ definePageMeta({
public: true,
})
useSidebar({ hasSidebar: false })
useSidebar('nc-left-sidebar', { hasSidebar: false })
const route = useRoute()

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

@ -17,7 +17,7 @@ import {
const { isLoading } = useApi()
useSidebar({ hasSidebar: false })
useSidebar('nc-left-sidebar', { hasSidebar: false })
const route = useRoute()

2
packages/nc-gui/pages/index/index/create-external.vue

@ -37,7 +37,7 @@ const { api, isLoading } = useApi()
const { $e } = useNuxtApp()
useSidebar({ hasSidebar: false })
useSidebar('nc-left-sidebar', { hasSidebar: false })
const { t } = useI18n()

2
packages/nc-gui/pages/index/index/create.vue

@ -18,7 +18,7 @@ const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
useSidebar({ hasSidebar: false })
useSidebar('nc-left-sidebar', { hasSidebar: false })
const nameValidationRules = [
{

35
packages/nc-gui/pages/index/index/index.vue

@ -9,6 +9,7 @@ import {
navigateTo,
projectThemeColors,
ref,
themeV2Colors,
useApi,
useNuxtApp,
useSidebar,
@ -25,7 +26,7 @@ const { api, isLoading } = useApi()
const { isUIAllowed } = useUIPermission()
useSidebar({ hasSidebar: true, isOpen: true })
useSidebar('nc-left-sidebar', { hasSidebar: false, isOpen: true })
const filterQuery = ref('')
@ -70,10 +71,14 @@ await loadProjects()
const handleProjectColor = async (projectId: string, color: string) => {
const tcolor = tinycolor(color)
if (tcolor.isValid()) {
const complement = tcolor.complement()
const project: ProjectType = await $api.project.read(projectId)
const meta = project?.meta && typeof project.meta === 'string' ? JSON.parse(project.meta) : project.meta || {}
await $api.project.update(projectId, {
color,
meta: JSON.stringify({
@ -84,8 +89,10 @@ const handleProjectColor = async (projectId: string, color: string) => {
},
}),
})
// Update local project
const localProject = projects.value?.find((p) => p.id === projectId)
if (localProject) {
localProject.color = color
localProject.meta = JSON.stringify({
@ -100,9 +107,21 @@ const handleProjectColor = async (projectId: string, color: string) => {
}
const getProjectPrimary = (project: ProjectType) => {
const meta = project?.meta && typeof project.meta === 'string' ? JSON.parse(project.meta) : project.meta || {}
return meta?.theme?.primaryColor || themeV2Colors['royal-blue'].DEFAULT
if (!project) return
const meta = project.meta && typeof project.meta === 'string' ? JSON.parse(project.meta) : project.meta || {}
return meta.theme?.primaryColor || themeV2Colors['royal-blue'].DEFAULT
}
const customRow = (record: ProjectType) => ({
onClick: async () => {
await navigateTo(`/nc/${record.id}`)
$e('a:project:open')
},
class: ['group'],
})
</script>
<template>
@ -183,15 +202,7 @@ const getProjectPrimary = (project: ProjectType) => {
<a-table
v-else
key="table"
:custom-row="
(record) => ({
onClick: () => {
navigateTo(`/nc/${record.id}`)
$e('a:project:open')
},
class: ['group'],
})
"
:custom-row="customRow"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
>

2
packages/nc-gui/pages/signin.vue

@ -19,7 +19,7 @@ const { api, isLoading } = useApi()
const { t } = useI18n()
useSidebar({ hasSidebar: false })
useSidebar('nc-left-sidebar', { hasSidebar: false })
definePageMeta({
requiresAuth: false,

36
packages/nc-gui/utils/memStorage.ts

@ -0,0 +1,36 @@
/**
* Stores all currently created store instances
*/
export class MemStorage<T = any> {
public currentId = 0
public items = new Map<string, T>()
static instance: MemStorage
public static getInstance(): MemStorage {
if (!MemStorage.instance) {
MemStorage.instance = new MemStorage()
}
return MemStorage.instance
}
public set(id: string, item: T) {
return this.items.set(id, item)
}
public get(id: string) {
return this.items.get(id)
}
public has(id: string) {
return this.items.has(id)
}
public remove(id: string) {
return this.items.delete(id)
}
public getId(prefix?: string) {
return `${prefix}${this.currentId++}`
}
}

805
packages/nocodb-sdk/package-lock.json generated

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save