mirror of https://github.com/nocodb/nocodb
github-actions[bot]
4 months ago
committed by
GitHub
209 changed files with 7948 additions and 2694 deletions
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.8 KiB |
@ -0,0 +1,150 @@
|
||||
<script lang="ts" setup> |
||||
import dayjs from 'dayjs' |
||||
|
||||
interface Props { |
||||
size?: 'medium' |
||||
selectedDate?: dayjs.Dayjs | null |
||||
pageDate?: dayjs.Dayjs |
||||
isCellInputField?: boolean |
||||
type: 'date' | 'time' | 'year' | 'month' |
||||
isOpen: boolean |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<Props>(), { |
||||
size: 'medium', |
||||
selectedDate: null, |
||||
pageDate: () => dayjs(), |
||||
isCellInputField: false, |
||||
type: 'date', |
||||
isOpen: false, |
||||
}) |
||||
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek']) |
||||
// Page date is the date we use to manage which month/date that is currently being displayed |
||||
const pageDate = useVModel(props, 'pageDate', emit) |
||||
|
||||
const selectedDate = useVModel(props, 'selectedDate', emit) |
||||
|
||||
const { type, isOpen } = toRefs(props) |
||||
|
||||
const localPageDate = ref() |
||||
|
||||
const localSelectedDate = ref() |
||||
|
||||
const pickerType = ref<Props['type'] | undefined>() |
||||
|
||||
const pickerStack = ref<Props['type'][]>([]) |
||||
|
||||
const tempPickerType = computed(() => pickerType.value || type.value) |
||||
|
||||
const handleUpdatePickerType = (value?: Props['type']) => { |
||||
if (value) { |
||||
pickerType.value = value |
||||
pickerStack.value.push(value) |
||||
} else { |
||||
if (pickerStack.value.length > 1) { |
||||
pickerStack.value.pop() |
||||
const lastPicker = pickerStack.value.pop() |
||||
pickerType.value = lastPicker |
||||
} else { |
||||
pickerStack.value = [] |
||||
pickerType.value = type.value |
||||
} |
||||
} |
||||
} |
||||
|
||||
const localStatePageDate = computed({ |
||||
get: () => { |
||||
if (localPageDate.value) { |
||||
return localPageDate.value |
||||
} |
||||
return pageDate.value |
||||
}, |
||||
set: (value) => { |
||||
pageDate.value = value |
||||
localPageDate.value = value |
||||
emit('update:pageDate', value) |
||||
}, |
||||
}) |
||||
|
||||
const localStateSelectedDate = computed({ |
||||
get: () => { |
||||
if (localSelectedDate.value) { |
||||
return localSelectedDate.value |
||||
} |
||||
return pageDate.value |
||||
}, |
||||
set: (value: dayjs.Dayjs) => { |
||||
if (!value.isValid()) return |
||||
|
||||
if (pickerType.value === type.value) { |
||||
localPageDate.value = value |
||||
emit('update:selectedDate', value) |
||||
localSelectedDate.value = undefined |
||||
return |
||||
} |
||||
|
||||
if (['date', 'month'].includes(type.value)) { |
||||
if (pickerType.value === 'year') { |
||||
localSelectedDate.value = dayjs(localPageDate.value ?? localSelectedDate.value ?? selectedDate.value ?? dayjs()).year( |
||||
+value.format('YYYY'), |
||||
) |
||||
} |
||||
if (type.value !== 'month' && pickerType.value === 'month') { |
||||
localSelectedDate.value = dayjs(localPageDate.value ?? localSelectedDate.value ?? selectedDate.value ?? dayjs()).month( |
||||
+value.format('MM') - 1, |
||||
) |
||||
} |
||||
|
||||
localPageDate.value = localSelectedDate.value |
||||
|
||||
handleUpdatePickerType() |
||||
} |
||||
}, |
||||
}) |
||||
|
||||
watch(isOpen, (next) => { |
||||
if (!next) { |
||||
pickerType.value = type.value |
||||
localPageDate.value = undefined |
||||
localSelectedDate.value = undefined |
||||
pickerStack.value = [] |
||||
} |
||||
}) |
||||
|
||||
onUnmounted(() => { |
||||
pickerType.value = type.value |
||||
localPageDate.value = undefined |
||||
localSelectedDate.value = undefined |
||||
pickerStack.value = [] |
||||
}) |
||||
onMounted(() => { |
||||
localPageDate.value = undefined |
||||
localSelectedDate.value = undefined |
||||
pickerStack.value = [] |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<NcDateWeekSelector |
||||
v-if="tempPickerType === 'date'" |
||||
v-model:page-date="localStatePageDate" |
||||
v-model:selected-date="localStateSelectedDate" |
||||
:picker-type="pickerType" |
||||
:is-monday-first="false" |
||||
is-cell-input-field |
||||
size="medium" |
||||
@update:picker-type="handleUpdatePickerType" |
||||
/> |
||||
<NcMonthYearSelector |
||||
v-if="['month', 'year'].includes(tempPickerType)" |
||||
v-model:page-date="localStatePageDate" |
||||
v-model:selected-date="localStateSelectedDate" |
||||
:picker-type="pickerType" |
||||
:is-year-picker="tempPickerType === 'year'" |
||||
is-cell-input-field |
||||
size="medium" |
||||
@update:picker-type="handleUpdatePickerType" |
||||
/> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped></style> |
@ -0,0 +1,104 @@
|
||||
<script lang="ts" setup> |
||||
import dayjs from 'dayjs' |
||||
|
||||
interface Props { |
||||
selectedDate: dayjs.Dayjs | null |
||||
is12hrFormat?: boolean |
||||
isMinGranularityPicker?: boolean |
||||
minGranularity?: number |
||||
isOpen?: boolean |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<Props>(), { |
||||
selectedDate: null, |
||||
is12hrFormat: false, |
||||
isMinGranularityPicker: false, |
||||
minGranularity: 30, |
||||
isOpen: false, |
||||
}) |
||||
const emit = defineEmits(['update:selectedDate']) |
||||
|
||||
const pageDate = ref<dayjs.Dayjs>(dayjs()) |
||||
|
||||
const selectedDate = useVModel(props, 'selectedDate', emit) |
||||
|
||||
const { is12hrFormat, isMinGranularityPicker, minGranularity, isOpen } = toRefs(props) |
||||
|
||||
const timeOptionsWrapperRef = ref<HTMLDivElement>() |
||||
|
||||
const compareTime = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => { |
||||
if (!date1 || !date2) return false |
||||
|
||||
return date1.format('HH:mm') === date2.format('HH:mm') |
||||
} |
||||
|
||||
const handleSelectTime = (time: dayjs.Dayjs) => { |
||||
pageDate.value = dayjs().set('hour', time.get('hour')).set('minute', time.get('minute')) |
||||
|
||||
selectedDate.value = pageDate.value |
||||
|
||||
// emit('update:selectedDate', pageDate.value) |
||||
} |
||||
|
||||
// TODO: 12hr time format & regular time picker |
||||
const timeOptions = computed(() => { |
||||
return Array.from({ length: is12hrFormat.value ? 12 : 24 }).flatMap((_, h) => { |
||||
return (isMinGranularityPicker.value ? [0, minGranularity.value] : Array.from({ length: 60 })).map((_m, m) => { |
||||
const time = dayjs() |
||||
.set('hour', h) |
||||
.set('minute', isMinGranularityPicker.value ? (_m as number) : m) |
||||
|
||||
return time |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
const handleAutoScroll = (behavior: ScrollBehavior = 'instant') => { |
||||
if (!timeOptionsWrapperRef.value || !selectedDate.value) return |
||||
|
||||
setTimeout(() => { |
||||
const timeEl = timeOptionsWrapperRef.value?.querySelector( |
||||
`[data-testid="time-option-${selectedDate.value?.format('HH:mm')}"]`, |
||||
) |
||||
|
||||
timeEl?.scrollIntoView({ behavior, block: 'center' }) |
||||
}, 50) |
||||
} |
||||
|
||||
watch([selectedDate, isOpen], () => { |
||||
if (timeOptionsWrapperRef.value && isOpen.value && selectedDate.value) { |
||||
handleAutoScroll() |
||||
} |
||||
}) |
||||
|
||||
onMounted(() => { |
||||
handleAutoScroll() |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex flex-col max-w-[350px]"> |
||||
<div v-if="isMinGranularityPicker" ref="timeOptionsWrapperRef" class="h-[180px] overflow-y-auto nc-scrollbar-thin"> |
||||
<div |
||||
v-for="time of timeOptions" |
||||
:key="time.format('HH:mm')" |
||||
class="hover:bg-gray-100 py-1 px-3 text-sm text-gray-600 font-weight-500 text-center cursor-pointer" |
||||
:class="{ |
||||
'nc-selected bg-gray-100': selectedDate && compareTime(time, selectedDate), |
||||
}" |
||||
:data-testid="`time-option-${time.format('HH:mm')}`" |
||||
@click="handleSelectTime(time)" |
||||
> |
||||
{{ time.format('HH:mm') }} |
||||
</div> |
||||
</div> |
||||
<div v-else></div> |
||||
<div class="px-2 py-1 box-border flex items-center justify-center"> |
||||
<NcButton :tabindex="-1" class="!h-7" size="small" type="secondary" @click="handleSelectTime(dayjs())"> |
||||
<span class="text-small"> {{ $t('general.now') }} </span> |
||||
</NcButton> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped></style> |
@ -0,0 +1,380 @@
|
||||
<script lang="ts" setup> |
||||
import StarterKit from '@tiptap/starter-kit' |
||||
import { EditorContent, useEditor } from '@tiptap/vue-3' |
||||
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 { Link } from '~/helpers/dbTiptapExtensions/links' |
||||
|
||||
const props = withDefaults( |
||||
defineProps<{ |
||||
hideOptions?: boolean |
||||
value?: string | null |
||||
readOnly?: boolean |
||||
syncValueChange?: boolean |
||||
autofocus?: boolean |
||||
placeholder?: string |
||||
renderAsText?: boolean |
||||
}>(), |
||||
{ |
||||
hideOptions: true, |
||||
}, |
||||
) |
||||
|
||||
const emits = defineEmits(['update:value', 'focus', 'blur', 'save']) |
||||
|
||||
const isGrid = inject(IsGridInj, ref(false)) |
||||
|
||||
const isFocused = ref(false) |
||||
|
||||
const keys = useMagicKeys() |
||||
|
||||
const turndownService = new TurndownService({}) |
||||
|
||||
turndownService.addRule('lineBreak', { |
||||
filter: (node) => { |
||||
return node.nodeName === 'BR' |
||||
}, |
||||
replacement: () => { |
||||
return '<br />' |
||||
}, |
||||
}) |
||||
|
||||
turndownService.addRule('strikethrough', { |
||||
filter: ['s'], |
||||
replacement: (content) => { |
||||
return `~${content}~` |
||||
}, |
||||
}) |
||||
|
||||
turndownService.keep(['u', 'del']) |
||||
|
||||
const editorDom = ref<HTMLElement | null>(null) |
||||
|
||||
const richTextLinkOptionRef = ref<HTMLElement | null>(null) |
||||
|
||||
const vModel = useVModel(props, 'value', emits, { defaultValue: '' }) |
||||
|
||||
const tiptapExtensions = [ |
||||
StarterKit.configure({ |
||||
heading: false, |
||||
}), |
||||
Underline, |
||||
Link, |
||||
Placeholder.configure({ |
||||
emptyEditorClass: 'is-editor-empty', |
||||
placeholder: props.placeholder, |
||||
}), |
||||
] |
||||
|
||||
const editor = useEditor({ |
||||
extensions: tiptapExtensions, |
||||
onUpdate: ({ editor }) => { |
||||
const markdown = turndownService |
||||
.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />')) |
||||
.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n') |
||||
vModel.value = markdown === '<br />' ? '' : markdown |
||||
}, |
||||
editable: !props.readOnly, |
||||
autofocus: props.autofocus, |
||||
onFocus: () => { |
||||
isFocused.value = true |
||||
emits('focus') |
||||
}, |
||||
onBlur: (e) => { |
||||
if ( |
||||
!(e?.event?.relatedTarget as HTMLElement)?.closest('.comment-bubble-menu, .nc-comment-rich-editor, .nc-rich-text-comment') |
||||
) { |
||||
isFocused.value = false |
||||
emits('blur') |
||||
} |
||||
}, |
||||
}) |
||||
|
||||
const setEditorContent = (contentMd: any, focusEndOfDoc?: boolean) => { |
||||
if (!editor.value) return |
||||
|
||||
const selection = editor.value.view.state.selection |
||||
|
||||
const contentHtml = contentMd ? marked.parse(contentMd) : '<p></p>' |
||||
|
||||
const content = generateJSON(contentHtml, tiptapExtensions) |
||||
|
||||
editor.value.chain().setContent(content).setTextSelection(selection.to).run() |
||||
|
||||
setTimeout(() => { |
||||
if (focusEndOfDoc) { |
||||
const docSize = editor.value!.state.doc.nodeSize |
||||
|
||||
editor.value |
||||
?.chain() |
||||
.setTextSelection(docSize - 1) |
||||
.run() |
||||
} |
||||
|
||||
;(editor.value!.state as any).history$.prevRanges = null |
||||
;(editor.value!.state as any).history$.done.eventCount = 0 |
||||
}, 100) |
||||
} |
||||
|
||||
const onFocusWrapper = () => { |
||||
if (!props.readOnly && !keys.shift.value) { |
||||
editor.value?.chain().focus().run() |
||||
} |
||||
} |
||||
|
||||
if (props.syncValueChange) { |
||||
watch([vModel, editor], () => { |
||||
setEditorContent(vModel.value) |
||||
}) |
||||
} |
||||
|
||||
useEventListener( |
||||
editorDom, |
||||
'focusout', |
||||
(e: FocusEvent) => { |
||||
const targetEl = e?.relatedTarget as HTMLElement |
||||
if ( |
||||
targetEl?.classList?.contains('tiptap') || |
||||
!targetEl?.closest('.comment-bubble-menu, .tippy-content, .nc-comment-rich-editor') |
||||
) { |
||||
isFocused.value = false |
||||
emits('blur') |
||||
} |
||||
}, |
||||
true, |
||||
) |
||||
useEventListener( |
||||
richTextLinkOptionRef, |
||||
'focusout', |
||||
(e: FocusEvent) => { |
||||
const targetEl = e?.relatedTarget as HTMLElement |
||||
if (!targetEl && (e.target as HTMLElement)?.closest('.comment-bubble-menu, .tippy-content, .nc-comment-rich-editor')) return |
||||
|
||||
if (!targetEl?.closest('.comment-bubble-menu, .tippy-content, .nc-comment-rich-editor')) { |
||||
isFocused.value = false |
||||
emits('blur') |
||||
} |
||||
}, |
||||
true, |
||||
) |
||||
onClickOutside(editorDom, (e) => { |
||||
if (!isFocused.value) return |
||||
|
||||
const targetEl = e?.target as HTMLElement |
||||
|
||||
if (!targetEl?.closest('.tippy-content, .comment-bubble-menu, .nc-comment-rich-editor')) { |
||||
isFocused.value = false |
||||
emits('blur') |
||||
} |
||||
}) |
||||
|
||||
const triggerSaveFromList = ref(false) |
||||
|
||||
const emitSave = (event: KeyboardEvent) => { |
||||
if (editor.value) { |
||||
if (triggerSaveFromList.value) { |
||||
// If Enter was pressed in the list, do not emit save |
||||
triggerSaveFromList.value = false |
||||
} else { |
||||
if (editor.value.isActive('bulletList') || editor.value.isActive('orderedList')) { |
||||
event.stopPropagation() |
||||
} else { |
||||
emits('save') |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
const handleEnterDown = (event: KeyboardEvent) => { |
||||
const isListsActive = editor.value?.isActive('bulletList') || editor.value?.isActive('orderedList') |
||||
if (isListsActive) { |
||||
triggerSaveFromList.value = true |
||||
setTimeout(() => { |
||||
triggerSaveFromList.value = false |
||||
}, 1000) |
||||
} else { |
||||
emitSave(event) |
||||
} |
||||
} |
||||
|
||||
const handleKeyPress = (event: KeyboardEvent) => { |
||||
if (event.altKey && event.key === 'Enter') { |
||||
event.stopPropagation() |
||||
} else if (event.shiftKey && event.key === 'Enter') { |
||||
event.stopPropagation() |
||||
} else if (event.key === 'Enter') { |
||||
handleEnterDown(event) |
||||
} else if (event.key === 'Escape') { |
||||
isFocused.value = false |
||||
emits('blur') |
||||
} |
||||
} |
||||
|
||||
defineExpose({ |
||||
setEditorContent, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
:class="{ |
||||
'readonly': readOnly, |
||||
'nc-rich-text-grid': isGrid, |
||||
}" |
||||
:tabindex="1" |
||||
class="nc-rich-text-comment flex flex-col w-full h-full" |
||||
@focus="onFocusWrapper" |
||||
> |
||||
<div v-if="renderAsText" class="truncate"> |
||||
<span v-if="editor"> {{ editor?.getText() ?? '' }}</span> |
||||
</div> |
||||
<template v-else> |
||||
<CellRichTextLinkOptions |
||||
v-if="editor" |
||||
ref="richTextLinkOptionRef" |
||||
:editor="editor" |
||||
:is-form-field="true" |
||||
@blur="isFocused = false" |
||||
/> |
||||
|
||||
<EditorContent |
||||
ref="editorDom" |
||||
:editor="editor" |
||||
class="flex flex-col nc-comment-rich-editor px-1.5 w-full scrollbar-thin scrollbar-thumb-gray-200 nc-truncate scrollbar-track-transparent" |
||||
@keydown.stop="handleKeyPress" |
||||
/> |
||||
|
||||
<div v-if="!hideOptions" class="flex justify-between px-2 py-2 items-center"> |
||||
<LazySmartsheetExpandedFormRichTextOptions :editor="editor" class="!bg-transparent" /> |
||||
<NcButton |
||||
v-e="['a:row-expand:comment:save']" |
||||
:disabled="!vModel?.length" |
||||
class="!disabled:bg-gray-100 !h-7 !w-7 !shadow-none" |
||||
size="xsmall" |
||||
@click="emits('save')" |
||||
> |
||||
<GeneralIcon icon="send" /> |
||||
</NcButton> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss"> |
||||
.nc-rich-text-comment { |
||||
.readonly { |
||||
.nc-comment-rich-editor { |
||||
.ProseMirror { |
||||
resize: none; |
||||
white-space: pre-line; |
||||
} |
||||
} |
||||
} |
||||
.nc-comment-rich-editor { |
||||
&.nc-truncate { |
||||
.tiptap.ProseMirror { |
||||
display: -webkit-box; |
||||
max-width: 100%; |
||||
outline: none; |
||||
-webkit-box-orient: vertical; |
||||
word-break: break-word; |
||||
} |
||||
&.nc-line-clamp-1 .tiptap.ProseMirror { |
||||
-webkit-line-clamp: 1; |
||||
} |
||||
&.nc-line-clamp-2 .tiptap.ProseMirror { |
||||
-webkit-line-clamp: 2; |
||||
} |
||||
&.nc-line-clamp-3 .tiptap.ProseMirror { |
||||
-webkit-line-clamp: 3; |
||||
} |
||||
&.nc-line-clamp-4 .tiptap.ProseMirror { |
||||
-webkit-line-clamp: 4; |
||||
} |
||||
} |
||||
.tiptap p.is-editor-empty:first-child::before { |
||||
color: #9aa2af; |
||||
content: attr(data-placeholder); |
||||
float: left; |
||||
height: 0; |
||||
pointer-events: none; |
||||
} |
||||
|
||||
.ProseMirror { |
||||
@apply flex-grow !border-0 rounded-lg; |
||||
caret-color: #3366ff; |
||||
} |
||||
|
||||
p { |
||||
@apply !m-0; |
||||
} |
||||
|
||||
.ProseMirror-focused { |
||||
// remove all border |
||||
outline: none; |
||||
} |
||||
|
||||
ul { |
||||
li { |
||||
@apply ml-4; |
||||
list-style-type: disc; |
||||
} |
||||
} |
||||
|
||||
ol { |
||||
@apply !pl-4; |
||||
li { |
||||
list-style-type: decimal; |
||||
} |
||||
} |
||||
|
||||
ul, |
||||
ol { |
||||
@apply !my-0; |
||||
} |
||||
|
||||
// Pre tag is the parent wrapper for Code block |
||||
pre { |
||||
border-color: #d0d5dd; |
||||
border: 1px; |
||||
color: black; |
||||
font-family: 'JetBrainsMono', monospace; |
||||
padding: 1rem; |
||||
border-radius: 0.5rem; |
||||
@apply overflow-auto mt-3 bg-gray-100; |
||||
|
||||
code { |
||||
@apply !px-0; |
||||
} |
||||
} |
||||
|
||||
code { |
||||
@apply rounded-md px-2 py-1 bg-gray-100; |
||||
color: inherit; |
||||
font-size: 0.8rem; |
||||
} |
||||
|
||||
blockquote { |
||||
border-left: 3px solid #d0d5dd; |
||||
padding: 0 1em; |
||||
color: #666; |
||||
margin: 1em 0; |
||||
font-style: italic; |
||||
} |
||||
|
||||
hr { |
||||
@apply !border-gray-300; |
||||
border: 0; |
||||
border-top: 1px solid #ccc; |
||||
margin: 1.5em 0; |
||||
} |
||||
|
||||
pre { |
||||
height: fit-content; |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,215 @@
|
||||
<script lang="ts" setup> |
||||
import type { Editor } from '@tiptap/vue-3' |
||||
import MdiFormatStrikeThrough from '~icons/mdi/format-strikethrough' |
||||
|
||||
interface Props { |
||||
editor: Editor | undefined |
||||
} |
||||
const props = withDefaults(defineProps<Props>(), {}) |
||||
|
||||
const { appInfo } = useGlobal() |
||||
|
||||
const { editor } = toRefs(props) |
||||
|
||||
const cmdOrCtrlKey = computed(() => { |
||||
return isMac() ? '⌘' : 'CTRL' |
||||
}) |
||||
|
||||
const shiftKey = computed(() => { |
||||
return isMac() ? '⇧' : 'Shift' |
||||
}) |
||||
|
||||
const tabIndex = computed(() => { |
||||
return -1 |
||||
}) |
||||
|
||||
const onToggleLink = () => { |
||||
if (!editor.value) return |
||||
|
||||
const activeNode = editor.value?.state?.selection?.$from?.nodeBefore || editor.value?.state?.selection?.$from?.nodeAfter |
||||
|
||||
const isLinkMarkedStoredInEditor = editor.value?.state?.storedMarks?.some((mark: any) => mark.type.name === 'link') |
||||
|
||||
const isActiveNodeMarkActive = activeNode?.marks?.some((mark: any) => mark.type.name === 'link') || isLinkMarkedStoredInEditor |
||||
|
||||
if (isActiveNodeMarkActive) { |
||||
editor.value.chain().focus().unsetLink().run() |
||||
} else { |
||||
if (editor.value?.state.selection.empty) { |
||||
editor |
||||
.value!.chain() |
||||
.focus() |
||||
.insertContent(' ') |
||||
.setTextSelection({ from: editor.value?.state.selection.$from.pos, to: editor.value.state.selection.$from.pos + 1 }) |
||||
.toggleLink({ |
||||
href: '', |
||||
}) |
||||
.setTextSelection({ from: editor.value?.state.selection.$from.pos, to: editor.value.state.selection.$from.pos + 1 }) |
||||
.deleteSelection() |
||||
.run() |
||||
} else { |
||||
editor |
||||
.value!.chain() |
||||
.focus() |
||||
.setLink({ |
||||
href: '', |
||||
}) |
||||
.selectTextblockEnd() |
||||
.run() |
||||
} |
||||
|
||||
setTimeout(() => { |
||||
const linkInput = document.querySelector('.nc-text-area-rich-link-option-input') |
||||
if (linkInput) { |
||||
;(linkInput as any).focus() |
||||
} |
||||
}, 100) |
||||
} |
||||
} |
||||
|
||||
const newMentionNode = () => { |
||||
editor.value?.commands.insertContent('@') |
||||
editor.value?.chain().focus().run() |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="comment-bubble-menu bg-transparent flex-row rounded-lg flex"> |
||||
<NcTooltip> |
||||
<template #title> |
||||
<div class="flex flex-col items-center"> |
||||
<div> |
||||
{{ $t('labels.bold') }} |
||||
</div> |
||||
<div class="text-xs">{{ cmdOrCtrlKey }} B</div> |
||||
</div> |
||||
</template> |
||||
<NcButton |
||||
:class="{ 'is-active': editor?.isActive('bold') }" |
||||
:tabindex="tabIndex" |
||||
class="!h-7 !w-7 !hover:bg-gray-200" |
||||
size="xsmall" |
||||
type="text" |
||||
@click="editor?.chain().focus().toggleBold().run()" |
||||
> |
||||
<GeneralIcon icon="bold" /> |
||||
</NcButton> |
||||
</NcTooltip> |
||||
|
||||
<NcTooltip :disabled="editor?.isActive('italic')"> |
||||
<template #title> |
||||
<div class="flex flex-col items-center"> |
||||
<div> |
||||
{{ $t('labels.italic') }} |
||||
</div> |
||||
<div>{{ cmdOrCtrlKey }} I</div> |
||||
</div> |
||||
</template> |
||||
<NcButton |
||||
:class="{ 'is-active': editor?.isActive('italic') }" |
||||
:tabindex="tabIndex" |
||||
class="!h-7 !w-7 !hover:bg-gray-200" |
||||
size="xsmall" |
||||
type="text" |
||||
@click=";(editor?.chain().focus() as any).toggleItalic().run()" |
||||
> |
||||
<GeneralIcon icon="italic" /> |
||||
</NcButton> |
||||
</NcTooltip> |
||||
<NcTooltip> |
||||
<template #title> |
||||
<div class="flex flex-col items-center"> |
||||
<div> |
||||
{{ $t('labels.underline') }} |
||||
</div> |
||||
<div>{{ cmdOrCtrlKey }} U</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<NcButton |
||||
:class="{ 'is-active': editor?.isActive('underline') }" |
||||
:tabindex="tabIndex" |
||||
class="!h-7 !w-7 !hover:bg-gray-200" |
||||
size="xsmall" |
||||
type="text" |
||||
@click="editor?.chain().focus().toggleUnderline().run()" |
||||
> |
||||
<GeneralIcon icon="underline" /> |
||||
</NcButton> |
||||
</NcTooltip> |
||||
<NcTooltip> |
||||
<template #title> |
||||
<div class="flex flex-col items-center"> |
||||
<div> |
||||
{{ $t('labels.strike') }} |
||||
</div> |
||||
<div>{{ shiftKey }} {{ cmdOrCtrlKey }} S</div> |
||||
</div> |
||||
</template> |
||||
<NcButton |
||||
:class="{ 'is-active': editor?.isActive('strike') }" |
||||
:tabindex="tabIndex" |
||||
class="!h-7 !w-7 !hover:bg-gray-200" |
||||
size="xsmall" |
||||
type="text" |
||||
@click="editor?.chain().focus().toggleStrike().run()" |
||||
> |
||||
<GeneralIcon icon="strike" /> |
||||
</NcButton> |
||||
</NcTooltip> |
||||
|
||||
<NcTooltip> |
||||
<template #title> {{ $t('general.link') }}</template> |
||||
<NcButton |
||||
:class="{ 'is-active': editor?.isActive('link') }" |
||||
:tabindex="tabIndex" |
||||
class="!h-7 !w-7 !hover:bg-gray-200" |
||||
size="xsmall" |
||||
type="text" |
||||
@click="onToggleLink" |
||||
> |
||||
<GeneralIcon icon="link2"></GeneralIcon> |
||||
</NcButton> |
||||
</NcTooltip> |
||||
<NcTooltip v-if="appInfo.ee"> |
||||
<template #title> |
||||
<div class="flex flex-col items-center"> |
||||
<div> |
||||
{{ $t('labels.mention') }} |
||||
</div> |
||||
<div>@</div> |
||||
</div> |
||||
</template> |
||||
<NcButton |
||||
:class="{ 'is-active': editor?.isActive('suggestions') }" |
||||
:tabindex="tabIndex" |
||||
class="!h-7 !w-7 !hover:bg-gray-200" |
||||
size="xsmall" |
||||
type="text" |
||||
@click="newMentionNode" |
||||
> |
||||
<GeneralIcon icon="atSign" /> |
||||
</NcButton> |
||||
</NcTooltip> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
.comment-bubble-menu { |
||||
@apply !border-none; |
||||
|
||||
.nc-button.is-active { |
||||
@apply text-brand-500; |
||||
outline: 1px; |
||||
} |
||||
.ant-select-selector { |
||||
@apply !rounded-md; |
||||
} |
||||
.ant-select-selector .ant-select-selection-item { |
||||
@apply !text-xs; |
||||
} |
||||
.ant-btn-loading-icon { |
||||
@apply pb-0.5; |
||||
} |
||||
} |
||||
</style> |
After Width: | Height: | Size: 267 KiB |
After Width: | Height: | Size: 160 KiB |
After Width: | Height: | Size: 145 KiB |
After Width: | Height: | Size: 180 KiB |
@ -0,0 +1,89 @@
|
||||
import { |
||||
Body, |
||||
Controller, |
||||
Delete, |
||||
Get, |
||||
HttpCode, |
||||
Param, |
||||
Patch, |
||||
Post, |
||||
Query, |
||||
Req, |
||||
UseGuards, |
||||
} from '@nestjs/common'; |
||||
import { GlobalGuard } from '~/guards/global/global.guard'; |
||||
import { PagedResponseImpl } from '~/helpers/PagedResponse'; |
||||
import { CommentsService } from '~/services/comments.service'; |
||||
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; |
||||
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard'; |
||||
import { NcRequest } from '~/interface/config'; |
||||
|
||||
@Controller() |
||||
@UseGuards(MetaApiLimiterGuard, GlobalGuard) |
||||
export class CommentsController { |
||||
constructor(private readonly commentsService: CommentsService) {} |
||||
|
||||
@Get(['/api/v1/db/meta/comments', '/api/v2/meta/comments']) |
||||
@Acl('commentList') |
||||
async commentList(@Req() req: any) { |
||||
return new PagedResponseImpl( |
||||
await this.commentsService.commentList({ query: req.query }), |
||||
); |
||||
} |
||||
|
||||
@Post(['/api/v1/db/meta/comments', '/api/v2/meta/comments']) |
||||
@HttpCode(200) |
||||
@Acl('commentRow') |
||||
async commentRow(@Req() req: NcRequest, @Body() body: any) { |
||||
return await this.commentsService.commentRow({ |
||||
user: req.user, |
||||
body: body, |
||||
req, |
||||
}); |
||||
} |
||||
|
||||
@Delete([ |
||||
'/api/v1/db/meta/comment/:commentId', |
||||
'/api/v2/meta/comment/:commentId', |
||||
]) |
||||
@Acl('commentDelete') |
||||
async commentDelete( |
||||
@Req() req: NcRequest, |
||||
@Param('commentId') commentId: string, |
||||
) { |
||||
return await this.commentsService.commentDelete({ |
||||
commentId, |
||||
user: req.user, |
||||
}); |
||||
} |
||||
|
||||
@Patch([ |
||||
'/api/v1/db/meta/comment/:commentId', |
||||
'/api/v2/meta/comment/:commentId', |
||||
]) |
||||
@Acl('commentUpdate') |
||||
async commentUpdate( |
||||
@Param('commentId') commentId: string, |
||||
@Req() req: any, |
||||
@Body() body: any, |
||||
) { |
||||
return await this.commentsService.commentUpdate({ |
||||
commentId: commentId, |
||||
user: req.user, |
||||
body: body, |
||||
req, |
||||
}); |
||||
} |
||||
|
||||
@Get(['/api/v1/db/meta/comments/count', '/api/v2/meta/comments/count']) |
||||
@Acl('commentsCount') |
||||
async commentsCount( |
||||
@Query('fk_model_id') fk_model_id: string, |
||||
@Query('ids') ids: string[], |
||||
) { |
||||
return await this.commentsService.commentsCount({ |
||||
fk_model_id, |
||||
ids, |
||||
}); |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue