Browse Source

Merge branch 'develop' into fix/gui-v2-table-rename

pull/3117/head
Wing-Kam Wong 2 years ago
parent
commit
030907348a
  1. 8
      packages/nc-gui-v2/app.vue
  2. 57
      packages/nc-gui-v2/assets/style-v2.scss
  3. 6
      packages/nc-gui-v2/components.d.ts
  4. 12
      packages/nc-gui-v2/components/cell/Checkbox.vue
  5. 2
      packages/nc-gui-v2/components/cell/DatePicker.vue
  6. 2
      packages/nc-gui-v2/components/cell/DateTimePicker.vue
  7. 1
      packages/nc-gui-v2/components/cell/Json.vue
  8. 1
      packages/nc-gui-v2/components/cell/MultiSelect.vue
  9. 119
      packages/nc-gui-v2/components/cell/Percent.vue
  10. 1
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  11. 2
      packages/nc-gui-v2/components/cell/TimePicker.vue
  12. 37
      packages/nc-gui-v2/components/cell/Url.vue
  13. 4
      packages/nc-gui-v2/components/cell/YearPicker.vue
  14. 2
      packages/nc-gui-v2/components/cell/attachment/Carousel.vue
  15. 12
      packages/nc-gui-v2/components/cell/attachment/Modal.vue
  16. 46
      packages/nc-gui-v2/components/cell/attachment/index.vue
  17. 64
      packages/nc-gui-v2/components/cell/attachment/utils.ts
  18. 14
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  19. 18
      packages/nc-gui-v2/components/dashboard/settings/Modal.vue
  20. 5
      packages/nc-gui-v2/components/general/PreviewAs.vue
  21. 187
      packages/nc-gui-v2/components/shared-view/Form.vue
  22. 10
      packages/nc-gui-v2/components/shared-view/Grid.vue
  23. 3
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  24. 2
      packages/nc-gui-v2/components/smartsheet-column/PercentOptions.vue
  25. 2
      packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue
  26. 5
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  27. 9
      packages/nc-gui-v2/components/smartsheet-header/Menu.vue
  28. 5
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  29. 85
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  30. 23
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  31. 30
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  32. 17
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  33. 36
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  34. 50
      packages/nc-gui-v2/components/smartsheet/Form.vue
  35. 56
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  36. 8
      packages/nc-gui-v2/components/smartsheet/VirtualCell.vue
  37. 6
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  38. 39
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  39. 20
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/AddRow.vue
  40. 7
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/DeleteTable.vue
  41. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue
  42. 7
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/Reload.vue
  43. 13
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  44. 33
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  45. 14
      packages/nc-gui-v2/components/tabs/auth/UserManagement.vue
  46. 4
      packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue
  47. 6
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  48. 17
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  49. 2
      packages/nc-gui-v2/components/virtual-cell/Lookup.vue
  50. 16
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  51. 6
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  52. 7
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  53. 7
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  54. 1
      packages/nc-gui-v2/composables/index.ts
  55. 4
      packages/nc-gui-v2/composables/useColumn.ts
  56. 30
      packages/nc-gui-v2/composables/useLTARStore.ts
  57. 197
      packages/nc-gui-v2/composables/useSharedFormViewStore.ts
  58. 18
      packages/nc-gui-v2/composables/useSharedView.ts
  59. 22
      packages/nc-gui-v2/composables/useSidebar/index.ts
  60. 97
      packages/nc-gui-v2/composables/useSmartsheetStore.ts
  61. 62
      packages/nc-gui-v2/composables/useViewColumns.ts
  62. 4
      packages/nc-gui-v2/context/index.ts
  63. 9
      packages/nc-gui-v2/layouts/base.vue
  64. 17
      packages/nc-gui-v2/layouts/shared-view.vue
  65. 8
      packages/nc-gui-v2/lib/types.ts
  66. 5
      packages/nc-gui-v2/middleware/auth.global.ts
  67. 119
      packages/nc-gui-v2/package-lock.json
  68. 3
      packages/nc-gui-v2/package.json
  69. 32
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  70. 16
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/index.vue
  71. 38
      packages/nc-gui-v2/pages/[projectType]/form/[viewId].vue
  72. 26
      packages/nc-gui-v2/pages/[projectType]/view/[viewId].vue
  73. 2
      packages/nc-gui-v2/pages/forgot-password.vue
  74. 186
      packages/nc-gui-v2/pages/index/index.vue
  75. 2
      packages/nc-gui-v2/pages/signin.vue
  76. 19
      packages/nc-gui-v2/pages/signup/[[token]].vue
  77. 6
      packages/nc-gui-v2/utils/NcAutocompleteTree.ts
  78. 38
      packages/nocodb-sdk/src/lib/Api.ts
  79. 5
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  80. 114
      scripts/sdk/swagger.json

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

@ -1,5 +1,11 @@
<script setup lang="ts">
const route = useRoute()
const disableBaseLayout = $computed(() => route.path.startsWith('/nc/view') || route.path.startsWith('/nc/form'))
</script>
<template>
<NuxtLayout name="base">
<NuxtLayout :name="disableBaseLayout ? false : 'base'">
<NuxtPage />
</NuxtLayout>
</template>

57
packages/nc-gui-v2/assets/style-v2.scss

@ -17,6 +17,10 @@ main {
@apply m-0 h-full w-full bg-white dark:(bg-black text-white);
}
html {
overflow-y: auto !important;
}
main {
@apply flex-0 w-full relative scrollbar-thin-dull;
overflow-x: hidden;
@ -43,14 +47,23 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
@apply color-transition;
}
html {
overflow-y: auto !important;
}
// menu item styling
.nc-menu-item {
@apply cursor-pointer text-xs flex items-center gap-2 px-4 py-3 relative after:(content-[''] absolute top-0 left-0 bottom-0 w-full h-full right-0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
&.disabled {
@apply text-black text-opacity-25 bg-[#f5f5f5] cursor-not-allowed text-shadow-none box-shadow-none border-[#d9d9d9];
}
}
.nc-project-menu-item {
@apply cursor-pointer flex items-center gap-2 py-2 hover:text-primary after:(content-[''] absolute top-0 left-0 bottom-0 right-0 w-full h-full bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
&:hover {
.nc-icon {
@apply text-pink-500;
}
}
}
.nc-sidebar-right-item {
@ -65,9 +78,17 @@ html {
@apply ring shadow-2xl transform scale-110;
}
&.disabled-ring:hover::after {
@apply ring-0;
}
svg {
@apply z-1 text-xl p-1 text-gray-500;
}
.disabled {
@apply cursor-not-allowed border-none;
}
}
// show a dot badge if some change present
@ -80,6 +101,10 @@ html {
@apply bg-primary/20 hover:(bg-primary/20);
}
.nc-locked-overlay {
@apply absolute h-full w-full z-2 top-0 left-0;
}
.ant-modal-wrap {
@apply !scrollbar-thin-dull;
}
@ -136,9 +161,9 @@ html {
@apply transition-opacity duration-300 ease-in-out;
}
.page-enter,
.page-enter-active,
.page-leave-active,
.layout-enter,
.layout-enter-active,
.layout-leave-active {
@apply opacity-0;
}
@ -166,4 +191,22 @@ html {
.glow-enter,
.glow-leave-active {
@apply opacity-0;
}
.scaling-btn {
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white bg-primary;
&::after {
@apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
content: '';
z-index: -1;
}
&:hover::after {
@apply transform scale-110 ring ring-pink-500;
}
&:active::after {
@apply ring ring-pink-500;
}
}

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

@ -103,6 +103,7 @@ declare module '@vue/runtime-core' {
MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiClose: typeof import('~icons/mdi/close')['default']
MdiCloseBox: typeof import('~icons/mdi/close-box')['default']
MdiCloseCircle: typeof import('~icons/mdi/close-circle')['default']
MdiCloseThick: typeof import('~icons/mdi/close-thick')['default']
MdiCodeJson: typeof import('~icons/mdi/code-json')['default']
@ -127,16 +128,19 @@ declare module '@vue/runtime-core' {
MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default']
MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default']
MdiFileExcel: typeof import('~icons/mdi/file-excel')['default']
MdiFileEyeOutline: typeof import('~icons/mdi/file-eye-outline')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFlashOutline: typeof import('~icons/mdi/flash-outline')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiGithub: typeof import('~icons/mdi/github')['default']
MdiGridLarge: typeof import('~icons/mdi/grid-large')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
MdiKeyPlus: typeof import('~icons/mdi/key-plus')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLink: typeof import('~icons/mdi/link')['default']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
@ -152,9 +156,11 @@ declare module '@vue/runtime-core' {
MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiPlusBoxOutline: typeof import('~icons/mdi/plus-box-outline')['default']
MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default']
MdiPlusRoundedOutline: typeof import('~icons/mdi/plus-rounded-outline')['default']
MdiRefresh: typeof import('~icons/mdi/refresh')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextKeyOutline: typeof import('~icons/mdi/script-text-key-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']
MdiSearch: typeof import('~icons/mdi/search')['default']
MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default']

12
packages/nc-gui-v2/components/cell/Checkbox.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { ColumnInj, IsFormInj, getMdiIcon, inject } from '#imports'
import { EditModeInj } from '~/context'
import { ColumnInj, IsFormInj, ReadonlyInj, getMdiIcon, inject } from '#imports'
interface Props {
modelValue?: boolean | undefined | number
@ -20,7 +19,7 @@ const column = inject(ColumnInj)
const isForm = inject(IsFormInj)
const editEnabled = inject(EditModeInj)
const readOnly = inject(ReadonlyInj)
const checkboxMeta = $computed(() => {
return {
@ -34,14 +33,17 @@ const checkboxMeta = $computed(() => {
})
function onClick() {
if (editEnabled) {
if (!readOnly) {
vModel = !vModel
}
}
</script>
<template>
<div class="flex" :class="{ 'justify-center': !isForm, 'nc-cell-hover-show': !vModel }">
<div
class="flex"
:class="{ 'justify-center': !isForm, 'nc-cell-hover-show': !vModel && !readOnly, 'opacity-0': readOnly && !vModel }"
>
<div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel }" @click="onClick">
<component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"

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

@ -55,7 +55,7 @@ watch(
{ flush: 'post' },
)
const placeholder = computed(() => (isDateInvalid ? 'Invalid date' : readOnly ? 'Select date' : ''))
const placeholder = computed(() => (isDateInvalid ? 'Invalid date' : ''))
</script>
<template>

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

@ -64,7 +64,7 @@ watch(
:bordered="false"
class="!w-full px-1"
format="YYYY-MM-DD HH:mm"
:placeholder="isDateInvalid ? 'Invalid date' : !readOnly ? 'Select date and time' : ''"
:placeholder="isDateInvalid ? 'Invalid date' : ''"
:allow-clear="!readOnly"
:input-read-only="true"
:dropdown-class-name="randomClass"

1
packages/nc-gui-v2/components/cell/Json.vue

@ -41,7 +41,6 @@ const localValue = computed<string | Record<string, any> | undefined>({
},
})
const clear = () => {
error = undefined

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

@ -129,7 +129,6 @@ watch(isOpen, (n, _o) => {
v-model:value="vModel"
mode="multiple"
class="w-full"
:placeholder="!readOnly ? 'Select an option' : ''"
:bordered="false"
:show-arrow="!readOnly"
:show-search="false"

119
packages/nc-gui-v2/components/cell/Percent.vue

@ -1,69 +1,76 @@
<script setup lang="ts">
import { ColumnInj, computed, getPercentStep, inject, isValidPercent, renderPercent } from '#imports'
import { EditModeInj } from '~/context'
import { EditModeInj, inject } from '#imports'
interface Props {
modelValue: number | string | null | undefined
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
const column = inject(ColumnInj)
const percent = ref()
const isEdited = ref(false)
const percentType = computed(() => column?.value?.meta?.precision || 0)
const percentStep = computed(() => getPercentStep(percentType.value))
const localState = computed({
get: () => {
return renderPercent(modelValue, percentType.value, !isEdited.value)
},
set: (val) => {
if (val === null) val = 0
if (isValidPercent(val, column?.value?.meta?.negative)) {
percent.value = val / 100
}
},
})
function onKeyDown(evt: KeyboardEvent) {
isEdited.value = true
return ['e', 'E', '+', '-'].includes(evt.key) && evt.preventDefault()
}
function onBlur() {
if (isEdited.value) {
emit('update:modelValue', percent.value)
isEdited.value = false
}
}
function onKeyDownEnter() {
if (isEdited.value) {
emit('update:modelValue', percent.value)
isEdited.value = false
}
}
const vModel = useVModel(props, 'modelValue', emits)
</script>
<template>
<input
v-if="isEdited"
v-model="localState"
type="number"
:step="percentStep"
@keydown="onKeyDown"
@blur="onBlur"
@keydown.enter="onKeyDownEnter"
/>
<input v-if="editEnabled" v-model="localState" type="text" @focus="isEdited = true" />
<span v-else>{{ localState }}</span>
<input v-if="editEnabled" v-model="vModel" type="number" />
<span v-else>{{ vModel }}</span>
</template>
<!-- <script setup lang="ts">
import { ColumnInj, computed, getPercentStep, inject, isValidPercent, renderPercent } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number | string | null | undefined
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
const column = inject(ColumnInj)
const percent = ref()
const isEdited = ref(false)
const percentType = computed(() => column?.value?.meta?.precision || 0)
const percentStep = computed(() => getPercentStep(percentType.value))
const localState = computed({
get: () => {
return renderPercent(modelValue, percentType.value, !isEdited.value)
},
set: (val) => {
if (val === null) val = 0
if (isValidPercent(val, column?.value?.meta?.negative)) {
percent.value = val / 100
}
},
})
function onKeyDown(evt: KeyboardEvent) {
isEdited.value = true
return ['e', 'E', '+', '-'].includes(evt.key) && evt.preventDefault()
}
function onBlur() {
if (isEdited.value) {
emit('update:modelValue', percent.value)
isEdited.value = false
}
}
function onKeyDownEnter() {
if (isEdited.value) {
emit('update:modelValue', percent.value)
isEdited.value = false
}
}
</script>
<template>
<input
v-if="isEdited"
v-model="localState"
type="number"
:step="percentStep"
@keydown="onKeyDown"
@blur="onBlur"
@keydown.enter="onKeyDownEnter"
/>
<input v-if="editEnabled" v-model="localState" type="text" @focus="isEdited = true" />
<span v-else>{{ localState }}</span>
</template> -->

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

@ -69,7 +69,6 @@ watch(isOpen, (n, _o) => {
v-model:value="vModel"
class="w-full"
:allow-clear="!column.rqd && active"
:placeholder="!readOnly ? 'Select an option' : ''"
:bordered="false"
:open="isOpen"
:disabled="readOnly"

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

@ -75,7 +75,7 @@ watch(
use12-hours
format="HH:mm"
class="!w-full px-1"
:placeholder="isTimeInvalid ? 'Invalid time' : !readOnly ? 'Select time' : ''"
:placeholder="isTimeInvalid ? 'Invalid time' : ''"
:allow-clear="!readOnly"
:input-read-only="true"
:open="readOnly ? false : open"

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

@ -1,6 +1,8 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { message } from 'ant-design-vue'
import { ColumnInj, EditModeInj, computed, inject, isValidURL } from '#imports'
import MiCircleWarning from '~icons/mi/circle-warning'
interface Props {
modelValue?: string | null
@ -14,10 +16,14 @@ const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)!
// Used in the logic of when to display error since we are not storing the url if its not valid
const localState = ref(value)
const vModel = computed({
get: () => value,
set: (val) => {
if (!column.value.meta?.validate || (val && isValidURL(val))) {
localState.value = val
if (!column.value.meta?.validate || (val && isValidURL(val)) || !val) {
emit('update:modelValue', val)
}
},
@ -35,14 +41,37 @@ const url = computed(() => {
})
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,
() => {
if (column.value.meta?.validate && !editEnabled.value && localState.value && !isValidURL(localState.value)) {
message.error('Invalid URL')
localState.value = undefined
return
}
localState.value = value
},
)
</script>
<template>
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none text-sm" @blur="editEnabled = false" />
<div class="flex flex-row items-center justify-between">
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none text-sm w-full" @blur="editEnabled = false" />
<nuxt-link v-else-if="isValid" class="text-sm underline hover:opacity-75" :to="url" target="_blank">{{ value }} </nuxt-link>
<nuxt-link v-else-if="isValid" class="text-sm underline hover:opacity-75" :to="url" target="_blank">{{ value }} </nuxt-link>
<span v-else class="w-9/10 overflow-ellipsis overflow-hidden">{{ value }}</span>
<span v-else>{{ value }}</span>
<div v-if="column.meta?.validate && !isValid && value?.length && !editEnabled" class="mr-1 w-1/10">
<a-tooltip placement="top">
<template #title> Invalid URL </template>
<div class="flex flex-row items-center">
<MiCircleWarning class="text-red-400 h-4" />
</div>
</a-tooltip>
</div>
</div>
</template>
<!--

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { computed, inject, onClickOutside, ref, watch } from '#imports'
import { ReadonlyInj, computed, inject, onClickOutside, ref, watch } from '#imports'
interface Props {
modelValue?: number | string | null
@ -53,7 +53,7 @@ watch(
{ flush: 'post' },
)
const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : readOnly ? 'Select year' : ''))
const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : ''))
</script>
<template>

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

@ -92,7 +92,7 @@ onClickOutside(carouselRef, () => {
<div v-for="item of imageItems" :key="item.url">
<div
:style="{ backgroundImage: `url('${item.url}')` }"
:style="{ backgroundImage: `url('${item.url || item.data}')` }"
class="min-w-70vw min-h-70vh w-full h-full bg-contain bg-center bg-no-repeat"
/>
</div>

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

@ -10,7 +10,6 @@ const {
open,
isLoading,
isPublicGrid,
isForm,
isReadonly,
visibleItems,
modalVisible,
@ -34,6 +33,8 @@ const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, is
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
const { isSharedForm } = useSmartsheetStoreOrThrow()
onKeyDown('Escape', () => {
modalVisible.value = false
isOverDropZone.value = false
@ -61,7 +62,7 @@ function onClick(item: Record<string, any>) {
<template #title>
<div class="flex gap-4">
<div
v-if="!isReadonly && (isForm || isUIAllowed('tableAttachment')) && !isPublicGrid && !isLocked"
v-if="isSharedForm || (!isReadonly && isUIAllowed('tableAttachment') && !isPublicGrid && !isLocked)"
class="nc-attach-file group"
@click="open"
>
@ -78,7 +79,7 @@ function onClick(item: Record<string, any>) {
</template>
<div ref="dropZoneRef">
<template v-if="!isReadonly && !dragging">
<template v-if="isSharedForm || (!isReadonly && !dragging)">
<general-overlay
v-model="isOverDropZone"
inline
@ -94,9 +95,8 @@ function onClick(item: Record<string, any>) {
<a-card class="nc-attachment-item group">
<a-tooltip v-if="!isReadonly">
<template #title> Remove File </template>
<MdiCloseCircle
v-if="isUIAllowed('tableAttachment') && !isPublicGrid && !isLocked"
v-if="isSharedForm || (isUIAllowed('tableAttachment') && !isPublicGrid && !isLocked)"
class="nc-attachment-remove"
@click.stop="removeFile(i)"
/>
@ -116,7 +116,7 @@ function onClick(item: Record<string, any>) {
>
<div
v-if="isImage(item.title, item.mimetype)"
:style="{ backgroundImage: `url('${item.url}')` }"
:style="{ backgroundImage: `url('${item.url || item.data}')` }"
class="w-full h-full bg-contain bg-center bg-no-repeat"
@click.stop="onClick(item)"
/>

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

@ -4,7 +4,16 @@ import { useProvideAttachmentCell } from './utils'
import { useSortable } from './sort'
import Modal from './Modal.vue'
import Carousel from './Carousel.vue'
import { computed, isImage, openLink, ref, useDropZone, useSmartsheetStoreOrThrow, watch } from '#imports'
import {
computed,
isImage,
openLink,
ref,
useDropZone,
useSmartsheetRowStoreOrThrow,
useSmartsheetStoreOrThrow,
watch,
} from '#imports'
interface Props {
modelValue: string | Record<string, any>[] | null
@ -23,13 +32,26 @@ const sortableRef = ref<HTMLDivElement>()
const { cellRefs } = useSmartsheetStoreOrThrow()!
const { column, modalVisible, attachments, visibleItems, onDrop, isLoading, open, FileIcon, selectedImage, isReadonly } =
useProvideAttachmentCell(updateModelValue)
const {
column,
modalVisible,
attachments,
visibleItems,
onDrop,
isLoading,
open,
FileIcon,
selectedImage,
isReadonly,
storedFiles,
} = useProvideAttachmentCell(updateModelValue)
const currentCellRef = computed(() => cellRefs.value.find((cell) => cell.dataset.key === `${rowIndex}${column.value.id}`))
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, isReadonly)
const { state: rowState } = useSmartsheetRowStoreOrThrow()
const { isOverDropZone } = useDropZone(currentCellRef, onDrop)
/** on new value, reparse our stored attachments */
@ -37,7 +59,11 @@ watch(
() => modelValue,
(nextModel) => {
if (nextModel) {
attachments.value = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean)
try {
attachments.value = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean)
} catch {
attachments.value = []
}
}
},
{ immediate: true },
@ -53,13 +79,23 @@ onKeyDown('Escape', () => {
modalVisible.value = false
isOverDropZone.value = false
})
/** sync storedFiles state with row state */
watch(
() => storedFiles.value.length || 0,
() => {
rowState.value[column.value.title!] = storedFiles.value
},
)
const { isSharedForm } = useSmartsheetStoreOrThrow()
</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 && !!currentCellRef">
<template v-if="isSharedForm || (!isReadonly && !dragging && !!currentCellRef)">
<general-overlay
v-model="isOverDropZone"
inline

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

@ -1,7 +1,7 @@
import { message } 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 { ColumnInj, EditModeInj, IsPublicInj, MetaInj, ReadonlyInj } from '~/context'
import { isImage } from '~/utils'
import { NOCO } from '~/lib'
import MdiPdfBox from '~icons/mdi/pdf-box'
@ -10,11 +10,17 @@ 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'
interface AttachmentProps {
data?: any
file: File
title: string
}
export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
(updateModelValue: (data: string | Record<string, any>[]) => void) => {
const isReadonly = inject(ReadonlyInj, false)
const isPublicForm = inject('isPublicForm', false)
const isPublic = inject(IsPublicInj, ref(false))
const isForm = inject('isForm', false)
@ -27,7 +33,11 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const editEnabled = inject(EditModeInj, ref(false))
const storedFiles = ref<{ title: string; file: File }[]>([])
/** keep user selected files data (in base encoded string format) and meta details */
const storedFilesData = ref<{ title: string; file: File }[]>([])
/** keep user selected File object */
const storedFiles = ref<File[]>([])
const attachments = ref<File[]>([])
@ -43,10 +53,11 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
/** remove a file from our stored attachments (either locally stored or saved ones) */
function removeFile(i: number) {
if (isPublicForm) {
if (isPublic.value) {
storedFilesData.value.splice(i, 1)
storedFiles.value.splice(i, 1)
updateModelValue(storedFiles.value.map((storedFile) => storedFile.file))
updateModelValue(storedFilesData.value.map((storedFile) => storedFile.file))
} else {
attachments.value.splice(i, 1)
updateModelValue(attachments.value)
@ -57,19 +68,33 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
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
}),
if (isPublic.value) {
storedFiles.value.push(...selectedFiles)
storedFilesData.value.push(
...(await Promise.all<AttachmentProps>(
Array.from(selectedFiles).map(
(file) =>
new Promise<AttachmentProps>((resolve) => {
const res: AttachmentProps = { file, title: file.name }
if (isImage(file.name, (file as any).mimetype)) {
const reader = new FileReader()
reader.onload = (e: any) => {
res.data = e.target?.result
resolve(res)
}
reader.onerror = () => {
resolve(res)
}
reader.readAsDataURL(file)
} else {
resolve(res)
}
}),
),
)),
)
return updateModelValue(storedFiles.value.map((storedFile) => storedFile.file))
return updateModelValue(storedFilesData.value.map((storedFile) => storedFile.file))
}
const newAttachments = []
@ -124,15 +149,15 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
}
/** 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[]))
const visibleItems = computed<any[]>(() => (isPublic.value ? storedFilesData.value : attachments.value) || ([] as any[]))
watch(files, (nextFiles) => nextFiles && onFileSelect(nextFiles))
return {
attachments,
storedFiles,
storedFilesData,
visibleItems,
isPublicForm,
isPublic,
isForm,
isPublicGrid,
isReadonly,
@ -149,6 +174,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
downloadFile,
updateModelValue,
selectedImage,
storedFiles,
}
},
'useAttachmentCell',

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

@ -23,6 +23,8 @@ const { deleteTable } = useTable()
const { isUIAllowed } = useUIPermission()
const isLocked = inject('TreeViewIsLockedInj')
const tablesById = $computed<Record<string, TableType>>(() =>
tables?.value?.reduce((acc: Record<string, TableType>, table: TableType) => {
acc[table.id as string] = table
@ -199,7 +201,11 @@ const activeTable = computed(() => {
<div class="nc-tbl-title flex-1">{{ table.title }}</div>
<a-dropdown v-if="isUIAllowed('table-rename') || isUIAllowed('table-delete')" :trigger="['click']" @click.stop>
<a-dropdown
v-if="!isLocked && (isUIAllowed('table-rename') || isUIAllowed('table-delete'))"
:trigger="['click']"
@click.stop
>
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
@ -209,7 +215,7 @@ const activeTable = computed(() => {
v-t="['c:table:rename']"
class="!text-xs"
@click="showRenameTableDlg(table)"
><div>Rename</div></a-menu-item
><div>{{ $t('general.rename') }}</div></a-menu-item
>
<a-menu-item
@ -218,7 +224,7 @@ const activeTable = computed(() => {
class="!text-xs"
@click="deleteTable(table)"
>
Delete</a-menu-item
{{ $t('general.delete') }}</a-menu-item
>
</a-menu>
</template>
@ -238,7 +244,7 @@ const activeTable = computed(() => {
</div>
</div>
<template #overlay>
<template v-if="!isLocked" #overlay>
<a-menu class="cursor-pointer">
<template v-if="contextMenuTarget.type === 'table'">
<a-menu-item

18
packages/nc-gui-v2/components/dashboard/settings/Modal.vue

@ -13,6 +13,7 @@ import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple'
import NootbookOutline from '~icons/mdi/notebook-outline'
import { useUIPermission, useVModel, watch } from '#imports'
import MdiCloseIcon from '~icons/mdi/close'
interface Props {
modelValue: boolean
@ -130,8 +131,21 @@ watch(
</script>
<template>
<a-modal v-model:visible="vModel" :footer="null" width="max(90vw, 600px)" @cancel="emits('update:modelValue', false)">
<a-typography-title class="ml-4 mb-2 select-none" type="secondary" :level="5">SETTINGS</a-typography-title>
<a-modal
v-model:visible="vModel"
:footer="null"
width="max(90vw, 600px)"
:closable="false"
@cancel="emits('update:modelValue', false)"
>
<div class="flex flex-row justify-between w-full items-center mb-1">
<a-typography-title class="ml-4 select-none" type="secondary" :level="5">SETTINGS</a-typography-title>
<a-button type="text" class="!rounded-md border-none -mt-1.5 -mr-1" @click="vModel = false">
<template #icon>
<MdiCloseIcon class="cursor-pointer mt-1" />
</template>
</a-button>
</div>
<a-layout class="mt-3 modal-body flex">
<!-- Side tabs -->

5
packages/nc-gui-v2/components/general/PreviewAs.vue

@ -7,6 +7,7 @@ import MdiEyeOutline from '~icons/mdi/eye-outline'
import MdiCommentAccountOutline from '~icons/mdi/comment-account-outline'
const { float } = defineProps<{ float?: boolean }>()
const position = useState('preview-as-position', () => ({
y: `${window.innerHeight - 100}px`,
x: `${window.innerWidth / 2 - 250}px`,
@ -98,10 +99,6 @@ watch(previewAs, () => window.location.reload())
</template>
<style scoped>
.nc-project-menu-item {
@apply cursor-pointer flex items-center gap-2 py-2 hover:text-primary after:(content-[''] absolute top-0 left-0 bottom-0 right-0 w-full h-full bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
}
.floating-reset-btn {
@apply z-1000 index-100 fixed text-white
@apply flex items-center overflow-hidden whitespace-nowrap gap-4 rounded shadow-md;

187
packages/nc-gui-v2/components/shared-view/Form.vue

@ -1,12 +1,20 @@
<script setup lang="ts">
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { FieldsInj, MetaInj } from '#imports'
import { useSharedFormStoreOrThrow } from '#imports'
const fields = inject(FieldsInj, ref([]))
const meta = inject(MetaInj)
const { sharedView } = useSharedView()
const formState = ref(fields.value.reduce((a, v) => ({ ...a, [v.title]: undefined }), {}))
const {
sharedFormView,
submitForm,
v$,
formState,
notFound,
formColumns,
submitted,
secondsRemain,
passwordDlg,
password,
loadSharedView,
} = useSharedFormStoreOrThrow()
function isRequired(_columnObj: Record<string, any>, required = false) {
let columnObj = _columnObj
@ -15,7 +23,7 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
columnObj.colOptions &&
columnObj.colOptions.type === RelationTypes.BELONGS_TO
) {
columnObj = fields.value.find((c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id) as Record<
columnObj = formColumns.value?.find((c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id) as Record<
string,
any
>
@ -23,58 +31,129 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
return required || (columnObj && columnObj.rqd && !columnObj.cdf)
}
useSmartsheetStoreOrThrow()
useProvideSmartsheetRowStore(meta, formState)
const formRef = ref()
</script>
<template>
<div class="flex flex-col my-4 space-y-2 mx-32 items-center">
<div class="flex w-2/3 flex-col mt-10">
<div class="flex flex-col items-start px-14 py-8 bg-gray-50 rounded-md w-full">
<a-typography-title class="border-b-1 border-gray-100 w-full pb-3" :level="1">
{{ sharedView.view.heading }}
</a-typography-title>
<a-typography class="pl-1 text-sm">{{ sharedView.view.subheading }}</a-typography>
</div>
<div class="bg-primary/100 !h-[100vh] overflow-auto w-100 flex flex-col">
<div>
<img src="~/assets/img/icons/512x512-trans.png" width="30" class="mx-4 mt-2" />
</div>
<div class="m-4 mt-2 bg-white rounded p-2 flex-1">
<a-alert v-if="notFound" type="warning" class="mx-auto mt-10 max-w-[300px]" message="Not found"> </a-alert>
<a-form ref="formRef" :model="formState" class="mt-8 pb-12 mb-8 px-3 bg-gray-50 rounded-md">
<div v-for="(field, index) in fields" :key="index" class="flex flex-col mt-4 px-10 pt-6 space-y-2">
<div class="flex">
<SmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)"
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
<SmartsheetHeaderCell
v-else
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
<template v-else-if="submitted">
<div class="flex justify-center">
<div v-if="sharedFormView" style="min-width: 350px" class="mt-3">
<a-alert type="success" outlined :message="sharedFormView.success_msg || 'Successfully submitted form data'">
</a-alert>
<p v-if="sharedFormView.show_blank_form" class="text-xs text-gray-500 text-center my-4">
New form will be loaded after {{ secondsRemain }} seconds
</p>
<div v-if="sharedFormView.submit_another_form" class="text-center">
<a-button type="primary" @click="submitted = false"> Submit Another Form</a-button>
</div>
</div>
<a-form-item
v-if="isVirtualCol(field)"
class="ma-0 gap-0 pa-0"
:name="field.title"
:rules="[{ required: field.required, message: `${field.title} is required` }]"
>
<SmartsheetVirtualCell v-model="formState[field.title]" class="nc-input" :column="field" />
</a-form-item>
<a-form-item
v-else
class="ma-0 gap-0 pa-0"
:name="field.title"
:rules="[{ required: field.required, message: `${field.title} is required` }]"
>
<SmartsheetCell v-model="formState[field.title]" class="nc-input" :column="field" :edit-enabled="true" />
</a-form-item>
</div>
</a-form>
</template>
<div v-else-if="sharedFormView" class="">
<a-row class="justify-center">
<a-col :md="20">
<div>
<div class="h-full ma-0 rounded-b-0">
<div
class="nc-form-wrapper pb-10 rounded shadow-xl"
style="background: linear-gradient(180deg, #dbdbdb 0, #dbdbdb 200px, white 200px)"
>
<div class="mt-10 flex items-center justify-center flex-col">
<div class="nc-form-banner backgroundColor darken-1 flex-column justify-center d-flex">
<div class="flex items-center justify-center grow h-[100px]">
<img src="~/assets/img/icon.png" width="50" class="mx-4" />
<span class="text-4xl font-weight-bold">NocoDB</span>
</div>
</div>
</div>
<div class="mx-auto nc-form bg-white shadow-lg pa-2 mb-10 max-w-[600px] mx-auto rounded">
<h2 class="mt-4 text-4xl font-weight-bold text-left mx-4 mb-3 px-1">
{{ sharedFormView.heading }}
</h2>
<div class="text-lg text-left mx-4 py-2 px-1 text-gray-500">
{{ sharedFormView.subheading }}
</div>
<div class="h-100">
<div v-for="(field, index) in formColumns" :key="index" class="flex flex-col mt-4 px-4 space-y-2">
<div class="flex">
<SmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)"
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
<SmartsheetHeaderCell
v-else
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
</div>
<div v-if="isVirtualCol(field)" class="mt-0">
<SmartsheetVirtualCell class="mt-0 nc-input" :column="field" />
<div v-if="field.description" class="text-gray-500 text-[10px] mb-2 ml-1">{{ field.description }}</div>
<template v-if="v$.virtual.$dirty && v$.virtual?.[field.title]">
<div v-for="error of v$.virtual[field.title].$errors" :key="error" class="text-xs text-red-500">
{{ error.$message }}
</div>
</template>
</div>
<div v-else class="mt-0">
<SmartsheetCell
v-model="formState[field.title]"
class="nc-input"
:column="field"
:edit-enabled="true"
/>
<div v-if="field.description" class="text-gray-500 text-[10px] mb-2 ml-1">{{ field.description }}</div>
<template v-if="v$.localState.$dirty && v$.localState?.[field.title]">
<div v-for="error of v$.localState[field.title].$errors" :key="error" class="text-xs text-red-500">
{{ error.$message }}
</div>
</template>
</div>
</div>
<div class="text-center my-9">
<a-button type="primary" size="large" @click="submitForm(formState, additionalState)"> Submit</a-button>
</div>
</div>
</div>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
<a-modal
v-model:visible="passwordDlg"
:closable="false"
width="28rem"
centered
:footer="null"
:mask-closable="false"
@close="passwordDlg = false"
>
<div class="w-full flex flex-col">
<a-typography-title :level="4">This shared view is protected</a-typography-title>
<a-form ref="formRef" :model="{ password }" class="mt-2" @finish="loadSharedView">
<a-form-item name="password" :rules="[{ required: true, message: 'Password is required' }]">
<a-input-password v-model:value="password" placeholder="Enter password" />
</a-form-item>
<a-button type="primary" html-type="submit">Unlock</a-button>
</a-form>
</div>
</a-modal>
</div>
</template>
@ -82,4 +161,8 @@ const formRef = ref()
.nc-input {
@apply w-full !bg-white rounded px-2 py-2 min-h-[40px] mt-2 mb-2 flex align-center border-solid border-1 border-primary;
}
.nc-form-wrapper {
@apply my-0 mx-auto max-w-[800px];
}
</style>

10
packages/nc-gui-v2/components/shared-view/Grid.vue

@ -4,23 +4,23 @@ import type { TableType } from 'nocodb-sdk'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReadonlyInj, ReloadViewDataHookInj } from '~/context'
const { sharedView, meta, columns } = useSharedView()
const { sharedView, meta } = useSharedView()
const reloadEventHook = createEventHook<void>()
provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReadonlyInj, ref(true))
provide(ReadonlyInj, true)
provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, columns)
provide(FieldsInj, ref(meta.value.columns as any[]))
provide(IsPublicInj, ref(true))
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta)
</script>
<template>
<div class="nc-container flex flex-col h-full mt-4 px-6">
<div class="nc-container flex flex-col h-full mt-1.5 px-12">
<SmartsheetToolbar />
<SmartsheetGrid class="px-3" />
<SmartsheetGrid />
</div>
</template>

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

@ -99,7 +99,7 @@ useEventListener(document, 'click', handleClose)
</script>
<template>
<div ref="editOrAddRef" class="min-w-[400px] max-h-[95vh] bg-white shadow p-6 overflow-auto" @click.stop>
<div ref="editOrAddRef" class="min-w-[400px] max-h-[95vh] bg-gray-50 shadow-lg p-6 overflow-auto !border" @click.stop>
<a-form v-if="formState" v-model="formState" name="column-create-or-edit" layout="vertical">
<div class="flex flex-col gap-2">
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title">
@ -132,7 +132,6 @@ useEventListener(document, 'click', handleClose)
v-model:value="formState"
/>
<SmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<SmartsheetColumnPercentOptions v-if="formState.uidt === UITypes.Percent" v-model:value="formState" />
<SmartsheetColumnSelectOptions
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"
v-model:value="formState"

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

@ -1,3 +1,5 @@
<!-- File not in use for now -->
<script setup lang="ts">
import { precisions } from '@/utils/percentUtils'

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

@ -43,7 +43,7 @@ const aggrFunctionsList = [
]
if (!vModel.value.fk_relation_column_id) vModel.value.fk_relation_column_id = null
if (!vModel.value.fk_rollup_column_id) vModel.value.value.fk_rollup_column_id = null
if (!vModel.value.fk_rollup_column_id) vModel.value.fk_rollup_column_id = null
if (!vModel.value.rollup_function) vModel.value.rollup_function = null
const refTables = $computed(() => {

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

@ -36,6 +36,7 @@ function onVisibleChange() {
<a-dropdown
v-model:visible="editColumnDropdown"
class="h-full"
:trigger="['click']"
placement="bottomRight"
@visible-change="onVisibleChange"
@ -62,8 +63,4 @@ function onVisibleChange() {
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.ant-dropdown-trigger) {
@apply h-full;
}
</style>

9
packages/nc-gui-v2/components/smartsheet-header/Menu.vue

@ -3,21 +3,22 @@ import { Modal, message } from 'ant-design-vue'
import { inject } from 'vue'
import { useI18n } from 'vue-i18n'
import { useNuxtApp } from '#app'
import { useMetas } from '#imports'
import { ColumnInj, MetaInj } from '~/context'
import { extractSdkResponseErrorMsg } from '~/utils'
import { ColumnInj, IsLockedInj, MetaInj, extractSdkResponseErrorMsg, useMetas } from '#imports'
import MdiEditIcon from '~icons/mdi/pencil'
import MdiStarIcon from '~icons/mdi/star'
import MdiDeleteIcon from '~icons/mdi/delete-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
const { virtual = false } = defineProps<{ virtual?: boolean }>()
const emit = defineEmits(['edit'])
const column = inject(ColumnInj)
const meta = inject(MetaInj)
const isLocked = inject(IsLockedInj)
const { $api, $e } = useNuxtApp()
const { t } = useI18n()
@ -53,7 +54,7 @@ const setAsPrimaryValue = async () => {
</script>
<template>
<a-dropdown placement="bottomRight" :trigger="['click']">
<a-dropdown v-if="!isLocked" placement="bottomRight" :trigger="['click']">
<MdiMenuDownIcon class="h-full text-grey nc-ui-dt-dropdown cursor-pointer outline-0" />
<template #overlay>
<a-menu class="shadow bg-white">

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

@ -120,6 +120,7 @@ function onVisibleChange() {
<a-dropdown
v-model:visible="editColumnDropdown"
class="h-full"
:trigger="['click']"
placement="bottomRight"
@visible-change="onVisibleChange"
@ -146,8 +147,4 @@ function onVisibleChange() {
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.ant-dropdown-trigger) {
@apply h-full;
}
</style>

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

@ -2,31 +2,42 @@
import type { FilterType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import FieldListAutoCompleteDropdown from './FieldListAutoCompleteDropdown.vue'
import { useNuxtApp } from '#app'
import { inject, useViewFilters } from '#imports'
import { comparisonOpList } from '~/utils/filterUtils'
import { ActiveViewInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import MdiDeleteIcon from '~icons/mdi/close-box'
import MdiAddIcon from '~icons/mdi/plus'
import type { Filter } from '~/lib/types'
const {
nested = false,
parentId,
autoSave = true,
hookId = null,
modelValue,
} = defineProps<{ nested?: boolean; parentId?: string; autoSave: boolean; hookId?: string; modelValue?: Filter[] }>()
import {
ActiveViewInj,
MetaInj,
ReloadViewDataHookInj,
comparisonOpList,
computed,
inject,
ref,
useNuxtApp,
useViewFilters,
watch,
} from '#imports'
import type { Filter } from '~/lib'
interface Props {
nested?: boolean
parentId?: string
autoSave: boolean
hookId?: string
modelValue?: Filter[]
}
const { nested = false, parentId, autoSave = true, hookId = null, modelValue } = defineProps<Props>()
const emit = defineEmits(['update:filtersLength'])
const meta = inject(MetaInj)
const logicalOps = [
{ value: 'and', text: 'AND' },
{ value: 'or', text: 'OR' },
]
const activeView = inject(ActiveViewInj)
const meta = inject(MetaInj)!
const reloadDataHook = inject(ReloadViewDataHookInj)
const activeView = inject(ActiveViewInj)!
// todo: replace with inject or get from state
const reloadDataHook = inject(ReloadViewDataHookInj)!
const { $e } = useNuxtApp()
@ -35,11 +46,13 @@ const { filters, deleteFilter, saveOrUpdate, loadFilters, addFilter, addFilterGr
parentId,
computed(() => autoSave),
() => {
reloadDataHook?.trigger()
reloadDataHook.trigger()
},
modelValue,
)
const nestedFilters = ref()
const filterUpdateCondition = (filter: FilterType, i: number) => {
saveOrUpdate(filter, i)
$e('a:filter:update', {
@ -63,12 +76,14 @@ const filterUpdateCondition = (filter: FilterType, i: number) => {
// return true
// })
const columns = computed(() => meta?.value?.columns)
const columns = computed(() => meta.value?.columns)
const types = computed(() => {
if (!meta?.value?.columns?.length) {
if (!meta.value?.columns?.length) {
return {}
}
return meta?.value?.columns?.reduce((obj: any, col: any) => {
return meta.value?.columns?.reduce((obj: any, col: any) => {
switch (col.uidt) {
case UITypes.Number:
case UITypes.Decimal:
@ -83,22 +98,15 @@ const types = computed(() => {
})
watch(
() => (activeView?.value as any)?.id,
() => activeView.value?.id,
(n, o) => {
if (n !== o) loadFilters(hookId as string)
},
{ immediate: true },
)
const nestedFilters = ref()
const logicalOps = [
{ value: 'and', text: 'AND' },
{ value: 'or', text: 'OR' },
]
watch(
() => filters?.value?.length,
() => filters.value.length,
(length) => {
emit('update:filtersLength', length ?? 0)
},
@ -106,7 +114,10 @@ watch(
const applyChanges = async (hookId?: string) => {
await sync(hookId)
for (const nestedFilter of nestedFilters?.value || []) {
if (!nestedFilters.value.length) return
for (const nestedFilter of nestedFilters.value) {
if (nestedFilter.parentId) {
await nestedFilter.applyChanges(hookId, true)
}
@ -128,7 +139,7 @@ defineExpose({
<template v-for="(filter, i) in filters" :key="filter.id || i">
<template v-if="filter.status !== 'delete'">
<template v-if="filter.is_group">
<MdiDeleteIcon
<MdiCloseBox
v-if="!filter.readOnly"
:key="i"
small
@ -174,7 +185,7 @@ defineExpose({
mdi-close-box
</v-icon> -->
<MdiDeleteIcon
<MdiCloseBox
v-if="!filter.readOnly"
class="nc-filter-item-remove-btn text-grey align-self-center"
@click.stop="deleteFilter(filter, i)"
@ -260,7 +271,7 @@ defineExpose({
<a-button class="elevation-0 text-capitalize" type="primary" ghost @click.stop="addFilter">
<div class="flex align-center gap-1">
<!-- <v-icon small color="grey"> mdi-plus </v-icon> -->
<MdiAddIcon />
<MdiPlus />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
@ -268,7 +279,7 @@ defineExpose({
<a-button class="text-capitalize !text-gray-500" @click.stop="addFilterGroup">
<div class="flex align-center gap-1">
<!-- <v-icon small color="grey"> mdi-plus </v-icon> -->
<MdiAddIcon />
<MdiPlus />
Add Filter Group
<!-- todo: add i18n {{ $t('activity.addFilterGroup') }} -->
</div>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import { ActiveViewInj, FieldsInj, IsLockedInj, IsPublicInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import { computed, inject, useNuxtApp, useViewColumns, watch } from '#imports'
const meta = inject(MetaInj)!
@ -8,6 +8,7 @@ const activeView = inject(ActiveViewInj)!
const reloadDataHook = inject(ReloadViewDataHookInj)!
const rootFields = inject(FieldsInj)
const isLocked = inject(IsLockedInj)
const isPublic = inject(IsPublicInj)
const { $e } = useNuxtApp()
@ -49,14 +50,10 @@ const onMove = (event: { moved: { newIndex: number } }) => {
if (fields.value.length < 2) return
if (fields.value.length - 1 === event.moved.newIndex) {
fields.value[event.moved.newIndex].order = (fields.value[event.moved.newIndex - 1].order || 1) + 1
} else if (event.moved.newIndex === 0) {
fields.value[event.moved.newIndex].order = (fields.value[1].order || 1) / 2
} else {
fields.value[event.moved.newIndex].order =
((fields.value[event.moved.newIndex - 1].order || 1) + (fields.value[event.moved.newIndex + 1].order || 1)) / 2
}
fields.value.map((field, index) => {
field.order = index + 1
return field
})
saveOrUpdate(fields.value[event.moved.newIndex], event.moved.newIndex)
@ -87,10 +84,10 @@ const onMove = (event: { moved: { newIndex: number } }) => {
<a-input v-model:value="filterQuery" size="small" :placeholder="$t('placeholder.searchFields')" />
</div>
<div class="nc-fields-list py-1">
<Draggable :list="fields" item-key="id" @change="onMove($event)">
<template #item="{ element: field }">
<Draggable v-model="fields" item-key="id" @change="onMove($event)">
<template #item="{ element: field, index: index }">
<div v-show="filteredFieldList.includes(field)" :key="field.id" class="px-2 py-1 flex" @click.stop>
<a-checkbox v-model:checked="field.show" class="flex-shrink" @change="saveOrUpdate(field, i)">
<a-checkbox v-model:checked="field.show" class="flex-shrink" @change="saveOrUpdate(field, index)">
<span class="">{{ field.title }}</span>
</a-checkbox>
<div class="flex-1" />
@ -101,7 +98,7 @@ const onMove = (event: { moved: { newIndex: number } }) => {
</div>
<v-divider class="my-2" />
<div class="p-2 py-1 flex" @click.stop>
<div v-if="!isPublic" class="p-2 py-1 flex" @click.stop>
<a-checkbox v-model:checked="showSystemFields">
<span class="text-xs"> {{ $t('activity.showSystemFields') }}</span>
</a-checkbox>

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

@ -1,12 +1,12 @@
<script lang="ts" setup>
import * as XLSX from 'xlsx'
// todo: export types is missing EXCEL
// import { ExportTypes } from 'nocodb-sdk'
import { ExportTypes } from 'nocodb-sdk'
import FileSaver from 'file-saver'
import { message } from 'ant-design-vue'
import {
ActiveViewInj,
FieldsInj,
IsLockedInj,
IsPublicInj,
MetaInj,
extractSdkResponseErrorMsg,
@ -17,27 +17,24 @@ import {
useUIPermission,
} from '#imports'
enum ExportTypes {
EXCEL = 'excel',
CSV = 'csv',
}
const sharedViewListDlg = ref(false)
const isPublicView = inject(IsPublicInj, ref(false))
const isView = false
// TODO: pending for shared view
const { project } = useProject()
const { $api } = useNuxtApp()
const meta = inject(MetaInj)
const fields = inject(FieldsInj, ref([]))
const selectedView = inject(ActiveViewInj)
const isLocked = inject(IsLockedInj)
const showWebhookDrawer = ref(false)
const quickImportDialog = ref(false)
@ -106,16 +103,16 @@ const exportFile = async (exportType: ExportTypes) => {
</a-button>
<template #overlay>
<div class="bg-white shadow-lg !border">
<div class="bg-gray-50 py-2 shadow-lg !border">
<div>
<div v-t="['a:actions:download-csv']" class="nc-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadOutline />
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</div>
<div v-t="['a:actions:download-excel']" class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadOutline />
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}
</div>
@ -124,9 +121,10 @@ const exportFile = async (exportType: ExportTypes) => {
v-if="isUIAllowed('csvImport') && !isView && !isPublicView"
v-t="['a:actions:upload-csv']"
class="nc-menu-item"
@click="quickImportDialog = true"
:class="{ disabled: isLocked }"
@click="!isLocked ? (quickImportDialog = true) : {}"
>
<MdiUploadOutline />
<MdiUploadOutline class="text-gray-500" />
<!-- Upload CSV -->
{{ $t('activity.uploadCSV') }}
</div>
@ -137,7 +135,7 @@ const exportFile = async (exportType: ExportTypes) => {
class="nc-menu-item"
@click="sharedViewListDlg = true"
>
<MdiViewListOutline />
<MdiViewListOutline class="text-gray-500" />
<!-- Shared View List -->
{{ $t('activity.listSharedView') }}
</div>
@ -148,7 +146,7 @@ const exportFile = async (exportType: ExportTypes) => {
class="nc-menu-item"
@click="showWebhookDrawer = true"
>
<MdiHook />
<MdiHook class="text-gray-500" />
{{ $t('objects.webhooks') }}
</div>
</div>

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

@ -21,7 +21,7 @@ const { isUIAllowed } = useUIPermission()
let showShareModel = $ref(false)
const passwordProtected = $ref(false)
let passwordProtected = $ref(false)
const shared = ref()
@ -100,6 +100,20 @@ const copyLink = () => {
copy(sharedViewUrl?.value as string)
message.success('Copied to clipboard')
}
watch(
() => passwordProtected,
(value) => {
if (!value) {
shared.value.password = ''
saveShareLinkPassword()
}
},
)
onMounted(() => {
if (shared.value?.password?.length) passwordProtected = true
})
</script>
<template>
@ -133,7 +147,6 @@ const copyLink = () => {
<a-collapse-panel key="1" header="More Options">
<div class="mb-2">
<a-checkbox v-model:checked="passwordProtected" class="!text-xs">{{ $t('msg.info.beforeEnablePwd') }} </a-checkbox>
<!-- todo: add password toggle -->
<div v-if="passwordProtected" class="flex gap-2 mt-2 mb-4">
<a-input
v-model:value="shared.password"

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

@ -1,7 +1,20 @@
<script setup lang="ts">
import { UITypes } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import { ActiveCellInj, ColumnInj, EditModeInj, computed, provide, toRef, useColumn, useDebounceFn, useVModel } from '#imports'
import {
ActiveCellInj,
ColumnInj,
EditModeInj,
IsFormInj,
IsLockedInj,
IsPublicInj,
computed,
provide,
toRef,
useColumn,
useDebounceFn,
useVModel,
} from '#imports'
import { NavigateDir } from '~/lib'
interface Props {
@ -10,6 +23,7 @@ interface Props {
editEnabled: boolean
rowIndex?: number
active?: boolean
virtual?: boolean
}
const props = defineProps<Props>()
@ -20,12 +34,20 @@ const column = toRef(props, 'column')
const active = toRef(props, 'active', false)
const virtual = toRef(props, 'virtual', false)
provide(ColumnInj, column)
provide(EditModeInj, useVModel(props, 'editEnabled', emit))
provide(ActiveCellInj, active)
const isForm = inject(IsFormInj)
const isPublic = inject(IsPublicInj)
const isLocked = inject(IsLockedInj)
let changed = $ref(false)
const syncValue = useDebounceFn(function () {
@ -54,10 +76,6 @@ const isManualSaved = $computed(() => {
return [UITypes.Currency, UITypes.Duration].includes(column?.value?.uidt as UITypes)
})
const isPrimary = computed(() => {
return column?.value?.pv
})
const vModel = computed({
get: () => props.modelValue,
set: (val) => {
@ -75,6 +93,7 @@ const vModel = computed({
})
const {
isPrimary,
isURL,
isEmail,
isJSON,
@ -99,7 +118,7 @@ const {
} = useColumn(column)
const syncAndNavigate = (dir: NavigateDir) => {
if (isJSON) return
if (isJSON.value) return
if (changed) {
emit('save')
@ -112,7 +131,7 @@ const syncAndNavigate = (dir: NavigateDir) => {
<template>
<div
class="nc-cell w-full h-full"
:class="{ 'text-blue-600': isPrimary }"
:class="{ 'text-blue-600': isPrimary && !virtual }"
@keydown.stop.left
@keydown.stop.right
@keydown.stop.up
@ -134,13 +153,14 @@ const syncAndNavigate = (dir: NavigateDir) => {
<CellEmail v-else-if="isEmail" v-model="vModel" />
<CellUrl v-else-if="isURL" v-model="vModel" />
<CellPhoneNumber v-else-if="isPhoneNumber" v-model="vModel" />
<CellPercent v-else-if="isPercent" 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" />
<CellJson v-else-if="isJSON" v-model="vModel" />
<CellText v-else v-model="vModel" />
<div v-if="(isLocked || (isPublic && !isForm)) && !isAttachment" class="nc-locked-overlay" @click.stop.prevent />
</div>
</template>

50
packages/nc-gui-v2/components/smartsheet/Form.vue

@ -48,7 +48,7 @@ const view = inject(ActiveViewInj)
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view as any)
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta as any, false, async () => {
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta as any, async () => {
await loadFormView()
setFormData()
})
@ -144,8 +144,19 @@ function isDbRequired(column: Record<string, any>) {
return isRequired
}
function onMoveCallback(event: any) {
if (shouldSkipColumn(event.draggedContext.element)) {
return false
}
}
function onMove(event: any) {
const { newIndex, element, oldIndex } = event.added || event.moved || event.removed
console.log(event)
if (shouldSkipColumn(element)) {
console.log('SKIPPED')
return
}
if (event.added) {
element.show = true
@ -171,7 +182,7 @@ function onMove(event: any) {
}
function hideColumn(idx: number) {
if (isDbRequired(localColumns.value[idx]) || localColumns.value[idx].required) {
if (shouldSkipColumn(localColumns.value[idx])) {
message.info("Required field can't be moved")
return
}
@ -189,7 +200,7 @@ function hideColumn(idx: number) {
}
async function addAllColumns() {
for (const col of (formColumnData as Record<string, any>)?.value) {
for (const col of (localColumns as Record<string, any>)?.value) {
if (!systemFieldsIds.value.includes(col.fk_column_id)) {
col.show = true
}
@ -198,17 +209,18 @@ async function addAllColumns() {
$e('a:form-view:add-all')
}
function shouldSkipColumn(col: Record<string, any>) {
return isDbRequired(col) || !!col.required || !!col.rqd
}
async function removeAllColumns() {
for (const col of (formColumnData as Record<string, any>)?.value) {
if (isDbRequired(col) || !!col.required) {
continue
}
col.show = false
for (const col of (localColumns as Record<string, any>)?.value) {
if (!shouldSkipColumn(col)) col.show = false
}
await hideAll(
(localColumns as Record<string, any>)?.value
.filter((f: Record<string, any>) => isDbRequired(f) || !!f.required)
.map((f: Record<string, any>) => f.fk_column_id),
.filter((col: Record<string, any>) => shouldSkipColumn(col))
.map((col: Record<string, any>) => col.fk_column_id),
)
$e('a:form-view:remove-all')
}
@ -251,7 +263,12 @@ function setFormData() {
localColumns.value = col
.filter(
(f: Record<string, any>) => f.show && f.uidt !== UITypes.Rollup && f.uidt !== UITypes.Lookup && f.uidt !== UITypes.Formula,
(f: Record<string, any>) =>
f.show &&
f.uidt !== UITypes.Rollup &&
f.uidt !== UITypes.Lookup &&
f.uidt !== UITypes.Formula &&
f.uidt !== UITypes.SpecificDBType,
)
.sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order)
.map((c: Record<string, any>) => ({ ...c, required: !!(c.required || 0) }))
@ -259,7 +276,13 @@ function setFormData() {
systemFieldsIds.value = getSystemColumns(col).map((c: Record<string, any>) => c.fk_column_id)
hiddenColumns.value = col.filter(
(f: Record<string, any>) => !f.show && !systemFieldsIds.value.includes(f.fk_column_id) && f.uidt !== UITypes.Formula,
(f: Record<string, any>) =>
!f.show &&
!systemFieldsIds.value.includes(f.fk_column_id) &&
f.uidt !== UITypes.Rollup &&
f.uidt !== UITypes.Lookup &&
f.uidt !== UITypes.Formula &&
f.uidt !== UITypes.SpecificDBType,
)
}
@ -447,7 +470,7 @@ onMounted(async () => {
<!-- for future implementation of cover image -->
</div>
<a-card
class="h-full ma-0 rounded-b-0 pa-4"
class="h-full ma-0 rounded-b-0 pa-4 border-none"
:body-style="{
maxWidth: '700px',
margin: '0 auto',
@ -496,6 +519,7 @@ onMounted(async () => {
draggable=".item"
group="form-inputs"
class="h-100"
:move="onMoveCallback"
@change="onMove($event)"
@start="drag = true"
@end="drag = false"

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

@ -41,7 +41,7 @@ const isPublicView = inject(IsPublicInj, ref(false))
// keep a root fields variable and will get modified from
// fields menu and get used in grid and gallery
const fields = inject(FieldsInj, ref([]))
const readonly = inject(ReadonlyInj, ref(false))
const readOnly = inject(ReadonlyInj, false)
const isLocked = inject(IsLockedInj, false)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
@ -59,7 +59,15 @@ const { xWhere, isPkAvail, cellRefs } = useSmartsheetStoreOrThrow()
const addColumnDropdown = ref(false)
const contextMenu = ref(false)
const _contextMenu = ref(false)
const contextMenu = computed({
get: () => _contextMenu.value,
set: (val) => {
if (!readOnly) {
_contextMenu.value = val
}
},
})
const contextMenuTarget = ref(false)
const expandedFormDlg = ref(false)
@ -272,11 +280,15 @@ const onNavigate = (dir: NavigateDir) => {
case NavigateDir.NEXT:
if (selected.row < data.value.length - 1) {
selected.row++
} else {
editEnabled = false
}
break
case NavigateDir.PREV:
if (selected.row > 0) {
selected.row--
} else {
editEnabled = false
}
break
}
@ -302,15 +314,20 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<tr class="nc-grid-header border-1 bg-gray-100 sticky top[-1px]">
<th>
<div class="w-full h-full bg-gray-100 flex min-w-[70px] pl-5 pr-1 items-center">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
<div
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
class="nc-check-all w-full align-center"
>
<a-checkbox v-model:checked="selectedAllRecords" />
<template v-if="!readOnly">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
<div
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
class="nc-check-all w-full align-center"
>
<a-checkbox v-model:checked="selectedAllRecords" />
<span class="flex-1" />
</div>
<span class="flex-1" />
</div>
</template>
<template v-else>
<div class="text-gray-500">#</div>
</template>
</div>
</th>
<th
@ -324,14 +341,13 @@ const expandForm = (row: Row, state: Record<string, any>) => {
@xcresized="resizingCol = null"
>
<div class="w-full h-full bg-gray-100 flex items-center">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readonly" />
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="readonly" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
</div>
</th>
<!-- v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" -->
<th
v-if="!readonly && isUIAllowed('add-column')"
v-if="!readOnly && !isLocked && isUIAllowed('add-column')"
v-t="['c:column:add']"
class="cursor-pointer"
@click.stop="addColumnDropdown = true"
@ -360,18 +376,22 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<tr class="nc-grid-row">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1">
<div class="align-center flex gap-1 min-w-[55px]">
<div v-if="!readonly" class="nc-row-no text-xs text-gray-500" :class="{ hidden: row.rowMeta.selected }">
<div
v-if="!readonly && !isLocked"
class="nc-row-no text-xs text-gray-500"
:class="{ hidden: row.rowMeta.selected }"
>
{{ rowIndex + 1 }}
</div>
<div
v-if="!readonly"
v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<div v-if="!readonly" class="nc-expand" :class="{ 'nc-comment': row.rowMeta?.commentCount }">
<div v-if="!readonly && !isLocked" class="nc-expand" :class="{ 'nc-comment': row.rowMeta?.commentCount }">
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
@ -459,7 +479,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
</tr>
</tbody>
</table>
<template #overlay>
<template v-if="!isLocked" #overlay>
<a-menu class="bg-white shadow" @click="contextMenu = false">
<a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)"
><span class="text-xs">Delete row</span></a-menu-item

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

@ -14,13 +14,13 @@ const ManyToMany = defineAsyncComponent(() => import('../virtual-cell/ManyToMany
const BelongsTo = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))
const Rollup = defineAsyncComponent(() => import('../virtual-cell/HasMany.vue'))
const Rollup = defineAsyncComponent(() => import('../virtual-cell/Rollup.vue') as any)
const Formula = defineAsyncComponent(() => import('../virtual-cell/ManyToMany.vue'))
const Formula = defineAsyncComponent(() => import('../virtual-cell/Formula.vue'))
const Count = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))
const Count = defineAsyncComponent(() => import('../virtual-cell/Count.vue'))
const Lookup = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))
const Lookup = defineAsyncComponent(() => import('../virtual-cell/Lookup.vue') as any)
interface Props {
column: ColumnType

6
packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -2,7 +2,7 @@
import type { ViewTypes } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { viewIcons } from '~/utils'
import { onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel } from '#imports'
import { IsLockedInj, onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel } from '#imports'
interface Props {
view: Record<string, any>
@ -27,6 +27,8 @@ const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const isLocked = inject(IsLockedInj)
/** Is editing the view name enabled */
let isEditing = $ref<boolean>(false)
@ -168,7 +170,7 @@ function onStopEdit() {
<div class="flex-1" />
<template v-if="!isEditing && isUIAllowed('virtualViewsCreateOrEdit')">
<template v-if="!isEditing && !isLocked && isUIAllowed('virtualViewsCreateOrEdit')">
<div class="flex items-center gap-1">
<a-tooltip placement="left">
<template #title>

39
packages/nc-gui-v2/components/smartsheet/sidebar/index.vue

@ -3,13 +3,28 @@ import type { FormType, GalleryType, GridType, KanbanType, ViewTypes } from 'noc
import MenuTop from './MenuTop.vue'
import MenuBottom from './MenuBottom.vue'
import Toolbar from './toolbar/index.vue'
import { computed, inject, provide, ref, useElementHover, useRoute, useRouter, useViews, watch } from '#imports'
import { ActiveViewInj, MetaInj, RightSidebarInj, ViewListInj } from '~/context'
import {
ActiveViewInj,
IsFormInj,
MetaInj,
ViewListInj,
computed,
inject,
provide,
ref,
useElementHover,
useRoute,
useRouter,
useViews,
watch,
} from '#imports'
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const isForm = inject(IsFormInj)
const { views, loadViews } = useViews(meta)
const { isUIAllowed } = useUIPermission()
@ -21,9 +36,9 @@ const route = useRoute()
provide(ViewListInj, views)
/** Sidebar visible */
const sidebarOpen = inject(RightSidebarInj, ref(true))
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
const sidebarCollapsed = computed(() => !sidebarOpen.value)
const sidebarCollapsed = computed(() => !isOpen.value)
/** Sidebar ref */
const sidebar = ref()
@ -96,21 +111,21 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
class="group color-transition cursor-pointer hover:ring active:ring-pink-500 z-1 flex items-center p-[1px] absolute top-1/2 left-[-1rem] shadow bg-gray-100 rounded-full"
>
<MaterialSymbolsChevronRightRounded
v-if="sidebarOpen"
v-if="isOpen"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400 nc-right-sidebar-toggle"
@click="sidebarOpen = false"
@click="isOpen = false"
/>
<MaterialSymbolsChevronLeftRounded
v-else
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400 nc-right-sidebar-toggle"
@click="sidebarOpen = true"
@click="isOpen = true"
/>
</div>
</Transition>
</a-tooltip>
<Toolbar v-if="sidebarOpen" class="flex items-center py-3 px-3 justify-between border-b-1" />
<Toolbar v-if="isOpen" :class="{ 'flex items-center py-3 px-3 justify-between border-b-1': !isForm }" />
<Toolbar v-else class="py-3 px-2 max-w-[50px] flex !flex-col-reverse gap-4 items-center mt-[-1px]">
<template #start>
@ -118,7 +133,7 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
<template #title> {{ $t('objects.webhooks') }}</template>
<div class="nc-sidebar-right-item hover:after:bg-gray-300">
<MdiHook />
<MdiHook @click.stop />
</div>
</a-tooltip>
@ -128,15 +143,15 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
<template #title> Get API Snippet</template>
<div class="nc-sidebar-right-item group hover:after:bg-yellow-500">
<MdiXml class="group-hover:(!text-white)" />
<MdiXml class="group-hover:(!text-white)" @click.stop />
</div>
</a-tooltip>
<div class="dot" />
<div v-if="!isForm" class="dot" />
</template>
</Toolbar>
<div v-if="sidebarOpen" class="flex-1 flex flex-col">
<div v-if="isOpen" class="flex-1 flex flex-col">
<MenuTop @open-modal="openModal" @deleted="loadViews" @sorted="loadViews" />
<a-divider v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="my-2" />

20
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/AddRow.vue

@ -1,18 +1,20 @@
<script setup lang="ts">
import { inject, ref } from '#imports'
import { RightSidebarInj } from '~/context'
const emits = defineEmits(['addRow'])
const sidebarOpen = inject(RightSidebarInj, ref(true))
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
const isLocked = inject(IsLockedInj)
</script>
<template>
<a-tooltip :placement="sidebarOpen ? 'bottomRight' : 'left'">
<a-tooltip :placement="isOpen ? 'bottomRight' : 'left'">
<template #title> {{ $t('activity.addRow') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-primary/75 group">
<MdiPlusOutline class="cursor-pointer group-hover:(!text-white)" @click="emits('addRow')" />
<div
:class="{ 'hover:after:bg-primary/75 group': !isLocked, 'disabled-ring': isLocked }"
class="nc-sidebar-right-item nc-sidebar-add-row"
>
<MdiPlusOutline
:class="{ 'cursor-pointer group-hover:(!text-white)': !isLocked, 'disabled': isLocked }"
@click="!isLocked ? emits('addRow') : {}"
/>
</div>
</a-tooltip>
</template>

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

@ -1,16 +1,15 @@
<script setup lang="ts">
import { inject, ref, useTable } from '#imports'
import { MetaInj, RightSidebarInj } from '~/context'
import { MetaInj, inject, useTable } from '#imports'
const meta = inject(MetaInj)!
const { deleteTable } = useTable()
const sidebarOpen = inject(RightSidebarInj, ref(true))
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
</script>
<template>
<a-tooltip :placement="sidebarOpen ? 'bottomRight' : 'left'">
<a-tooltip :placement="isOpen ? 'bottomRight' : 'left'">
<template #title> {{ $t('activity.deleteTable') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-red-500 group">

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

@ -49,7 +49,7 @@ const Icon = computed(() => {
<template>
<a-dropdown max-width="350" :trigger="['click']">
<div class="nc-sidebar-right-item hover:after:bg-indigo-500 group">
<div class="nc-sidebar-right-item hover:after:bg-indigo-500 group nc-sidebar-lock-menu">
<Icon class="cursor-pointer group-hover:(!text-white)" />
</div>

7
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/Reload.vue

@ -1,16 +1,15 @@
<script setup lang="ts">
import { ReloadViewDataHookInj, RightSidebarInj } from '~/context'
import { inject, ref } from '#imports'
import { ReloadViewDataHookInj, inject } from '#imports'
const reloadHook = inject(ReloadViewDataHookInj)!
const sidebarOpen = inject(RightSidebarInj, ref(true))
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
const onClick = () => reloadHook.trigger()
</script>
<template>
<a-tooltip :placement="sidebarOpen ? 'bottomRight' : 'left'">
<a-tooltip :placement="isOpen ? 'bottomRight' : 'left'">
<template #title> {{ $t('general.reload') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-green-500 group">

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

@ -5,9 +5,12 @@ import Reload from './Reload.vue'
import ExportCache from './ExportCache.vue'
import DeleteCache from './DeleteCache.vue'
import DebugMeta from './DebugMeta.vue'
import { IsFormInj } from '#imports'
const { isUIAllowed } = useUIPermission()
const isForm = inject(IsFormInj)
const debug = $ref(false)
const clickCount = $ref(0)
@ -15,6 +18,7 @@ const clickCount = $ref(0)
<template>
<div
v-if="!isForm"
class="flex gap-2"
@click="
() => {
@ -39,18 +43,21 @@ const clickCount = $ref(0)
<div class="dot" />
</template>
<LockMenu v-if="isUIAllowed('view-type')" />
<LockMenu v-if="isUIAllowed('view-type')" @click.stop />
<div v-if="isUIAllowed('view-type')" class="dot" />
<Reload />
<Reload @click.stop />
<div class="dot" />
<AddRow v-if="isUIAllowed('xcDatatableEditable')" />
<AddRow v-if="isUIAllowed('xcDatatableEditable')" @click.stop />
<slot name="end" />
</div>
<div v-else>
<slot name="start" />
</div>
</template>
<style scoped>

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

@ -2,8 +2,24 @@
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import SmartsheetGrid from '../smartsheet/Grid.vue'
import { computed, inject, provide, useMetas, useProvideSmartsheetStore, watch, watchEffect } from '#imports'
import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj, RightSidebarInj, TabMetaInj } from '~/context'
import {
ActiveViewInj,
FieldsInj,
IsFormInj,
IsLockedInj,
MetaInj,
ReloadViewDataHookInj,
TabMetaInj,
computed,
inject,
provide,
provideSidebar,
useMetas,
useProvideSmartsheetStore,
watch,
watchEffect,
} from '#imports'
import type { TabItem } from '~/composables'
const { getMeta, metas } = useMetas()
@ -27,20 +43,27 @@ watchEffect(async () => {
const reloadEventHook = createEventHook<void>()
const { isGallery, isGrid, isForm, isLocked } = useProvideSmartsheetStore(activeView as Ref<TableType>, meta)
// provide the sidebar injection state
provideSidebar({ storageKey: 'nc-right-sidebar' })
// todo: move to store
provide(MetaInj, meta)
provide(TabMetaInj, tabMeta)
provide(ActiveViewInj, activeView)
provide(IsLockedInj, false)
provide(IsLockedInj, isLocked)
provide(ReloadViewDataHookInj, reloadEventHook)
provide(FieldsInj, fields)
provide(RightSidebarInj, ref(false))
provide(IsFormInj, isForm)
const { isGallery, isGrid, isForm } = useProvideSmartsheetStore(activeView as Ref<TableType>, meta)
const treeViewIsLockedInj = inject('TreeViewIsLockedInj', ref(false))
watch(tabMeta, async (newTabMeta, oldTabMeta) => {
if (newTabMeta !== oldTabMeta && newTabMeta?.id) await getMeta(newTabMeta.id)
})
watch(isLocked, (nextValue) => (treeViewIsLockedInj.value = nextValue), { immediate: true })
</script>
<template>

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

@ -192,7 +192,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="text-gray-500">Reload</div>
</div>
</a-button>
<a-button v-if="isUIAllowed('newUser')" size="middle" type="primary" ghost @click="onInvite">
<a-button v-if="isUIAllowed('newUser')" size="middle" type="primary" ghost class="nc-invite-team" @click="onInvite">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MdiAccountPlusOutline class="mr-1" />
<div>{{ $t('activity.inviteTeam') }}</div>
@ -217,13 +217,13 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div>
</div>
<div v-for="(user, index) of users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2">
<div class="flex w-4/6 flex-wrap">
<div v-for="(user, index) of users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2 nc-user-row">
<div class="flex w-4/6 flex-wrap nc-user-email">
{{ user.email }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
<div class="rounded-full px-2 py-1" :style="{ backgroundColor: projectRoleTagColors[user.roles] }">
<div class="rounded-full px-2 py-1 nc-user-role" :style="{ backgroundColor: projectRoleTagColors[user.roles] }">
{{ user.roles }}
</div>
</div>
@ -232,7 +232,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<template #title>
<span>{{ $t('activity.editUser') }}</span>
</template>
<a-button type="text" class="!rounded-md" @click="onEdit(user)">
<a-button type="text" class="!rounded-md nc-user-edit" @click="onEdit(user)">
<template #icon>
<IcRoundEdit class="flex mx-auto h-[1rem] text-gray-500" />
</template>
@ -242,7 +242,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<template #title>
<span>Add user to the project</span>
</template>
<a-button type="text" class="!rounded-md" @click="inviteUser(user)">
<a-button type="text" class="!rounded-md nc-user-invite" @click="inviteUser(user)">
<template #icon>
<MdiPlus class="flex mx-auto h-[1.1rem] text-gray-500" />
</template>
@ -253,7 +253,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<template #title>
<span>Remove user from the project</span>
</template>
<a-button type="text" class="!rounded-md" @click="onDelete(user)">
<a-button type="text" class="!rounded-md nc-user-delete" @click="onDelete(user)">
<template #icon>
<MdiDeleteOutline class="flex mx-auto h-[1.1rem] text-gray-500" />
</template>

4
packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue

@ -205,8 +205,8 @@ const clickInviteMore = () => {
<div class="flex flex-col w-1/4">
<a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]">
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div>
<a-select v-model:value="usersData.role">
<a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role">
<a-select v-model:value="usersData.role" class="nc-user-roles">
<a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role" class="nc-role-option">
<div class="flex flex-row h-full justify-start items-center">
<div
class="px-2 py-1 flex rounded-full text-xs"

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

@ -33,6 +33,8 @@ const active = inject(ActiveCellInj)!
const readonly = inject(ReadonlyInj, false)
const isLocked = inject(IsLockedInj)
const listItemsDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
@ -72,10 +74,10 @@ const unlinkRef = async (rec: Record<string, any>) => {
<ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" />
</template>
</div>
<div v-if="!readonly" class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<div v-if="!readonly || !isLocked" class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<component
:is="addIcon"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500)"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500) nc-plus"
@click="listItemsDlg = true"
/>
</div>

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

@ -5,6 +5,7 @@ import {
CellValueInj,
ColumnInj,
IsFormInj,
IsLockedInj,
ReadonlyInj,
ReloadViewDataHookInj,
RowInj,
@ -34,6 +35,8 @@ const isForm = inject(IsFormInj)
const readonly = inject(ReadonlyInj, false)
const isLocked = inject(IsLockedInj)
const listItemsDlg = ref(false)
const childListDlg = ref(false)
@ -49,9 +52,9 @@ await loadRelatedTableMeta()
const localCellValue = computed(() => {
if (cellValue?.value) {
return cellValue?.value
return cellValue?.value ?? []
} else if (isNew.value) {
return state?.value?.[column?.value.title as string]
return state?.value?.[column?.value.title as string] ?? []
}
return []
})
@ -83,19 +86,19 @@ const unlinkRef = async (rec: Record<string, any>) => {
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<ItemChip v-for="(cell, i) of cells" :key="i" :item="cell.item" :value="cell.value" @unlink="unlinkRef(cell.item)" />
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true"
>more...
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">
more...
</span>
</template>
</div>
<div class="flex-grow flex justify-end gap-1 min-h-[30px] align-center">
<div v-if="!isLocked" class="flex-grow flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click="childListDlg = true"
/>
<MdiPlus
v-if="!readonly"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click="listItemsDlg = true"
/>
</div>

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

@ -64,7 +64,7 @@ const lookupColumnMetaProps = useColumn(lookupColumn)
:key="i"
:class="{ 'bg-gray-100 px-2 rounded-full': !lookupColumnMetaProps.isAttachment }"
>
<SmartsheetCell :model-value="v" :column="lookupColumn" :edit-enabled="false" />
<SmartsheetCell :model-value="v" :column="lookupColumn" :edit-enabled="false" :virtual="true" />
</div>
</template>
</template>

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

@ -5,6 +5,7 @@ import {
CellValueInj,
ColumnInj,
IsFormInj,
IsLockedInj,
ReadonlyInj,
ReloadViewDataHookInj,
RowInj,
@ -33,6 +34,8 @@ const isForm = inject(IsFormInj)
const readonly = inject(ReadonlyInj, false)
const isLocked = inject(IsLockedInj)
const listItemsDlg = ref(false)
const childListDlg = ref(false)
@ -49,9 +52,9 @@ await loadRelatedTableMeta()
const localCellValue = computed(() => {
if (cellValue?.value) {
return cellValue?.value
return cellValue?.value ?? []
} else if (isNew.value) {
return state?.value?.[column?.value.title as string]
return state?.value?.[column?.value.title as string] ?? []
}
return []
})
@ -88,12 +91,15 @@ const unlinkRef = async (rec: Record<string, any>) => {
</template>
</div>
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="childListDlg = true" />
<div v-if="!isLocked" class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click="childListDlg = true"
/>
<MdiPlus
v-if="!readonly"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click="listItemsDlg = true"
/>
</div>

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

@ -20,6 +20,8 @@ const active = inject(ActiveCellInj, ref(false))
const isForm = inject(IsFormInj)!
const isLocked = inject(IsLockedInj, ref(false))
const expandedFormDlg = ref(false)
</script>
@ -37,13 +39,13 @@ export default {
>
<span class="name">{{ value }}</span>
<div v-show="active || isForm" v-if="!readonly" class="flex align-center">
<div v-show="active || isForm" v-if="!readonly && !isLocked" class="flex align-center">
<MdiCloseThick class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click.stop="emit('unlink')" />
</div>
<Suspense>
<ExpandedForm
v-if="!readonly"
v-if="!readonly && !isLocked && expandedFormDlg"
v-model="expandedFormDlg"
:row="{ row: item }"
:meta="relatedTableMeta"

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

@ -11,6 +11,7 @@ import {
useVModel,
watch,
} from '#imports'
import { IsPublicInj } from '~/context'
const props = defineProps<{ modelValue?: boolean }>()
@ -22,6 +23,8 @@ const vModel = useVModel(props, 'modelValue', emit)
const isForm = inject(IsFormInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false))
const column = inject(ColumnInj)
const readonly = inject(ReadonlyInj, false)
@ -117,7 +120,7 @@ const expandedFormRow = ref()
@click.stop="unlinkRow(row)"
/>
<MdiDeleteOutline
v-if="!readonly"
v-if="!readonly && !isPublic"
class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
@click.stop="deleteRelatedRow(row, unlinkIfNewRow)"
/>
@ -145,7 +148,7 @@ const expandedFormRow = ref()
<Suspense>
<ExpandedForm
v-if="expandedFormRow"
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:row="{ row: expandedFormRow }"
:meta="relatedTableMeta"

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

@ -13,6 +13,7 @@ import {
useVModel,
watch,
} from '#imports'
import { IsPublicInj } from '~/context'
const props = defineProps<{ modelValue: boolean }>()
@ -38,6 +39,8 @@ const {
const { addLTARRef, isNew } = useSmartsheetRowStoreOrThrow()
const isPublic = inject(IsPublicInj, ref(false))
const linkRow = async (row: Record<string, any>) => {
if (isNew.value) {
addLTARRef(row, column?.value as ColumnType)
@ -104,8 +107,8 @@ const newRowState = computed(() => {
size="small"
></a-input>
<div class="flex-1" />
<MdiReload class="cursor-pointer text-gray-500" @click="loadChildrenExcludedList" />
<a-button type="primary" size="small" @click="expandedFormDlg = true">Add new record</a-button>
<MdiReload class="cursor-pointer text-gray-500 nc-reload" @click="loadChildrenExcludedList" />
<a-button v-if="!isPublic" type="primary" size="small" @click="expandedFormDlg = true">Add new record</a-button>
</div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0">

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

@ -21,3 +21,4 @@ export * from './useColumnCreateStore'
export * from './useSmartsheetStore'
export * from './useLTARStore'
export * from './useExpandedFormStore'
export * from './useSharedFormViewStore'

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

@ -56,10 +56,14 @@ export function useColumn(column: Ref<ColumnType>) {
const isManualSaved = computed(() =>
[UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(uiDatatype.value),
)
const isPrimary = computed(() => {
return column?.value?.pv
})
return {
abstractType,
dataTypeLow,
isPrimary,
isBoolean,
isString,
isTextArea,

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

@ -2,6 +2,7 @@ import type { ColumnType, LinkToAnotherRecordType, PaginatedType, TableType } fr
import type { ComputedRef, Ref } from 'vue'
import { Modal, message } from 'ant-design-vue'
import {
IsPublicInj,
NOCO,
computed,
extractSdkResponseErrorMsg,
@ -13,6 +14,7 @@ import {
useProject,
} from '#imports'
import type { Row } from '~/composables'
import { SharedViewPasswordInj } from '~/context'
interface DataApiResponse {
list: Record<string, any>
@ -26,6 +28,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const { metas, getMeta } = useMetas()
const { project } = useProject()
const { $api } = useNuxtApp()
const sharedViewPassword = inject(SharedViewPasswordInj, ref(null))
const childrenExcludedList = ref<DataApiResponse | undefined>()
const childrenList = ref<DataApiResponse | undefined>()
const childrenExcludedListPagination = reactive({
@ -39,6 +42,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
size: 10,
})
const isPublic: boolean = $(inject(IsPublicInj, ref(false)))
const colOptions = $computed(() => column?.value.colOptions as LinkToAnotherRecordType)
// getters
@ -79,8 +84,29 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const loadChildrenExcludedList = async () => {
try {
/** if new row load all records */
if (isNewRow?.value) {
if (isPublic) {
const route = useRoute()
childrenExcludedList.value = await $api.public.dataRelationList(
route.params.viewId as string,
column?.value?.id,
{},
{
headers: {
'xc-password': sharedViewPassword.value,
},
query: {
limit: childrenExcludedListPagination.size,
offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
where:
childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
fields: [relatedTablePrimaryValueProp.value, ...relatedTablePrimaryKeyProps.value],
} as any,
},
)
/** if new row load all records */
} else if (isNewRow?.value) {
childrenExcludedList.value = await $api.dbTableRow.list(
NOCO,
project.value.id as string,

197
packages/nc-gui-v2/composables/useSharedFormViewStore.ts

@ -0,0 +1,197 @@
import useVuelidate from '@vuelidate/core'
import { minLength, required } from '@vuelidate/validators'
import { message } from 'ant-design-vue'
import type { ColumnType, FormType, LinkToAnotherRecordType, TableType, ViewType } from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { SharedViewPasswordInj } from '~/context'
import { extractSdkResponseErrorMsg } from '~/utils'
import { useInjectionState, useMetas } from '#imports'
const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((sharedViewId: string) => {
const progress = ref(false)
const notFound = ref(false)
const submitted = ref(false)
const passwordDlg = ref(false)
const password = ref<string | null>(null)
const secondsRemain = ref(0)
provide(SharedViewPasswordInj, password)
const sharedView = ref<ViewType>()
const sharedFormView = ref<FormType>()
const meta = ref<TableType>()
const columns = ref<(ColumnType & { required?: boolean; show?: boolean })[]>()
const { $api } = useNuxtApp()
const { metas, setMeta } = useMetas()
const formState = ref({})
const { state: additionalState } = useProvideSmartsheetRowStore(
meta as Ref<TableType>,
ref({
row: formState,
rowMeta: { new: true },
oldRow: {},
}),
)
const formColumns = computed(() =>
columns?.value?.filter((c) => c.show)?.filter((col) => !isVirtualCol(col) || col.uidt === UITypes.LinkToAnotherRecord),
)
const loadSharedView = async () => {
try {
const viewMeta = await $api.public.sharedViewMetaGet(sharedViewId, {
headers: {
'xc-password': password.value,
},
})
passwordDlg.value = false
sharedView.value = viewMeta
sharedFormView.value = viewMeta.view
meta.value = viewMeta.model
columns.value = viewMeta.model?.columns
setMeta(viewMeta.model)
const relatedMetas = { ...viewMeta.relatedMetas }
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))
} catch (e: any) {
if (e.response && e.response.status === 404) {
notFound.value = true
} else if ((await extractSdkResponseErrorMsg(e)) === ErrorMessages.INVALID_SHARED_VIEW_PASSWORD) {
passwordDlg.value = true
}
}
}
const validators = computed(() => {
const obj: any = {
localState: {},
virtual: {},
}
for (const column of formColumns?.value ?? []) {
if (
!isVirtualCol(column) &&
((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || (column as any).required)
) {
obj.localState[column.title!] = { required }
} else if (
column.uidt === UITypes.LinkToAnotherRecord &&
column.colOptions &&
(column.colOptions as LinkToAnotherRecordType).type === RelationTypes.BELONGS_TO
) {
const col = columns.value?.find((c) => c.id === (column?.colOptions as LinkToAnotherRecordType)?.fk_child_column_id)
if ((col && col.rqd && !col.cdf) || column.required) {
if (col) {
obj.virtual[column.title!] = { required }
}
}
} else if (isVirtualCol(column) && column.required) {
obj.virtual[column.title!] = {
minLength: minLength(1),
required,
}
}
}
return obj
})
const v$ = useVuelidate(
validators,
computed(() => ({ localState: formState?.value, virtual: additionalState?.value })),
)
const submitForm = async () => {
try {
if (!(await v$.value?.$validate())) {
return
}
progress.value = true
const data: Record<string, any> = { ...(formState?.value ?? {}), ...(additionalState?.value || {}) }
const attachment: Record<string, any> = {}
for (const col of metas?.value?.[sharedView?.value?.fk_model_id as string]?.columns ?? []) {
if (col.uidt === UITypes.Attachment) {
attachment[`_${col.title}`] = data[col.title!]
delete data[col.title!]
}
}
await $api.public.dataCreate(
sharedView?.value?.uuid as string,
{
data,
...attachment,
},
{
headers: {
'xc-password': password.value,
},
},
)
submitted.value = true
progress.value = false
await message.success(sharedFormView.value?.success_msg || 'Saved successfully.')
} catch (e: any) {
console.log(e)
await message.error(await extractSdkResponseErrorMsg(e))
}
progress.value = false
}
/** reset form if show_blank_form is true */
watch(submitted, (nextVal: boolean) => {
if (nextVal && sharedFormView.value?.show_blank_form) {
secondsRemain.value = 5
const intvl = setInterval(() => {
secondsRemain.value = secondsRemain.value - 1
if (secondsRemain.value < 0) {
submitted.value = false
clearInterval(intvl)
}
}, 1000)
}
/** reset form state and validation */
if (!nextVal) {
additionalState.value = {}
formState.value = {}
v$.value?.$reset()
}
})
return {
sharedView,
sharedFormView,
loadSharedView,
columns,
submitForm,
progress,
meta,
validators,
v$,
formColumns,
formState,
notFound,
password,
submitted,
secondsRemain,
passwordDlg,
}
}, 'expanded-form-store')
export { useProvideSharedFormStore }
export function useSharedFormStoreOrThrow() {
const sharedFormStore = useSharedFormStore()
if (sharedFormStore == null) throw new Error('Please call `useProvideSharedFormStore` on the appropriate parent component')
return sharedFormStore
}

18
packages/nc-gui-v2/composables/useSharedView.ts

@ -1,4 +1,4 @@
import type { ColumnType, ExportTypes, FilterType, PaginatedType, SortType, TableType, ViewType } from 'nocodb-sdk'
import type { ExportTypes, FilterType, PaginatedType, SortType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { useNuxtApp } from '#app'
@ -13,11 +13,10 @@ export function useSharedView() {
const password = useState<string | undefined>('password')
const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false)
const meta = ref<TableType>(sharedView.value?.model)
const columns = ref<ColumnType[]>(sharedView.value?.model?.columns)
const meta = useState<TableType>('meta')
const formColumns = computed(
() =>
columns.value
meta.value.columns
.filter(
(f: Record<string, any>) =>
f.show && f.uidt !== UITypes.Rollup && f.uidt !== UITypes.Lookup && f.uidt !== UITypes.Formula,
@ -39,10 +38,14 @@ export function useSharedView() {
allowCSVDownload.value = JSON.parse(viewMeta.meta).allowCSVDownload
if (localPassword) password.value = localPassword
sharedView.value = viewMeta
sharedView.value = { ...viewMeta }
meta.value = { ...viewMeta.model }
meta.value = viewMeta.model
columns.value = viewMeta.model.columns
let order = 1
meta.value.columns = [...viewMeta.model.columns]
.filter((c) => c.show)
.map((c) => ({ ...c, order: order++ }))
.sort((a, b) => a.order - b.order)
setMeta(viewMeta.model)
@ -96,7 +99,6 @@ export function useSharedView() {
sharedView,
loadSharedView,
meta,
columns,
nestedFilters,
fetchSharedViewData,
paginationData,

22
packages/nc-gui-v2/composables/useSidebar/index.ts

@ -1,8 +1,10 @@
import { useInjectionState, useToggle, watch } from '#imports'
import { useStorage } from '@vueuse/core'
import { useInjectionState, watch } from '#imports'
interface UseSidebarProps {
hasSidebar?: boolean
isOpen?: boolean
storageKey?: string // if a storageKey is passed, use that key for localStorage
}
/**
@ -14,8 +16,22 @@ interface UseSidebarProps {
* If `provideSidebar` is not called explicitly, `useSidebar` will trigger the provider if no injection state can be found
*/
const [setup, use] = useInjectionState((props: UseSidebarProps = {}) => {
const [isOpen, toggle] = useToggle(props.isOpen ?? false)
const [hasSidebar, toggleHasSidebar] = useToggle(props.hasSidebar ?? true)
let isOpen = ref(props.isOpen ?? false)
let hasSidebar = ref(props.hasSidebar ?? true)
function toggle(state?: boolean) {
isOpen.value = state ?? !isOpen.value
}
function toggleHasSidebar(state?: boolean) {
hasSidebar.value = state ?? !hasSidebar.value
}
if (props.storageKey) {
const storage = toRefs(useStorage(props.storageKey, { isOpen, hasSidebar }, localStorage, { mergeDefaults: true }).value)
isOpen = storage.isOpen
hasSidebar = storage.hasSidebar
}
watch(
hasSidebar,

97
packages/nc-gui-v2/composables/useSmartsheetStore.ts

@ -3,53 +3,58 @@ import type { TableType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { computed, reactive, useInjectionState, useNuxtApp, useProject, useTemplateRefsList } from '#imports'
const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState((view: Ref<ViewType>, meta: Ref<TableType>) => {
const { $api } = useNuxtApp()
const { sqlUi } = useProject()
const cellRefs = useTemplateRefsList<HTMLTableDataCellElement>()
// state
// todo: move to grid view store
const search = reactive({
field: '',
query: '',
})
// getters
const isLocked = computed(() => (view?.value as any)?.lock_type === 'locked')
const isPkAvail = computed(() => meta?.value?.columns?.some((c) => c.pk))
const isGrid = computed(() => (view?.value as any)?.type === ViewTypes.GRID)
const isForm = computed(() => (view?.value as any)?.type === ViewTypes.FORM)
const isGallery = computed(() => (view?.value as any)?.type === ViewTypes.GALLERY)
const xWhere = computed(() => {
let where
const col = meta?.value?.columns?.find(({ id }) => id === search.field) || meta?.value?.columns?.find((v) => v.pv)
if (!col) return
if (!search.query.trim()) return
if (['text', 'string'].includes(sqlUi.value.getAbstractType(col)) && col.dt !== 'bigint') {
where = `(${col.title},like,%${search.query.trim()}%)`
} else {
where = `(${col.title},eq,${search.query.trim()})`
const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
(view: Ref<ViewType>, meta: Ref<TableType>, shared = false) => {
const { $api } = useNuxtApp()
const { sqlUi } = useProject()
const cellRefs = useTemplateRefsList<HTMLTableDataCellElement>()
// state
// todo: move to grid view store
const search = reactive({
field: '',
query: '',
})
// getters
const isLocked = computed(() => (view?.value as any)?.lock_type === 'locked')
const isPkAvail = computed(() => meta?.value?.columns?.some((c) => c.pk))
const isGrid = computed(() => (view?.value as any)?.type === ViewTypes.GRID)
const isForm = computed(() => (view?.value as any)?.type === ViewTypes.FORM)
const isSharedForm = computed(() => isForm?.value && shared)
const isGallery = computed(() => (view?.value as any)?.type === ViewTypes.GALLERY)
const xWhere = computed(() => {
let where
const col = meta?.value?.columns?.find(({ id }) => id === search.field) || meta?.value?.columns?.find((v) => v.pv)
if (!col) return
if (!search.query.trim()) return
if (['text', 'string'].includes(sqlUi.value.getAbstractType(col)) && col.dt !== 'bigint') {
where = `(${col.title},like,%${search.query.trim()}%)`
} else {
where = `(${col.title},eq,${search.query.trim()})`
}
return where
})
return {
view,
meta,
isLocked,
$api,
search,
xWhere,
isPkAvail,
isForm,
isGrid,
isGallery,
cellRefs,
isSharedForm,
}
return where
})
return {
view,
meta,
isLocked,
$api,
search,
xWhere,
isPkAvail,
isForm,
isGrid,
isGallery,
cellRefs,
}
}, 'smartsheet-store')
},
'smartsheet-store',
)
export { useProvideSmartsheetStore }

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

@ -4,18 +4,11 @@ import { watch } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import { useNuxtApp } from '#app'
import { IsPublicInj } from '#imports'
import type { Field } from '~/lib'
export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRef<TableType>, reloadData?: () => void) {
const isPublic = inject(IsPublicInj, ref(false))
const fields = ref<
{
order: number
show: number | boolean
title: string
fk_column_id?: string
system?: boolean
}[]
>()
const fields = ref<Field[]>()
const filterQuery = ref('')
@ -39,7 +32,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
}
}, {})
fields.value = meta.value?.columns
?.map((column) => {
?.map((column: ColumnType) => {
const currentColumnField = fieldById[column.id!] || {}
return {
@ -50,13 +43,13 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
system: isSystemColumn(currentColumnField.type || false),
}
})
.sort((a, b) => a.order - b.order)
.sort((a: Field, b: Field) => a.order - b.order)
}
}
const showAll = async (ignoreIds?: any) => {
if (isPublic.value) {
fields.value = fields.value?.map((field) => ({
fields.value = fields.value?.map((field: Field) => ({
...field,
show: true,
}))
@ -79,7 +72,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
}
const hideAll = async (ignoreIds?: any) => {
if (isPublic.value) {
fields.value = fields.value?.map((field) => ({
fields.value = fields.value?.map((field: Field) => ({
...field,
show: false,
}))
@ -101,9 +94,9 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
}
const saveOrUpdate = async (field: any, index: number) => {
if (isPublic && fields.value) {
if (isPublic.value && fields.value) {
fields.value[index] = field
meta.value.columns = meta.value?.columns?.map((column) => {
meta.value.columns = meta.value?.columns?.map((column: ColumnType) => {
if (column.id === field.fk_column_id) {
return {
...column,
@ -128,14 +121,15 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
reloadData?.()
}
const metaColumnById = computed(() => {
return (
meta.value?.columns?.reduce<Record<string, ColumnType>>((acc, curr) => {
return {
...acc,
[curr.id!]: curr,
}
}, {}) || {}
const metaColumnById = computed<Record<string, ColumnType>>(() => {
if (!meta.value?.columns) return {}
return meta.value?.columns?.reduce(
(acc: ColumnType, curr: ColumnType) => ({
...acc,
[curr.id!]: curr,
}),
{} as any,
)
})
@ -160,33 +154,37 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
const filteredFieldList = computed(() => {
return (
fields.value?.filter((field) => {
fields.value?.filter((field: Field) => {
// hide system columns if not enabled
if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) {
return false
}
return !filterQuery?.value || field.title.toLowerCase().includes(filterQuery.value.toLowerCase())
}) || {}
if (filterQuery.value === '') {
return true
} else {
return field.title.toLowerCase().includes(filterQuery.value.toLowerCase())
}
}) || []
)
})
const sortedAndFilteredFields = computed<ColumnType[]>(() => {
return (fields?.value
?.filter((c) => {
?.filter((field: Field) => {
// hide system columns if not enabled
if (
!showSystemFields.value &&
metaColumnById.value &&
metaColumnById?.value?.[c.fk_column_id!] &&
isSystemColumn(metaColumnById.value?.[c.fk_column_id!])
metaColumnById?.value?.[field.fk_column_id!] &&
isSystemColumn(metaColumnById.value?.[field.fk_column_id!])
) {
return false
}
return c.show && metaColumnById?.value?.[c.fk_column_id!]
return field.show && metaColumnById?.value?.[field.fk_column_id!]
})
?.sort((a, b) => a.order - b.order)
?.map((c) => metaColumnById?.value?.[c.fk_column_id!]) || []) as ColumnType[]
?.sort((a: Field, b: Field) => a.order - b.order)
?.map((field: Field) => metaColumnById?.value?.[field.fk_column_id!]) || []) as ColumnType[]
})
// reload view columns when table meta changes

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

@ -16,12 +16,12 @@ export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['pag
export const ChangePageInj: InjectionKey<ReturnType<typeof useViewData>['changePage']> = Symbol('pagination-data-injection')
export const IsFormInj: InjectionKey<Ref<boolean>> = Symbol('is-form-injection')
export const IsGridInj: InjectionKey<boolean> = Symbol('is-grid-injection')
export const IsLockedInj: InjectionKey<boolean> = Symbol('is-locked-injection')
export const IsLockedInj: InjectionKey<Ref<boolean>> = Symbol('is-locked-injection')
export const CellValueInj: InjectionKey<Ref<any>> = Symbol('cell-value-injection')
export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection')
export const ReadonlyInj: InjectionKey<boolean> = Symbol('readonly-injection')
export const ReloadViewDataHookInj: InjectionKey<EventHook<void>> = Symbol('reload-view-data-injection')
export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection')
export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection')
export const RightSidebarInj: InjectionKey<Ref<boolean>> = Symbol('right-sidebar-injection')
export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injection')
export const SharedViewPasswordInj: InjectionKey<Ref<string | null>> = Symbol('shared-view-password-injection')

9
packages/nc-gui-v2/layouts/base.vue

@ -22,7 +22,10 @@ const logout = () => {
<a-layout class="!flex-col">
<Transition name="layout">
<a-layout-header v-if="signedIn" class="flex !bg-primary items-center text-white pl-4 pr-5 shadow-lg">
<a-layout-header
v-if="!route.meta.public && signedIn"
class="flex !bg-primary items-center text-white pl-4 pr-5 shadow-lg"
>
<div
v-if="
route.name === 'index' || route.name === 'project-index-create' || route.name === 'project-index-create-external'
@ -48,7 +51,7 @@ const logout = () => {
<GeneralShareBaseButton />
<a-tooltip placement="bottom">
<template #title> Switch language </template>
<template #title> Switch language</template>
<div class="flex pr-4 items-center">
<GeneralLanguage class="cursor-pointer text-2xl" />
@ -88,7 +91,7 @@ const logout = () => {
</Transition>
<a-tooltip placement="bottom">
<template #title> Switch language </template>
<template #title> Switch language</template>
<Transition name="layout">
<div v-if="!signedIn" class="nc-lang-btn">

17
packages/nc-gui-v2/layouts/shared-view.vue

@ -1,5 +1,7 @@
<script lang="ts" setup>
import { navigateTo } from '#app'
const { isLoading } = useGlobal()
const { sharedView } = useSharedView()
</script>
<script lang="ts">
@ -10,12 +12,25 @@ export default {
<template>
<a-layout id="nc-app">
<a-layout class="!flex-col">
<a-layout class="!flex-col bg-white">
<a-layout-header class="flex !bg-primary items-center text-white pl-3 pr-4 shadow-lg">
<div class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105" @click="navigateTo('/')">
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<div>
<div class="flex justify-center items-center">
<div class="flex items-center gap-2 ml-3">
<template v-if="isLoading">
{{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
</template>
<div v-else class="text-xl font-semibold truncate">
{{ sharedView?.title }}
</div>
</div>
</div>
</div>
<div class="flex-1" />
</a-layout-header>

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

@ -21,6 +21,14 @@ export interface ProjectMetaInfo {
PackageVersion?: string
}
export interface Field {
order: number
show: number | boolean
title: string
fk_column_id?: string
system?: boolean
}
export type Roles = Record<Role, boolean>
export type Filter = FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string; readOnly?: boolean }

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

@ -23,9 +23,14 @@ import { useGlobal } from '#imports'
export default defineNuxtRouteMiddleware((to, from) => {
const state = useGlobal()
/** if public allow */
if (to.meta.public) return
/** if shred base allow without validating */
if (to.params?.projectType === 'base') return
if (to.meta.public) return
/** if auth is required or unspecified (same as required) and user is not signed in, redirect to signin page */
if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) {
return navigateTo('/signin')

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

@ -6,6 +6,8 @@
"": {
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@vuelidate/core": "^2.0.0-alpha.44",
"@vuelidate/validators": "^2.0.0-alpha.31",
"@vueuse/core": "^9.0.2",
"@vueuse/integrations": "^9.0.2",
"ant-design-vue": "^3.2.10",
@ -37,6 +39,7 @@
"@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25",
"@iconify-json/mi": "^1.1.2",
"@iconify-json/ri": "^1.1.3",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
@ -1056,6 +1059,15 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/mi": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@iconify-json/mi/-/mi-1.1.2.tgz",
"integrity": "sha512-NPyKBxoui8Oude4ChB80iBLRdI22xpM6JFFYpDHycxREQgzfVyoOuX1ukpbWOe+IkEVvFFLZzSDOlPcipn1cgw==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/ri": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@iconify-json/ri/-/ri-1.1.3.tgz",
@ -2862,6 +2874,72 @@
"vue": "^3.0.1"
}
},
"node_modules/@vuelidate/core": {
"version": "2.0.0-alpha.44",
"resolved": "https://registry.npmjs.org/@vuelidate/core/-/core-2.0.0-alpha.44.tgz",
"integrity": "sha512-3DlCe3E0RRXbB+OfPacUetKhLmXzmnjeHkzjnbkc03p06mKm6h9pXR5pd6Mv4s4tus4sieuKDb2YWNmKK6rQeA==",
"dependencies": {
"vue-demi": "^0.13.4"
}
},
"node_modules/@vuelidate/core/node_modules/vue-demi": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.8.tgz",
"integrity": "sha512-Vy1zbZhCOdsmvGR6tJhAvO5vhP7eiS8xkbYQSoVa7o6KlIy3W8Rc53ED4qI4qpeRDjv3mLfXSEpYU6Yq4pgXRg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vuelidate/validators": {
"version": "2.0.0-alpha.31",
"resolved": "https://registry.npmjs.org/@vuelidate/validators/-/validators-2.0.0-alpha.31.tgz",
"integrity": "sha512-+MFA9nZ7Y9zCpq383/voPDk/hiAmu6KqiJJhLOYB/FmrUPVoyKnuKnI9Bwiq8ok9GZlVkI8BnIrKPKGj9QpwiQ==",
"dependencies": {
"vue-demi": "^0.13.4"
}
},
"node_modules/@vuelidate/validators/node_modules/vue-demi": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.8.tgz",
"integrity": "sha512-Vy1zbZhCOdsmvGR6tJhAvO5vhP7eiS8xkbYQSoVa7o6KlIy3W8Rc53ED4qI4qpeRDjv3mLfXSEpYU6Yq4pgXRg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vuetify/loader-shared": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-1.5.0.tgz",
@ -15940,6 +16018,15 @@
"@iconify/types": "*"
}
},
"@iconify-json/mi": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@iconify-json/mi/-/mi-1.1.2.tgz",
"integrity": "sha512-NPyKBxoui8Oude4ChB80iBLRdI22xpM6JFFYpDHycxREQgzfVyoOuX1ukpbWOe+IkEVvFFLZzSDOlPcipn1cgw==",
"dev": true,
"requires": {
"@iconify/types": "*"
}
},
"@iconify-json/ri": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@iconify-json/ri/-/ri-1.1.3.tgz",
@ -17366,6 +17453,38 @@
"dev": true,
"requires": {}
},
"@vuelidate/core": {
"version": "2.0.0-alpha.44",
"resolved": "https://registry.npmjs.org/@vuelidate/core/-/core-2.0.0-alpha.44.tgz",
"integrity": "sha512-3DlCe3E0RRXbB+OfPacUetKhLmXzmnjeHkzjnbkc03p06mKm6h9pXR5pd6Mv4s4tus4sieuKDb2YWNmKK6rQeA==",
"requires": {
"vue-demi": "^0.13.4"
},
"dependencies": {
"vue-demi": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.8.tgz",
"integrity": "sha512-Vy1zbZhCOdsmvGR6tJhAvO5vhP7eiS8xkbYQSoVa7o6KlIy3W8Rc53ED4qI4qpeRDjv3mLfXSEpYU6Yq4pgXRg==",
"requires": {}
}
}
},
"@vuelidate/validators": {
"version": "2.0.0-alpha.31",
"resolved": "https://registry.npmjs.org/@vuelidate/validators/-/validators-2.0.0-alpha.31.tgz",
"integrity": "sha512-+MFA9nZ7Y9zCpq383/voPDk/hiAmu6KqiJJhLOYB/FmrUPVoyKnuKnI9Bwiq8ok9GZlVkI8BnIrKPKGj9QpwiQ==",
"requires": {
"vue-demi": "^0.13.4"
},
"dependencies": {
"vue-demi": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.8.tgz",
"integrity": "sha512-Vy1zbZhCOdsmvGR6tJhAvO5vhP7eiS8xkbYQSoVa7o6KlIy3W8Rc53ED4qI4qpeRDjv3mLfXSEpYU6Yq4pgXRg==",
"requires": {}
}
}
},
"@vuetify/loader-shared": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-1.5.0.tgz",

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

@ -12,6 +12,8 @@
},
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@vuelidate/core": "^2.0.0-alpha.44",
"@vuelidate/validators": "^2.0.0-alpha.31",
"@vueuse/core": "^9.0.2",
"@vueuse/integrations": "^9.0.2",
"ant-design-vue": "^3.2.10",
@ -43,6 +45,7 @@
"@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25",
"@iconify-json/mi": "^1.1.2",
"@iconify-json/ri": "^1.1.3",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",

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

@ -4,16 +4,19 @@ import {
navigateTo,
onKeyStroke,
openLink,
provide,
provideSidebar,
ref,
useClipboard,
useElementHover,
useGlobal,
useProject,
useRoute,
useTabs,
useUIPermission,
} from '#imports'
import { TabType } from '~/composables'
const route = useRoute()
const { appInfo, token } = useGlobal()
@ -26,6 +29,10 @@ const { isUIAllowed } = useUIPermission()
const { copy } = useClipboard()
const isLocked = ref(false)
provide('TreeViewIsLockedInj', isLocked)
// create a new sidebar state
const { isOpen, toggle } = provideSidebar({ isOpen: true })
@ -66,11 +73,13 @@ const isHovered = useElementHover(sidebar)
const copyProjectInfo = async () => {
try {
await loadProjectMetaInfo()
copy(
await copy(
Object.entries(projectMetaInfo.value!)
.map(([k, v]) => `${k}: **${v}**`)
.join('\n'),
)
message.info('Copied project info to clipboard')
} catch (e: any) {
console.log(e)
@ -80,7 +89,8 @@ const copyProjectInfo = async () => {
const copyAuthToken = async () => {
try {
copy(token.value!)
await copy(token.value!)
message.info('Copied auth token to clipboard')
} catch (e: any) {
console.log(e)
@ -134,11 +144,11 @@ const copyAuthToken = async () => {
</template>
</div>
<a-dropdown v-else :trigger="['click']" placement="bottom">
<a-dropdown v-else class="h-full" :trigger="['click']" placement="bottom">
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"
class="group cursor-pointer flex gap-4 items-center nc-project-menu"
class="group cursor-pointer flex gap-4 items-center nc-project-menu overflow-hidden"
>
<template v-if="isOpen">
<div class="text-xl font-semibold truncate">{{ project.title }}</div>
@ -177,6 +187,8 @@ const copyAuthToken = async () => {
</div>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="api">
<div
v-if="isUIAllowed('apiDocs')"
@ -191,7 +203,7 @@ const copyAuthToken = async () => {
<a-menu-item key="copy">
<div v-t="['a:navbar:user:copy-auth-token']" class="nc-project-menu-item group" @click.stop="copyAuthToken">
<MdiContentCopy class="group-hover:text-pink-500 nc-copy-project-info" />
<MdiScriptTextKeyOutline class="group-hover:text-pink-500 nc-copy-project-info" />
Copy Auth Token
</div>
</a-menu-item>
@ -215,7 +227,7 @@ const copyAuthToken = async () => {
<a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as" v-t="['c:navdraw:preview-as']">
<template #title>
<div class="nc-project-menu-item group">
<MdiContentCopy class="group-hover:text-pink-500 nc-project-preview" />
<MdiFileEyeOutline class="group-hover:text-pink-500 nc-project-preview" />
Preview Project As
<div class="flex-1" />
@ -272,10 +284,6 @@ const copyAuthToken = async () => {
</template>
<style lang="scss" scoped>
.nc-project-menu-item {
@apply cursor-pointer flex items-center gap-2 py-2 hover:text-primary after:(content-[''] absolute top-0 left-0 bottom-0 right-0 w-full h-full bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
@ -287,8 +295,4 @@ const copyAuthToken = async () => {
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
:deep(.ant-dropdown-trigger) {
@apply h-full;
}
</style>

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

@ -1,17 +1,3 @@
<script>
export default {
name: 'Index',
}
</script>
<template>
<div class="nc-main-tab">
<span>Welcome to NocoDB!</span>
</div>
<div class="h-full w-full prose text-3xl text-gray-400 flex items-center justify-center">Welcome to NocoDB!</div>
</template>
<style scoped>
.nc-main-tab {
@apply w-full text-3xl text-gray-400 flex align-center justify-center;
}
</style>

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

@ -1,39 +1,31 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReloadViewDataHookInj, useRoute } from '#imports'
import type { TableType } from 'nocodb-sdk/build/main'
import { useProvideSharedFormStore } from '~/composables/useSharedFormViewStore'
import { IsFormInj, IsPublicInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import { createEventHook, definePageMeta, provide, ref, useProvideSmartsheetStore, useRoute } from '#imports'
definePageMeta({
requiresAuth: false,
public: true,
})
const route = useRoute()
const reloadEventHook = createEventHook<void>()
const { sharedView, loadSharedView, meta, formColumns } = useSharedView()
await loadSharedView(route.params.viewId as string)
const { loadSharedView, sharedView, meta, notFound } = useProvideSharedFormStore(route.params.viewId as string)
provide(ReloadViewDataHookInj, reloadEventHook)
provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, formColumns)
provide(IsPublicInj, ref(true))
await loadSharedView()
if (!notFound.value) {
provide(ReloadViewDataHookInj, reloadEventHook)
provide(MetaInj, meta)
provide(IsPublicInj, ref(true))
provide(IsFormInj, ref(true))
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta)
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta as Ref<TableType>, true)
}
</script>
<template>
<NuxtLayout id="content" class="flex">
<div class="nc-container flex flex-col h-full mt-2 px-6">
<SharedViewForm />
</div>
</NuxtLayout>
<SharedViewForm />
</template>
<style scoped>
.nc-container {
height: calc(100% - var(--header-height));
flex: 1 1 100%;
}
</style>

26
packages/nc-gui-v2/pages/[projectType]/view/[viewId].vue

@ -1,7 +1,19 @@
<script setup lang="ts">
import { ReadonlyInj, ReloadViewDataHookInj, useRoute } from '#imports'
import { message } from 'ant-design-vue'
import {
ReadonlyInj,
ReloadViewDataHookInj,
createEventHook,
definePageMeta,
extractSdkResponseErrorMsg,
provide,
ref,
useRoute,
useSharedView,
} from '#imports'
definePageMeta({
public: true,
requiresAuth: false,
layout: 'shared-view',
})
@ -10,15 +22,20 @@ const route = useRoute()
const reloadEventHook = createEventHook<void>()
provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReadonlyInj, ref(true))
provide(ReadonlyInj, true)
const { loadSharedView } = useSharedView()
const showPassword = ref(false)
try {
await loadSharedView(route.params.viewId as string)
} catch (e) {
showPassword.value = true
} catch (e: any) {
if (e?.response?.status === 403) {
showPassword.value = true
} else {
console.error(e)
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>
@ -27,6 +44,7 @@ try {
<div v-if="showPassword">
<SharedViewAskPassword v-model="showPassword" />
</div>
<SharedViewGrid v-else />
</NuxtLayout>
</template>

2
packages/nc-gui-v2/pages/forgot-password.vue

@ -129,7 +129,7 @@ function resetError() {
}
.submit {
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white bg-primary;
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 text-white;
&::after {
@apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;

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

@ -69,103 +69,119 @@ onMounted(() => {
</div>
<div class="min-w-2/4 flex-auto">
<a-card :loading="isLoading" class="!rounded-lg shadow">
<h1 class="text-center text-4xl p-2 nc-project-page-title flex items-center justify-center gap-2 text-gray-600">
<a-card class="transition-all duration-300 ease-out !rounded-lg shadow">
<h1 class="flex items-center justify-center gap-2 leading-8 mb-8">
<!-- My Projects -->
<b>{{ $t('title.myProject') }}</b>
<MdiRefresh
v-t="['a:project:refresh']"
class="text-sm text-gray-500 hover:text-primary mt-1 cursor-pointer"
@click="loadProjects"
/>
<span class="text-4xl">{{ $t('title.myProject') }}</span>
<a-tooltip title="Reload projects">
<span
class="transition-all duration-200 h-full flex items-center group hover:ring active:(ring ring-pink-500) rounded-full mt-1"
:class="isLoading ? 'animate-spin ring ring-gray-200' : ''"
>
<MdiRefresh
v-t="['a:project:refresh']"
class="text-xl text-gray-500 group-hover:text-pink-500 cursor-pointer"
:class="isLoading ? '!text-primary' : ''"
@click="loadProjects"
/>
</span>
</a-tooltip>
</h1>
<div class="order-1 flex mb-6">
<a-input-search
v-model:value="filterQuery"
class="max-w-[200px] nc-project-page-search"
class="max-w-[250px] nc-project-page-search rounded"
:placeholder="$t('activity.searchProject')"
/>
<div class="flex-grow" />
<a-dropdown v-if="isUIAllowed('projectCreate', true)" @click.stop>
<a-button class="nc-new-project-menu !shadow">
<div class="flex align-center">
<a-dropdown v-if="isUIAllowed('projectCreate', true)" :trigger="['click']">
<button class="nc-new-project-menu">
<div class="flex items-center w-full">
{{ $t('title.newProj') }}
<MdiMenuDown class="menu-icon" />
</div>
</a-button>
</button>
<template #overlay>
<a-menu>
<div
v-t="['c:project:create:xcdb']"
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2 nc-create-xc-db-project"
@click="navigateTo('/project/create')"
>
<MdiPlus class="col-span-2 mr-1 mt-[1px] text-primary text-lg" />
<div class="col-span-10 text-sm xl:text-md">{{ $t('activity.createProject') }}</div>
</div>
<div
v-t="['c:project:create:extdb']"
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2 nc-create-external-db-project"
@click="navigateTo('/project/create-external')"
>
<MdiDatabaseOutline class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg" />
<div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')" />
</div>
<a-menu-item>
<div
v-t="['c:project:create:xcdb']"
class="nc-project-menu-item gap-4"
@click="navigateTo('/project/create')"
>
<MdiPlusOutline class="text-lg" />
<div>{{ $t('activity.createProject') }}</div>
</div>
</a-menu-item>
<a-menu-item>
<div
v-t="['c:project:create:extdb']"
class="nc-project-menu-item gap-4"
@click="navigateTo('/project/create-external')"
>
<MdiDatabaseOutline class="text-lg" />
<div v-html="$t('activity.createProjectExtended.extDB')" />
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div v-if="isLoading">
<a-skeleton />
</div>
<a-table
v-else
:custom-row="
(record) => ({
onClick: () => {
$e('a:project:open')
navigateTo(`/nc/${record.id}`)
},
})
"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
>
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text }">
<div
class="capitalize !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap nc-project-row"
:title="text"
>
{{ text }}
</div>
</template>
</a-table-column>
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div class="flex align-center">
<MdiEditOutline
v-t="['c:project:edit:rename']"
class="nc-action-btn"
@click.stop="navigateTo(`/project/${text}`)"
/>
<MdiDeleteOutline class="nc-action-btn" @click.stop="deleteProject(record)" />
</div>
</template>
</a-table-column>
</a-table>
<TransitionGroup name="layout" mode="out-in">
<div v-if="isLoading">
<a-skeleton />
</div>
<a-table
v-else
:custom-row="
(record) => ({
onClick: () => {
$e('a:project:open')
navigateTo(`/nc/${record.id}`)
},
class: ['group'],
})
"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
>
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text }">
<div
class="capitalize color-transition group-hover:text-pink-500 !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap"
>
{{ text }}
</div>
</template>
</a-table-column>
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div class="flex items-center gap-2">
<MdiEditOutline
v-t="['c:project:edit:rename']"
class="nc-action-btn"
@click.stop="navigateTo(`/project/${text}`)"
/>
<MdiDeleteOutline class="nc-action-btn" @click.stop="deleteProject(record)" />
</div>
</template>
</a-table-column>
</a-table>
</TransitionGroup>
</a-card>
</div>
@ -182,7 +198,25 @@ onMounted(() => {
<style scoped>
.nc-action-btn {
@apply text-gray-500 hover:text-primary mr-2 cursor-pointer p-2 w-[30px] h-[30px] hover:bg-gray-300/50 rounded-full;
@apply text-gray-500 hover:(text-pink-500 ring) active:(ring ring-pink-500) cursor-pointer p-2 w-[30px] h-[30px] hover:bg-gray-300/50 rounded-full;
}
.nc-new-project-menu {
@apply cursor-pointer z-1 relative color-transition rounded-md px-3 py-2 text-white;
&::after {
@apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
content: '';
z-index: -1;
}
&:hover::after {
@apply transform scale-110 ring ring-pink-500;
}
&:active::after {
@apply ring ring-pink-500;
}
}
:deep(.ant-table-cell) {

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

@ -167,7 +167,7 @@ function resetError() {
}
.submit {
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white bg-primary;
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 text-white;
&::after {
@apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;

19
packages/nc-gui-v2/pages/signup/[[token]].vue

@ -1,4 +1,5 @@
<script setup lang="ts">
import { validatePassword } from 'nocodb-sdk'
import {
definePageMeta,
extractSdkResponseErrorMsg,
@ -43,7 +44,7 @@ const formRules = {
{
validator: (_: unknown, v: string) => {
return new Promise((resolve, reject) => {
if (isEmail(v)) return resolve(true)
if (!v?.length || isEmail(v)) return resolve(true)
reject(new Error(t('msg.error.signUpRules.emailInvalid')))
})
},
@ -51,9 +52,15 @@ const formRules = {
},
],
password: [
// Password is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
{ min: 8, message: t('msg.error.signUpRules.passwdLength') },
{
validator: (_: unknown, v: string) => {
return new Promise((resolve, reject) => {
const { error, valid } = validatePassword(v)
if (valid) return resolve(true)
reject(new Error(error))
})
},
},
],
}
@ -162,7 +169,7 @@ function resetError() {
<div class="prose-sm mt-4 text-gray-500">
By signing up, you agree to the
<a class="prose-sm text-pink-500 underline" target="_blank" href="https://nocodb.com/policy-nocodb">Terms of Service</a>
<a class="prose-sm text-gray-500 underline" target="_blank" href="https://nocodb.com/policy-nocodb">Terms of Service</a>
</div>
</div>
</NuxtLayout>
@ -186,7 +193,7 @@ function resetError() {
}
.submit {
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white bg-primary;
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 text-white;
&::after {
@apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;

6
packages/nc-gui-v2/utils/NcAutocompleteTree.ts

@ -37,6 +37,7 @@ export class NcAutocompleteTree {
}
root.value = root.value || []
root.value.push(word)
root.isLeaf = true
}
find(word: string) {
@ -48,12 +49,11 @@ export class NcAutocompleteTree {
return null // if not found return null
}
}
return root // return the root where it ends search
}
traverse(root: Node) {
if (root.value && root.value.length) {
if (root.isLeaf) {
this.suggestions.push(...root.value)
}
@ -70,6 +70,8 @@ export class NcAutocompleteTree {
return this.suggestions
} // cannot suggest anything
this.suggestions.push(...root.value)
const children = root.children
let spread = 0

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

@ -130,6 +130,7 @@ export interface ViewType {
order?: number;
fk_model_id?: string;
slug?: string;
uuid?: string;
show_system_fields?: boolean;
lock_type?: 'collaborative' | 'locked' | 'personal';
}
@ -3062,32 +3063,41 @@ export class Api<
}),
/**
* No description
* @description Read project details
*
* @tags Public
* @name SharedViewMetaGet
* @request GET:/api/v1/db/public/shared-view/{sharedViewUuid}/meta
* @response `200` `object` OK
* @name SharedBaseGet
* @request GET:/api/v1/db/public/shared-base/{sharedBaseUuid}/meta
* @response `200` `{ project_id?: string }` OK
*/
sharedViewMetaGet: (sharedViewUuid: string, params: RequestParams = {}) =>
this.request<object, any>({
path: `/api/v1/db/public/shared-view/${sharedViewUuid}/meta`,
sharedBaseGet: (sharedBaseUuid: string, params: RequestParams = {}) =>
this.request<{ project_id?: string }, any>({
path: `/api/v1/db/public/shared-base/${sharedBaseUuid}/meta`,
method: 'GET',
format: 'json',
...params,
}),
/**
* @description Read project details
* No description
*
* @tags Public
* @name SharedBaseGet
* @request GET:/api/v1/db/public/shared-base/{sharedBaseUuid}/meta
* @response `200` `{ project_id?: string }` OK
* @name SharedViewMetaGet
* @request GET:/api/v1/db/public/shared-view/{sharedViewUuid}/meta
* @response `200` `(ViewType & { relatedMetas?: any, client?: string, columns?: ((GridColumnType | FormColumnType | GalleryColumnType) & ColumnType), model?: TableType } & { view?: (FormType | GridType | GalleryType) })` OK
*/
sharedBaseGet: (sharedBaseUuid: string, params: RequestParams = {}) =>
this.request<{ project_id?: string }, any>({
path: `/api/v1/db/public/shared-base/${sharedBaseUuid}/meta`,
sharedViewMetaGet: (sharedViewUuid: string, params: RequestParams = {}) =>
this.request<
ViewType & {
relatedMetas?: any;
client?: string;
columns?: (GridColumnType | FormColumnType | GalleryColumnType) &
ColumnType;
model?: TableType;
} & { view?: FormType | GridType | GalleryType },
any
>({
path: `/api/v1/db/public/shared-view/${sharedViewUuid}/meta`,
method: 'GET',
format: 'json',
...params,

5
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -1552,8 +1552,9 @@ class BaseModelSqlv2 {
switch (colOptions.type) {
case RelationTypes.BELONGS_TO:
{
const parentCol = await colOptions.getChildColumn();
insertObj[parentCol.column_name] =
const childCol = await colOptions.getChildColumn();
const parentCol = await colOptions.getParentColumn();
insertObj[childCol.column_name] =
nestedData?.[parentCol.title];
}
break;

114
scripts/sdk/swagger.json

@ -4366,28 +4366,22 @@
"description": ""
}
},
"/api/v1/db/public/shared-view/{sharedViewUuid}/meta": {
"/api/v1/db/public/shared-base/{sharedBaseUuid}/meta": {
"parameters": [
{
"schema": {
"type": "string"
},
"name": "sharedViewUuid",
"name": "sharedBaseUuid",
"in": "path",
"required": true
},
{
"schema": {
"type": "string"
},
"in": "header",
"name": "xc-password",
"description": "Shared view password"
}
],
"get": {
"summary": "",
"operationId": "public-shared-view-meta-get",
"operationId": "public-shared-base-get",
"description": "Read project details",
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -4395,58 +4389,117 @@
"application/json": {
"schema": {
"type": "object",
"properties": {}
"properties": {
"project_id": {
"type": "string"
}
}
}
},
"application/xml": {
"schema": {}
}
}
}
},
"tags": [
"Public"
],
"description": "",
"parameters": []
]
}
},
"/api/v1/db/public/shared-base/{sharedBaseUuid}/meta": {
"/api/v1/db/public/shared-view/{sharedViewUuid}/meta": {
"parameters": [
{
"schema": {
"type": "string"
},
"name": "sharedBaseUuid",
"name": "sharedViewUuid",
"in": "path",
"required": true
},
{
"schema": {
"type": "string"
},
"in": "header",
"name": "xc-password",
"description": "Shared view password"
}
],
"get": {
"summary": "",
"operationId": "public-shared-base-get",
"description": "Read project details",
"parameters": [],
"operationId": "public-shared-view-meta-get",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"project_id": {
"type": "string"
"allOf": [
{
"$ref": "#/components/schemas/View"
},
{
"type": "object",
"properties": {
"relatedMetas": {},
"client": {
"type": "string"
},
"columns": {
"allOf": [
{
"oneOf": [
{
"$ref": "#/components/schemas/GridColumn"
},
{
"$ref": "#/components/schemas/FormColumn"
},
{
"$ref": "#/components/schemas/GalleryColumn"
}
]
},
{
"$ref": "#/components/schemas/Column"
}
]
},
"model": {
"$ref": "#/components/schemas/Table"
}
}
},
{
"type": "object",
"properties": {
"view": {
"oneOf": [
{
"$ref": "#/components/schemas/Form"
},
{
"$ref": "#/components/schemas/Grid"
},
{
"$ref": "#/components/schemas/Gallery"
}
]
}
}
}
}
]
}
},
"application/xml": {
"schema": {}
}
}
}
},
"tags": [
"Public"
]
],
"description": "",
"parameters": []
}
},
"/api/v1/db/meta/audits/comments": {
@ -6096,6 +6149,9 @@
"slug": {
"type": "string"
},
"uuid": {
"type": "string"
},
"show_system_fields": {
"type": "boolean"
},

Loading…
Cancel
Save