Browse Source

Merge pull request #3065 from nocodb/fix/gui-v2-dashboard-issues

vue3: Dashboard issues
pull/3078/head
Pranav C 2 years ago committed by GitHub
parent
commit
76cdacb9d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      packages/nc-gui-v2/components/cell/DatePicker.vue
  2. 17
      packages/nc-gui-v2/components/cell/DateTimePicker.vue
  3. 21
      packages/nc-gui-v2/components/cell/TimePicker.vue
  4. 9
      packages/nc-gui-v2/components/cell/Url.vue
  5. 19
      packages/nc-gui-v2/components/cell/YearPicker.vue
  6. 17
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  7. 2
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  8. 2
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  9. 5
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  10. 2
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  11. 2
      packages/nc-gui-v2/components/smartsheet/Gallery.vue
  12. 2
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  13. 2
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  14. 1
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  15. 2
      packages/nc-gui-v2/components/tabs/auth/UserManagement.vue
  16. 6
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  17. 20
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  18. 4
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  19. 20
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  20. 16
      packages/nc-gui-v2/composables/useColumnCreateStore.ts
  21. 3
      packages/nc-gui-v2/composables/useExpandedFormStore.ts
  22. 7
      packages/nc-gui-v2/composables/useGridViewColumnWidth.ts
  23. 6
      packages/nc-gui-v2/composables/useLTARStore.ts
  24. 36
      packages/nc-gui-v2/composables/useMetas.ts
  25. 23
      packages/nc-gui-v2/composables/useTabs.ts
  26. 12
      packages/nc-gui-v2/composables/useViewColumns.ts
  27. 28
      packages/nc-gui-v2/composables/useViewFilters.ts
  28. 3
      packages/nc-gui-v2/composables/useViewSorts.ts
  29. 8
      packages/nc-gui-v2/composables/useViews.ts
  30. 8
      packages/nc-gui-v2/context/index.ts
  31. 2
      packages/nc-gui-v2/utils/urlUtils.ts
  32. 49
      packages/nocodb-sdk/src/lib/Api.ts
  33. 2
      packages/nocodb/src/lib/models/User.ts
  34. 118
      scripts/sdk/swagger.json
  35. 2
      scripts/sdk/templates/http-clients/axios-http-client.eta
  36. 4
      scripts/sdk/templates/http-clients/fetch-http-client.eta

17
packages/nc-gui-v2/components/cell/DatePicker.vue

@ -5,6 +5,7 @@ import { ColumnInj, ReadonlyInj } from '~/context'
interface Props {
modelValue: string | null
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
@ -39,6 +40,18 @@ const localState = $computed({
}
},
})
const open = ref(false)
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
watch(
open,
(next) => {
if (next) {
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false))
}
},
{ flush: 'post' },
)
</script>
<template>
@ -50,7 +63,9 @@ const localState = $computed({
:placeholder="isDateInvalid ? 'Invalid date' : !readOnlyMode ? 'Select date' : ''"
:allow-clear="!readOnlyMode"
:input-read-only="true"
:open="readOnlyMode ? false : undefined"
:dropdown-class-name="randomClass"
:open="readOnlyMode ? false : open"
@click="open = !open"
>
<template #suffixIcon></template>
</a-date-picker>

17
packages/nc-gui-v2/components/cell/DateTimePicker.vue

@ -41,6 +41,19 @@ const localState = $computed({
}
},
})
const open = ref(false)
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
watch(
open,
(next) => {
if (next) {
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false))
}
},
{ flush: 'post' },
)
</script>
<template>
@ -53,7 +66,9 @@ const localState = $computed({
:placeholder="isDateInvalid ? 'Invalid date' : !readOnlyMode ? 'Select date and time' : ''"
:allow-clear="!readOnlyMode"
:input-read-only="true"
:open="readOnlyMode ? false : undefined"
:dropdown-class-name="randomClass"
:open="readOnlyMode ? false : open"
@click="open = !open"
>
<template #suffixIcon></template>
</a-date-picker>

21
packages/nc-gui-v2/components/cell/TimePicker.vue

@ -1,9 +1,10 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context'
interface Props {
modelValue: string | null
modelValue?: string | null
}
const { modelValue } = defineProps<Props>()
@ -50,6 +51,19 @@ const localState = $computed({
}
},
})
const open = ref(false)
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
watch(
open,
(next) => {
if (next) {
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false))
}
},
{ flush: 'post' },
)
</script>
<template>
@ -64,7 +78,10 @@ const localState = $computed({
:placeholder="isTimeInvalid ? 'Invalid time' : !readOnlyMode ? 'Select time' : ''"
:allow-clear="!readOnlyMode"
:input-read-only="true"
:open="readOnlyMode ? false : undefined"
:open="readOnlyMode ? false : open"
:dropdown-class-name="randomClass"
@click="open = !open"
@ok="open = !open"
>
<template #suffixIcon></template>
</a-time-picker>

9
packages/nc-gui-v2/components/cell/Url.vue

@ -27,12 +27,19 @@ const vModel = computed({
const isValid = computed(() => value && isValidURL(value))
const url = computed<string | null>(() => {
if (!value || !isValidURL(value)) return null
/** add url scheme if missing */
if (/^https?:\/\//.test(value)) return value
return `https://${value}`
})
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script>
<template>
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none" @blur="editEnabled = false" />
<nuxt-link v-else-if="isValid" class="py-2 underline hover:opacity-75" :to="value || ''" target="_blank">{{ value }}</nuxt-link>
<nuxt-link v-else-if="isValid" class="py-2 underline hover:opacity-75" :to="url" target="_blank">{{ value }} </nuxt-link>
<span v-else>{{ value }}</span>
</template>

19
packages/nc-gui-v2/components/cell/YearPicker.vue

@ -1,4 +1,5 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context'
@ -39,6 +40,19 @@ const localState = $computed({
}
},
})
const open = ref(false)
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
watch(
open,
(next) => {
if (next) {
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false))
}
},
{ flush: 'post' },
)
</script>
<template>
@ -50,7 +64,10 @@ const localState = $computed({
:placeholder="isYearInvalid ? 'Invalid year' : !readOnlyMode ? 'Select year' : ''"
:allow-clear="!readOnlyMode"
:input-read-only="true"
:open="readOnlyMode ? false : undefined"
:open="readOnlyMode ? false : open"
:dropdown-class-name="randomClass"
@click="open = !open"
@change="open = !open"
>
<template #suffixIcon></template>
</a-date-picker>

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

@ -60,7 +60,11 @@ function onCancel() {
async function onSubmit() {
await addOrUpdate(reloadMetaAndData)
advancedOptions.value = false
// add delay to complete the minimize transition
setTimeout(() => {
advancedOptions.value = false
}, 500)
}
// create column meta if it's a new column
@ -103,15 +107,10 @@ if (!formState.value?.column_name) {
<template>
<div class="min-w-[350px] w-max max-h-[95vh] bg-white shadow p-4 overflow-auto" @click.stop>
<a-form v-model="formState" name="column-create-or-edit" layout="vertical">
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.column_name">
<a-input
ref="antInput"
v-model:value="formState.column_name"
size="small"
class="nc-column-name-input"
@input="onAlter(8)"
/>
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title">
<a-input ref="antInput" v-model:value="formState.title" size="small" class="nc-column-name-input" @input="onAlter(8)" />
</a-form-item>
<a-form-item
v-if="!(editColumnDropdown && !!onlyNameUpdateOnEditColumns.find((col) => col === formState.uidt))"
:label="$t('labels.columnType')"

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

@ -10,7 +10,7 @@ const props = defineProps<{ column: ColumnType & { meta: any }; required: boolea
const hideMenu = toRef(props, 'hideMenu')
const meta = inject(MetaInj)
const isForm = inject(IsFormInj, false)
const isForm = inject(IsFormInj, ref(false))
const column = toRef(props, 'column')

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

@ -14,7 +14,7 @@ provide(ColumnInj, column)
const { metas } = useMetas()
const meta = inject(MetaInj)
const isForm = inject(IsFormInj, false)
const isForm = inject(IsFormInj, ref(false))
const { isLookup, isBt, isRollup, isMm, isHm, isFormula } = useVirtualCell(column)

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

@ -65,12 +65,11 @@ async function saveAllowCSVDownload() {
try {
const meta = shared.value.meta && typeof shared.value.meta === 'string' ? JSON.parse(shared.value.meta) : shared.value.meta
// todo: update swagger
await $api.dbViewShare.update(shared.value.id, {
meta,
} as any)
})
toast.success('Successfully updated')
} catch (e) {
} catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e))
}
if (allowCSVDownload?.value) {

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

@ -53,7 +53,7 @@ const isAutoSaved = $computed(() => {
})
const isManualSaved = $computed(() => {
return [UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(column?.value?.uidt as UITypes)
return [UITypes.Currency, UITypes.Duration].includes(column?.value?.uidt as UITypes)
})
const vModel = computed({

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

@ -13,7 +13,7 @@ const view = inject(ActiveViewInj)
const { loadData, paginationData, formattedData: data, loadGalleryData, galleryData, changePage } = useViewData(meta, view as any)
provide(IsFormInj, false)
provide(IsFormInj, ref(false))
provide(IsGridInj, false)
provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage)

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

@ -82,7 +82,7 @@ const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useG
onMounted(loadGridViewColumns)
provide(IsFormInj, false)
provide(IsFormInj, ref(false))
provide(IsGridInj, true)
provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage)

2
packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue

@ -59,7 +59,7 @@ if (props.loadRow) {
useProvideSmartsheetStore(ref({}) as any, meta)
provide(IsFormInj, true)
provide(IsFormInj, ref(true))
// accept as a prop
// const row: Row = { row: {}, rowMeta: {}, oldRow: {} }

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

@ -151,7 +151,6 @@ async function onRename(view: ViewType) {
}
try {
// todo typing issues, order and id do not exist on all members of ViewTypes (Kanban, Gallery, Form, Grid)
await api.dbView.update(view.id!, {
title: view.title,
order: view.order,

2
packages/nc-gui-v2/components/tabs/auth/UserManagement.vue

@ -39,7 +39,7 @@ const loadUsers = async (page = currentPage, limit = currentLimit) => {
if (!project.value?.id) return
// TODO: Types of api is not correct
const response = await $api.auth.projectUserList(project.value?.id, {
const response: any = await $api.auth.projectUserList(project.value?.id, {
query: {
limit,
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,

6
packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue

@ -32,8 +32,7 @@ const loadBase = async () => {
try {
if (!project.value.id) return
// todo: result is missing roles in return-type
const res: any = await $api.project.sharedBaseGet(project.value.id)
const res = await $api.project.sharedBaseGet(project.value.id)
base = {
uuid: res.uuid,
url: res.url,
@ -50,8 +49,7 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
try {
if (!project.value.id) return
// todo: returns void?
const res: any = await $api.project.sharedBaseUpdate(project.value.id, {
const res = await $api.project.sharedBaseUpdate(project.value.id, {
roles: role,
})

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

@ -4,7 +4,9 @@ import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListItems from './components/ListItems.vue'
import { inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import { ActiveCellInj, CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import MdiArrowExpand from '~icons/mdi/arrow-expand'
import MdiPlus from '~icons/mdi/plus'
const column = inject(ColumnInj)
@ -14,7 +16,7 @@ const cellValue = inject(CellValueInj, ref<any>(null))
const row = inject(RowInj)
const active = false
const active = inject(ActiveCellInj)
const listItemsDlg = ref(false)
@ -28,6 +30,8 @@ const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvid
await loadRelatedTableMeta()
const addIcon = computed(() => (cellValue?.value ? MdiArrowExpand : MdiPlus))
const value = computed(() => {
if (cellValue?.value) {
return cellValue?.value
@ -54,7 +58,8 @@ const unlinkRef = async (rec: Record<string, any>) => {
</template>
</div>
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand
<component
:is="addIcon"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500)"
@click="listItemsDlg = true"
/>
@ -63,12 +68,15 @@ const unlinkRef = async (rec: Record<string, any>) => {
</div>
</template>
<style scoped>
<style scoped lang="scss">
.nc-action-icon {
@apply hidden cursor-pointer;
}
.chips-wrapper:hover .nc-action-icon {
@apply inline-block;
.chips-wrapper:hover,
.chips-wrapper.active {
.nc-action-icon {
@apply inline-block;
}
}
</style>

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

@ -66,9 +66,9 @@ const unlinkRef = async (rec: Record<string, any>) => {
<template v-if="!isForm">
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<ItemChip v-for="(cell, i) of cells" :key="i" :item="ch" :value="cell.value" @unlink="unlinkRef(cell.item)" />
<ItemChip v-for="(cell, i) of cells" :key="i" :item="cell.item" :value="cell.value" @unlink="unlinkRef(cell.item)" />
<span v-if="value?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span>
<span v-if="cells?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span>
</template>
</div>

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

@ -1,14 +1,14 @@
<script lang="ts" setup>
import { Modal } from 'ant-design-vue'
import type { ColumnType } from 'nocodb-sdk'
import { useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel, watch } from '#imports'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel, watch } from '#imports'
import { ColumnInj, IsFormInj } from '~/context'
const props = defineProps<{ modelValue?: boolean }>()
const emit = defineEmits(['update:modelValue', 'attachRecord'])
const vModel = useVModel(props, 'modelValue', emit)
const isForm = inject(IsFormInj, false)
const isForm = inject(IsFormInj, ref(false))
const column = inject(ColumnInj)
const {
@ -25,11 +25,15 @@ const {
const { isNew, state, removeLTARRef } = useSmartsheetRowStoreOrThrow()
watch([vModel, isForm], (nextVal) => {
if (nextVal[0] || nextVal[1]) {
loadChildrenList()
}
})
watch(
[vModel, isForm],
(nextVal) => {
if (nextVal[0] || nextVal[1]) {
loadChildrenList()
}
},
{ immediate: true },
)
const unlinkRow = async (row: Record<string, any>) => {
if (isNew.value) {
@ -41,7 +45,7 @@ const unlinkRow = async (row: Record<string, any>) => {
}
const container = computed(() =>
isForm
isForm?.value
? h('div', {
class: 'w-full p-2',
})

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

@ -35,15 +35,14 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
title: 'title',
uidt: UITypes.SingleLineText,
...(column?.value || {}),
// todo: swagger json update - include meta
meta: (column?.value as any)?.meta || {},
meta: column?.value?.meta || {},
})
const additionalValidations = ref<Record<string, any>>({})
const validators = computed(() => {
return {
column_name: [
title: [
{
required: true,
message: 'Column name is required',
@ -88,6 +87,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const generateNewColumnMeta = () => {
setAdditionalValidations({})
formState.value = { meta: {}, ...sqlUi.value.getNewColumn((meta.value?.columns?.length || 0) + 1) }
formState.value.title = formState.value.title || formState.value.column_name
}
const onUidtOrIdTypeChange = () => {
@ -174,13 +174,13 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
if (cdf) formState.value.cdf = formState.value.cdf || null
}
const addOrUpdate = async (onSuccess: () => {}) => {
const addOrUpdate = async (onSuccess: () => void) => {
try {
console.log(formState, validators)
if (!(await validate())) return
formState.value.table_name = meta.value.table_name
formState.value.title = formState.value.column_name
// formState.value.title = formState.value.column_name
if (column?.value) {
await $api.dbTableColumn.update(column?.value?.id as string, formState.value)
toast.success('Column updated')
@ -210,6 +210,12 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
}
}
/** set column name same as title which is actual name in db */
watch(
() => formState.value?.title,
(newTitle) => (formState.value.column_name = newTitle),
)
return {
formState,
resetFields,

3
packages/nc-gui-v2/composables/useExpandedFormStore.ts

@ -84,9 +84,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
await api.utils.commentRow({
fk_model_id: meta.value?.id as string,
row_id: rowId,
// todo: swagger type correction
description: comment.value,
} as any)
})
comment.value = ''
message.success('Comment added successfully')

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

@ -4,7 +4,6 @@ import type { Ref } from 'vue'
import { useMetas } from './useMetas'
import { useUIPermission } from './useUIPermission'
// todo: update swagger
export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) | undefined>) {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useUIPermission()
@ -18,8 +17,7 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
const columns = computed<ColumnType[]>(() => metas?.value?.[(view?.value as any)?.fk_model_id as string]?.columns)
watch(
// todo : update type in swagger
() => [gridViewCols, resizingCol, resizingColWidth, columns],
[gridViewCols, resizingCol, resizingColWidth],
() => {
let style = ''
for (const c of columns?.value || []) {
@ -49,6 +47,9 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
loadCss()
}
/** when columns changes(create/delete) reload grid columns */
watch(columns, loadGridViewColumns)
const updateWidth = (id: string, width: string) => {
if (gridViewCols?.value?.[id]) {
gridViewCols.value[id].width = width

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

@ -94,14 +94,13 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
rowId.value,
colOptions.type as 'mm' | 'hm',
column?.value?.title,
// todo: swagger type correction
{
limit: childrenExcludedListPagination.size,
offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
where:
childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
} as any,
},
)
}
} catch (e: any) {
@ -123,12 +122,11 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
rowId.value,
colOptions.type as 'mm' | 'hm',
column?.value?.title,
// todo: swagger type correction
{
limit: childrenListPagination.size,
offset: childrenListPagination.size * (childrenListPagination.page - 1),
where: childrenListPagination.query && `(${relatedTablePrimaryValueProp.value},like,${childrenListPagination.query})`,
} as any,
},
)
} catch (e: any) {
notification.error({

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

@ -1,3 +1,4 @@
import type { WatchStopHandle } from 'vue'
import type { TableInfoType, TableType } from 'nocodb-sdk'
import { useProject } from './useProject'
import { useNuxtApp, useState } from '#app'
@ -7,24 +8,55 @@ export function useMetas() {
const { tables } = useProject()
const metas = useState<{ [idOrTitle: string]: TableType | any }>('metas', () => ({}))
const loadingState = useState<Record<string, boolean>>('metas-loading-state', () => ({}))
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => {
if (!force && metas.value[tableIdOrTitle as string]) return metas.value[tableIdOrTitle as string]
if (!force && metas.value[tableIdOrTitle]) return metas.value[tableIdOrTitle]
const modelId = (tables.value.find((t) => t.title === tableIdOrTitle || t.id === tableIdOrTitle) || {}).id
if (!modelId) {
console.warn(`Table '${tableIdOrTitle}' is not found in the table list`)
return null
}
const model = await $api.dbTable.read(modelId)
/** wait until loading is finished if requesting same meta */
if (!force) {
await new Promise((resolve) => {
let unwatch: WatchStopHandle
const timeout = setTimeout(() => {
unwatch?.()
clearTimeout(timeout)
resolve(null)
}, 20000)
unwatch = watch(
() => loadingState.value[modelId],
(isLoading) => {
if (!isLoading) {
clearTimeout(timeout)
resolve(null)
unwatch?.()
}
},
{ immediate: true },
)
})
if (metas.value[modelId]) return metas.value[modelId]
}
loadingState.value[modelId] = true
const model = await $api.dbTable.read(modelId)
metas.value = {
...metas.value,
[model.id!]: model,
[model.title]: model,
}
loadingState.value[modelId] = false
return model
}

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

@ -61,15 +61,7 @@ export function useTabs() {
const tab = tabs.value[index]
if (!tab) return
switch (tab.type) {
case TabType.TABLE:
return navigateTo(`/nc/${route.params.projectId}/table/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`)
case TabType.VIEW:
return navigateTo(`/nc/${route.params.projectId}/view/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`)
case TabType.AUTH:
return navigateTo(`/nc/${route.params.projectId}/auth`)
}
return navigateToTab(tab)
}
},
})
@ -101,12 +93,23 @@ export function useTabs() {
if (newTabIndex === -1) {
await navigateTo(`/nc/${route.params.projectId}`)
} else {
await navigateTo(`/nc/${route.params.projectId}/table/${tabs.value?.[newTabIndex]?.title}`)
await navigateToTab(tabs.value?.[newTabIndex])
}
}
tabs.value.splice(index, 1)
}
function navigateToTab(tab: TabItem) {
switch (tab.type) {
case TabType.TABLE:
return navigateTo(`/nc/${route.params.projectId}/table/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`)
case TabType.VIEW:
return navigateTo(`/nc/${route.params.projectId}/view/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`)
case TabType.AUTH:
return navigateTo(`/nc/${route.params.projectId}/auth`)
}
}
const updateTab = (key: number | Partial<TabItem>, newTabItemProps: Partial<TabItem>) => {
const tab = typeof key === 'number' ? tabs.value[key] : tabs.value.find(getPredicate(key))
if (tab) {

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

@ -1,11 +1,11 @@
import { isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, FormType, GalleryType, GridType, TableType } from 'nocodb-sdk'
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { watch } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import { useNuxtApp } from '#app'
export function useViewColumns(
view: Ref<(GridType | FormType | GalleryType) & { id?: string }> | undefined,
view: Ref<ViewType> | undefined,
meta: ComputedRef<TableType>,
isPublic = false,
reloadData?: () => void,
@ -110,16 +110,14 @@ export function useViewColumns(
const showSystemFields = computed({
get() {
// todo: update swagger
return (view?.value as any)?.show_system_fields || false
return view?.value?.show_system_fields || false
},
set(v) {
set(v: boolean) {
if (view?.value?.id) {
$api.dbView
.update(view.value.id, {
// todo: update swagger
show_system_fields: v,
} as any)
})
.finally(() => reloadData?.())
;(view.value as any).show_system_fields = v
}

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

@ -1,19 +1,20 @@
import type { FilterType, GalleryType, GridType, KanbanType } from 'nocodb-sdk'
import type { FilterType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { useNuxtApp, useUIPermission } from '#imports'
import { useMetas } from '~/composables/useMetas'
export function useViewFilters(
view: Ref<(GridType | KanbanType | GalleryType) & { id?: string }> | undefined,
view: Ref<ViewType> | undefined,
parentId?: string,
autoApply?: ComputedRef<boolean>,
reloadData?: () => void,
shared = false,
) {
// todo: update swagger
const filters = ref<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>([])
const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const { metas } = useMetas()
const loadFilters = async () => {
if (parentId) {
@ -77,11 +78,10 @@ export function useViewFilters(
fk_parent_id: parentId,
})
} else {
// todo: return type correction
filters.value[i] = (await $api.dbTableFilter.create(view?.value?.id as string, {
filters.value[i] = await $api.dbTableFilter.create(view?.value?.id as string, {
...filter,
fk_parent_id: parentId,
})) as any
})
}
reloadData?.()
}
@ -106,5 +106,21 @@ export function useViewFilters(
await saveOrUpdate(filters.value[index], index, true)
}
/** on column delete reload filters, identify by checking columns count */
watch(
() => {
if (!view?.value || !metas?.value?.[view?.value?.fk_model_id as string]) {
return 0
}
return metas?.value?.[view?.value?.fk_model_id as string]?.columns?.length || 0
},
async (nextColsLength, oldColsLength) => {
if (nextColsLength < oldColsLength) {
await loadFilters()
}
},
)
return { filters, loadFilters, sync, deleteFilter, saveOrUpdate, addFilter, addFilterGroup }
}

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

@ -12,8 +12,7 @@ export function useViewSorts(
const loadSorts = async () => {
if (!view?.value) return
// todo: api correction
sorts.value = ((await $api.dbTableSort.list(view?.value?.id as string)) as any)?.sorts?.list as any[]
sorts.value = ((await $api.dbTableSort.list(view?.value?.id as string)) as any)?.sorts?.list
}
const saveOrUpdate = async (sort: SortType, i: number) => {

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

@ -1,18 +1,18 @@
import type { FormType, GalleryType, GridType, KanbanType, TableType } from 'nocodb-sdk'
import type { TableType, ViewType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp } from '#app'
export function useViews(meta: MaybeRef<TableType | undefined>) {
let views = $ref<(GridType | FormType | KanbanType | GalleryType)[]>([])
let views = $ref<ViewType[]>([])
const { $api } = useNuxtApp()
const loadViews = async () => {
const _meta = unref(meta)
if (_meta && _meta.id) {
const response = (await $api.dbView.list(_meta.id)).list as any[]
const response = (await $api.dbView.list(_meta.id)).list
if (response) {
views = response.sort((a, b) => a.order - b.order)
views = response.sort((a, b) => a.order! - b.order!)
}
}
}

8
packages/nc-gui-v2/context/index.ts

@ -1,4 +1,4 @@
import type { ColumnType, FormType, GalleryType, GridType, KanbanType, TableType } from 'nocodb-sdk'
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, InjectionKey, Ref } from 'vue'
import type { EventHook } from '@vueuse/core'
import type { useViewData } from '#imports'
@ -14,15 +14,15 @@ export const TabMetaInj: InjectionKey<ComputedRef<TabItem>> = Symbol('tab-meta-i
export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> =
Symbol('pagination-data-injection')
export const ChangePageInj: InjectionKey<ReturnType<typeof useViewData>['changePage']> = Symbol('pagination-data-injection')
export const IsFormInj: InjectionKey<boolean> = Symbol('is-form-injection')
export const IsFormInj: InjectionKey<Ref<boolean>> = Symbol('is-form-injection')
export const IsGridInj: InjectionKey<boolean> = Symbol('is-grid-injection')
export const IsLockedInj: InjectionKey<boolean> = Symbol('is-locked-injection')
export const CellValueInj: InjectionKey<Ref<any>> = Symbol('cell-value-injection')
export const ActiveViewInj: InjectionKey<Ref<GridType | FormType | KanbanType | GalleryType>> = Symbol('active-view-injection')
export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection')
export const ReadonlyInj: InjectionKey<any> = Symbol('readonly-injection')
export const ReloadViewDataHookInj: InjectionKey<EventHook<void>> = Symbol('reload-view-data-injection')
export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection')
export const ViewListInj: InjectionKey<Ref<(GridType | FormType | KanbanType | GalleryType)[]>> = Symbol('view-list-injection')
export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection')
export const RightSidebarInj: InjectionKey<Ref<boolean>> = Symbol('right-sidebar-injection')
export const EditModeInj: InjectionKey<ComputedRef<boolean>> = Symbol('edit-mode-injection')

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

@ -24,7 +24,7 @@ export const dashboardUrl = () => {
// ref : https://stackoverflow.com/a/5717133
export const isValidURL = (str: string) => {
const pattern =
/^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00A1-\uFFFF0-9]-*)*[a-z\u00A1-\uFFFF0-9]+)(?:\.(?:[a-z\u00A1-\uFFFF0-9]-*)*[a-z\u00A1-\uFFFF0-9]+)*(?:\.(?:[a-z\u00A1-\uFFFF]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
/^(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00A1-\uFFFF0-9]-*)*[a-z\u00A1-\uFFFF0-9]+)(?:\.(?:[a-z\u00A1-\uFFFF0-9]-*)*[a-z\u00A1-\uFFFF0-9]+)*(?:\.(?:[a-z\u00A1-\uFFFF]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
return !!pattern.test(str)
}

49
packages/nocodb-sdk/src/lib/Api.ts

@ -11,13 +11,16 @@
export interface UserType {
/** Unique identifier for the given user. */
id: number;
id: string;
firstname: string;
lastname: string;
/** @format email */
email: string;
/** @format email */
roles?: string;
/**
* @format date
* @example 1997-10-31
@ -127,6 +130,7 @@ export interface ViewType {
order?: number;
fk_model_id?: string;
slug?: string;
show_system_fields?: boolean;
lock_type?: 'collaborative' | 'locked' | 'personal';
}
@ -437,7 +441,7 @@ export interface SharedViewListType {
}
export interface ViewListType {
list?: GridType | FormType | KanbanType | GalleryType;
list?: ViewType[];
pageInfo?: PaginatedType;
}
@ -662,10 +666,7 @@ export interface FullRequestParams
body?: unknown;
}
export type RequestParams = Omit<
FullRequestParams,
'body' | 'method' | 'query' | 'path'
>;
export type RequestParams = Omit<FullRequestParams, 'body' | 'method' | 'path'>;
export interface ApiConfig<SecurityDataType = unknown>
extends Omit<AxiosRequestConfig, 'data' | 'cancelToken'> {
@ -1272,10 +1273,10 @@ export class Api<
* @tags Project
* @name SharedBaseGet
* @request GET:/api/v1/db/meta/projects/{projectId}/shared
* @response `200` `{ uuid?: string, url?: string }` OK
* @response `200` `{ uuid?: string, url?: string, roles?: string }` OK
*/
sharedBaseGet: (projectId: string, params: RequestParams = {}) =>
this.request<{ uuid?: string; url?: string }, any>({
this.request<{ uuid?: string; url?: string; roles?: string }, any>({
path: `/api/v1/db/meta/projects/${projectId}/shared`,
method: 'GET',
format: 'json',
@ -1303,14 +1304,14 @@ export class Api<
* @tags Project
* @name SharedBaseCreate
* @request POST:/api/v1/db/meta/projects/{projectId}/shared
* @response `200` `{ url?: string, uuid?: string }` OK
* @response `200` `{ uuid?: string, url?: string, roles?: string }` OK
*/
sharedBaseCreate: (
projectId: string,
data: { roles?: string; password?: string },
params: RequestParams = {}
) =>
this.request<{ url?: string; uuid?: string }, any>({
this.request<{ uuid?: string; url?: string; roles?: string }, any>({
path: `/api/v1/db/meta/projects/${projectId}/shared`,
method: 'POST',
body: data,
@ -1325,18 +1326,19 @@ export class Api<
* @tags Project
* @name SharedBaseUpdate
* @request PATCH:/api/v1/db/meta/projects/{projectId}/shared
* @response `200` `void` OK
* @response `200` `{ uuid?: string, url?: string, roles?: string }` OK
*/
sharedBaseUpdate: (
projectId: string,
data: { roles?: string; password?: string },
params: RequestParams = {}
) =>
this.request<void, any>({
this.request<{ uuid?: string; url?: string; roles?: string }, any>({
path: `/api/v1/db/meta/projects/${projectId}/shared`,
method: 'PATCH',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
@ -1937,7 +1939,7 @@ export class Api<
*/
update: (
viewId: string,
data: { password?: string },
data: { password?: string; meta?: any },
params: RequestParams = {}
) =>
this.request<SharedViewType, any>({
@ -2025,10 +2027,10 @@ export class Api<
* @tags DB table sort
* @name List
* @request GET:/api/v1/db/meta/views/{viewId}/sorts
* @response `200` `{ uuid?: string, url?: string }` OK
* @response `200` `{ sorts?: { list?: (SortType)[] } }` OK
*/
list: (viewId: string, params: RequestParams = {}) =>
this.request<{ uuid?: string; url?: string }, any>({
this.request<{ sorts?: { list?: SortType[] } }, any>({
path: `/api/v1/db/meta/views/${viewId}/sorts`,
method: 'GET',
format: 'json',
@ -2123,14 +2125,15 @@ export class Api<
* @tags DB table filter
* @name Create
* @request POST:/api/v1/db/meta/views/{viewId}/filters
* @response `200` `void` OK
* @response `200` `FilterType` OK
*/
create: (viewId: string, data: FilterType, params: RequestParams = {}) =>
this.request<void, any>({
this.request<FilterType, any>({
path: `/api/v1/db/meta/views/${viewId}/filters`,
method: 'POST',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
@ -2593,7 +2596,11 @@ export class Api<
rowId: string,
relationType: 'mm' | 'hm',
columnName: string,
query?: { limit?: string; offset?: string },
query?: {
limit?: string | number;
offset?: string | number;
where?: string;
},
params: RequestParams = {}
) =>
this.request<any, any>({
@ -2674,7 +2681,11 @@ export class Api<
rowId: string,
relationType: 'mm' | 'hm',
columnName: string,
query?: { limit?: string; offset?: string },
query?: {
limit?: string | number;
offset?: string | number;
where?: string;
},
params: RequestParams = {}
) =>
this.request<any, any>({

2
packages/nocodb/src/lib/models/User.ts

@ -4,7 +4,7 @@ import Noco from '../Noco';
import extractProps from '../meta/helpers/extractProps';
import NocoCache from '../cache/NocoCache';
export default class User implements UserType {
id: number;
id: string;
/** @format email */
email: string;

118
scripts/sdk/swagger.json

@ -916,6 +916,9 @@
},
"url": {
"type": "string"
},
"roles": {
"type": "string"
}
}
}
@ -947,10 +950,13 @@
"schema": {
"type": "object",
"properties": {
"uuid": {
"type": "string"
},
"url": {
"type": "string"
},
"uuid": {
"roles": {
"type": "string"
}
}
@ -985,7 +991,25 @@
"operationId": "project-shared-base-update",
"responses": {
"200": {
"description": "OK"
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"uuid": {
"type": "string"
},
"url": {
"type": "string"
},
"roles": {
"type": "string"
}
}
}
}
}
}
},
"requestBody": {
@ -1703,7 +1727,8 @@
"properties": {
"password": {
"type": "string"
}
},
"meta": {}
}
}
}
@ -1828,11 +1853,16 @@
"schema": {
"type": "object",
"properties": {
"uuid": {
"type": "string"
},
"url": {
"type": "string"
"sorts": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Sort"
}
}
}
}
}
}
@ -1968,7 +1998,14 @@
"operationId": "db-table-filter-create",
"responses": {
"200": {
"description": "OK"
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Filter"
}
}
}
}
},
"tags": [
@ -3808,17 +3845,30 @@
"parameters": [
{
"schema": {
"type": "string"
"type": [
"string",
"number"
]
},
"in": "query",
"name": "limit"
},
{
"schema": {
"type": "string"
"type": [
"string",
"number"
]
},
"in": "query",
"name": "offset"
},
{
"schema": {
"type": "string"
},
"in": "query",
"name": "where"
}
]
}
@ -4012,17 +4062,30 @@
"parameters": [
{
"schema": {
"type": "string"
"type": [
"string",
"number"
]
},
"in": "query",
"name": "limit"
},
{
"schema": {
"type": "string"
"type": [
"string",
"number"
]
},
"in": "query",
"name": "offset"
},
{
"schema": {
"type": "string"
},
"in": "query",
"name": "where"
}
]
}
@ -5444,7 +5507,7 @@
"description": "",
"examples": [
{
"id": 142,
"id": "142",
"firstName": "Alice",
"lastName": "Smith",
"email": "alice.smith@gmail.com",
@ -5456,7 +5519,7 @@
"x-internal": false,
"properties": {
"id": {
"type": "integer",
"type": "string",
"description": "Unique identifier for the given user."
},
"firstname": {
@ -5469,6 +5532,10 @@
"type": "string",
"format": "email"
},
"roles": {
"type": "string",
"format": "email"
},
"date_of_birth": {
"type": "string",
"format": "date",
@ -6026,6 +6093,9 @@
"slug": {
"type": "string"
},
"show_system_fields": {
"type": "boolean"
},
"lock_type": {
"type": "string",
"enum": [
@ -7359,20 +7429,10 @@
},
"properties": {
"list": {
"oneOf": [
{
"$ref": "#/components/schemas/Grid"
},
{
"$ref": "#/components/schemas/Form"
},
{
"$ref": "#/components/schemas/Kanban"
},
{
"$ref": "#/components/schemas/Gallery"
}
]
"type": "array",
"items": {
"$ref": "#/components/schemas/View"
}
},
"pageInfo": {
"$ref": "#/components/schemas/Paginated"

2
scripts/sdk/templates/http-clients/axios-http-client.eta

@ -23,7 +23,7 @@ export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "pa
body?: unknown;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "path">;
export interface ApiConfig<SecurityDataType = unknown> extends Omit<AxiosRequestConfig, "data" | "cancelToken"> {
securityWorker?: (securityData: SecurityDataType | null) => Promise<AxiosRequestConfig | void> | AxiosRequestConfig | void;

4
scripts/sdk/templates/http-clients/fetch-http-client.eta

@ -24,7 +24,7 @@ export interface FullRequestParams extends Omit<RequestInit, "body"> {
cancelToken?: CancelToken;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "path">
export interface ApiConfig<SecurityDataType = unknown> {
@ -68,7 +68,7 @@ export class HttpClient<SecurityDataType = unknown> {
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
}
private encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key);
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;

Loading…
Cancel
Save