Browse Source

Nc feat/form view builder field settings in right pannel and fixed column delete modal virtual cell icon issue (#7927)

* feat(nc-gui): form field in right pannel setup

* fix(nc-gui): inline form field reorder issue

* fix(nc-gui): make edit form field right panel scrollable

* fix(nc-gui): form field limit option hide btn focus box shadow style issue

* fix(nc-gui): add support to edit form column in form view builder

* feat(nc-gui): added form field header menu dropdown

* fix(nc-gui): tab issue in form builder

* feat(nc-gui): add support to edit column from form builder itself

* fix(nc-gui): wrong virtual cell icon in column delete modal

* feat(nc-gui): column edit, hide, delete option in form builder field settings

* fix(nc-gui): move all form field settings radio btns to the right side

* chore(nc-gui): lint

* chore(nc-gui): lint errors

* chore(nc-gui): lint

* fix(nc-gui): update 'change icon color' text case

* fix(nc-gui): small changes

* fix(nc-gui): form builder side panel field div key issue

* fix(nc-gui): form view outsideclick fild toggle issue

* chore(nc-gui): lint

* fix(nc-gui): hide select dropdown in value is selected and show if value is not selected

* fix(nc-gui): suggested review changes

* fix(nc-gui): make form field rich text options sticky at bottom

* chore(nc-gui): lint

* fix(nc-gui): small changes

* fix(nc-gui): lazy import richtext component

* fix(nc-gui): set the max height for form rich text fields

* fix(nc-gui): move form settings switch inputs to the right side

* fix(nc-gui): move form select type field layout option to appearance settings section

* fix(nc-gui): select form active field text on focus

* fix(nc-gui): form rich text element menu option tabindex issue

* fix(nc-gui): form search field input autofill issue

* fix(nc-gui): update position of rich text menu option of form description

* feat(nc-gui): adjustable form view sidebar width

* chore(nc-gui): lint

* fix(nc-gui): typo mistake

* fix(nc-gui): PR review changes
nc-enable-calendar-view
Ramesh Mane 8 months ago committed by GitHub
parent
commit
2a78930923
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      packages/nc-gui/components/cell/MultiSelect.vue
  2. 56
      packages/nc-gui/components/cell/RichText.vue
  3. 311
      packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue
  4. 5
      packages/nc-gui/components/cell/SingleSelect.vue
  5. 8
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  6. 2
      packages/nc-gui/components/general/ShareProject.vue
  7. 1861
      packages/nc-gui/components/smartsheet/Form.vue
  8. 248
      packages/nc-gui/components/smartsheet/form/FieldMenu.vue
  9. 94
      packages/nc-gui/components/smartsheet/form/Layout.vue
  10. 6
      packages/nc-gui/components/smartsheet/form/LimitOptions.vue
  11. 8
      packages/nc-gui/components/smartsheet/header/DeleteColumnModal.vue
  12. 6
      packages/nc-gui/lang/en.json
  13. 17
      packages/nc-gui/lib/enums.ts
  14. 13
      packages/nc-gui/store/sidebar.ts
  15. 2
      tests/playwright/pages/Dashboard/Form/index.ts

5
packages/nc-gui/components/cell/MultiSelect.vue

@ -10,6 +10,7 @@ import {
EditColumnInj, EditColumnInj,
EditModeInj, EditModeInj,
IsKanbanInj, IsKanbanInj,
IsSurveyFormInj,
ReadonlyInj, ReadonlyInj,
RowHeightInj, RowHeightInj,
computed, computed,
@ -64,6 +65,8 @@ const isEditColumn = inject(EditColumnInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined)) const rowHeight = inject(RowHeightInj, ref(undefined))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const selectedIds = ref<string[]>([]) const selectedIds = ref<string[]>([])
const aselect = ref<typeof AntSelect>() const aselect = ref<typeof AntSelect>()
@ -386,6 +389,8 @@ const onFocus = () => {
isFocusing.value = false isFocusing.value = false
}, 250) }, 250)
if (isSurveyForm.value && vModel.value?.length) return
isOpen.value = true isOpen.value = true
} }
</script> </script>

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

@ -9,22 +9,31 @@ import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder' 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 type { RichTextBubbleMenuOptions } from '#imports'
import { IsExpandedFormOpenInj, IsFormInj, IsGridInj, ReadonlyInj, RowHeightInj } from '#imports' import { IsExpandedFormOpenInj, IsFormInj, IsGridInj, ReadonlyInj, RowHeightInj } from '#imports'
const props = defineProps<{ const props = withDefaults(
value?: string | null defineProps<{
readOnly?: boolean value?: string | null
syncValueChange?: boolean readOnly?: boolean
showMenu?: boolean syncValueChange?: boolean
fullMode?: boolean showMenu?: boolean
isFormField?: boolean fullMode?: boolean
autofocus?: boolean isFormField?: boolean
placeholder?: string autofocus?: boolean
renderAsText?: boolean placeholder?: string
}>() renderAsText?: boolean
hiddenBubbleMenuOptions?: RichTextBubbleMenuOptions[]
}>(),
{
hiddenBubbleMenuOptions: () => [],
},
)
const emits = defineEmits(['update:value']) const emits = defineEmits(['update:value'])
const { hiddenBubbleMenuOptions } = toRefs(props)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const rowHeight = inject(RowHeightInj, ref(1 as const)) const rowHeight = inject(RowHeightInj, ref(1 as const))
@ -208,7 +217,8 @@ useEventListener(
editorDom, editorDom,
'focusout', 'focusout',
(e: FocusEvent) => { (e: FocusEvent) => {
if (!(e?.relatedTarget as HTMLElement)?.closest('.bubble-menu, .nc-textarea-rich-editor')) { const targetEl = e?.relatedTarget as HTMLElement
if (targetEl?.classList?.contains('tiptap') || !targetEl?.closest('.bubble-menu, .nc-textarea-rich-editor')) {
isFocused.value = false isFocused.value = false
} }
}, },
@ -223,7 +233,7 @@ useEventListener(
'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, 'nc-form-rich-text-field !p-0 relative': isFormField,
'nc-rich-text-grid': isGrid, 'nc-rich-text-grid': isGrid,
}" }"
:tabindex="readOnlyCell || isFormField ? -1 : 0" :tabindex="readOnlyCell || isFormField ? -1 : 0"
@ -251,21 +261,26 @@ useEventListener(
class="flex flex-col nc-textarea-rich-editor w-full" class="flex flex-col nc-textarea-rich-editor w-full"
:class="{ :class="{
'mt-2.5 flex-grow': fullMode, 'mt-2.5 flex-grow': fullMode,
'nc-scrollbar-md': !fullMode || (!fullMode && isExpandedFormOpen), 'scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent': !fullMode || (!fullMode && isExpandedFormOpen),
'flex-grow': isExpandedFormOpen, 'flex-grow': isExpandedFormOpen,
[`!overflow-hidden children:line-clamp-${rowHeight}`]: [`!overflow-hidden children:line-clamp-${rowHeight}`]:
!fullMode && readOnly && rowHeight && !isExpandedFormOpen && !isForm, !fullMode && readOnly && rowHeight && !isExpandedFormOpen && !isForm,
}" }"
/> />
<div v-if="isFormField && !readOnly"> <div v-if="isFormField && !readOnly" class="nc-form-field-bubble-menu-wrapper overflow-hidden">
<div <div
class="overflow-hidden"
:class="isFocused ? 'max-h-[50px]' : 'max-h-0'" :class="isFocused ? 'max-h-[50px]' : 'max-h-0'"
:style="{ :style="{
transition: 'max-height 0.2s ease-in-out', transition: 'max-height 0.2s ease-in-out',
}" }"
> >
<CellRichTextSelectedBubbleMenu v-if="editor" :editor="editor" embed-mode is-form-field /> <CellRichTextSelectedBubbleMenu
v-if="editor"
:editor="editor"
embed-mode
is-form-field
:hidden-options="hiddenBubbleMenuOptions"
/>
</div> </div>
</div> </div>
</template> </template>
@ -343,7 +358,7 @@ useEventListener(
.nc-textarea-rich-editor { .nc-textarea-rich-editor {
.tiptap p.is-editor-empty:first-child::before { .tiptap p.is-editor-empty:first-child::before {
color: #6a7184; color: #9aa2af;
content: attr(data-placeholder); content: attr(data-placeholder);
float: left; float: left;
height: 0; height: 0;
@ -462,4 +477,9 @@ useEventListener(
height: fit-content; height: fit-content;
} }
} }
.nc-form-field-bubble-menu-wrapper {
@apply absolute -bottom-9 left-1/2 z-50 rounded-lg;
transform: translateX(-50%);
box-shadow: 0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.1);
}
</style> </style>

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

@ -10,16 +10,22 @@ import MsFormatH3 from '~icons/material-symbols/format-h3'
import TablerBlockQuote from '~icons/tabler/blockquote' import TablerBlockQuote from '~icons/tabler/blockquote'
import MsCode from '~icons/material-symbols/code' import MsCode from '~icons/material-symbols/code'
import MsFormatQuote from '~icons/material-symbols/format-quote' import MsFormatQuote from '~icons/material-symbols/format-quote'
import { RichTextBubbleMenuOptions } from '#imports'
interface Props { interface Props {
editor: Editor editor: Editor
embedMode?: boolean embedMode?: boolean
isFormField?: boolean isFormField?: boolean
hiddenOptions?: RichTextBubbleMenuOptions[]
} }
const props = defineProps<Props>() const props = withDefaults(defineProps<Props>(), {
embedMode: false,
isFormField: false,
hiddenOptions: () => [],
})
const { editor, embedMode } = toRefs(props) const { editor, embedMode, isFormField, hiddenOptions } = toRefs(props)
const cmdOrCtrlKey = computed(() => { const cmdOrCtrlKey = computed(() => {
return isMac() ? '⌘' : 'CTRL' return isMac() ? '⌘' : 'CTRL'
@ -33,6 +39,14 @@ const altKey = computed(() => {
return isMac() ? '⌥' : 'Alt' return isMac() ? '⌥' : 'Alt'
}) })
const tooltipPlacement = computed(() => {
if (isFormField.value) return 'bottom'
})
const tabIndex = computed(() => {
return isFormField.value ? -1 : 0
})
const onToggleLink = () => { const onToggleLink = () => {
const activeNode = editor.value?.state?.selection?.$from?.nodeBefore || editor.value?.state?.selection?.$from?.nodeAfter const activeNode = editor.value?.state?.selection?.$from?.nodeBefore || editor.value?.state?.selection?.$from?.nodeAfter
@ -74,19 +88,29 @@ const onToggleLink = () => {
}, 100) }, 100)
} }
} }
const isOptionVisible = (option: RichTextBubbleMenuOptions) => {
if (isFormField.value) return !hiddenOptions.value.includes(option)
return true
}
const showDivider = (options: RichTextBubbleMenuOptions[]) => {
return !isFormField.value || options.some((o) => !hiddenOptions.value.includes(o))
}
</script> </script>
<template> <template>
<div <div
class="bubble-menu flex-row gap-x-1 py-1 rounded-lg" class="bubble-menu flex-row gap-x-1 rounded-lg"
:class="{ :class="{
'inline-flex !bg-transparent': isFormField, 'nc-form-field-bubble-menu inline-flex py-0': isFormField,
'flex bg-gray-100 px-1': !isFormField, 'flex bg-gray-100 px-1 py-1': !isFormField,
'embed-mode': embedMode, 'embed-mode': embedMode,
'full-mode': !embedMode, 'full-mode': !embedMode,
}" }"
> >
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')"> <NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<template #title> <template #title>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div> <div>
@ -100,14 +124,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" :tabindex="tabIndex"
@click="editor!.chain().focus().toggleBold().run()" @click="editor!.chain().focus().toggleBold().run()"
> >
<MdiFormatBold /> <MdiFormatBold />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')"> <NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<template #title> <template #title>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div> <div>
@ -121,13 +145,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" :tabindex="tabIndex"
@click=";(editor!.chain().focus() as any).toggleItalic().run()" @click=";(editor!.chain().focus() as any).toggleItalic().run()"
> >
<MdiFormatItalic /> <MdiFormatItalic />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')"> <NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<template #title> <template #title>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div> <div>
@ -142,13 +166,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" :tabindex="tabIndex"
@click="editor!.chain().focus().toggleUnderline().run()" @click="editor!.chain().focus().toggleUnderline().run()"
> >
<MdiFormatUnderline /> <MdiFormatUnderline />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')"> <NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<template #title> <template #title>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div> <div>
@ -162,156 +186,179 @@ 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" :tabindex="tabIndex"
@click="editor!.chain().focus().toggleStrike().run()" @click="editor!.chain().focus().toggleStrike().run()"
> >
<MdiFormatStrikeThrough /> <MdiFormatStrikeThrough />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<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 v-if="embedMode && isOptionVisible(RichTextBubbleMenuOptions.code)" :placement="tooltipPlacement">
<NcTooltip> <template #title> {{ $t('general.code') }}</template>
<template #title> <NcButton
<div class="flex flex-col items-center"> size="small"
<div> type="text"
{{ $t('labels.heading1') }} :tabindex="tabIndex"
</div> :class="{ 'is-active': editor.isActive('codeBlock') }"
<div>{{ cmdOrCtrlKey }} {{ altKey }} 1</div> @click="editor!.chain().focus().toggleCodeBlock().run()"
</div> >
</template> <MsCode />
<NcButton </NcButton>
size="small" </NcTooltip>
type="text" <NcTooltip
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }" v-if="isFormField ? isOptionVisible(RichTextBubbleMenuOptions.quote) : !embedMode"
@click="editor!.chain().focus().toggleHeading({ level: 1 }).run()" :placement="tooltipPlacement"
> :disabled="editor.isActive('codeBlock')"
<MsFormatH1 /> >
</NcButton> <template #title> {{ $t('general.quote') }}</template>
</NcTooltip> <NcButton
<NcTooltip> size="small"
<template #title> type="text"
<div class="flex flex-col items-center"> :tabindex="tabIndex"
<div> :class="{ 'is-active': editor.isActive('code') }"
{{ $t('labels.heading2') }} :disabled="editor.isActive('codeBlock')"
</div> @click="editor!.chain().focus().toggleCode().run()"
<div>{{ cmdOrCtrlKey }} {{ altKey }} 2</div> >
</div> <MsFormatQuote />
</template> </NcButton>
<NcButton </NcTooltip>
size="small" <div class="divider"></div>
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('blockquote') }"
@click="editor!.chain().focus().toggleBlockquote().run()"
>
<TablerBlockQuote class="-mt-0.25" />
</NcButton>
</NcTooltip>
<template v-if="embedMode && !isFormField">
<NcTooltip> <NcTooltip>
<template #title> {{ $t('labels.bulletList') }}</template> <template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading1') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 1</div>
</div>
</template>
<NcButton <NcButton
size="small" size="small"
type="text" type="text"
:class="{ 'is-active': editor.isActive('bulletList') }" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
@click="editor!.chain().focus().toggleBulletList().run()" @click="editor!.chain().focus().toggleHeading({ level: 1 }).run()"
> >
<MdiFormatBulletList /> <MsFormatH1 />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip> <NcTooltip>
<template #title> {{ $t('labels.numberedList') }}</template> <template #title>
<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('orderedList') }" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
@click="editor!.chain().focus().toggleOrderedList().run()" @click="editor!.chain().focus().toggleHeading({ level: 2 }).run()"
> >
<MdiFormatListNumber /> <MsFormatH2 />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip> <NcTooltip>
<template #title> {{ $t('labels.taskList') }}</template> <template #title>
<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('taskList') }" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
@click="editor!.chain().focus().toggleTaskList().run()" @click="editor!.chain().focus().toggleHeading({ level: 3 }).run()"
> >
<MdiFormatListCheckbox /> <MsFormatH3 />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<div class="divider"></div> <div class="divider"></div>
</template> </template>
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')"> <NcTooltip v-if="embedMode && isOptionVisible(RichTextBubbleMenuOptions.blockQuote)" :placement="tooltipPlacement">
<template #title> {{ $t('labels.blockQuote') }}</template>
<NcButton
size="small"
type="text"
:tabindex="tabIndex"
:class="{ 'is-active': editor.isActive('blockquote') }"
@click="editor!.chain().focus().toggleBlockquote().run()"
>
<TablerBlockQuote class="-mt-0.25" />
</NcButton>
</NcTooltip>
<NcTooltip v-if="isOptionVisible(RichTextBubbleMenuOptions.bulletList)" :placement="tooltipPlacement">
<template #title> {{ $t('labels.bulletList') }}</template>
<NcButton
size="small"
type="text"
:tabindex="tabIndex"
:class="{ 'is-active': editor.isActive('bulletList') }"
@click="editor!.chain().focus().toggleBulletList().run()"
>
<MdiFormatBulletList />
</NcButton>
</NcTooltip>
<NcTooltip v-if="isOptionVisible(RichTextBubbleMenuOptions.numberedList)" :placement="tooltipPlacement">
<template #title> {{ $t('labels.numberedList') }}</template>
<NcButton
size="small"
type="text"
:tabindex="tabIndex"
:class="{ 'is-active': editor.isActive('orderedList') }"
@click="editor!.chain().focus().toggleOrderedList().run()"
>
<MdiFormatListNumber />
</NcButton>
</NcTooltip>
<NcTooltip v-if="isOptionVisible(RichTextBubbleMenuOptions.taskList)" :placement="tooltipPlacement">
<template #title> {{ $t('labels.taskList') }}</template>
<NcButton
size="small"
type="text"
:tabindex="tabIndex"
:class="{ 'is-active': editor.isActive('taskList') }"
@click="editor!.chain().focus().toggleTaskList().run()"
>
<MdiFormatListCheckbox />
</NcButton>
</NcTooltip>
<div
v-if="
showDivider([
RichTextBubbleMenuOptions.blockQuote,
RichTextBubbleMenuOptions.bulletList,
RichTextBubbleMenuOptions.numberedList,
RichTextBubbleMenuOptions.taskList,
])
"
class="divider"
></div>
<NcTooltip
v-if="isOptionVisible(RichTextBubbleMenuOptions.link)"
:placement="tooltipPlacement"
: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" :tabindex="tabIndex"
@click="onToggleLink" @click="onToggleLink"
> >
<GeneralIcon v-if="isFormField" icon="link2"></GeneralIcon> <GeneralIcon v-if="isFormField" icon="link2"></GeneralIcon>
@ -349,14 +396,14 @@ const onToggleLink = () => {
box-shadow: 0px 0px 1.2rem 0 rgb(230, 230, 230) !important; box-shadow: 0px 0px 1.2rem 0 rgb(230, 230, 230) !important;
} }
.bubble-menu.embed-mode { .bubble-menu.embed-mode:not(.nc-form-field-bubble-menu) {
@apply border-transparent !shadow-none; @apply border-transparent !shadow-none;
} }
.bubble-menu.form-field-mode { .bubble-menu.form-field-mode {
@apply bg-transparent px-0; @apply bg-transparent px-0;
} }
.embed-mode.bubble-menu { .embed-mode.bubble-menu:not(.nc-form-field-bubble-menu) {
@apply !py-0 !my-0 !border-0; @apply !py-0 !my-0 !border-0;
.divider { .divider {
@ -373,12 +420,20 @@ const onToggleLink = () => {
@apply bg-white; @apply bg-white;
border-width: 1px; border-width: 1px;
&.nc-form-field-bubble-menu {
.divider {
@apply border-r-1 border-gray-200 my-0;
}
}
.nc-button.is-active { .nc-button.is-active {
@apply !hover:outline-gray-200 bg-gray-100 text-brand-500; @apply !hover:outline-gray-200 bg-gray-100 text-brand-500;
outline: 1px; outline: 1px;
} }
.divider { &:not(.nc-form-field-bubble-menu) {
@apply border-r-1 border-gray-200 !h-6 !mx-0.5 my-1; .divider {
@apply border-r-1 border-gray-200 !h-6 !mx-0.5 my-1;
}
} }
.ant-select-selector { .ant-select-selector {
@apply !rounded-md; @apply !rounded-md;

5
packages/nc-gui/components/cell/SingleSelect.vue

@ -11,6 +11,7 @@ import {
EditModeInj, EditModeInj,
IsFormInj, IsFormInj,
IsKanbanInj, IsKanbanInj,
IsSurveyFormInj,
ReadonlyInj, ReadonlyInj,
computed, computed,
enumColor, enumColor,
@ -62,6 +63,8 @@ const isPublic = inject(IsPublicInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false)) const isEditColumn = inject(EditColumnInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const searchVal = ref() const searchVal = ref()
@ -312,6 +315,8 @@ const onFocus = () => {
isFocusing.value = false isFocusing.value = false
}, 250) }, 250)
if (isSurveyForm.value && vModel.value) return
isOpen.value = true isOpen.value = true
} }
</script> </script>

8
packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue

@ -370,9 +370,9 @@ watchEffect(() => {})
<div> <div>
{{ $t('activity.surveyMode') }} {{ $t('activity.surveyMode') }}
</div> </div>
<NcTooltip> <NcTooltip class="flex items-center">
<template #title> {{ $t('tooltip.surveyFormInfo') }}</template> <template #title> {{ $t('tooltip.surveyFormInfo') }}</template>
<GeneralIcon icon="info" class="text-gray-600 cursor-pointer"></GeneralIcon> <GeneralIcon icon="info" class="flex-none text-gray-600 cursor-pointer"></GeneralIcon>
</NcTooltip> </NcTooltip>
</div> </div>
<a-switch <a-switch
@ -405,13 +405,13 @@ watchEffect(() => {})
{{ $t('activity.preFilledFields.title') }} {{ $t('activity.preFilledFields.title') }}
</div> </div>
<NcTooltip> <NcTooltip class="flex items-center">
<template #title> <template #title>
<div class="text-center"> <div class="text-center">
{{ $t('tooltip.preFillFormInfo') }} {{ $t('tooltip.preFillFormInfo') }}
</div> </div>
</template> </template>
<GeneralIcon icon="info" class="text-gray-600 cursor-pointer"></GeneralIcon> <GeneralIcon icon="info" class="flex-none text-gray-600 cursor-pointer"></GeneralIcon>
</NcTooltip> </NcTooltip>
</div> </div>
<a-switch <a-switch

2
packages/nc-gui/components/general/ShareProject.vue

@ -47,7 +47,7 @@ const copySharedBase = async () => {
<template> <template>
<div <div
v-if="!isSharedBase && isUIAllowed('baseShare') && visibility !== 'hidden' && (activeTable || base)" v-if="!isSharedBase && isUIAllowed('baseShare') && visibility !== 'hidden' && (activeTable || base)"
class="flex flex-col justify-center h-full" class="nc-share-base-button flex flex-col justify-center h-full"
data-testid="share-base-button" data-testid="share-base-button"
:data-sharetype="visibility" :data-sharetype="visibility"
> >

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

File diff suppressed because it is too large Load Diff

248
packages/nc-gui/components/smartsheet/form/FieldMenu.vue

@ -0,0 +1,248 @@
<script lang="ts" setup>
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { computed } from 'vue'
import {
ActiveViewInj,
ColumnInj,
IsLockedInj,
MetaInj,
ReloadViewDataHookInj,
SmartsheetStoreEvents,
iconMap,
inject,
message,
toRefs,
useI18n,
useMetas,
useNuxtApp,
useSmartsheetStoreOrThrow,
} from '#imports'
const props = defineProps<{
column: ColumnType
formColumn: Record<string, any>
isRequired?: boolean
isOpen: boolean
onDelete: () => void
}>()
const emit = defineEmits(['hideField', 'update:isOpen', 'delete'])
const { column, isRequired } = toRefs(props)
const isOpen = useVModel(props, 'isOpen', emit)
const { eventBus } = useSmartsheetStoreOrThrow()
const reloadDataHook = inject(ReloadViewDataHookInj)
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj)
provide(ColumnInj, column)
const { $api } = useNuxtApp()
const { t } = useI18n()
const { getMeta } = useMetas()
const showDeleteColumnModal = ref(false)
const isDuplicateDlgOpen = ref(false)
const selectedColumnExtra = ref<any>()
const duplicateDialogRef = ref<any>()
const duplicateVirtualColumn = async () => {
let columnCreatePayload = {}
// generate duplicate column title
const duplicateColumnTitle = getUniqueColumnName(`${column!.value.title} copy`, meta!.value!.columns!)
columnCreatePayload = {
...column!.value!,
...(column!.value.colOptions ?? {}),
title: duplicateColumnTitle,
column_name: duplicateColumnTitle.replace(/\s/g, '_'),
id: undefined,
colOptions: undefined,
order: undefined,
system: false,
}
try {
const gridViewColumnList = (await $api.dbViewColumn.list(view.value?.id as string)).list
const currentColumnIndex = gridViewColumnList.findIndex((f) => f.fk_column_id === column!.value.id)
let newColumnOrder
if (currentColumnIndex === gridViewColumnList.length - 1) {
newColumnOrder = gridViewColumnList[currentColumnIndex].order! + 1
} else {
newColumnOrder = (gridViewColumnList[currentColumnIndex].order! + gridViewColumnList[currentColumnIndex + 1].order!) / 2
}
await $api.dbTableColumn.create(meta!.value!.id!, {
...columnCreatePayload,
pv: false,
view_id: view.value!.id as string,
column_order: {
order: newColumnOrder,
view_id: view.value!.id as string,
},
} as ColumnReqType)
await getMeta(meta!.value!.id!, true)
eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD)
reloadDataHook?.trigger()
// message.success(t('msg.success.columnDuplicated'))
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
// closing dropdown
isOpen.value = false
}
const openDuplicateDlg = async () => {
if (!column?.value) return
if (
column.value.uidt &&
[
UITypes.Lookup,
UITypes.Rollup,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(column.value.uidt as UITypes)
) {
duplicateVirtualColumn()
} else {
const gridViewColumnList = (await $api.dbViewColumn.list(view.value?.id as string)).list
const currentColumnIndex = gridViewColumnList.findIndex((f) => f.fk_column_id === column!.value.id)
let newColumnOrder
if (currentColumnIndex === gridViewColumnList.length - 1) {
newColumnOrder = gridViewColumnList[currentColumnIndex].order! + 1
} else {
newColumnOrder = (gridViewColumnList[currentColumnIndex].order! + gridViewColumnList[currentColumnIndex + 1].order!) / 2
}
selectedColumnExtra.value = {
pv: false,
view_id: view.value!.id as string,
column_order: {
order: newColumnOrder,
view_id: view.value!.id as string,
},
}
if (column.value.uidt === UITypes.Formula) {
nextTick(() => {
duplicateDialogRef?.value?.duplicate()
})
} else {
isDuplicateDlgOpen.value = true
}
isOpen.value = false
}
}
// hide the field in view
const hideField = async () => {
if (isRequired.value) return
isOpen.value = false
emit('hideField')
}
const handleDelete = () => {
// closing the dropdown
// when modal opens
isOpen.value = false
showDeleteColumnModal.value = true
}
const isDeleteAllowed = computed(() => {
return column?.value && !column.value.system
})
const isDuplicateAllowed = computed(() => {
return column?.value && !column.value.system
})
</script>
<template>
<a-dropdown
v-if="!isLocked"
v-model:visible="isOpen"
:trigger="['click']"
placement="bottomLeft"
overlay-class-name="nc-dropdown-form-column-operations !border-1 rounded-lg !shadow-xl"
@click.stop="isOpen = !isOpen"
>
<NcButton
type="secondary"
size="small"
class="nc-form-add-field"
data-testid="nc-form-add-field"
@click.stop="showAddColumnDropdown = true"
>
<component :is="iconMap.threeDotVertical" class="flex-none w-4 h-4" />
</NcButton>
<template #overlay>
<NcMenu class="flex flex-col gap-1 border-gray-200 nc-column-options">
<!-- Todo: Duplicate column with form column settings -->
<!-- eslint-disable vue/no-constant-condition -->
<NcMenuItem v-if="false" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg">
<div class="nc-column-duplicate nc-form-header-menu-item">
<component :is="iconMap.duplicate" />
<!-- Duplicate -->
{{ t('general.duplicate') }}
</div>
</NcMenuItem>
<NcMenuItem :disabled="isRequired" @click="hideField">
<div class="nc-column-insert-before nc-form-header-menu-item">
<component :is="iconMap.eye" class="!w-3.75 !h-3.75" />
<!-- Hide Field -->
{{ $t('general.hideField') }}
</div>
</NcMenuItem>
<template v-if="!column?.pv">
<a-divider class="!my-0" />
<NcMenuItem :disabled="!isDeleteAllowed" class="!hover:bg-red-50" @click="handleDelete">
<div class="nc-column-delete nc-form-header-menu-item text-red-600">
<component :is="iconMap.delete" />
<!-- Delete -->
{{ $t('general.delete') }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</a-dropdown>
<SmartsheetHeaderDeleteColumnModal
v-model:visible="showDeleteColumnModal"
class="nc-form-column-delete-dropdown"
:on-delete-column="onDelete"
/>
<DlgColumnDuplicate
v-if="column"
ref="duplicateDialogRef"
v-model="isDuplicateDlgOpen"
:column="column"
:extra="selectedColumnExtra"
/>
</template>
<style scoped>
.nc-form-header-menu-item {
@apply flex items-center gap-2;
}
</style>

94
packages/nc-gui/components/smartsheet/form/Layout.vue

@ -0,0 +1,94 @@
<script lang="ts" setup>
import { Pane, Splitpanes } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
const { leftSidebarWidth, windowSize, formRightSidebarState, formRightSidebarWidthPercent } = storeToRefs(useSidebarStore())
const formPreviewSize = computed(() => 100 - formRightSidebarWidthPercent.value)
function onResize(widthPercent: any) {
const sidebarWidth = (widthPercent * (windowSize.value - leftSidebarWidth.value)) / 100
if (sidebarWidth > formRightSidebarState.value.maxWidth) {
formRightSidebarState.value.width = formRightSidebarState.value.maxWidth
} else if (sidebarWidth < formRightSidebarState.value.minWidth) {
formRightSidebarState.value.width = formRightSidebarState.value.minWidth
} else {
formRightSidebarState.value.width = sidebarWidth
}
}
const normalizeSidebarWidth = computed(() => {
if (formRightSidebarState.value.width > formRightSidebarState.value.maxWidth) {
return formRightSidebarState.value.maxWidth
} else if (formRightSidebarState.value.width < formRightSidebarState.value.minWidth) {
return formRightSidebarState.value.minWidth
} else {
return formRightSidebarState.value.width
}
})
</script>
<template>
<Splitpanes
class="nc-form-right-sidebar-content-resizable-wrapper w-full h-full"
@resize="(event: any) => onResize(event[1].size)"
>
<Pane :size="formPreviewSize" class="flex-1 h-full">
<slot name="preview" />
</Pane>
<Pane
min-size="15%"
class="nc-sidebar-splitpane relative"
:size="formRightSidebarWidthPercent"
:style="{
minWidth: `${formRightSidebarState.minWidth}px !important`,
maxWidth: `${normalizeSidebarWidth}px !important`,
}"
>
<slot name="sidebar" />
</Pane>
</Splitpanes>
</template>
<style lang="scss">
/** Split pane CSS */
.nc-form-right-sidebar-content-resizable-wrapper > {
.splitpanes__splitter {
@apply !w-0 relative overflow-visible;
}
.splitpanes__splitter:before {
@apply bg-gray-200 w-0.25 absolute left-0 top-0 h-full z-40;
content: '';
}
.splitpanes__splitter:hover:before {
@apply bg-scrollbar;
width: 3px !important;
left: 0px;
}
.splitpanes--dragging .splitpanes__splitter:before {
@apply bg-scrollbar;
width: 3px !important;
left: 0px;
}
.splitpanes--dragging .splitpanes__splitter {
@apply w-1 mr-0;
}
}
.splitpanes__pane {
transition: width 0.15s ease-in-out !important;
}
.splitpanes--dragging {
cursor: col-resize;
> .splitpanes__pane {
transition: none !important;
}
}
</style>

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

@ -162,7 +162,7 @@ const showOrHideAll = (showAll: boolean) => {
</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"> <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"> <NcTooltip :disabled="!isRequired">
<template #title> {{ $t('msg.info.preventHideAllOptions') }} </template> <template #title> {{ $t('msg.info.preventHideAllOptions') }} </template>
<NcButton <NcButton
@ -172,7 +172,7 @@ const showOrHideAll = (showAll: boolean) => {
:disabled="isRequired || vModel.filter((o) => !o.show).length === vModel.length" :disabled="isRequired || vModel.filter((o) => !o.show).length === vModel.length"
@click="showOrHideAll(false)" @click="showOrHideAll(false)"
> >
Hide all {{ $t('general.hideAll') }}
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
@ -184,7 +184,7 @@ const showOrHideAll = (showAll: boolean) => {
:disabled="vModel.filter((o) => o.show).length === vModel.length" :disabled="vModel.filter((o) => o.show).length === vModel.length"
@click="showOrHideAll(true)" @click="showOrHideAll(true)"
> >
Show all {{ $t('general.showAll') }}
</NcButton> </NcButton>
</div> </div>
</div> </div>

8
packages/nc-gui/components/smartsheet/header/DeleteColumnModal.vue

@ -1,9 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { LinkToAnotherRecordType } from 'nocodb-sdk' import type { LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, isLinksOrLTAR } from 'nocodb-sdk' import { RelationTypes, isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
onDeleteColumn?: () => void
}>() }>()
const emits = defineEmits(['update:visible']) const emits = defineEmits(['update:visible'])
@ -46,6 +47,8 @@ const onDelete = async () => {
$e('a:column:delete') $e('a:column:delete')
visible.value = false visible.value = false
props.onDeleteColumn?.()
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} finally { } finally {
@ -58,7 +61,8 @@ const onDelete = async () => {
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.column')" :on-delete="onDelete"> <GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.column')" :on-delete="onDelete">
<template #entity-preview> <template #entity-preview>
<div v-if="column" class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700 mb-4"> <div v-if="column" class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700 mb-4">
<SmartsheetHeaderCellIcon class="nc-view-icon"></SmartsheetHeaderCellIcon> <SmartsheetHeaderVirtualCellIcon v-if="isVirtualCol(column)" class="nc-view-icon"></SmartsheetHeaderVirtualCellIcon>
<SmartsheetHeaderCellIcon v-else class="nc-view-icon"></SmartsheetHeaderCellIcon>
<div <div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.5" class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.5"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"

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

@ -198,7 +198,9 @@
"logo": "Logo", "logo": "Logo",
"dropdown": "Dropdown", "dropdown": "Dropdown",
"list": "List", "list": "List",
"apply": "Apply" "apply": "Apply",
"text": "Text",
"appearance": "Appearance"
}, },
"objects": { "objects": {
"day": "Day", "day": "Day",
@ -972,7 +974,7 @@
"clientKey": "Select .key file", "clientKey": "Select .key file",
"clientCert": "Select .cert file", "clientCert": "Select .cert file",
"clientCA": "Select CA file", "clientCA": "Select CA file",
"changeIconColour": "Change Icon Colour", "changeIconColour": "Change icon colour",
"preFillFormInfo": "To get a prefilled link, make sure you’ve filled the necessary fields in the form view builder.", "preFillFormInfo": "To get a prefilled link, make sure you’ve filled the necessary fields in the form view builder.",
"surveyFormInfo": "Form mode with one field per page" "surveyFormInfo": "Form mode with one field per page"
}, },

17
packages/nc-gui/lib/enums.ts

@ -142,3 +142,20 @@ export enum PreFilledMode {
Hidden = 'hidden', Hidden = 'hidden',
Locked = 'locked', Locked = 'locked',
} }
export enum RichTextBubbleMenuOptions {
bold = 'bold',
italic = 'italic',
underline = 'underline',
strike = 'strike',
code = 'code',
quote = 'quote',
heading1 = 'heading1',
heading2 = 'heading2',
heading3 = 'heading3',
blockQuote = 'blockQuote',
bulletList = 'bulletList',
numberedList = 'numberedList',
taskList = 'taskList',
link = 'link',
}

13
packages/nc-gui/store/sidebar.ts

@ -52,6 +52,16 @@ export const useSidebarStore = defineStore('sidebarStore', () => {
const nonHiddenLeftSidebarWidth = computed(() => (width.value * nonHiddenMobileSidebarSize.value) / 100) const nonHiddenLeftSidebarWidth = computed(() => (width.value * nonHiddenMobileSidebarSize.value) / 100)
const formRightSidebarState = ref({
minWidth: 384,
maxWidth: 600,
width: 384,
})
const formRightSidebarWidthPercent = computed(() => {
return (formRightSidebarState.value.width / (width.value - leftSidebarWidth.value)) * 100
})
return { return {
isLeftSidebarOpen, isLeftSidebarOpen,
isRightSidebarOpen, isRightSidebarOpen,
@ -61,6 +71,9 @@ export const useSidebarStore = defineStore('sidebarStore', () => {
leftSidebarWidth, leftSidebarWidth,
mobileNormalizedSidebarSize, mobileNormalizedSidebarSize,
nonHiddenLeftSidebarWidth, nonHiddenLeftSidebarWidth,
windowSize: width,
formRightSidebarState,
formRightSidebarWidthPercent,
} }
}) })

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

@ -60,7 +60,7 @@ export class FormPage extends BasePage {
} }
getFormFieldsRequired() { getFormFieldsRequired() {
return this.get().locator('[data-testid="nc-form-input-required"] + button'); return this.get().locator('[data-testid="nc-form-input-required"]');
} }
getFormFieldsInputLabel() { getFormFieldsInputLabel() {

Loading…
Cancel
Save