Browse Source

Merge branch 'develop' into chore/docker-build

pull/3273/head
Wing-Kam Wong 2 years ago
parent
commit
7ade9860cb
  1. 1
      packages/nc-gui-v2/components.d.ts
  2. 4
      packages/nc-gui-v2/components/api-client/Headers.vue
  3. 5
      packages/nc-gui-v2/components/cell/Text.vue
  4. 30
      packages/nc-gui-v2/components/cell/attachment/index.vue
  5. 61
      packages/nc-gui-v2/components/cell/attachment/utils.ts
  6. 2
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  7. 3
      packages/nc-gui-v2/components/general/HelpAndSupport.vue
  8. 168
      packages/nc-gui-v2/components/shared-view/Form.vue
  9. 3
      packages/nc-gui-v2/components/shared-view/Grid.vue
  10. 17
      packages/nc-gui-v2/components/smartsheet-toolbar/Export.vue
  11. 80
      packages/nc-gui-v2/components/smartsheet-toolbar/ExportSubActions.vue
  12. 4
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  13. 39
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  14. 73
      packages/nc-gui-v2/components/smartsheet-toolbar/ViewActions.vue
  15. 19
      packages/nc-gui-v2/components/smartsheet-toolbar/ViewInfo.vue
  16. 7
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  17. 12
      packages/nc-gui-v2/components/smartsheet/Form.vue
  18. 34
      packages/nc-gui-v2/components/smartsheet/Gallery.vue
  19. 7
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  20. 13
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  21. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue
  22. 6
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  23. 11
      packages/nc-gui-v2/components/webhook/Editor.vue
  24. 2
      packages/nc-gui-v2/components/webhook/List.vue
  25. 62
      packages/nc-gui-v2/composables/useSharedFormViewStore.ts
  26. 28
      packages/nc-gui-v2/composables/useSmartsheetRowStore.ts
  27. 1
      packages/nc-gui-v2/composables/useUIPermission/rolePermissions.ts
  28. 37
      packages/nc-gui-v2/composables/useViewColumns.ts
  29. 2
      packages/nc-gui-v2/layouts/base.vue
  30. 12
      packages/nc-gui-v2/layouts/default.vue
  31. 6
      packages/nc-gui-v2/layouts/shared-view.vue
  32. 1
      packages/nc-gui-v2/nuxt-shim.d.ts
  33. 7
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/index.vue
  34. 30
      packages/nc-gui-v2/pages/[projectType]/form/[viewId].vue
  35. 174
      packages/nc-gui-v2/pages/[projectType]/form/[viewId]/index.vue
  36. 2
      packages/nc-gui-v2/pages/index/index/index.vue
  37. 5
      packages/nc-gui-v2/utils/urlUtils.ts
  38. 4
      packages/nc-gui-v2/windi.config.ts
  39. 9
      packages/nocodb/src/run/docker.ts
  40. 5
      packages/nocodb/src/run/dockerRunMysql.ts
  41. 5
      packages/nocodb/src/run/dockerRunPG.ts
  42. 5
      packages/nocodb/src/run/dockerRunPG_CyQuick.ts

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

@ -202,6 +202,7 @@ declare module '@vue/runtime-core' {
MdiSearch: typeof import('~icons/mdi/search')['default'] MdiSearch: typeof import('~icons/mdi/search')['default']
MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default'] MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default'] MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiSort: typeof import('~icons/mdi/sort')['default']
MdiStar: typeof import('~icons/mdi/star')['default'] MdiStar: typeof import('~icons/mdi/star')['default']
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default'] MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
MdiStore: typeof import('~icons/mdi/store')['default'] MdiStore: typeof import('~icons/mdi/store')['default']

4
packages/nc-gui-v2/components/api-client/Headers.vue

@ -87,7 +87,7 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
</td> </td>
<td class="px-2 w-min-[400px]"> <td class="px-2 w-min-[400px]">
<a-form-item> <a-form-item>
<a-select v-model:value="headerRow.name" size="large" placeholder="Key"> <a-select v-model:value="headerRow.name" size="large" placeholder="Key" class="nc-input-hook-header-key">
<a-select-option v-for="(header, i) in headerList" :key="i" :value="header"> <a-select-option v-for="(header, i) in headerList" :key="i" :value="header">
{{ header }} {{ header }}
</a-select-option> </a-select-option>
@ -96,7 +96,7 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
</td> </td>
<td class="px-2 w-min-[400px]"> <td class="px-2 w-min-[400px]">
<a-form-item> <a-form-item>
<a-input v-model:value="headerRow.value" size="large" placeholder="Value" /> <a-input v-model:value="headerRow.value" size="large" placeholder="Value" class="nc-input-hook-header-value" />
</a-form-item> </a-form-item>
</td> </td>
<td class="relative"> <td class="relative">

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

@ -1,10 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { inject } from '#imports' import { EditModeInj, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: string | null | undefined modelValue?: string | null
} }
const props = defineProps<Props>() const props = defineProps<Props>()

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

@ -5,7 +5,9 @@ import { useSortable } from './sort'
import Modal from './Modal.vue' import Modal from './Modal.vue'
import Carousel from './Carousel.vue' import Carousel from './Carousel.vue'
import { import {
IsFormInj,
computed, computed,
inject,
isImage, isImage,
openLink, openLink,
ref, ref,
@ -16,8 +18,8 @@ import {
} from '#imports' } from '#imports'
interface Props { interface Props {
modelValue: string | Record<string, any>[] | null modelValue?: string | Record<string, any>[] | null
rowIndex: number rowIndex?: number
} }
interface Emits { interface Emits {
@ -28,6 +30,10 @@ const { modelValue, rowIndex } = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const isForm = inject(IsFormInj, ref(false))
const attachmentCellRef = ref<HTMLDivElement>()
const sortableRef = ref<HTMLDivElement>() const sortableRef = ref<HTMLDivElement>()
const { cellRefs } = useSmartsheetStoreOrThrow()! const { cellRefs } = useSmartsheetStoreOrThrow()!
@ -46,7 +52,11 @@ const {
storedFiles, storedFiles,
} = useProvideAttachmentCell(updateModelValue) } = useProvideAttachmentCell(updateModelValue)
const currentCellRef = computed(() => cellRefs.value.find((cell) => cell.dataset.key === `${rowIndex}${column.value.id}`)) const currentCellRef = computed(() =>
!rowIndex && isForm.value
? attachmentCellRef.value
: cellRefs.value.find((cell) => cell.dataset.key === `${rowIndex}${column.value.id}`),
)
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, isReadonly) const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, isReadonly)
@ -61,7 +71,8 @@ watch(
if (nextModel) { if (nextModel) {
try { try {
attachments.value = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean) attachments.value = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean)
} catch { } catch (e) {
console.error(e)
attachments.value = [] attachments.value = []
} }
} }
@ -92,7 +103,10 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
</script> </script>
<template> <template>
<div class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1"> <div
ref="attachmentCellRef"
class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1"
>
<Carousel /> <Carousel />
<template v-if="isSharedForm || (!isReadonly && !dragging && !!currentCellRef)"> <template v-if="isSharedForm || (!isReadonly && !dragging && !!currentCellRef)">
@ -124,6 +138,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
</div> </div>
</a-tooltip> </a-tooltip>
</div> </div>
<div v-else class="flex" />
<template v-if="visibleItems.length"> <template v-if="visibleItems.length">
<div <div
@ -133,9 +148,8 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
> >
<div <div
v-for="(item, i) of visibleItems" v-for="(item, i) of visibleItems"
:id="item.url"
:key="item.url || item.title" :key="item.url || item.title"
:class="isImage(item.title, item.mimetype) ? '' : 'border-1 rounded'" :class="isImage(item.title, item.mimetype ?? item.type) ? '' : 'border-1 rounded'"
class="nc-attachment flex items-center justify-center min-h-[50px]" class="nc-attachment flex items-center justify-center min-h-[50px]"
> >
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
@ -144,7 +158,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
</template> </template>
<nuxt-img <nuxt-img
v-if="isImage(item.title, item.mimetype)" v-if="isImage(item.title, item.mimetype ?? item.type) && (item.url || item.data)"
quality="75" quality="75"
placeholder placeholder
:alt="item.title || `#${i}`" :alt="item.title || `#${i}`"

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

@ -1,19 +1,33 @@
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import FileSaver from 'file-saver' import FileSaver from 'file-saver'
import { computed, inject, ref, useApi, useFileDialog, useInjectionState, useProject, watch } from '#imports' import {
import { ColumnInj, EditModeInj, IsPublicInj, MetaInj, ReadonlyInj } from '~/context' ColumnInj,
import { isImage } from '~/utils' EditModeInj,
import { NOCO } from '~/lib' IsPublicInj,
MetaInj,
NOCO,
ReadonlyInj,
computed,
inject,
isImage,
ref,
useApi,
useFileDialog,
useInjectionState,
useProject,
watch,
} from '#imports'
import MdiPdfBox from '~icons/mdi/pdf-box' import MdiPdfBox from '~icons/mdi/pdf-box'
import MdiFileWordOutline from '~icons/mdi/file-word-outline' import MdiFileWordOutline from '~icons/mdi/file-word-outline'
import MdiFilePowerpointBox from '~icons/mdi/file-powerpoint-box' import MdiFilePowerpointBox from '~icons/mdi/file-powerpoint-box'
import MdiFileExcelOutline from '~icons/mdi/file-excel-outline' import MdiFileExcelOutline from '~icons/mdi/file-excel-outline'
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file' import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file'
interface AttachmentProps { interface AttachmentProps extends File {
data?: any data?: any
file: File file: File
title: string title: string
mimetype: string
} }
export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState( export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
@ -22,22 +36,14 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const isForm = inject('isForm', false)
// todo: replace placeholder var
const isPublicGrid = $ref(false)
const meta = inject(MetaInj)! const meta = inject(MetaInj)!
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(EditModeInj, ref(false))
/** 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 */ /** keep user selected File object */
const storedFiles = ref<File[]>([]) const storedFiles = ref<AttachmentProps[]>([])
const attachments = ref<File[]>([]) const attachments = ref<File[]>([])
@ -54,37 +60,41 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
/** remove a file from our stored attachments (either locally stored or saved ones) */ /** remove a file from our stored attachments (either locally stored or saved ones) */
function removeFile(i: number) { function removeFile(i: number) {
if (isPublic.value) { if (isPublic.value) {
storedFilesData.value.splice(i, 1)
storedFiles.value.splice(i, 1) storedFiles.value.splice(i, 1)
updateModelValue(storedFilesData.value.map((storedFile) => storedFile.file)) updateModelValue(storedFiles.value.map((stored) => stored.file))
} else { } else {
attachments.value.splice(i, 1) attachments.value.splice(i, 1)
updateModelValue(attachments.value) updateModelValue(attachments.value)
} }
} }
/** save a file on select / drop, either locally (in-memory) or in the db */ /** save a file on select / drop, either locally (in-memory) or in the db */
async function onFileSelect(selectedFiles: FileList | File[]) { async function onFileSelect(selectedFiles: FileList | File[]) {
if (!selectedFiles.length || isPublicGrid) return if (!selectedFiles.length) return
if (isPublic.value) { if (isPublic.value) {
storedFiles.value.push(...selectedFiles) storedFiles.value.push(
storedFilesData.value.push(
...(await Promise.all<AttachmentProps>( ...(await Promise.all<AttachmentProps>(
Array.from(selectedFiles).map( Array.from(selectedFiles).map(
(file) => (file) =>
new Promise<AttachmentProps>((resolve) => { new Promise<AttachmentProps>((resolve) => {
const res: AttachmentProps = { file, title: file.name } const res: AttachmentProps = { ...file, file, title: file.name, mimetype: file.type }
if (isImage(file.name, (file as any).mimetype)) {
if (isImage(file.name, (<any>file).mimetype ?? file.type)) {
const reader = new FileReader() const reader = new FileReader()
reader.onload = (e: any) => {
reader.onload = (e) => {
res.data = e.target?.result res.data = e.target?.result
resolve(res) resolve(res)
} }
reader.onerror = () => { reader.onerror = () => {
resolve(res) resolve(res)
} }
reader.readAsDataURL(file) reader.readAsDataURL(file)
} else { } else {
resolve(res) resolve(res)
@ -94,7 +104,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
)), )),
) )
return updateModelValue(storedFilesData.value.map((storedFile) => storedFile.file)) return updateModelValue(storedFiles.value.map((stored) => stored.file))
} }
const newAttachments = [] const newAttachments = []
@ -149,17 +159,14 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
} }
/** our currently visible items, either the locally stored or the ones from db, depending on isPublicForm status */ /** our currently visible items, either the locally stored or the ones from db, depending on isPublicForm status */
const visibleItems = computed<any[]>(() => (isPublic.value ? storedFilesData.value : attachments.value) || ([] as any[])) const visibleItems = computed<any[]>(() => (isPublic.value ? storedFiles.value : attachments.value))
watch(files, (nextFiles) => nextFiles && onFileSelect(nextFiles)) watch(files, (nextFiles) => nextFiles && onFileSelect(nextFiles))
return { return {
attachments, attachments,
storedFilesData,
visibleItems, visibleItems,
isPublic, isPublic,
isForm,
isPublicGrid,
isReadonly, isReadonly,
meta, meta,
column, column,

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

@ -238,7 +238,7 @@ function openTableCreateDialog() {
> >
<MdiPlus /> <MdiPlus />
<span class="text-gray-500 group-hover:(text-primary/100) flex-1">{{ $t('tooltip.addTable') }}</span> <span class="text-gray-500 group-hover:(text-primary/100) flex-1 nc-add-new-table">{{ $t('tooltip.addTable') }}</span>
<a-dropdown v-if="!isSharedBase" :trigger="['click']" @click.stop> <a-dropdown v-if="!isSharedBase" :trigger="['click']" @click.stop>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" /> <MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" />

3
packages/nc-gui-v2/components/general/HelpAndSupport.vue

@ -19,12 +19,13 @@ const openSwaggerLink = () => {
class="flex items-center space-x-1 w-full cursor-pointer pl-3 py-1.5 hover:(text-primary bg-primary bg-opacity-5)" class="flex items-center space-x-1 w-full cursor-pointer pl-3 py-1.5 hover:(text-primary bg-primary bg-opacity-5)"
@click="showDrawer = true" @click="showDrawer = true"
> >
<MdiCommentTextOutline class="mr-1 nc-share-base" /> <MdiCommentTextOutline class="mr-1" />
<!-- todo: i18n --> <!-- todo: i18n -->
<div>APIs & Support</div> <div>APIs & Support</div>
</div> </div>
<a-drawer <a-drawer
v-bind="$attrs"
v-model:visible="showDrawer" v-model:visible="showDrawer"
class="h-full relative" class="h-full relative"
placement="right" placement="right"

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

@ -1,168 +0,0 @@
<script setup lang="ts">
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { useSharedFormStoreOrThrow } from '#imports'
const {
sharedFormView,
submitForm,
v$,
formState,
notFound,
formColumns,
submitted,
secondsRemain,
passwordDlg,
password,
loadSharedView,
} = useSharedFormStoreOrThrow()
function isRequired(_columnObj: Record<string, any>, required = false) {
let columnObj = _columnObj
if (
columnObj.uidt === UITypes.LinkToAnotherRecord &&
columnObj.colOptions &&
columnObj.colOptions.type === RelationTypes.BELONGS_TO
) {
columnObj = formColumns.value?.find((c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id) as Record<
string,
any
>
}
return required || (columnObj && columnObj.rqd && !columnObj.cdf)
}
</script>
<template>
<div class="bg-primary !h-[100vh] overflow-auto w-full 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>
<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>
</div>
</template>
<div v-else-if="sharedFormView" class="">
<a-row class="justify-center">
<a-col :md="20">
<div>
<div class="h-full m-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-col justify-center flex">
<div class="flex items-center justify-center flex-1 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 p-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-full">
<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>
<style scoped lang="scss">
.nc-input {
@apply w-full !bg-white rounded px-2 py-2 min-h-[40px] mt-2 mb-2 flex items-center border-solid border-1 border-primary;
}
.nc-form-wrapper {
@apply my-0 mx-auto max-w-[800px];
}
</style>

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

@ -26,7 +26,8 @@ useProvideSmartsheetStore(sharedView as Ref<TableType>, meta, true, sorts, neste
<style scoped> <style scoped>
.nc-container { .nc-container {
height: calc(100% - var(--header-height)); height: 100%;
padding-bottom: 0.5rem;
flex: 1 1 100%; flex: 1 1 100%;
} }
</style> </style>

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

@ -0,0 +1,17 @@
<template>
<a-dropdown :trigger="['click']">
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center">
<MdiDownload class="group-hover:text-accent text-gray-500" />
<span class="text-capitalize !text-sm font-weight-normal">Download</span>
<MdiMenuDown class="text-grey" />
</div>
</a-button>
<template #overlay>
<a-menu class="ml-6 !text-sm !px-0 !py-2 !rounded">
<SmartsheetToolbarExportSubActions />
</a-menu>
</template>
</a-dropdown>
</template>

80
packages/nc-gui-v2/components/smartsheet-toolbar/ExportSubActions.vue

@ -0,0 +1,80 @@
<script setup lang="ts">
import { ExportTypes } from 'nocodb-sdk'
import FileSaver from 'file-saver'
import * as XLSX from 'xlsx'
import { message } from 'ant-design-vue'
const isPublicView = inject(IsPublicInj, ref(false))
const fields = inject(FieldsInj, ref([]))
const { project } = useProject()
const { $api } = useNuxtApp()
const meta = inject(MetaInj)
const selectedView = inject(ActiveViewInj)
const exportFile = async (exportType: ExportTypes) => {
let offset = 0
let c = 1
const responseType = exportType === ExportTypes.EXCEL ? 'base64' : 'blob'
try {
while (!isNaN(offset) && offset > -1) {
let res
if (isPublicView.value) {
const { exportFile: sharedViewExportFile } = useSharedView()
res = await sharedViewExportFile(fields.value, offset, exportType, responseType)
} else {
res = await $api.dbViewRow.export(
'noco',
project?.value.title as string,
meta?.value.title as string,
selectedView?.value.title as string,
exportType,
{
responseType,
query: {
offset,
},
} as any,
)
}
const { data, headers } = res
if (exportType === ExportTypes.EXCEL) {
const workbook = XLSX.read(data, { type: 'base64' })
XLSX.writeFile(workbook, `${meta?.value.title}_exported_${c++}.xlsx`)
} else if (exportType === ExportTypes.CSV) {
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
FileSaver.saveAs(blob, `${meta?.value.title}_exported_${c++}.csv`)
}
offset = +headers['nc-export-offset']
if (offset > -1) {
message.info('Downloading more files')
} else {
message.success('Successfully exported all table data')
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>
<template>
<a-menu-item>
<div v-t="['a:actions:download-csv']" class="nc-project-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</div>
</a-menu-item>
<a-menu-item>
<div v-t="['a:actions:download-excel']" class="nc-project-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}
</div>
</a-menu-item>
</template>

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

@ -64,7 +64,7 @@ watch(
{ immediate: true }, { immediate: true },
) )
const isAnyFieldHidden = computed(() => fields.value?.some((field) => !(!showSystemFields && field.system) && !field.show)) const isAnyFieldHidden = computed(() => filteredFieldList.value?.some((field) => !field.show))
const onMove = (event: { moved: { newIndex: number } }) => { const onMove = (event: { moved: { newIndex: number } }) => {
// todo : sync with server // todo : sync with server
@ -130,7 +130,7 @@ const getIcon = (c: ColumnType) =>
<a-divider class="!my-2" /> <a-divider class="!my-2" />
<div v-if="!isPublic" 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"> <a-checkbox v-model:checked="showSystemFields" class="!items-center">
<span class="text-xs"> {{ $t('activity.showSystemFields') }}</span> <span class="text-xs"> {{ $t('activity.showSystemFields') }}</span>
</a-checkbox> </a-checkbox>
</div> </div>

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

@ -1,27 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import FieldListAutoCompleteDropdown from './FieldListAutoCompleteDropdown.vue' import FieldListAutoCompleteDropdown from './FieldListAutoCompleteDropdown.vue'
import { getSortDirectionOptions } from '~/utils/sortUtils' import {
import { computed, inject, useViewSorts } from '#imports' ActiveViewInj,
import { ActiveViewInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context' IsLockedInj,
import MdiMenuDownIcon from '~icons/mdi/menu-down' MetaInj,
import MdiSortIcon from '~icons/mdi/sort' ReloadViewDataHookInj,
import MdiDeleteIcon from '~icons/mdi/close-box' computed,
import MdiAddIcon from '~icons/mdi/plus' getSortDirectionOptions,
inject,
ref,
useViewSorts,
watch,
} from '#imports'
const meta = inject(MetaInj) const meta = inject(MetaInj)
const view = inject(ActiveViewInj) const view = inject(ActiveViewInj)
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj, ref(false))
const reloadDataHook = inject(ReloadViewDataHookInj) const reloadDataHook = inject(ReloadViewDataHookInj)
const { sorts, saveOrUpdate, loadSorts, addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger()) const { sorts, saveOrUpdate, loadSorts, addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger())
const columns = computed(() => meta?.value?.columns || []) const columns = computed(() => meta?.value?.columns || [])
const columnByID = computed<Record<string, ColumnType>>(() =>
columns?.value?.reduce((obj: any, col: any) => { const columnByID = computed(() =>
obj[col.id] = col columns.value.reduce((obj, col) => {
obj[col.id!] = col
return obj return obj
}, {}), }, {} as Record<string, ColumnType>),
) )
watch( watch(
@ -38,10 +45,10 @@ watch(
<div :class="{ 'nc-badge nc-active-btn': sorts?.length }"> <div :class="{ 'nc-badge nc-active-btn': sorts?.length }">
<a-button v-t="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked" <a-button v-t="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked"
><div class="flex items-center gap-1"> ><div class="flex items-center gap-1">
<MdiSortIcon /> <MdiSort />
<!-- Sort --> <!-- Sort -->
<span class="text-capitalize !text-sm font-weight-normal">{{ $t('activity.sort') }}</span> <span class="text-capitalize !text-sm font-weight-normal">{{ $t('activity.sort') }}</span>
<MdiMenuDownIcon class="text-grey" /> <MdiMenuDown class="text-grey" />
</div> </div>
</a-button> </a-button>
</div> </div>
@ -49,7 +56,7 @@ watch(
<div class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto !border"> <div class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto !border">
<div v-if="sorts?.length" class="sort-grid mb-2" @click.stop> <div v-if="sorts?.length" class="sort-grid mb-2" @click.stop>
<template v-for="(sort, i) in sorts || []" :key="i"> <template v-for="(sort, i) in sorts || []" :key="i">
<MdiDeleteIcon class="nc-sort-item-remove-btn text-grey self-center" small @click.stop="deleteSort(sort, i)" /> <MdiCloseBox class="nc-sort-item-remove-btn text-grey self-center" small @click.stop="deleteSort(sort, i)" />
<FieldListAutoCompleteDropdown <FieldListAutoCompleteDropdown
v-model="sort.fk_column_id" v-model="sort.fk_column_id"
@ -79,7 +86,7 @@ watch(
</div> </div>
<a-button class="text-capitalize mb-1 mt-4" type="primary" ghost @click.stop="addSort"> <a-button class="text-capitalize mb-1 mt-4" type="primary" ghost @click.stop="addSort">
<div class="flex gap-1 items-center"> <div class="flex gap-1 items-center">
<MdiAddIcon /> <MdiPlus />
<!-- Add Sort Option --> <!-- Add Sort Option -->
{{ $t('activity.addSort') }} {{ $t('activity.addSort') }}
</div> </div>

73
packages/nc-gui-v2/components/smartsheet-toolbar/ViewActions.vue

@ -1,21 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import * as XLSX from 'xlsx'
import { ExportTypes } from 'nocodb-sdk'
import FileSaver from 'file-saver'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { LockType } from '~/lib' import { LockType } from '~/lib'
import { viewIcons } from '~/utils' import { viewIcons } from '~/utils'
import { import {
ActiveViewInj, ActiveViewInj,
FieldsInj,
IsLockedInj, IsLockedInj,
IsPublicInj, IsPublicInj,
MetaInj,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
inject, inject,
ref, ref,
useNuxtApp, useNuxtApp,
useProject,
useUIPermission, useUIPermission,
} from '#imports' } from '#imports'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline' import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
@ -28,14 +22,8 @@ const isPublicView = inject(IsPublicInj, ref(false))
const isView = false const isView = false
const { project } = useProject()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const meta = inject(MetaInj)
const fields = inject(FieldsInj, ref([]))
const selectedView = inject(ActiveViewInj) const selectedView = inject(ActiveViewInj)
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj)
@ -46,52 +34,6 @@ const quickImportDialog = ref(false)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const exportFile = async (exportType: ExportTypes) => {
let offset = 0
let c = 1
const responseType = exportType === ExportTypes.EXCEL ? 'base64' : 'blob'
try {
while (!isNaN(offset) && offset > -1) {
let res
if (isPublicView.value) {
const { exportFile: sharedViewExportFile } = useSharedView()
res = await sharedViewExportFile(fields.value, offset, exportType, responseType)
} else {
res = await $api.dbViewRow.export(
'noco',
project?.value.title as string,
meta?.value.title as string,
selectedView?.value.title as string,
exportType,
{
responseType,
query: {
offset,
},
} as any,
)
}
const { data, headers } = res
if (exportType === ExportTypes.EXCEL) {
const workbook = XLSX.read(data, { type: 'base64' })
XLSX.writeFile(workbook, `${meta?.value.title}_exported_${c++}.xlsx`)
} else if (exportType === ExportTypes.CSV) {
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
FileSaver.saveAs(blob, `${meta?.value.title}_exported_${c++}.csv`)
}
offset = +headers['nc-export-offset']
if (offset > -1) {
message.info('Downloading more files')
} else {
message.success('Successfully exported all table data')
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const Icon = computed(() => { const Icon = computed(() => {
switch ((selectedView?.value as any)?.lock_type) { switch ((selectedView?.value as any)?.lock_type) {
case LockType.Personal: case LockType.Personal:
@ -185,20 +127,7 @@ async function changeLockType(type: LockType) {
</template> </template>
<template #expandIcon></template> <template #expandIcon></template>
<a-menu-item> <SmartsheetToolbarExportSubActions />
<div v-t="['a:actions:download-csv']" class="nc-project-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</div>
</a-menu-item>
<a-menu-item>
<div v-t="['a:actions:download-excel']" class="nc-project-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}
</div>
</a-menu-item>
</a-sub-menu> </a-sub-menu>
<template v-if="isUIAllowed('csvImport') && !isView && !isPublicView"> <template v-if="isUIAllowed('csvImport') && !isView && !isPublicView">
<a-sub-menu key="upload"> <a-sub-menu key="upload">

19
packages/nc-gui-v2/components/smartsheet-toolbar/ViewInfo.vue

@ -0,0 +1,19 @@
<script setup lang="ts">
import { ActiveViewInj, viewIcons } from '#imports'
const selectedView = inject(ActiveViewInj)
</script>
<template>
<div class="flex gap-2 items-center ml-2 mr-2 pr-4 pb-1 py-0.5 border-r-1 border-gray-100">
<component
:is="viewIcons[selectedView?.type].icon"
v-if="selectedView?.type"
class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[selectedView?.type].color }"
/>
<span class="!text-sm font-medium max-w-36 overflow-ellipsis overflow-hidden whitespace-nowrap">{{
selectedView?.title
}}</span>
</div>
</template>

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

@ -23,6 +23,7 @@ interface Props {
column: ColumnType column: ColumnType
modelValue: any modelValue: any
editEnabled: boolean editEnabled: boolean
readOnly?: boolean
rowIndex?: number rowIndex?: number
active?: boolean active?: boolean
virtual?: boolean virtual?: boolean
@ -38,12 +39,18 @@ const active = toRef(props, 'active', false)
const virtual = toRef(props, 'virtual', false) const virtual = toRef(props, 'virtual', false)
const readOnly = toRef(props, 'readOnly', undefined)
provide(ColumnInj, column) provide(ColumnInj, column)
provide(EditModeInj, useVModel(props, 'editEnabled', emit)) provide(EditModeInj, useVModel(props, 'editEnabled', emit))
provide(ActiveCellInj, active) provide(ActiveCellInj, active)
if (readOnly?.value) {
provide(ReadonlyInj, readOnly.value)
}
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))

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

@ -152,9 +152,8 @@ function onMoveCallback(event: any) {
function onMove(event: any) { function onMove(event: any) {
const { newIndex, element, oldIndex } = event.added || event.moved || event.removed const { newIndex, element, oldIndex } = event.added || event.moved || event.removed
console.log(event)
if (shouldSkipColumn(element)) { if (shouldSkipColumn(element)) {
console.log('SKIPPED')
return return
} }
@ -408,7 +407,14 @@ onMounted(async () => {
</div> </div>
</div> </div>
</div> </div>
<Draggable :list="hiddenColumns" draggable=".item" group="form-inputs" @start="drag = true" @end="drag = false"> <Draggable
:list="hiddenColumns"
item-key="id"
draggable=".item"
group="form-inputs"
@start="drag = true"
@end="drag = false"
>
<template #item="{ element }"> <template #item="{ element }">
<a-card <a-card
size="small" size="small"

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

@ -2,6 +2,7 @@
import { isVirtualCol } from 'nocodb-sdk' import { isVirtualCol } from 'nocodb-sdk'
import { inject, provide, useViewData } from '#imports' import { inject, provide, useViewData } from '#imports'
import Row from '~/components/smartsheet/Row.vue' import Row from '~/components/smartsheet/Row.vue'
import type { Row as RowType } from '~/composables'
import { ActiveViewInj, ChangePageInj, FieldsInj, IsFormInj, IsGridInj, MetaInj, PaginationDataInj, ReadonlyInj } from '~/context' import { ActiveViewInj, ChangePageInj, FieldsInj, IsFormInj, IsGridInj, MetaInj, PaginationDataInj, ReadonlyInj } from '~/context'
import ImageIcon from '~icons/mdi/file-image-box' import ImageIcon from '~icons/mdi/file-image-box'
@ -11,14 +12,21 @@ interface Attachment {
const meta = inject(MetaInj) const meta = inject(MetaInj)
const view = inject(ActiveViewInj) const view = inject(ActiveViewInj)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
const expandedFormDlg = ref(false)
const expandedFormRow = ref<RowType>()
const expandedFormRowState = ref<Record<string, any>>()
const { loadData, paginationData, formattedData: data, loadGalleryData, galleryData, changePage } = useViewData(meta, view as any) const { loadData, paginationData, formattedData: data, loadGalleryData, galleryData, changePage } = useViewData(meta, view as any)
const { isUIAllowed } = useUIPermission()
provide(IsFormInj, ref(false)) provide(IsFormInj, ref(false))
provide(IsGridInj, false) provide(IsGridInj, false)
provide(PaginationDataInj, paginationData) provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage) provide(ChangePageInj, changePage)
provide(ReadonlyInj, true) provide(ReadonlyInj, !isUIAllowed('xcDatatableEditable'))
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))
@ -49,12 +57,23 @@ const attachments = (record: any): Array<Attachment> => {
return [] return []
} }
} }
reloadViewDataHook?.on(async () => {
await loadData()
})
const expandForm = (row: RowType, state?: Record<string, any>) => {
if (!isUIAllowed('xcDatatableEditable')) return
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
}
</script> </script>
<template> <template>
<div class="flex flex-col h-full w-full"> <div class="flex flex-col h-full w-full">
<div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3 overflow-auto"> <div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3 overflow-auto">
<div v-for="(record, recordIndex) in data" :key="recordIndex" class="flex flex-col"> <div v-for="(record, recordIndex) in data" :key="recordIndex" class="flex flex-col" @click="expandForm(record)">
<Row :row="record"> <Row :row="record">
<a-card hoverable class="!rounded-lg h-full"> <a-card hoverable class="!rounded-lg h-full">
<template #cover> <template #cover>
@ -84,8 +103,8 @@ const attachments = (record: any): Array<Attachment> => {
<div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start"> <div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start">
<div v-if="isRowEmpty(record, col)" class="h-3 bg-gray-200 px-5 rounded-lg"></div> <div v-if="isRowEmpty(record, col)" class="h-3 bg-gray-200 px-5 rounded-lg"></div>
<template v-else> <template v-else>
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="record.row[col.title]" :column="col" /> <SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="record.row[col.title]" :column="col" :row="record" />
<SmartsheetCell v-else v-model="record.row[col.title]" :column="col" :edit-enabled="false" /> <SmartsheetCell v-else v-model="record.row[col.title]" :column="col" :edit-enabled="false" :read-only="true" />
</template> </template>
</div> </div>
</div> </div>
@ -94,6 +113,13 @@ const attachments = (record: any): Array<Attachment> => {
</div> </div>
</div> </div>
<SmartsheetPagination /> <SmartsheetPagination />
<SmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:row="expandedFormRow"
:state="expandedFormRowState"
:meta="meta"
/>
</div> </div>
</template> </template>

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

@ -15,6 +15,7 @@ import {
PaginationDataInj, PaginationDataInj,
ReadonlyInj, ReadonlyInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,
createEventHook,
enumColor, enumColor,
inject, inject,
onClickOutside, onClickOutside,
@ -44,8 +45,8 @@ const fields = inject(FieldsInj, ref([]))
const readOnly = inject(ReadonlyInj, false) const readOnly = inject(ReadonlyInj, false)
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const reloadViewDataHook = inject(ReloadViewDataHookInj) const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj) const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
@ -382,7 +383,7 @@ const onNavigate = (dir: NavigateDir) => {
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1"> <td key="row-index" class="caption nc-grid-cell pl-5 pr-1">
<div class="items-center flex gap-1 min-w-[55px]"> <div class="items-center flex gap-1 min-w-[55px]">
<div <div
v-if="!readOnly && !isLocked" v-if="!readOnly || !isLocked"
class="nc-row-no text-xs text-gray-500" class="nc-row-no text-xs text-gray-500"
:class="{ hidden: row.rowMeta.selected }" :class="{ hidden: row.rowMeta.selected }"
> >

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

@ -4,7 +4,7 @@ import ToggleDrawer from '~/components/smartsheet/sidebar/toolbar/ToggleDrawer.v
const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow() const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow()
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const { allowCSVDownload } = useSharedView() const { isUIAllowed } = useUIPermission()
const { isOpen } = useSidebar() const { isOpen } = useSidebar()
</script> </script>
@ -15,11 +15,13 @@ const { isOpen } = useSidebar()
style="z-index: 7" style="z-index: 7"
> >
<SmartsheetToolbarViewActions <SmartsheetToolbarViewActions
v-if="(isGrid && !isPublic) || (isGrid && isPublic && allowCSVDownload)" v-if="isGrid && !isPublic && isUIAllowed('dataInsert')"
:show-system-fields="false" :show-system-fields="false"
class="ml-1" class="ml-1"
/> />
<SmartsheetToolbarViewInfo v-if="!isUIAllowed('dataInsert') && !isPublic" />
<SmartsheetToolbarFieldsMenu v-if="isGrid || isGallery" :show-system-fields="false" class="ml-1" /> <SmartsheetToolbarFieldsMenu v-if="isGrid || isGallery" :show-system-fields="false" class="ml-1" />
<SmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery" /> <SmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery" />
@ -28,14 +30,15 @@ const { isOpen } = useSidebar()
<SmartsheetToolbarShareView v-if="(isForm || isGrid) && !isPublic" /> <SmartsheetToolbarShareView v-if="(isForm || isGrid) && !isPublic" />
<SmartsheetToolbarExport v-if="!isUIAllowed('dataInsert')" />
<div class="flex-1" /> <div class="flex-1" />
<SmartsheetToolbarReload class="mx-1" /> <SmartsheetToolbarReload v-if="!isPublic" class="mx-1" />
<SmartsheetToolbarAddRow class="mx-1" /> <SmartsheetToolbarAddRow v-if="isUIAllowed('dataInsert') && !isPublic" class="mx-1" />
<SmartsheetToolbarSearchData v-if="(isGrid || isGallery) && !isPublic" class="shrink mr-2 ml-2" /> <SmartsheetToolbarSearchData v-if="(isGrid || isGallery) && !isPublic" class="shrink mr-2 ml-2" />
<ToggleDrawer v-if="!isOpen" class="mr-2" /> <ToggleDrawer v-if="!isOpen && !isPublic" class="mr-2" />
</div> </div>
</template> </template>

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

@ -5,7 +5,7 @@ const { isOpen, toggle } = useSidebar({ storageKey: 'nc-right-sidebar' })
<template> <template>
<div :class="{ 'nc-active-btn': isOpen }"> <div :class="{ 'nc-active-btn': isOpen }">
<a-button size="small" @click="toggle(!isOpen)"> <a-button size="small" class="nc-toggle-right-navbar" @click="toggle(!isOpen)">
<div class="flex items-center gap-1 text-xs" :class="{ 'text-gray-500': !isOpen }"> <div class="flex items-center gap-1 text-xs" :class="{ 'text-gray-500': !isOpen }">
<MdiMenu class="!text-xs" /> <MdiMenu class="!text-xs" />
Views Views

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

@ -143,7 +143,7 @@ onMounted(() => {
<div class="text-xs">{{ $t('activity.shareBase.link') }}</div> <div class="text-xs">{{ $t('activity.shareBase.link') }}</div>
</div> </div>
<div v-if="base?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm w-full justify-between"> <div v-if="base?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm w-full justify-between">
<span class="flex text-xs overflow-x-hidden overflow-ellipsis text-gray-700 pl-2">{{ url }}</span> <span class="flex text-xs overflow-x-hidden overflow-ellipsis text-gray-700 pl-2 nc-url">{{ url }}</span>
<div class="flex border-l-1 pt-1 pl-1"> <div class="flex border-l-1 pt-1 pl-1">
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
@ -191,7 +191,7 @@ onMounted(() => {
<div class="mt-4 flex flex-row justify-between mx-1"> <div class="mt-4 flex flex-row justify-between mx-1">
<a-dropdown v-model="showEditBaseDropdown" class="flex"> <a-dropdown v-model="showEditBaseDropdown" class="flex">
<a-button> <a-button>
<div class="flex flex-row items-center space-x-2"> <div class="flex flex-row items-center space-x-2 nc-disable-shared-base">
<div v-if="base?.uuid">{{ $t('activity.shareBase.enable') }}</div> <div v-if="base?.uuid">{{ $t('activity.shareBase.enable') }}</div>
<div v-else>{{ $t('activity.shareBase.disable') }}</div> <div v-else>{{ $t('activity.shareBase.disable') }}</div>
<IcRoundKeyboardArrowDown class="h-[1rem]" /> <IcRoundKeyboardArrowDown class="h-[1rem]" />
@ -208,7 +208,7 @@ onMounted(() => {
</template> </template>
</a-dropdown> </a-dropdown>
<a-select v-if="base?.uuid" v-model:value="base.role" class="flex"> <a-select v-if="base?.uuid" v-model:value="base.role" class="flex nc-shared-base-role">
<template #suffixIcon> <template #suffixIcon>
<div class="flex flex-row"> <div class="flex flex-row">
<IcRoundKeyboardArrowDown class="text-black -mt-0.5 h-[1rem]" /> <IcRoundKeyboardArrowDown class="text-black -mt-0.5 h-[1rem]" />

11
packages/nc-gui-v2/components/webhook/Editor.vue

@ -413,14 +413,14 @@ onMounted(async () => {
<a-row type="flex"> <a-row type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos.title"> <a-form-item v-bind="validateInfos.title">
<a-input v-model:value="hook.title" size="large" :placeholder="$t('general.title')" /> <a-input v-model:value="hook.title" size="large" :placeholder="$t('general.title')" class="nc-text-field-hook-title" />
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-row type="flex" :gutter="[16, 16]"> <a-row type="flex" :gutter="[16, 16]">
<a-col :span="12"> <a-col :span="12">
<a-form-item v-bind="validateInfos.eventOperation"> <a-form-item v-bind="validateInfos.eventOperation">
<a-select v-model:value="hook.eventOperation" size="large" :placeholder="$t('general.event')"> <a-select v-model:value="hook.eventOperation" size="large" :placeholder="$t('general.event')" class="nc-text-field-hook-event">
<a-select-option v-for="(event, i) in eventList" :key="i" :value="event.value.join(' ')"> <a-select-option v-for="(event, i) in eventList" :key="i" :value="event.value.join(' ')">
{{ event.text.join(' ') }} {{ event.text.join(' ') }}
</a-select-option> </a-select-option>
@ -432,6 +432,7 @@ onMounted(async () => {
<a-select <a-select
v-model:value="hook.notification.type" v-model:value="hook.notification.type"
size="large" size="large"
class="nc-select-hook-notification-type"
:placeholder="$t('general.notification')" :placeholder="$t('general.notification')"
@change="onNotTypeChange(true)" @change="onNotTypeChange(true)"
> >
@ -470,7 +471,7 @@ onMounted(async () => {
<a-col :span="18"> <a-col :span="18">
<a-form-item v-bind="validateInfos['notification.payload.path']"> <a-form-item v-bind="validateInfos['notification.payload.path']">
<a-input v-model:value="hook.notification.payload.path" size="large" placeholder="http://example.com" /> <a-input v-model:value="hook.notification.payload.path" size="large" placeholder="http://example.com" class="nc-text-field-hook-url-path" />
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -482,7 +483,7 @@ onMounted(async () => {
<a-tab-pane key="params" tab="Params" force-render> <a-tab-pane key="params" tab="Params" force-render>
<ApiClientParams v-model="hook.notification.payload.parameters" /> <ApiClientParams v-model="hook.notification.payload.parameters" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="headers" tab="Headers"> <a-tab-pane key="headers" tab="Headers" class="nc-tab-headers">
<ApiClientHeaders v-model="hook.notification.payload.headers" /> <ApiClientHeaders v-model="hook.notification.payload.headers" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="auth" tab="Auth"> <a-tab-pane key="auth" tab="Auth">
@ -566,7 +567,7 @@ onMounted(async () => {
<a-row class="mb-5" type="flex"> <a-row class="mb-5" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-card> <a-card>
<a-checkbox v-model:checked="hook.condition">On Condition</a-checkbox> <a-checkbox v-model:checked="hook.condition" class="nc-check-box-hook-condition">On Condition</a-checkbox>
<SmartsheetToolbarColumnFilter v-if="hook.condition" ref="filterRef" :auto-save="false" :hook-id="hook.id" /> <SmartsheetToolbarColumnFilter v-if="hook.condition" ref="filterRef" :auto-save="false" :hook-id="hook.id" />
</a-card> </a-card>
</a-col> </a-col>

2
packages/nc-gui-v2/components/webhook/List.vue

@ -52,7 +52,7 @@ onMounted(() => {
<div class=""> <div class="">
<div class="mb-2"> <div class="mb-2">
<div class="float-left font-bold text-xl mt-2 mb-4">{{ meta.title }} : Webhooks</div> <div class="float-left font-bold text-xl mt-2 mb-4">{{ meta.title }} : Webhooks</div>
<a-button class="float-right" type="primary" size="large" @click="emit('add')"> <a-button class="float-right nc-btn-create-webhook" type="primary" size="large" @click="emit('add')">
{{ $t('activity.addWebhook') }} {{ $t('activity.addWebhook') }}
</a-button> </a-button>
</div> </div>

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

@ -1,12 +1,21 @@
import useVuelidate from '@vuelidate/core' import useVuelidate from '@vuelidate/core'
import { minLength, required } from '@vuelidate/validators' import { minLength, required } from '@vuelidate/validators'
import type { Ref } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { ColumnType, FormType, LinkToAnotherRecordType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnType, FormType, LinkToAnotherRecordType, TableType, ViewType } from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue' import {
import { SharedViewPasswordInj } from '~/context' SharedViewPasswordInj,
import { extractSdkResponseErrorMsg } from '~/utils' computed,
import { useInjectionState, useMetas } from '#imports' extractSdkResponseErrorMsg,
provide,
ref,
useApi,
useInjectionState,
useMetas,
useProvideSmartsheetRowStore,
watch,
} from '#imports'
const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((sharedViewId: string) => { const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((sharedViewId: string) => {
const progress = ref(false) const progress = ref(false)
@ -23,8 +32,10 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const meta = ref<TableType>() const meta = ref<TableType>()
const columns = ref<(ColumnType & { required?: boolean; show?: boolean })[]>() const columns = ref<(ColumnType & { required?: boolean; show?: boolean })[]>()
const { $api } = useNuxtApp() const { api, isLoading } = useApi()
const { metas, setMeta } = useMetas() const { metas, setMeta } = useMetas()
const formState = ref({}) const formState = ref({})
const { state: additionalState } = useProvideSmartsheetRowStore( const { state: additionalState } = useProvideSmartsheetRowStore(
@ -37,11 +48,11 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
) )
const formColumns = computed(() => const formColumns = computed(() =>
columns?.value?.filter((c) => c.show)?.filter((col) => !isVirtualCol(col) || col.uidt === UITypes.LinkToAnotherRecord), columns.value?.filter((c) => c.show).filter((col) => !isVirtualCol(col) || col.uidt === UITypes.LinkToAnotherRecord),
) )
const loadSharedView = async () => { const loadSharedView = async () => {
try { try {
const viewMeta = await $api.public.sharedViewMetaGet(sharedViewId, { const viewMeta: Record<string, any> = await api.public.sharedViewMetaGet(sharedViewId, {
headers: { headers: {
'xc-password': password.value, 'xc-password': password.value,
}, },
@ -54,9 +65,10 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
meta.value = viewMeta.model meta.value = viewMeta.model
columns.value = viewMeta.model?.columns columns.value = viewMeta.model?.columns
setMeta(viewMeta.model) await setMeta(viewMeta.model)
const relatedMetas = { ...viewMeta.relatedMetas } const relatedMetas = { ...viewMeta.relatedMetas }
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key])) Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))
} catch (e: any) { } catch (e: any) {
if (e.response && e.response.status === 404) { if (e.response && e.response.status === 404) {
@ -68,11 +80,14 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
} }
const validators = computed(() => { const validators = computed(() => {
const obj: any = { const obj: Record<string, Record<string, any>> = {
localState: {}, localState: {},
virtual: {}, virtual: {},
} }
for (const column of formColumns?.value ?? []) {
if (!formColumns.value) return obj
for (const column of formColumns.value) {
if ( if (
!isVirtualCol(column) && !isVirtualCol(column) &&
((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || (column as any).required) ((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || (column as any).required)
@ -103,7 +118,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const v$ = useVuelidate( const v$ = useVuelidate(
validators, validators,
computed(() => ({ localState: formState?.value, virtual: additionalState?.value })), computed(() => ({ localState: formState.value, virtual: additionalState.value })),
) )
const submitForm = async () => { const submitForm = async () => {
@ -113,18 +128,20 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
} }
progress.value = true progress.value = true
const data: Record<string, any> = { ...(formState?.value ?? {}), ...(additionalState?.value || {}) } const data: Record<string, any> = { ...formState.value, ...additionalState.value }
const attachment: Record<string, any> = {} const attachment: Record<string, any> = {}
for (const col of metas?.value?.[sharedView?.value?.fk_model_id as string]?.columns ?? []) { /** find attachments in form data */
for (const col of metas.value?.[sharedView.value?.fk_model_id as string]?.columns) {
if (col.uidt === UITypes.Attachment) { if (col.uidt === UITypes.Attachment) {
attachment[`_${col.title}`] = data[col.title!] if (data[col.title]) {
delete data[col.title!] attachment[`_${col.title}`] = data[col.title].map((item: { file: File }) => item.file)
}
} }
} }
await $api.public.dataCreate( await api.public.dataCreate(
sharedView?.value?.uuid as string, (sharedView.value as any)?.uuid as string,
{ {
data, data,
...attachment, ...attachment,
@ -139,7 +156,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
submitted.value = true submitted.value = true
progress.value = false progress.value = false
await message.success(sharedFormView.value?.success_msg || 'Saved successfully.') await message.success(sharedFormView.value?.sucess_msg || 'Saved successfully.')
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
await message.error(await extractSdkResponseErrorMsg(e)) await message.error(await extractSdkResponseErrorMsg(e))
@ -148,13 +165,15 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
} }
/** reset form if show_blank_form is true */ /** reset form if show_blank_form is true */
watch(submitted, (nextVal: boolean) => { watch(submitted, (nextVal) => {
if (nextVal && sharedFormView.value?.show_blank_form) { if (nextVal && (sharedFormView.value as any)?.show_blank_form) {
secondsRemain.value = 5 secondsRemain.value = 5
const intvl = setInterval(() => { const intvl = setInterval(() => {
secondsRemain.value = secondsRemain.value - 1 secondsRemain.value = secondsRemain.value - 1
if (secondsRemain.value < 0) { if (secondsRemain.value < 0) {
submitted.value = false submitted.value = false
clearInterval(intvl) clearInterval(intvl)
} }
}, 1000) }, 1000)
@ -185,6 +204,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
submitted, submitted,
secondsRemain, secondsRemain,
passwordDlg, passwordDlg,
isLoading,
} }
}, 'expanded-form-store') }, 'expanded-form-store')
@ -192,6 +212,8 @@ export { useProvideSharedFormStore }
export function useSharedFormStoreOrThrow() { export function useSharedFormStoreOrThrow() {
const sharedFormStore = useSharedFormStore() const sharedFormStore = useSharedFormStore()
if (sharedFormStore == null) throw new Error('Please call `useProvideSharedFormStore` on the appropriate parent component') if (sharedFormStore == null) throw new Error('Please call `useProvideSharedFormStore` on the appropriate parent component')
return sharedFormStore return sharedFormStore
} }

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

@ -2,27 +2,41 @@ import { message } from 'ant-design-vue'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, RelationTypes, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, RelationTypes, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { MaybeRef } from '@vueuse/core'
import type { Row } from './useViewData' import type { Row } from './useViewData'
import { useInjectionState, useMetas, useNuxtApp, useProject, useVirtualCell } from '#imports' import {
import { NOCO } from '~/lib' NOCO,
import { deepCompare, extractPkFromRow, extractSdkResponseErrorMsg } from '~/utils' computed,
deepCompare,
extractPkFromRow,
extractSdkResponseErrorMsg,
ref,
unref,
useInjectionState,
useMetas,
useNuxtApp,
useProject,
useVirtualCell,
} from '#imports'
const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState((meta: Ref<TableType>, row: Ref<Row>) => { const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState((meta: Ref<TableType>, row: MaybeRef<Row>) => {
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { project } = useProject() const { project } = useProject()
const { metas } = useMetas() const { metas } = useMetas()
// state // state
const state = ref<Record<string, Record<string, any> | Record<string, any>[] | null>>({}) const state = ref<Record<string, Record<string, any> | Record<string, any>[] | null>>({})
// getters // getters
const isNew = computed(() => row.value?.rowMeta?.new ?? false) const isNew = computed(() => unref(row).rowMeta?.new ?? false)
// actions // actions
const addLTARRef = async (value: Record<string, any>, column: ColumnType) => { const addLTARRef = async (value: Record<string, any>, column: ColumnType) => {
const { isHm, isMm, isBt } = $(useVirtualCell(ref(column))) const { isHm, isMm, isBt } = $(useVirtualCell(ref(column)))
if (isHm || isMm) { if (isHm || isMm) {
state.value[column.title!] = state.value[column.title!] || [] if (!state.value[column.title!]) state.value[column.title!] = []
if (state.value[column.title!]!.find((ln: Record<string, any>) => deepCompare(ln, value))) { if (state.value[column.title!]!.find((ln: Record<string, any>) => deepCompare(ln, value))) {
return message.info('This value is already in the list') return message.info('This value is already in the list')
@ -106,6 +120,8 @@ export { useProvideSmartsheetRowStore }
export function useSmartsheetRowStoreOrThrow() { export function useSmartsheetRowStoreOrThrow() {
const smartsheetRowStore = useSmartsheetRowStore() const smartsheetRowStore = useSmartsheetRowStore()
if (smartsheetRowStore == null) throw new Error('Please call `useSmartsheetRowStore` on the appropriate parent component') if (smartsheetRowStore == null) throw new Error('Please call `useSmartsheetRowStore` on the appropriate parent component')
return smartsheetRowStore return smartsheetRowStore
} }

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

@ -8,6 +8,7 @@ const rolePermissions = {
column: true, column: true,
tableAttachment: true, tableAttachment: true,
tableRowUpdate: true, tableRowUpdate: true,
dataInsert: true,
rowComments: true, rowComments: true,
gridViewOptions: true, gridViewOptions: true,
sortSync: true, sortSync: true,

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

@ -19,6 +19,22 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
const { isSharedBase } = useProject() const { isSharedBase } = useProject()
const isLocalMode = computed(
() => isPublic.value || !isUIAllowed('hideAllColumns') || !isUIAllowed('showAllColumns') || isSharedBase.value,
)
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,
)
})
const loadViewColumns = async () => { const loadViewColumns = async () => {
if (!meta || !view) return if (!meta || !view) return
@ -35,6 +51,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
[curr.fk_column_id]: curr, [curr.fk_column_id]: curr,
} }
}, {}) }, {})
fields.value = meta.value?.columns fields.value = meta.value?.columns
?.map((column: ColumnType) => { ?.map((column: ColumnType) => {
const currentColumnField = fieldById[column.id!] || {} const currentColumnField = fieldById[column.id!] || {}
@ -44,7 +61,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
fk_column_id: column.id, fk_column_id: column.id,
...currentColumnField, ...currentColumnField,
order: currentColumnField.order || order++, order: currentColumnField.order || order++,
system: isSystemColumn(currentColumnField.type || false), system: isSystemColumn(metaColumnById?.value?.[currentColumnField.fk_column_id!]),
} }
}) })
.sort((a: Field, b: Field) => a.order - b.order) .sort((a: Field, b: Field) => a.order - b.order)
@ -52,7 +69,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
} }
const showAll = async (ignoreIds?: any) => { const showAll = async (ignoreIds?: any) => {
if (isPublic.value || isSharedBase.value) { if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({ fields.value = fields.value?.map((field: Field) => ({
...field, ...field,
show: true, show: true,
@ -75,7 +92,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
reloadData?.() reloadData?.()
} }
const hideAll = async (ignoreIds?: any) => { const hideAll = async (ignoreIds?: any) => {
if (isPublic.value || isSharedBase.value) { if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({ fields.value = fields.value?.map((field: Field) => ({
...field, ...field,
show: false, show: false,
@ -125,18 +142,6 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
reloadData?.() reloadData?.()
} }
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,
)
})
const showSystemFields = computed({ const showSystemFields = computed({
get() { get() {
// todo: show_system_fields missing from ViewType // todo: show_system_fields missing from ViewType
@ -144,7 +149,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
}, },
set(v: boolean) { set(v: boolean) {
if (view?.value?.id) { if (view?.value?.id) {
if (!isPublic.value && !isSharedBase.value) { if (!isLocalMode.value) {
$api.dbView $api.dbView
.update(view.value.id, { .update(view.value.id, {
show_system_fields: v, show_system_fields: v,

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

@ -59,8 +59,6 @@ hooks.hook('page:finish', () => {
<GeneralReleaseInfo /> <GeneralReleaseInfo />
<GeneralShareBaseButton v-if="!isSharedBase" class="pr-4 font-semibold" />
<a-tooltip placement="bottom" :mouse-enter-delay="1"> <a-tooltip placement="bottom" :mouse-enter-delay="1">
<template #title> Switch language</template> <template #title> Switch language</template>

12
packages/nc-gui-v2/layouts/default.vue

@ -1,14 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from 'vue-i18n' import { useTitle } from '@vueuse/core'
import { useHead, useRoute } from '#imports' import { useI18n, useRoute, useSidebar } from '#imports'
const route = useRoute() const route = useRoute()
const { te, t } = useI18n() const { te, t } = useI18n()
useHead({ const { hasSidebar } = useSidebar()
title: route.meta?.title && te(route.meta.title as string) ? `${t(route.meta.title as string)} | NocoDB` : 'NocoDB',
}) useTitle(route.meta?.title && te(route.meta.title) ? `${t(route.meta.title)} | NocoDB` : 'NocoDB')
</script> </script>
<script lang="ts"> <script lang="ts">
@ -19,7 +19,7 @@ export default {
<template> <template>
<div class="w-full h-full"> <div class="w-full h-full">
<Teleport to="#nc-sidebar-left"> <Teleport :to="hasSidebar ? '#nc-sidebar-left' : null" :disabled="!hasSidebar">
<slot name="sidebar" /> <slot name="sidebar" />
</Teleport> </Teleport>

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

@ -19,11 +19,11 @@ export default {
</div> </div>
<div> <div>
<div class="flex justify-center items-center"> <div class="flex justify-center items-center">
<div class="flex items-center gap-2 ml-3"> <div class="flex items-center gap-2 ml-3 text-white">
<template v-if="isLoading"> <template v-if="isLoading">
{{ $t('general.loading') }} <span class="text-white">{{ $t('general.loading') }}</span>
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" /> <MdiReload :class="{ 'animate-infinite animate-spin ': isLoading }" />
</template> </template>
<div v-else class="text-xl font-semibold truncate text-white"> <div v-else class="text-xl font-semibold truncate text-white">
{{ sharedView?.title }} {{ sharedView?.title }}

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

@ -30,5 +30,6 @@ declare module 'vue-router' {
requiresAuth?: boolean requiresAuth?: boolean
public?: boolean public?: boolean
hideHeader?: boolean hideHeader?: boolean
title?: string
} }
} }

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

@ -12,6 +12,7 @@ const { isOverDropZone } = useDropZone(dropZone, onDrop)
const { files, open, reset } = useFileDialog() const { files, open, reset } = useFileDialog()
const { isSharedBase } = useProject() const { isSharedBase } = useProject()
const { isUIAllowed } = useUIPermission()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -140,8 +141,12 @@ function onDropZoneClick(e: MouseEvent) {
<template> <template>
<div class="h-full w-full text-gray-600 flex items-center justify-center relative"> <div class="h-full w-full text-gray-600 flex items-center justify-center relative">
<div v-if="isSharedBase" class="flex flex-col gap-6 items-center justify-center mx-auto text-center"> <div
v-if="isSharedBase || !isUIAllowed('dataInsert')"
class="flex flex-col gap-6 items-center justify-center mx-auto text-center text-gray-500 border-gray-300 border-1 w-3/5 h-1/2 rounded-md"
>
<div class="text-3xl">Welcome to NocoDB!</div> <div class="text-3xl">Welcome to NocoDB!</div>
<div class="prose-lg leading-8">To get started, click on a table in the left pane</div>
</div> </div>
<div v-else ref="dropZone"> <div v-else ref="dropZone">
<general-overlay <general-overlay

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

@ -1,23 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk/build/main' import type { TableType } from 'nocodb-sdk'
import { useProvideSharedFormStore } from '~/composables/useSharedFormViewStore' import {
import { IsFormInj, IsPublicInj, MetaInj, ReloadViewDataHookInj } from '~/context' IsFormInj,
import { createEventHook, definePageMeta, provide, ref, useProvideSmartsheetStore, useRoute } from '#imports' IsPublicInj,
MetaInj,
ReloadViewDataHookInj,
createEventHook,
definePageMeta,
provide,
ref,
useProvideSharedFormStore,
useProvideSmartsheetStore,
useRoute,
useSidebar,
} from '#imports'
definePageMeta({ definePageMeta({
public: true, public: true,
}) })
const route = useRoute() useSidebar({ hasSidebar: false })
const reloadEventHook = createEventHook<void>() const route = useRoute()
const { loadSharedView, sharedView, meta, notFound } = useProvideSharedFormStore(route.params.viewId as string) const { loadSharedView, sharedView, meta, notFound } = useProvideSharedFormStore(route.params.viewId as string)
await loadSharedView() await loadSharedView()
if (!notFound.value) { if (!notFound.value) {
provide(ReloadViewDataHookInj, reloadEventHook) provide(ReloadViewDataHookInj, createEventHook())
provide(MetaInj, meta) provide(MetaInj, meta)
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
provide(IsFormInj, ref(true)) provide(IsFormInj, ref(true))
@ -27,5 +39,7 @@ if (!notFound.value) {
</script> </script>
<template> <template>
<SharedViewForm /> <NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template> </template>

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

@ -0,0 +1,174 @@
<script setup lang="ts">
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { useSharedFormStoreOrThrow } from '#imports'
const {
sharedFormView,
submitForm,
v$,
formState,
notFound,
formColumns,
submitted,
secondsRemain,
passwordDlg,
password,
loadSharedView,
isLoading,
} = useSharedFormStoreOrThrow()
function isRequired(_columnObj: Record<string, any>, required = false) {
let columnObj = _columnObj
if (
columnObj.uidt === UITypes.LinkToAnotherRecord &&
columnObj.colOptions &&
columnObj.colOptions.type === RelationTypes.BELONGS_TO
) {
columnObj = formColumns.value?.find((c) => c.id === columnObj.colOptions.fk_child_column_id) as Record<string, any>
}
return !!(required || (columnObj && columnObj.rqd && !columnObj.cdf))
}
</script>
<template>
<div
class="nc-form-view md:bg-primary bg-opacity-5 h-full min-h-[600px] flex flex-col justify-center items-center nc-form-signin"
>
<div
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full lg:max-w-1/2 max-w-500px mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<general-noco-icon class="color-transition hover:(ring ring-accent)" :class="[isLoading ? 'animated-bg-gradient' : '']" />
<h1 class="prose-2xl font-bold self-center my-4">{{ sharedFormView.heading }}</h1>
<h2 v-if="sharedFormView.subheading" class="prose-lg text-gray-500 self-center">{{ sharedFormView.subheading }}</h2>
<a-alert v-if="notFound" type="warning" class="my-4 text-center" message="Not found" />
<template v-else-if="submitted">
<div class="flex justify-center">
<div v-if="sharedFormView" class="min-w-350px mt-3">
<a-alert
type="success"
class="my-4 text-center"
outlined
:message="sharedFormView.success_msg || 'Successfully submitted form data'"
/>
<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>
</div>
</template>
<template v-else-if="sharedFormView">
<div class="nc-form-wrapper">
<div class="nc-form h-full max-w-3/4 mx-auto">
<div v-for="(field, index) in formColumns" :key="index" class="flex flex-col my-6 gap-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">
<button type="submit" class="submit" @click="submitForm">
{{ $t('general.submit') }}
</button>
</div>
</div>
</div>
</template>
<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: $t('msg.error.signUpRules.passwdRequired') }]">
<a-input-password v-model:value="password" :placeholder="$t('msg.info.signUp.enterPassword')" />
</a-form-item>
<!-- todo: i18n -->
<a-button type="primary" html-type="submit">Unlock</a-button>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<style lang="scss">
.nc-form-view {
.nc-input {
@apply w-full rounded p-2 min-h-[40px] flex items-center border-solid border-1 border-primary;
}
.submit {
@apply z-1 relative color-transition rounded p-3 text-white shadow-sm;
&::after {
@apply rounded 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-accent;
}
&:active::after {
@apply ring ring-accent;
}
}
}
</style>

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

@ -72,7 +72,7 @@ await loadProjects()
<h1 class="flex items-center justify-center gap-2 leading-8 mb-8 mt-4"> <h1 class="flex items-center justify-center gap-2 leading-8 mb-8 mt-4">
<!-- My Projects --> <!-- My Projects -->
<span class="text-4xl">{{ $t('title.myProject') }}</span> <span class="text-4xl nc-project-page-title">{{ $t('title.myProject') }}</span>
<a-tooltip title="Reload projects"> <a-tooltip title="Reload projects">
<span <span

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

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

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

@ -65,10 +65,6 @@ export default defineConfig({
primary: 'rgba(var(--color-primary), var(--tw-bg-opacity))', primary: 'rgba(var(--color-primary), var(--tw-bg-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-bg-opacity))', accent: 'rgba(var(--color-accent), var(--tw-bg-opacity))',
}, },
ringColor: {
primary: 'rgba(var(--color-primary), var(--tw-ring-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-ring-opacity))',
},
colors: { colors: {
...windiColors, ...windiColors,
...themeColors, ...themeColors,

9
packages/nocodb/src/run/docker.ts

@ -6,7 +6,8 @@ process.env.NC_VERSION = '0009044';
const server = express(); const server = express();
server.enable('trust proxy'); server.enable('trust proxy');
server.disable('etag');
server.disable('x-powered-by');
server.use( server.use(
cors({ cors({
exposedHeaders: 'xc-db-response', exposedHeaders: 'xc-db-response',
@ -15,12 +16,6 @@ server.use(
server.set('view engine', 'ejs'); server.set('view engine', 'ejs');
// process.env[`NC_DB`] = `mysql2://localhost:3306?u=root&p=password&d=mar_21`;
// process.env[`NC_DB`] = `pg://localhost:3306?u=root&p=password&d=mar_24`;
// process.env[`NC_DB`] = `pg://localhost:5432?u=postgres&p=password&d=abcde`;
// process.env[`NC_TRY`] = 'true';
// process.env[`NC_DASHBOARD_URL`] = '/test';
process.env[`DEBUG`] = 'xc*'; process.env[`DEBUG`] = 'xc*';
(async () => { (async () => {

5
packages/nocodb/src/run/dockerRunMysql.ts

@ -6,7 +6,8 @@ process.env.NC_VERSION = '0009044';
const server = express(); const server = express();
server.enable('trust proxy'); server.enable('trust proxy');
server.disable('etag');
server.disable('x-powered-by');
server.use( server.use(
cors({ cors({
exposedHeaders: 'xc-db-response', exposedHeaders: 'xc-db-response',
@ -25,7 +26,7 @@ process.env[`NC_DB`] = `mysql2://localhost:3306?u=root&p=password&d=${metaDb}`;
// process.env[`NC_TRY`] = 'true'; // process.env[`NC_TRY`] = 'true';
// process.env[`NC_DASHBOARD_URL`] = '/test'; // process.env[`NC_DASHBOARD_URL`] = '/test';
process.env[`DEBUG`] = 'xc*'; // process.env[`DEBUG`] = 'xc*';
(async () => { (async () => {
const httpServer = server.listen(process.env.PORT || 8080, () => { const httpServer = server.listen(process.env.PORT || 8080, () => {

5
packages/nocodb/src/run/dockerRunPG.ts

@ -6,7 +6,8 @@ process.env.NC_VERSION = '0009044';
const server = express(); const server = express();
server.enable('trust proxy'); server.enable('trust proxy');
server.disable('etag');
server.disable('x-powered-by');
server.use( server.use(
cors({ cors({
exposedHeaders: 'xc-db-response', exposedHeaders: 'xc-db-response',
@ -25,7 +26,7 @@ process.env[`NC_DB`] = `pg://localhost:5432?u=postgres&p=password&d=${metaDb}`;
// process.env[`NC_TRY`] = 'true'; // process.env[`NC_TRY`] = 'true';
// process.env[`NC_DASHBOARD_URL`] = '/test'; // process.env[`NC_DASHBOARD_URL`] = '/test';
process.env[`DEBUG`] = 'xc*'; // process.env[`DEBUG`] = 'xc*';
(async () => { (async () => {
const httpServer = server.listen(process.env.PORT || 8080, () => { const httpServer = server.listen(process.env.PORT || 8080, () => {

5
packages/nocodb/src/run/dockerRunPG_CyQuick.ts

@ -6,7 +6,8 @@ process.env.NC_VERSION = '0009044';
const server = express(); const server = express();
server.enable('trust proxy'); server.enable('trust proxy');
server.disable('etag');
server.disable('x-powered-by');
server.use( server.use(
cors({ cors({
exposedHeaders: 'xc-db-response', exposedHeaders: 'xc-db-response',
@ -18,7 +19,7 @@ process.env[
`NC_DB` `NC_DB`
] = `pg://localhost:5432?u=postgres&p=password&d=meta_v2_2022_06_13`; ] = `pg://localhost:5432?u=postgres&p=password&d=meta_v2_2022_06_13`;
process.env[`DEBUG`] = 'xc*'; //process.env[`DEBUG`] = 'xc*';
(async () => { (async () => {
const httpServer = server.listen(process.env.PORT || 8080, () => { const httpServer = server.listen(process.env.PORT || 8080, () => {

Loading…
Cancel
Save