Browse Source

Merge pull request #2877 from nocodb/feat/use-global-loading

pull/2931/head
Braks 2 years ago committed by GitHub
parent
commit
8af0aecf6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      packages/nc-gui-v2/app.vue
  2. 8
      packages/nc-gui-v2/components/cell/Attachment.vue
  3. 2
      packages/nc-gui-v2/components/cell/Email.vue
  4. 40
      packages/nc-gui-v2/components/cell/Rating.vue
  5. 13
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  6. 1
      packages/nc-gui-v2/components/dashboard/settings/AppStore.vue
  7. 2
      packages/nc-gui-v2/components/dlg/AirtableImport.vue
  8. 3
      packages/nc-gui-v2/components/dlg/TableCreate.vue
  9. 4
      packages/nc-gui-v2/components/dlg/ViewDelete.vue
  10. 2
      packages/nc-gui-v2/components/general/Share.vue
  11. 2
      packages/nc-gui-v2/components/general/Sponsors.test.ts
  12. 4
      packages/nc-gui-v2/components/monaco/Editor.vue
  13. 2
      packages/nc-gui-v2/components/smartsheet-column/AdvancedOptions.vue
  14. 4
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  15. 4
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  16. 2
      packages/nc-gui-v2/components/smartsheet-header/CellIcon.vue
  17. 2
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  18. 9
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  19. 6
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  20. 6
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  21. 5
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  22. 3
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  23. 5
      packages/nc-gui-v2/components/smartsheet/Gallery.vue
  24. 17
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  25. 2
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  26. 2
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  27. 3
      packages/nc-gui-v2/components/smartsheet/VirtualCell.vue
  28. 27
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  29. 2
      packages/nc-gui-v2/components/tabs/auth/user-management/FeedbackForm.vue
  30. 2
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  31. 3
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  32. 3
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  33. 2
      packages/nc-gui-v2/components/virtual-cell/Rollup.vue
  34. 10
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  35. 2
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  36. 2
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  37. 20
      packages/nc-gui-v2/composables/index.ts
  38. 141
      packages/nc-gui-v2/composables/useApi/index.ts
  39. 16
      packages/nc-gui-v2/composables/useApi/interceptors.ts
  40. 26
      packages/nc-gui-v2/composables/useApi/types.ts
  41. 4
      packages/nc-gui-v2/composables/useAttachment.ts
  42. 9
      packages/nc-gui-v2/composables/useBelongsTo.ts
  43. 4
      packages/nc-gui-v2/composables/useColors.ts
  44. 2
      packages/nc-gui-v2/composables/useColumn.ts
  45. 4
      packages/nc-gui-v2/composables/useColumnCreateStore.ts
  46. 54
      packages/nc-gui-v2/composables/useGlobal/actions.ts
  47. 25
      packages/nc-gui-v2/composables/useGlobal/getters.ts
  48. 71
      packages/nc-gui-v2/composables/useGlobal/index.ts
  49. 95
      packages/nc-gui-v2/composables/useGlobal/state.ts
  50. 45
      packages/nc-gui-v2/composables/useGlobal/types.ts
  51. 169
      packages/nc-gui-v2/composables/useGlobalState/index.ts
  52. 5
      packages/nc-gui-v2/composables/useGlobalState/initialFeedBackForm.ts
  53. 6
      packages/nc-gui-v2/composables/useGridViewColumnWidth.ts
  54. 9
      packages/nc-gui-v2/composables/useHasMany.ts
  55. 9
      packages/nc-gui-v2/composables/useManyToMany.ts
  56. 4
      packages/nc-gui-v2/composables/useMetas.ts
  57. 4
      packages/nc-gui-v2/composables/useProject.ts
  58. 4
      packages/nc-gui-v2/composables/useTableCreate.ts
  59. 2
      packages/nc-gui-v2/composables/useTabs.ts
  60. 14
      packages/nc-gui-v2/composables/useUIPermission/index.ts
  61. 15
      packages/nc-gui-v2/composables/useUIPermission/rolePermissions.ts
  62. 2
      packages/nc-gui-v2/composables/useViewColumns.ts
  63. 6
      packages/nc-gui-v2/composables/useViewData.ts
  64. 2
      packages/nc-gui-v2/composables/useViewFilters.ts
  65. 2
      packages/nc-gui-v2/composables/useViewSorts.ts
  66. 8
      packages/nc-gui-v2/composables/useViews.ts
  67. 4
      packages/nc-gui-v2/composables/useVirtualCell.ts
  68. 33
      packages/nc-gui-v2/lib/types.ts
  69. 9
      packages/nc-gui-v2/middleware/auth.global.ts
  70. 4
      packages/nc-gui-v2/nuxt-shim.d.ts
  71. 45
      packages/nc-gui-v2/package-lock.json
  72. 2
      packages/nc-gui-v2/package.json
  73. 13
      packages/nc-gui-v2/pages/index/index.vue
  74. 5
      packages/nc-gui-v2/pages/nc/[projectId]/index.vue
  75. 4
      packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue
  76. 2
      packages/nc-gui-v2/pages/project/index/[id].vue
  77. 4
      packages/nc-gui-v2/pages/projects/index.vue
  78. 2
      packages/nc-gui-v2/pages/projects/index/index.vue
  79. 1
      packages/nc-gui-v2/pages/projects/index/list.vue
  80. 5
      packages/nc-gui-v2/pages/signin.vue
  81. 7
      packages/nc-gui-v2/plugins/a.i18n.ts
  82. 5
      packages/nc-gui-v2/plugins/api.ts
  83. 2
      packages/nc-gui-v2/plugins/initializeFeedbackForm.ts
  84. 17
      packages/nc-gui-v2/plugins/state.ts
  85. 4
      packages/nc-gui-v2/plugins/tele.ts
  86. 3
      packages/nc-gui-v2/tsconfig.json
  87. 2
      packages/nc-gui-v2/utils/colorsUtils.ts
  88. 1
      packages/nc-gui-v2/utils/index.ts
  89. 2
      packages/nc-gui-v2/utils/projectCreateUtils.ts
  90. 2
      packages/nc-gui-v2/windi.config.ts

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

@ -5,23 +5,22 @@ import MdiDotsVertical from '~icons/mdi/dots-vertical'
import MaterialSymbolsMenu from '~icons/material-symbols/menu'
import MdiReload from '~icons/mdi/reload'
import { navigateTo } from '#app'
import { useGlobal } from '#imports'
const { $state } = useNuxtApp()
const { isLoading } = useApi({ useGlobalInstance: true })
const state = useGlobal()
const sidebar = ref<HTMLDivElement>()
const email = computed(() => $state.user?.value?.email ?? '---')
const email = computed(() => state.user.value?.email ?? '---')
const signOut = () => {
$state.signOut()
state.signOut()
navigateTo('/signin')
}
const sidebarCollapsed = computed({
get: () => !$state.sidebarOpen.value,
set: (val) => ($state.sidebarOpen.value = !val),
get: () => !state.sidebarOpen.value,
set: (val) => (state.sidebarOpen.value = !val),
})
const toggleSidebar = () => {
@ -32,7 +31,7 @@ const toggleSidebar = () => {
<template>
<a-layout class="min-h-[100vh]">
<a-layout-header class="flex !bg-primary items-center text-white px-4 shadow-md">
<MaterialSymbolsMenu v-if="$state.signedIn.value" class="text-xl cursor-pointer" @click="toggleSidebar" />
<MaterialSymbolsMenu v-if="state.signedIn.value" class="text-xl cursor-pointer" @click="toggleSidebar" />
<div class="flex-1" />
@ -42,8 +41,9 @@ const toggleSidebar = () => {
<span class="prose-xl">NocoDB</span>
</div>
<div v-show="isLoading" class="text-gray-400 ml-3">
{{ $t('general.loading') }} <MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
<div v-show="state.isLoading.value" class="text-gray-400 ml-3">
{{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': state.isLoading.value }" />
</div>
</div>
@ -52,7 +52,7 @@ const toggleSidebar = () => {
<div class="flex justify-end gap-4">
<general-language class="mr-3" />
<template v-if="$state.signedIn.value">
<template v-if="state.signedIn.value">
<a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />

8
packages/nc-gui-v2/components/cell/Attachment.vue

@ -3,8 +3,8 @@ import { useToast } from 'vue-toastification'
import { inject, ref, useProject, watchEffect } from '#imports'
import { useNuxtApp } from '#app'
import { ColumnInj, MetaInj } from '~/context'
import { NOCO } from '~/lib/constants'
import { isImage } from '~/utils/fileUtils'
import { NOCO } from '~/lib'
import { isImage } from '~/utils'
import MaterialPlusIcon from '~icons/mdi/plus'
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand'
@ -37,7 +37,7 @@ watchEffect(() => {
}
})
const selectImage = (file: any, i) => {
const selectImage = (file: any, i: unknown) => {
// todo: implement
}
@ -49,7 +49,7 @@ const addFile = () => {
fileInput.value?.click()
}
const onFileSelection = async (e) => {
const onFileSelection = async (e: unknown) => {
// if (this.isPublicGrid) {
// return
// }

2
packages/nc-gui-v2/components/cell/Email.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { computed } from '#imports'
import { isEmail } from '~/utils/validation'
import { isEmail } from '~/utils'
const { modelValue: value } = defineProps<Props>()

40
packages/nc-gui-v2/components/cell/Rating.vue

@ -5,47 +5,27 @@ import MdiStarIcon from '~icons/mdi/star'
import MdiStarOutlineIcon from '~icons/mdi/star-outline'
interface Props {
modelValue?: string | number
modelValue?: number
readOnly?: boolean
}
const { modelValue: value, readOnly } = defineProps<Props>()
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emit)
const column = inject(ColumnInj)
const isForm = inject(IsFormInj)
const ratingMeta = computed(() => {
return {
icon: {
full: 'mdi-star',
empty: 'mdi-star-outline',
},
color: '#fcb401',
max: 5,
// ...(column?.meta || {})
}
})
const localState = computed({
get: () => value,
set: (val) => emit('update:modelValue', val),
})
</script>
<template>
<div class="d-100 h-100" :class="{ 'nc-cell-hover-show': localState === 0 || !localState }">
<v-rating v-model="localState" :length="ratingMeta.max" dense x-small :readonly="readOnly" clearable>
<!-- todo: use the proper slot -->
<div class="d-100 h-100" :class="{ 'nc-cell-hover-show': vModel === 0 || !vModel }">
<v-rating v-model="vModel" :length="5" dense x-small :readonly="readOnly" clearable>
<template #item="{ isFilled, click }">
<!-- todo : custom color and icon -->
<!-- <v-icon v-if="isFilled"- :size="15" :color="ratingMeta.color" @click="click"> -->
<MdiStarIcon v-if="isFilled" :class="`text-[${ratingMeta.color}]`" @click="click" />
<!-- </v-icon> -->
<!-- <v-icon v-else :color="ratingMeta.color" :size="15" class="nc-cell-hover-show" @click="click"> -->
<MdiStarOutlineIcon v-else :class="`text-[${ratingMeta.color}]`" @click="click" />
<!-- </v-icon> -->
<MdiStarIcon v-if="isFilled" class="text-[#fcb40]" @click="click" />
<MdiStarOutlineIcon v-else class="text-[#fcb40]" @click="click" />
</template>
</v-rating>
</div>
</template>
<style scoped></style>

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

@ -1,15 +1,14 @@
<script setup lang="ts">
import { computed } from '@vue/reactivity'
import { Modal } from 'ant-design-vue'
import { UITypes } from 'nocodb-sdk'
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { useToast } from 'vue-toastification'
import { watchEffect } from '#imports'
import SettingsModal from './settings/SettingsModal.vue'
import { useProject, useTabs, useUIPermission, watchEffect } from '#imports'
import { useNuxtApp, useRoute } from '#app'
import useProject from '~/composables/useProject'
import useTabs from '~/composables/useTabs'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { extractSdkResponseErrorMsg } from '~/utils'
import MdiSettingIcon from '~icons/mdi/cog'
import MdiTable from '~icons/mdi/table'
import MdiView from '~icons/mdi/eye-circle-outline'
@ -19,7 +18,7 @@ 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'
import SettingsModal from '~/components/dashboard/settings/SettingsModal.vue'
import { TabType } from '~/composables'
const { addTab } = useTabs()
const toast = useToast()
@ -165,7 +164,7 @@ const deleteTable = (table: TableType) => {
await $api.dbTable.delete(table?.id as string)
closeTab({
type: 'table',
type: TabType.TABLE,
id: table.id,
title: table.title,
})

1
packages/nc-gui-v2/components/dashboard/settings/AppStore.vue

@ -4,6 +4,7 @@ import AppInstall from './app-store/AppInstall.vue'
import MdiEditIcon from '~icons/ic/round-edit'
import MdiCloseCircleIcon from '~icons/mdi/close-circle-outline'
import MdiPlusIcon from '~icons/mdi/plus'
const { $api, $e } = useNuxtApp()
const toast = useToast()

2
packages/nc-gui-v2/components/dlg/AirtableImport.vue

@ -142,7 +142,7 @@ async function sync() {
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
params: {
id: socket.id,
id: socket?.id,
},
})
} catch (e: any) {

3
packages/nc-gui-v2/components/dlg/TableCreate.vue

@ -4,6 +4,7 @@ import { Form } from 'ant-design-vue'
import { useToast } from 'vue-toastification'
import { onMounted, useProject, useTableCreate, useTabs } from '#imports'
import { validateTableName } from '~/utils/validation'
import { TabType } from '~/composables'
interface Props {
modelValue?: boolean
@ -34,7 +35,7 @@ const { table, createTable, generateUniqueTitle, tables, project } = useTableCre
addTab({
id: table.id as string,
title: table.title,
type: 'table',
type: TabType.TABLE,
})
dialogShow.value = false
})

4
packages/nc-gui-v2/components/dlg/ViewDelete.vue

@ -60,7 +60,9 @@ async function onDelete() {
<template #footer>
<a-button key="back" @click="vModel = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" type="danger" :loading="isLoading" @click="onDelete">{{ $t('general.submit') }}</a-button>
<a-button key="submit" danger html-type="submit" :loading="isLoading" @click="onDelete">{{
$t('general.submit')
}}</a-button>
</template>
</a-modal>
</template>

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

@ -3,7 +3,7 @@ interface Props {
url: string
socialMedias: string[]
title?: string
summary: string
summary?: string
hashTags?: string
css?: string
iconClass?: string

2
packages/nc-gui-v2/components/general/Sponsors.test.ts

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import { expect, test } from 'vitest'
import Sponsors from './Sponsors.vue'
import { createVuetifyPlugin } from '~/plugins/vuetify'
import { createI18nPlugin } from '~/plugins/i18n'
import { createI18nPlugin } from '~/plugins/a.i18n'
const mountComponent = async (nav: boolean) => {
const vuetify = createVuetifyPlugin()

4
packages/nc-gui-v2/components/monaco/Editor.vue

@ -3,7 +3,7 @@ import * as monaco from 'monaco-editor'
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import { onMounted } from '#imports'
import { deepCompare } from '~/utils/deepCompare'
import { deepCompare } from '~/utils'
const { modelValue } = defineProps<{ modelValue: any }>()
@ -83,5 +83,3 @@ watch(
<template>
<div ref="root"></div>
</template>
<style scoped></style>

2
packages/nc-gui-v2/components/smartsheet-column/AdvancedOptions.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { useColumnCreateStoreOrThrow } from '~/composables/useColumnCreateStore'
import { useColumnCreateStoreOrThrow } from '#imports'
const { formState, validateInfos, setAdditionalValidations, sqlUi, onDataTypeChange, onAlter } = useColumnCreateStoreOrThrow()

4
packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue

@ -1,7 +1,5 @@
<script lang="ts" setup>
import { computed, inject, watchEffect } from '#imports'
import { useColumnCreateStoreOrThrow } from '~/composables/useColumnCreateStore'
import useMetas from '~/composables/useMetas'
import { computed, inject, useColumnCreateStoreOrThrow, useMetas, watchEffect } from '#imports'
import { MetaInj } from '~/context'
import { uiTypes } from '~/utils/columnUtils'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'

4
packages/nc-gui-v2/components/smartsheet-header/Cell.vue

@ -2,8 +2,8 @@
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { inject } from 'vue'
import { ColumnInj, MetaInj } from '../../context'
import { useProvideColumnCreateStore } from '~/composables/useColumnCreateStore'
import { ColumnInj, MetaInj } from '~/context'
import { useProvideColumnCreateStore } from '#imports'
const { column } = defineProps<{ column: ColumnType & { meta: any } }>()
provide(ColumnInj, column)

2
packages/nc-gui-v2/components/smartsheet-header/CellIcon.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { ColumnInj } from '~/context'
import useColumn from '~/composables/useColumn'
import { useColumn } from '#imports'
import KeyIcon from '~icons/mdi/key-variant'
import JSONIcon from '~icons/mdi/code-json'
// import FKIcon from '~icons/mdi/link-variant'

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { ColumnInj } from '../../context'
import { ColumnInj } from '~/context'
import { provide } from '#imports'
const { column } = defineProps<{ column: ColumnType & { meta: any } }>()

9
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue

@ -1,11 +1,11 @@
<script setup lang="ts">
import type { FilterType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import FieldListAutoCompleteDropdown from './FieldListAutoCompleteDropdown.vue'
import { useNuxtApp } from '#app'
import { inject } from '#imports'
import { inject, useViewFilters } from '#imports'
import { comparisonOpList } from '~/utils/filterUtils'
import { ActiveViewInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import useViewFilters from '~/composables/useViewFilters'
import MdiDeleteIcon from '~icons/mdi/close-box'
import MdiAddIcon from '~icons/mdi/plus'
const { nested = false, parentId } = defineProps<{ nested?: boolean; parentId?: string }>()
@ -20,8 +20,9 @@ const { filters, deleteFilter, saveOrUpdate, loadFilters, addFilter } = useViewF
reloadDataHook?.trigger()
})
const filterUpdateCondition = (filter, i) => {
const filterUpdateCondition = (filter: FilterType, i: number) => {
saveOrUpdate(filter, i)
$e('a:filter:update', {
logical: filter.logical_op,
comparison: filter.comparison_op,
@ -63,7 +64,7 @@ const types = computed(() => {
})
watch(
() => activeView?.value?.id,
() => (activeView?.value as any)?.id,
(n, o) => {
if (n !== o) loadFilters()
},

6
packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue

@ -4,7 +4,7 @@ import type { ComputedRef } from 'vue'
import { computed, inject } from 'vue'
import Draggable from 'vuedraggable'
import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import useViewColumns from '~/composables/useViewColumns'
import { useViewColumns } from '#imports'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiEyeIcon from '~icons/mdi/eye-off-outline'
import MdiDragIcon from '~icons/mdi/drag'
@ -43,7 +43,7 @@ const {
} = useViewColumns(activeView, meta as ComputedRef<TableType>, false, () => reloadDataHook?.trigger())
watch(
() => activeView?.value?.id,
() => (activeView?.value as any)?.id,
async (newVal, oldVal) => {
if (newVal !== oldVal && meta?.value) {
await loadViewColumns()
@ -59,7 +59,7 @@ watch(
{ immediate: true },
)
const onMove = (event) => {
const onMove = (event: unknown) => {
// todo : sync with server
// if (!sortedFields?.value) return
// if (sortedFields?.value.length - 1 === event.moved.newIndex) {

6
packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue

@ -3,9 +3,9 @@ import { ExportTypes } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import FileSaver from 'file-saver'
import { useNuxtApp } from '#app'
import useProject from '~/composables/useProject'
import { useProject } from '#imports'
import { ActiveViewInj, MetaInj } from '~/context'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { extractSdkResponseErrorMsg } from '~/utils'
import MdiFlashIcon from '~icons/mdi/flash-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiDownloadIcon from '~icons/mdi/download-outline'
@ -80,7 +80,7 @@ const exportCsv = async () => {
toast.success('Successfully exported all table data')
}
}
} catch (e) {
} catch (e: any) {
toast.error(extractSdkResponseErrorMsg(e))
}
}

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

@ -1,8 +1,7 @@
<script setup lang="ts">
import FieldListAutoCompleteDropdown from './FieldListAutoCompleteDropdown.vue'
import { computed, inject } from '#imports'
import { computed, inject, useViewSorts } from '#imports'
import { ActiveViewInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import useViewSorts from '~/composables/useViewSorts'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiSortIcon from '~icons/mdi/sort'
import MdiDeleteIcon from '~icons/mdi/close-box'
@ -18,7 +17,7 @@ const { sorts, saveOrUpdate, loadSorts, addSort, deleteSort } = useViewSorts(vie
const columns = computed(() => meta?.value?.columns || [])
watch(
() => view?.value?.id,
() => (view?.value as any)?.id,
() => {
loadSorts()
},

3
packages/nc-gui-v2/components/smartsheet/Cell.vue

@ -1,9 +1,8 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { provide } from 'vue'
import { computed } from '#imports'
import { computed, useColumn } from '#imports'
import { ColumnInj } from '~/context'
import useColumn from '~/composables/useColumn'
interface Props {
column: ColumnType

5
packages/nc-gui-v2/components/smartsheet/Gallery.vue

@ -1,8 +1,7 @@
<script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk'
import { inject, onKeyStroke, onMounted, provide } from '#imports'
import { inject, provide, useViewData } from '#imports'
import { ActiveViewInj, ChangePageInj, IsFormInj, IsGridInj, MetaInj, PaginationDataInj, ReadonlyInj } from '~/context'
import useViewData from '~/composables/useViewData'
const meta = inject(MetaInj)
const view = inject(ActiveViewInj)
@ -13,7 +12,7 @@ const isPublicView = false
const selected = reactive<{ row?: number | null; col?: number | null }>({})
const editEnabled = ref(false)
const { loadData, paginationData, formattedData: data, updateRowProperty, changePage } = useViewData(meta, view)
const { loadData, paginationData, formattedData: data, updateRowProperty, changePage } = useViewData(meta, view as any)
provide(IsFormInj, false)
provide(IsGridInj, false)

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

@ -1,8 +1,14 @@
<script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk'
import { inject, onKeyStroke, onMounted, provide } from '#imports'
import { useProvideColumnCreateStore } from '~/composables/useColumnCreateStore'
import useGridViewColumnWidth from '~/composables/useGridViewColumnWidth'
import {
inject,
onKeyStroke,
onMounted,
provide,
useGridViewColumnWidth,
useProvideColumnCreateStore,
useViewData,
} from '#imports'
import {
ActiveViewInj,
ChangePageInj,
@ -13,7 +19,6 @@ import {
PaginationDataInj,
ReloadViewDataHookInj,
} from '~/context'
import useViewData from '~/composables/useViewData'
import MdiPlusIcon from '~icons/mdi/plus'
const meta = inject(MetaInj)
@ -29,7 +34,7 @@ const selected = reactive<{ row?: number | null; col?: number | null }>({})
const editEnabled = ref(false)
const addColumnDropdown = ref(false)
const { loadData, paginationData, formattedData: data, updateRowProperty, changePage } = useViewData(meta, view)
const { loadData, paginationData, formattedData: data, updateRowProperty, changePage } = useViewData(meta, view as any)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view)
onMounted(loadGridViewColumns)
@ -55,7 +60,7 @@ onKeyStroke(['Enter'], (e) => {
})
watch(
() => view?.value?.id,
() => (view?.value as any)?.id,
async (n?: string, o?: string) => {
if (n && n !== o) {
await loadData()

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

@ -73,7 +73,7 @@ export default {
@keydown.enter="changePage(page)"
>
<template #append>
<MdiKeyboardIcon small icon.class="mt-1" @click="changePage(page)" />
<MdiKeyboardIcon class="mt-1" @click="changePage(page)" />
</template>
</v-text-field>
</div>

2
packages/nc-gui-v2/components/smartsheet/Toolbar.vue

@ -1,5 +1,3 @@
<script setup lang="ts"></script>
<template>
<div class="nc-table-toolbar w-full py-1 flex gap-1 items-center" style="z-index: 7">
<SmartsheetToolbarSearchData class="flex-shrink" />

3
packages/nc-gui-v2/components/smartsheet/VirtualCell.vue

@ -1,8 +1,7 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { provide } from '#imports'
import { provide, useVirtualCell } from '#imports'
import { ColumnInj } from '~/context'
import useVirtualCell from '~/composables/useVirtualCell'
interface Props {
column: ColumnType

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

@ -1,17 +1,23 @@
<script setup lang="ts">
import type { ColumnType, ViewType } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
import { computed, inject, provide, watch, watchEffect } from '#imports'
import SmartsheetGrid from '../smartsheet/Grid.vue'
import { computed, inject, provide, useMetas, watch, watchEffect } from '#imports'
import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj, TabMetaInj } from '~/context'
import useMetas from '~/composables/useMetas'
import type { TabItem } from '~/composables'
const { getMeta, metas } = useMetas()
const activeView = ref<ViewType>()
const el = ref<any>()
const activeView = ref()
const el = ref<typeof SmartsheetGrid>()
const fields = ref<ColumnType[]>([])
const tabMeta = inject(TabMetaInj)
const tabMeta = inject(
TabMetaInj,
computed(() => ({} as TabItem)),
)
const meta = computed(() => metas.value?.[tabMeta?.value?.id as string])
@ -29,12 +35,9 @@ provide(ReloadViewDataHookInj, reloadEventHook)
provide(FieldsInj, fields)
provide('navDrawerOpen', ref(true))
watch(
() => tabMeta && tabMeta?.id,
async (newVal, oldVal) => {
if (newVal !== oldVal) await getMeta(newVal)
},
)
watch(tabMeta, async (newTabMeta, oldTabMeta) => {
if (newTabMeta !== oldTabMeta && newTabMeta.id) await getMeta(newTabMeta.id)
})
</script>
<template>

2
packages/nc-gui-v2/components/tabs/auth/user-management/FeedbackForm.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import CloseIcon from '~icons/material-symbols/close-rounded'
const { feedbackForm } = $(useGlobalState())
const { feedbackForm } = useGlobal()
</script>
<template>

2
packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue

@ -2,7 +2,7 @@
import type { ColumnType } from 'nocodb-sdk'
import ItemChip from './components/ItemChip.vue'
import { ColumnInj } from '~/context'
import useBelongsTo from '~/composables/useBelongsTo'
import { useBelongsTo } from '#imports'
const column = inject(ColumnInj)
const value = inject('value')

3
packages/nc-gui-v2/components/virtual-cell/HasMany.vue

@ -2,7 +2,8 @@
import type { ColumnType } from 'nocodb-sdk'
import ItemChip from './components/ItemChip.vue'
import { ColumnInj } from '~/context'
import useHasMany from '~/composables/useHasMany'
import { useHasMany } from '#imports'
const column = inject(ColumnInj)
const value = inject('value')
const active = false

3
packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue

@ -2,7 +2,8 @@
import type { ColumnType } from 'nocodb-sdk'
import ItemChip from './components/ItemChip.vue'
import { ColumnInj } from '~/context'
import useManyToMany from '~/composables/useManyToMany'
import { useManyToMany } from '#imports'
const column = inject(ColumnInj)
const value = inject('value')
const active = false

2
packages/nc-gui-v2/components/virtual-cell/Rollup.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
const { value } = defineProps<{ value: any }>()
const { value } = defineProps<{ value?: any }>()
</script>
<template>

10
packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue

@ -14,16 +14,10 @@ const readonly = inject(ReadonlyInj, false)
<template>
<v-chip class="chip" :class="{ active }" small text-color="textColor">
<!--
:color="isDark ? '' : 'primary lighten-5'"
@click="!readonly && active && $emit('edit', item)" -->
<span class="name" :title="value">{{ value }}</span>
<span class="name">{{ value }}</span>
<!-- && _isUIAllowed('xcDatatableEditable') -->
<div v-show="active" v-if="!readonly" class="mr-n1 ml-2">
<MdiCloseThickIcon class="unlink-icon">
<!-- @click.stop="$emit('unlink', item)" -->
</MdiCloseThickIcon>
<MdiCloseThickIcon class="unlink-icon" />
</div>
</v-chip>
</template>

2
packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue

@ -148,7 +148,7 @@ export default {
:tooltip="`Unlink this '${meta.title}' from '${parentMeta.title}'`"
:color="['error', 'grey']"
small
icon.class="mr-1 mt-n1"
class="mr-1 mt-n1"
@click.stop="$emit('unlink', ch, i)"
>
mdi-link-variant-remove

2
packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue

@ -122,7 +122,7 @@ export default {
@keydown.enter="loadData"
>
<template #append>
<x-icon tooltip="Apply filter" small icon.class="mt-1" @click="loadData"> mdi-keyboard-return </x-icon>
<x-icon tooltip="Apply filter" small icon class="mt-1" @click="loadData"> mdi-keyboard-return </x-icon>
</template>
</v-text-field>
<v-spacer />

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

@ -0,0 +1,20 @@
export * from './useApi'
export * from './useGlobal'
export * from './useUIPermission'
export * from './useAttachment'
export * from './useBelongsTo'
export * from './useColors'
export * from './useColumn'
export * from './useGridViewColumnWidth'
export * from './useHasMany'
export * from './useManyToMany'
export * from './useMetas'
export * from './useProject'
export * from './useTableCreate'
export * from './useTabs'
export * from './useViewColumns'
export * from './useViewData'
export * from './useViewFilters'
export * from './useViews'
export * from './useViewSorts'
export * from './useVirtualCell'

141
packages/nc-gui-v2/composables/useApi/index.ts

@ -1,65 +1,118 @@
import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { AxiosError, AxiosResponse } from 'axios'
import { Api } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { EventHook, MaybeRef } from '@vueuse/core'
import { addAxiosInterceptors } from './interceptors'
import { createEventHook, ref, unref, useNuxtApp } from '#imports'
import type { NuxtApp } from '#app'
interface UseApiReturn<D = any, R = any> {
api: Api<any>
isLoading: Ref<boolean>
error: Ref<AxiosError<D, R> | null>
response: Ref<AxiosResponse<D, R> | null>
onError: EventHook<AxiosError<D, R>>['on']
onResponse: EventHook<AxiosResponse<D, R>>['on']
}
export function createApiInstance(app: NuxtApp, baseURL = 'http://localhost:8080') {
const api = new Api({
baseURL,
})
addAxiosInterceptors(api, app)
return api
}
/** todo: add props? */
interface UseApiProps<D = any> {
axiosConfig?: MaybeRef<AxiosRequestConfig<D>>
useGlobalInstance?: MaybeRef<boolean>
import type { CreateApiOptions, UseApiProps, UseApiReturn } from './types'
import { createEventHook, ref, unref, useCounter, useGlobal, useNuxtApp } from '#imports'
export function createApiInstance<SecurityDataType = any>(options: CreateApiOptions = {}): Api<SecurityDataType> {
return addAxiosInterceptors(
new Api<SecurityDataType>({
baseURL: options.baseURL ?? 'http://localhost:8080',
}),
)
}
export function useApi<Data = any, RequestConfig = any>(props: UseApiProps<Data> = {}): UseApiReturn<Data, RequestConfig> {
/**
* Api composable that provides loading, error and response refs, as well as event hooks for error and response.
*
* You can use this composable to generate a fresh api instance with its own loading and error refs.
*
* Any request called by useApi will be pushed into the global requests counter which toggles the global loading state.
*
* @example
* ```js
* const { api, isLoading, error, response, onError, onResponse } = useApi()
*
* const onSignIn = async () => {
* const { token } = await api.auth.signIn(form)
* }
*/
export function useApi<Data = any, RequestConfig = any>({
useGlobalInstance = false,
apiOptions,
axiosConfig,
}: UseApiProps<Data> = {}): UseApiReturn<Data, RequestConfig> {
const state = useGlobal()
/**
* Local state of running requests, do not confuse with global state of running requests
* This state is only counting requests made by this instance of `useApi` and not by other instances.
*/
const { count, inc, dec } = useCounter(0)
/** is request loading */
const isLoading = ref(false)
/** latest request error */
const error = ref(null)
const response = ref<any>(null)
/** latest request response */
const response = ref<unknown | null>(null)
const errorHook = createEventHook<AxiosError<Data, RequestConfig>>()
const responseHook = createEventHook<AxiosResponse<Data, RequestConfig>>()
const api = unref(props.useGlobalInstance) ? useNuxtApp().$api : createApiInstance(useNuxtApp())
/** global api instance */
const $api = useNuxtApp().$api
/** api instance - with interceptors for token refresh already bound */
const api = useGlobalInstance && !!$api ? $api : createApiInstance(apiOptions)
/** set loading to true and increment local and global request counter */
function onRequestStart() {
isLoading.value = true
/** local count */
inc()
/** global count */
state.runningRequests.inc()
}
/** decrement local and global request counter and check if we can stop loading */
function onRequestFinish() {
/** local count */
dec()
/** global count */
state.runningRequests.dec()
/** try to stop loading */
stopLoading()
}
/** set loading state to false *only* if no request is still running */
function stopLoading() {
if (count.value === 0) {
isLoading.value = false
}
}
/** reset response and error refs */
function reset() {
error.value = null
response.value = null
}
api.instance.interceptors.request.use(
(config) => {
error.value = null
response.value = null
isLoading.value = true
reset()
onRequestStart()
return {
...config,
...unref(props),
...unref(axiosConfig),
}
},
(requestError) => {
errorHook.trigger(requestError)
error.value = requestError
response.value = null
isLoading.value = false
onRequestFinish()
return requestError
},
@ -68,20 +121,28 @@ export function useApi<Data = any, RequestConfig = any>(props: UseApiProps<Data>
api.instance.interceptors.response.use(
(apiResponse) => {
responseHook.trigger(apiResponse as AxiosResponse<Data, RequestConfig>)
// can't properly typecast
response.value = apiResponse
isLoading.value = false
onRequestFinish()
return apiResponse
},
(apiError) => {
errorHook.trigger(apiError)
error.value = apiError
isLoading.value = false
onRequestFinish()
return apiError
},
)
return { api, isLoading, response, error, onError: errorHook.on, onResponse: responseHook.on }
return {
api,
isLoading,
response: response as Ref<AxiosResponse<Data, RequestConfig>>,
error,
onError: errorHook.on,
onResponse: responseHook.on,
}
}

16
packages/nc-gui-v2/composables/useApi/interceptors.ts

@ -1,17 +1,17 @@
import type { Api } from 'nocodb-sdk'
import { navigateTo, useRoute, useRouter } from '#imports'
import type { NuxtApp } from '#app'
import { navigateTo, useGlobal, useRoute, useRouter } from '#imports'
const DbNotFoundMsg = 'Database config not found'
export function addAxiosInterceptors(api: Api<any>, app: NuxtApp) {
export function addAxiosInterceptors(api: Api<any>) {
const state = useGlobal()
const router = useRouter()
const route = useRoute()
api.instance.interceptors.request.use((config) => {
config.headers['xc-gui'] = 'true'
if (app.$state.token.value) config.headers['xc-auth'] = app.$state.token.value
if (state.token.value) config.headers['xc-auth'] = state.token.value
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) {
// config.headers['xc-preview'] = store.state.users.previewAs
@ -38,7 +38,7 @@ export function addAxiosInterceptors(api: Api<any>, app: NuxtApp) {
// Logout user if token refresh didn't work or user is disabled
if (error.config.url === '/auth/refresh-token') {
app.$state.signOut()
state.signOut()
return Promise.reject(error)
}
@ -52,7 +52,7 @@ export function addAxiosInterceptors(api: Api<any>, app: NuxtApp) {
// New request with new token
const config = error.config
config.headers['xc-auth'] = token.data.token
app.$state.signIn(token.data.token)
state.signIn(token.data.token)
return new Promise((resolve, reject) => {
api.instance
@ -66,7 +66,7 @@ export function addAxiosInterceptors(api: Api<any>, app: NuxtApp) {
})
})
.catch(async (error) => {
app.$state.signOut()
state.signOut()
// todo: handle new user
navigateTo('/signIn')
@ -75,4 +75,6 @@ export function addAxiosInterceptors(api: Api<any>, app: NuxtApp) {
})
},
)
return api
}

26
packages/nc-gui-v2/composables/useApi/types.ts

@ -0,0 +1,26 @@
import type { Api } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { EventHook, MaybeRef } from '@vueuse/core'
export interface UseApiReturn<D = any, R = any> {
api: Api<any>
isLoading: Ref<boolean>
error: Ref<AxiosError<D, R> | null>
response: Ref<AxiosResponse<D, R> | null>
onError: EventHook<AxiosError<D, R>>['on']
onResponse: EventHook<AxiosResponse<D, R>>['on']
}
/** {@link Api} options */
export interface CreateApiOptions {
baseURL?: string
}
export interface UseApiProps<D = any> {
/** additional axios config for requests */
axiosConfig?: MaybeRef<AxiosRequestConfig<D>>
/** {@link Api} options */
apiOptions?: CreateApiOptions
useGlobalInstance?: boolean
}

4
packages/nc-gui-v2/composables/useAttachment.ts

@ -1,5 +1,5 @@
// todo:
export default () => {
// todo: implement useAttachment
export function useAttachment() {
const localFilesState = reactive([])
const attachments = ref([])

9
packages/nc-gui-v2/composables/useBelongsTo.ts

@ -1,15 +1,14 @@
import type { ColumnType, TableType } from 'nocodb-sdk'
import type LinkToAnotherRecordColumn from '../../nocodb/src/lib/models/LinkToAnotherRecordColumn'
import useMetas from '~/composables/useMetas'
import { useMetas } from './useMetas'
export default function (column: ColumnType) {
export function useBelongsTo(column: ColumnType) {
const { metas, getMeta } = useMetas()
const parentMeta = computed<TableType>(() => {
return metas.value?.[(column.colOptions as LinkToAnotherRecordColumn)?.fk_related_model_id as string]
return metas.value?.[(column.colOptions as any)?.fk_related_model_id as string]
})
const loadParentMeta = async () => {
await getMeta((column.colOptions as LinkToAnotherRecordColumn)?.fk_related_model_id as string)
await getMeta((column.colOptions as any)?.fk_related_model_id as string)
}
const primaryValueProp = computed(() => {

4
packages/nc-gui-v2/composables/useColors.ts

@ -1,9 +1,9 @@
import type { MaybeRef } from '@vueuse/core'
import { computed, effectScope, tryOnScopeDispose, unref, watch, watchEffect } from '#build/imports'
import { useNuxtApp } from '#app'
import theme from '~/utils/colorsUtils'
import { theme } from '~/utils'
export default function useColors(darkMode?: MaybeRef<boolean>) {
export function useColors(darkMode?: MaybeRef<boolean>) {
const scope = effectScope()
let mode = $ref(unref(darkMode))

2
packages/nc-gui-v2/composables/useColumn.ts

@ -2,7 +2,7 @@ import type { ColumnType } from 'nocodb-sdk'
import { SqlUiFactory, UITypes, isVirtualCol } from 'nocodb-sdk'
import { useProject } from '#imports'
export default (column: ColumnType) => {
export function useColumn(column: ColumnType) {
const { project } = useProject()
const uiDatatype: UITypes = (column && column.uidt) as UITypes

4
packages/nc-gui-v2/composables/useColumnCreateStore.ts

@ -4,9 +4,9 @@ import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useToast } from 'vue-toastification'
import { useColumn } from './useColumn'
import { computed } from '#imports'
import { useNuxtApp } from '#app'
import useColumn from '~/composables/useColumn'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
const useForm = Form.useForm
@ -193,7 +193,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
toast.success('Column created')
}
onSuccess()
} catch (e) {
} catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e))
}
}

54
packages/nc-gui-v2/composables/useGlobal/actions.ts

@ -0,0 +1,54 @@
import { notification } from 'ant-design-vue'
import type { Actions, State } from './types'
import { useNuxtApp } from '#imports'
export function useGlobalActions(state: State): Actions {
// todo replace with just `new Api()`? Would solve recursion issues
/** we have to use the globally injected api instance, otherwise we run into recursion as `useApi` calls `useGlobal` */
const { $api } = useNuxtApp()
/** Sign out by deleting the token from localStorage */
const signOut: Actions['signOut'] = () => {
state.token.value = null
state.user.value = null
}
/** Sign in by setting the token in localStorage */
const signIn: Actions['signIn'] = async (newToken) => {
state.token.value = newToken
if (state.jwtPayload.value) {
state.user.value = {
id: state.jwtPayload.value.id,
email: state.jwtPayload.value.email,
firstname: state.jwtPayload.value.firstname,
lastname: state.jwtPayload.value.lastname,
roles: state.jwtPayload.value.roles,
}
}
}
/** manually try to refresh token */
const refreshToken = async () => {
$api.instance
.post('/auth/refresh-token', null, {
withCredentials: true,
})
.then((response) => {
if (response.data?.token) {
signIn(response.data.token)
}
})
.catch((err) => {
notification.error({
// todo: add translation
message: err.message || 'You have been signed out.',
})
console.error(err)
signOut()
})
}
return { signIn, signOut, refreshToken }
}

25
packages/nc-gui-v2/composables/useGlobal/getters.ts

@ -0,0 +1,25 @@
import type { Getters, State } from './types'
import { computed } from '#imports'
export function useGlobalGetters(state: State): Getters {
/** Verify that a user is signed in by checking if token exists and is not expired */
const signedIn: Getters['signedIn'] = computed(
() =>
!!(
!!state.token &&
state.token.value !== '' &&
state.jwtPayload.value &&
state.jwtPayload.value.exp &&
state.jwtPayload.value.exp > state.timestamp.value / 1000
),
)
/** global loading state */
let loading = $ref(false)
const isLoading = computed({
get: () => state.runningRequests.count.value > 0 || loading,
set: (_loading) => (loading = _loading),
})
return { signedIn, isLoading }
}

71
packages/nc-gui-v2/composables/useGlobal/index.ts

@ -0,0 +1,71 @@
import { useGlobalState } from './state'
import { useGlobalActions } from './actions'
import type { UseGlobalReturn } from './types'
import { useGlobalGetters } from './getters'
import { useNuxtApp, watch } from '#imports'
/**
* Global state is injected by {@link import('~/plugins/state') state} plugin into our nuxt app (available as `$state`).
* You can still call `useGlobal` to receive the `$state` object and access the global state.
* If it's not available yet, a new global state object is created and injected into the nuxt app.
*
* Part of the state is stored in {@link WindowLocalStorage localStorage}, so it will be available even if the user closes the browser tab.
* Check the {@link StoredState StoredState} type for more information.
*
* @example
* ```js
* import { useNuxtApp } from '#app'
*
* const { $state } = useNuxtApp()
*
* const token = $state.token.value
* const user = $state.user.value
* ```
*
* @example
* ```js
* import { useGlobal } from '#imports'
*
* const globalState = useGlobal()
*
* cont token = globalState.token.value
* const user = globalState.user.value
*
* console.log(state.isLoading.value) // isLoading = true if any api request is still running
* ```
*/
export const useGlobal = (): UseGlobalReturn => {
const { $state, provide } = useNuxtApp()
/** If state already exists, return it */
if (typeof $state !== 'undefined') return $state
const state = useGlobalState()
const getters = useGlobalGetters(state)
const actions = useGlobalActions(state)
/** try to refresh token before expiry (5 min before expiry) */
watch(
() =>
!!(
state.jwtPayload.value &&
state.jwtPayload.value.exp &&
state.jwtPayload.value.exp - 5 * 60 < state.timestamp.value / 1000
),
async (expiring) => {
if (getters.signedIn.value && state.jwtPayload.value && expiring) {
await actions.refreshToken()
}
},
{ immediate: true },
)
const globalState = { ...state, ...getters, ...actions } as UseGlobalReturn
/** provide a fresh state instance into nuxt app */
provide('state', globalState)
return globalState
}

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

@ -0,0 +1,95 @@
import { usePreferredLanguages, useStorage } from '@vueuse/core'
import { useJwt } from '@vueuse/integrations/useJwt'
import type { JwtPayload } from 'jwt-decode'
import type { State, StoredState } from './types'
import { computed, ref, toRefs, useCounter, useNuxtApp, useTimestamp } from '#imports'
import type { User } from '~/lib'
export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
/** get the preferred languages of a user, according to browser settings */
const preferredLanguages = $(usePreferredLanguages())
/** todo: reimplement; get the preferred dark mode setting, according to browser settings */
// const prefersDarkMode = $(usePreferredDark())
const prefersDarkMode = false
/** reactive timestamp to check token expiry against */
const timestamp = useTimestamp({ immediate: true, interval: 100 })
const {
vueApp: { i18n },
} = useNuxtApp()
/**
* Set initial language based on browser settings.
* If the user has not set a preferred language, we fallback to 'en'.
* If the user has set a preferred language, we try to find a matching locale in the available locales.
*/
const preferredLanguage = preferredLanguages.reduce((locale, language) => {
/** split language to language and code, e.g. en-GB -> [en, GB] */
const [lang, code] = language.split(/[_-]/)
/** find all locales that match the language */
let availableLocales = i18n.availableLocales.filter((locale) => locale.startsWith(lang))
/** If we can match more than one locale, we check if the code of the language matches as well */
if (availableLocales.length > 1) {
availableLocales = availableLocales.filter((locale) => locale.endsWith(code))
}
/** if there are still multiple locales, pick the first one */
const availableLocale = availableLocales[0]
/** if we found a matching locale, return it */
if (availableLocale) locale = availableLocale
return locale
}, 'en' /** fallback locale */)
/** State */
const initialState: StoredState = {
token: null,
user: null,
lang: preferredLanguage,
darkMode: prefersDarkMode,
feedbackForm: {
url: 'https://docs.google.com/forms/d/e/1FAIpQLSeTlAfZjszgr53lArz3NvUEnJGOT9JtG9NAU5d0oQwunDS2Pw/viewform?embedded=true',
createdAt: new Date('2020-01-01T00:00:00.000Z').toISOString(),
isHidden: false,
},
}
/** saves a reactive state, any change to these values will write/delete to localStorage */
const storage = useStorage<StoredState>(storageKey, initialState)
/** force turn off of dark mode, regardless of previously stored settings */
storage.value.darkMode = false
/** current token ref, used by `useJwt` to reactively parse our token payload */
const token = computed({
get: () => storage.value.token || '',
set: (val) => (storage.value.token = val),
})
/** reactive token payload */
const { payload } = useJwt<JwtPayload & User>(token)
/** is sidebar open */
const sidebarOpen = ref(false)
/** currently running requests */
const runningRequests = useCounter()
/** global error */
const error = ref()
return {
...toRefs(storage.value),
storage,
token,
jwtPayload: payload,
sidebarOpen,
timestamp,
runningRequests,
error,
}
}

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

@ -0,0 +1,45 @@
import type { ComputedRef, Ref, ToRefs } from 'vue'
import type { WritableComputedRef } from '@vue/reactivity'
import type { JwtPayload } from 'jwt-decode'
import type { User } from '~/lib'
import type { useCounter } from '#imports'
export interface FeedbackForm {
url: string
createdAt: string
isHidden: boolean
lastFormPollDate?: string
}
export interface StoredState {
token: string | null
user: User | null
lang: string
darkMode: boolean
feedbackForm: FeedbackForm
}
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>
}
export interface Getters {
signedIn: ComputedRef<boolean>
isLoading: WritableComputedRef<boolean>
}
export interface Actions {
signOut: () => void
signIn: (token: string) => void
refreshToken: () => void
}
export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State, 'token' | 'user'>
export type UseGlobalReturn = Getters & Actions & ReadonlyState

169
packages/nc-gui-v2/composables/useGlobalState/index.ts

@ -1,169 +0,0 @@
import { breakpointsTailwind, usePreferredLanguages, useStorage } from '@vueuse/core'
import { useJwt } from '@vueuse/integrations/useJwt'
import type { JwtPayload } from 'jwt-decode'
import { notification } from 'ant-design-vue'
import initialFeedBackForm from './initialFeedbackForm'
import { computed, ref, toRefs, useBreakpoints, useNuxtApp, useTimestamp, watch } from '#imports'
import type { Actions, Getters, GlobalState, StoredState, User } from '~/lib/types'
const storageKey = 'nocodb-gui-v2'
/**
* Global state is injected by {@link import('~/plugins/state') state} plugin into our nuxt app (available as `$state`).
* Manual initialization is unnecessary and should be avoided.
*
* The state is stored in {@link WindowLocalStorage localStorage}, so it will be available even if the user closes the browser tab.
*
* @example
* ```js
* import { useNuxtApp } from '#app'
*
* const { $state } = useNuxtApp()
*
* const token = $state.token.value
* const user = $state.user.value
* ```
*/
export const useGlobalState = (): GlobalState => {
const { $state } = useNuxtApp()
if ($state) {
console.warn(
'[useGlobalState] Global state is injected by state plugin. Manual initialization is unnecessary and should be avoided.',
)
return $state
}
/** get the preferred languages of a user, according to browser settings */
const preferredLanguages = $(usePreferredLanguages())
/** todo: reimplement; get the preferred dark mode setting, according to browser settings */
// const prefersDarkMode = $(usePreferredDark())
const prefersDarkMode = false
/** get current breakpoints (for enabling sidebar) */
const breakpoints = useBreakpoints(breakpointsTailwind)
/** reactive timestamp to check token expiry against */
const timestamp = $(useTimestamp({ immediate: true, interval: 100 }))
const {
$api,
vueApp: { i18n },
} = useNuxtApp()
/**
* Set initial language based on browser settings.
* If the user has not set a preferred language, we fallback to 'en'.
* If the user has set a preferred language, we try to find a matching locale in the available locales.
*/
const preferredLanguage = preferredLanguages.reduce<string>((locale, language) => {
/** split language to language and code, e.g. en-GB -> [en, GB] */
const [lang, code] = language.split(/[_-]/)
/** find all locales that match the language */
let availableLocales = i18n.availableLocales.filter((locale) => locale.startsWith(lang))
/** If we can match more than one locale, we check if the code of the language matches as well */
if (availableLocales.length > 1) {
availableLocales = availableLocales.filter((locale) => locale.endsWith(code))
}
/** if there are still multiple locales, pick the first one */
const availableLocale = availableLocales[0]
/** if we found a matching locale, return it */
if (availableLocale) locale = availableLocale
return locale
}, 'en' /** fallback locale */)
/** State */
const initialState: StoredState = {
token: null,
user: null,
lang: preferredLanguage,
darkMode: prefersDarkMode,
feedbackForm: initialFeedBackForm,
}
/** saves a reactive state, any change to these values will write/delete to localStorage */
const storage = $(useStorage<StoredState>(storageKey, initialState))
/** force turn off of dark mode, regardless of previously stored settings */
storage.darkMode = false
/** current token ref, used by `useJwt` to reactively parse our token payload */
let token = $computed({
get: () => storage.token || '',
set: (val) => (storage.token = val),
})
/** reactive token payload */
const { payload } = $(useJwt<JwtPayload & User>($$(token!)))
/** Getters */
/** Verify that a user is signed in by checking if token exists and is not expired */
const signedIn: Getters['signedIn'] = computed(
() => !!(!!token && token !== '' && payload && payload.exp && payload.exp > timestamp / 1000),
)
/** is sidebar open */
const sidebarOpen = ref(signedIn.value && breakpoints.greater('md').value)
/** Actions */
/** Sign out by deleting the token from localStorage */
const signOut: Actions['signOut'] = () => {
storage.token = null
storage.user = null
}
/** Sign in by setting the token in localStorage */
const signIn: Actions['signIn'] = async (newToken) => {
token = newToken
if (payload) {
storage.user = {
id: payload.id,
email: payload.email,
firstname: payload.firstname,
lastname: payload.lastname,
roles: payload.roles,
}
}
}
/** manually try to refresh token */
const refreshToken = async () => {
$api.instance
.post('/auth/refresh-token', null, {
withCredentials: true,
})
.then((response) => {
if (response.data?.token) {
signIn(response.data.token)
}
})
.catch((err) => {
notification.error({
// todo: add translation
message: err.message || 'You have been signed out.',
})
console.error(err)
signOut()
})
}
/** try to refresh token before expiry (5 min before expiry) */
watch(
() => !!(payload && payload.exp && payload.exp - 5 * 60 < timestamp / 1000),
async (expiring) => {
if (signedIn.value && payload && expiring) {
await refreshToken()
}
},
{ immediate: true },
)
return { ...toRefs(storage), signedIn, signOut, signIn, sidebarOpen }
}

5
packages/nc-gui-v2/composables/useGlobalState/initialFeedBackForm.ts

@ -1,5 +0,0 @@
export default {
url: 'https://docs.google.com/forms/d/e/1FAIpQLSeTlAfZjszgr53lArz3NvUEnJGOT9JtG9NAU5d0oQwunDS2Pw/viewform?embedded=true',
createdAt: new Date('2020-01-01T00:00:00.000Z').toISOString(),
isHidden: false,
}

6
packages/nc-gui-v2/composables/useGridViewColumnWidth.ts

@ -1,11 +1,11 @@
import { useStyleTag } from '@vueuse/core'
import type { ColumnType, GridColumnType, GridType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import useMetas from '~/composables/useMetas'
import useUIPermission from '~/composables/useUIPermission'
import { useMetas } from './useMetas'
import { useUIPermission } from './useUIPermission'
// todo: update swagger
export default (view: Ref<GridType & { id?: string }>) => {
export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) | undefined>) {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useUIPermission()
const { $api } = useNuxtApp()

9
packages/nc-gui-v2/composables/useHasMany.ts

@ -1,15 +1,14 @@
import type { ColumnType, TableType } from 'nocodb-sdk'
import type LinkToAnotherRecordColumn from '../../nocodb/src/lib/models/LinkToAnotherRecordColumn'
import useMetas from '~/composables/useMetas'
import { useMetas } from './useMetas'
export default function (column: ColumnType) {
export function useHasMany(column: ColumnType) {
const { metas, getMeta } = useMetas()
const childMeta = computed<TableType>(() => {
return metas.value?.[(column.colOptions as LinkToAnotherRecordColumn)?.fk_related_model_id as string]
return metas.value?.[(column.colOptions as any)?.fk_related_model_id as string]
})
const loadChildMeta = async () => {
await getMeta((column.colOptions as LinkToAnotherRecordColumn)?.fk_related_model_id as string)
await getMeta((column.colOptions as any)?.fk_related_model_id as string)
}
const primaryValueProp = computed(() => {

9
packages/nc-gui-v2/composables/useManyToMany.ts

@ -1,15 +1,14 @@
import type { ColumnType, TableType } from 'nocodb-sdk'
import type LinkToAnotherRecordColumn from '../../nocodb/src/lib/models/LinkToAnotherRecordColumn'
import useMetas from '~/composables/useMetas'
import { useMetas } from './useMetas'
export default function (column: ColumnType) {
export function useManyToMany(column: ColumnType) {
const { metas, getMeta } = useMetas()
const childMeta = computed<TableType>(() => {
return metas.value?.[(column.colOptions as LinkToAnotherRecordColumn)?.fk_related_model_id as string]
return metas.value?.[(column.colOptions as any)?.fk_related_model_id as string]
})
const loadChildMeta = async () => {
await getMeta((column.colOptions as LinkToAnotherRecordColumn)?.fk_related_model_id as string)
await getMeta((column.colOptions as any)?.fk_related_model_id as string)
}
const primaryValueProp = computed(() => {

4
packages/nc-gui-v2/composables/useMetas.ts

@ -1,8 +1,8 @@
import type { TableInfoType, TableType } from 'nocodb-sdk'
import { useProject } from './useProject'
import { useNuxtApp, useState } from '#app'
import { useProject } from '#imports'
export default () => {
export function useMetas() {
const { $api } = useNuxtApp()
const { tables } = useProject()

4
packages/nc-gui-v2/composables/useProject.ts

@ -2,9 +2,9 @@ import { SqlUiFactory } from 'nocodb-sdk'
import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp, useState } from '#app'
import { USER_PROJECT_ROLES } from '~/lib/constants'
import { USER_PROJECT_ROLES } from '~/lib'
export default (projectId?: MaybeRef<string>) => {
export function useProject(projectId?: MaybeRef<string>) {
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const { $api } = useNuxtApp()

4
packages/nc-gui-v2/composables/useTableCreate.ts

@ -1,9 +1,9 @@
import type { TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { useProject } from './useProject'
import { useNuxtApp } from '#app'
import { useProject } from '#imports'
export default (onTableCreate?: (tableMeta: TableType) => void) => {
export function useTableCreate(onTableCreate?: (tableMeta: TableType) => void) {
const table = reactive<{ title: string; table_name: string; columns: string[] }>({
title: '',
table_name: '',

2
packages/nc-gui-v2/composables/useTabs.ts

@ -20,7 +20,7 @@ function getPredicate(key: Partial<TabItem>) {
(!('type' in key) || tab.type === key.id)
}
export default () => {
export function useTabs() {
const tabs = useState<TabItem[]>('tabs', () => [])
const route = useRoute()

14
packages/nc-gui-v2/composables/useUIPermission/index.ts

@ -1,12 +1,13 @@
import type { RolePermissions } from './rolePermissions'
import rolePermissions from './rolePermissions'
import { useState } from '#app'
import { USER_PROJECT_ROLES } from '~/lib/constants'
export default () => {
export function useUIPermission() {
const { $state } = useNuxtApp()
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const isUIAllowed = (permission: string, _skipPreviewAs = false) => {
const isUIAllowed = (permission: RolePermissions, _skipPreviewAs = false) => {
const user = $state.user
let userRoles = user?.value?.roles || {}
@ -32,7 +33,14 @@ export default () => {
// }
return Object.entries(roles).some(([role, hasRole]) => {
return hasRole && (rolePermissions[role] === '*' || (rolePermissions[role] as Record<string, boolean>)?.[permission])
const rolePermission = rolePermissions[role as keyof typeof rolePermissions]
return (
hasRole &&
(rolePermission === '*' || typeof rolePermission === 'object'
? rolePermission[permission as keyof typeof rolePermission] === true
: false)
)
})
}

15
packages/nc-gui-v2/composables/useUIPermission/rolePermissions.ts

@ -1,4 +1,4 @@
const permissions: Record<string, Record<string, boolean> | '*'> = {
const rolePermissions = {
creator: '*',
owner: '*',
guest: {},
@ -34,6 +34,15 @@ const permissions: Record<string, Record<string, boolean> | '*'> = {
projectActions: true,
projectSettings: true,
},
}
} as const
export default permissions
export default rolePermissions
type GetKeys<T> = T extends Record<string, any> ? keyof T : never
export type RolePermissions<T extends typeof rolePermissions = typeof rolePermissions, K extends keyof T = keyof T> =
| T[K] extends string
? T[K]
: never & T[K] extends Record<string, any>
? GetKeys<T[K]>
: never

2
packages/nc-gui-v2/composables/useViewColumns.ts

@ -4,7 +4,7 @@ import { watch } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import { useNuxtApp } from '#app'
export default function (
export function useViewColumns(
view: Ref<(GridType | FormType | GalleryType) & { id?: string }> | undefined,
meta: ComputedRef<TableType>,
isPublic = false,

6
packages/nc-gui-v2/composables/useViewData.ts

@ -2,7 +2,7 @@ import type { Api, FormType, GalleryType, GridType, PaginatedType, TableType } f
import type { ComputedRef, Ref } from 'vue'
import { useNuxtApp } from '#app'
import { useProject } from '#imports'
import { NOCO } from '~/lib/constants'
import { NOCO } from '~/lib'
const formatData = (list: Record<string, any>[]) =>
list.map((row) => ({
@ -11,13 +11,13 @@ const formatData = (list: Record<string, any>[]) =>
rowMeta: {},
}))
export default (
export function useViewData(
meta: Ref<TableType> | ComputedRef<TableType> | undefined,
viewMeta:
| Ref<(GridType | GalleryType | FormType) & { id: string }>
| ComputedRef<(GridType | GalleryType | FormType) & { id: string }>
| undefined,
) => {
) {
const data = ref<Record<string, any>[]>()
const formattedData = ref<{ row: Record<string, any>; oldRow: Record<string, any>; rowMeta?: any }[]>()
const paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 })

2
packages/nc-gui-v2/composables/useViewFilters.ts

@ -2,7 +2,7 @@ import type { FilterType, GalleryType, GridType, KanbanType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useNuxtApp } from '#imports'
export default function (
export function useViewFilters(
view: Ref<(GridType | KanbanType | GalleryType) & { id?: string }> | undefined,
parentId?: string,
reloadData?: () => void,

2
packages/nc-gui-v2/composables/useViewSorts.ts

@ -2,7 +2,7 @@ import type { GalleryType, GridType, KanbanType, SortType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useNuxtApp } from '#imports'
export default function (
export function useViewSorts(
view: Ref<(GridType | KanbanType | GalleryType) & { id?: string }> | undefined,
reloadData?: () => void,
) {

8
packages/nc-gui-v2/composables/useViews.ts

@ -1,9 +1,8 @@
import type { FormType, GalleryType, GridType, KanbanType, TableType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import type { WatchOptions } from '@vue/runtime-core'
import { useNuxtApp } from '#app'
export default function (meta: MaybeRef<TableType | undefined>, watchOptions: WatchOptions = {}) {
export function useViews(meta: MaybeRef<TableType | undefined>) {
let views = $ref<(GridType | FormType | KanbanType | GalleryType)[]>([])
const { $api } = useNuxtApp()
@ -18,10 +17,7 @@ export default function (meta: MaybeRef<TableType | undefined>, watchOptions: Wa
}
}
watch(() => meta, loadViews, {
immediate: true,
...watchOptions,
})
watch(() => meta, loadViews, { immediate: true })
return { views: $$(views), loadViews }
}

4
packages/nc-gui-v2/composables/useVirtualCell.ts

@ -1,8 +1,8 @@
import { computed } from '@vue/reactivity'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import { computed } from '#imports'
export default function useVirtualCell(column: ColumnType) {
export function useVirtualCell(column: ColumnType) {
const isHm = computed(
() =>
column.uidt === UITypes.LinkToAnotherRecord && (<LinkToAnotherRecordType>column.colOptions).type === RelationTypes.HAS_MANY,

33
packages/nc-gui-v2/lib/types.ts

@ -1,4 +1,3 @@
import type { ComputedRef, ToRefs } from 'vue'
import type { Role } from './enums'
export interface User {
@ -9,36 +8,4 @@ export interface User {
roles: Roles
}
export interface FeedbackForm {
url: string
createdAt: string
isHidden: boolean
lastFormPollDate?: string
}
export interface StoredState {
token: string | null
user: User | null
lang: string
darkMode: boolean
feedbackForm: FeedbackForm
}
export interface State extends StoredState {
sidebarOpen: boolean
}
export interface Getters {
signedIn: ComputedRef<boolean>
}
export interface Actions {
signOut: () => void
signIn: (token: string) => void
}
export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State, 'token' | 'user'>
export type GlobalState = Getters & Actions & ToRefs<ReadonlyState>
export type Roles = Record<Role, boolean>

9
packages/nc-gui-v2/middleware/auth.global.ts

@ -1,4 +1,5 @@
import { defineNuxtRouteMiddleware, navigateTo, useNuxtApp } from '#app'
import { defineNuxtRouteMiddleware, navigateTo } from '#app'
import { useGlobal } from '#imports'
/**
* Global auth middleware
@ -20,12 +21,12 @@ import { defineNuxtRouteMiddleware, navigateTo, useNuxtApp } from '#app'
* ```
*/
export default defineNuxtRouteMiddleware((to, from) => {
const { $state } = useNuxtApp()
const state = useGlobal()
/** if auth is required or unspecified (same as required) and user is not signed in, redirect to signin page */
if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !$state.signedIn.value) {
if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) {
return navigateTo('/signin')
} else if (to.meta.requiresAuth === false && $state.signedIn.value) {
} else if (to.meta.requiresAuth === false && state.signedIn.value) {
/**
* if user was turned away from non-auth page but also came from a non-auth page (e.g. user went to /signin and reloaded the page)
* redirect to home page

4
packages/nc-gui-v2/nuxt-shim.d.ts vendored

@ -1,6 +1,6 @@
import type { Api as BaseAPI } from 'nocodb-sdk'
import type { I18n } from 'vue-i18n'
import type { GlobalState } from './src/lib/types'
import type { UseGlobalReturn } from './composables/useGlobal/types'
import type en from './lang/en.json'
@ -15,7 +15,7 @@ declare module '#app/nuxt' {
}
/** {@link import('./plugins/tele') Telemetry} Emit telemetry event */
$e: (event: string, data?: any) => void
$state: GlobalState
$state: UseGlobalReturn
}
}

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

@ -19,7 +19,7 @@
"unique-names-generator": "^4.7.1",
"vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0",
"vuedraggable": "^2.24.3",
"vuetify": "^3.0.0-alpha.13",
"xlsx": "^0.17.3"
},
@ -2301,9 +2301,9 @@
"dev": true
},
"node_modules/@types/papaparse": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.3.tgz",
"integrity": "sha512-i7fV8u843vb7nIGpcwdCsG3WjfBONeytRHK1mQL9d5KQAvFeAK2rRisDHicreYpoQ0MXocUDEqunKHTeXdvibg==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.2.tgz",
"integrity": "sha512-BNbCHJkTE4RwmAFkCxEalET4mDvGr/1ld7ZtQ4i/laWI/iiVt+GL07stdvufle4KfywyvloqqpIiJscXNCrKxA==",
"dev": true,
"dependencies": {
"@types/node": "*"
@ -13869,20 +13869,17 @@
}
},
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"version": "2.24.3",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
"integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
"dependencies": {
"sortablejs": "1.14.0"
},
"peerDependencies": {
"vue": "^3.0.1"
"sortablejs": "1.10.2"
}
},
"node_modules/vuedraggable/node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
},
"node_modules/vuetify": {
"version": "3.0.0-beta.5",
@ -16180,9 +16177,9 @@
"dev": true
},
"@types/papaparse": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.3.tgz",
"integrity": "sha512-i7fV8u843vb7nIGpcwdCsG3WjfBONeytRHK1mQL9d5KQAvFeAK2rRisDHicreYpoQ0MXocUDEqunKHTeXdvibg==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.2.tgz",
"integrity": "sha512-BNbCHJkTE4RwmAFkCxEalET4mDvGr/1ld7ZtQ4i/laWI/iiVt+GL07stdvufle4KfywyvloqqpIiJscXNCrKxA==",
"dev": true,
"requires": {
"@types/node": "*"
@ -24684,17 +24681,17 @@
}
},
"vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"version": "2.24.3",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
"integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
"requires": {
"sortablejs": "1.14.0"
"sortablejs": "1.10.2"
},
"dependencies": {
"sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
}
}
},

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

@ -25,7 +25,7 @@
"unique-names-generator": "^4.7.1",
"vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0",
"vuedraggable": "^2.24.3",
"vuetify": "^3.0.0-alpha.13",
"xlsx": "^0.17.3"
},

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

@ -4,8 +4,7 @@ import type { ProjectType } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { navigateTo } from '#app'
import { computed, onMounted } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { extractSdkResponseErrorMsg } from '~/utils'
import MdiDeleteOutline from '~icons/mdi/delete-outline'
import MdiEditOutline from '~icons/mdi/edit-outline'
import MdiRefresh from '~icons/mdi/refresh'
@ -18,7 +17,7 @@ const toast = useToast()
const filterQuery = ref('')
const loading = ref(true)
const projects = ref()
const projects = ref<ProjectType[]>()
const loadProjects = async () => {
loading.value = true
@ -28,8 +27,10 @@ const loadProjects = async () => {
}
const filteredProjects = computed(() => {
return projects.value.filter(
(project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()),
return (
projects.value?.filter(
(project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()),
) ?? []
)
})
@ -44,7 +45,7 @@ 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)
projects.value?.splice(projects.value.indexOf(project), 1)
} catch (e) {
toast.error(await extractSdkResponseErrorMsg(e))
}

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

@ -1,5 +1,6 @@
<script setup lang="ts">
import useTabs from '~/composables/useTabs'
import { useTabs } from '#imports'
import { TabType } from '~/composables'
const route = useRoute()
const { loadProject, loadTables } = useProject(route.params.projectId as string)
@ -7,7 +8,7 @@ const { addTab } = useTabs()
const { $state } = useNuxtApp()
if (!route.params.type) {
addTab({ type: 'auth', title: 'Team & Auth' })
addTab({ type: TabType.AUTH, title: 'Team & Auth' })
}
await loadProject(route.params.projectId as string)

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

@ -1,7 +1,6 @@
<script setup lang="ts">
import useTabs from '~/composables/useTabs'
import { TabMetaInj } from '~/context'
import useUIPermission from '~/composables/useUIPermission'
import { useTabs, useUIPermission } from '#imports'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiTableIcon from '~icons/mdi/table'
import MdiCsvIcon from '~icons/mdi/file-document-outline'
@ -9,7 +8,6 @@ import MdiExcelIcon from '~icons/mdi/file-excel'
import MdiJSONIcon from '~icons/mdi/code-json'
import MdiAirTableIcon from '~icons/mdi/table-large'
import MdiRequestDataSourceIcon from '~icons/mdi/open-in-new'
import MdiAccountGroupIcon from '~icons/mdi/account-group'
const { tabs, activeTabIndex, activeTab, closeTab } = useTabs()

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

@ -41,7 +41,7 @@ const renameProject = async () => {
await $api.project.update(route.params.id as string, formState)
navigateTo(`/nc/${route.params.id}`)
} catch (e) {
} catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e))
}
loading.value = false

4
packages/nc-gui-v2/pages/projects/index.vue

@ -1,16 +1,14 @@
<script lang="ts" setup>
import { createVNode } from '@vue/runtime-core'
import { Modal } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { navigateTo } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { extractSdkResponseErrorMsg } from '~/utils'
import MaterialSymbolsFormatListBulletedRounded from '~icons/material-symbols/format-list-bulleted-rounded'
import MaterialSymbolsGridView from '~icons/material-symbols/grid-view'
import MdiPlus from '~icons/mdi/plus'
import MdiDatabaseOutline from '~icons/mdi/database-outline'
import MdiFolderOutline from '~icons/mdi/folder-outline'
import ExclamationCircleOutlined from '~icons/mdi/information-outline'
const navDrawerOptions = [
{

2
packages/nc-gui-v2/pages/projects/index/index.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { ProjectType } from 'nocodb-sdk'
import { navigateTo } from '#app'
import useColors from '~/composables/useColors'
import { useColors } from '#imports'
import MdiMenuDown from '~icons/mdi/menu-down'
import MdiDeleteOutline from '~icons/mdi/delete-outline'
import MdiPlus from '~icons/mdi/plus'

1
packages/nc-gui-v2/pages/projects/index/list.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { ProjectType } from 'nocodb-sdk'
import { navigateTo } from '#app'
import MdiDeleteOutline from '~icons/mdi/delete-outline'
import MdiEditOutline from '~icons/mdi/edit-outline'

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

@ -1,5 +1,6 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { RuleObject } from 'ant-design-vue/es/form'
import { definePageMeta } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { navigateTo, useNuxtApp } from '#app'
@ -25,7 +26,7 @@ const form = reactive({
password: '',
})
const formRules = {
const formRules: Record<string, RuleObject[]> = {
email: [
// E-mail is required
{ required: true, message: t('msg.error.signUpRules.emailReqd') },
@ -33,7 +34,7 @@ const formRules = {
{
validator: (_: unknown, v: string) => {
return new Promise((resolve, reject) => {
if (isEmail(v)) return resolve(true)
if (isEmail(v)) return resolve()
reject(new Error(t('msg.error.signUpRules.emailInvalid')))
})
},

7
packages/nc-gui-v2/plugins/i18n.ts → packages/nc-gui-v2/plugins/a.i18n.ts

@ -1,11 +1,6 @@
import { defineNuxtPlugin } from 'nuxt/app'
import { createI18n } from 'vue-i18n'
// import type en from '~/lang/en.json'
// todo: Type-define 'en' as the master schema for the resource
// type MessageSchema = typeof en
export const createI18nPlugin = async () =>
createI18n({
locale: 'en', // Set the initial locale
@ -51,7 +46,7 @@ export const createI18nPlugin = async () =>
export default defineNuxtPlugin(async (nuxtApp) => {
const i18n = await createI18nPlugin()
nuxtApp.vueApp.i18n = i18n.global
nuxtApp.vueApp.i18n = i18n.global as any
nuxtApp.vueApp.use(i18n)
})

5
packages/nc-gui-v2/plugins/api.ts

@ -1,7 +1,6 @@
import { defineNuxtPlugin } from '#imports'
import { createApiInstance } from '~/composables/useApi'
import { defineNuxtPlugin, useApi } from '#imports'
export default defineNuxtPlugin((nuxtApp) => {
/** injects a global api instance */
nuxtApp.provide('api', createApiInstance(nuxtApp))
nuxtApp.provide('api', useApi().api)
})

2
packages/nc-gui-v2/plugins/initializeFeedbackForm.ts

@ -3,7 +3,7 @@ import dayjs from 'dayjs'
import { defineNuxtPlugin } from '#app'
const handleFeedbackForm = async () => {
let { feedbackForm: currentFeedbackForm } = $(useGlobalState())
let { feedbackForm: currentFeedbackForm } = $(useGlobal())
if (!currentFeedbackForm) return
const { $api } = useNuxtApp()

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

@ -1,6 +1,6 @@
import { breakpointsTailwind } from '@vueuse/core'
import { defineNuxtPlugin } from '#app'
import { useDark, watch } from '#imports'
import { useGlobalState } from '~/composables/useGlobalState'
import { useBreakpoints, useDark, useGlobal, watch } from '#imports'
/**
* Injects global state into nuxt app.
@ -15,20 +15,25 @@ import { useGlobalState } from '~/composables/useGlobalState'
* ```
*/
export default defineNuxtPlugin((nuxtApp) => {
const storage = useGlobalState()
const state = useGlobal()
const darkMode = useDark()
/** get current breakpoints (for enabling sidebar) */
const breakpoints = useBreakpoints(breakpointsTailwind)
/** set i18n locale to stored language */
nuxtApp.vueApp.i18n.locale.value = storage.lang.value
nuxtApp.vueApp.i18n.locale.value = state.lang.value
/** set current dark mode from storage */
watch(
storage.darkMode,
state.darkMode,
(newMode) => {
darkMode.value = newMode
},
{ immediate: true },
)
nuxtApp.provide('state', storage)
/** is initial sidebar open */
state.sidebarOpen.value = state.signedIn.value && breakpoints.greater('md').value
})

4
packages/nc-gui-v2/plugins/tele.ts

@ -1,7 +1,7 @@
import { defineNuxtPlugin } from 'nuxt/app'
import type { Socket } from 'socket.io-client'
import io from 'socket.io-client'
import type { GlobalState } from '~/lib/types'
import type { UseGlobalReturn } from '~/composables/useGlobal/types'
// todo: ignore init if tele disabled
export default defineNuxtPlugin(async (nuxtApp) => {
@ -81,7 +81,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
}
}
watch((nuxtApp.$state as GlobalState).token, (newToken, oldToken) => {
watch((nuxtApp.$state as UseGlobalReturn).token, (newToken, oldToken) => {
if (newToken && newToken !== oldToken) init(newToken)
else if (!newToken) socket.disconnect()
})

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

@ -14,7 +14,8 @@
"@intlify/vite-plugin-vue-i18n/client",
"vue-i18n",
"unplugin-icons/types/vue",
"nuxt-windicss"
"nuxt-windicss",
"vite/client"
]
},
"exclude": [

2
packages/nc-gui-v2/utils/colorsUtils.ts

@ -1,4 +1,4 @@
export default {
export const theme = {
light: ['#ffdce5', '#fee2d5', '#ffeab6', '#d1f7c4', '#ede2fe', '#eee', '#cfdffe', '#d0f1fd', '#c2f5e8', '#ffdaf6'],
dark: [
'#f82b6099',

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

@ -1,5 +1,6 @@
export * from './colorsUtils'
export * from './dateTimeUtils'
export * from './deepCompare'
export * from './durationHelper'
export * from './errorUtils'
export * from './fileUtils'

2
packages/nc-gui-v2/utils/projectCreateUtils.ts

@ -1,4 +1,4 @@
import { ClientType } from '~/lib/enums'
import { ClientType } from '~/lib'
export interface ProjectCreateForm {
title: string

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

@ -11,7 +11,7 @@ import animations from '@windicss/plugin-animations'
// @ts-expect-error no types for plugin-question-mark
import questionMark from '@windicss/plugin-question-mark'
import colors, { themeColors } from './utils/colorsUtils'
import { theme as colors, themeColors } from './utils/colorsUtils'
export default defineConfig({
extract: {

Loading…
Cancel
Save