Browse Source

Merge pull request #7741 from nocodb/nc-feat/form-view-tbd-3

Nc feat/form view tbd 3
pull/7764/head
Raju Udava 7 months ago committed by GitHub
parent
commit
d8102d81c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      packages/nc-gui/assets/nc-icons/link.svg
  2. 147
      packages/nc-gui/components/cell/RichText.vue
  3. 248
      packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue
  4. 2
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  5. 2
      packages/nc-gui/components/dlg/TableDuplicate.vue
  6. 4
      packages/nc-gui/components/general/ColorPicker.vue
  7. 12
      packages/nc-gui/components/general/FormBanner.vue
  8. 12
      packages/nc-gui/components/general/ImageCropper.vue
  9. 25
      packages/nc-gui/components/project/AllTables.vue
  10. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  11. 1756
      packages/nc-gui/components/smartsheet/Form.vue
  12. 76
      packages/nc-gui/components/smartsheet/form/LimitOptions.vue
  13. 9
      packages/nc-gui/lang/en.json
  14. 10
      packages/nc-gui/lib/types.ts
  15. 1
      packages/nc-gui/package.json
  16. 37
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  17. 59
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
  18. 55
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue
  19. 2
      packages/nc-gui/utils/iconUtils.ts
  20. 13
      pnpm-lock.yaml
  21. 32
      tests/playwright/pages/Dashboard/Form/index.ts
  22. 1
      tests/playwright/tests/db/views/viewForm.spec.ts

10
packages/nc-gui/assets/nc-icons/link.svg

@ -1,4 +1,8 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.6665 9.16668C6.95281 9.54943 7.31808 9.86613 7.73754 10.0953C8.157 10.3245 8.62084 10.4608 9.0976 10.4949C9.57437 10.529 10.0529 10.4603 10.5007 10.2932C10.9486 10.1261 11.3552 9.86473 11.6932 9.52668L13.6932 7.52668C14.3004 6.89801 14.6363 6.056 14.6288 5.18201C14.6212 4.30802 14.2706 3.47198 13.6526 2.85395C13.0345 2.23592 12.1985 1.88536 11.3245 1.87777C10.4505 1.87017 9.60851 2.20615 8.97984 2.81335L7.83317 3.95335" stroke="#3366FF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path
<path d="M9.33347 7.83332C9.04716 7.45057 8.68189 7.13387 8.26243 6.90469C7.84297 6.67552 7.37913 6.53924 6.90237 6.5051C6.4256 6.47095 5.94707 6.53974 5.49924 6.7068C5.0514 6.87386 4.64472 7.13527 4.3068 7.47332L2.3068 9.47332C1.69961 10.102 1.36363 10.944 1.37122 11.818C1.37881 12.692 1.72938 13.528 2.3474 14.146C2.96543 14.7641 3.80147 15.1146 4.67546 15.1222C5.54945 15.1298 6.39146 14.7938 7.02013 14.1867L8.16013 13.0467" stroke="#3366FF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> d="M6.6665 9.16668C6.95281 9.54943 7.31808 9.86613 7.73754 10.0953C8.157 10.3245 8.62084 10.4608 9.0976 10.4949C9.57437 10.529 10.0529 10.4603 10.5007 10.2932C10.9486 10.1261 11.3552 9.86473 11.6932 9.52668L13.6932 7.52668C14.3004 6.89801 14.6363 6.056 14.6288 5.18201C14.6212 4.30802 14.2706 3.47198 13.6526 2.85395C13.0345 2.23592 12.1985 1.88536 11.3245 1.87777C10.4505 1.87017 9.60851 2.20615 8.97984 2.81335L7.83317 3.95335"
</svg> stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M9.33347 7.83332C9.04716 7.45057 8.68189 7.13387 8.26243 6.90469C7.84297 6.67552 7.37913 6.53924 6.90237 6.5051C6.4256 6.47095 5.94707 6.53974 5.49924 6.7068C5.0514 6.87386 4.64472 7.13527 4.3068 7.47332L2.3068 9.47332C1.69961 10.102 1.36363 10.944 1.37122 11.818C1.37881 12.692 1.72938 13.528 2.3474 14.146C2.96543 14.7641 3.80147 15.1146 4.67546 15.1222C5.54945 15.1298 6.39146 14.7938 7.02013 14.1867L8.16013 13.0467"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

147
packages/nc-gui/components/cell/RichText.vue

@ -6,6 +6,7 @@ import TurndownService from 'turndown'
import { marked } from 'marked' import { marked } from 'marked'
import { generateJSON } from '@tiptap/html' import { generateJSON } from '@tiptap/html'
import Underline from '@tiptap/extension-underline' import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder'
import { TaskItem } from '@/helpers/dbTiptapExtensions/task-item' import { TaskItem } from '@/helpers/dbTiptapExtensions/task-item'
import { Link } from '@/helpers/dbTiptapExtensions/links' import { Link } from '@/helpers/dbTiptapExtensions/links'
import { IsExpandedFormOpenInj, IsFormInj, ReadonlyInj, RowHeightInj } from '#imports' import { IsExpandedFormOpenInj, IsFormInj, ReadonlyInj, RowHeightInj } from '#imports'
@ -16,6 +17,10 @@ const props = defineProps<{
syncValueChange?: boolean syncValueChange?: boolean
showMenu?: boolean showMenu?: boolean
fullMode?: boolean fullMode?: boolean
isFormField?: boolean
autofocus?: boolean
placeholder?: string
renderAsText?: boolean
}>() }>()
const emits = defineEmits(['update:value']) const emits = defineEmits(['update:value'])
@ -28,6 +33,8 @@ const readOnlyCell = inject(ReadonlyInj, ref(false))
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const isFocused = ref(false)
const turndownService = new TurndownService({}) const turndownService = new TurndownService({})
turndownService.addRule('lineBreak', { turndownService.addRule('lineBreak', {
@ -105,13 +112,19 @@ const editorDom = ref<HTMLElement | null>(null)
const vModel = useVModel(props, 'value', emits, { defaultValue: '' }) const vModel = useVModel(props, 'value', emits, { defaultValue: '' })
const tiptapExtensions = [ const tiptapExtensions = [
StarterKit, StarterKit.configure({
heading: props.isFormField ? false : undefined,
}),
TaskList, TaskList,
TaskItem.configure({ TaskItem.configure({
nested: true, nested: true,
}), }),
Underline, Underline,
Link, Link,
Placeholder.configure({
emptyEditorClass: 'is-editor-empty',
placeholder: props.placeholder,
}),
] ]
const editor = useEditor({ const editor = useEditor({
@ -121,9 +134,18 @@ const editor = useEditor({
.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />')) .turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n') .replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n')
vModel.value = markdown vModel.value = props.isFormField && markdown === '<br />' ? '' : markdown
}, },
editable: !props.readOnly, editable: !props.readOnly,
autofocus: props.autofocus,
onFocus: () => {
isFocused.value = true
},
onBlur: (e) => {
if (!(e?.event?.relatedTarget as HTMLElement)?.closest('.bubble-menu, .nc-textarea-rich-editor')) {
isFocused.value = false
}
},
}) })
const setEditorContent = (contentMd: any, focusEndOfDoc?: boolean) => { const setEditorContent = (contentMd: any, focusEndOfDoc?: boolean) => {
@ -153,21 +175,43 @@ const setEditorContent = (contentMd: any, focusEndOfDoc?: boolean) => {
} }
if (props.syncValueChange) { if (props.syncValueChange) {
watch(vModel, () => { watch([vModel, editor], () => {
setEditorContent(vModel.value) setEditorContent(vModel.value)
}) })
} }
if (props.isFormField) {
watch([props, editor], () => {
if (props.readOnly) {
editor.value?.setEditable(false)
} else {
editor.value?.setEditable(true)
}
})
}
watch(editorDom, () => { watch(editorDom, () => {
if (!editorDom.value) return if (!editorDom.value) return
setEditorContent(vModel.value, true) setEditorContent(vModel.value, true)
if (props.isFormField) return
// Focus editor after editor is mounted // Focus editor after editor is mounted
setTimeout(() => { setTimeout(() => {
editor.value?.chain().focus().run() editor.value?.chain().focus().run()
}, 50) }, 50)
}) })
useEventListener(
editorDom,
'focusout',
(e: FocusEvent) => {
if (!(e?.relatedTarget as HTMLElement)?.closest('.bubble-menu, .nc-textarea-rich-editor')) {
isFocused.value = false
}
},
true,
)
</script> </script>
<template> <template>
@ -177,34 +221,51 @@ watch(editorDom, () => {
'flex flex-col flex-grow nc-rich-text-full': fullMode, 'flex flex-col flex-grow nc-rich-text-full': fullMode,
'nc-rich-text-embed flex flex-col pl-1 w-full': !fullMode, 'nc-rich-text-embed flex flex-col pl-1 w-full': !fullMode,
'readonly': readOnly, 'readonly': readOnly,
'nc-form-rich-text-field !p-0': isFormField,
}" }"
:tabindex="readOnlyCell ? -1 : 0" :tabindex="readOnlyCell || isFormField ? -1 : 0"
> >
<div <div v-if="renderAsText" class="truncate">
v-if="showMenu && !readOnly" <span v-if="editor"> {{ editor?.getText() ?? '' }}</span>
class="absolute top-0 right-0.5 xs:hidden"
:class="{
'max-w-[calc(100%_-_198px)] flex justify-end rounded-tr-2xl overflow-hidden': fullMode,
}"
>
<div class="nc-longtext-scrollbar">
<CellRichTextSelectedBubbleMenu v-if="editor" :editor="editor" embed-mode />
</div>
</div> </div>
<CellRichTextSelectedBubbleMenuPopup v-if="editor" :editor="editor" /> <template v-else>
<CellRichTextLinkOptions v-if="editor" :editor="editor" /> <div
<EditorContent v-if="showMenu && !readOnly && !isFormField"
ref="editorDom" class="absolute top-0 right-0.5 xs:hidden"
:editor="editor" :class="{
class="flex flex-col nc-textarea-rich-editor w-full" 'max-w-[calc(100%_-_198px)] flex justify-end rounded-tr-2xl overflow-hidden': fullMode,
:class="{ }"
'mt-2.5 flex-grow': fullMode, >
'nc-scrollbar-md': !fullMode || (!fullMode && isExpandedFormOpen), <div class="nc-longtext-scrollbar">
'flex-grow': isExpandedFormOpen, <CellRichTextSelectedBubbleMenu v-if="editor" :editor="editor" embed-mode :is-form-field="isFormField" />
[`!overflow-hidden children:line-clamp-${rowHeight}`]: </div>
!fullMode && readOnly && rowHeight && !isExpandedFormOpen && !isForm, </div>
}" <CellRichTextSelectedBubbleMenuPopup v-if="editor && !isFormField" :editor="editor" />
/> <CellRichTextLinkOptions v-if="editor" :editor="editor" />
<EditorContent
ref="editorDom"
:editor="editor"
class="flex flex-col nc-textarea-rich-editor w-full"
:class="{
'mt-2.5 flex-grow': fullMode,
'nc-scrollbar-md': !fullMode || (!fullMode && isExpandedFormOpen),
'flex-grow': isExpandedFormOpen,
[`!overflow-hidden children:line-clamp-${rowHeight}`]:
!fullMode && readOnly && rowHeight && !isExpandedFormOpen && !isForm,
}"
/>
<div v-if="isFormField && !readOnly">
<div
class="overflow-hidden"
:class="isFocused ? 'max-h-[50px]' : 'max-h-0'"
:style="{
transition: 'max-height 0.2s ease-in-out',
}"
>
<CellRichTextSelectedBubbleMenu v-if="editor" :editor="editor" embed-mode is-form-field />
</div>
</div>
</template>
</div> </div>
</template> </template>
@ -223,7 +284,28 @@ watch(editorDom, () => {
.nc-rich-text-embed { .nc-rich-text-embed {
.ProseMirror { .ProseMirror {
@apply !border-transparent max-h-full; @apply !border-transparent max-h-full;
min-height: 8rem; }
&:not(.nc-form-rich-text-field) {
.ProseMirror {
min-height: 8rem;
}
}
&.nc-form-rich-text-field {
.ProseMirror {
padding: 0;
}
&.readonly {
ul[data-type='taskList'] li input[type='checkbox'] {
background-color: #d5d5d9 !important;
&:not(:checked) {
@apply !border-gray-400;
}
&:focus {
box-shadow: none !important;
background-color: #d5d5d9 !important;
}
}
}
} }
&.readonly { &.readonly {
.nc-textarea-rich-editor { .nc-textarea-rich-editor {
@ -256,6 +338,13 @@ watch(editorDom, () => {
} }
.nc-textarea-rich-editor { .nc-textarea-rich-editor {
.tiptap p.is-editor-empty:first-child::before {
color: #6a7184;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.ProseMirror { .ProseMirror {
@apply flex-grow pt-1 border-1 border-gray-200 rounded-lg; @apply flex-grow pt-1 border-1 border-gray-200 rounded-lg;

248
packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue

@ -14,13 +14,12 @@ import MsFormatQuote from '~icons/material-symbols/format-quote'
interface Props { interface Props {
editor: Editor editor: Editor
embedMode?: boolean embedMode?: boolean
isFormField?: boolean
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const editor = computed(() => props.editor) const { editor, embedMode } = toRefs(props)
const embedMode = computed(() => props.embedMode)
const cmdOrCtrlKey = computed(() => { const cmdOrCtrlKey = computed(() => {
return isMac() ? '⌘' : 'CTRL' return isMac() ? '⌘' : 'CTRL'
@ -79,13 +78,15 @@ const onToggleLink = () => {
<template> <template>
<div <div
class="bubble-menu flex flex-row gap-x-1 bg-gray-100 py-1 rounded-lg px-1" class="bubble-menu flex-row gap-x-1 py-1 rounded-lg"
:class="{ :class="{
'inline-flex !bg-transparent': isFormField,
'flex bg-gray-100 px-1': !isFormField,
'embed-mode': embedMode, 'embed-mode': embedMode,
'full-mode': !embedMode, 'full-mode': !embedMode,
}" }"
> >
<NcTooltip :disabled="editor.isActive('codeBlock')"> <NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<template #title> <template #title>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div> <div>
@ -99,13 +100,14 @@ const onToggleLink = () => {
type="text" type="text"
:class="{ 'is-active': editor.isActive('bold') }" :class="{ 'is-active': editor.isActive('bold') }"
:disabled="editor.isActive('codeBlock')" :disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
@click="editor!.chain().focus().toggleBold().run()" @click="editor!.chain().focus().toggleBold().run()"
> >
<MdiFormatBold /> <MdiFormatBold />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip :disabled="editor.isActive('codeBlock')"> <NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<template #title> <template #title>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div> <div>
@ -119,12 +121,13 @@ const onToggleLink = () => {
type="text" type="text"
:disabled="editor.isActive('codeBlock')" :disabled="editor.isActive('codeBlock')"
:class="{ 'is-active': editor.isActive('italic') }" :class="{ 'is-active': editor.isActive('italic') }"
:tabindex="isFormField ? -1 : 0"
@click=";(editor!.chain().focus() as any).toggleItalic().run()" @click=";(editor!.chain().focus() as any).toggleItalic().run()"
> >
<MdiFormatItalic /> <MdiFormatItalic />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip :disabled="editor.isActive('codeBlock')"> <NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<template #title> <template #title>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div> <div>
@ -139,12 +142,13 @@ const onToggleLink = () => {
type="text" type="text"
:class="{ 'is-active': editor.isActive('underline') }" :class="{ 'is-active': editor.isActive('underline') }"
:disabled="editor.isActive('codeBlock')" :disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
@click="editor!.chain().focus().toggleUnderline().run()" @click="editor!.chain().focus().toggleUnderline().run()"
> >
<MdiFormatUnderline /> <MdiFormatUnderline />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip :disabled="editor.isActive('codeBlock')"> <NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<template #title> <template #title>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div> <div>
@ -158,155 +162,160 @@ const onToggleLink = () => {
type="text" type="text"
:class="{ 'is-active': editor.isActive('strike') }" :class="{ 'is-active': editor.isActive('strike') }"
:disabled="editor.isActive('codeBlock')" :disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
@click="editor!.chain().focus().toggleStrike().run()" @click="editor!.chain().focus().toggleStrike().run()"
> >
<MdiFormatStrikeThrough /> <MdiFormatStrikeThrough />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip v-if="embedMode"> <template v-if="!isFormField">
<template #title> {{ $t('general.code') }}</template> <NcTooltip v-if="embedMode">
<NcButton <template #title> {{ $t('general.code') }}</template>
size="small" <NcButton
type="text" size="small"
:class="{ 'is-active': editor.isActive('codeBlock') }" type="text"
@click="editor!.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }"
> @click="editor!.chain().focus().toggleCodeBlock().run()"
<MsCode /> >
</NcButton> <MsCode />
</NcTooltip> </NcButton>
<NcTooltip v-else :disabled="editor.isActive('codeBlock')"> </NcTooltip>
<template #title> {{ $t('general.quote') }}</template> <NcTooltip v-else :disabled="editor.isActive('codeBlock')">
<NcButton <template #title> {{ $t('general.quote') }}</template>
size="small" <NcButton
type="text" size="small"
:class="{ 'is-active': editor.isActive('code') }" type="text"
:disabled="editor.isActive('codeBlock')" :class="{ 'is-active': editor.isActive('code') }"
@click="editor!.chain().focus().toggleCode().run()" :disabled="editor.isActive('codeBlock')"
> @click="editor!.chain().focus().toggleCode().run()"
<MsFormatQuote /> >
</NcButton> <MsFormatQuote />
</NcTooltip> </NcButton>
<div class="divider"></div> </NcTooltip>
<div class="divider"></div>
<template v-if="embedMode"> <template v-if="embedMode">
<NcTooltip> <NcTooltip>
<template #title> <template #title>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div> <div>
{{ $t('labels.heading1') }} {{ $t('labels.heading1') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 1</div>
</div> </div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 1</div> </template>
</div> <NcButton
</template> size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 1 }).run()"
>
<MsFormatH1 />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading2') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 2</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 2 }).run()"
>
<MsFormatH2 />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading3') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 3</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 3 }).run()"
>
<MsFormatH3 />
</NcButton>
</NcTooltip>
<div class="divider"></div>
</template>
<NcTooltip v-if="embedMode">
<template #title> {{ $t('labels.blockQuote') }}</template>
<NcButton <NcButton
size="small" size="small"
type="text" type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }" :class="{ 'is-active': editor.isActive('blockquote') }"
@click="editor!.chain().focus().toggleHeading({ level: 1 }).run()" @click="editor!.chain().focus().toggleBlockquote().run()"
> >
<MsFormatH1 /> <TablerBlockQuote class="-mt-0.25" />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip> <NcTooltip>
<template #title> <template #title> {{ $t('labels.bulletList') }}</template>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading2') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 2</div>
</div>
</template>
<NcButton <NcButton
size="small" size="small"
type="text" type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }" :class="{ 'is-active': editor.isActive('bulletList') }"
@click="editor!.chain().focus().toggleHeading({ level: 2 }).run()" @click="editor!.chain().focus().toggleBulletList().run()"
> >
<MsFormatH2 /> <MdiFormatBulletList />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip> <NcTooltip>
<template #title> <template #title> {{ $t('labels.numberedList') }}</template>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading3') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 3</div>
</div>
</template>
<NcButton <NcButton
size="small" size="small"
type="text" type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }" :class="{ 'is-active': editor.isActive('orderedList') }"
@click="editor!.chain().focus().toggleHeading({ level: 3 }).run()" @click="editor!.chain().focus().toggleOrderedList().run()"
> >
<MsFormatH3 /> <MdiFormatListNumber />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title> {{ $t('labels.taskList') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('taskList') }"
@click="editor!.chain().focus().toggleTaskList().run()"
>
<MdiFormatListCheckbox />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<div class="divider"></div> <div class="divider"></div>
</template> </template>
<NcTooltip v-if="embedMode"> <NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<template #title> {{ $t('labels.blockQuote') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('blockquote') }"
@click="editor!.chain().focus().toggleBlockquote().run()"
>
<TablerBlockQuote class="-mt-0.25" />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title> {{ $t('labels.bulletList') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('bulletList') }"
@click="editor!.chain().focus().toggleBulletList().run()"
>
<MdiFormatBulletList />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title> {{ $t('labels.numberedList') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('orderedList') }"
@click="editor!.chain().focus().toggleOrderedList().run()"
>
<MdiFormatListNumber />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title> {{ $t('labels.taskList') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('taskList') }"
@click="editor!.chain().focus().toggleTaskList().run()"
>
<MdiFormatListCheckbox />
</NcButton>
</NcTooltip>
<div class="divider"></div>
<NcTooltip :disabled="editor.isActive('codeBlock')">
<template #title> {{ $t('general.link') }}</template> <template #title> {{ $t('general.link') }}</template>
<NcButton <NcButton
size="small" size="small"
type="text" type="text"
:class="{ 'is-active': editor.isActive('link') }" :class="{ 'is-active': editor.isActive('link') }"
:disabled="editor.isActive('codeBlock')" :disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
@click="onToggleLink" @click="onToggleLink"
> >
<div class="flex flex-row items-center px-0.5"> <GeneralIcon v-if="isFormField" icon="link2"></GeneralIcon>
<div v-else class="flex flex-row items-center px-0.5">
<MdiLink /> <MdiLink />
<div class="!text-xs !ml-1">{{ $t('general.link') }}</div> <div class="!text-xs !ml-1">{{ $t('general.link') }}</div>
</div> </div>
@ -343,6 +352,9 @@ const onToggleLink = () => {
.bubble-menu.embed-mode { .bubble-menu.embed-mode {
@apply border-transparent !shadow-none; @apply border-transparent !shadow-none;
} }
.bubble-menu.form-field-mode {
@apply bg-transparent px-0;
}
.embed-mode.bubble-menu { .embed-mode.bubble-menu {
@apply !py-0 !my-0 !border-0; @apply !py-0 !my-0 !border-0;

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

@ -284,6 +284,8 @@ const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggl
} }
if (!isProjectPopulated) { if (!isProjectPopulated) {
base.isLoading = false
const updatedProject = bases.value.get(base.id!)! const updatedProject = bases.value.get(base.id!)!
updatedProject.isLoading = false updatedProject.isLoading = false
} }

2
packages/nc-gui/components/dlg/TableDuplicate.vue

@ -144,7 +144,7 @@ const isEaster = ref(false)
{{ $t('general.duplicate') }} {{ $t('objects.table') }} {{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div> </div>
<div class="mt-4">{{ $t('msg.warning.duplicateProject') }}</div> <div class="mt-4">{{ $t('msg.warning.duplicateTable') }}</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div> <div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>

4
packages/nc-gui/components/general/ColorPicker.vue

@ -117,6 +117,10 @@ watch(picked, (n, _o) => {
filter: brightness(90%); filter: brightness(90%);
-webkit-filter: brightness(90%); -webkit-filter: brightness(90%);
} }
.color-selector:focus.new-design {
outline: none;
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
.color-selector.selected.new-design { .color-selector.selected.new-design {
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe; box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
} }

12
packages/nc-gui/components/general/FormBanner.vue

@ -11,7 +11,8 @@ const { getPossibleAttachmentSrc } = useAttachment()
<template> <template>
<div <div
class="nc-form-banner-wrapper w-full mx-auto bg-white border-1 border-gray-200 rounded-2xl overflow-hidden" class="nc-form-banner-wrapper w-full mx-auto rounded-2xl overflow-hidden"
:class="!bannerImageUrl ? 'shadow-sm' : ''"
:style="{ aspectRatio: 4 / 1 }" :style="{ aspectRatio: 4 / 1 }"
> >
<LazyCellAttachmentImage <LazyCellAttachmentImage
@ -19,16 +20,15 @@ const { getPossibleAttachmentSrc } = useAttachment()
:srcs="getPossibleAttachmentSrc(parseProp(bannerImageUrl))" :srcs="getPossibleAttachmentSrc(parseProp(bannerImageUrl))"
class="nc-form-banner-image object-cover w-full" class="nc-form-banner-image object-cover w-full"
/> />
<!-- Todo: aspect ratio and cover image uploader and image cropper to crop image in fixed aspect ratio --> <div v-else class="h-full flex items-stretch justify-between bg-white">
<div v-else class="h-full flex items-stretch justify-between"> <div class="flex -mt-1">
<div class="flex">
<img src="~assets/img/form-banner-left.png" alt="form-banner-left'" /> <img src="~assets/img/form-banner-left.png" alt="form-banner-left'" />
</div> </div>
<div class="w-[91px] flex justify-center"> <div class="w-[91px] flex justify-center">
<img class="max-h-full self-center" src="~assets/img/icons/256x256.png" alt="form-banner-logo'" /> <img class="max-h-full self-center" src="~assets/img/icons/256x256.png" alt="form-banner-logo" />
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end -mb-1">
<img src="~assets/img/form-banner-right.png" alt="form-banner-left'" /> <img src="~assets/img/form-banner-right.png" alt="form-banner-left'" />
</div> </div>
</div> </div>

12
packages/nc-gui/components/general/ImageCropper.vue

@ -5,22 +5,21 @@ import 'vue-advanced-cropper/dist/theme.classic.css'
import type { AttachmentReqType } from 'nocodb-sdk' import type { AttachmentReqType } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, useApi } from '#imports' import { extractSdkResponseErrorMsg, useApi } from '#imports'
import type { ImageCropperConfig } from '~/lib'
interface Props { interface Props {
imageConfig: { imageConfig: {
src: string src: string
type: string type: string
name: string name: string
} }
cropperConfig: { cropperConfig: ImageCropperConfig
aspectRatio?: number
}
uploadConfig?: { uploadConfig?: {
path?: string path?: string
} }
showCropper: boolean showCropper: boolean
} }
const { imageConfig, cropperConfig, uploadConfig, ...props } = defineProps<Props>() const { imageConfig, cropperConfig, uploadConfig, ...props } = defineProps<Props>()
const emit = defineEmits(['update:showCropper', 'submit']) const emit = defineEmits(['update:showCropper', 'submit'])
const showCropper = useVModel(props, 'showCropper', emit) const showCropper = useVModel(props, 'showCropper', emit)
@ -103,7 +102,10 @@ watch(showCropper, () => {
class="nc-cropper relative" class="nc-cropper relative"
:src="imageConfig.src" :src="imageConfig.src"
:auto-zoom="true" :auto-zoom="true"
:stencil-props="cropperConfig?.aspectRatio ? { aspectRatio: cropperConfig.aspectRatio } : {}" :stencil-props="cropperConfig?.stencilProps || {}"
:min-height="cropperConfig?.minHeight"
:min-width="cropperConfig?.minWidth"
:image-restriction="cropperConfig?.imageRestriction"
/> />
<div v-if="previewImage.src" class="result_preview"> <div v-if="previewImage.src" class="result_preview">
<img :src="previewImage.src" alt="Preview Image" /> <img :src="previewImage.src" alt="Preview Image" />

25
packages/nc-gui/components/project/AllTables.vue

@ -7,6 +7,8 @@ const { activeTables } = storeToRefs(useTablesStore())
const { openTable } = useTablesStore() const { openTable } = useTablesStore()
const { openedProject } = storeToRefs(useBases()) const { openedProject } = storeToRefs(useBases())
const { base } = useBase()
const isNewBaseModalOpen = ref(false) const isNewBaseModalOpen = ref(false)
const isDataSourceLimitReached = computed(() => Number(openedProject.value?.sources?.length) > 1) const isDataSourceLimitReached = computed(() => Number(openedProject.value?.sources?.length) > 1)
@ -78,7 +80,12 @@ const onCreateBaseClick = () => {
<template> <template>
<div class="nc-all-tables-view"> <div class="nc-all-tables-view">
<div class="flex flex-row gap-x-6 pb-3 pt-6"> <div
class="flex flex-row gap-x-6 pb-3 pt-6"
:class="{
'pointer-events-none': base?.isLoading,
}"
>
<div <div
v-if="isUIAllowed('tableCreate')" v-if="isUIAllowed('tableCreate')"
role="button" role="button"
@ -121,7 +128,21 @@ const onCreateBaseClick = () => {
</div> </div>
</component> </component>
</div> </div>
<template v-if="activeTables.length"> <div
v-if="base?.isLoading"
class="flex items-center justify-center text-center"
:style="{
height: 'calc(100vh - var(--topbar-height) - 18rem)',
}"
>
<div>
<GeneralLoader size="xlarge" />
<div class="mt-2">
{{ $t('general.loading') }}
</div>
</div>
</div>
<template v-else-if="activeTables.length">
<div class="flex flex-row w-full text-gray-400 border-b-1 border-gray-50 py-3 px-2.5"> <div class="flex flex-row w-full text-gray-400 border-b-1 border-gray-50 py-3 px-2.5">
<div class="w-2/5">{{ $t('objects.table') }}</div> <div class="w-2/5">{{ $t('objects.table') }}</div>
<div class="w-1/5">{{ $t('general.source') }}</div> <div class="w-1/5">{{ $t('general.source') }}</div>

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

@ -202,7 +202,7 @@ onUnmounted(() => {
{ {
'text-brand-500': isPrimary(column) && !props.virtual && !isForm && !isCalendar, 'text-brand-500': isPrimary(column) && !props.virtual && !isForm && !isCalendar,
'nc-grid-numeric-cell-right': isGrid && isNumericField && !isEditColumnMenu && !isForm && !isExpandedFormOpen, 'nc-grid-numeric-cell-right': isGrid && isNumericField && !isEditColumnMenu && !isForm && !isExpandedFormOpen,
'h-10': isForm && !isSurveyForm && !isAttachment(column) && !props.virtual, 'h-10': isForm && !isSurveyForm && !isAttachment(column) && !isTextArea(column) && !isJSON(column) && !props.virtual,
'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu, 'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu,
'!min-h-30': isTextArea(column) && (isForm || isSurveyForm), '!min-h-30': isTextArea(column) && (isForm || isSurveyForm),
}, },

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

File diff suppressed because it is too large Load Diff

76
packages/nc-gui/components/smartsheet/form/LimitOptions.vue

@ -9,6 +9,7 @@ import { MetaInj, iconMap } from '#imports'
const props = defineProps<{ const props = defineProps<{
modelValue: FormFieldsLimitOptionsType[] modelValue: FormFieldsLimitOptionsType[]
column: ColumnType column: ColumnType
isRequired?: boolean
}>() }>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -121,6 +122,18 @@ async function onMove(_event: { moved: { newIndex: number; oldIndex: number; ele
vModel.value = [...vModel.value] vModel.value = [...vModel.value]
} }
const showOrHideAll = (showAll: boolean) => {
if (props.isRequired && !showAll) {
return
}
vModel.value = vModel.value.map((o) => {
return {
...o,
show: showAll,
}
})
}
</script> </script>
<template> <template>
@ -147,13 +160,42 @@ async function onMove(_event: { moved: { newIndex: number; oldIndex: number; ele
</template> </template>
</a-input> </a-input>
</div> </div>
<div v-if="vModel.length" class="flex items-stretch gap-2 pr-2 pl-3 py-1.5 rounded-t-lg border-1 border-b-0 border-gray-200">
<NcTooltip class="truncate max-w-full" :disabled="!isRequired">
<template #title> {{ $t('msg.info.preventHideAllOptions') }} </template>
<NcButton
type="secondary"
size="xxsmall"
class="!border-none !px-2 !text-xs !text-gray-500 !disabled:text-gray-300"
:disabled="isRequired || vModel.filter((o) => !o.show).length === vModel.length"
@click="showOrHideAll(false)"
>
Hide all
</NcButton>
</NcTooltip>
<div>
<NcButton
type="secondary"
size="xxsmall"
class="!border-none !px-2 !text-xs !text-gray-500 !disabled:text-gray-300"
:disabled="vModel.filter((o) => o.show).length === vModel.length"
@click="showOrHideAll(true)"
>
Show all
</NcButton>
</div>
</div>
<Draggable <Draggable
v-if="vModel.length" v-if="vModel.length"
:model-value="vModel" :model-value="vModel"
item-key="id" item-key="id"
handle=".nc-child-draggable-icon" handle=".nc-child-draggable-icon"
ghost-class="nc-form-field-limit-option-ghost" ghost-class="nc-form-field-limit-option-ghost"
class="rounded-lg border-1 border-gray-200 !max-h-[224px] overflow-y-auto nc-form-scrollbar" class="rounded-b-lg border-1 border-gray-200 !max-h-[224px] overflow-y-auto nc-form-scrollbar"
@change="onMove($event)" @change="onMove($event)"
@start="drag = true" @start="drag = true"
@end="drag = false" @end="drag = false"
@ -175,19 +217,25 @@ async function onMove(_event: { moved: { newIndex: number; oldIndex: number; ele
> >
<component :is="iconMap.drag" class="nc-child-draggable-icon flex-none cursor-move !h-4 !w-4 text-gray-600" /> <component :is="iconMap.drag" class="nc-child-draggable-icon flex-none cursor-move !h-4 !w-4 text-gray-600" />
<div <NcTooltip :disabled="!isRequired || !(element.show && isRequired && vModel.filter((o) => o.show).length === 1)">
@click=" <template #title> {{ $t('msg.info.preventHideAllOptions') }} </template>
() => {
element.show = !element.show <div
vModel = [...vModel] class="!border-none !px-2"
} @click="
" () => {
> if (element.show && isRequired && vModel.filter((o) => o.show).length === 1) return
<component element.show = !element.show
:is="element.show ? iconMap.eye : iconMap.eyeSlash" vModel = [...vModel]
class="flex-none cursor-pointer !h-4 !w-4 text-gray-600" }
/> "
</div> >
<component
:is="element.show ? iconMap.eye : iconMap.eyeSlash"
class="flex-none cursor-pointer !h-4 !w-4 text-gray-600"
/>
</div>
</NcTooltip>
<a-tag v-if="column.uidt === UITypes.User" class="rounded-tag max-w-[calc(100%_-_70px)] !pl-0" color="'#ccc'"> <a-tag v-if="column.uidt === UITypes.User" class="rounded-tag max-w-[calc(100%_-_70px)] !pl-0" color="'#ccc'">
<span <span

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

@ -694,9 +694,9 @@
"appearanceSettings": "Appearance Settings", "appearanceSettings": "Appearance Settings",
"backgroundColor": "Background Color", "backgroundColor": "Background Color",
"hideNocodbBranding": "Hide NocoDB Branding", "hideNocodbBranding": "Hide NocoDB Branding",
"showOnConditions": "Show on condtions", "showOnConditions": "Show on conditions",
"showFieldOnConditionsMet": "Shows field only when conditions are met", "showFieldOnConditionsMet": "Shows field only when conditions are met",
"limitOptions": "Limit ptions", "limitOptions": "Limit options",
"limitOptionsSubtext": "Limit options visible to users by selecting available options", "limitOptionsSubtext": "Limit options visible to users by selecting available options",
"clearSelection": "Clear selection" "clearSelection": "Clear selection"
}, },
@ -1181,6 +1181,7 @@
"formInput": "Enter form input label", "formInput": "Enter form input label",
"formHelpText": "Add some help text", "formHelpText": "Add some help text",
"onlyCreator": "Only visible to Creator", "onlyCreator": "Only visible to Creator",
"formTitle": "Add form Title",
"formDesc": "Add form description", "formDesc": "Add form description",
"beforeEnablePwd": "Restrict access with a password", "beforeEnablePwd": "Restrict access with a password",
"afterEnablePwd": "Access is password restricted", "afterEnablePwd": "Access is password restricted",
@ -1305,8 +1306,10 @@
"groupPasteIsNotSupportedOnLinksColumn": "Group paste operation is not supported on Links/LinkToAnotherRecord column", "groupPasteIsNotSupportedOnLinksColumn": "Group paste operation is not supported on Links/LinkToAnotherRecord column",
"groupClearIsNotSupportedOnLinksColumn": "Group clear operation is not supported on Links/LinkToAnotherRecord column", "groupClearIsNotSupportedOnLinksColumn": "Group clear operation is not supported on Links/LinkToAnotherRecord column",
"upgradeToEnterpriseEdition": "Upgrade to Enterprise Edition {extraInfo}", "upgradeToEnterpriseEdition": "Upgrade to Enterprise Edition {extraInfo}",
"thisFeatureIsOnlyAvailableInEnterpriseEdition": "This feature is only available in enterprise edition",
"yourCurrentRoleIs": "Your current role is", "yourCurrentRoleIs": "Your current role is",
"pleaseRequestAccessForView": "Please request for higher permission from the Admin / Base owner / Workspace owner to get access to this {viewName}" "pleaseRequestAccessForView": "Please request for higher permission from the Admin / Base owner / Workspace owner to get access to this {viewName}",
"preventHideAllOptions": "You cannot hide all options if field is required"
}, },
"error": { "error": {
"fetchingCalendarData": "Error fetching calendar data", "fetchingCalendarData": "Error fetching calendar data",

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

@ -207,6 +207,15 @@ interface FormFieldsLimitOptionsType {
show: boolean show: boolean
} }
interface ImageCropperConfig {
stencilProps?: {
aspectRatio?: number
}
minHeight?: number
minWidth?: number
imageRestriction?: 'fill-area' | 'fit-area' | 'stencil' | 'none'
}
export type { export type {
User, User,
ProjectMetaInfo, ProjectMetaInfo,
@ -236,4 +245,5 @@ export type {
CommandPaletteType, CommandPaletteType,
CalendarRangeType, CalendarRangeType,
FormFieldsLimitOptionsType, FormFieldsLimitOptionsType,
ImageCropperConfig,
} }

1
packages/nc-gui/package.json

@ -41,6 +41,7 @@
"@iconify/vue": "^4.1.1", "@iconify/vue": "^4.1.1",
"@pinia/nuxt": "^0.5.1", "@pinia/nuxt": "^0.5.1",
"@tiptap/extension-link": "2.2.4", "@tiptap/extension-link": "2.2.4",
"@tiptap/extension-placeholder": "^2.2.4",
"@tiptap/extension-task-list": "2.2.4", "@tiptap/extension-task-list": "2.2.4",
"@tiptap/extension-underline": "^2.2.4", "@tiptap/extension-underline": "^2.2.4",
"@tiptap/html": "2.2.4", "@tiptap/html": "2.2.4",

37
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue

@ -9,11 +9,6 @@ const route = useRoute()
const router = useRouter() const router = useRouter()
// For now dark theme is disabled
// const onClick = () => {
// isDark.value = !isDark.value
// }
onMounted(() => { onMounted(() => {
isDark.value = false isDark.value = false
}) })
@ -39,16 +34,6 @@ router.afterEach((to) => shouldRedirect(to.name as string))
}" }"
> >
<NuxtPage /> <NuxtPage />
<!-- <div
class="color-transition flex items-center justify-center cursor-pointer absolute top-4 md:top-15 right-4 md:right-15 rounded-full p-2 bg-white dark:(bg-slate-600) shadow hover:(ring-1 ring-accent ring-opacity-100)"
@click="onClick"
>
<Transition name="slide-left" duration="250" mode="out-in">
<MaterialSymbolsDarkModeOutline v-if="isDark" />
<MaterialSymbolsLightModeOutline v-else />
</Transition>
</div> -->
</div> </div>
</template> </template>
@ -100,7 +85,7 @@ p {
@apply w-full; @apply w-full;
&:not(.layout-list) { &:not(.layout-list) {
@apply rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500; @apply rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500 overflow-hidden;
& > div { & > div {
@apply !bg-transparent; @apply !bg-transparent;
@ -153,32 +138,26 @@ p {
& > div { & > div {
@apply w-full; @apply w-full;
} }
:deep(textarea) {
@apply !p-2;
&:focus { textarea {
box-shadow: none !important; @apply px-3;
}
} }
} }
&:not(.nc-cell-longtext) { &:not(.nc-cell-longtext) {
@apply p-2; @apply p-2;
} }
textarea {
@apply px-4 py-2 rounded;
&:focus {
box-shadow: none !important;
}
}
&.nc-cell-json { &.nc-cell-json {
@apply h-auto; @apply h-auto;
& > div { & > div {
@apply w-full; @apply w-full;
} }
} }
.ant-picker,
input.nc-cell-field {
@apply !py-0 !px-1;
}
} }
} }

59
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue

@ -79,47 +79,60 @@ const onDecode = async (scannedCodeValue: string) => {
</script> </script>
<template> <template>
<div class="h-full flex flex-col items-center"> <div class="h-full flex flex-col items-center w-full max-w-[max(33%,688px)] mx-auto">
<GeneralFormBanner <GeneralFormBanner
v-if="sharedFormView" v-if="sharedFormView && !parseProp(sharedFormView?.meta).hide_banner"
:banner-image-url="sharedFormView.banner_image_url" :banner-image-url="sharedFormView.banner_image_url"
class="flex-none dark:border-none" class="flex-none dark:border-none"
/> />
<div <div
class="transition-all duration-300 ease-in relative flex flex-col justify-center gap-2 w-full max-w-[max(33%,688px)] mx-auto my-6 bg-white dark:bg-transparent rounded-3xl border-1 border-gray-200 px-4 py-8 lg:p-12 md:(p-8 dark:bg-slate-700)" class="transition-all duration-300 ease-in relative flex flex-col justify-center gap-2 w-full my-6 bg-white dark:bg-transparent rounded-3xl border-1 border-gray-200 px-4 py-8 lg:p-12 md:(p-8 dark:bg-slate-700)"
> >
<template v-if="sharedFormView"> <template v-if="sharedFormView">
<div class="mb-4"> <div>
<h1 class="text-2xl font-bold text-gray-900 mb-4"> <h1 class="text-2xl font-bold text-gray-900 mb-4">
{{ sharedFormView.heading }} {{ sharedFormView.heading }}
</h1> </h1>
<h2 v-if="sharedFormView.subheading" class="font-medium text-base text-gray-500 dark:text-slate-300 mb-4"> <div v-if="sharedFormView.subheading">
{{ sharedFormView.subheading }} <LazyCellRichText
</h2> :value="sharedFormView.subheading"
class="font-medium text-base text-gray-500 dark:text-slate-300 !h-auto mb-4 -ml-1"
is-form-field
read-only
sync-value-change
/>
</div>
</div> </div>
<a-alert v-if="notFound" type="warning" class="my-4 text-center" message="Not found" /> <a-alert v-if="notFound" type="warning" class="!mt-2 !mb-4 text-center" message="Not found" />
<template v-else-if="submitted"> <template v-else-if="submitted">
<div class="flex justify-center"> <div class="flex justify-center">
<div v-if="sharedFormView" class="w-full lg:w-[95%]"> <div v-if="sharedFormView" class="w-full">
<a-alert <a-alert class="!mt-2 !mb-4 !py-4 text-left !rounded-lg" type="success" outlined>
type="success" <template #message>
class="!my-4 text-center !rounded-lg" <LazyCellRichText
outlined v-if="sharedFormView?.success_msg?.trim()"
:message="sharedFormView.success_msg || 'Successfully submitted form data'" :value="sharedFormView?.success_msg"
/> class="!h-auto -ml-1"
is-form-field
read-only
sync-value-change
/>
<span v-else> {{ $t('msg.successfullySubmittedFormData') }} </span>
</template>
</a-alert>
<p v-if="sharedFormView.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 text-center my-4"> <p v-if="sharedFormView.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 my-4">
{{ $t('msg.newFormWillBeLoaded', { seconds: secondsRemain }) }} {{ $t('msg.newFormWillBeLoaded', { seconds: secondsRemain }) }}
</p> </p>
<div v-if="sharedFormView.submit_another_form" class="text-center"> <div v-if="sharedFormView.submit_another_form" class="text-right">
<NcButton type="primary" size="medium" @click="submitted = false"> <NcButton type="primary" size="medium" @click="submitted = false">
{{ $t('activity.submitAnotherForm') }}</NcButton {{ $t('activity.submitAnotherForm') }}
> </NcButton>
</div> </div>
</div> </div>
</div> </div>
@ -157,7 +170,13 @@ const onDecode = async (scannedCodeValue: string) => {
<span v-if="isRequired(field, field.required)" class="text-red-500 text-base leading-[18px]">&nbsp;*</span> <span v-if="isRequired(field, field.required)" class="text-red-500 text-base leading-[18px]">&nbsp;*</span>
</div> </div>
<div v-if="field?.description" class="nc-form-column-description text-gray-500 text-sm"> <div v-if="field?.description" class="nc-form-column-description text-gray-500 text-sm">
{{ field?.description }} <LazyCellRichText
:value="field?.description"
class="!h-auto -ml-1"
is-form-field
read-only
sync-value-change
/>
</div> </div>
<div> <div>

55
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue

@ -239,20 +239,22 @@ onMounted(() => {
<div <div
v-if="sharedFormView" v-if="sharedFormView"
style="height: max(40vh, 225px); min-height: 225px" style="height: max(40vh, 225px); min-height: 225px"
class="max-w-[max(33%,600px)] mx-auto flex flex-col justify-end" class="w-full max-w-[max(33%,600px)] mx-auto flex flex-col justify-end"
> >
<div class="px-4 md:px-0 flex flex-col justify-end"> <div class="px-4 md:px-0 flex flex-col justify-end">
<h1 class="text-2xl font-bold text-gray-900 self-center my-4" data-testid="nc-survey-form__heading"> <h1 class="text-2xl font-bold text-gray-900 self-center text-center my-4" data-testid="nc-survey-form__heading">
{{ sharedFormView.heading }} {{ sharedFormView.heading }}
</h1> </h1>
<div v-if="sharedFormView.subheading?.trim()" class="w-full">
<h2 <LazyCellRichText
v-if="sharedFormView.subheading && sharedFormView.subheading !== ''" :value="sharedFormView.subheading"
class="font-medium text-base text-gray-500 dark:text-gray-300 self-center mb-4" class="font-medium text-base text-gray-500 dark:text-slate-300 !h-auto mb-4 -ml-1"
data-testid="nc-survey-form__sub-heading" is-form-field
> read-only
{{ sharedFormView?.subheading }} sync-value-change
</h2> data-testid="nc-survey-form__sub-heading"
/>
</div>
</div> </div>
</div> </div>
@ -275,7 +277,7 @@ onMounted(() => {
class="nc-form-column-description text-gray-500 text-sm" class="nc-form-column-description text-gray-500 text-sm"
data-testid="nc-survey-form__field-description" data-testid="nc-survey-form__field-description"
> >
{{ field?.description }} <LazyCellRichText :value="field?.description" class="!h-auto -ml-1" is-form-field read-only sync-value-change />
</div> </div>
<LazySmartsheetDivDataCell v-if="field.title" class="relative nc-form-data-cell"> <LazySmartsheetDivDataCell v-if="field.title" class="relative nc-form-data-cell">
@ -374,20 +376,35 @@ onMounted(() => {
<Transition name="slide-left"> <Transition name="slide-left">
<div v-if="submitted" class="flex flex-col justify-center items-center text-center"> <div v-if="submitted" class="flex flex-col justify-center items-center text-center">
<a-alert <a-alert
class="!my-4 !py-4 !rounded-lg text-left w-full"
type="success" type="success"
class="!my-4 !py-4 text-center !rounded-lg"
data-testid="nc-survey-form__success-msg" data-testid="nc-survey-form__success-msg"
outlined outlined
:message="sharedFormView?.success_msg || $t('msg.info.thankYou')" >
:description="sharedFormView?.success_msg ? undefined : $t('msg.info.submittedFormData')" <template #message>
/> <LazyCellRichText
v-if="sharedFormView?.success_msg?.trim()"
<div v-if="sharedFormView" class="mt-3"> :value="sharedFormView?.success_msg"
<p v-if="sharedFormView?.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 text-center my-4"> class="!h-auto -ml-1"
is-form-field
read-only
sync-value-change
/>
<span v-else>
{{ $t('msg.info.thankYou') }}
</span>
</template>
<template v-if="!sharedFormView?.success_msg?.trim()" #description>
{{ $t('msg.info.submittedFormData') }}
</template>
</a-alert>
<div v-if="sharedFormView" class="mt-3 w-full">
<p v-if="sharedFormView?.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 text-left my-4">
{{ $t('labels.newFormLoaded') }} {{ secondsRemain }} {{ $t('general.seconds') }} {{ $t('labels.newFormLoaded') }} {{ secondsRemain }} {{ $t('general.seconds') }}
</p> </p>
<div v-if="sharedFormView?.submit_another_form" class="text-center"> <div v-if="sharedFormView?.submit_another_form" class="text-right">
<NcButton type="primary" size="medium" data-testid="nc-survey-form__btn-submit-another-form" @click="resetForm"> <NcButton type="primary" size="medium" data-testid="nc-survey-form__btn-submit-another-form" @click="resetForm">
{{ $t('activity.submitAnotherForm') }} {{ $t('activity.submitAnotherForm') }}
</NcButton> </NcButton>

2
packages/nc-gui/utils/iconUtils.ts

@ -128,6 +128,7 @@ import NcItalic from '~icons/nc-icons/italic'
import NcBold from '~icons/nc-icons/bold' import NcBold from '~icons/nc-icons/bold'
import NcUnderline from '~icons/nc-icons/underline' import NcUnderline from '~icons/nc-icons/underline'
import NcCrop from '~icons/nc-icons/crop' import NcCrop from '~icons/nc-icons/crop'
import NcLink from '~icons/nc-icons/link'
// keep it for reference // keep it for reference
// todo: remove it after all icons are migrated // todo: remove it after all icons are migrated
@ -326,6 +327,7 @@ export const iconMap = {
translate: h('span', { class: 'material-symbols' }, 'translate'), translate: h('span', { class: 'material-symbols' }, 'translate'),
preview: h('span', { class: 'material-symbols' }, 'visibility'), preview: h('span', { class: 'material-symbols' }, 'visibility'),
link: h('span', { class: 'material-symbols' }, 'link'), link: h('span', { class: 'material-symbols' }, 'link'),
link2: NcLink,
returnKey: h('span', { class: 'material-symbols' }, 'keyboard_return'), returnKey: h('span', { class: 'material-symbols' }, 'keyboard_return'),
keyboard: h('span', { class: 'material-symbols' }, 'keyboard'), keyboard: h('span', { class: 'material-symbols' }, 'keyboard'),
accountPlus: h('span', { class: 'material-symbols' }, 'person_add'), accountPlus: h('span', { class: 'material-symbols' }, 'person_add'),

13
pnpm-lock.yaml

@ -42,6 +42,9 @@ importers:
'@tiptap/extension-link': '@tiptap/extension-link':
specifier: 2.2.4 specifier: 2.2.4
version: 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) version: 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)
'@tiptap/extension-placeholder':
specifier: ^2.2.4
version: 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)
'@tiptap/extension-task-list': '@tiptap/extension-task-list':
specifier: 2.2.4 specifier: 2.2.4
version: 2.2.4(@tiptap/core@2.2.4) version: 2.2.4(@tiptap/core@2.2.4)
@ -8742,6 +8745,16 @@ packages:
'@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4)
dev: false dev: false
/@tiptap/extension-placeholder@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4):
resolution: {integrity: sha512-UL4Fn9T33SoS7vdI3NnSxBJVeGUIgCIutgXZZ5J8CkcRoDIeS78z492z+6J+qGctHwTd0xUL5NzNJI82HfiTdg==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.2.4(@tiptap/pm@2.2.4)
'@tiptap/pm': 2.2.4
dev: false
/@tiptap/extension-strike@2.2.4(@tiptap/core@2.2.4): /@tiptap/extension-strike@2.2.4(@tiptap/core@2.2.4):
resolution: {integrity: sha512-/a2EwQgA+PpG17V2tVRspcrIY0SN3blwcgM7lxdW4aucGkqSKnf7+91dkhQEwCZ//o8kv9mBCyRoCUcGy6S5Xg==} resolution: {integrity: sha512-/a2EwQgA+PpG17V2tVRspcrIY0SN3blwcgM7lxdW4aucGkqSKnf7+91dkhQEwCZ//o8kv9mBCyRoCUcGy6S5Xg==}
peerDependencies: peerDependencies:

32
tests/playwright/pages/Dashboard/Form/index.ts

@ -10,8 +10,7 @@ export class FormPage extends BasePage {
readonly topbar: TopbarPage; readonly topbar: TopbarPage;
// todo: All the locator should be private // todo: All the locator should be private
readonly addAllButton: Locator; readonly addOrRemoveAllButton: Locator;
readonly removeAllButton: Locator;
readonly submitButton: Locator; readonly submitButton: Locator;
readonly showAnotherFormRadioButton: Locator; readonly showAnotherFormRadioButton: Locator;
@ -30,8 +29,10 @@ export class FormPage extends BasePage {
this.toolbar = new ToolbarPage(this); this.toolbar = new ToolbarPage(this);
this.topbar = new TopbarPage(this); this.topbar = new TopbarPage(this);
this.addAllButton = dashboard.get().locator('[data-testid="nc-form-show-all-fields"]').locator('.nc-switch'); this.addOrRemoveAllButton = dashboard
this.removeAllButton = dashboard.get().locator('[data-testid="nc-form-show-all-fields"]').locator('.nc-switch'); .get()
.locator('[data-testid="nc-form-show-all-fields"]')
.locator('.nc-switch');
this.submitButton = dashboard.get().locator('[data-testid="nc-form-submit"]'); this.submitButton = dashboard.get().locator('[data-testid="nc-form-submit"]');
this.showAnotherFormRadioButton = dashboard.get().locator('[data-testid="nc-form-checkbox-submit-another-form"]'); this.showAnotherFormRadioButton = dashboard.get().locator('[data-testid="nc-form-checkbox-submit-another-form"]');
@ -40,8 +41,8 @@ export class FormPage extends BasePage {
.locator('[data-testid="nc-form-checkbox-show-blank-form"]'); .locator('[data-testid="nc-form-checkbox-show-blank-form"]');
this.emailMeRadioButton = dashboard.get().locator('[data-testid="nc-form-checkbox-send-email"]'); this.emailMeRadioButton = dashboard.get().locator('[data-testid="nc-form-checkbox-send-email"]');
this.formHeading = dashboard.get().locator('[data-testid="nc-form-heading"]'); this.formHeading = dashboard.get().locator('[data-testid="nc-form-heading"]');
this.formSubHeading = dashboard.get().locator('[data-testid="nc-form-sub-heading"]'); this.formSubHeading = dashboard.get().locator('[data-testid="nc-form-sub-heading"] .tiptap.ProseMirror');
this.afterSubmitMsg = dashboard.get().locator('[data-testid="nc-form-after-submit-msg"]'); this.afterSubmitMsg = dashboard.get().locator('[data-testid="nc-form-after-submit-msg"] .tiptap.ProseMirror');
this.formFields = dashboard.get().locator('.nc-form-fields-list'); this.formFields = dashboard.get().locator('.nc-form-fields-list');
} }
@ -67,7 +68,7 @@ export class FormPage extends BasePage {
} }
getFormFieldsInputHelpText() { getFormFieldsInputHelpText() {
return this.get().locator('textarea[data-testid="nc-form-input-help-text"]:visible'); return this.get().locator('[data-testid="nc-form-input-help-text"] .tiptap.ProseMirror:visible');
} }
async verifyFormFieldLabel({ index, label }: { index: number; label: string }) { async verifyFormFieldLabel({ index, label }: { index: number; label: string }) {
@ -83,7 +84,7 @@ export class FormPage extends BasePage {
} }
async verifyAfterSubmitMsg({ msg }: { msg: string }) { async verifyAfterSubmitMsg({ msg }: { msg: string }) {
expect((await this.afterSubmitMsg.inputValue()).includes(msg)).toBeTruthy(); expect((await this.afterSubmitMsg.textContent()).includes(msg)).toBeTruthy();
} }
async verifyFormViewFieldsOrder({ fields }: { fields: string[] }) { async verifyFormViewFieldsOrder({ fields }: { fields: string[] }) {
@ -136,11 +137,18 @@ export class FormPage extends BasePage {
} }
async removeAllFields() { async removeAllFields() {
await this.removeAllButton.click(); if (await this.addOrRemoveAllButton.isChecked()) {
await this.addOrRemoveAllButton.click();
} else {
await this.addOrRemoveAllButton.click();
await this.addOrRemoveAllButton.click();
}
} }
async addAllFields() { async addAllFields() {
await this.addAllButton.click(); if (!(await this.addOrRemoveAllButton.isChecked())) {
await this.addOrRemoveAllButton.click();
}
} }
async configureHeader(param: { subtitle: string; title: string }) { async configureHeader(param: { subtitle: string; title: string }) {
@ -166,7 +174,7 @@ export class FormPage extends BasePage {
async verifyHeader(param: { subtitle: string; title: string }) { async verifyHeader(param: { subtitle: string; title: string }) {
await expect.poll(async () => await this.formHeading.inputValue()).toBe(param.title); await expect.poll(async () => await this.formHeading.inputValue()).toBe(param.title);
await expect.poll(async () => await this.formSubHeading.inputValue()).toBe(param.subtitle); await expect.poll(async () => await this.formSubHeading.textContent()).toBe(param.subtitle);
} }
async fillForm(param: { field: string; value: string }[]) { async fillForm(param: { field: string; value: string }[]) {
@ -233,7 +241,7 @@ export class FormPage extends BasePage {
const fieldHelpText = this.get() const fieldHelpText = this.get()
.locator(`.nc-form-drag-${field.replace(' ', '')}`) .locator(`.nc-form-drag-${field.replace(' ', '')}`)
.locator('div[data-testid="nc-form-input-help-text-label"]'); .locator('div[data-testid="nc-form-input-help-text-label"] .tiptap.ProseMirror');
await expect(fieldHelpText).toHaveText(helpText); await expect(fieldHelpText).toHaveText(helpText);
} }

1
tests/playwright/tests/db/views/viewForm.spec.ts

@ -6,7 +6,6 @@ import { SharedFormPage } from '../../../pages/SharedForm';
import { Api, UITypes } from 'nocodb-sdk'; import { Api, UITypes } from 'nocodb-sdk';
import { LoginPage } from '../../../pages/LoginPage'; import { LoginPage } from '../../../pages/LoginPage';
import { getDefaultPwd } from '../../../tests/utils/general'; import { getDefaultPwd } from '../../../tests/utils/general';
import { WorkspacePage } from '../../../pages/WorkspacePage';
import { enableQuickRun, isEE } from '../../../setup/db'; import { enableQuickRun, isEE } from '../../../setup/db';
// todo: Move most of the ui actions to page object and await on the api response // todo: Move most of the ui actions to page object and await on the api response

Loading…
Cancel
Save