Browse Source

Merge pull request #7406 from nocodb/develop

pull/7407/head 0.204.0
github-actions[bot] 8 months ago committed by GitHub
parent
commit
c3fc91116b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      packages/nc-cli/package-lock.json
  2. 43
      packages/nc-gui/components/account/UserList.vue
  3. 79
      packages/nc-gui/components/account/UserMenu.vue
  4. 6
      packages/nc-gui/components/cell/DatePicker.vue
  5. 7
      packages/nc-gui/components/cell/DateTimePicker.vue
  6. 2
      packages/nc-gui/components/cell/Decimal.vue
  7. 2
      packages/nc-gui/components/cell/Duration.vue
  8. 2
      packages/nc-gui/components/cell/MultiSelect.vue
  9. 2
      packages/nc-gui/components/cell/Percent.vue
  10. 22
      packages/nc-gui/components/cell/ReadOnlyDateTimePicker.vue
  11. 22
      packages/nc-gui/components/cell/ReadOnlyUser.vue
  12. 10
      packages/nc-gui/components/cell/SingleSelect.vue
  13. 6
      packages/nc-gui/components/cell/TimePicker.vue
  14. 2
      packages/nc-gui/components/cell/Url.vue
  15. 67
      packages/nc-gui/components/cell/User.vue
  16. 6
      packages/nc-gui/components/cell/YearPicker.vue
  17. 7
      packages/nc-gui/components/erd/utils.ts
  18. 66
      packages/nc-gui/components/project/AccessSettings.vue
  19. 13
      packages/nc-gui/components/smartsheet/Form.vue
  20. 4
      packages/nc-gui/components/smartsheet/Gallery.vue
  21. 4
      packages/nc-gui/components/smartsheet/Kanban.vue
  22. 5
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  23. 12
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  24. 14
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  25. 16
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  26. 17
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  27. 3
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  28. 147
      packages/nc-gui/components/smartsheet/details/Fields.vue
  29. 28
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  30. 41
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  31. 12
      packages/nc-gui/components/smartsheet/grid/GroupByLabel.vue
  32. 22
      packages/nc-gui/components/smartsheet/grid/Table.vue
  33. 25
      packages/nc-gui/components/smartsheet/header/Menu.vue
  34. 6
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  35. 18
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  36. 8
      packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue
  37. 7
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  38. 17
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  39. 20
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  40. 2
      packages/nc-gui/components/tabs/Smartsheet.vue
  41. 13
      packages/nc-gui/components/template/Editor.vue
  42. 3
      packages/nc-gui/components/virtual-cell/Formula.vue
  43. 6
      packages/nc-gui/components/virtual-cell/Lookup.vue
  44. 7
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  45. 22
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  46. 6
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  47. 35
      packages/nc-gui/components/workspace/CollaboratorsList.vue
  48. 6
      packages/nc-gui/composables/useApi/interceptors.ts
  49. 12
      packages/nc-gui/composables/useColumnCreateStore.ts
  50. 63
      packages/nc-gui/composables/useData.ts
  51. 19
      packages/nc-gui/composables/useExpandedFormStore.ts
  52. 2
      packages/nc-gui/composables/useGlobal/index.ts
  53. 57
      packages/nc-gui/composables/useLTARStore.ts
  54. 4
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  55. 20
      packages/nc-gui/composables/useMultiSelect/index.ts
  56. 2
      packages/nc-gui/composables/useSmartsheetStore.ts
  57. 177
      packages/nc-gui/composables/useUserSorts.ts
  58. 9
      packages/nc-gui/composables/useViewColumns.ts
  59. 14
      packages/nc-gui/composables/useViewData.ts
  60. 8
      packages/nc-gui/composables/useViewFilters.ts
  61. 12
      packages/nc-gui/composables/useViewGroupBy.ts
  62. 1
      packages/nc-gui/context/index.ts
  63. 5
      packages/nc-gui/lang/ar.json
  64. 5
      packages/nc-gui/lang/bn_IN.json
  65. 5
      packages/nc-gui/lang/cs.json
  66. 5
      packages/nc-gui/lang/da.json
  67. 211
      packages/nc-gui/lang/de.json
  68. 6
      packages/nc-gui/lang/en.json
  69. 5
      packages/nc-gui/lang/es.json
  70. 5
      packages/nc-gui/lang/eu.json
  71. 5
      packages/nc-gui/lang/fa.json
  72. 5
      packages/nc-gui/lang/fi.json
  73. 5
      packages/nc-gui/lang/fr.json
  74. 5
      packages/nc-gui/lang/he.json
  75. 5
      packages/nc-gui/lang/hi.json
  76. 5
      packages/nc-gui/lang/hr.json
  77. 5
      packages/nc-gui/lang/id.json
  78. 5
      packages/nc-gui/lang/it.json
  79. 5
      packages/nc-gui/lang/ja.json
  80. 5
      packages/nc-gui/lang/ko.json
  81. 5
      packages/nc-gui/lang/lv.json
  82. 5
      packages/nc-gui/lang/nl.json
  83. 5
      packages/nc-gui/lang/no.json
  84. 5
      packages/nc-gui/lang/pl.json
  85. 5
      packages/nc-gui/lang/pt.json
  86. 5
      packages/nc-gui/lang/pt_BR.json
  87. 167
      packages/nc-gui/lang/ru.json
  88. 5
      packages/nc-gui/lang/sk.json
  89. 5
      packages/nc-gui/lang/sl.json
  90. 5
      packages/nc-gui/lang/sv.json
  91. 5
      packages/nc-gui/lang/th.json
  92. 5
      packages/nc-gui/lang/tr.json
  93. 5
      packages/nc-gui/lang/uk.json
  94. 5
      packages/nc-gui/lang/vi.json
  95. 19
      packages/nc-gui/lang/zh-Hans.json
  96. 5
      packages/nc-gui/lang/zh-Hant.json
  97. 6
      packages/nc-gui/lib/types.ts
  98. 20
      packages/nc-gui/package.json
  99. 4
      packages/nc-gui/utils/cell.ts
  100. 21
      packages/nc-gui/utils/columnUtils.ts
  101. Some files were not shown because too many files have changed in this diff Show More

12
packages/nc-cli/package-lock.json generated

@ -6410,9 +6410,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"funding": [
{
"type": "individual",
@ -20501,9 +20501,9 @@
"integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q=="
},
"follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw=="
},
"for-in": {
"version": "1.0.2",

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

@ -2,7 +2,16 @@
import { OrgUserRoles } from 'nocodb-sdk'
import type { OrgUserReqType, RequestParams, UserType } from 'nocodb-sdk'
import type { User } from '#imports'
import { extractSdkResponseErrorMsg, iconMap, useApi, useCopy, useDashboard, useDebounceFn, useNuxtApp } from '#imports'
import {
extractSdkResponseErrorMsg,
iconMap,
useApi,
useCopy,
useDashboard,
useDebounceFn,
useNuxtApp,
useUserSorts,
} from '#imports'
const { api, isLoading } = useApi()
@ -19,8 +28,14 @@ const { user: loggedInUser } = useGlobal()
const { copy } = useCopy()
const { sorts, sortDirection, loadSorts, saveOrUpdate, handleGetSortedData } = useUserSorts('Org')
const users = ref<UserType[]>([])
const sortedUsers = computed(() => {
return handleGetSortedData(users.value, sorts.value) as UserType[]
})
const currentPage = ref(1)
const currentLimit = ref(10)
@ -64,6 +79,7 @@ const loadUsers = useDebounceFn(async (page = currentPage.value, limit = current
onMounted(() => {
loadUsers()
loadSorts()
})
const updateRole = async (userId: string, roles: string) => {
@ -73,6 +89,12 @@ const updateRole = async (userId: string, roles: string) => {
} as OrgUserReqType)
message.success(t('msg.success.roleUpdated'))
users.value.forEach((user) => {
if (user.id === userId) {
user.roles = roles
}
})
$e('a:org-user:role-updated', { role: roles })
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -176,10 +198,21 @@ const openDeleteModal = (user: UserType) => {
</div>
<div class="w-full rounded-md max-w-250 h-[calc(100%-12rem)] rounded-md overflow-hidden mt-5">
<div class="flex w-full bg-gray-50 border-1 rounded-t-md">
<div class="py-3.5 text-gray-500 font-medium text-3.5 w-2/3 text-start pl-6" data-rec="true">
{{ $t('labels.email') }}
<div
class="py-3.5 text-gray-500 font-medium text-3.5 w-2/3 text-start pl-6 flex items-center space-x-2"
data-rec="true"
>
<span>
{{ $t('labels.email') }}
</span>
<LazyAccountUserMenu :direction="sortDirection.email" field="email" :handle-user-sort="saveOrUpdate" />
</div>
<div class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start flex items-center space-x-2" data-rec="true">
<span>
{{ $t('objects.role') }}
</span>
<LazyAccountUserMenu :direction="sortDirection.roles" field="roles" :handle-user-sort="saveOrUpdate" />
</div>
<div class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start" data-rec="true">{{ $t('objects.role') }}</div>
<div class="flex py-3.5 text-gray-500 font-medium text-3.5 w-28 justify-end mr-4" data-rec="true">
{{ $t('labels.action') }}
</div>
@ -193,7 +226,7 @@ const openDeleteModal = (user: UserType) => {
</div>
<section v-else class="tbody h-[calc(100%-4rem)] nc-scrollbar-md border-t-0 !overflow-auto">
<div
v-for="el of users"
v-for="el of sortedUsers"
:key="el.id"
data-testid="nc-token-list"
class="user flex py-3 justify-around px-1 border-b-1 border-l-1 border-r-1"

79
packages/nc-gui/components/account/UserMenu.vue

@ -0,0 +1,79 @@
<script lang="ts" setup>
import { iconMap } from '#imports'
import type { UsersSortType } from '~/lib'
const { field, direction, handleUserSort } = defineProps<{
field: UsersSortType['field']
direction: UsersSortType['direction']
handleUserSort: Function
}>()
const isOpen = ref(false)
const sortUserBy = (direction?: UsersSortType['direction']) => {
handleUserSort({
field,
direction,
})
isOpen.value = false
}
</script>
<template>
<a-dropdown
v-model:visible="isOpen"
:trigger="['click']"
placement="bottomLeft"
overlay-class-name="nc-user-menu-column-operations !border-1 rounded-lg !shadow-xl"
@click.stop="isOpen = !isOpen"
>
<div>
<GeneralIcon
:icon="direction === 'asc' || direction === 'desc' ? 'sortDesc' : 'arrowDown'"
class="text-grey h-full text-grey nc-user-menu-trigger cursor-pointer outline-0 mr-2 transition-none"
:style="{ transform: direction === 'asc' ? 'rotate(180deg)' : undefined }"
/>
</div>
<template #overlay>
<NcMenu class="flex flex-col gap-1 border-gray-200 nc-user-menu-column-options">
<NcMenuItem @click="sortUserBy('asc')">
<div class="nc-column-insert-after nc-user-menu-item">
<component
:is="iconMap.sortDesc"
class="text-gray-700 !rotate-180 !w-4.25 !h-4.25"
:style="{
transform: 'rotate(180deg)',
}"
/>
<!-- Sort Ascending -->
{{ $t('general.sortAsc') }}
</div>
</NcMenuItem>
<NcMenuItem @click="sortUserBy('desc')">
<div class="nc-column-insert-before nc-user-menu-item">
<component :is="iconMap.sortDesc" class="text-gray-700 !w-4.25 !h-4.25 ml-0.5 mr-0.25" />
<!-- Sort Descending -->
{{ $t('general.sortDesc') }}
</div>
</NcMenuItem>
</NcMenu>
</template>
</a-dropdown>
</template>
<style scoped>
.nc-user-menu-item {
@apply flex items-center gap-2;
}
.nc-user-menu-column-options {
.nc-icons {
@apply !w-5 !h-5;
}
}
:deep(.ant-dropdown-menu-item) {
@apply !hover:text-black text-gray-700;
}
</style>

6
packages/nc-gui/components/cell/DatePicker.vue

@ -104,7 +104,7 @@ const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null')
return t('general.null').toUpperCase()
} else if (isDateInvalid.value) {
return t('msg.invalidDate')
} else {
@ -243,7 +243,7 @@ const clickHandler = () => {
:picker="picker"
:tabindex="0"
:bordered="false"
class="!w-full !py-1 !border-none"
class="!w-full !py-1 !border-none !text-current"
:class="{ 'nc-null': modelValue === null && showNull, '!px-2': isExpandedFormOpen, '!px-0': !isExpandedFormOpen }"
:format="dateFormat"
:placeholder="placeholder"
@ -260,7 +260,7 @@ const clickHandler = () => {
</template>
<style scoped>
:deep(.ant-picker-input > input[disabled]) {
:deep(.ant-picker-input > input) {
@apply !text-current;
}
</style>

7
packages/nc-gui/components/cell/DateTimePicker.vue

@ -157,7 +157,7 @@ const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null')
return t('general.null').toUpperCase()
} else if (isDateInvalid.value) {
return t('msg.invalidDate')
} else {
@ -253,6 +253,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
const cellClickHook = inject(CellClickHookInj, null)
const cellClickHandler = () => {
if (readOnly.value) return
open.value = (active.value || editable.value) && !open.value
}
@ -296,7 +297,7 @@ const isColDisabled = computed(() => {
:disabled="isColDisabled"
:show-time="true"
:bordered="false"
class="!w-full !py-1 !border-none"
class="!w-full !py-1 !border-none !text-current"
:class="{ 'nc-null': modelValue === null && showNull, '!px-2': isExpandedFormOpen, '!px-0': !isExpandedFormOpen }"
:format="dateTimeFormat"
:placeholder="placeholder"
@ -313,7 +314,7 @@ const isColDisabled = computed(() => {
</template>
<style scoped>
:deep(.ant-picker-input > input[disabled]) {
:deep(.ant-picker-input > input) {
@apply !text-current;
}
</style>

2
packages/nc-gui/components/cell/Decimal.vue

@ -112,7 +112,7 @@ watch(isExpandedFormOpen, () => {
@selectstart.capture.stop
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null capitalize">{{ $t('general.null') }}</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<span v-else class="text-sm">{{ displayValue }}</span>
</template>

2
packages/nc-gui/components/cell/Duration.vue

@ -111,7 +111,7 @@ const focus: VNodeRef = (el) =>
@mousedown.stop
/>
<span v-else-if="modelValue === null && showNull" class="nc-null capitalize">{{ $t('general.null') }}</span>
<span v-else-if="modelValue === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<span v-else> {{ localState }}</span>

2
packages/nc-gui/components/cell/MultiSelect.vue

@ -567,7 +567,7 @@ const onFocus = () => {
}
:deep(.ant-select-selector) {
@apply !px-0;
@apply !pl-0;
}
:deep(.ant-select-selection-search-input) {

2
packages/nc-gui/components/cell/Percent.vue

@ -143,7 +143,7 @@ const onTabPress = (e: KeyboardEvent) => {
@selectstart.capture.stop
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null capitalize">{{ $t('general.null') }}</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<div v-else-if="percentMeta.is_progress === true && vModel !== null && vModel !== undefined" class="px-2">
<a-progress
:percent="Number(parseFloat(vModel.toString()).toFixed(2))"

22
packages/nc-gui/components/cell/ReadOnlyDateTimePicker.vue

@ -0,0 +1,22 @@
<script setup lang="ts">
import { ActiveCellInj, EditModeInj, ReadonlyInj, provide, ref } from '#imports'
interface Props {
modelValue?: string | null
}
defineProps<Props>()
provide(ReadonlyInj, ref(true))
provide(EditModeInj, ref(true))
provide(ActiveCellInj, ref(true))
</script>
<template>
<div class="relative">
<LazyCellDateTimePicker class="z-0" :model-value="modelValue" :is-pk="false" />
<div class="w-full h-full z-1 absolute top-0 left-0"></div>
</div>
</template>

22
packages/nc-gui/components/cell/ReadOnlyUser.vue

@ -0,0 +1,22 @@
<script setup lang="ts">
import { ActiveCellInj, EditModeInj, ReadonlyInj, provide, ref } from '#imports'
interface Props {
modelValue?: string | null
}
defineProps<Props>()
provide(ReadonlyInj, ref(true))
provide(EditModeInj, ref(true))
provide(ActiveCellInj, ref(true))
</script>
<template>
<div class="relative">
<LazyCellUser class="z-0" :model-value="modelValue" />
<div class="w-full h-full z-1 absolute top-0 left-0"></div>
</div>
</template>

10
packages/nc-gui/components/cell/SingleSelect.vue

@ -322,7 +322,8 @@ const onFocus = () => {
:disabled="readOnly || !editAllowed"
:show-search="!isMobileMode && isOpen && active"
:show-arrow="hasEditRoles && !readOnly && active && (vModel === null || vModel === undefined)"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && active ? 'active' : ''}`"
:dropdown-class-name="`nc-dropdown-single-select-cell !min-w-200px ${isOpen && active ? 'active' : ''}`"
:dropdown-match-select-width="true"
@select="onSelect"
@keydown="onKeydown($event)"
@search="search"
@ -399,7 +400,12 @@ const onFocus = () => {
}
:deep(.ant-select-selector) {
@apply !px-0;
@apply !pl-0 !pr-4;
}
:deep(.ant-select-selector .ant-select-selection-item) {
@apply flex items-center;
text-overflow: clip;
}
:deep(.ant-select-selection-search-input) {

6
packages/nc-gui/components/cell/TimePicker.vue

@ -96,7 +96,7 @@ const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null')
return t('general.null').toUpperCase()
} else if (isTimeInvalid.value) {
return t('msg.invalidTime')
} else {
@ -133,7 +133,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:bordered="false"
use12-hours
format="HH:mm"
class="!w-full !py-1 !border-none"
class="!w-full !py-1 !border-none !text-current"
:class="{ 'nc-null': modelValue === null && showNull, '!px-2': isExpandedFormOpen, '!px-0': !isExpandedFormOpen }"
:placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk"
@ -148,7 +148,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
</template>
<style scoped>
:deep(.ant-picker-input > input[disabled]) {
:deep(.ant-picker-input > input) {
@apply !text-current;
}
</style>

2
packages/nc-gui/components/cell/Url.vue

@ -108,7 +108,7 @@ watch(
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase"> $t('general.null')</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase"> {{ $t('general.null') }}</span>
<nuxt-link
v-else-if="isValid && !cellUrlOptions?.overlay"

67
packages/nc-gui/components/cell/User.vue

@ -27,7 +27,7 @@ import {
import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props {
modelValue?: UserFieldRecordType[] | string | null
modelValue?: UserFieldRecordType[] | UserFieldRecordType | string | null
rowIndex?: number
location?: 'cell' | 'filter'
forceMulti?: boolean
@ -113,17 +113,18 @@ const vModel = computed({
return acc
}, [] as { label: string; value: string }[])
} else {
selected =
modelValue?.reduce((acc, item) => {
const label = item?.display_name || item?.email
if (label) {
acc.push({
label,
value: item.id,
})
}
return acc
}, [] as { label: string; value: string }[]) || []
selected = modelValue
? (Array.isArray(modelValue) ? modelValue : [modelValue]).reduce((acc, item) => {
const label = item?.display_name || item?.email
if (label) {
acc.push({
label,
value: item.id,
})
}
return acc
}, [] as { label: string; value: string }[])
: []
}
return selected
@ -280,7 +281,7 @@ const filterOption = (input: string, option: any) => {
}"
>
<template v-for="selectedOpt of vModel" :key="selectedOpt.value">
<a-tag class="rounded-tag" color="'#ccc'">
<a-tag class="rounded-tag max-w-full" color="'#ccc'">
<span
:style="{
'color': tinycolor.isReadable('#ccc' || '#ccc', '#fff', { level: 'AA', size: 'large' })
@ -290,7 +291,21 @@ const filterOption = (input: string, option: any) => {
}"
:class="{ 'text-sm': isKanban }"
>
{{ selectedOpt.label }}
<NcTooltip class="truncate max-w-full" show-on-truncate-only>
<template #title>
{{ selectedOpt.label }}
</template>
<span
class="text-ellipsis overflow-hidden"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}"
>
{{ selectedOpt.label }}
</span>
</NcTooltip>
</span>
</a-tag>
</template>
@ -310,7 +325,7 @@ const filterOption = (input: string, option: any) => {
:open="isOpen && editAllowed"
:disabled="readOnly || !editAllowed"
:class="{ 'caret-transparent': !hasEditRoles }"
:dropdown-class-name="`nc-dropdown-user-select-cell ${isOpen ? 'active' : ''}`"
:dropdown-class-name="`nc-dropdown-user-select-cell !min-w-200px ${isOpen ? 'active' : ''}`"
:filter-option="filterOption"
@search="search"
@keydown.stop
@ -326,7 +341,7 @@ const filterOption = (input: string, option: any) => {
:class="`nc-select-option-${column.title}-${op.email}`"
@click.stop
>
<a-tag class="rounded-tag" color="'#ccc'">
<a-tag class="rounded-tag max-w-full !pl-0" color="'#ccc'">
<span
:style="{
'color': tinycolor.isReadable('#ccc' || '#ccc', '#fff', { level: 'AA', size: 'large' })
@ -334,9 +349,25 @@ const filterOption = (input: string, option: any) => {
: tinycolor.mostReadable('#ccc' || '#ccc', ['#0b1d05', '#fff']).toHex8String(),
'font-size': '13px',
}"
class="flex items-center gap-2"
:class="{ 'text-sm': isKanban }"
>
{{ op.display_name?.length ? op.display_name : op.email }}
<GeneralUserIcon size="medium" :email="op.email" />
<NcTooltip class="truncate max-w-full" show-on-truncate-only>
<template #title>
{{ op.display_name?.length ? op.display_name : op.email }}
</template>
<span
class="text-ellipsis overflow-hidden"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}"
>
{{ op.display_name?.length ? op.display_name : op.email }}
</span>
</NcTooltip>
</span>
</a-tag>
</a-select-option>
@ -433,7 +464,7 @@ const filterOption = (input: string, option: any) => {
}
:deep(.ant-select-selector) {
@apply !px-0;
@apply !pl-0;
}
:deep(.ant-select-selection-search-input) {

6
packages/nc-gui/components/cell/YearPicker.vue

@ -83,7 +83,7 @@ const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null')
return t('general.null').toUpperCase()
} else if (isYearInvalid.value) {
return t('msg.invalidTime')
} else {
@ -119,7 +119,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:tabindex="0"
picker="year"
:bordered="false"
class="!w-full !py-1 !border-none"
class="!w-full !py-1 !border-none !text-current"
:class="{ 'nc-null': modelValue === null && showNull, '!px-2': isExpandedFormOpen, '!px-0': !isExpandedFormOpen }"
:placeholder="placeholder"
:allow-clear="(!readOnly && !localState && !isPk) || isEditColumn"
@ -136,7 +136,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
</template>
<style scoped>
:deep(.ant-picker-input > input[disabled]) {
:deep(.ant-picker-input > input) {
@apply !text-current;
}
</style>

7
packages/nc-gui/components/erd/utils.ts

@ -175,9 +175,10 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<ER
if (!table.id) return acc
const columns =
metasWithIdAsKey.value[table.id].columns?.filter(
(col) => config.value.showAllColumns || (!config.value.showAllColumns && isLinksOrLTAR(col)),
) || []
metasWithIdAsKey.value[table.id].columns?.filter((col) => {
if ([UITypes.CreatedBy, UITypes.LastModifiedBy].includes(col.uidt as UITypes) && col.system) return false
return config.value.showAllColumns || (!config.value.showAllColumns && isLinksOrLTAR(col))
}) || []
const pkAndFkColumns = columns
.filter(() => config.value.showPkAndFk)

66
packages/nc-gui/components/project/AccessSettings.vue

@ -8,8 +8,9 @@ import {
parseStringDateTime,
timeAgo,
} from 'nocodb-sdk'
import type { WorkspaceUserRoles } from 'nocodb-sdk'
import { isEeUI, storeToRefs } from '#imports'
import type { Roles, WorkspaceUserRoles } from 'nocodb-sdk'
import { isEeUI, storeToRefs, useUserSorts } from '#imports'
import type { User } from '#imports'
const basesStore = useBases()
const { getBaseUsers, createProjectUser, updateProjectUser, removeProjectUser } = basesStore
@ -17,6 +18,8 @@ const { activeProjectId } = storeToRefs(basesStore)
const { orgRoles, baseRoles } = useRoles()
const { sorts, sortDirection, loadSorts, saveOrUpdate, handleGetSortedData } = useUserSorts('Project')
const isSuper = computed(() => orgRoles.value?.[OrgUserRoles.SUPER_ADMIN])
interface Collaborators {
@ -24,6 +27,7 @@ interface Collaborators {
email: string
main_roles: OrgUserRoles
roles: ProjectRoles
base_roles: Roles
workspace_roles: WorkspaceUserRoles
created_at: string
}
@ -35,6 +39,14 @@ const isLoading = ref(false)
const isSearching = ref(false)
const accessibleRoles = ref<(typeof ProjectRoles)[keyof typeof ProjectRoles][]>([])
const filteredCollaborators = computed(() =>
collaborators.value.filter((collab) => collab.email.toLowerCase().includes(userSearchText.value.toLowerCase())),
)
const sortedCollaborators = computed(() => {
return handleGetSortedData(filteredCollaborators.value, sorts.value)
})
const loadCollaborators = async () => {
try {
const { users, totalRows } = await getBaseUsers({
@ -64,29 +76,33 @@ const loadCollaborators = async () => {
}
const updateCollaborator = async (collab: any, roles: ProjectRoles) => {
const currentCollaborator = collaborators.value.find((coll) => coll.id === collab.id)!
try {
if (
!roles ||
(roles === ProjectRoles.NO_ACCESS && !isEeUI) ||
(collab.workspace_roles && WorkspaceRolesToProjectRoles[collab.workspace_roles as WorkspaceUserRoles] === roles && isEeUI)
(currentCollaborator.workspace_roles &&
WorkspaceRolesToProjectRoles[currentCollaborator.workspace_roles as WorkspaceUserRoles] === roles &&
isEeUI)
) {
await removeProjectUser(activeProjectId.value!, collab)
await removeProjectUser(activeProjectId.value!, currentCollaborator as unknown as User)
if (
collab.workspace_roles &&
WorkspaceRolesToProjectRoles[collab.workspace_roles as WorkspaceUserRoles] === roles &&
currentCollaborator.workspace_roles &&
WorkspaceRolesToProjectRoles[currentCollaborator.workspace_roles as WorkspaceUserRoles] === roles &&
isEeUI
) {
collab.roles = WorkspaceRolesToProjectRoles[collab.workspace_roles as WorkspaceUserRoles]
currentCollaborator.roles = WorkspaceRolesToProjectRoles[currentCollaborator.workspace_roles as WorkspaceUserRoles]
} else {
collab.roles = ProjectRoles.NO_ACCESS
currentCollaborator.roles = ProjectRoles.NO_ACCESS
}
} else if (collab.base_roles) {
collab.roles = roles
await updateProjectUser(activeProjectId.value!, collab)
} else if (currentCollaborator.base_roles) {
currentCollaborator.roles = roles
await updateProjectUser(activeProjectId.value!, currentCollaborator as unknown as User)
} else {
collab.roles = roles
collab.base_roles = roles
await createProjectUser(activeProjectId.value!, collab)
currentCollaborator.roles = roles
currentCollaborator.base_roles = roles
await createProjectUser(activeProjectId.value!, currentCollaborator as unknown as User)
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -106,16 +122,13 @@ onMounted(async () => {
} else if (currentRoleIndex !== -1) {
accessibleRoles.value = OrderedProjectRoles.slice(currentRoleIndex + 1)
}
loadSorts()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
}
})
const filteredCollaborators = computed(() =>
collaborators.value.filter((collab) => collab.email.toLowerCase().includes(userSearchText.value.toLowerCase())),
)
</script>
<template>
@ -145,14 +158,25 @@ const filteredCollaborators = computed(() =>
<div v-else class="nc-collaborators-list mt-6 h-full">
<div class="flex flex-col rounded-lg overflow-hidden border-1 max-w-350 max-h-[calc(100%-8rem)]">
<div class="flex flex-row bg-gray-50 min-h-12 items-center border-b-1">
<div class="text-gray-700 users-email-grid">{{ $t('objects.users') }}</div>
<div class="text-gray-700 user-access-grid">{{ $t('general.access') }}</div>
<div class="text-gray-700 users-email-grid flex items-center space-x-2">
<span>
{{ $t('objects.users') }}
</span>
<LazyAccountUserMenu :direction="sortDirection.email" field="email" :handle-user-sort="saveOrUpdate" />
</div>
<div class="text-gray-700 user-access-grid flex items-center space-x-2">
<span>
{{ $t('general.access') }}
</span>
<LazyAccountUserMenu :direction="sortDirection.roles" field="roles" :handle-user-sort="saveOrUpdate" />
</div>
<div class="text-gray-700 date-joined-grid">{{ $t('title.dateJoined') }}</div>
</div>
<div class="flex flex-col nc-scrollbar-md">
<div
v-for="(collab, i) of filteredCollaborators"
v-for="(collab, i) of sortedCollaborators"
:key="i"
class="user-row flex flex-row border-b-1 py-1 min-h-14 items-center"
>

13
packages/nc-gui/components/smartsheet/Form.vue

@ -34,7 +34,18 @@ provide(IsGalleryInj, ref(false))
// todo: generate hideCols based on default values
const hiddenCols = ['created_at', 'updated_at']
const hiddenColTypes = [UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.QrCode, UITypes.Barcode, UITypes.SpecificDBType]
const hiddenColTypes = [
UITypes.Rollup,
UITypes.Lookup,
UITypes.Formula,
UITypes.QrCode,
UITypes.Barcode,
UITypes.SpecificDBType,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
]
const { isMobileMode, user } = useGlobal()

4
packages/nc-gui/components/smartsheet/Gallery.vue

@ -295,7 +295,7 @@ watch(
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField)"
v-model="record.row[displayField.title]"
class="!text-gray-600"
class="!text-brand-500"
:column="displayField"
:row="record"
/>
@ -303,7 +303,7 @@ watch(
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
class="!text-gray-600"
class="!text-brand-500"
:column="displayField"
:edit-enabled="false"
:read-only="true"

4
packages/nc-gui/components/smartsheet/Kanban.vue

@ -593,7 +593,7 @@ const getRowId = (row: RowType) => {
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField)"
v-model="record.row[displayField.title]"
class="!text-gray-600"
class="!text-brand-500"
:column="displayField"
:row="record"
/>
@ -601,7 +601,7 @@ const getRowId = (row: RowType) => {
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
class="!text-gray-600"
class="!text-brand-500"
:column="displayField"
:edit-enabled="false"
:read-only="true"

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

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { isCreatedOrLastModifiedByCol, isCreatedOrLastModifiedTimeCol } from 'nocodb-sdk'
import {
ActiveCellInj,
CellValueInj,
@ -19,6 +20,7 @@ import {
isLink,
isLookup,
isMm,
isPrimary,
isQrCode,
isRollup,
provide,
@ -99,6 +101,7 @@ onUnmounted(() => {
class="nc-virtual-cell w-full flex items-center"
:class="{
'text-right justify-end': isGrid && !isForm && isRollup(column) && !isExpandedForm,
'text-brand-500': isPrimary(column) && !isForm,
}"
@keydown.enter.exact="onNavigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)"
@ -114,6 +117,8 @@ onUnmounted(() => {
<LazyVirtualCellBarcode v-else-if="isBarcode(column)" />
<LazyVirtualCellCount v-else-if="isCount(column)" />
<LazyVirtualCellLookup v-else-if="isLookup(column)" />
<LazyCellReadOnlyDateTimePicker v-else-if="isCreatedOrLastModifiedTimeCol(column)" :model-value="modelValue" />
<LazyCellReadOnlyUser v-else-if="isCreatedOrLastModifiedByCol(column)" :model-value="modelValue" />
</template>
</div>
</template>

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

@ -79,7 +79,16 @@ const mounted = ref(false)
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup, UITypes.Links]
const onlyNameUpdateOnEditColumns = [
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Rollup,
UITypes.Links,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
]
// To close column type dropdown on escape and
// close modal only when the type popup is close
@ -344,6 +353,7 @@ if (props.fromTableExplorer) {
<SmartsheetColumnSelectOptions
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"
v-model:value="formState"
:from-table-explorer="props.fromTableExplorer || false"
/>
</template>
</div>

14
packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -5,6 +5,7 @@ import jsep from 'jsep'
import {
FormulaError,
UITypes,
isCreatedOrLastModifiedByCol,
jsepCurlyHook,
substituteColumnIdWithAliasInFormula,
validateFormulaAndExtractTreeWithType,
@ -51,7 +52,18 @@ const { predictFunction: _predictFunction } = useNocoEe()
const meta = inject(MetaInj, ref())
const supportedColumns = computed(
() => meta?.value?.columns?.filter((col) => !uiTypesNotSupportedInFormulas.includes(col.uidt as UITypes)) || [],
() =>
meta?.value?.columns?.filter((col) => {
if (uiTypesNotSupportedInFormulas.includes(col.uidt as UITypes)) {
return false
}
if (isCreatedOrLastModifiedByCol(col) && col.system) {
return false
}
return true
}) || [],
)
const { getMeta } = useMetas()

16
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -22,7 +22,7 @@ const baseStore = useBase()
const { tables } = storeToRefs(baseStore)
const { metas } = useMetas()
const { metas, getMeta } = useMetas()
setAdditionalValidations({
fk_relation_column_id: [{ required: true, message: t('general.required') }],
@ -48,12 +48,15 @@ const refTables = computed(() => {
return _refTables as Required<TableType & { column: ColumnType; col: Required<LinkToAnotherRecordType> }>[]
})
const selectedTable = computed(() => {
return refTables.value.find((t) => t.column.id === vModel.value.fk_relation_column_id)
})
const columns = computed<ColumnType[]>(() => {
const selectedTable = refTables.value.find((t) => t.column.id === vModel.value.fk_relation_column_id)
if (!selectedTable?.id) {
if (!selectedTable.value?.id) {
return []
}
return metas.value[selectedTable.id].columns.filter(
return metas.value[selectedTable.value.id]?.columns.filter(
(c: ColumnType) =>
vModel.value.fk_lookup_column_id === c.id || (!isSystemColumn(c) && c.id !== vModel.value.id && c.uidt !== UITypes.Links),
)
@ -66,7 +69,10 @@ onMounted(() => {
}
})
const onRelationColChange = () => {
const onRelationColChange = async () => {
if (selectedTable.value) {
await getMeta(selectedTable.value.id)
}
vModel.value.fk_lookup_column_id = columns.value?.[0]?.id
onDataTypeChange()
}

17
packages/nc-gui/components/smartsheet/column/RollupOptions.vue

@ -35,7 +35,7 @@ const baseStore = useBase()
const { tables } = storeToRefs(baseStore)
const { metas } = useMetas()
const { metas, getMeta } = useMetas()
const { t } = useI18n()
@ -70,14 +70,16 @@ const refTables = computed(() => {
return _refTables as Required<TableType & { column: ColumnType; col: Required<LinkToAnotherRecordType> }>[]
})
const columns = computed(() => {
const selectedTable = refTables.value.find((t) => t.column.id === vModel.value.fk_relation_column_id)
const selectedTable = computed(() => {
return refTables.value.find((t) => t.column.id === vModel.value.fk_relation_column_id)
})
if (!selectedTable?.id) {
const columns = computed<ColumnType[]>(() => {
if (!selectedTable.value?.id) {
return []
}
return metas.value[selectedTable.id].columns.filter(
return metas.value[selectedTable.value.id]?.columns.filter(
(c: ColumnType) => !isVirtualCol(c.uidt as UITypes) && (!isSystemColumn(c) || c.pk),
)
})
@ -90,7 +92,10 @@ onMounted(() => {
}
})
const onRelationColChange = () => {
const onRelationColChange = async () => {
if (selectedTable.value) {
await getMeta(selectedTable.value.id)
}
vModel.value.fk_rollup_column_id = columns.value?.[0]?.id
onDataTypeChange()
}

3
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -15,6 +15,7 @@ interface Option {
const props = defineProps<{
value: any
fromTableExplorer?: boolean
}>()
const emit = defineEmits(['update:value'])
@ -308,7 +309,7 @@ const loadListData = async ($state: any) => {
ref="optionsWrapperDomRef"
class="nc-col-option-select-option overflow-x-auto scrollbar-thin-dull"
:style="{
maxHeight: 'calc(min(30vh, 250px))',
maxHeight: props.fromTableExplorer ? 'calc(100vh - (var(--topbar-height) * 3.6) - 320px)' : 'calc(min(30vh, 250px))',
}"
>
<InfiniteLoading v-if="isReverseLazyLoad" v-bind="$attrs" @infinite="loadListDataReverse">

147
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -100,6 +100,7 @@ const fields = computed<TableExplorerColumn[]>({
const x = ((meta.value?.columns as ColumnType[]) ?? [])
.filter((field) => !field.fk_column_id && !isSystemColumn(field))
.concat(newFields.value)
.map((field) => updateDefaultColumnValues(field))
.sort((a, b) => {
return getFieldOrder(a) - getFieldOrder(b)
})
@ -268,8 +269,30 @@ const duplicateField = async (field: TableExplorerColumn) => {
const onFieldUpdate = (state: TableExplorerColumn) => {
const col = fields.value.find((col) => compareCols(col, state))
if (!col) return
const diffs = diff(col, state)
if (Object.keys(diffs).length === 0 || (Object.keys(diffs).length === 1 && 'altered' in diffs)) {
// hack to prevent update status `Updated Field` when clicking on field first time
let isUpdated = true
if (
[UITypes.SingleSelect, UITypes.MultiSelect].includes(col.uidt) &&
Object.keys(diffs).length === 1 &&
diffs?.colOptions?.options &&
(diffs?.colOptions?.options?.length === 0 ||
(diffs?.colOptions?.options[0]?.index !== undefined && Object.keys(diffs?.colOptions?.options[0] || {}).length === 1))
) {
isUpdated = false
}
if (!isUpdated) {
let field = fields.value.find((field) => compareCols(field, state))
if (field) {
field = state
}
}
if (Object.keys(diffs).length === 0 || (Object.keys(diffs).length === 1 && 'altered' in diffs) || !isUpdated) {
ops.value = ops.value.filter((op) => op.op === 'add' || !compareCols(op.column, state))
} else {
const field = ops.value.find((op) => compareCols(op.column, state))
@ -291,7 +314,7 @@ const onFieldUpdate = (state: TableExplorerColumn) => {
return
}
if (field && !moveField) {
if (field || (field && moveField)) {
field.column = state
} else {
ops.value.push({
@ -376,6 +399,19 @@ const onMove = (_event: { moved: { newIndex: number; oldIndex: number } }) => {
return
}
const mop = moveOps.value.find((op) => compareCols(op.column, fields.value[_event.moved.oldIndex]))
if (mop) {
mop.index = _event.moved.newIndex
mop.order = order
} else {
moveOps.value.push({
op: 'move',
column: fields.value[_event.moved.oldIndex],
index: _event.moved.newIndex,
order,
})
}
if (op) {
onFieldUpdate({
...op.column,
@ -393,19 +429,6 @@ const onMove = (_event: { moved: { newIndex: number; oldIndex: number } }) => {
},
})
}
const mop = moveOps.value.find((op) => compareCols(op.column, fields.value[_event.moved.oldIndex]))
if (mop) {
mop.index = _event.moved.newIndex
mop.order = order
} else {
moveOps.value.push({
op: 'move',
column: fields.value[_event.moved.oldIndex],
index: _event.moved.newIndex,
order,
})
}
}
const isColumnValid = (column: TableExplorerColumn) => {
@ -438,6 +461,52 @@ const isColumnValid = (column: TableExplorerColumn) => {
return true
}
function updateDefaultColumnValues(column: TableExplorerColumn) {
if (column.uidt === UITypes.QrCode && column.colOptions?.fk_qr_value_column_id) {
if (!column?.fk_qr_value_column_id) {
column.fk_qr_value_column_id = column.colOptions.fk_qr_value_column_id
}
}
if (column.uidt === UITypes.Barcode && column.colOptions?.fk_barcode_value_column_id) {
if (!column?.fk_barcode_value_column_id) {
column.fk_barcode_value_column_id = column.colOptions.fk_barcode_value_column_id
}
}
if (column.uidt === UITypes.Lookup && column?.colOptions?.fk_lookup_column_id && column?.colOptions?.fk_relation_column_id) {
if (!column?.fk_lookup_column_id) {
column.fk_lookup_column_id = column.colOptions.fk_lookup_column_id
}
if (!column?.fk_relation_column_id) {
column.fk_relation_column_id = column.colOptions.fk_relation_column_id
}
}
if (
column.uidt === UITypes.Rollup &&
column?.colOptions?.fk_relation_column_id &&
column?.colOptions?.fk_rollup_column_id &&
column?.colOptions?.rollup_function
) {
if (!column?.fk_relation_column_id) {
column.fk_relation_column_id = column.colOptions.fk_relation_column_id
}
if (!column?.fk_rollup_column_id) {
column.fk_rollup_column_id = column.colOptions.fk_rollup_column_id
}
if (!column?.rollup_function) {
column.rollup_function = column.colOptions.rollup_function
}
}
if (column.uidt === UITypes.Formula && column.colOptions?.formula_raw && !column?.formula_raw) {
column.formula_raw = column.colOptions?.formula_raw
}
return column
}
const recoverField = (state: TableExplorerColumn) => {
const field = ops.value.find((op) => compareCols(op.column, state))
if (field) {
@ -512,10 +581,13 @@ const saveChanges = async () => {
view_id: view.value?.id as string,
}
}
}
for (const f of fields.value) {
console.log(f.title, getFieldOrder(f))
if (op && op.op === 'update') {
op.column.column_order = {
order: mop.order,
view_id: view.value?.id as string,
}
}
}
for (const op of ops.value) {
@ -545,7 +617,10 @@ const saveChanges = async () => {
await loadViewColumns()
if (res) {
ops.value = (res.failedOps as op[]) || []
ops.value =
res.failedOps && res.failedOps?.length
? (res.failedOps as (op & { error: unknown })[]).map(({ error: _, ...rest }) => rest)
: []
newFields.value = newFields.value.filter((col) => {
if (res.failedOps) {
const op = res.failedOps.find((fop) => {
@ -631,8 +706,14 @@ onKeyDown('ArrowUp', () => {
onKeyDown('Delete', () => {
if (isLocked.value) return
if (document.activeElement?.tagName === 'INPUT') return
if (document.activeElement?.tagName === 'TEXTAREA') return
if (
document.activeElement?.tagName === 'INPUT' ||
document.activeElement?.tagName === 'TEXTAREA' ||
// A rich text editor is a div with the contenteditable attribute set to true.
document.activeElement?.getAttribute('contenteditable')
) {
return
}
const isDeletedField = fieldStatus(activeField.value) === 'delete'
if (!isDeletedField && activeField.value) {
@ -643,8 +724,14 @@ onKeyDown('Delete', () => {
onKeyDown('Backspace', () => {
if (isLocked.value) return
if (document.activeElement?.tagName === 'INPUT') return
if (document.activeElement?.tagName === 'TEXTAREA') return
if (
document.activeElement?.tagName === 'INPUT' ||
document.activeElement?.tagName === 'TEXTAREA' ||
// A rich text editor is a div with the contenteditable attribute set to true.
document.activeElement?.getAttribute('contenteditable')
) {
return
}
const isDeletedField = fieldStatus(activeField.value) === 'delete'
if (!isDeletedField && activeField.value) {
@ -705,6 +792,16 @@ const onFieldOptionUpdate = () => {
isFieldIdCopied.value = false
}, 200)
}
watch(
fields,
() => {
if (activeField.value) {
activeField.value = fields.value.find((field) => field.id === activeField.value.id) || activeField.value
}
},
{ deep: true },
)
</script>
<template>
@ -771,7 +868,7 @@ const onFieldOptionUpdate = () => {
</div>
<div class="flex flex-row rounded-lg border-1 overflow-clip border-gray-200">
<div ref="fieldsListWrapperDomRef" class="nc-scrollbar-md !overflow-auto flex-1 flex-grow-1 nc-fields-height">
<Draggable v-model="fields" :disabled="isLocked" item-key="id" @change="onMove($event)">
<Draggable :model-value="fields" :disabled="isLocked" item-key="id" @change="onMove($event)">
<template #item="{ element: field }">
<div
v-if="field.title.toLowerCase().includes(searchQuery.toLowerCase()) && !field.pv"
@ -1080,7 +1177,7 @@ const onFieldOptionUpdate = () => {
</template>
</Draggable>
</div>
<Transition v-if="!changingField" name="slide-fade">
<Transition name="slide-fade">
<div v-if="!changingField" class="border-gray-200 border-l-1 nc-scrollbar-md nc-fields-height !overflow-y-auto">
<SmartsheetColumnEditOrAddProvider
v-if="activeField"

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

@ -1,6 +1,13 @@
<script setup lang="ts">
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { ViewTypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import {
ViewTypes,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
isSystemColumn,
isVirtualCol,
} from 'nocodb-sdk'
import type { Ref } from 'vue'
import MdiChevronDown from '~icons/mdi/chevron-down'
@ -278,7 +285,7 @@ const reloadHook = createEventHook()
reloadHook.on(() => {
reloadParentRowHook?.trigger(false)
if (isNew.value) return
_loadRow()
_loadRow(null, true)
})
provide(ReloadRowDataHookInj, reloadHook)
@ -458,7 +465,16 @@ const onIsExpandedUpdate = (v: boolean) => {
}
const isReadOnlyVirtualCell = (column: ColumnType) => {
return isRollup(column) || isFormula(column) || isBarcode(column) || isLookup(column) || isQrCode(column)
return (
isRollup(column) ||
isFormula(column) ||
isBarcode(column) ||
isLookup(column) ||
isQrCode(column) ||
isSystemColumn(column) ||
isCreatedOrLastModifiedTimeCol(column) ||
isCreatedOrLastModifiedByCol(column)
)
}
// Small hack. We need to scroll to the bottom of the form after its mounted and back to top.
@ -686,7 +702,7 @@ export default {
:ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="bg-white w-80 xs:w-full px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
:class="{
'!bg-gray-50 !px-0 !select-text': isReadOnlyVirtualCell(col),
'!bg-gray-50 !px-0 !select-text nc-system-field': isReadOnlyVirtualCell(col),
}"
>
<LazySmartsheetVirtualCell
@ -924,4 +940,8 @@ export default {
.nc-data-cell:focus-within {
@apply !border-1 !border-brand-500 !rounded-lg !shadow-none !ring-0;
}
:deep(.nc-system-field input) {
@apply bg-transparent;
}
</style>

41
packages/nc-gui/components/smartsheet/grid/GroupBy.vue

@ -1,13 +1,12 @@
<script lang="ts" setup>
import tinycolor from 'tinycolor2'
import dayjs from 'dayjs'
import { UITypes, dateFormats, timeFormats } from 'nocodb-sdk'
import { UITypes, dateFormats, parseStringDateTime, timeFormats } from 'nocodb-sdk'
import Table from './Table.vue'
import GroupBy from './GroupBy.vue'
import GroupByTable from './GroupByTable.vue'
import GroupByLabel from './GroupByLabel.vue'
import { GROUP_BY_VARS, computed, ref } from '#imports'
import type { Group, Row } from '#imports'
import { GROUP_BY_VARS, computed, ref } from '#imports'
const props = defineProps<{
group: Group
@ -154,21 +153,23 @@ const parseKey = (group: Group) => {
}
// show the groupBy dateTime field title format as like cell format
if (key && group.column?.uidt === UITypes.DateTime && dayjs(key).isValid()) {
const dateFormat = parseProp(group.column?.meta)?.date_format ?? dateFormats[0]
const timeFormat = parseProp(group.column?.meta)?.time_format ?? timeFormats[0]
const dateTimeFormat = `${dateFormat} ${timeFormat}`
return [dayjs(key).utc().local().format(dateTimeFormat)]
if (key && group.column?.uidt === UITypes.DateTime) {
return [
parseStringDateTime(
key,
`${parseProp(group.column?.meta)?.date_format ?? dateFormats[0]} ${
parseProp(group.column?.meta)?.time_format ?? timeFormats[0]
}`,
),
]
}
// show the groupBy time field title format as like cell format
if (key && group.column?.uidt === UITypes.Time && dayjs(key).isValid()) {
return [dayjs(key).format(timeFormats[0])]
if (key && group.column?.uidt === UITypes.Time) {
return [parseStringDateTime(key, timeFormats[0], false)]
}
if (key && group.column?.uidt === UITypes.User) {
if (key && [UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(group.column?.uidt as UITypes)) {
try {
const parsedKey = JSON.parse(key)
return [parsedKey]
@ -181,7 +182,19 @@ const parseKey = (group: Group) => {
}
const shouldRenderCell = (column) =>
[UITypes.Lookup, UITypes.Attachment, UITypes.Barcode, UITypes.QrCode, UITypes.Links, UITypes.User].includes(column?.uidt)
[
UITypes.Lookup,
UITypes.Attachment,
UITypes.Barcode,
UITypes.QrCode,
UITypes.Links,
UITypes.User,
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(column?.uidt)
</script>
<template>

12
packages/nc-gui/components/smartsheet/grid/GroupByLabel.vue

@ -1,19 +1,27 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
import { IsGroupByLabelInj, ReadonlyInj } from '#imports'
defineProps<{
column: ColumnType
modelValue: any
}>()
provide(ReadonlyInj, true)
provide(ReadonlyInj, ref(true))
provide(IsGroupByLabelInj, ref(true))
</script>
<template>
<div class="pointer-events-none">
<LazySmartsheetRow :row="{ row: { [column.title]: modelValue }, rowMeta: {} }">
<LazySmartsheetVirtualCell v-if="isVirtualCol(column)" :model-value="modelValue" class="!text-gray-600" :column="column" />
<LazySmartsheetVirtualCell
v-if="isVirtualCol(column)"
:model-value="modelValue"
class="!text-gray-600"
:column="column"
:read-only="true"
/>
<LazySmartsheetCell
v-else

22
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -1,8 +1,16 @@
<script lang="ts" setup>
import axios from 'axios'
import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { type ColumnReqType, type ColumnType, type PaginatedType, type TableType, type ViewType } from 'nocodb-sdk'
import {
UITypes,
ViewTypes,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
isSystemColumn,
isVirtualCol,
} from 'nocodb-sdk'
import { useColumnDrag } from './useColumnDrag'
import usePaginationShortcuts from './usePaginationShortcuts'
@ -1010,7 +1018,9 @@ const showFillHandle = computed(
!(
isLookup(fields.value[activeCell.col]) ||
isRollup(fields.value[activeCell.col]) ||
isFormula(fields.value[activeCell.col])
isFormula(fields.value[activeCell.col]) ||
isCreatedOrLastModifiedTimeCol(fields.value[activeCell.col]) ||
isCreatedOrLastModifiedByCol(fields.value[activeCell.col])
),
)
@ -1570,7 +1580,11 @@ onKeyStroke('ArrowDown', onDown)
'align-top': rowHeight && rowHeight !== 1,
'filling': isCellInFillRange(rowIndex, colIndex),
'readonly':
(isLookup(columnObj) || isRollup(columnObj) || isFormula(columnObj)) &&
(isLookup(columnObj) ||
isRollup(columnObj) ||
isFormula(columnObj) ||
isCreatedOrLastModifiedTimeCol(columnObj) ||
isCreatedOrLastModifiedByCol(columnObj)) &&
hasEditPermission &&
isCellSelected(rowIndex, colIndex),
'!border-r-blue-400 !border-r-3': toBeDroppedColId === columnObj.id,

25
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { ColumnReqType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import { computed } from 'vue'
import {
ActiveViewInj,
ColumnInj,
@ -128,6 +129,7 @@ const duplicateVirtualColumn = async () => {
id: undefined,
colOptions: undefined,
order: undefined,
system: false,
}
try {
@ -165,7 +167,17 @@ const duplicateVirtualColumn = async () => {
const openDuplicateDlg = async () => {
if (!column?.value) return
if (column.value.uidt && [UITypes.Lookup, UITypes.Rollup].includes(column.value.uidt as UITypes)) {
if (
column.value.uidt &&
[
UITypes.Lookup,
UITypes.Rollup,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(column.value.uidt as UITypes)
) {
duplicateVirtualColumn()
} else {
const gridViewColumnList = (await $api.dbViewColumn.list(view.value?.id as string)).list
@ -276,6 +288,13 @@ const onInsertAfter = () => {
isOpen.value = false
addColumn()
}
const isDeleteAllowed = computed(() => {
return column?.value && !column.value.system
})
const isDuplicateAllowed = computed(() => {
return column?.value && !column.value.system
})
</script>
<template>
@ -345,7 +364,7 @@ const onInsertAfter = () => {
<a-divider v-if="!column?.pk" class="!my-0" />
<NcMenuItem v-if="!column?.pk" @click="openDuplicateDlg">
<NcMenuItem v-if="!column?.pk" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg">
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
<component :is="iconMap.duplicate" class="text-gray-700" />
<!-- Duplicate -->
@ -368,7 +387,7 @@ const onInsertAfter = () => {
</NcMenuItem>
<a-divider v-if="!column?.pv" class="!my-0" />
<NcMenuItem v-if="!column?.pv" class="!hover:bg-red-50" @click="handleDelete">
<NcMenuItem v-if="!column?.pv" :disabled="!isDeleteAllowed" class="!hover:bg-red-50" @click="handleDelete">
<div class="nc-column-delete nc-header-menu-item text-red-600">
<component :is="iconMap.delete" />
<!-- Delete -->

6
packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts

@ -62,6 +62,12 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
return { icon: iconMap.rollup, color: 'text-grey' }
case UITypes.Count:
return { icon: CountIcon, color: 'text-grey' }
case UITypes.CreatedTime:
case UITypes.LastModifiedTime:
return { icon: iconMap.datetime, color: 'text-grey' }
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
return { icon: iconMap.phUser, color: 'text-grey' }
}
return { icon: iconMap.generic, color: 'text-grey' }

18
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { ColumnType, FilterType } from 'nocodb-sdk'
import { PlanLimitTypes, UITypes } from 'nocodb-sdk'
import type { Filter } from '#imports'
import {
ActiveViewInj,
AllFiltersInj,
@ -17,7 +18,6 @@ import {
useViewFilters,
watch,
} from '#imports'
import type { Filter } from '#imports'
interface Props {
nestedLevel?: number
@ -143,7 +143,7 @@ const filterUpdateCondition = (filter: FilterType, i: number) => {
// hence remove the previous value
filter.value = null
filter.comparison_sub_op = null
} else if ([UITypes.Date, UITypes.DateTime].includes(col.uidt as UITypes)) {
} else if (isDateType(col.uidt as UITypes)) {
// for date / datetime,
// the input type could be decimal or datepicker / datetime picker
// hence remove the previous value
@ -188,7 +188,7 @@ watch(
() => activeView.value?.id,
(n, o) => {
// if nested no need to reload since it will get reloaded from parent
if (!nested.value && n !== o && (hookId?.value || !webHook.value)) loadFilters(hookId?.value)
if (!nested.value && n !== o && (hookId?.value || !webHook.value)) loadFilters(hookId?.value, webHook.value)
},
)
@ -241,7 +241,7 @@ const selectFilterField = (filter: Filter, index: number) => {
isComparisonOpAllowed(filter, compOp),
)?.value as FilterType['comparison_op']
if ([UITypes.Date, UITypes.DateTime].includes(col.uidt as UITypes) && !['blank', 'notblank'].includes(filter.comparison_op!)) {
if (isDateType(col.uidt as UITypes) && !['blank', 'notblank'].includes(filter.comparison_op!)) {
if (filter.comparison_op === 'isWithin') {
filter.comparison_sub_op = 'pastNumberOfDays'
} else {
@ -325,7 +325,7 @@ const showFilterInput = (filter: Filter) => {
}
onMounted(() => {
loadFilters(hookId?.value)
loadFilters(hookId?.value, webHook.value)
})
onMounted(async () => {
@ -335,6 +335,10 @@ onMounted(async () => {
onBeforeUnmount(() => {
if (parentId.value) delete allFilters.value[parentId.value]
})
function isDateType(uidt: UITypes) {
return [UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(uidt)
}
</script>
<template>
@ -477,7 +481,7 @@ onBeforeUnmount(() => {
<div v-if="['blank', 'notblank'].includes(filter.comparison_op)" class="flex flex-grow"></div>
<NcSelect
v-else-if="[UITypes.Date, UITypes.DateTime].includes(getColumn(filter)?.uidt)"
v-else-if="isDateType(getColumn(filter)?.uidt)"
v-model:value="filter.comparison_sub_op"
v-e="['c:filter:sub-comparison-op:select']"
:dropdown-match-select-width="false"
@ -527,7 +531,7 @@ onBeforeUnmount(() => {
@update-filter-value="(value) => updateFilterValue(value, filter, i)"
@click.stop
/>
<div v-else-if="![UITypes.Date, UITypes.DateTime].includes(getColumn(filter)?.uidt)" class="flex-grow"></div>
<div v-else-if="!isDateType(getColumn(filter)?.uidt)" class="flex-grow"></div>
<NcButton
v-if="!filter.readOnly"

8
packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { RelationTypes, UITypes, isCreatedOrLastModifiedByCol, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
const props = defineProps<{
// As we need to focus search box when the parent is opened
@ -39,6 +39,12 @@ const options = computed<ColumnType[]>(
/** ignore virtual fields which are system fields ( mm relation ) and qr code fields */
return false
}
if (isCreatedOrLastModifiedByCol(c)) {
/** ignore created by and last modified by system field */
return false
}
return showSystemFields.value
} else if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode || c.uidt === UITypes.ID) {
return false

7
packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { RelationTypes, UITypes, isCreatedOrLastModifiedByCol, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
const props = defineProps<{
// As we need to focus search box when the parent is opened
@ -33,6 +33,11 @@ const options = computed<ColumnType[]>(
return true
}
if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
if (isCreatedOrLastModifiedByCol(c)) {
/** ignore created by and last modified by system field */
return false
}
return (
/** hide system columns if not enabled */
showSystemFields.value

17
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { RelationTypes, UITypes, isCreatedOrLastModifiedByCol, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { MetaInj, computed, inject, ref, resolveComponent, useViewColumnsOrThrow } from '#imports'
const { modelValue, isSort, allowEmpty, ...restProps } = defineProps<{
@ -26,12 +26,25 @@ const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const options = computed<SelectProps['options']>(() =>
(
customColumns.value ||
customColumns.value?.filter((c: ColumnType) => {
if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
if (isCreatedOrLastModifiedByCol(c)) {
/** ignore created by and last modified by system field */
return false
}
}
return true
}) ||
meta.value?.columns?.filter((c: ColumnType) => {
if (c.uidt === UITypes.Links) {
return true
}
if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
if (isCreatedOrLastModifiedByCol(c)) {
/** ignore created by and last modified by system field */
return false
}
return (
/** if the field is used in filter, then show it anyway */
localValue.value === c.id ||

20
packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue

@ -18,6 +18,8 @@ import {
isMultiSelect,
isPercent,
isRating,
isReadonlyDateTime,
isReadonlyUser,
isSingleSelect,
isTextArea,
isTime,
@ -68,7 +70,7 @@ provide(EditModeInj, readonly(editEnabled))
provide(ReadonlyInj, ref(false))
const checkTypeFunctions = {
const checkTypeFunctions: Record<string, (column: ColumnType, abstractType?: string) => boolean> = {
isSingleSelect,
isMultiSelect,
isDate,
@ -80,11 +82,13 @@ const checkTypeFunctions = {
isPercent,
isCurrency,
isDecimal,
isReadonlyDateTime,
isInt,
isFloat,
isTextArea,
isLinks: (col: ColumnType) => col.uidt === UITypes.Links,
isUser,
isReadonlyUser,
}
type FilterType = keyof typeof checkTypeFunctions
@ -102,7 +106,7 @@ const checkType = (filterType: FilterType) => {
return false
}
return checkTypeFunction(column.value, abstractType)
return checkTypeFunction(column.value, abstractType.value)
}
const filterInput = computed({
@ -142,6 +146,7 @@ const componentMap: Partial<Record<FilterType, any>> = computed(() => {
isDate: renderDateFilterInput(props.filter.comparison_sub_op!),
isYear: YearPicker,
isDateTime: renderDateFilterInput(props.filter.comparison_sub_op!),
isReadonlyDateTime: renderDateFilterInput(props.filter.comparison_sub_op!),
isTime: TimePicker,
isRating: Rating,
isDuration: Duration,
@ -152,6 +157,7 @@ const componentMap: Partial<Record<FilterType, any>> = computed(() => {
isFloat: Float,
isLinks: Integer,
isUser: User,
isReadonlyUser: User,
}
})
@ -178,6 +184,12 @@ const componentProps = computed(() => {
case 'isUser': {
return { forceMulti: true }
}
case 'isReadonlyUser': {
if (['anyof', 'nanyof'].includes(props.filter.comparison_op!)) {
return { forceMulti: true }
}
return {}
}
default: {
return {}
}
@ -212,7 +224,7 @@ provide(IsFormInj, ref(true))
/>
<div
v-else
class="bg-white border-1 flex flex-grow min-h-4 h-full items-center nc-filter-input-wrapper !rounded-lg"
class="bg-white border-1 flex flex-grow min-h-4 h-full px-1 items-center nc-filter-input-wrapper !rounded-lg"
:class="{ 'px-2': hasExtraPadding, 'border-brand-500': isInputBoxOnFocus }"
@mouseup.stop
>
@ -222,7 +234,7 @@ provide(IsFormInj, ref(true))
:disabled="filter.readOnly"
placeholder="Enter a value"
:column="column"
class="flex"
class="flex !rounded-lg"
v-bind="componentProps"
location="filter"
@focus="isInputBoxOnFocus = true"

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

@ -87,7 +87,7 @@ const onDrop = async (event: DragEvent) => {
event.preventDefault()
try {
// Access the dropped data
const data = JSON.parse(event.dataTransfer!.getData('text/json'))
const data = JSON.parse(event.dataTransfer!.getData('text/json') || '{}')
// Do something with the received data
// if dragged item is not from the same source, return

13
packages/nc-gui/components/template/Editor.vue

@ -100,9 +100,16 @@ const uiTypeOptions = ref<Option[]>(
.filter(
(uiType) =>
!isVirtualCol(UITypes[uiType]) &&
![UITypes.ForeignKey, UITypes.ID, UITypes.CreateTime, UITypes.LastModifiedTime, UITypes.Barcode, UITypes.Button].includes(
UITypes[uiType],
),
![
UITypes.ForeignKey,
UITypes.ID,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Barcode,
UITypes.Button,
].includes(UITypes[uiType]),
)
.map<Option>((uiType) => ({
value: uiType,

3
packages/nc-gui/components/virtual-cell/Formula.vue

@ -11,7 +11,6 @@ import {
renderValue,
replaceUrlsWithLink,
useBase,
useGlobal,
} from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck
@ -23,8 +22,6 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const { isPg } = useBase()
const { showNull } = useGlobal()
const result = computed(() =>
isPg(column.value.source_id) ? renderValue(handleTZ(cellValue?.value)) : renderValue(cellValue?.value),
)

6
packages/nc-gui/components/virtual-cell/Lookup.vue

@ -25,6 +25,8 @@ const meta = inject(MetaInj, ref())
const cellValue = inject(CellValueInj, ref())
const isGroupByLabel = inject(IsGroupByLabelInj, ref(false))
// Change the row height of the child cell under lookup
// Other wise things like text will can take multi line tag
const providedHeightRef = ref(1) as any
@ -100,7 +102,7 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
<div
class="h-full w-full nc-lookup-cell"
tabindex="-1"
:style="{ height: rowHeight && rowHeight !== 1 ? `${rowHeight * 2}rem` : `2.85rem` }"
:style="{ height: isGroupByLabel ? undefined : rowHeight && rowHeight !== 1 ? `${rowHeight * 2}rem` : `2.85rem` }"
@dblclick="activateShowEditNonEditableFieldWarning"
>
<div
@ -169,6 +171,8 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
UITypes.MultiSelect,
UITypes.SingleSelect,
UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(lookupColumn.uidt),
'min-h-0 min-w-0': isAttachment(lookupColumn),
}"

7
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -49,15 +49,15 @@ const {
loadChildrenList,
childrenListPagination,
relatedTableDisplayValueProp,
displayValueTypeAndFormatProp,
unlink,
isChildrenListLoading,
isChildrenListLinked,
isChildrenLoading,
relatedTableMeta,
row,
link,
meta,
displayValueProp,
headerDisplayValue,
} = useLTARStoreOrThrow()
const { isNew, state, removeLTARRef, addLTARRef } = useSmartsheetRowStoreOrThrow()
@ -195,7 +195,7 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
:table-title="meta?.title"
:header="$t('activity.linkedRecords')"
:related-table-title="relatedTableMeta?.title"
:display-value="row.row[displayValueProp]"
:display-value="headerDisplayValue"
/>
<div v-if="!isForm" class="flex mt-2 mb-2 items-center gap-2">
<div
@ -260,6 +260,7 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
data-testid="nc-child-list-item"
:attachment="attachmentCol"
:related-table-display-value-prop="relatedTableDisplayValueProp"
:display-value-type-and-format-prop="displayValueTypeAndFormatProp"
:is-linked="childrenList?.list ? isChildrenListLinked[Number.parseInt(id)] : true"
:is-loading="isChildrenListLoading[Number.parseInt(id)]"
@expand="onClick(refRow)"

22
packages/nc-gui/components/virtual-cell/components/ListItem.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk'
import { UITypes, isVirtualCol, parseStringDateTime } from 'nocodb-sdk'
import {
type ComputedRef,
IsExpandedFormOpenInj,
@ -22,6 +22,7 @@ const props = defineProps<{
fields: any[]
attachment: any
relatedTableDisplayValueProp: string
displayValueTypeAndFormatProp: { type: string; format: string }
isLoading: boolean
isLinked: boolean
}>()
@ -68,6 +69,21 @@ const attachments: ComputedRef<Attachment[]> = computed(() => {
return []
}
})
const displayValue = computed(() => {
if (
row.value[props.relatedTableDisplayValueProp] &&
props.displayValueTypeAndFormatProp.type &&
props.displayValueTypeAndFormatProp.format
) {
return parseStringDateTime(
row.value[props.relatedTableDisplayValueProp],
props.displayValueTypeAndFormatProp.format,
!(props.displayValueTypeAndFormatProp.format === UITypes.Time),
)
}
return row.value[props.relatedTableDisplayValueProp]
})
</script>
<template>
@ -103,8 +119,8 @@ const attachments: ComputedRef<Attachment[]> = computed(() => {
<div class="flex flex-col m-[.75rem] gap-1 flex-grow justify-center overflow-hidden">
<div class="flex justify-between xs:gap-x-2">
<span class="font-semibold text-gray-800 nc-display-value xs:(truncate)">
{{ row[relatedTableDisplayValueProp] }}
<span class="font-semibold text-brand-500 nc-display-value xs:(truncate)">
{{ displayValue }}
</span>
<div
v-if="isLinked && !isLoading"

6
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -37,18 +37,19 @@ const {
childrenExcludedList,
isChildrenExcludedListLinked,
isChildrenExcludedListLoading,
displayValueProp,
isChildrenExcludedLoading,
childrenListCount,
loadChildrenExcludedList,
loadChildrenList,
childrenExcludedListPagination,
relatedTableDisplayValueProp,
displayValueTypeAndFormatProp,
link,
relatedTableMeta,
meta,
unlink,
row,
headerDisplayValue,
} = useLTARStoreOrThrow()
const { addLTARRef, isNew, removeLTARRef, state: rowState } = useSmartsheetRowStoreOrThrow()
@ -235,7 +236,7 @@ const onCreatedRecord = (record: any) => {
:relation="relation"
:table-title="meta?.title"
:related-table-title="relatedTableMeta?.title"
:display-value="row.row[displayValueProp]"
:display-value="headerDisplayValue"
:header="$t('activity.addNewLink')"
/>
<div class="flex mt-2 mb-2 items-center gap-2">
@ -315,6 +316,7 @@ const onCreatedRecord = (record: any) => {
:fields="fields"
:attachment="attachmentCol"
:related-table-display-value-prop="relatedTableDisplayValueProp"
:display-value-type-and-format-prop="displayValueTypeAndFormatProp"
:is-loading="isChildrenExcludedListLoading[Number.parseInt(id)]"
:is-linked="isChildrenExcludedListLinked[Number.parseInt(id)]"
@expand="

35
packages/nc-gui/components/workspace/CollaboratorsList.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { OrderedWorkspaceRoles, WorkspaceUserRoles, parseStringDateTime, timeAgo } from 'nocodb-sdk'
import { storeToRefs, useWorkspace } from '#imports'
import { storeToRefs, useUserSorts, useWorkspace } from '#imports'
const { workspaceRoles, loadRoles } = useRoles()
@ -9,6 +9,9 @@ const workspaceStore = useWorkspace()
const { removeCollaborator, updateCollaborator: _updateCollaborator } = workspaceStore
const { collaborators } = storeToRefs(workspaceStore)
const { sorts, sortDirection, loadSorts, saveOrUpdate, handleGetSortedData } = useUserSorts('Workspace')
const userSearchText = ref('')
const filterCollaborators = computed(() => {
@ -19,11 +22,20 @@ const filterCollaborators = computed(() => {
return collaborators.value.filter((collab) => collab.email!.includes(userSearchText.value))
})
const sortedCollaborators = computed(() => {
return handleGetSortedData(filterCollaborators.value, sorts.value)
})
const updateCollaborator = async (collab: any, roles: WorkspaceUserRoles) => {
collab.roles = roles
try {
await _updateCollaborator(collab.id, collab.roles)
await _updateCollaborator(collab.id, roles)
message.success('Successfully updated user role')
collaborators.value?.forEach((collaborator) => {
if (collaborator.id === collab.id) {
collaborator.roles = roles
}
})
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -39,6 +51,7 @@ const accessibleRoles = computed<WorkspaceUserRoles[]>(() => {
onMounted(async () => {
await loadRoles()
loadSorts()
})
</script>
@ -59,15 +72,25 @@ onMounted(async () => {
<div v-else class="nc-collaborators-list mt-6 h-full">
<div class="flex flex-col rounded-lg overflow-hidden border-1 max-w-350 max-h-[calc(100%-8rem)]">
<div class="flex flex-row bg-gray-50 min-h-12 items-center">
<div class="text-gray-700 users-email-grid w-3/8 ml-10 mr-3">{{ $t('objects.users') }}</div>
<div class="text-gray-700 user-access-grid w-2/8 mr-3">{{ $t('general.access') }}</div>
<div class="text-gray-700 users-email-grid w-3/8 ml-10 mr-3 flex items-center space-x-2">
<span>
{{ $t('objects.users') }}
</span>
<LazyAccountUserMenu :direction="sortDirection.email" field="email" :handle-user-sort="saveOrUpdate" />
</div>
<div class="text-gray-700 user-access-grid w-2/8 mr-3 flex items-center space-x-2">
<span>
{{ $t('general.access') }}
</span>
<LazyAccountUserMenu :direction="sortDirection.roles" field="roles" :handle-user-sort="saveOrUpdate" />
</div>
<div class="text-gray-700 date-joined-grid w-2/8 mr-3">{{ $t('title.dateJoined') }}</div>
<div class="text-gray-700 user-access-grid w-1/8">Actions</div>
</div>
<div class="flex flex-col nc-scrollbar-md">
<div
v-for="(collab, i) of filterCollaborators"
v-for="(collab, i) of sortedCollaborators"
:key="i"
class="flex flex-row border-b-1 py-1 min-h-14 items-center justify-around last"
>

6
packages/nc-gui/composables/useApi/interceptors.ts

@ -84,11 +84,15 @@ export function addAxiosInterceptors(api: Api<any>) {
// ignore since it could have already been handled and redirected to sign in
})
} else {
// if
refreshTokenPromise = new Promise<string>((resolve, reject) => {
refreshTokenPromiseRes = resolve
refreshTokenPromiseRej = reject
})
// set a catch on the promise to avoid unhandled promise rejection
refreshTokenPromise.catch(() => {
// ignore
})
}
// Try request again with new token

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

@ -113,6 +113,18 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
{
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
if (
(tableExplorerColumns?.value || meta.value?.columns)?.some(
(c) =>
c.id !== formState.value.id && // ignore current column
// compare against column_name and title
((value || '').toLowerCase() === (c.column_name || '').toLowerCase() ||
(value || '').toLowerCase() === (c.title || '').toLowerCase()) &&
c.system,
)
) {
return reject(new Error(t('msg.error.duplicateSystemColumnName')))
}
if (
(tableExplorerColumns?.value || meta.value?.columns)?.some(
(c) =>

63
packages/nc-gui/composables/useData.ts

@ -1,5 +1,5 @@
import { UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, RelationTypes, TableType, ViewType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import {
NOCO,
@ -236,14 +236,18 @@ export function useData(args: {
toUpdate.row,
metaValue!.columns!.reduce<Record<string, any>>((acc: Record<string, any>, col: ColumnType) => {
if (
col.uidt === UITypes.Formula ||
col.uidt === UITypes.QrCode ||
col.uidt === UITypes.Barcode ||
col.uidt === UITypes.Rollup ||
col.uidt === UITypes.Checkbox ||
col.uidt === UITypes.User ||
col.au ||
col.cdf?.includes(' on update ')
col.title in updatedRowData &&
(col.uidt === UITypes.Formula ||
col.uidt === UITypes.QrCode ||
col.uidt === UITypes.Barcode ||
col.uidt === UITypes.Rollup ||
col.uidt === UITypes.Checkbox ||
col.uidt === UITypes.User ||
col.uidt === UITypes.LastModifiedTime ||
col.uidt === UITypes.LastModifiedBy ||
col.uidt === UITypes.Lookup ||
col.au ||
(col.cdf && / on update /i.test(col.cdf)))
)
acc[col.title!] = updatedRowData[col.title!]
return acc
@ -376,33 +380,26 @@ export function useData(args: {
}
for (const row of rows) {
if (!undo) {
/** update row data(to sync formula and other related columns)
* update only formula, rollup and auto updated datetime columns data to avoid overwriting any changes made by user
*/
Object.assign(
row.row,
metaValue!.columns!.reduce<Record<string, any>>((acc: Record<string, any>, col: ColumnType) => {
if (
col.uidt === UITypes.Formula ||
col.uidt === UITypes.QrCode ||
col.uidt === UITypes.Barcode ||
col.uidt === UITypes.Rollup ||
col.uidt === UITypes.Checkbox ||
col.uidt === UITypes.User ||
col.au ||
col.cdf?.includes(' on update ')
)
acc[col.title!] = row.row[col.title!]
return acc
}, {} as Record<string, any>),
)
Object.assign(row.oldRow, row.row)
}
if (row.rowMeta) row.rowMeta.saving = false
}
// reload data since row update may change the other columns data( Formula, QrCode, Barcode, Rollup, Checkbox, User, Auto Updated Datetime, etc...)
if (
metaValue!.columns!.some((col) =>
[
UITypes.QrCode,
UITypes.LastModifiedTime,
UITypes.Barcode,
UITypes.Formula,
UITypes.Lookup,
UITypes.Rollup,
UITypes.LinkToAnotherRecord,
UITypes.LastModifiedBy,
].includes(col.uidt),
)
) {
await callbacks?.loadData?.()
}
await callbacks?.globalCallback?.()
}

19
packages/nc-gui/composables/useExpandedFormStore.ts

@ -1,4 +1,4 @@
import { UITypes, ViewTypes } from 'nocodb-sdk'
import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk'
import type { AuditType, ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import dayjs from 'dayjs'
@ -295,8 +295,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
changedColumns.value = new Set()
}
const loadRow = async (rowId?: string) => {
const record = await $api.dbTableRow.read(
const loadRow = async (rowId?: string, onlyVirtual = false) => {
let record = await $api.dbTableRow.read(
NOCO,
// todo: base_id missing on view type
(base?.value?.id || (sharedView.value?.view as any)?.base_id) as string,
@ -307,6 +307,19 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
},
)
// update only virtual columns value if `onlyVirtual` is true
if (onlyVirtual) {
record = {
...row.value.row,
...meta.value.columns.reduce((partialRecord, col) => {
if (isVirtualCol(col) && col.title in record) {
partialRecord[col.title] = record[col.title]
}
return partialRecord
}, {} as Record<string, any>),
}
}
Object.assign(row.value, {
row: record,
oldRow: { ...record },

2
packages/nc-gui/composables/useGlobal/index.ts

@ -66,6 +66,8 @@ export const useGlobal = createGlobalState((): UseGlobalReturn => {
(nextPayload) => {
if (nextPayload) {
state.user.value = {
// keep existing props if user id same as before
...(state.user.value?.id === nextPayload.id ? state.user.value || {} : {}),
id: nextPayload.id,
email: nextPayload.email,
firstname: nextPayload.firstname,

57
packages/nc-gui/composables/useLTARStore.ts

@ -1,4 +1,14 @@
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, RequestParams, TableType } from 'nocodb-sdk'
import {
type ColumnType,
type LinkToAnotherRecordType,
type PaginatedType,
type RequestParams,
type TableType,
UITypes,
dateFormats,
parseStringDateTime,
timeFormats,
} from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import {
IsPublicInj,
@ -9,6 +19,7 @@ import {
extractSdkResponseErrorMsg,
inject,
message,
parseProp,
reactive,
ref,
storeToRefs,
@ -124,10 +135,52 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const relatedTablePrimaryKeyProps = computed(() => {
return relatedTableMeta.value?.columns?.filter((c) => c.pk)?.map((c) => c.title) ?? []
})
const displayValueProp = computed(() => {
return (meta.value?.columns?.find((c: Required<ColumnType>) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title
})
const displayValueTypeAndFormatProp = computed(() => {
let displayValueTypeAndFormat = {
type: '',
format: '',
}
const currentColumn = relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0]
if (currentColumn) {
if (currentColumn?.uidt === UITypes.DateTime) {
displayValueTypeAndFormat = {
type: currentColumn?.uidt,
format: `${parseProp(currentColumn?.meta)?.date_format ?? dateFormats[0]} ${
parseProp(currentColumn?.meta)?.time_format ?? timeFormats[0]
}`,
}
}
if (currentColumn?.uidt === UITypes.Time) {
displayValueTypeAndFormat = {
type: currentColumn?.uidt,
format: `${timeFormats[0]}`,
}
}
}
return displayValueTypeAndFormat
})
const headerDisplayValue = computed(() => {
if (
row.value.row[displayValueProp.value] &&
displayValueTypeAndFormatProp.value.type &&
displayValueTypeAndFormatProp.value.format
) {
return parseStringDateTime(
row.value.row[displayValueProp.value],
displayValueTypeAndFormatProp.value.format,
!(displayValueTypeAndFormatProp.value.format === UITypes.Time),
)
}
return row.value.row[displayValueProp.value]
})
const loadChildrenExcludedList = async (activeState?: any) => {
if (activeState) newRowState.state = activeState
try {
@ -470,6 +523,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
relatedTableMeta,
loadRelatedTableMeta,
relatedTableDisplayValueProp,
displayValueTypeAndFormatProp,
childrenExcludedList,
childrenList,
childrenListCount,
@ -491,6 +545,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
isChildrenExcludedLoading,
deleteRelatedRow,
getRelatedTableRowId,
headerDisplayValue,
}
},
'ltar-store',

4
packages/nc-gui/composables/useMultiSelect/convertCellData.ts

@ -195,7 +195,9 @@ export default function convertCellData(
return validVals.join(',')
}
case UITypes.User: {
case UITypes.User:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy: {
let parsedVal
try {
try {

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

@ -1,6 +1,6 @@
import type { Ref } from 'vue'
import { computed } from 'vue'
import dayjs from 'dayjs'
import type { Ref } from 'vue'
import type { MaybeRef } from '@vueuse/core'
import type { ColumnType, LinkToAnotherRecordType, TableType, UserFieldRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, dateFormats, isDateMonthFormat, isSystemColumn, isVirtualCol, timeFormats } from 'nocodb-sdk'
@ -112,13 +112,15 @@ export function useMultiSelect(
textToCopy = !!textToCopy
}
if (columnObj.uidt === UITypes.User) {
if (textToCopy && Array.isArray(textToCopy)) {
textToCopy = textToCopy
.map((user: UserFieldRecordType) => {
return user.email
})
.join(', ')
if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(columnObj.uidt as UITypes)) {
if (textToCopy) {
textToCopy = Array.isArray(textToCopy)
? textToCopy
: [textToCopy]
.map((user: UserFieldRecordType) => {
return user.email
})
.join(', ')
}
}
@ -136,7 +138,7 @@ export function useMultiSelect(
})
}
if (columnObj.uidt === UITypes.DateTime) {
if ([UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(columnObj.uidt)) {
// remove `"`
// e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z
textToCopy = textToCopy.replace(/["']/g, '')

2
packages/nc-gui/composables/useSmartsheetStore.ts

@ -21,7 +21,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
const { sqlUis } = storeToRefs(baseStore)
const sqlUi = ref(
const sqlUi = computed(() =>
(meta.value as TableType)?.source_id ? sqlUis.value[(meta.value as TableType).source_id!] : Object.values(sqlUis.value)[0],
)

177
packages/nc-gui/composables/useUserSorts.ts

@ -0,0 +1,177 @@
import rfdc from 'rfdc'
import { OrderedOrgRoles, OrderedProjectRoles, OrderedWorkspaceRoles } from 'nocodb-sdk'
import type { UsersSortType } from '~/lib'
import { useGlobal } from '#imports'
/**
* Hook for managing user sorts and sort configurations.
*
* @param {string} roleType - The type of role for which user sorts are managed ('Workspace', 'Org', or 'Project').
* @returns {object} An object containing reactive values and functions related to user sorts.
*/
export function useUserSorts(roleType: 'Workspace' | 'Org' | 'Project') {
const clone = rfdc()
const { user } = useGlobal()
const sorts = ref<UsersSortType>({})
// Key for storing user sort configurations in local storage
const userSortConfigKey = 'userSortConfig'
// Default user ID if no user found (fallback)
const defaultUserId = 'default'
/**
* Computed property that returns a record of sort directions based on the current sort configurations.
* @type {ComputedRef<Record<string, UsersSortType['direction']>>}
*/
const sortDirection: ComputedRef<Record<string, UsersSortType['direction']>> = computed(() => {
if (sorts.value.field) {
return { [sorts.value.field]: sorts.value.direction } as Record<string, UsersSortType['direction']>
}
return {} as Record<string, UsersSortType['direction']>
})
/**
* Loads user sort configurations from local storage based on the current user ID.
*/
function loadSorts(): void {
try {
// Retrieve sort configuration from local storage
const storedConfig = localStorage.getItem(userSortConfigKey)
const sortConfig = storedConfig ? JSON.parse(storedConfig) : ({} as Record<string, UsersSortType>)
if (sortConfig && isValidSortConfig(sortConfig)) {
// Load user-specific sort configurations or default configurations
sorts.value = user.value?.id ? sortConfig[user.value.id] || {} : sortConfig[defaultUserId] || {}
} else {
throw new Error('Invalid sort config stored in local storage')
}
} catch (error) {
console.error(error)
// remove sortConfig from localStorage in case of error
localStorage.removeItem(userSortConfigKey)
// Set sorts to an empty obj in case of an error
sorts.value = {}
}
}
/**
* Saves or updates a user sort configuration and updates local storage.
* @param {UsersSortType} newSortConfig - The new sort configuration to save or update.
*/
function saveOrUpdate(newSortConfig: UsersSortType): void {
try {
if (newSortConfig.field && newSortConfig.direction) {
sorts.value = { ...newSortConfig }
} else {
sorts.value = {}
}
// Update local storage with the new sort configurations
const storedConfig = localStorage.getItem(userSortConfigKey)
const sortConfig = storedConfig ? JSON.parse(storedConfig) : {}
if (user.value?.id) {
// Save or delete user-specific sort configurations
if (sorts.value.field) {
sortConfig[user.value.id] = sorts.value
} else {
delete sortConfig[user.value.id]
}
} else {
// Save or delete default user sort configurations
sortConfig[defaultUserId] = sorts.value
}
localStorage.setItem(userSortConfigKey, JSON.stringify(sortConfig))
} catch (error) {
console.error('Error while saving sort configuration into local storage:', error)
}
}
/**
* Sorts and returns a deep copy of an array of objects based on the provided sort configurations.
*
* @param data - The array of objects to be sorted.
* @param sortsConfig - The object of sort configurations.
* @returns A new array containing sorted objects.
* @template T - The type of objects in the input array.
*/
function handleGetSortedData<T extends Record<string, any>>(data: T[], sortsConfig: UsersSortType = sorts.value): T[] {
let userRoleOrder: string[] = []
if (roleType === 'Workspace') {
userRoleOrder = Object.values(OrderedWorkspaceRoles)
} else if (roleType === 'Org') {
userRoleOrder = Object.values(OrderedOrgRoles)
} else if (roleType === 'Project') {
userRoleOrder = Object.values(OrderedProjectRoles)
}
data = clone(data)
const superUserIndex = data.findIndex((user) => user?.roles?.includes('super'))
const superUser = sortsConfig.field === 'roles' && superUserIndex !== -1 ? data.splice(superUserIndex, 1) : null
let sortedData = data.sort((a, b) => {
switch (sortsConfig.field) {
case 'roles': {
const roleA = a?.roles?.split(',')[0]
const roleB = b?.roles?.split(',')[0]
if (sortsConfig.direction === 'asc') {
return userRoleOrder.indexOf(roleA) - userRoleOrder.indexOf(roleB)
} else {
return userRoleOrder.indexOf(roleB) - userRoleOrder.indexOf(roleA)
}
}
case 'email': {
if (sortsConfig.direction === 'asc') {
return a[sortsConfig.field]?.localeCompare(b[sortsConfig.field])
} else {
return b[sortsConfig.field]?.localeCompare(a[sortsConfig.field])
}
}
}
return 0
})
if (superUser && superUser.length) {
if (sortsConfig.direction === 'desc') {
sortedData = [...sortedData, superUser[0]]
} else {
sortedData = [superUser[0], ...sortedData]
}
}
return sortedData
}
/**
* Checks if the provided sort configuration has the expected structure.
* @param sortConfig - The sort configuration to validate.
* @param expectedStructure - The expected structure for the sort configuration.
* Defaults to { field: 'email', direction: 'asc' }.
* @returns `true` if the sort configuration is valid, otherwise `false`.
*/
function isValidSortConfig(
sortConfig: Record<string, any>,
expectedStructure: UsersSortType = { field: 'email', direction: 'asc' },
): boolean {
// Check if the sortConfig has the expected keys
for (const key in sortConfig) {
const isValidConfig = Object.keys(sortConfig[key]).every((key) =>
Object.prototype.hasOwnProperty.call(expectedStructure, key),
)
if (!isValidConfig) return false
}
return true
}
return { sorts, sortDirection, loadSorts, saveOrUpdate, handleGetSortedData }
}

9
packages/nc-gui/composables/useViewColumns.ts

@ -1,4 +1,4 @@
import { ViewTypes, isSystemColumn } from 'nocodb-sdk'
import { ViewTypes, isCreatedOrLastModifiedByCol, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, GridColumnReqType, GridColumnType, MapType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { computed, ref, storeToRefs, useBase, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
@ -70,7 +70,12 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
}, {})
fields.value = meta.value?.columns
?.map((column: ColumnType) => {
?.filter((column: ColumnType) => {
// filter created by and last modified by system columns
if (isCreatedOrLastModifiedByCol(column) && column.system) return false
return true
})
.map((column: ColumnType) => {
const currentColumnField = fieldById[column.id!] || {}
return {

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

@ -211,7 +211,8 @@ export function useViewData(
if (error.code === 'ERR_CANCELED') {
return
}
throw error
console.error(error)
return message.error(await extractSdkResponseErrorMsg(error))
}
formattedData.value = formatData(response.list)
paginationData.value = response.pageInfo
@ -238,10 +239,13 @@ export function useViewData(
async function changePage(page: number) {
paginationData.value.page = page
await loadData({
offset: (page - 1) * (paginationData.value.pageSize || appInfoDefaultLimit),
where: where?.value,
} as any)
await loadData(
{
offset: (page - 1) * (paginationData.value.pageSize || appInfoDefaultLimit),
where: where?.value,
} as any,
true,
)
}
const {

8
packages/nc-gui/composables/useViewFilters.ts

@ -190,7 +190,7 @@ export function useViewFilters(
}
}
const loadFilters = async (hookId?: string) => {
const loadFilters = async (hookId?: string, isWebhook = false) => {
if (!view.value?.id) return
if (nestedMode.value) {
@ -199,11 +199,11 @@ export function useViewFilters(
}
try {
if (hookId) {
if (isWebhook || hookId) {
if (parentId) {
filters.value = (await $api.dbTableFilter.childrenRead(parentId)).list as Filter[]
} else {
filters.value = (await $api.dbTableWebhookFilter.read(hookId!)).list as Filter[]
} else if (hookId) {
filters.value = (await $api.dbTableWebhookFilter.read(hookId)).list as Filter[]
}
} else {
if (parentId) {

12
packages/nc-gui/composables/useViewGroupBy.ts

@ -90,7 +90,7 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE
}
if (col.uidt === UITypes.User) {
if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(col.uidt as UITypes)) {
if (!value) {
return GROUP_BY_VARS.NULL
}
@ -161,11 +161,15 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
acc += `${acc.length ? '~and' : ''}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})`
} else if ([UITypes.Date, UITypes.DateTime].includes(curr.column_uidt as UITypes)) {
acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})`
} else if (curr.column_uidt === UITypes.User) {
} else if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(curr.column_uidt as UITypes)) {
try {
const value = JSON.parse(curr.key)
acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${value.map((v: any) => v.id).join(',')})`
} catch (e) {}
acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${(Array.isArray(value) ? value : [value])
.map((v: any) => v.id)
.join(',')})`
} catch (e) {
console.error(e)
}
} else {
acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${curr.key})`
}

1
packages/nc-gui/context/index.ts

@ -13,6 +13,7 @@ export const IsFormInj: InjectionKey<Ref<boolean>> = Symbol('is-form-injection')
export const IsSurveyFormInj: InjectionKey<Ref<boolean>> = Symbol('is-survey-form-injection')
export const IsGridInj: InjectionKey<Ref<boolean>> = Symbol('is-grid-injection')
export const IsGroupByInj: InjectionKey<Ref<boolean>> = Symbol('is-group-by-injection')
export const IsGroupByLabelInj: InjectionKey<Ref<boolean>> = Symbol('is-group-by-label-injection')
export const IsGalleryInj: InjectionKey<Ref<boolean>> = Symbol('is-gallery-injection')
export const IsKanbanInj: InjectionKey<Ref<boolean>> = Symbol('is-kanban-injection')
export const IsLockedInj: InjectionKey<Ref<boolean>> = Symbol('is-locked-injection')

5
packages/nc-gui/lang/ar.json

@ -273,7 +273,7 @@
"Count": "العد",
"Lookup": "مشاهدة بيانات",
"DateTime": "تاريخ وقت",
"CreateTime": "إنشاء وقت",
"CreatedTime": "إنشاء وقت",
"LastModifiedTime": "وقت آخر تعديل",
"AutoNumber": "عدد تلقائي",
"Barcode": "رمز",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

5
packages/nc-gui/lang/bn_IN.json

@ -273,7 +273,7 @@
"Count": "গণন",
"Lookup": "খ",
"DateTime": "তিখ সময",
"CreateTime": "সমযি করন",
"CreatedTime": "সমযি করন",
"LastModifiedTime": "শষ পরিবরিত সময",
"AutoNumber": "অট নমবর",
"Barcode": "বরকড",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

5
packages/nc-gui/lang/cs.json

@ -273,7 +273,7 @@
"Count": "Počet",
"Lookup": "Vyhledávání",
"DateTime": "Datum a čas",
"CreateTime": "Vytvořit čas",
"CreatedTime": "Vytvořit čas",
"LastModifiedTime": "Čas poslední úpravy",
"AutoNumber": "Automatické číslo",
"Barcode": "Čárový kód",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Následující znaky nejsou povoleny",
"columnNameRequired": "Název sloupce je povinný",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Název projektu přesahuje 50 znaků",
@ -1347,4 +1348,4 @@
"roleUpdated": "Role byla úspěšně aktualizována"
}
}
}
}

5
packages/nc-gui/lang/da.json

@ -273,7 +273,7 @@
"Count": "Tælle",
"Lookup": "Kig op",
"DateTime": "Dato tid",
"CreateTime": "Opret tid",
"CreatedTime": "Opret tid",
"LastModifiedTime": "Sidste ændret tid",
"AutoNumber": "Auto nummer.",
"Barcode": "Stregkode.",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Følgende tegn er ikke tilladt",
"columnNameRequired": "Kolonnens navn er påkrævet",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "Længden af kolonnenavnet overstiger maks. {value} tegn",
"projectNameExceeds50Characters": "Projektnavnet overstiger 50 tegn",
@ -1347,4 +1348,4 @@
"roleUpdated": "Rolle opdateret med succes"
}
}
}
}

211
packages/nc-gui/lang/de.json

@ -1,7 +1,7 @@
{
"dashboards": {
"create_new_dashboard_project": "Create New Interface",
"connect_data_sources": "Connect data sources",
"connect_data_sources": "Datenquellen hinzufügen",
"alert": "Alert",
"alert-message": "No databases have been connected. Connect database bases to build interfaces. Skip this step and add databases from the base home page later.",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Select Database Bases that you want to link to this Interface.",
@ -53,12 +53,12 @@
"add": "Hinzufügen",
"edit": "Bearbeiten",
"link": "Link",
"links": "Links",
"links": "Verknüpfung",
"remove": "Entfernen",
"import": "Import",
"logout": "Log Out",
"logout": "Ausloggen",
"empty": "Empty",
"changeIcon": "Change Icon",
"changeIcon": "Icon ändern",
"save": "Speichern",
"available": "Available",
"abort": "Abort",
@ -101,7 +101,7 @@
"reset": "Zurücksetzen",
"install": "Installieren",
"show": "Anzeigen",
"access": "Access",
"access": "Zugriff",
"visibility": "Visibility",
"hide": "Verstecken",
"deprecated": "Deprecated",
@ -161,13 +161,13 @@
"sortAsc": "Aufsteigend sortieren",
"sortDesc": "Absteigend sortieren",
"move": "Move",
"geoDataField": "GeoDaten Feld",
"geoDataField": "Geodaten Feld",
"type": "Type",
"name": "Name",
"changes": "Changes",
"new": "New",
"old": "Old",
"data": "Data",
"data": "Daten",
"source": "Source",
"destination": "Destination",
"active": "Active",
@ -192,8 +192,8 @@
"paste": "Paste"
},
"objects": {
"workspace": "Workspace",
"workspaces": "Workspaces",
"workspace": "Arbeitsbereich",
"workspaces": "Arbeitsbereiche",
"project": "Projekt",
"projects": "Projekte",
"table": "Tabelle",
@ -230,20 +230,20 @@
"editor": "Bearbeiter",
"commenter": "Kommentator",
"viewer": "Betrachter",
"noaccess": "No Access",
"noaccess": "Kein Zugriff",
"superAdmin": "Super Admin",
"orgLevelCreator": "Organisationsebenen-Ersteller",
"orgLevelViewer": "Organisationsebenen-Betrachter"
},
"sqlVIew": "SQL Ansicht",
"rowHeight": "Record Height",
"rowHeight": "Zeilenhöhe",
"heightClass": {
"short": "Short",
"medium": "Medium",
"tall": "Tall",
"extra": "Extra"
"short": "klein",
"medium": "mittel",
"tall": "groß",
"extra": "riesig"
},
"externalDb": "External Database"
"externalDb": "Externe Datenbank"
},
"datatype": {
"ID": "ID",
@ -266,14 +266,14 @@
"Currency": "Währung",
"Percent": "Prozent",
"Duration": "Dauer",
"GeoData": "GeoDaten",
"GeoData": "Geodaten",
"Rating": "Bewertung",
"Formula": "Formel",
"Rollup": "Zusammenfassung",
"Count": "Zählen",
"Lookup": "Nachschlagen",
"DateTime": "Datum/Zeit",
"CreateTime": "Zeit erstellen",
"CreatedTime": "Zeit erstellen",
"LastModifiedTime": "Zuletzt bearbeitet",
"AutoNumber": "Auto-Nummerierung",
"Barcode": "Barcode",
@ -298,24 +298,24 @@
"isNotNull": "ist nicht Null"
},
"title": {
"docs": "Docs",
"docs": "Dokumentation",
"forum": "Forum",
"parameter": "Parameter",
"headers": "Headers",
"parameterName": "Parameter Name",
"currencyLocale": "Currency Locale",
"currencyCode": "Currency Code",
"searchMembers": "Search Members",
"currencyCode": "Währungscode",
"searchMembers": "Nach Nutzern suchen",
"noMembersFound": "No members found",
"dateJoined": "Date Joined",
"tokenName": "Token name",
"tokenName": "Name des Tokens",
"inDesktop": "in Desktop",
"rowData": "Record data",
"creator": "Creator",
"qrCode": "QR Code",
"qrCode": "QR-Code",
"termsOfService": "Terms of Service",
"updateSelectedRows": "Update Selected Records",
"noFiltersAdded": "No filters added",
"noFiltersAdded": "Keine Filter verwendet",
"editCards": "Edit Cards",
"noFieldsFound": "No fields found",
"displayValue": "Display Value",
@ -332,19 +332,19 @@
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
"renamingWs": "Renaming Workspace",
"renameWs": "Rename Workspace",
"deleteWs": "Delete Workspace",
"renameWs": "Arbeitsbereich umbenennen",
"deleteWs": "Arbeitsbereich löschen",
"deletingWs": "Deleting Workspace",
"copyAuthToken": "Copy Auth Token",
"copyAuthToken": "Auth-Token kopieren",
"copiedAuthToken": "Copied Auth Token",
"copyInviteToken": "Copy Invite Token",
"showSidebar": "Show Sidebar",
"hideSidebar": "Hide Sidebar",
"creatingTable": "Creating Table",
"erdView": "ERD Ansicht",
"newBase": "New Data Source",
"newBase": "Neue Datenquelle hinzufügen",
"newProj": "Neues Projekt",
"createBase": "Create Base",
"createBase": "Projekt erstellen",
"myProject": "Meine Projekte",
"formTitle": "Formular Titel",
"collaborative": "Collaborative",
@ -353,7 +353,7 @@
"appStore": "App-Store",
"teamAndAuth": "Team & Authentifizierung",
"rolesUserMgmt": "Rollen- & Benutzermanagement",
"userMgmt": "Benutzermanagement",
"userMgmt": "Benutzerverwaltung",
"apiTokens": "API Tokens",
"apiTokenMgmt": "API-Token-Management",
"rolesMgmt": "Rollenmanagement",
@ -391,17 +391,17 @@
"codeSnippet": "Code Ausschnitt",
"keyboardShortcut": "Tastenkürzel",
"generateRandomName": "Zufälligen Namen generieren",
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"findRowByScanningCode": "Zeile durch Scannen eines QR-Codes oder Barcodes finden",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"addNewToken": "Neuen Token hinzufügen",
"accountSettings": "Kontoeinstellungen",
"resetPasswordMenu": "Passwort zurücksetzen",
"tokens": "Tokens",
"userManagement": "User Management",
"userManagement": "Benutzerverwaltung",
"accountManagement": "Account management",
"licence": "Licence",
"allowAllMimeTypes": "Allow All Mime Types",
"defaultView": "Default View",
"defaultView": "Standardansicht",
"relations": "Relations",
"switchLanguage": "Switch Language",
"renameFile": "Rename File",
@ -427,7 +427,7 @@
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"tokenLimit": "Nur ein Token pro Benutzer ist erlaubt",
"duplicateAttachment": "File with name {filename} already attached",
"viewIdColon": "VIEW ID: {viewId}",
"toAddress": "To Address",
@ -444,17 +444,17 @@
"duplicateRecord": "Duplicate record",
"binaryEncodingFormat": "Binary encoding format",
"syntax": "Syntax",
"examples": "Examples",
"durationInfo": "A duration of time in minutes or seconds (e.g. 1:23).",
"examples": "Beispiele",
"durationInfo": "Dauer der Zeit in Minuten oder Sekunden (z.B. 1:23).",
"addHeader": "Add Header",
"enterDefaultUrlOptional": "Enter default URL (Optional)",
"enterDefaultUrlOptional": "Standard-URL eingeben (optional)",
"negative": "Negative",
"discard": "Discard",
"default": "Default",
"defaultNumberPercent": "Default Number (%)",
"durationFormat": "Duration Format",
"dateFormat": "Date Format",
"timeFormat": "Time Format",
"durationFormat": "Format der Zeitspanne",
"dateFormat": "Datumsformat",
"timeFormat": "Zeitformat",
"singularLabel": "Singular Label",
"pluralLabel": "Plural Label",
"optional": "(Optional)",
@ -467,29 +467,29 @@
"forRole": "for role",
"clickToCopyViewID": "Click to copy View ID",
"viewMode": "View Mode",
"searchUsers": "Search Users",
"searchUsers": "Benutzer durchsuchen",
"superAdmin": "Super Admin",
"allTables": "All Tables",
"members": "Members",
"dataSources": "Data Sources",
"connectDataSource": "Connect a Data Source",
"allTables": "Alle Tabellen",
"members": "Mitglieder",
"dataSources": "Datenquellen",
"connectDataSource": "Eine Datenquelle hinzufügen",
"searchProjects": "Search Bases",
"createdBy": "Erstellt von",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"createdOn": "Erstellt am",
"notifyVia": "Benachrichtigen mit",
"projName": "Projektname",
"profile": "Profile",
"accountDetails": "Account Details",
"controlAppearance": "Control your Appearance.",
"profile": "Profil",
"accountDetails": "Kontoinformationen",
"controlAppearance": "Bearbeite dein Erscheinungsbild.",
"accountEmailID": "Account Email ID",
"backToWorkspace": "Back to Workspace",
"backToWorkspace": "Zurück zum Arbeitsbereich",
"untitledToken": "Untitled token",
"tableName": "Tabellenname",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Neue Ansicht erstellen",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -529,7 +529,7 @@
"where": "Wo",
"cache": "Zwischenspeicher",
"chat": "Chat",
"showOrHide": "Show or Hide",
"showOrHide": "Anzeigen / Verbergen",
"airtable": "Airtable",
"csv": "CSV",
"csvFile": "CSV File",
@ -544,10 +544,10 @@
"syncState": "Sync-Status",
"created": "Erstellt",
"sqlOutput": "SQL-Ausgabe",
"addOption": "Option hinzufügen",
"addOption": "Auswahloption hinzufügen",
"interfaceColor": "Interface Color",
"qrCodeValueColumn": "Spalte mit QR-Code",
"barcodeValueColumn": "Spalte mit Barcode-Wert",
"qrCodeValueColumn": "Spalte mit Werten für QR-Codes",
"barcodeValueColumn": "Spalte mit Werten für Barcode",
"barcodeFormat": "Barcode-Format",
"qrCodeValueTooLong": "Zu viele Zeichen für einen QR-Code",
"barcodeValueTooLong": "Zu viele Zeichen für einen Barcode",
@ -584,7 +584,7 @@
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link zu einem anderen Datensatz",
"links": "Links",
"links": "Verknüpfung",
"onUpdate": "Update",
"onDelete": "Löschen",
"account": "Benutzerkonto",
@ -602,7 +602,7 @@
"importLookupColumns": "Suchspalten importieren",
"importAttachmentColumns": "Spalten für Anhänge importieren",
"importFormulaColumns": "Formelspalten importieren",
"importUsers": "Import Users (by email)",
"importUsers": "Benutzer importieren (per E-Mail)",
"noData": "Keine Daten",
"goToDashboard": "Zum Dashboard gehen",
"importing": "Wird importiert",
@ -629,7 +629,7 @@
"prevRow": "Vorherige Reihe",
"addRowGrid": "Manually add data in grid view",
"addRowForm": "Enter record data through a form",
"noAccess": "No access",
"noAccess": "Kein Zugriff",
"restApis": "Rest APIs",
"apis": "APIs",
"includeData": "Include Data",
@ -639,7 +639,7 @@
"embedInSite": "Embed this view in your site",
"titleRequired": "title is required.",
"sourceNameRequired": "Source name is required",
"changeWsName": "Change Workspace Name",
"changeWsName": "Name des Arbeitsbereichs ändern",
"pressEnter": "Press Enter",
"newFormLoaded": "New form will be loaded after",
"webhook": "Webhook"
@ -659,11 +659,11 @@
"submitAnotherForm": "Submit Another Form",
"dragAndDropFieldsHereToAdd": "Drag and drop fields here to add",
"editSource": "Edit Data Source",
"enterText": "Enter text",
"enterText": "Text eingeben",
"okEditBase": "Ok & Edit Base",
"showInUI": "Show in UI",
"outOfSync": "Out of sync",
"newSource": "New Data Source",
"newSource": "Neue Datenquelle hinzufügen",
"newWebhook": "New Webhook",
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
@ -718,8 +718,8 @@
"filter": "Filter",
"addFilter": "Filter hinzufügen",
"share": "Teilen",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"groupBy": "Gruppieren nach",
"addSubGroup": "Untergruppe hinzufügen",
"shareBase": {
"label": "Share base",
"disable": "Freigegebene Datenbank deaktivieren",
@ -748,7 +748,7 @@
"copyApiURL": "API-URL kopieren",
"createTable": "Create New Table",
"createDashboard": "Create Dashboard",
"createWorkspace": "Create Workspace",
"createWorkspace": "Arbeitsbereich erstellen",
"refreshTable": "Tabellen aktualisieren",
"renameTable": "Tabelle umbenennen",
"renameLayout": "Layout Rename",
@ -831,9 +831,9 @@
"markAllAsRead": "Mark all as read",
"column": {
"delete": "Delete Field",
"addNumber": "Add Number Field",
"addSingleLineText": "Add SingleLineText Field",
"addLongText": "Add LongText Field",
"addNumber": "Nummernfeld hinzufügen",
"addSingleLineText": "Einzeiliges Textfeld hinzufügen",
"addLongText": "Langes Textfeld hinzufügen",
"addOther": "Add Other Field"
},
"erd": {
@ -897,12 +897,12 @@
"webhookTitle": "Webhook Title",
"barcodeColumn": "Select a field for the Barcode value",
"notFoundContent": "No valid field Type can be found.",
"selectBarcodeFormat": "Select a Barcode format",
"selectBarcodeFormat": "Barcode-Format auswählen",
"projName": "Projektnamen eingeben",
"selectGroupField": "Select a Grouping Field",
"selectGroupFieldNotFound": "No Single Select Field can be found. Please create one first.",
"selectGeoField": "Select a GeoData Field",
"selectGeoFieldNotFound": "No GeoData Field can be found. Please create one first.",
"selectGeoField": "Geodaten Feld auswählen",
"selectGeoFieldNotFound": "Es konnte kein Geodaten Feld gefunden werden. Bitte erstellen Sie zuerst eins.",
"password": {
"enter": "Passwort eingeben",
"current": "Aktuelles Passwort",
@ -911,7 +911,7 @@
"confirm": "Neues Passwort bestätigen"
},
"selectAColumnForTheQRCodeValue": "Select a field for the QR code value",
"allowNegativeNumbers": "Allow negative numbers",
"allowNegativeNumbers": "Negative nummern erlauben",
"searchProjectTree": "Tabellen suchen",
"searchFields": "Felder suchen",
"searchColumn": "Spalten suchen {search}",
@ -922,7 +922,7 @@
"filterByEmail": "Filtern nach E-Mail",
"filterQuery": "Filter-Abfrage",
"selectField": "Feld wählen",
"precision": "Precision",
"precision": "Dezimalstellen",
"decimal1": "1.0",
"decimal2": "1.00",
"decimal3": "1.000",
@ -931,12 +931,12 @@
"decimal6": "1.000000",
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"value": "Wert",
"key": "Key"
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"enterPassword": "Passwort eingeben",
"bySigningUp": "By signing up, you agree to the",
"subscribeToOurWeeklyNewsletter": "Subscribe to our weekly newsletter",
"verifyingPassword": "Verifying Password",
@ -947,10 +947,10 @@
"optimizedQueryDisabled": "Optimized query is disabled",
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"invalidTime": "Ungültige Uhrzeit",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"invalidPhoneNumber": "Ungültige Telefonnummer",
"pageSizeChanged": "Page size changed",
"errorLoadingData": "Error loading data",
"webhookBodyMsg1": "Use context variable",
@ -984,18 +984,18 @@
"columnNotMatchedWithType": "{columnName} is not matched with {columnType}"
},
"selectOption": {
"cantBeNull": "Select options can't be null",
"multiSelectCantHaveCommas": "MultiSelect fields can't have commas(',')",
"cantHaveDuplicates": "Select options can't have duplicates",
"cantBeNull": "Auswahloptionen dürfen nicht leer sein",
"multiSelectCantHaveCommas": "Auswahloptionen dürfen keine Kommas (\",\") enthalten",
"cantHaveDuplicates": "Auswahloptionen dürfen keine Duplikate enthalten",
"createNewOptionNamed": "Create new option named"
},
"plsEnterANumber": "Please enter a number",
"plsInputEmail": "Please input email",
"invalidDate": "Invalid date",
"plsEnterANumber": "Bitte geben Sie eine Zahl ein",
"plsInputEmail": "Bitte E-Mail-Adresse eingeben",
"invalidDate": "Ungültiges Datum",
"invalidLocale": "Invalid locale",
"invalidCurrencyCode": "Invalid Currency Code",
"invalidCurrencyCode": "Ungültiger Währungscode",
"postgresHasItsOwnCurrencySettings": "PostgreSQL 'money' type has own currency settings",
"validColumnsForBarCode": "The valid Field Types for a Barcode Field are: Number, Single Line Text, Long Text, Phone Number, URL, Email, Decimal. Please create one first.",
"validColumnsForBarCode": "Die gültigen Feldtypen für ein Barcodefeld sind: Zahl, einzeiliger Text, langer Text, Telefonnummer, URL, E-Mail und Dezimalzahl. Bitte erstellen Sie zuerst ein gültiges Feld.",
"hm": {
"title": "Has Many Relation",
"tooltip_desc": "A single record from table ",
@ -1020,9 +1020,9 @@
"noRecordsLinked": "No records linked",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",
"apiTokenCreate": "Personalisierte API Tokens zur Verwendung in Automatisierungen oder externen Apps erstellen.",
"selectFieldToSort": "Feld zum Sortieren auswählen",
"selectFieldToGroup": "Feld zum Gruppieren auswählen",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
@ -1045,7 +1045,7 @@
"nonEditableFields": {
"computedFieldUnableToClear": "Warnung: Berechnetes Feld - Text kann nicht gelöscht werden",
"qrFieldsCannotBeDirectlyChanged": "Warnung: QR-Felder können nicht direkt geändert werden.",
"barcodeFieldsCannotBeDirectlyChanged": "Warning: Barcode fields cannot be directly changed."
"barcodeFieldsCannotBeDirectlyChanged": "Achtung: Barcode Felder können nicht direkt geändert werden."
},
"duplicateProject": "Are you sure you want to duplicate the base?",
"duplicateTable": "Are you sure you want to duplicate the table?"
@ -1060,7 +1060,7 @@
},
"codeScanner": {
"loadingScanner": "Lade Scanner...",
"selectColumn": "Select a column (QR code or Barcode) that you want to use for finding a row by scanning.",
"selectColumn": "Eine Spalte (QR-Code oder Barcode) auswählen, nach der beim Scannen des Codes gesucht werden soll.",
"moreThanOneRowFoundForCode": "More than one row found for this code. Currently only unique codes are supported.",
"noRowFoundForCode": "No row found for this code for the selected column"
},
@ -1197,14 +1197,14 @@
"computedFieldEditWarning": "Berechnetes Feld: Der Inhalt ist schreibgeschützt. Verwenden Sie das Menü \"Spalten bearbeiten\", um das Feld neu zu konfigurieren.",
"computedFieldDeleteWarning": "Berechnetes Feld: Inhalt ist schreibgeschützt. Inhalt kann nicht gelöscht werden.",
"noMoreRecords": "Keine weiteren Aufzeichnungen",
"tokenNameNotEmpty": "Token name should not be empty",
"tokenNameMaxLength": "Token name should not be more than 255 characters",
"tokenNameNotEmpty": "Token Name darf nicht leer sein",
"tokenNameMaxLength": "Token Name darf nicht mehr als 255 Zeichen lang sein",
"dbNameRequired": "Database name is required",
"wsNameRequired": "Workspace name required",
"wsNameMinLength": "Workspace name must be at least 3 characters long",
"wsNameMaxLength": "Workspace name must be at most 50 characters long",
"wsDeleteDlg": "Delete this workspace and all it’s contents.",
"userConfirmation": "I understand that this action is irreversible",
"wsNameRequired": "Name des Arbeitsbereiches darf nicht Leer sein",
"wsNameMinLength": "Name des Arbeitsbereiches muss mindestens 3 Zeichen lang sein",
"wsNameMaxLength": "Name des Arbeitsbereiches darf nicht länger als 50 Zeichen sein",
"wsDeleteDlg": "Diesen Arbeitsbereich und all seine Inhalte löschen.",
"userConfirmation": "Ich verstehe, dass diese Aktion nicht rückgängig gemacht werden kann",
"pageNotFound": "Page Not Found",
"makeLineBreak": "to make a line break",
"goToPrevious": "Go to previous",
@ -1224,13 +1224,13 @@
"invalidChar": "Ungültiges Zeichen im Ordnerpfad.",
"invalidDbCredentials": "Ungültige Datenbankanmeldeinformationen.",
"unableToConnectToDb": "Es kann keine Verbindung zur Datenbank hergestellt werden, bitte überprüfen Sie Ihre Datenbank.",
"invalidYear": "Invalid year",
"invalidYear": "Ungültiges Jahr",
"userDoesntHaveSufficientPermission": "Den Benutzer gibt es nicht oder er hat keine ausreichenden Rechte, um das Schema zu erstellen.",
"dbConnectionStatus": "Ungültige Datenbankparameter",
"dbConnectionFailed": "Verbindungsfehler:",
"nullFilterExists": "Null filter exists. Please remove them",
"signUpRules": {
"emailRequired": "Email is required",
"emailRequired": "E-Mail-Adresse ist erforderlich",
"emailInvalid": "Email muß gültig sein",
"passwdRequired": "Passwort ist erforderlich",
"passwdLength": "Ihr Passwort muß mindestens 8 Zeichen haben",
@ -1241,8 +1241,8 @@
"atLeastOneNumber": "Eine Nummer",
"atLeastOneSpecialChar": "Ein Sonderzeichen",
"allowedSpecialCharList": "Erlaubte Sonderzeichenliste",
"invalidEmails": "Invalid emails",
"invalidEmail": "Invalid Email"
"invalidEmails": "Ungültige E-Mail-Adressen",
"invalidEmail": "Ungültige E-Mail-Adresse"
},
"invalidURL": "Ungültige URL",
"invalidEmail": "Ungültige E-Mail",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Folgende Zeichen sind nicht erlaubt",
"columnNameRequired": "Spaltenname ist erforderlich",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Projektname überschreitet 50 Zeichen",
@ -1322,7 +1323,7 @@
"tableDataExported": "Erfolgreich alle Tabellendaten exportiert",
"updated": "Erfolgreich aktualisiert",
"sharedViewDeleted": "Gemeinsame Ansicht erfolgreich gelöscht",
"userDeleted": "Benutzer erfolgreich gelöscht",
"userDeleted": "Benutzer wurde erfolgreich gelöscht",
"viewRenamed": "Ansicht erfolgreich umbenannt",
"tokenGenerated": "Token erfolgreich generiert",
"tokenDeleted": "Token erfolgreich gelöscht",
@ -1347,4 +1348,4 @@
"roleUpdated": "Rolle erfolgreich aktualisiert"
}
}
}
}

6
packages/nc-gui/lang/en.json

@ -273,7 +273,7 @@
"Count": "Count",
"Lookup": "Lookup",
"DateTime": "Date Time",
"CreateTime": "Create Time",
"CreatedTime": "Create Time",
"LastModifiedTime": "Last Modified Time",
"AutoNumber": "Auto Number",
"Barcode": "Barcode",
@ -1212,7 +1212,6 @@
"thankYou": "Thank you!",
"submittedFormData": "You have successfully submitted the form data.",
"editingSystemKeyNotSupported": "Editing system key not supported"
},
"error": {
"nameRequired": "Name Required",
@ -1275,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Field name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of field name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Base name exceeds 50 characters",
@ -1348,4 +1348,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

5
packages/nc-gui/lang/es.json

@ -273,7 +273,7 @@
"Count": "Cuenta",
"Lookup": "Búsqueda",
"DateTime": "Fecha y hora",
"CreateTime": "Fecha de creación",
"CreatedTime": "Fecha de creación",
"LastModifiedTime": "Fecha de modificación",
"AutoNumber": "Número automático",
"Barcode": "Código de barras",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Los siguientes caracteres no están permitidos",
"columnNameRequired": "El nombre de la columna es obligatorio",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "El nombre del proyecto supera los 50 caracteres",
@ -1347,4 +1348,4 @@
"roleUpdated": "Función actualizada correctamente"
}
}
}
}

5
packages/nc-gui/lang/eu.json

@ -273,7 +273,7 @@
"Count": "Count",
"Lookup": "Lookup",
"DateTime": "Date Time",
"CreateTime": "Create Time",
"CreatedTime": "Create Time",
"LastModifiedTime": "Azkeneko aldatze ordua",
"AutoNumber": "Auto Number",
"Barcode": "Barcode",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

5
packages/nc-gui/lang/fa.json

@ -273,7 +273,7 @@
"Count": "شمردن",
"Lookup": "جستوجو",
"DateTime": "تاریخ و زمان",
"CreateTime": "ایجاد زمان",
"CreatedTime": "ایجاد زمان",
"LastModifiedTime": "زمان آخرین اصلاح",
"AutoNumber": "شماره خودکار",
"Barcode": "بارکد",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "این کاراکتر ها مجاز نیستند",
"columnNameRequired": "Column name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "نقش با موفقیت بهروزرسانی شد"
}
}
}
}

5
packages/nc-gui/lang/fi.json

@ -273,7 +273,7 @@
"Count": "Kreivi",
"Lookup": "Katso ylös",
"DateTime": "Treffiaika",
"CreateTime": "Luoda aikaa",
"CreatedTime": "Luoda aikaa",
"LastModifiedTime": "Viimeksi muutettu aika",
"AutoNumber": "Automaattinen numero",
"Barcode": "Viivakoodi",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Seuraavat merkit eivät ole sallittuja",
"columnNameRequired": "Sarakkeen nimi vaaditaan",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Hankkeen nimi ylittää 50 merkkiä",
@ -1347,4 +1348,4 @@
"roleUpdated": "Rooli päivitetty onnistuneesti"
}
}
}
}

5
packages/nc-gui/lang/fr.json

@ -273,7 +273,7 @@
"Count": "Compteur",
"Lookup": "Consulter",
"DateTime": "Date et heure",
"CreateTime": "Date de création",
"CreatedTime": "Date de création",
"LastModifiedTime": "Dernière modification",
"AutoNumber": "Numérotation automatique",
"Barcode": "Code-barres",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Les caractères suivants ne sont pas autorisés",
"columnNameRequired": "Nom de la colonne requis",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Le nom du projet dépasse les 50 caractères",
@ -1347,4 +1348,4 @@
"roleUpdated": "Rôle mis à jour avec succès"
}
}
}
}

5
packages/nc-gui/lang/he.json

@ -273,7 +273,7 @@
"Count": "ספירה",
"Lookup": "הבט מעלה",
"DateTime": "תאריך שעה",
"CreateTime": "צור זמן",
"CreatedTime": "צור זמן",
"LastModifiedTime": "שונה לאחרונה",
"AutoNumber": "מספר אוטומטי",
"Barcode": "ברקוד",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "תפקיד שונה בהצלחה"
}
}
}
}

5
packages/nc-gui/lang/hi.json

@ -273,7 +273,7 @@
"Count": "गिनत करन",
"Lookup": "द",
"DateTime": "दिक और समय",
"CreateTime": "निण क समय",
"CreatedTime": "निण क समय",
"LastModifiedTime": "अिम सित समय",
"AutoNumber": "वहन नबर",
"Barcode": "बरकड",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

5
packages/nc-gui/lang/hr.json

@ -273,7 +273,7 @@
"Count": "Računati",
"Lookup": "Pogledaj",
"DateTime": "Datum vrijeme",
"CreateTime": "Stvoriti vrijeme",
"CreatedTime": "Stvoriti vrijeme",
"LastModifiedTime": "Posljednje izmijenjeno vrijeme",
"AutoNumber": "Automatski broj",
"Barcode": "Barkod",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Nije dopušteno koristiti sljedeće znakove",
"columnNameRequired": "Column name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "Uloga je uspješno aktualizirana"
}
}
}
}

5
packages/nc-gui/lang/id.json

@ -273,7 +273,7 @@
"Count": "Menghitung",
"Lookup": "Mencari",
"DateTime": "Tanggal Waktu",
"CreateTime": "Buat waktu",
"CreatedTime": "Buat waktu",
"LastModifiedTime": "Waktu yang dimodifikasi terakhir",
"AutoNumber": "Nomor otomatis",
"Barcode": "Barcode.",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Karakter berikut tidak diperbolehkan",
"columnNameRequired": "Nama kolom harus diisi",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Nama proyek melebihi 50 karakter",
@ -1347,4 +1348,4 @@
"roleUpdated": "Peran berhasil diperbarui"
}
}
}
}

5
packages/nc-gui/lang/it.json

@ -273,7 +273,7 @@
"Count": "Contatore",
"Lookup": "Consultazione",
"DateTime": "Data e ora",
"CreateTime": "Data di creazione",
"CreatedTime": "Data di creazione",
"LastModifiedTime": "Data di ultima modifica",
"AutoNumber": "Numerazione automatica",
"Barcode": "Codice a barre",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "I seguenti caratteri non sono ammessi",
"columnNameRequired": "Il nome della colonna è richiesto",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Il nome del progetto supera i 50 caratteri",
@ -1347,4 +1348,4 @@
"roleUpdated": "Ruolo aggiornato con successo"
}
}
}
}

5
packages/nc-gui/lang/ja.json

@ -273,7 +273,7 @@
"Count": "カウント",
"Lookup": "ルックアップ",
"DateTime": "DateTime型",
"CreateTime": "作成時刻",
"CreatedTime": "作成時刻",
"LastModifiedTime": "最終更新日時",
"AutoNumber": "自動採番",
"Barcode": "バーコード",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "以下の文字種は使用できません",
"columnNameRequired": "列名が必要です",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "プロジェクト名が50文字を超えています",
@ -1347,4 +1348,4 @@
"roleUpdated": "ロールを更新しました"
}
}
}
}

5
packages/nc-gui/lang/ko.json

@ -273,7 +273,7 @@
"Count": "카운트",
"Lookup": "조회",
"DateTime": "일시",
"CreateTime": "생성시간",
"CreatedTime": "생성시간",
"LastModifiedTime": "최종수정시간",
"AutoNumber": "자동번호",
"Barcode": "바코드",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "다음 문자는 허용되지 않습니다.",
"columnNameRequired": "컬럼 이름이 필요합니다.",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "열 이름의 길이가 최대 {value}자를 초과합니다.",
"projectNameExceeds50Characters": "프로젝트 이름의 길이가 최대 50자를 초과합니다.",
@ -1347,4 +1348,4 @@
"roleUpdated": "역할이 성공적으로 업데이트되었습니다."
}
}
}
}

5
packages/nc-gui/lang/lv.json

@ -273,7 +273,7 @@
"Count": "Skaits",
"Lookup": "Uzmeklēšana",
"DateTime": "Datums un laiks",
"CreateTime": "Izveidošanas laiks",
"CreatedTime": "Izveidošanas laiks",
"LastModifiedTime": "Modificēšanas laiks",
"AutoNumber": "Automātiska numerācija",
"Barcode": "Svītru kods",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Šādas rakstzīmes nav atļautas",
"columnNameRequired": "Slejas nosaukums ir obligāts",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Projekta nosaukums pārsniedz 50 rakstzīmes",
@ -1347,4 +1348,4 @@
"roleUpdated": "Loma veiksmīgi atjaunināta"
}
}
}
}

5
packages/nc-gui/lang/nl.json

@ -273,7 +273,7 @@
"Count": "Telling",
"Lookup": "Zoekopdracht",
"DateTime": "Tijdstip",
"CreateTime": "Maak Tijd",
"CreatedTime": "Maak Tijd",
"LastModifiedTime": "Laatst gewijzigd",
"AutoNumber": "Automatische nummering",
"Barcode": "Barcode",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "De volgende tekens zijn niet toegestaan",
"columnNameRequired": "Kolomnaam is verplicht",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Projectnaam meer dan 50 tekens",
@ -1347,4 +1348,4 @@
"roleUpdated": "Rol succesvol bijgewerkt"
}
}
}
}

5
packages/nc-gui/lang/no.json

@ -273,7 +273,7 @@
"Count": "Telle",
"Lookup": "Se opp",
"DateTime": "Dato tid",
"CreateTime": "Skape tid",
"CreatedTime": "Skape tid",
"LastModifiedTime": "Sist endret tid",
"AutoNumber": "Auto nummer",
"Barcode": "Strekkode",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

5
packages/nc-gui/lang/pl.json

@ -273,7 +273,7 @@
"Count": "Licznik",
"Lookup": "Wyszukiwanie",
"DateTime": "Data i czas",
"CreateTime": "Data utworzenia",
"CreatedTime": "Data utworzenia",
"LastModifiedTime": "Data ostatniej modyfikacji",
"AutoNumber": "Automatyczny numer",
"Barcode": "Kod kreskowy",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Następujące znaki są niedozwolone",
"columnNameRequired": "Nazwa kolumny jest wymagana",
"duplicateColumnName": "Zduplikowana nazwa kolumny",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "Typ danych UI jest wymagany",
"columnNameExceedsCharacters": "Nazwa kolumny przekracza maksymalną długość {value} znaków",
"projectNameExceeds50Characters": "Nazwa projektu przekracza 50 znaków",
@ -1347,4 +1348,4 @@
"roleUpdated": "Rola została pomyślnie zaktualizowana"
}
}
}
}

5
packages/nc-gui/lang/pt.json

@ -273,7 +273,7 @@
"Count": "Contar",
"Lookup": "Olho para cima",
"DateTime": "Data hora",
"CreateTime": "Criar tempo",
"CreatedTime": "Criar tempo",
"LastModifiedTime": "Última hora modificada",
"AutoNumber": "Número automático",
"Barcode": "Código de barras",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Os seguintes caracteres não são permitidos",
"columnNameRequired": "O nome da coluna é obrigatório",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "O nome do projecto excede 50 caracteres",
@ -1347,4 +1348,4 @@
"roleUpdated": "Papel actualizado com sucesso"
}
}
}
}

5
packages/nc-gui/lang/pt_BR.json

@ -273,7 +273,7 @@
"Count": "Quantidade",
"Lookup": "Procurar",
"DateTime": "Data e hora",
"CreateTime": "Hora da criação",
"CreatedTime": "Hora da criação",
"LastModifiedTime": "Hora da última alteração",
"AutoNumber": "Número automático",
"Barcode": "Código de barras",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Os seguintes caracteres não são permitidos",
"columnNameRequired": "O nome da coluna é obrigatório",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "O nome do projecto excede 50 caracteres",
@ -1347,4 +1348,4 @@
"roleUpdated": "Papel actualizado com sucesso"
}
}
}
}

167
packages/nc-gui/lang/ru.json

@ -78,36 +78,36 @@
"quote": "Quote",
"submit": "Отправить",
"create": "Создать",
"createEntity": "Create {entity}",
"creating": "Creating",
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"createEntity": "Создать {entity}",
"creating": "Создание",
"creatingEntity": "Создание {entity}",
"details": "Подробности",
"skip": "Пропустить",
"code": "Код",
"duplicate": "Копировать",
"duplicating": "Duplicating",
"activate": "Activate",
"action": "Action",
"duplicating": "Дублирование",
"activate": "Активация",
"action": "Действие",
"insert": "Вставить",
"delete": "Удалить",
"deleteEntity": "Delete {entity}",
"bulkInsert": "Bulk Insert",
"bulkDelete": "Bulk Delete",
"bulkUpdate": "Bulk Update",
"deleting": "Deleting",
"deleteEntity": "Удалить {entity}",
"bulkInsert": "Массовая вставка",
"bulkDelete": "Массовое удаление",
"bulkUpdate": "Массовое обновление",
"deleting": "Удаление",
"update": "Обновить",
"rename": "Переименовать",
"reload": "Перезагрузить",
"reset": "Сброс настроек",
"install": "Установить",
"show": "Показать",
"access": "Access",
"visibility": "Visibility",
"access": "Доступ",
"visibility": "Видимость",
"hide": "Скрыть",
"deprecated": "Deprecated",
"deprecated": "Устарело",
"showAll": "Показать все",
"hideAll": "Скрыть все",
"notFound": "Not found",
"notFound": "Не найдено",
"showMore": "Подробнее",
"showOptions": "Открыть настройки",
"hideOptions": "Скрыть настройки",
@ -127,8 +127,8 @@
"upload": "Загрузить",
"download": "Скачать",
"default": "По умолчанию",
"base": "Source",
"datasource": "Data Source",
"base": "Источник",
"datasource": "Источник данных",
"more": "Больше",
"less": "Меньше",
"event": "Событие",
@ -136,7 +136,7 @@
"after": "После",
"before": "До",
"search": "Найти",
"searchIn": "Search In",
"searchIn": "Искать в",
"notification": "Уведомление",
"reference": "Ссылка",
"function": "Функция",
@ -160,29 +160,29 @@
"hideField": "Скрыть поле",
"sortAsc": "По Возрастанию",
"sortDesc": "По убыванию",
"move": "Move",
"move": "Переместить",
"geoDataField": "Поле геоданных",
"type": "Type",
"name": "Name",
"changes": "Changes",
"new": "New",
"old": "Old",
"data": "Data",
"source": "Source",
"destination": "Destination",
"active": "Active",
"inactive": "Inactive",
"linked": "linked",
"finish": "Finish",
"min": "Min",
"max": "Max",
"avg": "Avg",
"sum": "Sum",
"count": "Count",
"countDistinct": "Count Distinct",
"sumDistinct": "Sum Distinct",
"avgDistinct": "Avg Distinct",
"join": "Join",
"type": "Тип",
"name": "Имя",
"changes": "Изменения",
"new": "Создать",
"old": "Старый",
"data": "Данные",
"source": "Источник",
"destination": "Назначение",
"active": "Активно",
"inactive": "Неактивный",
"linked": "связанный",
"finish": "Готово",
"min": "Минимум",
"max": "Максимум",
"avg": "Среднее",
"sum": "Сумма",
"count": "Количество",
"countDistinct": "Количество уникальных",
"sumDistinct": "Сумма уникальных",
"avgDistinct": "Среднее уникальных",
"join": "Присоединиться",
"options": "Опции",
"primaryValue": "Первичное значение",
"useSurveyMode": "Использовать режим анкеты",
@ -273,7 +273,7 @@
"Count": "Количество",
"Lookup": "Подстановка (Lookup)",
"DateTime": "Дата и время",
"CreateTime": "Создан",
"CreatedTime": "Создан",
"LastModifiedTime": "Изменен",
"AutoNumber": "Счетчик",
"Barcode": "Штрих-код",
@ -329,22 +329,22 @@
"linkMore": "Ссылка Подробнее",
"linkMoreRecords": "Связать больше записей",
"downloadFile": "Скачать файл",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
"renamingWs": "Renaming Workspace",
"renameWs": "Rename Workspace",
"deleteWs": "Delete Workspace",
"deletingWs": "Deleting Workspace",
"copyAuthToken": "Copy Auth Token",
"copiedAuthToken": "Copied Auth Token",
"copyInviteToken": "Copy Invite Token",
"showSidebar": "Show Sidebar",
"hideSidebar": "Hide Sidebar",
"creatingTable": "Creating Table",
"renameTable": "Переименовать таблицу",
"renamingTable": "Переименование таблицы",
"renamingWs": "Переименование рабочей области",
"renameWs": "Переименовать рабочую область",
"deleteWs": "Удалить рабочую область",
"deletingWs": "Удаление рабочей области",
"copyAuthToken": "Скопировать токен авторизации",
"copiedAuthToken": "Токен аутентификации скопирован",
"copyInviteToken": "Скопировать токен приглашения",
"showSidebar": "Показать боковую панель",
"hideSidebar": "Скрыть боковую панель",
"creatingTable": "Создание таблицы",
"erdView": "Представление ERD",
"newBase": "New Data Source",
"newBase": "Новый источник данных",
"newProj": "Новый проект",
"createBase": "Create Base",
"createBase": "Создать базу",
"myProject": "Мои проекты",
"formTitle": "Заголовок формы",
"collaborative": "Collaborative",
@ -354,7 +354,7 @@
"teamAndAuth": "Пользватели и API",
"rolesUserMgmt": "Роли и управление пользователями",
"userMgmt": "Управление пользователями",
"apiTokens": "API Tokens",
"apiTokens": "API токены",
"apiTokenMgmt": "Управление токенами API",
"rolesMgmt": "Управление ролями",
"projMeta": "Метаданные проекта",
@ -381,44 +381,44 @@
"swaggerDocumentation": "Документация Swagger",
"quickImportFrom": "Быстрый импорт из",
"quickImport": "Быстрый импорт",
"quickImportAirtable": "Quick Import - Airtable",
"quickImportCSV": "Quick Import - CSV",
"quickImportExcel": "Quick Import - Excel",
"quickImportJSON": "Quick Import - JSON",
"jsonEditor": "JSON Editor",
"quickImportAirtable": "Быстрый импорт - Airtable",
"quickImportCSV": "Быстрый импорт - CSV",
"quickImportExcel": "Быстрый импорт - Excel",
"quickImportJSON": "Быстрый импорт - JSON",
"jsonEditor": "Редактор JSON",
"comingSoon": "Coming Soon",
"advancedSettings": "Расширенные настройки",
"codeSnippet": "Сниппет кода",
"keyboardShortcut": "Горячие клавиши",
"generateRandomName": "Сгенерировать случайное имя",
"findRowByScanningCode": "Найти строку путем сканирования QR или штрих-кода",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
"userManagement": "User Management",
"accountManagement": "Account management",
"licence": "Licence",
"allowAllMimeTypes": "Allow All Mime Types",
"tokenManagement": "Управление токенами",
"addNewToken": "Добавить новый токен",
"accountSettings": "Настройки аккаунта",
"resetPasswordMenu": "Сбросить пароль",
"tokens": "Токены",
"userManagement": "Управление пользователями",
"accountManagement": "Управление аккаунтом",
"licence": "Лицензия",
"allowAllMimeTypes": "Разрешить все типы Mime",
"defaultView": "Default View",
"relations": "Relations",
"switchLanguage": "Switch Language",
"renameFile": "Rename File",
"switchLanguage": "Переключить язык",
"renameFile": "Переименовать файл",
"links": {
"noAction": "No Action",
"cascade": "Cascade",
"restrict": "Restrict",
"setNull": "Set NULL",
"setDefault": "Set Default"
"setNull": "Установить в NULL",
"setDefault": "Установить по умолчанию"
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"heading1": "Заголовок 1",
"heading2": "Заголовок 2",
"heading3": "Заголовок 3",
"bold": "Жирный",
"italic": "Курсив",
"underline": "Underline",
"strike": "Strike",
"taskList": "Список задач",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Нельзя использовать следующие символы",
"columnNameRequired": "Требуется название столбца",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "Длина имени колонки превышает максимальную {value} символов",
"projectNameExceeds50Characters": "Название проекта превышает 50 символов",
@ -1347,4 +1348,4 @@
"roleUpdated": "Роль успешно обновлена"
}
}
}
}

5
packages/nc-gui/lang/sk.json

@ -273,7 +273,7 @@
"Count": "Počítajte",
"Lookup": "Vyhľadávanie",
"DateTime": "Dátum Čas",
"CreateTime": "Vytvoriť čas",
"CreatedTime": "Vytvoriť čas",
"LastModifiedTime": "Čas poslednej úpravy",
"AutoNumber": "Automatické číslo",
"Barcode": "Čiarový kód",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Nasledujúce znaky nie sú povolené",
"columnNameRequired": "Vyžaduje sa názov stĺpca",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Názov projektu presahuje 50 znakov",
@ -1347,4 +1348,4 @@
"roleUpdated": "Úloha bola úspešne aktualizovaná"
}
}
}
}

5
packages/nc-gui/lang/sl.json

@ -273,7 +273,7 @@
"Count": "Count.",
"Lookup": "Poglej gor",
"DateTime": "Datum čas",
"CreateTime": "Ustvarite čas",
"CreatedTime": "Ustvarite čas",
"LastModifiedTime": "Zadnji spremenjen čas",
"AutoNumber": "Samodejna številka",
"Barcode": "Črtna koda",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Naslednji znaki niso dovoljeni",
"columnNameRequired": "Ime stolpca je obvezno",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Ime projekta presega 50 znakov",
@ -1347,4 +1348,4 @@
"roleUpdated": "Vloga je bila uspešno posodobljena"
}
}
}
}

5
packages/nc-gui/lang/sv.json

@ -273,7 +273,7 @@
"Count": "Räkna",
"Lookup": "Slå upp",
"DateTime": "Datum Tid",
"CreateTime": "Skapa tid",
"CreatedTime": "Skapa tid",
"LastModifiedTime": "Senast ändrad tid",
"AutoNumber": "Automatisk nummer",
"Barcode": "Streckkod",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Följande tecken är inte tillåtna",
"columnNameRequired": "Kolumnnamn krävs",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Projektnamnet har mer än 50 tecken",
@ -1347,4 +1348,4 @@
"roleUpdated": "Rollen har uppdaterats framgångsrikt"
}
}
}
}

5
packages/nc-gui/lang/th.json

@ -273,7 +273,7 @@
"Count": "นบ",
"Lookup": "การคนหา",
"DateTime": "วนเวลา",
"CreateTime": "สรางเวลา",
"CreatedTime": "สรางเวลา",
"LastModifiedTime": "เวลาทแกไขลาสด",
"AutoNumber": "หมายเลขอตโนม",
"Barcode": "บารโคด",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

5
packages/nc-gui/lang/tr.json

@ -273,7 +273,7 @@
"Count": "Say",
"Lookup": "Referans",
"DateTime": "Tarih saat",
"CreateTime": "Oluşturma zamanı",
"CreatedTime": "Oluşturma zamanı",
"LastModifiedTime": "Son değiştirilme",
"AutoNumber": "Otomatik Sayı",
"Barcode": "Barkod",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Aşağıdaki karakterlere izin verilmez",
"columnNameRequired": "Sütun adı gereklidir",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "Sütun adının uzunluğu maksimum {value} karakter sınırını aşıyor",
"projectNameExceeds50Characters": "Proje adı 50 karakteri aşıyor",
@ -1347,4 +1348,4 @@
"roleUpdated": "Rol başarıyla güncellendi"
}
}
}
}

5
packages/nc-gui/lang/uk.json

@ -273,7 +273,7 @@
"Count": "Кількість",
"Lookup": "Пошук",
"DateTime": "Дата і час",
"CreateTime": "Час створення",
"CreatedTime": "Час створення",
"LastModifiedTime": "Час останньої зміни",
"AutoNumber": "Автоматичне прирощування",
"Barcode": "Штрих-код",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Наступні символи не допускаються",
"columnNameRequired": "Ім'я стовпця є обов'язковим",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "Довжина назви стовпця перевищує максимальну кількість в {value} символів",
"projectNameExceeds50Characters": "Назва проєкту перевищує 50 символів",
@ -1347,4 +1348,4 @@
"roleUpdated": "Роль успішно оновлено"
}
}
}
}

5
packages/nc-gui/lang/vi.json

@ -273,7 +273,7 @@
"Count": "Đếm",
"Lookup": "Tra cứu",
"DateTime": "Ngày giờ",
"CreateTime": "Tạo thời gian",
"CreatedTime": "Tạo thời gian",
"LastModifiedTime": "Thời gian sửa đổi lần cuối",
"AutoNumber": "Số tự động",
"Barcode": "Mã vạch.",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

19
packages/nc-gui/lang/zh-Hans.json

@ -56,7 +56,7 @@
"links": "链接",
"remove": "移除",
"import": "导入",
"logout": "退出登录",
"logout": "退出",
"empty": "空",
"changeIcon": "更换图标",
"save": "保存",
@ -189,7 +189,7 @@
"shift": "Shift 键",
"enter": "回车键",
"seconds": "秒",
"paste": "Paste"
"paste": "粘贴"
},
"objects": {
"workspace": "工作区",
@ -273,7 +273,7 @@
"Count": "计数",
"Lookup": "查找",
"DateTime": "日期时间",
"CreateTime": "创建时间",
"CreatedTime": "创建时间",
"LastModifiedTime": "最后修改时间",
"AutoNumber": "自动编号",
"Barcode": "条形码",
@ -587,7 +587,7 @@
"links": "链接",
"onUpdate": "更新时",
"onDelete": "删除时",
"account": "户",
"account": "户",
"language": "语言",
"primaryColor": "主色调",
"accentColor": "强调色",
@ -642,7 +642,7 @@
"changeWsName": "更改工作区名称",
"pressEnter": "按回车键",
"newFormLoaded": "新表格将在",
"webhook": "Webhook"
"webhook": "网络钩子"
},
"activity": {
"openInANewTab": "在新标签页中打开",
@ -1022,7 +1022,7 @@
"acceptOnlyValid": "仅接受",
"apiTokenCreate": "创建个人 API tokens,以便在自动化或外部应用程序中使用。",
"selectFieldToSort": "选择要排序的字段",
"selectFieldToGroup": "Select Field to Group",
"selectFieldToGroup": "选择要分组的字段",
"thereAreNoRecordsInTable": "表中没有记录",
"createWebhookMsg1": "开始使用Web Hooks!",
"createWebhookMsg2": "创建Web Hooks来实现自动化",
@ -1211,7 +1211,7 @@
"goToNext": "转到下一个",
"thankYou": "谢谢你!",
"submittedFormData": "您已成功提交表单数据。",
"editingSystemKeyNotSupported": "Editing system key not supported"
"editingSystemKeyNotSupported": "不支持编辑系统密钥"
},
"error": {
"nameRequired": "您必须指定一个名字",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "不允许使用以下字符",
"columnNameRequired": "列名是必填项",
"duplicateColumnName": "字段名称重复",
"duplicateSystemColumnName": "字段名称已被系统使用",
"uiDataTypeRequired": "需要 UI 数据类型",
"columnNameExceedsCharacters": "列名的长度超过了 {value} 字符的限制",
"projectNameExceeds50Characters": "项目名称超过 50 个字符",
@ -1288,7 +1289,7 @@
"fieldRequired": "{value} 不能为空。",
"projectNotAccessible": "无权访问此项目",
"copyToClipboardError": "未能复制到剪贴板",
"pasteFromClipboardError": "Failed to paste from clipboard"
"pasteFromClipboardError": "从剪贴板粘贴失败"
},
"toast": {
"exportMetadata": "项目元数据成功导出",
@ -1347,4 +1348,4 @@
"roleUpdated": "权限已更新"
}
}
}
}

5
packages/nc-gui/lang/zh-Hant.json

@ -273,7 +273,7 @@
"Count": "計數",
"Lookup": "查閱",
"DateTime": "日期時間",
"CreateTime": "創建時間",
"CreatedTime": "創建時間",
"LastModifiedTime": "最後修改時間",
"AutoNumber": "自動編號",
"Barcode": "條碼",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "以下字元不允許",
"columnNameRequired": "欄位名稱必填",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "專案名稱超過 50 個字元",
@ -1347,4 +1348,4 @@
"roleUpdated": "角色已成功更新"
}
}
}
}

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

@ -177,6 +177,11 @@ interface SidebarTableNode extends TableType {
isViewsLoading?: boolean
}
interface UsersSortType {
field?: 'email' | 'roles'
direction?: 'asc' | 'desc'
}
export type {
User,
ProjectMetaInfo,
@ -202,4 +207,5 @@ export type {
ViewPageType,
NcButtonSize,
SidebarTableNode,
UsersSortType,
}

20
packages/nc-gui/package.json

@ -73,14 +73,14 @@
"marked": "^4.3.0",
"monaco-editor": "^0.33.0",
"monaco-sql-languages": "^0.11.0",
"nocodb-sdk": "0.203.2",
"nocodb-sdk": "workspace:^",
"papaparse": "^5.4.1",
"parse-github-url": "^1.0.2",
"pinia": "^2.1.7",
"qrcode": "^1.5.3",
"rfdc": "^1.3.0",
"showdown": "^2.1.0",
"socket.io-client": "^4.7.2",
"socket.io-client": "^4.7.3",
"sortablejs": "^1.15.1",
"splitpanes": "^3.1.5",
"tinycolor2": "^1.4.2",
@ -105,7 +105,7 @@
"@antfu/eslint-config": "^0.26.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@iconify-json/ant-design": "^1.1.13",
"@iconify-json/bi": "^1.1.22",
"@iconify-json/bi": "^1.1.23",
"@iconify-json/carbon": "^1.1.27",
"@iconify-json/cil": "^1.1.8",
"@iconify-json/clarity": "^1.1.12",
@ -114,18 +114,18 @@
"@iconify-json/ion": "^1.1.15",
"@iconify-json/la": "^1.1.8",
"@iconify-json/logos": "^1.1.42",
"@iconify-json/lucide": "^1.1.149",
"@iconify-json/material-symbols": "^1.1.68",
"@iconify-json/mdi": "^1.1.63",
"@iconify-json/lucide": "^1.1.152",
"@iconify-json/material-symbols": "^1.1.69",
"@iconify-json/mdi": "^1.1.64",
"@iconify-json/mi": "^1.1.8",
"@iconify-json/ph": "^1.1.9",
"@iconify-json/ri": "^1.1.18",
"@iconify-json/simple-icons": "^1.1.86",
"@iconify-json/simple-icons": "^1.1.87",
"@iconify-json/system-uicons": "^1.1.12",
"@iconify-json/tabler": "^1.1.102",
"@iconify-json/tabler": "^1.1.103",
"@iconify-json/vscode-icons": "^1.1.32",
"@intlify/unplugin-vue-i18n": "^0.12.3",
"@nuxt/image-edge": "1.1.0-28393680.ddd021d",
"@nuxt/image-edge": "1.1.0-28408112.572f502",
"@types/d3-scale": "^4.0.8",
"@types/dagre": "^0.7.52",
"@types/file-saver": "^2.0.7",
@ -156,7 +156,7 @@
"nuxt": "^3.8.2",
"nuxt-windicss": "^2.6.1",
"prettier": "^2.8.8",
"sass": "^1.69.5",
"sass": "^1.69.7",
"ts-loader": "^9.4.4",
"unplugin-icons": "^0.14.15",
"unplugin-vue-components": "^0.22.12",

4
packages/nc-gui/utils/cell.ts

@ -15,6 +15,10 @@ export const isYear = (column: ColumnType, abstractType: any) => abstractType ==
export const isTime = (column: ColumnType, abstractType: any) => abstractType === 'time' || column.uidt === UITypes.Time
export const isDateTime = (column: ColumnType, abstractType: any) =>
abstractType === 'datetime' || column.uidt === UITypes.DateTime
export const isReadonlyDateTime = (column: ColumnType, _abstractType: any) =>
column.uidt === UITypes.CreatedTime || column.uidt === UITypes.LastModifiedTime
export const isReadonlyUser = (column: ColumnType, _abstractType: any) =>
column.uidt === UITypes.CreatedBy || column.uidt === UITypes.LastModifiedBy
export const isJSON = (column: ColumnType) => column.uidt === UITypes.JSON
export const isEnum = (column: ColumnType) => column.uidt === UITypes.SingleSelect
export const isSingleSelect = (column: ColumnType) => column.uidt === UITypes.SingleSelect

21
packages/nc-gui/utils/columnUtils.ts

@ -136,7 +136,23 @@ const uiTypes = [
},
{
name: UITypes.User,
icon: iconMap.account,
icon: iconMap.phUser,
},
{
name: UITypes.CreatedTime,
icon: iconMap.datetime,
},
{
name: UITypes.LastModifiedTime,
icon: iconMap.datetime,
},
{
name: UITypes.CreatedBy,
icon: iconMap.phUser,
},
{
name: UITypes.LastModifiedBy,
icon: iconMap.phUser,
},
]
@ -145,7 +161,7 @@ const getUIDTIcon = (uidt: UITypes | string) => {
[
...uiTypes,
{
name: UITypes.CreateTime,
name: UITypes.CreatedTime,
icon: iconMap.calendar,
},
{
@ -168,6 +184,7 @@ const isColumnRequired = (col?: ColumnType) => col && col.rqd && !col.cdf && !co
const isVirtualColRequired = (col: ColumnType, columns: ColumnType[]) =>
col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions &&
(<LinkToAnotherRecordType>col.colOptions).type === RelationTypes.BELONGS_TO &&
isColumnRequired(columns.find((c) => c.id === (<LinkToAnotherRecordType>col.colOptions).fk_child_column_id))

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save