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 9 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">
<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 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"/>
</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="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 { generateJSON } from '@tiptap/html'
import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder'
import { TaskItem } from '@/helpers/dbTiptapExtensions/task-item'
import { Link } from '@/helpers/dbTiptapExtensions/links'
import { IsExpandedFormOpenInj, IsFormInj, ReadonlyInj, RowHeightInj } from '#imports'
@ -16,6 +17,10 @@ const props = defineProps<{
syncValueChange?: boolean
showMenu?: boolean
fullMode?: boolean
isFormField?: boolean
autofocus?: boolean
placeholder?: string
renderAsText?: boolean
}>()
const emits = defineEmits(['update:value'])
@ -28,6 +33,8 @@ const readOnlyCell = inject(ReadonlyInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isFocused = ref(false)
const turndownService = new TurndownService({})
turndownService.addRule('lineBreak', {
@ -105,13 +112,19 @@ const editorDom = ref<HTMLElement | null>(null)
const vModel = useVModel(props, 'value', emits, { defaultValue: '' })
const tiptapExtensions = [
StarterKit,
StarterKit.configure({
heading: props.isFormField ? false : undefined,
}),
TaskList,
TaskItem.configure({
nested: true,
}),
Underline,
Link,
Placeholder.configure({
emptyEditorClass: 'is-editor-empty',
placeholder: props.placeholder,
}),
]
const editor = useEditor({
@ -121,9 +134,18 @@ const editor = useEditor({
.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n')
vModel.value = markdown
vModel.value = props.isFormField && markdown === '<br />' ? '' : markdown
},
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) => {
@ -153,21 +175,43 @@ const setEditorContent = (contentMd: any, focusEndOfDoc?: boolean) => {
}
if (props.syncValueChange) {
watch(vModel, () => {
watch([vModel, editor], () => {
setEditorContent(vModel.value)
})
}
if (props.isFormField) {
watch([props, editor], () => {
if (props.readOnly) {
editor.value?.setEditable(false)
} else {
editor.value?.setEditable(true)
}
})
}
watch(editorDom, () => {
if (!editorDom.value) return
setEditorContent(vModel.value, true)
if (props.isFormField) return
// Focus editor after editor is mounted
setTimeout(() => {
editor.value?.chain().focus().run()
}, 50)
})
useEventListener(
editorDom,
'focusout',
(e: FocusEvent) => {
if (!(e?.relatedTarget as HTMLElement)?.closest('.bubble-menu, .nc-textarea-rich-editor')) {
isFocused.value = false
}
},
true,
)
</script>
<template>
@ -177,34 +221,51 @@ watch(editorDom, () => {
'flex flex-col flex-grow nc-rich-text-full': fullMode,
'nc-rich-text-embed flex flex-col pl-1 w-full': !fullMode,
'readonly': readOnly,
'nc-form-rich-text-field !p-0': isFormField,
}"
:tabindex="readOnlyCell ? -1 : 0"
:tabindex="readOnlyCell || isFormField ? -1 : 0"
>
<div
v-if="showMenu && !readOnly"
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 v-if="renderAsText" class="truncate">
<span v-if="editor"> {{ editor?.getText() ?? '' }}</span>
</div>
<CellRichTextSelectedBubbleMenuPopup v-if="editor" :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,
}"
/>
<template v-else>
<div
v-if="showMenu && !readOnly && !isFormField"
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 :is-form-field="isFormField" />
</div>
</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>
</template>
@ -223,7 +284,28 @@ watch(editorDom, () => {
.nc-rich-text-embed {
.ProseMirror {
@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 {
.nc-textarea-rich-editor {
@ -256,6 +338,13 @@ watch(editorDom, () => {
}
.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 {
@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 {
editor: Editor
embedMode?: boolean
isFormField?: boolean
}
const props = defineProps<Props>()
const editor = computed(() => props.editor)
const embedMode = computed(() => props.embedMode)
const { editor, embedMode } = toRefs(props)
const cmdOrCtrlKey = computed(() => {
return isMac() ? '⌘' : 'CTRL'
@ -79,13 +78,15 @@ const onToggleLink = () => {
<template>
<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="{
'inline-flex !bg-transparent': isFormField,
'flex bg-gray-100 px-1': !isFormField,
'embed-mode': embedMode,
'full-mode': !embedMode,
}"
>
<NcTooltip :disabled="editor.isActive('codeBlock')">
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
@ -99,13 +100,14 @@ const onToggleLink = () => {
type="text"
:class="{ 'is-active': editor.isActive('bold') }"
:disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
@click="editor!.chain().focus().toggleBold().run()"
>
<MdiFormatBold />
</NcButton>
</NcTooltip>
<NcTooltip :disabled="editor.isActive('codeBlock')">
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
@ -119,12 +121,13 @@ const onToggleLink = () => {
type="text"
:disabled="editor.isActive('codeBlock')"
:class="{ 'is-active': editor.isActive('italic') }"
:tabindex="isFormField ? -1 : 0"
@click=";(editor!.chain().focus() as any).toggleItalic().run()"
>
<MdiFormatItalic />
</NcButton>
</NcTooltip>
<NcTooltip :disabled="editor.isActive('codeBlock')">
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
@ -139,12 +142,13 @@ const onToggleLink = () => {
type="text"
:class="{ 'is-active': editor.isActive('underline') }"
:disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
@click="editor!.chain().focus().toggleUnderline().run()"
>
<MdiFormatUnderline />
</NcButton>
</NcTooltip>
<NcTooltip :disabled="editor.isActive('codeBlock')">
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
@ -158,155 +162,160 @@ const onToggleLink = () => {
type="text"
:class="{ 'is-active': editor.isActive('strike') }"
:disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
@click="editor!.chain().focus().toggleStrike().run()"
>
<MdiFormatStrikeThrough />
</NcButton>
</NcTooltip>
<NcTooltip v-if="embedMode">
<template #title> {{ $t('general.code') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('codeBlock') }"
@click="editor!.chain().focus().toggleCodeBlock().run()"
>
<MsCode />
</NcButton>
</NcTooltip>
<NcTooltip v-else :disabled="editor.isActive('codeBlock')">
<template #title> {{ $t('general.quote') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('code') }"
:disabled="editor.isActive('codeBlock')"
@click="editor!.chain().focus().toggleCode().run()"
>
<MsFormatQuote />
</NcButton>
</NcTooltip>
<div class="divider"></div>
<template v-if="!isFormField">
<NcTooltip v-if="embedMode">
<template #title> {{ $t('general.code') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('codeBlock') }"
@click="editor!.chain().focus().toggleCodeBlock().run()"
>
<MsCode />
</NcButton>
</NcTooltip>
<NcTooltip v-else :disabled="editor.isActive('codeBlock')">
<template #title> {{ $t('general.quote') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('code') }"
:disabled="editor.isActive('codeBlock')"
@click="editor!.chain().focus().toggleCode().run()"
>
<MsFormatQuote />
</NcButton>
</NcTooltip>
<div class="divider"></div>
<template v-if="embedMode">
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading1') }}
<template v-if="embedMode">
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading1') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 1</div>
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 1</div>
</div>
</template>
</template>
<NcButton
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
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('blockquote') }"
@click="editor!.chain().focus().toggleBlockquote().run()"
>
<MsFormatH1 />
<TablerBlockQuote class="-mt-0.25" />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading2') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 2</div>
</div>
</template>
<template #title> {{ $t('labels.bulletList') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
@click="editor!.chain().focus().toggleBulletList().run()"
>
<MsFormatH2 />
<MdiFormatBulletList />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading3') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 3</div>
</div>
</template>
<template #title> {{ $t('labels.numberedList') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
@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>
</NcTooltip>
<div class="divider"></div>
</template>
<NcTooltip v-if="embedMode">
<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')">
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<template #title> {{ $t('general.link') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('link') }"
:disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
@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 />
<div class="!text-xs !ml-1">{{ $t('general.link') }}</div>
</div>
@ -343,6 +352,9 @@ const onToggleLink = () => {
.bubble-menu.embed-mode {
@apply border-transparent !shadow-none;
}
.bubble-menu.form-field-mode {
@apply bg-transparent px-0;
}
.embed-mode.bubble-menu {
@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) {
base.isLoading = false
const updatedProject = bases.value.get(base.id!)!
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') }}
</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>

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

@ -117,6 +117,10 @@ watch(picked, (n, _o) => {
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 {
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>
<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 }"
>
<LazyCellAttachmentImage
@ -19,16 +20,15 @@ const { getPossibleAttachmentSrc } = useAttachment()
:srcs="getPossibleAttachmentSrc(parseProp(bannerImageUrl))"
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">
<div class="flex">
<div v-else class="h-full flex items-stretch justify-between bg-white">
<div class="flex -mt-1">
<img src="~assets/img/form-banner-left.png" alt="form-banner-left'" />
</div>
<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 class="flex justify-end">
<div class="flex justify-end -mb-1">
<img src="~assets/img/form-banner-right.png" alt="form-banner-left'" />
</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 { extractSdkResponseErrorMsg, useApi } from '#imports'
import type { ImageCropperConfig } from '~/lib'
interface Props {
imageConfig: {
src: string
type: string
name: string
}
cropperConfig: {
aspectRatio?: number
}
cropperConfig: ImageCropperConfig
uploadConfig?: {
path?: string
}
showCropper: boolean
}
const { imageConfig, cropperConfig, uploadConfig, ...props } = defineProps<Props>()
const emit = defineEmits(['update:showCropper', 'submit'])
const showCropper = useVModel(props, 'showCropper', emit)
@ -103,7 +102,10 @@ watch(showCropper, () => {
class="nc-cropper relative"
:src="imageConfig.src"
: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">
<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 { openedProject } = storeToRefs(useBases())
const { base } = useBase()
const isNewBaseModalOpen = ref(false)
const isDataSourceLimitReached = computed(() => Number(openedProject.value?.sources?.length) > 1)
@ -78,7 +80,12 @@ const onCreateBaseClick = () => {
<template>
<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
v-if="isUIAllowed('tableCreate')"
role="button"
@ -121,7 +128,21 @@ const onCreateBaseClick = () => {
</div>
</component>
</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="w-2/5">{{ $t('objects.table') }}</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,
'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,
'!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<{
modelValue: FormFieldsLimitOptionsType[]
column: ColumnType
isRequired?: boolean
}>()
const emit = defineEmits(['update:modelValue'])
@ -121,6 +122,18 @@ async function onMove(_event: { moved: { newIndex: number; oldIndex: number; ele
vModel.value = [...vModel.value]
}
const showOrHideAll = (showAll: boolean) => {
if (props.isRequired && !showAll) {
return
}
vModel.value = vModel.value.map((o) => {
return {
...o,
show: showAll,
}
})
}
</script>
<template>
@ -147,13 +160,42 @@ async function onMove(_event: { moved: { newIndex: number; oldIndex: number; ele
</template>
</a-input>
</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
v-if="vModel.length"
:model-value="vModel"
item-key="id"
handle=".nc-child-draggable-icon"
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)"
@start="drag = true"
@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" />
<div
@click="
() => {
element.show = !element.show
vModel = [...vModel]
}
"
>
<component
:is="element.show ? iconMap.eye : iconMap.eyeSlash"
class="flex-none cursor-pointer !h-4 !w-4 text-gray-600"
/>
</div>
<NcTooltip :disabled="!isRequired || !(element.show && isRequired && vModel.filter((o) => o.show).length === 1)">
<template #title> {{ $t('msg.info.preventHideAllOptions') }} </template>
<div
class="!border-none !px-2"
@click="
() => {
if (element.show && isRequired && vModel.filter((o) => o.show).length === 1) return
element.show = !element.show
vModel = [...vModel]
}
"
>
<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'">
<span

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

@ -694,9 +694,9 @@
"appearanceSettings": "Appearance Settings",
"backgroundColor": "Background Color",
"hideNocodbBranding": "Hide NocoDB Branding",
"showOnConditions": "Show on condtions",
"showOnConditions": "Show on conditions",
"showFieldOnConditionsMet": "Shows field only when conditions are met",
"limitOptions": "Limit ptions",
"limitOptions": "Limit options",
"limitOptionsSubtext": "Limit options visible to users by selecting available options",
"clearSelection": "Clear selection"
},
@ -1181,6 +1181,7 @@
"formInput": "Enter form input label",
"formHelpText": "Add some help text",
"onlyCreator": "Only visible to Creator",
"formTitle": "Add form Title",
"formDesc": "Add form description",
"beforeEnablePwd": "Restrict access with a password",
"afterEnablePwd": "Access is password restricted",
@ -1305,8 +1306,10 @@
"groupPasteIsNotSupportedOnLinksColumn": "Group paste 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}",
"thisFeatureIsOnlyAvailableInEnterpriseEdition": "This feature is only available in enterprise edition",
"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": {
"fetchingCalendarData": "Error fetching calendar data",

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

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

1
packages/nc-gui/package.json

@ -41,6 +41,7 @@
"@iconify/vue": "^4.1.1",
"@pinia/nuxt": "^0.5.1",
"@tiptap/extension-link": "2.2.4",
"@tiptap/extension-placeholder": "^2.2.4",
"@tiptap/extension-task-list": "2.2.4",
"@tiptap/extension-underline": "^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()
// For now dark theme is disabled
// const onClick = () => {
// isDark.value = !isDark.value
// }
onMounted(() => {
isDark.value = false
})
@ -39,16 +34,6 @@ router.afterEach((to) => shouldRedirect(to.name as string))
}"
>
<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>
</template>
@ -100,7 +85,7 @@ p {
@apply w-full;
&: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 {
@apply !bg-transparent;
@ -153,32 +138,26 @@ p {
& > div {
@apply w-full;
}
:deep(textarea) {
@apply !p-2;
&:focus {
box-shadow: none !important;
}
textarea {
@apply px-3;
}
}
&:not(.nc-cell-longtext) {
@apply p-2;
}
textarea {
@apply px-4 py-2 rounded;
&:focus {
box-shadow: none !important;
}
}
&.nc-cell-json {
@apply h-auto;
& > div {
@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>
<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
v-if="sharedFormView"
v-if="sharedFormView && !parseProp(sharedFormView?.meta).hide_banner"
:banner-image-url="sharedFormView.banner_image_url"
class="flex-none dark:border-none"
/>
<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">
<div class="mb-4">
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-4">
{{ sharedFormView.heading }}
</h1>
<h2 v-if="sharedFormView.subheading" class="font-medium text-base text-gray-500 dark:text-slate-300 mb-4">
{{ sharedFormView.subheading }}
</h2>
<div v-if="sharedFormView.subheading">
<LazyCellRichText
: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>
<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">
<div class="flex justify-center">
<div v-if="sharedFormView" class="w-full lg:w-[95%]">
<a-alert
type="success"
class="!my-4 text-center !rounded-lg"
outlined
:message="sharedFormView.success_msg || 'Successfully submitted form data'"
/>
<div v-if="sharedFormView" class="w-full">
<a-alert class="!mt-2 !mb-4 !py-4 text-left !rounded-lg" type="success" outlined>
<template #message>
<LazyCellRichText
v-if="sharedFormView?.success_msg?.trim()"
: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 }) }}
</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">
{{ $t('activity.submitAnotherForm') }}</NcButton
>
{{ $t('activity.submitAnotherForm') }}
</NcButton>
</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>
</div>
<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>

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

@ -239,20 +239,22 @@ onMounted(() => {
<div
v-if="sharedFormView"
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">
<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 }}
</h1>
<h2
v-if="sharedFormView.subheading && sharedFormView.subheading !== ''"
class="font-medium text-base text-gray-500 dark:text-gray-300 self-center mb-4"
data-testid="nc-survey-form__sub-heading"
>
{{ sharedFormView?.subheading }}
</h2>
<div v-if="sharedFormView.subheading?.trim()" class="w-full">
<LazyCellRichText
: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
data-testid="nc-survey-form__sub-heading"
/>
</div>
</div>
</div>
@ -275,7 +277,7 @@ onMounted(() => {
class="nc-form-column-description text-gray-500 text-sm"
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>
<LazySmartsheetDivDataCell v-if="field.title" class="relative nc-form-data-cell">
@ -374,20 +376,35 @@ onMounted(() => {
<Transition name="slide-left">
<div v-if="submitted" class="flex flex-col justify-center items-center text-center">
<a-alert
class="!my-4 !py-4 !rounded-lg text-left w-full"
type="success"
class="!my-4 !py-4 text-center !rounded-lg"
data-testid="nc-survey-form__success-msg"
outlined
:message="sharedFormView?.success_msg || $t('msg.info.thankYou')"
:description="sharedFormView?.success_msg ? undefined : $t('msg.info.submittedFormData')"
/>
<div v-if="sharedFormView" class="mt-3">
<p v-if="sharedFormView?.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 text-center my-4">
>
<template #message>
<LazyCellRichText
v-if="sharedFormView?.success_msg?.trim()"
:value="sharedFormView?.success_msg"
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') }}
</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">
{{ $t('activity.submitAnotherForm') }}
</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 NcUnderline from '~icons/nc-icons/underline'
import NcCrop from '~icons/nc-icons/crop'
import NcLink from '~icons/nc-icons/link'
// keep it for reference
// todo: remove it after all icons are migrated
@ -326,6 +327,7 @@ export const iconMap = {
translate: h('span', { class: 'material-symbols' }, 'translate'),
preview: h('span', { class: 'material-symbols' }, 'visibility'),
link: h('span', { class: 'material-symbols' }, 'link'),
link2: NcLink,
returnKey: h('span', { class: 'material-symbols' }, 'keyboard_return'),
keyboard: h('span', { class: 'material-symbols' }, 'keyboard'),
accountPlus: h('span', { class: 'material-symbols' }, 'person_add'),

13
pnpm-lock.yaml

@ -42,6 +42,9 @@ importers:
'@tiptap/extension-link':
specifier: 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':
specifier: 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)
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):
resolution: {integrity: sha512-/a2EwQgA+PpG17V2tVRspcrIY0SN3blwcgM7lxdW4aucGkqSKnf7+91dkhQEwCZ//o8kv9mBCyRoCUcGy6S5Xg==}
peerDependencies:

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

@ -10,8 +10,7 @@ export class FormPage extends BasePage {
readonly topbar: TopbarPage;
// todo: All the locator should be private
readonly addAllButton: Locator;
readonly removeAllButton: Locator;
readonly addOrRemoveAllButton: Locator;
readonly submitButton: Locator;
readonly showAnotherFormRadioButton: Locator;
@ -30,8 +29,10 @@ export class FormPage extends BasePage {
this.toolbar = new ToolbarPage(this);
this.topbar = new TopbarPage(this);
this.addAllButton = dashboard.get().locator('[data-testid="nc-form-show-all-fields"]').locator('.nc-switch');
this.removeAllButton = dashboard.get().locator('[data-testid="nc-form-show-all-fields"]').locator('.nc-switch');
this.addOrRemoveAllButton = dashboard
.get()
.locator('[data-testid="nc-form-show-all-fields"]')
.locator('.nc-switch');
this.submitButton = dashboard.get().locator('[data-testid="nc-form-submit"]');
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"]');
this.emailMeRadioButton = dashboard.get().locator('[data-testid="nc-form-checkbox-send-email"]');
this.formHeading = dashboard.get().locator('[data-testid="nc-form-heading"]');
this.formSubHeading = dashboard.get().locator('[data-testid="nc-form-sub-heading"]');
this.afterSubmitMsg = dashboard.get().locator('[data-testid="nc-form-after-submit-msg"]');
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"] .tiptap.ProseMirror');
this.formFields = dashboard.get().locator('.nc-form-fields-list');
}
@ -67,7 +68,7 @@ export class FormPage extends BasePage {
}
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 }) {
@ -83,7 +84,7 @@ export class FormPage extends BasePage {
}
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[] }) {
@ -136,11 +137,18 @@ export class FormPage extends BasePage {
}
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() {
await this.addAllButton.click();
if (!(await this.addOrRemoveAllButton.isChecked())) {
await this.addOrRemoveAllButton.click();
}
}
async configureHeader(param: { subtitle: string; title: string }) {
@ -166,7 +174,7 @@ export class FormPage extends BasePage {
async verifyHeader(param: { subtitle: string; title: string }) {
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 }[]) {
@ -233,7 +241,7 @@ export class FormPage extends BasePage {
const fieldHelpText = this.get()
.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);
}

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 { LoginPage } from '../../../pages/LoginPage';
import { getDefaultPwd } from '../../../tests/utils/general';
import { WorkspacePage } from '../../../pages/WorkspacePage';
import { enableQuickRun, isEE } from '../../../setup/db';
// todo: Move most of the ui actions to page object and await on the api response

Loading…
Cancel
Save