Browse Source

Merge branch 'develop' into feat/gui-v2-rating-options

pull/2979/head
Wing-Kam Wong 2 years ago
parent
commit
cefec05509
  1. 8
      packages/nc-gui-v2/components.d.ts
  2. 338
      packages/nc-gui-v2/components/cell/Attachment.vue
  3. 34
      packages/nc-gui-v2/components/cell/Currency.vue
  4. 11
      packages/nc-gui-v2/components/cell/Email.vue
  5. 13
      packages/nc-gui-v2/components/cell/Float.vue
  6. 15
      packages/nc-gui-v2/components/cell/Integer.vue
  7. 13
      packages/nc-gui-v2/components/cell/JsonEditableCell.vue
  8. 54
      packages/nc-gui-v2/components/cell/MultiSelect.vue
  9. 35
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  10. 17
      packages/nc-gui-v2/components/cell/Text.vue
  11. 17
      packages/nc-gui-v2/components/cell/TextArea.vue
  12. 18
      packages/nc-gui-v2/components/cell/Url.vue
  13. 148
      packages/nc-gui-v2/components/cell/attachment/Carousel.vue
  14. 211
      packages/nc-gui-v2/components/cell/attachment/Modal.vue
  15. 168
      packages/nc-gui-v2/components/cell/attachment/index.vue
  16. 65
      packages/nc-gui-v2/components/cell/attachment/sort.ts
  17. 157
      packages/nc-gui-v2/components/cell/attachment/utils.ts
  18. 58
      packages/nc-gui-v2/components/general/Overlay.vue
  19. 195
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  20. 84
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  21. 85
      packages/nc-gui-v2/composables/useViewColumns.ts
  22. 1
      packages/nc-gui-v2/context/index.ts
  23. 2
      packages/nc-gui-v2/nuxt.config.ts
  24. 1257
      packages/nc-gui-v2/package-lock.json
  25. 10
      packages/nc-gui-v2/package.json
  26. 8
      packages/nc-gui-v2/pages/project/index/create-external.vue
  27. 3
      packages/nc-gui-v2/tsconfig.json
  28. 9
      packages/nc-gui-v2/utils/projectCreateUtils.ts
  29. 4
      packages/nc-gui-v2/utils/urlUtils.ts
  30. 100
      packages/nc-gui/components/utils/Language.vue
  31. 522
      packages/nc-gui/lang/bn.json
  32. 522
      packages/nc-gui/lang/hi.json
  33. 4
      packages/nc-gui/plugins/i18n.js
  34. 7
      packages/nocodb/src/lib/meta/api/sync/helpers/job.ts
  35. 2
      scripts/cypress/integration/common/6d_language_validation.js

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

@ -8,10 +8,10 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
AAnchorLink: typeof import('ant-design-vue/es')['AnchorLink']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACarousel: typeof import('ant-design-vue/es')['Carousel']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse']
@ -20,7 +20,6 @@ declare module '@vue/runtime-core' {
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
ADropdownButton: typeof import('ant-design-vue/es')['DropdownButton']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']
@ -46,20 +45,17 @@ declare module '@vue/runtime-core' {
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space']
ASkeletonImage: typeof import('ant-design-vue/es')['SkeletonImage']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATableColumn: typeof import('ant-design-vue/es')['TableColumn']
ATableColumnGroup: typeof import('ant-design-vue/es')['TableColumnGroup']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
RouterLink: typeof import('vue-router')['RouterLink']

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

@ -1,338 +0,0 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import { inject, ref, useProject, watchEffect } from '#imports'
import { useNuxtApp } from '#app'
import { ColumnInj, MetaInj } from '~/context'
import { NOCO } from '~/lib'
import { isImage } from '~/utils'
import MaterialPlusIcon from '~icons/mdi/plus'
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand'
interface Props {
modelValue: string | any[] | null
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const isPublicForm = inject<boolean>('isPublicForm', false)
const isForm = inject<boolean>('isForm', false)
const meta = inject(MetaInj)
const column = inject(ColumnInj)
const editEnabled = inject<boolean>('editEnabled', false)
const localFilesState = reactive([])
const attachments = ref([])
const uploading = ref(false)
const fileInput = ref<HTMLInputElement>()
const { $api } = useNuxtApp()
const { project } = useProject()
const toast = useToast()
watchEffect(() => {
if (modelValue) {
attachments.value = ((typeof modelValue === 'string' ? JSON.parse(modelValue) : modelValue) || []).filter(Boolean)
}
})
const selectImage = (file: any, i: unknown) => {
// todo: implement
}
const openUrl = (url: string, target = '_blank') => {
window.open(url, target)
}
const addFile = () => {
fileInput.value?.click()
}
const onFileSelection = async (e: unknown) => {
// if (this.isPublicGrid) {
// return
// }
// if (!this.$refs.file.files || !this.$refs.file.files.length) {
// return
// }
// if (this.isPublicForm) {
// this.localFilesState.push(...Array.from(this.$refs.file.files).map((file) => {
// const res = { file, title: file.name }
// if (isImage(file.name, file.mimetype)) {
// const reader = new FileReader()
// reader.onload = (e) => {
// this.$set(res, 'data', e.target.result)
// }
// reader.readAsDataURL(file)
// }
// return res
// }))
//
// this.$emit('input', this.localFilesState.map(f => f.file))
// return
// }
// todo : move to com
uploading.value = true
const newAttachments = []
for (const file of fileInput.value?.files ?? []) {
try {
const data = await $api.storage.upload(
{
path: [NOCO, project.value.title, meta?.value?.title, column?.title].join('/'),
},
{
files: file,
json: '{}',
},
)
newAttachments.push(...data)
} catch (e: any) {
toast.error(e.message || 'Some internal error occurred')
uploading.value = false
return
}
}
uploading.value = false
emit('update:modelValue', JSON.stringify([...attachments.value, ...newAttachments]))
// this.$emit('input', JSON.stringify(this.localState))
// this.$emit('update')
}
</script>
<template>
<div class="main h-100">
<div class="d-flex align-center img-container">
<div class="d-flex no-overflow">
<div
v-for="(item, i) in isPublicForm ? localFilesState : attachments"
:key="item.url || item.title"
class="thumbnail align-center justify-center d-flex"
>
<!-- <v-tooltip bottom> -->
<!-- <template #activator="{ on }"> -->
<!-- <v-img
v-if="isImage(item.title, item.mimetype)"
lazy-src="https://via.placeholder.com/60.png?text=Loading..."
alt="#"
max-height="99px"
contain
:src="item.url || item.data"
v-on="on"
@click="selectImage(item.url || item.data, i)"
> -->
<img
v-if="isImage(item.title, item.mimetype)"
alt="#"
style="max-height: 30px; max-width: 30px"
:src="item.url || item.data"
@click="selectImage(item.url || item.data, i)"
/>
<!-- <template #placeholder> -->
<!-- <v-skeleton-loader type="image" :height="active ? 33 : 22" :width="active ? 33 : 22" /> -->
<!-- </template> -->
<v-icon v-else-if="item.icon" :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')">
{{ item.icon }}
</v-icon>
<v-icon v-else :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')"> mdi-file </v-icon>
<!-- </template> -->
<!-- <span>{{ item.title }}</span> -->
<!-- </v-tooltip> -->
</div>
</div>
<!-- todo: hide or toggle based on ancestor -->
<div class="add d-flex align-center justify-center px-1 nc-attachment-add" @click="addFile">
<v-icon v-if="uploading" small color="primary" class="nc-attachment-add-spinner"> mdi-loading mdi-spin</v-icon>
<!-- <v-btn v-else-if="isForm" outlined x-small color="" text class="nc-attachment-add-btn">
<v-icon x-small color="" icon="MaterialPlusIcon"> mdi-plus </v-icon>
Attachment
</v-btn>
<v-icon small color="primary nc-attachment-add-icon">
mdi-plus
</v-icon> -->
<MaterialPlusIcon />
</div>
<v-spacer />
<MaterialArrowExpandIcon @click.stop="dialog = true" />
<!-- <v-icon class="expand-icon mr-1" x-small color="primary" @click.stop="dialog = true"> mdi-arrow-expand </v-icon> -->
</div>
<input ref="fileInput" type="file" multiple class="d-none" @change="onFileSelection" />
</div>
</template>
<style scoped lang="scss">
.thumbnail {
height: 30px;
width: 30px;
margin: 2px;
border-radius: 4px;
img {
max-height: 33px;
max-width: 33px;
}
}
.expand-icon {
margin-left: 8px;
border-radius: 2px;
transition: 0.3s background-color;
}
.expand-icon:hover {
background-color: var(--v-primary-lighten4);
}
/*.img-container {
margin: 0 -2px;
}
.no-overflow {
overflow: hidden;
}
.add {
transition: 0.2s background-color;
!*background-color: #666666ee;*!
border-radius: 4px;
height: 33px;
margin: 5px 2px;
}
.add:hover {
!*background-color: #66666699;*!
}
.thumbnail {
height: 99px;
width: 99px;
margin: 2px;
border-radius: 4px;
}
.thumbnail img {
!*max-height: 33px;*!
max-width: 99px;
}
.main {
min-height: 20px;
position: relative;
height: auto;
}
.expand-icon {
margin-left: 8px;
border-radius: 2px;
!*opacity: 0;*!
transition: 0.3s background-color;
}
.expand-icon:hover {
!*opacity: 1;*!
background-color: var(--v-primary-lighten4);
}
.modal-thumbnail img {
height: 50px;
max-width: 100%;
border-radius: 4px;
}
.modal-thumbnail {
position: relative;
margin: 10px 10px;
}
.remove-icon {
position: absolute;
top: 5px;
right: 5px;
}
.modal-thumbnail-card {
.download-icon {
position: absolute;
bottom: 5px;
right: 5px;
opacity: 0;
transition: 0.4s opacity;
}
&:hover .download-icon {
opacity: 1;
}
}
.image-overlay-container {
max-height: 100vh;
overflow-y: auto;
position: relative;
}
.image-overlay-container .close-icon {
position: fixed;
top: 15px;
right: 15px;
}
.overlay-thumbnail {
transition: 0.4s transform, 0.4s opacity;
opacity: 0.5;
}
.overlay-thumbnail.active {
transform: scale(1.4);
opacity: 1;
}
.overlay-thumbnail:hover {
opacity: 1;
}
.modal-title {
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
overflow: hidden;
}
.modal-thumbnail-card {
transition: 0.4s transform;
}
.modal-thumbnail-card:hover {
transform: scale(1.05);
}
.drop-overlay {
z-index: 5;
position: absolute;
width: 100%;
height: 100%;
left: 0;
right: 0;
top: 0;
bottom: 5px;
background: #aaaaaa44;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
}
.expand-icon {
opacity: 0;
transition: 0.4s opacity;
}
.main:hover .expand-icon {
opacity: 1;
}*/
</style>

34
packages/nc-gui-v2/components/cell/Currency.vue

@ -1,24 +1,22 @@
<script setup lang="ts">
import { computed, inject } from '#imports'
import { ColumnInj } from '~/context'
import { computed, inject, ref, useVModel } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
const { modelValue: value } = defineProps<Props>()
interface Props {
modelValue: number
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)
const editEnabled = inject<boolean>('editEnabled')
const editEnabled = inject(EditModeInj, ref(false))
interface Props {
modelValue: number
}
const root = ref<HTMLInputElement>()
const localState = computed({
get: () => value,
set: (val) => emit('update:modelValue', val),
})
const vModel = useVModel(props, 'modelValue', emit)
const currencyMeta = computed(() => {
return {
@ -29,22 +27,20 @@ const currencyMeta = computed(() => {
})
const currency = computed(() => {
try {
return isNaN(value)
? value
return isNaN(vModel.value)
? vModel.value
: new Intl.NumberFormat(currencyMeta?.value?.currency_locale || 'en-US', {
style: 'currency',
currency: currencyMeta?.value?.currency_code || 'USD',
}).format(value)
}).format(vModel.value)
} catch (e) {
return value
return vModel.value
}
})
</script>
<template>
<input v-if="editEnabled" ref="root" v-model="localState" />
<span v-else-if="value">{{ currency }}</span>
<input v-if="editEnabled" ref="root" v-model="vModel" />
<span v-else-if="vModel">{{ currency }}</span>
<span v-else />
</template>
<style scoped></style>

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

@ -1,9 +1,10 @@
<script lang="ts" setup>
import { computed } from '#imports'
import { computed, inject, useVModel } from '#imports'
import { isEmail } from '~/utils'
import { EditModeInj } from '~/context'
interface Props {
modelValue: string
modelValue: string | null
}
interface Emits {
@ -14,13 +15,13 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const root = ref<HTMLInputElement>()
const editEnabled = inject<boolean>('editEnabled')
const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits)
const validEmail = computed(() => isEmail(vModel.value))
const focus = (el: HTMLInputElement) => el?.focus()
</script>
<template>

13
packages/nc-gui-v2/components/cell/Float.vue

@ -1,5 +1,6 @@
<script lang="ts" setup>
import { computed, inject, onMounted, ref } from '#imports'
import { inject, ref, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number
@ -13,21 +14,17 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const editEnabled = inject<boolean>('editEnabled')
const root = ref<HTMLInputElement>()
const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits)
onMounted(() => {
root.value?.focus()
})
const focus = (el: HTMLInputElement) => el?.focus()
</script>
<template>
<input
v-if="editEnabled"
ref="root"
:ref="focus"
v-model="vModel"
class="outline-none pa-0 border-none w-full h-full prose-sm"
type="number"

15
packages/nc-gui-v2/components/cell/Integer.vue

@ -1,4 +1,7 @@
<script setup lang="ts">
import { inject, ref, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number
}
@ -11,15 +14,11 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const editEnabled = inject<boolean>('editEnabled')
const root = ref<HTMLInputElement>()
const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits)
const vModel = useVModel(props, 'modelValue', emit)
onMounted(() => {
root.value?.focus()
})
const focus = (el: HTMLInputElement) => el?.focus()
function onKeyDown(evt: KeyboardEvent) {
return evt.key === '.' && evt.preventDefault()
@ -29,7 +28,7 @@ function onKeyDown(evt: KeyboardEvent) {
<template>
<input
v-if="editEnabled"
ref="root"
:ref="focus"
v-model="vModel"
class="outline-none pa-0 border-none w-full h-full prose-sm"
type="number"

13
packages/nc-gui-v2/components/cell/JsonEditableCell.vue

@ -1,6 +1,7 @@
<script lang="ts" setup>
import MonacoJsonObjectEditor from '@/components/monaco/Editor.vue'
import { inject } from '#imports'
import { computed, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: string | Record<string, any>
@ -11,7 +12,7 @@ const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel'])
const editEnabled = inject('editEnabled')
const editEnabled = inject(EditModeInj)
let expand = $ref(false)
@ -19,7 +20,7 @@ let isValid = $ref(true)
let error = $ref()
const localState = computed({
const vModel = computed({
get: () => (typeof props.modelValue === 'string' ? JSON.parse(props.modelValue) : props.modelValue),
set: (val) => {
if (props.isForm) {
@ -30,7 +31,7 @@ const localState = computed({
function save() {
expand = false
emits('update:modelValue', JSON.stringify(props.modelValue))
emits('update:modelValue', JSON.stringify(vModel.value))
}
function validate(n: boolean, e: any) {
@ -69,14 +70,14 @@ export default {
</div>
<MonacoJsonObjectEditor
v-if="expand"
v-model="localState"
v-model="vModel"
class="text-left caption"
style="width: 300px; min-height: min(600px, 80vh); min-width: 100%"
@validate="validate"
/>
<MonacoJsonObjectEditor
v-else
v-model="localState"
v-model="vModel"
class="text-left caption"
style="width: 300px; min-height: 200px; min-width: 100%"
@validate="validate"

54
packages/nc-gui-v2/components/cell/MultiSelect.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { computed, inject } from '#imports'
import { ColumnInj } from '~/context'
import { ColumnInj, EditModeInj } from '~/context'
interface Props {
modelValue: string
@ -12,7 +12,7 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)
const isForm = inject<boolean>('isForm', false)
const editEnabled = inject<boolean>('editEnabled', false)
const editEnabled = inject(EditModeInj, ref(false))
const options = computed(() => column?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || [])
@ -24,56 +24,6 @@ const localState = computed({
emit('update:modelValue', val?.filter((v) => options.value.includes(v)).join(','))
},
})
/* import colors from '@/components/project/spreadsheet/helpers/colors'
export default {
name: 'SetListCheckboxCell',
props: {
value: String,
column: Object,
values: Array,
},
data() {},
computed: {
colors() {
return this.$store.state.settings.darkTheme ? colors.dark : colors.light
},
localState: {
get() {
return (this.value && this.value.split(',')) || []
},
set(val) {
this.$emit('input', val.join(','))
this.$emit('update')
},
},
setValues() {
if (this.column && this.column.dtxp) {
return this.column.dtxp.split(',').map((v) => v.replace(/^'|'$/g, ''))
}
return this.values || []
},
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
},
},
mounted() {
this.$el.focus()
const event = document.createEvent('MouseEvents')
event.initMouseEvent('mousedown', true, true, window)
this.$el.dispatchEvent(event)
},
} */
</script>
<template>

35
packages/nc-gui-v2/components/cell/SingleSelect.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { computed, inject } from '#imports'
import { ColumnInj } from '~/context'
import { ColumnInj, EditModeInj } from '~/context'
interface Props {
modelValue: string
@ -12,45 +12,18 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)
const isForm = inject<boolean>('isForm', false)
const editEnabled = inject<boolean>('editEnabled', false)
const editEnabled = inject(EditModeInj, ref(false))
const localState = computed({
const vModel = computed({
get: () => modelValue?.replace(/\\'/g, "'").replace(/^'|'$/g, ''),
set: (val) => emit('update:modelValue', val),
})
const options = computed(() => column?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || [])
/* import colors from '@/mixins/colors'
export default {
name: 'EnumListEditableCell',
mixins: [colors],
props: {
value: String,
column: Object,
isForm: Boolean,
},
computed: {
parentListeners() {
const $listeners = {}
if (this.$listeners.blur) {
$listeners.blur = this.$listeners.blur
}
if (this.$listeners.focus) {
$listeners.focus = this.$listeners.focus
}
return $listeners
},
},
} */
</script>
<template>
<v-select v-model="localState" :items="options" hide-details :clearable="!column.rqd" variation="outlined">
<v-select v-model="vModel" :items="options" hide-details :clearable="!column.rqd" variation="outlined">
<!-- v-on="parentListeners"
<template #selection="{ item }">
<div

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

@ -1,28 +1,23 @@
<script setup lang="ts">
import { inject, onMounted, ref } from '#imports'
import { inject, ref, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: any
modelValue: string
}
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const editEnabled = inject<boolean>('editEnabled', false)
const root = ref<HTMLInputElement>()
const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits)
const onSetRef = (el: HTMLInputElement) => {
el.focus()
}
const focus = (el: HTMLInputElement) => el?.focus()
</script>
<template>
<input v-if="editEnabled" :ref="onSetRef" v-model="vModel" class="h-full w-full outline-none" />
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="h-full w-full outline-none" />
<span v-else>{{ vModel }}</span>
</template>
<style scoped></style>

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

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, inject, onMounted, ref } from '#imports'
import { inject, ref, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue?: string
@ -9,19 +10,17 @@ const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const editEnabled = inject<boolean>('editEnabled', false)
const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits)
const onSetRef = (el: HTMLInputElement) => {
el.focus()
}
const focus = (el: HTMLTextAreaElement) => el?.focus()
</script>
<template>
<textarea
v-if="editEnabled"
:ref="onSetRef"
:ref="focus"
v-model="vModel"
rows="4"
class="h-full w-full min-h-[60px] outline-none"
@ -30,9 +29,3 @@ const onSetRef = (el: HTMLInputElement) => {
/>
<span v-else>{{ vModel }}</span>
</template>
<style scoped>
textarea:focus {
@apply ring-transparent;
}
</style>

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

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref } from '#imports'
import { ColumnInj } from '~/context'
import { isValidURL } from '~/utils/urlUtils'
import { computed, inject, onMounted, ref } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
import { isValidURL } from '~/utils'
interface Props {
modelValue: string
@ -13,7 +13,7 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)
const editEnabled = inject<boolean>('editEnabled')
const editEnabled = inject(EditModeInj, ref(false))
const vModel = computed({
get: () => value,
@ -26,21 +26,15 @@ const vModel = computed({
const isValid = computed(() => value && isValidURL(value))
const root = ref<HTMLInputElement>()
onMounted(() => {
root.value?.focus()
})
const focus = (el: HTMLInputElement) => el?.focus()
</script>
<template>
<input v-if="editEnabled" ref="root" v-model="vModel" class="outline-none" />
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none" />
<nuxt-link v-else-if="isValid" class="py-2 underline hover:opacity-75" :to="value" target="_blank">{{ value }}</nuxt-link>
<span v-else>{{ value }}</span>
</template>
<style scoped></style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

148
packages/nc-gui-v2/components/cell/attachment/Carousel.vue

@ -0,0 +1,148 @@
<script lang="ts" setup>
import { onKeyDown } from '@vueuse/core'
import { useAttachmentCell } from './utils'
import { isImage } from '~/utils'
import { computed, onClickOutside, ref, watch } from '#imports'
import MaterialSymbolsArrowCircleRightRounded from '~icons/material-symbols/arrow-circle-right-rounded'
import MaterialSymbolsArrowCircleLeftRounded from '~icons/material-symbols/arrow-circle-left-rounded'
import MdiCloseCircle from '~icons/mdi/close-circle'
const { selectedImage, visibleItems, downloadFile } = useAttachmentCell()!
const carouselRef = ref()
const imageItems = computed(() => visibleItems.value.filter((item) => isImage(item.title, item.mimetype)))
/** navigate to previous image on button click */
onKeyDown(
(e) => ['Left', 'ArrowLeft', 'A'].includes(e.key),
() => {
if (carouselRef.value) carouselRef.value.prev()
},
)
/** navigate to next image on button click */
onKeyDown(
(e) => ['Right', 'ArrowRight', 'D'].includes(e.key),
() => {
if (carouselRef.value) carouselRef.value.next()
},
)
/** set our selected image when slide changes */
function onSlideChange(index: number) {
selectedImage.value = imageItems.value[index]
}
/** set our carousel ref and move to initial slide */
const setCarouselRef = (el: Element) => {
carouselRef.value = el
carouselRef.value?.goTo(
imageItems.value.findIndex((item) => item === selectedImage.value),
true,
)
}
/** close overlay view when clicking outside of image */
onClickOutside(carouselRef, () => {
selectedImage.value = false
})
</script>
<template>
<general-overlay v-model="selectedImage">
<template v-if="selectedImage">
<div class="overflow-hidden p-12 text-center relative">
<div class="text-white group absolute top-5 right-5">
<MdiCloseCircle class="group-hover:text-red-500 cursor-pointer text-4xl" @click.stop="selectedImage = false" />
</div>
<div
class="select-none group hover:ring active:ring-pink-500 cursor-pointer leading-8 inline-block px-3 py-1 bg-gray-300 text-white mb-4 text-center rounded shadow"
@click.stop="downloadFile(selectedImage)"
>
<h3 class="group-hover:text-primary">{{ selectedImage && selectedImage.title }}</h3>
</div>
<a-carousel
v-if="!!selectedImage"
:ref="setCarouselRef"
dots-class="slick-dots slick-thumb"
:after-change="onSlideChange"
arrows
>
<template #prevArrow>
<div class="custom-slick-arrow left-2 z-1">
<MaterialSymbolsArrowCircleLeftRounded class="bg-white rounded-full" />
</div>
</template>
<template #nextArrow>
<div class="custom-slick-arrow !right-2 z-1">
<MaterialSymbolsArrowCircleRightRounded class="bg-white rounded-full" />
</div>
</template>
<template #customPaging="props">
<a>
<nuxt-img
class="!block"
:alt="imageItems[props.i].title || `#${props.i}`"
:src="imageItems[props.i].url || imageItems[props.i].data"
/>
</a>
</template>
<div v-for="item of imageItems" :key="item.url">
<div
:style="{ backgroundImage: `url('${item.url}')` }"
class="min-w-70vw min-h-70vh w-full h-full bg-contain bg-center bg-no-repeat"
/>
</div>
</a-carousel>
</div>
</template>
</general-overlay>
</template>
<style scoped>
.ant-carousel :deep(.slick-dots) {
@apply relative mt-4;
}
.ant-carousel :deep(.slick-slide) {
@apply w-full;
}
.ant-carousel :deep(.slick-slide img) {
@apply border-1 m-auto;
}
.ant-carousel :deep(.slick-thumb) {
@apply bottom-2;
}
.ant-carousel :deep(.slick-thumb li) {
@apply w-[60px] h-[45px];
}
.ant-carousel :deep(.slick-thumb li img) {
@apply w-full h-full block;
filter: grayscale(100%);
}
.ant-carousel :deep .slick-thumb li.slick-active img {
filter: grayscale(0%);
}
.ant-carousel :deep(.slick-arrow.custom-slick-arrow) {
@apply text-4xl text-white hover:text-primary active:text-pink-500 opacity-100 cursor-pointer z-1;
}
.ant-carousel :deep(.custom-slick-arrow:before) {
display: none;
}
.ant-carousel :deep(.custom-slick-arrow:hover) {
opacity: 0.5;
}
</style>

211
packages/nc-gui-v2/components/cell/attachment/Modal.vue

@ -0,0 +1,211 @@
<script lang="ts" setup>
import { onKeyDown } from '@vueuse/core'
import { useAttachmentCell } from './utils'
import { useSortable } from './sort'
import { ref, useDropZone, useUIPermission } from '#imports'
import { isImage, openLink } from '~/utils'
import MaterialSymbolsAttachFile from '~icons/material-symbols/attach-file'
import MdiCloseCircle from '~icons/mdi/close-circle'
import MdiDownload from '~icons/mdi/download'
import MaterialSymbolsFileCopyOutline from '~icons/material-symbols/file-copy-outline'
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file'
const { isUIAllowed } = useUIPermission()
const {
open,
isLoading,
isPublicGrid,
isForm,
isReadonly,
visibleItems,
modalVisible,
column,
FileIcon,
removeFile,
onDrop,
downloadFile,
updateModelValue,
selectedImage,
} = useAttachmentCell()!
// todo: replace placeholder var
const isLocked = ref(false)
const dropZoneRef = ref<HTMLDivElement>()
const sortableRef = ref<HTMLDivElement>()
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, isReadonly)
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
onKeyDown('Escape', () => {
modalVisible.value = false
isOverDropZone.value = false
})
</script>
<template>
<a-modal v-model:visible="modalVisible" class="nc-attachment-modal" width="80%" :footer="null">
<template #title>
<div class="flex gap-4">
<div
v-if="!isReadonly && (isForm || isUIAllowed('tableAttachment')) && !isPublicGrid && !isLocked"
class="nc-attach-file group"
@click="open"
>
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120)" />
Attach File
</div>
<div class="flex items-center gap-2">
<div v-if="isReadonly" class="text-gray-400">[Readonly]</div>
Viewing Attachments of
<div class="font-semibold underline">{{ column.title }}</div>
</div>
</div>
</template>
<div ref="dropZoneRef">
<template v-if="!isReadonly && !dragging">
<general-overlay
v-model="isOverDropZone"
inline
class="text-white ring ring-pink-500 bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl"
>
<MaterialSymbolsFileCopyOutline class="text-pink-500" height="35" width="35" />
<div class="text-white text-3xl">Drop here</div>
</general-overlay>
</template>
<div ref="sortableRef" :class="{ dragging }" class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-6 relative p-6">
<div v-for="(item, i) of visibleItems" :key="`${item.title}-${i}`" class="flex flex-col gap-1">
<a-card class="nc-attachment-item group">
<a-tooltip v-if="!isReadonly">
<template #title> Remove File </template>
<MdiCloseCircle
v-if="isUIAllowed('tableAttachment') && !isPublicGrid && !isLocked"
class="nc-attachment-remove"
@click.stop="removeFile(i)"
/>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title> Download file </template>
<div class="nc-attachment-download group-hover:(opacity-100)">
<MdiDownload @click.stop="downloadFile(item)" />
</div>
</a-tooltip>
<div
:class="[dragging ? 'cursor-move' : 'cursor-pointer']"
class="nc-attachment h-full w-full flex items-center justify-center"
>
<div
v-if="isImage(item.title, item.mimetype)"
:style="{ backgroundImage: `url('${item.url}')` }"
class="w-full h-full bg-contain bg-center bg-no-repeat"
@click.stop="() => (selectedImage = item) && (modalVisible = false)"
/>
<component
:is="FileIcon(item.icon)"
v-else-if="item.icon"
height="150"
width="150"
@click.stop="openLink(item.url || item.data)"
/>
<IcOutlineInsertDriveFile v-else height="150" width="150" @click.stop="openLink(item.url || item.data)" />
</div>
</a-card>
<div class="truncate" :title="item.title">
{{ item.title }}
</div>
</div>
<div v-if="isLoading" class="flex flex-col gap-1">
<a-card class="nc-attachment-item group">
<div class="nc-attachment h-full w-full flex items-center justify-center">
<a-skeleton-image class />
</div>
</a-card>
</div>
</div>
</div>
</a-modal>
</template>
<style lang="scss">
.nc-attachment-modal {
.nc-attach-file {
@apply select-none cursor-pointer color-transition flex items-center gap-1 border-1 p-2 rounded
@apply hover:(bg-primary/10 text-primary ring);
@apply active:(ring-pink-500 bg-primary/20);
}
.nc-attachment-item {
@apply !h-2/3 !min-h-[200px] flex items-center justify-center relative;
@supports (-moz-appearance: none) {
@apply hover:border-0;
}
&::after {
@apply pointer-events-none rounded absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out;
content: '';
}
@supports (-moz-appearance: none) {
&:hover::after {
@apply ring shadow transform scale-103;
}
&:active::after {
@apply ring ring-pink-500 shadow transform scale-103;
}
}
}
.nc-attachment-download {
@apply bg-white absolute bottom-2 right-2;
@apply transition-opacity duration-150 ease-in opacity-0 hover:ring;
@apply cursor-pointer rounded shadow flex items-center p-1 border-1;
@apply active:(ring border-0 ring-pink-500);
}
.nc-attachment-remove {
@apply absolute top-2 right-2 bg-white;
@apply hover:(ring ring-red-500);
@apply cursor-pointer rounded-full border-2;
@apply active:(ring border-0 ring-red-500);
}
.ant-card-body {
@apply !p-2 w-full h-full;
}
.ant-modal-body {
@apply !p-0;
}
.ghost,
.ghost > * {
@apply !pointer-events-none;
}
.dragging {
.nc-attachment-item {
@apply !pointer-events-none;
}
.ant-tooltip {
@apply !hidden;
}
}
}
</style>

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

@ -0,0 +1,168 @@
<script setup lang="ts">
import { onKeyDown } from '@vueuse/core'
import { useProvideAttachmentCell } from './utils'
import Modal from './Modal.vue'
import { useSortable } from './sort'
import Carousel from './Carousel.vue'
import { onMounted, ref, useDropZone, watch } from '#imports'
import { isImage, openLink } from '~/utils'
import MaterialSymbolsAttachFile from '~icons/material-symbols/attach-file'
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand'
import MaterialSymbolsFileCopyOutline from '~icons/material-symbols/file-copy-outline'
import MdiReload from '~icons/mdi/reload'
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file'
interface Props {
modelValue: string | Record<string, any>[] | null
}
interface Emits {
(event: 'update:modelValue', value: string | Record<string, any>): void
}
const { modelValue } = defineProps<Props>()
const emits = defineEmits<Emits>()
const dropZoneRef = ref<HTMLTableDataCellElement>()
const sortableRef = ref<HTMLDivElement>()
const { column, modalVisible, attachments, visibleItems, onDrop, isLoading, open, FileIcon, selectedImage, isReadonly } =
useProvideAttachmentCell(updateModelValue)
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, isReadonly)
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
/** on new value, reparse our stored attachments */
watch(
() => modelValue,
(nextModel) => {
if (nextModel) {
attachments.value = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean)
}
},
{ immediate: true },
)
/** updates attachments array for autosave */
function updateModelValue(data: string | Record<string, any>) {
emits('update:modelValue', typeof data !== 'string' ? JSON.stringify(data) : data)
}
/** Close modal on escape press, disable dropzone as well */
onKeyDown('Escape', () => {
modalVisible.value = false
isOverDropZone.value = false
})
/** if possible, on mounted we try to fetch the relevant `td` cell to use as a dropzone */
onMounted(() => {
if (typeof document !== 'undefined') {
dropZoneRef.value = document.querySelector(`td[data-col="${column.id}"]`) as HTMLTableDataCellElement
}
})
</script>
<template>
<div class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1">
<Carousel />
<template v-if="!isReadonly && !dragging && dropZoneRef">
<general-overlay
v-model="isOverDropZone"
inline
:target="`td[data-col='${column.id}']`"
class="text-white text-lg ring ring-pink-500 bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl"
>
<MaterialSymbolsFileCopyOutline class="text-pink-500" /> Drop here
</general-overlay>
</template>
<div
v-if="!isReadonly"
:class="{ 'mx-auto px-4': !visibleItems.length }"
class="group flex gap-1 items-center active:ring rounded border-1 p-1 hover:bg-primary/10"
@click.stop="open"
>
<MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<a-tooltip v-else placement="bottom">
<template #title> Click or drop a file into cell </template>
<div class="flex items-center gap-2">
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120)" />
<div v-if="!visibleItems.length" class="group-hover:text-primary">Add file(s)</div>
</div>
</a-tooltip>
</div>
<template v-if="visibleItems.length">
<div ref="sortableRef" :class="{ dragging }" class="flex flex-wrap gap-2 p-1 scrollbar-thin-primary">
<div
v-for="(item, i) of visibleItems"
:id="item.url"
:key="item.url || item.title"
style="flex: 1 1 50px"
:class="isImage(item.title, item.mimetype) ? '' : 'border-1 rounded'"
class="nc-attachment flex items-center justify-center"
>
<a-tooltip placement="bottom">
<template #title>
<div class="text-center w-full">{{ item.title }}</div>
</template>
<nuxt-img
v-if="isImage(item.title, item.mimetype)"
placeholder
width="150"
height="150"
:alt="item.title || `#${i}`"
:src="item.url || item.data"
class="ring-1 ring-gray-300 rounded"
@click="selectedImage = item"
/>
<component :is="FileIcon(item.icon)" v-else-if="item.icon" @click="openLink(item.url || item.data)" />
<IcOutlineInsertDriveFile v-else @click.stop="openLink(item.url || item.data)" />
</a-tooltip>
</div>
</div>
<div class="group flex gap-1 items-center border-1 active:ring rounded p-1 hover:bg-primary/10">
<MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<a-tooltip v-else placement="bottom">
<template #title> View attachments </template>
<MaterialArrowExpandIcon
class="select-none transform group-hover:(text-pink-500 scale-120)"
@click.stop="modalVisible = true"
/>
</a-tooltip>
</div>
</template>
<Modal />
</div>
</template>
<style lang="scss">
.nc-cell {
.nc-attachment-cell {
.ghost,
.ghost > * {
@apply !pointer-events-none;
}
.dragging {
.ant-tooltip {
@apply !hidden;
}
}
}
}
</style>

65
packages/nc-gui-v2/components/cell/attachment/sort.ts

@ -0,0 +1,65 @@
import type { SortableEvent } from 'sortablejs'
import Sortable from 'sortablejs'
import type { MaybeRef } from '@vueuse/core'
import { watchPostEffect } from '@vue/runtime-core'
import { unref } from '#imports'
export function useSortable(
element: MaybeRef<HTMLElement | undefined>,
items: MaybeRef<any[]>,
updateModelValue: (data: string | Record<string, any>[]) => void,
isReadonly: MaybeRef<boolean> = false,
) {
let dragging = $ref(false)
function onSortStart(evt: SortableEvent) {
evt.stopImmediatePropagation()
evt.preventDefault()
dragging = true
}
async function onSortEnd(evt: SortableEvent) {
evt.stopImmediatePropagation()
evt.preventDefault()
dragging = false
const _items = unref(items)
if (_items.length < 2) return
const { newIndex = 0, oldIndex = 0 } = evt
if (newIndex === oldIndex) return
_items.splice(newIndex, 0, ..._items.splice(oldIndex, 1))
updateModelValue(_items)
}
let sortable: Sortable
// todo: replace with vuedraggable
const initSortable = (el: HTMLElement) => {
sortable = new Sortable(el, {
handle: '.nc-attachment',
ghostClass: 'ghost',
onStart: onSortStart,
onEnd: onSortEnd,
})
}
watchPostEffect((onCleanup) => {
const _element = unref(element)
onCleanup(() => {
if (_element && sortable) sortable.destroy()
})
if (_element && !unref(isReadonly)) initSortable(_element)
})
return {
dragging: $$(dragging),
initSortable,
}
}

157
packages/nc-gui-v2/components/cell/attachment/utils.ts

@ -0,0 +1,157 @@
import { notification } from 'ant-design-vue'
import FileSaver from 'file-saver'
import { computed, inject, ref, useApi, useFileDialog, useInjectionState, useProject, watch } from '#imports'
import { ColumnInj, EditModeInj, MetaInj, ReadonlyInj } from '~/context'
import { isImage } from '~/utils'
import { NOCO } from '~/lib'
import MdiPdfBox from '~icons/mdi/pdf-box'
import MdiFileWordOutline from '~icons/mdi/file-word-outline'
import MdiFilePowerpointBox from '~icons/mdi/file-powerpoint-box'
import MdiFileExcelOutline from '~icons/mdi/file-excel-outline'
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file'
export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
(updateModelValue: (data: string | Record<string, any>[]) => void) => {
const isReadonly = inject(ReadonlyInj, false)
const isPublicForm = inject('isPublicForm', false)
const isForm = inject('isForm', false)
// todo: replace placeholder var
const isPublicGrid = $ref(false)
const meta = inject(MetaInj)!
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj, ref(false))
const storedFiles = ref<{ title: string; file: File }[]>([])
const attachments = ref<File[]>([])
const modalVisible = ref(false)
const selectedImage = ref()
const { project } = useProject()
const { api, isLoading } = useApi()
const { files, open } = useFileDialog()
/** remove a file from our stored attachments (either locally stored or saved ones) */
function removeFile(i: number) {
if (isPublicForm) {
storedFiles.value.splice(i, 1)
updateModelValue(storedFiles.value.map((storedFile) => storedFile.file))
} else {
attachments.value.splice(i, 1)
updateModelValue(attachments.value)
}
}
/** save a file on select / drop, either locally (in-memory) or in the db */
async function onFileSelect(selectedFiles: FileList | File[]) {
if (!selectedFiles.length || isPublicGrid) return
if (isPublicForm) {
storedFiles.value.push(
...Array.from(selectedFiles).map((file) => {
const res = { file, title: file.name }
if (isImage(file.name, (file as any).mimetype)) {
const reader = new FileReader()
reader.readAsDataURL(file)
}
return res
}),
)
return updateModelValue(storedFiles.value.map((storedFile) => storedFile.file))
}
const newAttachments = []
for (const file of selectedFiles) {
try {
const data = await api.storage.upload(
{
path: [NOCO, project.value.title, meta.value.title, column.title].join('/'),
},
{
files: file,
json: '{}',
},
)
newAttachments.push(...data)
} catch (e: any) {
notification.error({
message: e.message || 'Some internal error occurred',
})
}
}
updateModelValue([...attachments.value, ...newAttachments])
}
/** save files on drop */
async function onDrop(droppedFiles: File[] | null) {
if (droppedFiles) {
// set files
await onFileSelect(droppedFiles)
}
}
/** download a file */
async function downloadFile(item: Record<string, any>) {
FileSaver.saveAs(item.url || item.data, item.title)
}
const FileIcon = (icon: string) => {
switch (icon) {
case 'mdi-pdf-box':
return MdiPdfBox
case 'mdi-file-word-outline':
return MdiFileWordOutline
case 'mdi-file-powerpoint-box':
return MdiFilePowerpointBox
case 'mdi-file-excel-outline':
return MdiFileExcelOutline
default:
return IcOutlineInsertDriveFile
}
}
/** our currently visible items, either the locally stored or the ones from db, depending on isPublicForm status */
const visibleItems = computed<any[]>(() => (isPublicForm ? storedFiles.value : attachments.value) || ([] as any[]))
watch(files, (nextFiles) => nextFiles && onFileSelect(nextFiles))
return {
attachments,
storedFiles,
visibleItems,
isPublicForm,
isForm,
isPublicGrid,
isReadonly,
meta,
column,
editEnabled,
isLoading,
api,
open,
onDrop,
modalVisible,
FileIcon,
removeFile,
downloadFile,
updateModelValue,
selectedImage,
}
},
'attachmentCell',
)

58
packages/nc-gui-v2/components/general/Overlay.vue

@ -0,0 +1,58 @@
<script lang="ts" setup>
import { onKeyDown } from '@vueuse/core'
import type { TeleportProps } from '@vue/runtime-core'
import { useVModel, watch } from '#imports'
interface Props {
modelValue?: any
/** if true, overlay will use `position: absolute` instead of `position: fixed` */
inline?: boolean
/** target to teleport to */
target?: TeleportProps['to']
teleportDisabled?: TeleportProps['disabled']
transition?: boolean
}
interface Emits {
(event: 'update:modelValue', value: boolean): void
(event: 'close'): void
(event: 'open'): void
}
const { transition = true, teleportDisabled = false, inline = false, target, ...rest } = defineProps<Props>()
const emits = defineEmits<Emits>()
const vModel = useVModel(rest, 'modelValue', emits)
onKeyDown('Escape', () => {
vModel.value = false
})
watch(vModel, (nextVal) => {
if (nextVal) emits('open')
else emits('close')
})
</script>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<template>
<teleport :disabled="teleportDisabled || (inline && !target)" :to="target || 'body'">
<div
v-bind="$attrs"
:class="[
vModel ? 'opacity-100' : 'opacity-0 pointer-events-none',
inline ? 'absolute' : 'fixed',
transition ? 'transition-opacity duration-200 ease-in-out' : '',
]"
class="z-100 top-0 left-0 bottom-0 right-0 bg-gray-700/75"
>
<slot :is-open="vModel" />
</div>
</teleport>
</template>

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

@ -1,30 +1,25 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { provide } from 'vue'
import { computed, useColumn } from '#imports'
import { useColumn, useVModel } from '#imports'
import { ColumnInj } from '~/context'
interface Props {
column: ColumnType
modelValue: any
editEnabled: boolean
}
const { column, modelValue: value, editEnabled } = defineProps<Props>()
interface Emits {
(event: 'update:modelValue', value: any): void
}
const emit = defineEmits(['update:modelValue'])
const { column, ...rest } = defineProps<Props>()
provide(ColumnInj, column)
const emit = defineEmits<Emits>()
provide(
'editEnabled',
computed(() => editEnabled),
)
provide(ColumnInj, column)
const localState = computed({
get: () => value,
set: (val) => emit('update:modelValue', val),
})
const vModel = useVModel(rest, 'modelValue', emit)
const {
isURL,
@ -53,155 +48,27 @@ const {
<template>
<div class="nc-cell" @keydown.stop.left @keydown.stop.right @keydown.stop.up @keydown.stop.down>
<!--
todo :
JSONCell
-->
<!-- <RatingCell -->
<!-- v-if="isRating" -->
<!-- /> -->
<!-- v-model="localState"
:active="active"
:is-form="isForm"
:column="column"
:is-public-grid="isPublic && !isForm"
:is-public-form="isPublic && isForm"
:is-locked="isLocked"
v-on="$listeners"
/> -->
<!-- <DurationCell -->
<!-- v-else-if="isDuration" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :active="active" -->
<!-- :is-form="isForm" -->
<!-- :column="column" -->
<!-- :is-locked="isLocked" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <DatePickerCell -->
<!-- v-else-if="isDate" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <TimePickerCell -->
<!-- v-else-if="isTime" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- v-on="parentListeners" -->
<!-- @save="$emit('save')" -->
<!-- />&ndash;&gt; -->
<!-- <DateTimePickerCell -->
<!-- v-else-if="isDateTime" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- ignore-focus -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <EnumCell -->
<!-- v-else-if="isEnum && ((!isForm && !active) || isLocked || (isPublic && !isForm))" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <EnumListCell -->
<!-- v-else-if="isEnum" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState"&ndash;&gt; -->
<!-- &lt;!&ndash; :is-form="isForm"&ndash;&gt; -->
<!-- &lt;!&ndash; :column="column"&ndash;&gt; -->
<!-- &lt;!&ndash; v-on="parentListeners"&ndash;&gt; -->
<!-- &lt;!&ndash; />&ndash;&gt; -->
<!-- <JsonEditableCell -->
<!-- v-else-if="isJSON" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :is-form="isForm" -->
<!-- v-on="parentListeners" -->
<!-- @input="$emit('save')" -->
<!-- />&ndash;&gt; -->
<!-- <SetListEditableCell -->
<!-- v-else-if="isSet && (active || isForm) && !isLocked && !(isPublic && !isForm)" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <SetListCell -->
<!-- v-else-if="isSet" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<!-- <UrlCell v-else-if="isURL" -->
<!-- /> -->
<!-- &lt;!&ndash; v-model="localState" v-on="parentListeners" &ndash;&gt; -->
<!-- &lt;!&ndash; />&ndash;&gt; -->
<CellTextArea v-if="isTextArea" v-model="localState" />
<!-- v-model="localState"
:is-form="isForm"
v-on="parentListeners"
/> -->
<CellCheckbox v-else-if="isBoolean" v-model="localState" />
<!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" -->
<!-- :is-form="isForm" -->
<!-- v-on="parentListeners" -->
<!-- />&ndash;&gt; -->
<CellAttachment v-else-if="isAttachment" v-model="localState" />
<CellSingleSelect v-else-if="isSingleSelect" v-model="localState" />
<CellMultiSelect v-else-if="isMultiSelect" v-model="localState" />
<CellDatePicker v-else-if="isDate" v-model="localState" />
<CellYearPicker v-else-if="isYear" v-model="localState" />
<CellDateTimePicker v-else-if="isDateTime" v-model="localState" />
<CellTimePicker v-else-if="isTime" v-model="localState" />
<CellRating v-else-if="isRating" v-model="localState" />
<!-- v-model="localState"
:active="active"
:db-alias="dbAlias"
:meta="meta"
:is-form="isForm"
:column="column"
:is-public-grid="isPublic && !isForm"
:is-public-form="isPublic && isForm"
:view-id="viewId"
:is-locked="isLocked"
v-on="$listeners"
/> -->
<CellDuration v-else-if="isDuration" v-model="localState" />
<CellEmail v-else-if="isEmail" v-model="localState" />
<CellUrl v-else-if="isURL" v-model="localState" />
<CellPhoneNumber v-else-if="isPhoneNumber" v-model="localState" />
<!-- v-on="parentListeners"
/>
-->
<CellCurrency v-else-if="isCurrency" v-model="localState" />
<CellDecimal v-else-if="isDecimal" v-model="localState" />
<CellInteger v-else-if="isInt" v-model="localState" />
<CellFloat v-else-if="isFloat" v-model="localState" />
<CellText v-else-if="isString" v-model="localState" />
<!-- v-on="parentListeners"
/>
-->
<CellPercent v-else-if="isPercent" v-model="localState" />
<CellText v-else v-model="localState" />
<!-- v-on="$listeners" <span v-if="hint" class="nc-hint">{{ hint }}</span> -->
<!-- <div v-if="(isLocked || (isPublic && !isForm)) && !isAttachment" class="nc-locked-overlay" /> -->
<CellTextArea v-if="isTextArea" v-model="vModel" />
<CellCheckbox v-else-if="isBoolean" v-model="vModel" />
<CellAttachment v-else-if="isAttachment" v-model="vModel" />
<CellSingleSelect v-else-if="isSingleSelect" v-model="vModel" />
<CellMultiSelect v-else-if="isMultiSelect" v-model="vModel" />
<CellDatePicker v-else-if="isDate" v-model="vModel" />
<CellYearPicker v-else-if="isYear" v-model="vModel" />
<CellDateTimePicker v-else-if="isDateTime" v-model="vModel" />
<CellTimePicker v-else-if="isTime" v-model="vModel" />
<CellRating v-else-if="isRating" v-model="vModel" />
<CellDuration v-else-if="isDuration" v-model="vModel" />
<CellEmail v-else-if="isEmail" v-model="vModel" />
<CellUrl v-else-if="isURL" v-model="vModel" />
<CellPhoneNumber v-else-if="isPhoneNumber" v-model="vModel" />
<CellCurrency v-else-if="isCurrency" v-model="vModel" />
<CellDecimal v-else-if="isDecimal" v-model="vModel" />
<CellInteger v-else-if="isInt" v-model="vModel" />
<CellFloat v-else-if="isFloat" v-model="vModel" />
<CellText v-else-if="isString" v-model="vModel" />
<CellPercent v-else-if="isPercent" v-model="vModel" />
<CellText v-else v-model="vModel" />
</div>
</template>
@ -222,7 +89,9 @@ div {
}
.nc-cell {
position: relative;
@apply relative h-full;
width: inherit;
display: inherit;
}
.nc-locked-overlay {

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

@ -12,6 +12,7 @@ import {
import {
ActiveViewInj,
ChangePageInj,
EditModeInj,
FieldsInj,
IsFormInj,
IsGridInj,
@ -44,6 +45,7 @@ provide(IsFormInj, false)
provide(IsGridInj, true)
provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage)
provide(EditModeInj, editEnabled)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
reloadViewDataHook?.on(() => {
@ -142,68 +144,6 @@ if (meta) useProvideColumnCreateStore(meta)
@click="selectCell(rowIndex, colIndex)"
@dblclick="editEnabled = true"
>
<!-- @contextmenu=" -->
<!-- showRowContextMenu($event, rowObj, rowMeta, row, col, columnObj) -->
<!-- " -->
<!-- > -->
<!-- <virtual-cell -->
<!-- v-if="isVirtualCol(columnObj)" -->
<!-- :password="password" -->
<!-- :is-public="isPublicView" -->
<!-- :metas="metas" -->
<!-- :is-locked="isLocked" -->
<!-- :column="columnObj" -->
<!-- :row="rowObj" -->
<!-- :nodes="nodes" -->
<!-- :meta="meta" -->
<!-- :api="api" -->
<!-- :active="selected.col === col && selected.row === row" -->
<!-- :sql-ui="sqlUi" -->
<!-- :is-new="rowMeta.new" -->
<!-- v-on="$listeners" -->
<!-- @updateCol=" -->
<!-- (...args) => -->
<!-- updateCol( -->
<!-- ...args, -->
<!-- columnObj.bt -->
<!-- && meta.columns.find( -->
<!-- (c) => c.column_name === columnObj.bt.column_name, -->
<!-- ), -->
<!-- col, -->
<!-- row, -->
<!-- ) -->
<!-- " -->
<!-- @saveRow="onCellValueChange(col, row, columnObj, true)" -->
<!-- /> -->
<!-- <editable-cell -->
<!-- v-else-if=" -->
<!-- ((isPkAvail || rowMeta.new) -->
<!-- && !isView -->
<!-- && !isLocked -->
<!-- && !isPublicView -->
<!-- && editEnabled.col === col -->
<!-- && editEnabled.row === row) -->
<!-- || enableEditable(columnObj) -->
<!-- " -->
<!-- v-model="rowObj[columnObj.title]" -->
<!-- :column="columnObj" -->
<!-- :meta="meta" -->
<!-- :active="selected.col === col && selected.row === row" -->
<!-- :sql-ui="sqlUi" -->
<!-- :db-alias="nodes.dbAlias" -->
<!-- :is-locked="isLocked" -->
<!-- :is-public="isPublicView" -->
<!-- :view-id="viewId" -->
<!-- @save="editEnabled = {}; onCellValueChange(col, row, columnObj, true);" -->
<!-- @cancel="editEnabled = {}" -->
<!-- @update="onCellValueChange(col, row, columnObj, false)" -->
<!-- @blur="onCellValueChange(col, row, columnObj, true)" -->
<!-- @input="unsaved = true" -->
<!-- @navigateToNext="navigateToNext" -->
<!-- @navigateToPrev="navigateToPrev" -->
<!-- /> -->
<SmartsheetVirtualCell v-if="isVirtualCol(columnObj)" v-model="row[columnObj.title]" :column="columnObj" />
<SmartsheetCell
@ -213,20 +153,6 @@ if (meta) useProvideColumnCreateStore(meta)
:edit-enabled="editEnabled && selected.col === colIndex && selected.row === rowIndex"
@update:model-value="updateRowProperty(row, columnObj.title)"
/>
<!-- <SmartsheetCell v-else :column="columnObj" :value="row[columnObj.title]" /> -->
<!-- :selected="selected.col === col && selected.row === row" -->
<!-- :is-locked="isLocked" -->
<!-- :column="columnObj" -->
<!-- :meta="meta" -->
<!-- :db-alias="nodes.dbAlias" -->
<!-- :value="rowObj[columnObj.title]" -->
<!-- :sql-ui="sqlUi" -->
<!-- @enableedit=" -->
<!-- makeSelected(col, row); -->
<!-- makeEditable(col, row, columnObj.ai, rowMeta); -->
<!-- " -->
<!-- /> -->
</td>
</tr>
</tbody>
@ -249,10 +175,6 @@ if (meta) useProvideColumnCreateStore(meta)
height: 41px !important;
position: relative;
padding: 0 5px;
& > * {
@apply flex align-center h-auto;
}
overflow: hidden;
}
@ -287,7 +209,7 @@ if (meta) useProvideColumnCreateStore(meta)
// todo: replace with css variable
td.active::after {
border: 2px solid #0040bc; /*var(--v-primary-lighten1);*/
@apply border-2 border-solid border-primary;
}
td.active::before {

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

@ -28,23 +28,30 @@ export function useViewColumns(
if (!meta || !view) return
let order = 1
if (view?.value?.id) {
const data = await $api.dbViewColumn.list(view?.value?.id as string)
const fieldById: Record<string, any> = data.reduce((o: Record<string, any>, f: any) => {
f.show = !!f.show
if (view.value?.id) {
const data = (await $api.dbViewColumn.list(view.value.id)) as any[]
const fieldById = data.reduce<Record<string, any>>((acc, curr) => {
curr.show = !!curr.show
return {
...o,
[f.fk_column_id as string]: f,
...acc,
[curr.fk_column_id]: curr,
}
}, {})
fields.value = meta.value?.columns
?.map((c) => ({
title: c.title,
fk_column_id: c.id,
...(fieldById[c.id as string] ? fieldById[c.id as string] : {}),
order: (fieldById[c.id as string] && fieldById[c.id as string].order) || order++,
system: isSystemColumn(fieldById[c.fk_model_id as string]?.type as ColumnType),
}))
fields.value = meta.value.columns
?.map((column) => {
const currentColumnField = fieldById[column.id!] || {}
return {
title: column.title,
fk_column_id: column.id,
...currentColumnField,
order: currentColumnField.order || order++,
system: isSystemColumn(currentColumnField.type || false),
}
})
.sort((a, b) => a.order - b.order)
} else if (isPublic) {
fields.value = meta.value.columns as any
@ -52,32 +59,37 @@ export function useViewColumns(
}
const showAll = async () => {
await $api.dbView.showAllColumn(view?.value?.id as string)
if (view?.value?.id) await $api.dbView.showAllColumn(view.value.id)
await loadViewColumns()
reloadData?.()
}
const hideAll = async () => {
await $api.dbView.hideAllColumn(view?.value?.id as string)
if (view?.value?.id) await $api.dbView.hideAllColumn(view.value.id)
await loadViewColumns()
reloadData?.()
}
const saveOrUpdate = async (field: any, index: number) => {
if (field.id) {
await $api.dbViewColumn.update(view?.value?.id as string, field.id, field)
} else {
if (fields.value) fields.value[index] = (await $api.dbViewColumn.create(view?.value?.id as string, field)) as any
if (field.id && view?.value?.id) {
await $api.dbViewColumn.update(view.value.id, field.id, field)
} else if (view?.value?.id) {
if (fields.value) fields.value[index] = (await $api.dbViewColumn.create(view.value.id, field)) as any
}
reloadData?.()
}
const metaColumnById = computed(() => {
return meta?.value?.columns?.reduce<Record<string, ColumnType>>((o: Record<string, any>, c: any) => {
return (
meta.value.columns?.reduce<Record<string, ColumnType>>((acc, curr) => {
return {
...o,
[c.id]: c,
...acc,
[curr.id!]: curr,
}
}, {})
}, {}) || {}
)
})
const showSystemFields = computed({
@ -86,28 +98,27 @@ export function useViewColumns(
return (view?.value as any)?.show_system_fields || false
},
set(v) {
if (view?.value) {
$api.dbView.update(
view?.value?.id as string,
{
if (view?.value?.id) {
$api.dbView.update(view.value.id, {
// todo: update swagger
show_system_fields: v,
} as any,
)
} as any)
;(view.value as any).show_system_fields = v
}
},
})
const filteredFieldList = computed(() => {
return fields.value?.filter((field) => {
return (
fields.value?.filter((field) => {
// hide system columns if not enabled
if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id as string])) {
if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) {
return false
}
return !filterQuery?.value || field.title.toLowerCase().includes(filterQuery.value.toLowerCase())
})
}) || {}
)
})
const sortedAndFilteredFields = computed<ColumnType[]>(() => {
@ -117,15 +128,15 @@ export function useViewColumns(
if (
!showSystemFields.value &&
metaColumnById.value &&
metaColumnById?.value?.[c.fk_column_id as string] &&
isSystemColumn(metaColumnById?.value?.[c.fk_column_id as string])
metaColumnById?.value?.[c.fk_column_id!] &&
isSystemColumn(metaColumnById.value?.[c.fk_column_id!])
) {
return false
}
return c.show
})
?.sort((c1, c2) => c1.order - c2.order)
?.map((c) => metaColumnById?.value?.[c.fk_column_id as string]) || []) as ColumnType[]
?.sort((a, b) => a.order - b.order)
?.map((c) => metaColumnById?.value?.[c.fk_column_id!]) || []) as ColumnType[]
})
// reload view columns when table meta changes

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

@ -21,3 +21,4 @@ export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection')
export const ViewListInj: InjectionKey<Ref<(GridType | FormType | KanbanType | GalleryType)[]>> = Symbol('view-list-injection')
export const RightSidebarInj: InjectionKey<Ref<boolean>> = Symbol('right-sidebar-injection')
export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injection')

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

@ -8,7 +8,7 @@ import monacoEditorPlugin from 'vite-plugin-monaco-editor'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
modules: ['@vueuse/nuxt', 'nuxt-windicss'],
modules: ['@vueuse/nuxt', 'nuxt-windicss', '@nuxt/image-edge'],
ssr: false,

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

File diff suppressed because it is too large Load Diff

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

@ -12,9 +12,9 @@
},
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@vueuse/core": "^8.7.5",
"@vueuse/integrations": "^8.9.1",
"ant-design-vue": "^3.1.0-rc.6",
"@vueuse/core": "^9.0.2",
"@vueuse/integrations": "^9.0.2",
"ant-design-vue": "^3.2.10",
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"jwt-decode": "^3.1.2",
@ -41,14 +41,16 @@
"@iconify-json/mdi": "^1.1.25",
"@iconify-json/ri": "^1.1.3",
"@intlify/vite-plugin-vue-i18n": "^4.0.0",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",
"@types/file-saver": "^2.0.5",
"@types/papaparse": "^5.3.2",
"@types/sortablejs": "^1.13.0",
"@vitejs/plugin-vue": "^2.3.3",
"@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37",
"@vue/test-utils": "^2.0.2",
"@vueuse/nuxt": "^8.6.0",
"@vueuse/nuxt": "^9.0.2",
"@windicss/plugin-animations": "^1.0.9",
"@windicss/plugin-question-mark": "^0.1.1",
"@windicss/plugin-scrollbar": "^1.2.3",

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

@ -59,7 +59,7 @@ const validators = computed(() => {
'dataSource.connection.database': [fieldRequiredValidator],
...([ClientType.PG, ClientType.MSSQL].includes(formState.dataSource.client)
? {
'dataSource.connection.searchPath.0': [fieldRequiredValidator],
'dataSource.searchPath.0': [fieldRequiredValidator],
}
: {}),
}),
@ -288,11 +288,11 @@ onMounted(() => {
</a-form-item>
<!-- Schema name -->
<a-form-item
v-if="[ClientType.MSSQL, ClientType.PG].includes(formState.dataSource.client)"
v-if="[ClientType.MSSQL, ClientType.PG].includes(formState.dataSource.client) && formState.dataSource.searchPath"
:label="$t('labels.schemaName')"
v-bind="validateInfos['dataSource.connection.searchPath.0']"
v-bind="validateInfos['dataSource.searchPath.0']"
>
<a-input v-model:value="formState.dataSource.connection.searchPath[0]" size="small" />
<a-input v-model:value="formState.dataSource.searchPath[0]" size="small" />
</a-form-item>
<a-collapse ghost expand-icon-position="right" class="mt-6">

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

@ -15,7 +15,8 @@
"vue-i18n",
"unplugin-icons/types/vue",
"nuxt-windicss",
"vite/client"
"vite/client",
"@nuxt/image-edge"
]
},
"exclude": [

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

@ -12,7 +12,6 @@ export interface ProjectCreateForm {
password: string
port: number | string
ssl?: Record<string, string>
searchPath?: string[]
}
| {
client?: ClientType.SQLITE
@ -22,6 +21,7 @@ export interface ProjectCreateForm {
}
useNullAsDefault?: boolean
}
searchPath?: string[]
}
inflection: {
inflectionColumn?: string
@ -73,7 +73,6 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
user: 'postgres',
password: 'password',
database: '_test',
searchPath: ['public'],
ssl: {
ca: '',
key: '',
@ -110,7 +109,6 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
user: 'sa',
password: 'Password123.',
database: '_test',
searchPath: ['dbo'],
ssl: {
ca: '',
key: '',
@ -203,6 +201,11 @@ export const getDefaultConnectionConfig = (client: ClientType): ProjectCreateFor
return {
client,
connection: sampleConnectionData[client],
searchPath: [ClientType.PG, ClientType.MSSQL].includes(client)
? client === ClientType.PG
? ['public']
: ['dbo']
: undefined,
}
}

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

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

100
packages/nc-gui/components/utils/Language.vue

@ -2,7 +2,9 @@
<div>
<v-menu top offset-y>
<template #activator="{ on }">
<v-icon size="20" class="ml-2 nc-menu-translate" v-on="on"> mdi-translate </v-icon>
<v-icon size="20" class="ml-2 nc-menu-translate" v-on="on">
mdi-translate
</v-icon>
</template>
<v-list dense class="nc-language-list">
<v-list-item-group v-model="language">
@ -40,104 +42,106 @@ export default {
name: 'Language',
data: () => ({
labels: {
bn: 'ব',
da: 'Dansk',
de: 'Deutsch',
en: 'English',
es: 'Español',
fa: 'فارسی',
fi: 'Suomalainen',
fr: 'Français',
hi: 'हि',
hr: 'Hrvatski',
id: 'Bahasa Indonesia',
ja: '日本語',
it_IT: 'Italiano',
iw: ִברִית',
ja: '日本語',
ko: '한국인',
lv: 'Latviešu',
nl: 'Nederlandse',
no: 'Norsk',
pt_BR: 'Português (Brasil)',
ru: 'Pусский',
zh_CN: '大陆简体',
zh_HK: '香港繁體',
zh_TW: '臺灣正體',
sl: 'Slovenščina',
sv: 'Svenska',
th: 'ไทย',
tr: 'Turkish',
da: 'Dansk',
vi: 'Tiếng Việt',
no: 'Norsk',
iw: ִברִית',
fi: 'Suomalainen',
uk: 'Українська',
hr: 'Hrvatski',
th: 'ไทย',
sl: 'Slovenščina',
pt_BR: 'Português (Brasil)',
},
vi: 'Tiếng Việt',
zh_CN: '大陆简体',
zh_HK: '香港繁體',
zh_TW: '臺灣正體'
}
}),
computed: {
languages() {
return ((this.$i18n && this.$i18n.availableLocales) || ['en']).sort();
return ((this.$i18n && this.$i18n.availableLocales) || ['en']).sort()
},
language: {
get() {
return this.$store.state.settings.language;
return this.$store.state.settings.language
},
set(val) {
this.$store.commit('settings/MutLanguage', val);
this.applyDirection();
},
},
this.$store.commit('settings/MutLanguage', val)
this.applyDirection()
}
}
},
mounted() {
this.applyDirection();
this.applyDirection()
},
methods: {
applyDirection() {
const targetDirection = this.isRtlLang() ? 'rtl' : 'ltr';
const oppositeDirection = targetDirection == 'ltr' ? 'rtl' : 'ltr';
document.body.classList.remove(oppositeDirection);
document.body.classList.add(targetDirection);
document.body.style.direction = targetDirection;
const targetDirection = this.isRtlLang() ? 'rtl' : 'ltr'
const oppositeDirection = targetDirection == 'ltr' ? 'rtl' : 'ltr'
document.body.classList.remove(oppositeDirection)
document.body.classList.add(targetDirection)
document.body.style.direction = targetDirection
},
isRtlLang() {
return ['fa'].includes(this.language);
return ['fa'].includes(this.language)
},
changeLan(lan) {
this.language = lan;
const count = 200;
this.language = lan
const count = 200
const defaults = {
origin: { y: 0.7 },
};
origin: { y: 0.7 }
}
function fire(particleRatio, opts) {
window.confetti(
Object.assign({}, defaults, opts, {
particleCount: Math.floor(count * particleRatio),
particleCount: Math.floor(count * particleRatio)
})
);
)
}
fire(0.25, {
spread: 26,
startVelocity: 55,
});
startVelocity: 55
})
fire(0.2, {
spread: 60,
});
spread: 60
})
fire(0.35, {
spread: 100,
decay: 0.91,
scalar: 0.8,
});
scalar: 0.8
})
fire(0.1, {
spread: 120,
startVelocity: 25,
decay: 0.92,
scalar: 1.2,
});
scalar: 1.2
})
fire(0.1, {
spread: 120,
startVelocity: 45,
});
this.$e('c:navbar:lang', { lang: lan });
},
},
};
startVelocity: 45
})
this.$e('c:navbar:lang', { lang: lan })
}
}
}
</script>
<style scoped lang="scss">

522
packages/nc-gui/lang/bn.json

@ -0,0 +1,522 @@
{
"general": {
"home": "বি",
"load": "ভর",
"open": "খ",
"close": "বনধ",
"yes": "হ",
"no": "ন",
"ok": "ঠিক আছ",
"and": "এব",
"or": "ব",
"add": "যগ করন",
"edit": "সমদন",
"remove": "অপসরণ",
"save": "সরকষণ",
"cancel": "বিল",
"submit": "জমিন",
"create": "সি",
"insert": "Insert",
"delete": "ম",
"update": "হলনদ",
"rename": "নম পরিবরতন",
"reload": "পনরড",
"reset": "রিট",
"install": "ইনসটল করন",
"show": "শ",
"hide": "লন",
"showAll": "সব দও",
"hideAll": "সব লও",
"showMore": "আরন",
"showOptions": "বিকলপগিন",
"hideOptions": "বিকলপগিন",
"showMenu": "মন",
"hideMenu": "মন",
"addAll": "সব যগ কর",
"removeAll": "সব মন",
"signUp": "নিবনধন করন",
"signIn": "সইন ইন করন",
"signOut": "সইন আউট",
"required": "পরযজন",
"preferred": "পছনদসই",
"mandatory": "বযতলক",
"loading": "লড হচ ...",
"title": "শিম",
"upload": "আপলড করন",
"download": "ডউনলড করন",
"default": "Default",
"more": "আরও",
"less": "কম",
"event": "ঘটন",
"condition": "অবস",
"after": "পর",
"before": "আগ",
"search": "Search",
"notification": "বিঞপি",
"reference": "পরসঙগ",
"function": "নিিট করম"
},
"objects": {
"project": "পরকলপ",
"projects": "পরকলপ",
"table": "টিল",
"tables": "টিল",
"field": "কর",
"fields": "কর",
"column": "কলম",
"columns": "কলম",
"page": "প",
"pages": "পি",
"record": "রকরড",
"records": "রকরড",
"webhook": "Webhook",
"webhooks": "ওযবহকস",
"view": "দন",
"views": "ভিউ",
"viewType": {
"grid": "গিড",
"gallery": "গ",
"form": "ফরম",
"kanban": "কনবন",
"calendar": "কর"
},
"user": "বযবহরক",
"users": "বযবহরক",
"role": "ভি",
"roles": "ভি",
"roleType": {
"owner": "মিক",
"creator": "সরষ",
"editor": "সমদক",
"commenter": "মনতবযক",
"viewer": "দরশক"
}
},
"datatype": {
"ID": "আইডি",
"ForeignKey": "বিি",
"SingleLineText": "একক লইন পয",
"LongText": "দঘ পয",
"Attachment": "সি",
"Checkbox": "চকবকস",
"MultiSelect": "মিিচন করন",
"SingleSelect": "একক নিচন",
"Collaborator": "সহয",
"Date": "তিখ",
"Year": "বছর",
"Time": "সময",
"PhoneNumber": "ফন নমবর",
"Email": "ইমল",
"URL": "Url",
"Number": "স",
"Decimal": "দশমিক",
"Currency": "ম",
"Percent": "শতশ",
"Duration": "সমযল",
"Rating": "রি",
"Formula": "সর",
"Rollup": "রলআপ",
"Count": "গণন",
"Lookup": "খ",
"DateTime": "তিখ সময",
"CreateTime": "সমযি করন",
"LastModifiedTime": "শষ পরিবরিত সময",
"AutoNumber": "অট নমবর",
"Barcode": "বরকড",
"Button": "বম",
"Password": "পসওযড",
"relationProperties": {
"noAction": "No Action",
"cascade": "Cascade",
"restrict": "र",
"setNull": "NULL সট করন",
"setDefault": "ডিফলট সট করন"
}
},
"filterOperation": {
"isEqual": "সমন",
"isNotEqual": "সমন নয",
"isLike": "মত হয",
"isNot like": "পছনদ হয",
"isEmpty": "খি",
"isNotEmpty": "খি",
"isNull": "নল",
"isNotNull": "নল নয"
},
"title": {
"newProj": "নতন পরকলপ",
"myProject": "আমর পরকলপ",
"formTitle": "ফরম শিম",
"collabView": "সহয দরশন",
"lockedView": "লক ভিউ",
"personalView": "বযকিগত দয",
"appStore": "অপ সর",
"teamAndAuth": "দল ও আথ",
"rolesUserMgmt": "ভি এবযবহরক পরিলন",
"userMgmt": "বযবহরক পরিলন",
"apiTokenMgmt": "এপিআই টন পরিলন",
"rolesMgmt": "ভি পরিলন",
"projMeta": "পরকলপ ম",
"metaMgmt": "ম পরিলন",
"metadata": "ম",
"exportImportMeta": "রফতি / আমদি",
"uiACL": "UI অস কনল",
"metaOperations": "ম অপশনস",
"audit": "নি",
"auditLogs": "হিবনির বিবরণ",
"sqlMigrations": "এসকিউএল মইগশন",
"dbCredentials": "ডস শপতর",
"advancedParameters": "এসএসএল এব উননত পরিি",
"headCreateProject": "পরকলপ তি করন | নকডিি",
"headLogin": "লগ ইন | নকডিি",
"resetPassword": "আপনর পসওযড পনরট করন",
"teamAndSettings": "দল এবিস",
"apiDocs": "API Docs",
"importFromAirtable": "এযরটবল থ আমদি করন"
},
"labels": {
"notifyVia": "এর মযম অবহিত করন",
"projName": "পরকলর নম",
"tableName": "Table name",
"viewName": "নম দন",
"viewLink": "লিক দন",
"columnName": "কলর নম",
"columnType": "কলম পরকর",
"roleName": "নমভি",
"roleDescription": "ভি বরণন",
"databaseType": "ডস টইপ করন",
"lengthValue": "দয/ মন",
"dbType": "ডস পরকর",
"sqliteFile": "সইট ফইল",
"hostAddress": "হট ঠি",
"port": "পট নর",
"username": "বযবহরকর নম",
"password": "পসওযড",
"schemaName": "সির নম",
"action": "করম",
"actions": "কি",
"operation": "অপশন",
"operationType": "অপশন টইপ",
"operationSubType": "অপশন সব-টইপ",
"description": "বরণন",
"authentication": "পরমকরণ",
"token": "টন",
"where": "ক",
"cache": "ক",
"chat": "চট",
"email": "ই-মইল",
"storage": "সজ",
"uiAcl": "UI-ACL",
"models": "মডল",
"syncState": "সিক সট",
"created": "তি",
"sqlOutput": "এসকিউএল আউটপট",
"addOption": "বিকলপ যগ করন",
"aggregateFunction": "সমগিক ফশন",
"database": "তথযশ",
"dbCreateIfNotExists": "ডস: উপসিত নকলি করন",
"clientKey": "কট ক",
"clientCert": "কট সট",
"serverCA": "সর সিএ",
"requriedCa": "পরযজন-সিএ",
"requriedIdentity": "পরযজন-পরিচয",
"inflection": {
"tableName": "পরতিছবি - টির নম",
"columnName": "পরতিছবি - কলর নম"
},
"community": {
"starUs1": "ত",
"starUs2": "আমর গি",
"bookDemo": "একটিিক করন",
"getAnswered": "আপনর পরশর উততর পন",
"joinDiscord": "ডিসকরগ দিন",
"joinCommunity": "NocoDB কমিউনিিগ দিন",
"joinReddit": "/r/NocoDB-এ যগ দিন",
"followNocodb": "NocoDB অনসরণ করন"
},
"docReference": "Document Reference",
"selectUserRole": "বযবহরকর ভিিচন করন",
"childTable": "Child table",
"childColumn": "Child column",
"onUpdate": "আপড",
"onDelete": "ডিিটএ"
},
"activity": {
"createProject": "পরকলপ তি করন",
"importProject": "আমদিরকলপ",
"searchProject": "অনসনন পরকলপ",
"editProject": "পরকলপ সমদন করন",
"stopProject": "পরকলপ বনধ করন",
"startProject": "পরকলপ শ করন",
"restartProject": "পরকলপ পনর করন",
"deleteProject": "পরকলপ মন",
"refreshProject": "রিশ পরকলপগি",
"saveProject": "সরকষণ পরকলপ",
"createProjectExtended": {
"extDB": "একটিিক ড <br> সত করি করন",
"excel": "একল থরকলপ তি করন",
"template": "টমপট থরকলপ তি করন"
},
"OkSaveProject": "ঠিক আছ এবরকলপ সরকষণ করন",
"upgrade": {
"available": "পরত উননত সকরণ",
"releaseNote": "অবহতি পতর",
"howTo": "কি আপগড করবন?"
},
"translate": "অনদ করত সহ করন",
"account": {
"authToken": "অনিি আথ টন",
"swagger": "সর এপিস ডক",
"projInfo": "পরকলর তথয অনিি করন",
"themes": "থিম"
},
"sort": "সন",
"addSort": "সর বিকলপ যত করন",
"filter": "ছকনি",
"addFilter": "ফির যত করন",
"share": "শর",
"shareBase": {
"disable": "ভগ করস অকষম করন",
"enable": "লিক সহ যউ",
"link": "ভগ করস লিক"
},
"invite": "আমনরণ",
"inviteMore": "আরও আমনরণ",
"inviteTeam": "দলক আমনরণ করন",
"inviteToken": "টনক আমনরণ করন",
"newUser": "নতন বযবহরক",
"editUser": "বযবহরক সমদন করন",
"deleteUser": "পরকলপ থযবহরক সরন",
"resendInvite": "आमरण ईमल द",
"copyInviteURL": "অনিি ইউআরএল আমনরণ করন",
"newRole": "নতন ভি",
"reloadRoles": "পনরড ভি",
"nextPage": "পরবর",
"prevPage": "আগর প",
"nextRecord": "পরবরকরড",
"previousRecord": "পববরকরড",
"copyApiURL": "অনিি এপিআই ইউআরএল",
"createTable": "টিল তি",
"refreshTable": "টিল রিশ",
"renameTable": "টিল নম পরিবরতন",
"deleteTable": "टबल मि",
"addField": "এই টি নতন কর যত করন",
"setPrimary": "পথমিক মন হিট করন",
"addRow": "নতন সিত করন",
"saveRow": "সিরকষণ করন",
"insertRow": "নতন সিন",
"deleteRow": "সিন",
"deleteSelectedRow": "নিিত সিিন",
"importExcel": "একল আমদি করন",
"importCSV": "Import CSV",
"downloadCSV": "সিএসভিিউনলড করন",
"uploadCSV": "সিএসভি আপলড করন",
"import": "আমদি",
"importMetadata": "আমদি",
"exportMetadata": "রফতি",
"clearMetadata": "পরির ম",
"exportToFile": "ফইল রফতি",
"changePwd": "পসওযড পরিবরতন করন",
"createView": "একটিয তি করন",
"shareView": "শর ভিউ",
"listSharedView": "ভগ করিউ তি",
"ListView": "ভিউ তি",
"copyView": "অনিিন",
"renameView": "ভিউ নমকরণ",
"deleteView": "দয মন",
"createGrid": "গিড ভিউ তি করন",
"createGallery": "গিউ তি করন",
"createCalendar": "কর ভিউ তি করন",
"createKanban": "কনবন ভিউ তি করন",
"createForm": "ফরম ভিউ তি করন",
"showSystemFields": "সিম করগিন",
"copyUrl": "কপি ইউআরএল",
"openTab": "নতন টব খন",
"iFrame": "এমডযয এইচটিএমএল কড অনিি করন",
"addWebhook": "নতন ওযবহক যত করন",
"newToken": "নতন টন যত করন",
"exportZip": "রফতিিপ",
"importZip": "আমদিিপ",
"metaSync": "এখন সিক",
"settings": "সিস",
"previewAs": "পবরপ হি",
"resetReview": "পবরপ পনরট করন",
"testDbConn": "টট ডস সগ",
"removeDbFromEnv": "পরিশ থস সরন",
"editConnJson": "সগ json সমদন",
"sponsorUs": "আমর সপনসর",
"sendEmail": "ইমইল পন"
},
"tooltip": {
"saveChanges": "परिवरतनरकित कर",
"xcDB": "একটি নতন পরকলপ তি করন",
"extDB": "মইএসকিউএল, পটগসকিউএল, এসকিউএল সর এব এসকিউএলইট সমরথন কর",
"apiRest": "REST API এর মযমসযয",
"apiGQL": "গফকিউএল এপিআইযর মযমসযয",
"theme": {
"dark": "এটি আস (^⇧b)",
"light": "এটি আস? (^⇧b)"
},
"addTable": "নতন টিল যত করন",
"inviteMore": "আরও বযবহরকর আমনরণ করন",
"toggleNavDraw": "টগল নিশন ডরযর",
"reloadApiToken": "এপিআই টনগিনরড করন",
"generateNewApiToken": "নতন এপিআই টন তি করন",
"addRole": "নতন ভিত করন",
"reloadList": "পনরড তি",
"metaSync": "সিক ম",
"sqlMigration": "সতর পনরড",
"updateRestart": "আপডট এবনর করন",
"cancelReturn": "বিল এবি",
"exportMetadata": "মিলগিিটরি সমসত ম রফতি করন।",
"importMetadata": "মিটরিিলগি সমসত ম আমদি করন।",
"clearMetadata": "মিলগি সমসত মফ করন।",
"clientKey": ".key ফইল নিচন করন",
"clientCert": ".cert ফইল নিচন করন",
"clientCA": "সিএ ফইল নিচন করন"
},
"placeholder": {
"projName": "পরকলর নম লিন",
"password": {
"enter": "পসওযড লিন",
"current": "বরতমন পসওযড",
"new": "নতন পসওযড",
"save": "পসওযড সরকষণ",
"confirm": "নিিত কর নতন গপননমবর"
},
"searchProjectTree": "টিল অনসনন করন",
"searchFields": "কর অনসনন করন",
"searchColumn": "অনসনন {অনসনন} কলম",
"searchApps": "অনসনন অিশন",
"searchModels": "অনসনন মডল",
"noItemsFound": "কনও আইটম পওযি",
"defaultValue": "ডিফলট মন",
"filterByEmail": "ই-মইল দির"
},
"msg": {
"info": {
"footerInfo": "পরতিি",
"upload": "আপলড করতইল নিচন করন",
"upload_sub": "অথবইল ট আনন",
"excelSupport": "সমরিত: .xls, .xlsx, .xlsm, .ods, .ots",
"excelURL": "একল ফইল ইউআরএল লিন",
"csvURL": "CSV ফইলর URL লিন",
"footMsg": "# সিির জনয ডইপটি অনন করতস",
"excelImport": "শট (গি) আমদির জনয উপলবধ",
"exportMetadata": "আপনিিিলগি রফতি করতন?",
"importMetadata": "আপনিিিলগি আমদি করতন?",
"clearMetadata": "আপনিিিলগিফ করতন?",
"projectEmptyMessage": "একটি নতন পরকলপ তি কর করন",
"stopProject": "আপনিিরকলপটি বনধ করতন?",
"startProject": "আপনিিরকলপটি করতন?",
"restartProject": "আপনিিরকলপটিনর করতন?",
"deleteProject": "আপনিিরকলপটিছতন?",
"shareBasePrivate": "সরবজননভগযয পঠনযয বস উতপনন করন",
"shareBasePublic": "এই লিকটি সহ ইনরনউ দখতন",
"userInviteNoSMTP": "দ মন হচ আপনি এখনও মর কনফির করন নি! উপরর আমনরণ লিকটি অনিি করন এব এটিরণ করন",
"dragDropHide": "লর জনয এখ আনন এবরপ করন",
"formInput": "ফরম ইনপট লল পরবশ করন",
"formHelpText": "কি সহয যত করন",
"onlyCreator": "কবল সরষর কযমন",
"formDesc": "ফরম বরণনগ করন",
"beforeEnablePwd": "একটিসওযড দিস সবদধ করন",
"afterEnablePwd": "অস পসওযড সবদধ",
"privateLink": "এই ভিউটি একটিযকিগত লির মযমগ কর হয",
"privateLinkAdditionalInfo": "বযকিগত লিকযত লবল এই দষগিযমন দখত",
"afterFormSubmitted": "ফরম জমওযর পর",
"apiOptions": "মযমস পরকলপ",
"submitAnotherForm": "অনয ফরম জমিন' বমটিন",
"showBlankForm": "5 সড পর একটি ফরম দন",
"emailForm": "আম ইমল করন",
"showSysFields": "সিম করগিন",
"filterAutoApply": "অটরযগ",
"showMessage": "এই বিন",
"viewNotShared": "বরতমন ভিউ ভগ কর হয!",
"showAllViews": "এই টির সমসত ভগ কর দরশন দন",
"collabView": "সমদন অনমতি উচচতর সহ সহযিউ কনফিশন পরিবরতন করত।",
"lockedView": "এটি আনলক ন হওয পরযনত কউ ভিউ কনফিশন সমদন করত।",
"personalView": "কবলমর আপনিিউ কনফিশন সমদন করতন। অনয সহযর বযকিগত মতমত ডিফলটর।",
"ownerDesc": "সরষত/অপসরণ করতন। এব সমণ সমদনস সকচর এবরগি।",
"creatorDesc": "ডস ক এবনগি সমণর সমদন করত।",
"editorDesc": "রকরডগি সমদন করত তবস/করগির ক পরিবরতন করত।",
"commenterDesc": "রকরডগিখত এব মনতবয করত তবি সমদন করত",
"viewerDesc": "রকরডগিখত তবি সমদন করত",
"addUser": "নতন বযবহরকত করন",
"staticRoleInfo": "সিম সিত ভি সমদন কর",
"exportZip": "জিপ ফইল এবউনলরকলপ ম রফতি করন।",
"importZip": "পরকলপ মিপ ফইল আমদি করন এবনর করন।",
"importText": "মিপ ফইল আপলড করকডিিরকলপটি আমদি করন",
"metaNoChange": "কনও পরিবরতন চিিত কর হযি",
"sqlMigration": "সিইগশনগিবযিি কর হব। একটিিল তি করন এব এই পিিশ করন।",
"dbConnectionStatus": "পরিশ বধ",
"dbConnected": "সগ সফল ছিল",
"notifications": {
"no_new": "কনও নতন বিঞপিই",
"clear": "সপষট"
},
"sponsor": {
"header": "আপনি আমর সয করতন!",
"message": "আমরিি ওপন সস করর জনয প সমযজ করছি এমন একটির দল। আমরিস করিিির মত একটি সরঞম ইনরনরতিি সমস সমনক অব উপলবধ হওয উচিত।"
},
"loginMsg": "Nocodb এ লগ ইন করন",
"passwordRecovery": {
"message_1": "আপনিইন আপ করর সময দয কর আপনি ইমল ঠিিযবহর করন ত সরবরহ করন।",
"message_2": "আমর আপনর পসওযডটিনরট করত একটিিক সহ একটি ইমল পরণ করব।",
"success": "পসওযডটিনরট করত আপনর ইমলটি পর করন"
},
"signUp": {
"superAdmin": "আপনি 'সর অডমিন' হবন",
"alreadyHaveAccount": "ইতিমধ একটি সদসযপদ আছ ?",
"workEmail": "আপনর কর ইমল লিন",
"enterPassword": "আপনর পসওযড লিন",
"forgotPassword": "আপনিিসওযড ভন ?",
"dontHaveAccount": "অউনট নই?"
},
"addView": {
"grid": "গিড ভিউ যত করন",
"gallery": "গিউ যত করন",
"form": "ফরম ভিউ যগ করন",
"kanban": "কনবন ভিউ যত করন",
"calendar": "কর ভিউ যত করন"
},
"tablesMetadataInSync": "টির মিক কর আছ",
"addMultipleUsers": "आप एकिक COMMA (,) द अलग ईमल जड़ सकत",
"enterTableName": "টির নম লিন",
"addDefaultColumns": "ডিফলট কলম যগ করন",
"tableNameInDb": "ডরকিত টির নম"
},
"error": {
"searchProject": "আপনর অনসনন {search} এর জনয কনও ফলফল পওযি",
"invalidChar": "ফর পথ অবধ চরির।",
"invalidDbCredentials": "অবধ ডস শপতরগি।",
"unableToConnectToDb": "ডর সগ করত অকষম, অনরহ কর পর করন আপনর ডস আপ আছ।",
"userDoesntHaveSufficientPermission": "বযবহরকর অসিব নই বিি করর জনয পরত অনমতিই।",
"dbConnectionStatus": "অবধ ডস পরিি",
"dbConnectionFailed": "সগ বিিন:",
"signUpRules": {
"emailReqd": "ই-মইল পরযজন",
"emailInvalid": "ইমল বধ হত হব",
"passwdRequired": "পসওযড পরযজন",
"passwdLength": "আপনর পসওযড অবশযই কমপক 8 টি অকষর হত হব",
"passwdMismatch": "পসওযড মিলছ"
}
},
"toast": {
"exportMetadata": "পরকলপ ম সফলভ রফতি কর",
"importMetadata": "পরকলপ ম সফলভ আমদি কর",
"clearMetadata": "পরকলপ ম সফলভফ কর",
"stopProject": "পরকলপ সফলভ বনধ",
"startProject": "পরকলপ সফলভ হযিল",
"restartProject": "পরকলপ সফলভনর হয",
"deleteProject": "পরকলপ সফলভ হয",
"authToken": "কিপব AUTH টন ক কপি কর হয়",
"projInfo": "কিপবরজট সমবন তথয কপি কর হয়",
"inviteUrlCopy": "আমণরণর url কিপব কপি কর হয়",
"createView": "দয সফলভি কর হয",
"formEmailSMTP": "ইমল বিঞপি সকষম করর জনয দয করপ স এসএমটিিগইনটি সকি করন",
"collabView": "সফলভ সহযিউতইচ কর",
"lockedView": "সফলভ লক ভিউতইচ কর",
"futureRelease": "শরই আসছ!"
}
}
}

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

@ -0,0 +1,522 @@
{
"general": {
"home": "घर",
"load": "भर",
"open": "खआ",
"close": "बद करन",
"yes": "ह",
"no": "नह",
"ok": "ठक ह",
"and": "और",
"or": "य",
"add": "ज",
"edit": "सदन करन",
"remove": "हट",
"save": "बच",
"cancel": "रदद करन",
"submit": "परसत करन",
"create": "सजन करन",
"insert": "Insert",
"delete": "मि",
"update": "अदयतन",
"rename": "नम बदल",
"reload": "पड कर",
"reset": "रट",
"install": "सित करन",
"show": "परदरशन",
"hide": "छि",
"showAll": "सब दि",
"hideAll": "सभि",
"showMore": "और दिओ",
"showOptions": "विकलप दि",
"hideOptions": "विकलप छि",
"showMenu": "मिओ",
"hideMenu": "मि",
"addAll": "सभ",
"removeAll": "सभ हट",
"signUp": "सइन अप कर",
"signIn": "सइन इन कर",
"signOut": "सइन आउट",
"required": "आवशयक",
"preferred": "पस",
"mandatory": "अनिय",
"loading": "लड ह रह ...",
"title": "शषक",
"upload": "डलन",
"download": "निलन",
"default": "Default",
"more": "ज",
"less": "कम",
"event": "घटन",
"condition": "शरत",
"after": "बद म",
"before": "पहल",
"search": "ख",
"notification": "सित कर",
"reference": "सदरभ",
"function": "समह"
},
"objects": {
"project": "परिजन",
"projects": "परिजन",
"table": "मज",
"tables": "टबल",
"field": "खत",
"fields": "खत",
"column": "कलम",
"columns": "कलम",
"page": "पठ",
"pages": "प",
"record": "अभिख",
"records": "अभिख",
"webhook": "Webhook",
"webhooks": "Webhooks",
"view": "रय",
"views": "वि",
"viewType": {
"grid": "जल",
"gallery": "गलर",
"form": "परपतर",
"kanban": "Kanban",
"calendar": "पग"
},
"user": "उपयगकर",
"users": "उपयगकर",
"role": "भि",
"roles": "भि",
"roleType": {
"owner": "स",
"creator": "बन",
"editor": "सदक",
"commenter": "टिपणर",
"viewer": "दरशक"
}
},
"datatype": {
"ID": "पहचन",
"ForeignKey": "वि",
"SingleLineText": "एकल पिठ",
"LongText": "लठ",
"Attachment": "अनरकि",
"Checkbox": "चक बस",
"MultiSelect": "बह चयन",
"SingleSelect": "एकल चयन",
"Collaborator": "सहय",
"Date": "दिक",
"Year": "सल",
"Time": "समय",
"PhoneNumber": "फन नबर",
"Email": "ईमल",
"URL": "यआरएल",
"Number": "स",
"Decimal": "दशमलव",
"Currency": "म",
"Percent": "परतिशत",
"Duration": "अवधि",
"Rating": "रिग",
"Formula": "सर",
"Rollup": "जमन",
"Count": "गिनत करन",
"Lookup": "द",
"DateTime": "दिक और समय",
"CreateTime": "निण क समय",
"LastModifiedTime": "अिम सित समय",
"AutoNumber": "वहन नबर",
"Barcode": "बरकड",
"Button": "बटन",
"Password": "पसवरड",
"relationProperties": {
"noAction": "No Action",
"cascade": "Cascade",
"restrict": "र",
"setNull": "Set NULL",
"setDefault": "Set Default"
}
},
"filterOperation": {
"isEqual": "बरबर ह",
"isNotEqual": "समन नह",
"isLike": "क समन ह",
"isNot like": "पसद नह",
"isEmpty": "ख",
"isNotEmpty": "ख नह",
"isNull": "शय ह",
"isNotNull": "निररथक नह"
},
"title": {
"newProj": "नयम",
"myProject": "म परिजन",
"formTitle": "परपतर शषक",
"collabView": "सहयमक दय",
"lockedView": "बद दय",
"personalView": "वयकिगत दिण",
"appStore": "ऐप सर",
"teamAndAuth": "टम और पिक",
"rolesUserMgmt": "भि और उपयगकररबधन",
"userMgmt": "उपयगकररबधन",
"apiTokenMgmt": "एपआई टकन परबधन",
"rolesMgmt": "भिरबधन",
"projMeta": "पट म",
"metaMgmt": "मरबधन",
"metadata": "म",
"exportImportMeta": "नित म",
"uiACL": "UI Access Control",
"metaOperations": "मलन",
"audit": "अषण",
"auditLogs": "हिब सि",
"sqlMigrations": "SQL परवसन",
"dbCredentials": "डस सख",
"advancedParameters": "SSL और उननत पटर",
"headCreateProject": "पट बन | नडब",
"headLogin": "लग इन | नडब",
"resetPassword": "अपनसवरड रट कर",
"teamAndSettings": "दल एविस",
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
},
"labels": {
"notifyVia": "Notify Via",
"projName": "परिजनम",
"tableName": "Table name",
"viewName": "नम द",
"viewLink": "लिक द",
"columnName": "आम नम",
"columnType": "सभ परकर",
"roleName": "भिम",
"roleDescription": "भििवरण",
"databaseType": "डस मइप कर",
"lengthValue": "लई/ मय",
"dbType": "डस परकर",
"sqliteFile": "Sqlite फइल",
"hostAddress": "मन क पत",
"port": "पट स",
"username": "उपयगकरम",
"password": "पसवरड",
"schemaName": "Schema name",
"action": "गतििि",
"actions": "करवई",
"operation": "सलन",
"operationType": "परचलन परकर",
"operationSubType": "परचलन उप-परकर",
"description": "विवरण",
"authentication": "परमकरण",
"token": "टकन",
"where": "कह",
"cache": "कश",
"chat": "बत करन",
"email": "ईमल",
"storage": "भरण",
"uiAcl": "UI-ACL",
"models": "मडल",
"syncState": "सिक अवस",
"created": "बन",
"sqlOutput": "SQL आउटपट",
"addOption": "विकलप ज",
"aggregateFunction": "कल समह",
"database": "डस",
"dbCreateIfNotExists": "डस: बन यदिद नह",
"clientKey": "गहक क",
"clientCert": "गहक परमण पतर",
"serverCA": "सरवर सए",
"requriedCa": "आवशयक सए",
"requriedIdentity": "अपित-पहचन",
"inflection": {
"tableName": "विभकि - तिम",
"columnName": "विभकि - सभ नम"
},
"community": {
"starUs1": "सि",
"starUs2": "हम GitHub पर",
"bookDemo": "एक मत डक कर",
"getAnswered": "अपन सव जवब द",
"joinDiscord": "डिड मिल ह",
"joinCommunity": "NocoDB क समय स",
"joinReddit": "/r/NocoDB म",
"followNocodb": "NocoDB स"
},
"docReference": "Document Reference",
"selectUserRole": "Select User Role",
"childTable": "Child table",
"childColumn": "Child column",
"onUpdate": "On Update",
"onDelete": "On Delete"
},
"activity": {
"createProject": "पट बन",
"importProject": "आयत परिजन",
"searchProject": "खज परिजन",
"editProject": "पट सित कर",
"stopProject": "बद पट",
"startProject": "पट श कर",
"restartProject": "पट कनरभ करन",
"deleteProject": "पट हट",
"refreshProject": "तस",
"saveProject": "परिजनरकित कर",
"createProjectExtended": {
"extDB": "बहरस स <br> कनट करक बन",
"excel": "एकल सट बन",
"template": "टपलट सट बन"
},
"OkSaveProject": "ठक ह और पट सह",
"upgrade": {
"available": "अपगड उपलबद ह",
"releaseNote": "रिज नस",
"howTo": "अपगड क कर?"
},
"translate": "अनद करन मदद कर",
"account": {
"authToken": "किक टकन",
"swagger": "सगर एपआईएस डक",
"projInfo": "कट जनक",
"themes": "विषय"
},
"sort": "करम स लग",
"addSort": "सट विकलप ज",
"filter": "फिटर",
"addFilter": "फिटर ज",
"share": "शयर करन",
"shareBase": {
"disable": "स आधर अकषम कर",
"enable": "कई भयकििसकस लिक ह",
"link": "स आधर लिक"
},
"invite": "आमित करन",
"inviteMore": "अधिक आमित कर",
"inviteTeam": "टम क आमित कर",
"inviteToken": "टकन क आमित कर",
"newUser": "नय उपयगकर",
"editUser": "यजर कित कर",
"deleteUser": "परिजन उपयगकरि",
"resendInvite": "आमरण ईमल द",
"copyInviteURL": "क आमित URL",
"newRole": "नयि",
"reloadRoles": "पड भि",
"nextPage": "अगलठ",
"prevPage": "पिछलठ",
"nextRecord": "अगलिड",
"previousRecord": "पिछलिड",
"copyApiURL": "API URL क कर",
"createTable": "ति बन",
"refreshTable": "टबलस रिश",
"renameTable": "तिम",
"deleteTable": "टबल मि",
"addField": "इस ति नयड ज",
"setPrimary": "पथमिक मय कप मट कर",
"addRow": "नई पि",
"saveRow": "पि सह",
"insertRow": "नई पि",
"deleteRow": "पि हट",
"deleteSelectedRow": "चयनित पि हट",
"importExcel": "आयत एकल",
"importCSV": "Import CSV",
"downloadCSV": "CSV कप मउनलड कर",
"uploadCSV": "CSV अपलड कर",
"import": "आयत",
"importMetadata": "आयत म",
"exportMetadata": "नित म",
"clearMetadata": "सपषट म",
"exportToFile": "फइल कित",
"changePwd": "पसवरड बदल",
"createView": "एक दय बन",
"shareView": "शयर दय",
"listSharedView": "सय स",
"ListView": "दय स",
"copyView": "परतिििय",
"renameView": "नम बदल",
"deleteView": "डिट व",
"createGrid": "गिड व बन",
"createGallery": "गलरय बन",
"createCalendar": "कडर दय बन",
"createKanban": "कनबन व बन",
"createForm": "फम व बन",
"showSystemFields": "सिटम फड दि",
"copyUrl": "यआरएल क कर",
"openTab": "नयब ख",
"iFrame": "एमबल HTML कड क कर",
"addWebhook": "नयबहक ज",
"newToken": "नयकन ज",
"exportZip": "नित जिप",
"importZip": "आयत जिप",
"metaSync": "अभिक कर",
"settings": "समजन",
"previewAs": "कप मवलकन",
"resetReview": "रट पवलकन",
"testDbConn": "परषण डस कनशन",
"removeDbFromEnv": "परवरण सस नि",
"editConnJson": "कनशन JSON सित कर",
"sponsorUs": "हमित कर",
"sendEmail": "ईमल भ"
},
"tooltip": {
"saveChanges": "परिवरतनरकित कर",
"xcDB": "एक नयट बन",
"extDB": "MySQL, PostgreSQL, SQL सरवर और SQLite क समरथन करत",
"apiRest": "रट एपआई कयम सलभ",
"apiGQL": "गफकल एपआई कयम सलभ",
"theme": {
"dark": "यह कग म आत (^⇧B)",
"light": "क यह कग म आत? (^⇧b)"
},
"addTable": "नई ति",
"inviteMore": "अधिक उपयगकर आमित कर",
"toggleNavDraw": "निशन दरज कगल कर",
"reloadApiToken": "एपआई टकन कड कर",
"generateNewApiToken": "नय एपआई टकन उतपनन कर",
"addRole": "नई भि",
"reloadList": "पड स",
"metaSync": "सिक म",
"sqlMigration": "पनरित पलयन",
"updateRestart": "अदयतन और पनरभ कर",
"cancelReturn": "रदद कर और वपस ल",
"exportMetadata": "मबल स सभििित कर।",
"importMetadata": "मििबल तक सभ आयत कर।",
"clearMetadata": "मबल स सभफ कर।",
"clientKey": ".Key फइल क चयन कर",
"clientCert": ".Cert फइल क चयन कर",
"clientCA": "सए फइल क चयन कर"
},
"placeholder": {
"projName": "पट नम दरज कर",
"password": {
"enter": "पसवरड दरज कर",
"current": "वरतमन पसवरड",
"new": "नयसवरड",
"save": "पसवरड क बचओ",
"confirm": "नए पसवरड कि कर"
},
"searchProjectTree": "खज टबल",
"searchFields": "खज फड",
"searchColumn": "खज {खज} कलम",
"searchApps": "खज एपिशन",
"searchModels": "खज मडल",
"noItemsFound": "कछ नहि",
"defaultValue": "डिट मन",
"filterByEmail": "ई-मल दिटर"
},
"msg": {
"info": {
"footerInfo": "परतिठ पि",
"upload": "अपलड करनिए फइल क चयन कर",
"upload_sub": "यग एड डप फइल",
"excelSupport": "समरित: .xls, .xlsx, .xlsm, .ods, .ots",
"excelURL": "Excel फइल URL दरज कर",
"csvURL": "csv फइल क url दरज कर",
"footMsg": "# पिन डइप करनिए पस करनिए",
"excelImport": "शट आयत किए उपलबध ह",
"exportMetadata": "क आप मबल सित करनहत?",
"importMetadata": "क आप मबल स आयत करनहत?",
"clearMetadata": "क आप मबल सफ करनहत?",
"projectEmptyMessage": "एक नई परिजन बनकर आरभ कर",
"stopProject": "क आप परिजनकनहत?",
"startProject": "क आप पट श करनहत?",
"restartProject": "क आप परिजनिर स करनहत?",
"deleteProject": "क आप परिजन हटहत?",
"shareBasePrivate": "सवजनिक रप स करनय रनलस उतपनन कर",
"shareBasePublic": "इस लिक कथ इटरनट पर कई भयकिख सकत",
"userInviteNoSMTP": "लगति आपन अभ तक मलर किगर नहि! कपय ऊपर दिए गए लिक क ऊपर क कर और इस",
"dragDropHide": "छििए यहड क और डप कर",
"formInput": "फम इनपट लबल दरज कर",
"formHelpText": "कछ सहयतठ ज",
"onlyCreator": "कवल नििई द",
"formDesc": "फम विवरण ज",
"beforeEnablePwd": "एक पसवरड कथ पहच करतिित कर",
"afterEnablePwd": "एकस पसवरड परतिित ह",
"privateLink": "यह दय एक नििक कयम सि गय",
"privateLinkAdditionalInfo": "नििक वग कवल इस दय मिई दिख सकत",
"afterFormSubmitted": "फम कद परसत कि",
"apiOptions": "कयम सट",
"submitAnotherForm": "एक और फम सबमिट कर' बटन दि",
"showBlankForm": "5 सड कद एक खम दि",
"emailForm": "म ई-मल कर",
"showSysFields": "सिटम फड दि",
"filterAutoApply": "ऑट कर",
"showMessage": "यह सश दि",
"viewNotShared": "वरतमन दय स नह!",
"showAllViews": "इस ति सभिर दि",
"collabView": "सदन अनमति उचचतर कथ सहयय किगरशन क बदल सकत।",
"lockedView": "कई भय किगरशन क तब तक सित नह कर सकत जब तक कि यह अनलक न हए।",
"personalView": "कवल आप दय किगरशन कित कर सकत। अनय सहयियकिगत विर डिट रप सिए ह।",
"ownerDesc": "रचन/हट सकत। और पण सदन डस सरचन और फड।",
"creatorDesc": "डस सरचन और म तरह सित कर सकत।",
"editorDesc": "रिड कित कर सकतिन डस/फड करचन नह बदल सकत।",
"commenterDesc": "रिड दख सकत और टिपण कर सकतिन कछ भित नह कर सकत",
"viewerDesc": "रिड दख सकतिन कछ भित नह कर सकत",
"addUser": "नई उपयगकर",
"staticRoleInfo": "सिटम परिित भिित नहि सकत",
"exportZip": "फइल और डउनलड करनिए पट मित कर।",
"importZip": "आयत परिजनिप फइल और पनरभ।",
"importText": "मिप फइल अपलड करक NOCODB परिजन आयत कर",
"metaNoChange": "कई परिवरतन नह पहचन",
"sqlMigration": "सइगशन सवचित रप स बनए ज। एक ति बन और इस पठ क कर।",
"dbConnectionStatus": "मय परवरण",
"dbConnected": "कनशन सफल रह",
"notifications": {
"no_new": "कई नए सश नह",
"clear": "स"
},
"sponsor": {
"header": "आप हम मदद कर सकत!",
"message": "हम एक छम ह NOCODB क ओपन-सस बनिए प समय कम कर रह। हमनन ि एनओसओड उपकरण कटरनट पर हर समसिए सवतर रप स उपलबध हिए।"
},
"loginMsg": "NoCODB मग इन कर",
"passwordRecovery": {
"message_1": "जब आप सइन अप करतपय आपक उपयग किए गए ईमल पतरदन कर।",
"message_2": "हम आपक अपनसवरड रट करनिए एक लिक कथ एक ईमल भ।",
"success": "कपयसवरड रट करनिए अपन ईमल द"
},
"signUp": {
"superAdmin": "आप 'सपर एडमिन' ह",
"alreadyHaveAccount": "क आपकस पहल एक खद ह ?",
"workEmail": "अपनम ईमल दरज कर",
"enterPassword": "अपनसवरड ड",
"forgotPassword": "पसवरड भल गए ह ?",
"dontHaveAccount": "कई ख नह?"
},
"addView": {
"grid": "गिड दय ज",
"gallery": "गलरय ज",
"form": "फम व",
"kanban": "कनबन दय ज",
"calendar": "कडर दय ज"
},
"tablesMetadataInSync": "टबल क SYNC ह",
"addMultipleUsers": "आप एकिक COMMA (,) द अलग ईमल जड़ सकत",
"enterTableName": "टबल कम लि",
"addDefaultColumns": "डिट कलम ज",
"tableNameInDb": "डस मबल कम बच गय"
},
"error": {
"searchProject": "{search} किए आपकज कई परिम नहि",
"invalidChar": "फडर पथ म अमय चरिर।",
"invalidDbCredentials": "अमय डस कियलस।",
"unableToConnectToDb": "डस स कनट करन असमरथ, कपयच करि आपकस ऊपर ह।",
"userDoesntHaveSufficientPermission": "उपयगकरद नह बनिए परत अनमति।",
"dbConnectionStatus": "अमय डस पटर",
"dbConnectionFailed": "कनशन विफलत:",
"signUpRules": {
"emailReqd": "ईमल क जररत ह",
"emailInvalid": "ईमल मय हिए",
"passwdRequired": "पसवरड क आवशयकत",
"passwdLength": "आप पसवरड कम स कम 8 वरण हिए",
"passwdMismatch": "पसवरड मल नह"
}
},
"toast": {
"exportMetadata": "परिजन सफलतवक नित क गई",
"importMetadata": "परिजन सफलतवक आयत क गई",
"clearMetadata": "पट म सफलतवक म",
"stopProject": "परिजन सफलतवक बद ह गई",
"startProject": "परिजन सफलतवक शई",
"restartProject": "परिजन सफलतवक फिर सि गय",
"deleteProject": "परिजन सफलतवक हटि गय",
"authToken": "किपबड म AUTH टकन कि गय",
"projInfo": "किपबड मट कनक गय",
"inviteUrlCopy": "आमरण url किपबड मि गय",
"createView": "व सफलतवक बन गय",
"formEmailSMTP": "कपय ईमल अधिचन सकषम करनिए ऐप सर म SMTP पलगइन क सकिय कर",
"collabView": "सफलतवक सहयय पर सिच कि गय",
"lockedView": "सफलतवक लक किए गए दय पर सिच कि गय",
"futureRelease": "जलद आ रह!"
}
}
}

4
packages/nc-gui/plugins/i18n.js

@ -44,7 +44,9 @@ export default ({ app, store }) => {
sl: require('~/lang/sl.json'),
pt_BR: require('~/lang/pt_BR.json'),
fa: require('~/lang/fa.json'),
tr: require('~/lang/tr.json')
tr: require('~/lang/tr.json'),
hi: require('~/lang/hi.json'),
bn: require('~/lang/bn.json')
}
})

7
packages/nocodb/src/lib/meta/api/sync/helpers/job.ts

@ -945,7 +945,10 @@ export default async (
nestedLookupTbl[0].typeOptions.foreignTableRollupColumnId
);
if (ncLookupColumnId === undefined) {
if (
ncLookupColumnId === undefined ||
ncRelationColumnId === undefined
) {
continue;
}
@ -1146,7 +1149,7 @@ export default async (
nestedLookupTbl[0].typeOptions.foreignTableRollupColumnId
);
if (ncLookupColumnId === undefined) {
if (ncLookupColumnId === undefined || ncRelationColumnId === undefined) {
continue;
}

2
scripts/cypress/integration/common/6d_language_validation.js

@ -44,6 +44,7 @@ export const genTest = (apiType, dbType) => {
};
let langMenu = [
"bn.json",
"da.json",
"de.json",
"en.json",
@ -51,6 +52,7 @@ export const genTest = (apiType, dbType) => {
"fa.json",
"fi.json",
"fr.json",
"hi.json",
"hr.json",
"id.json",
"it_IT.json",

Loading…
Cancel
Save