Implement @everyone/@here (#8890)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2025-05-12 14:42:58 +07:00
committed by GitHub
parent 137260fe5b
commit 3295adee57
47 changed files with 675 additions and 351 deletions
@@ -15,11 +15,14 @@
-->
<script lang="ts">
import { showPopup, resizeObserver, deviceOptionsStore as deviceInfo, PopupResult } from '@hcengineering/ui'
import { Ref, Class, Doc } from '@hcengineering/core'
import { onDestroy, onMount } from 'svelte'
import MentionPopup from './MentionPopup.svelte'
import DummyPopup from './DummyPopup.svelte'
export let docClass: Ref<Class<Doc>> | undefined = undefined
export let query: string = ''
export let multipleMentions: boolean = false
export let clientRect: () => ClientRect
export let command: (props: any) => void
export let close: () => void
@@ -115,7 +118,9 @@
>
<MentionPopup
bind:this={searchPopup}
{docClass}
{query}
{multipleMentions}
on:close={(evt) => {
dispatchItem(evt.detail)
}}
@@ -14,23 +14,95 @@
// limitations under the License.
-->
<script lang="ts">
import { SearchResultDoc } from '@hcengineering/core'
import presentation, { SearchResult, reduceCalls, searchFor, type SearchItem } from '@hcengineering/presentation'
import core, { SearchResultDoc, Ref, Class, Doc } from '@hcengineering/core'
import presentation, {
SearchResult,
reduceCalls,
searchFor,
type SearchItem,
getClient
} from '@hcengineering/presentation'
import { Label, ListView, resizeObserver } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import contact from '@hcengineering/contact'
import { getReferenceLabel, getReferenceObject } from './extension/reference'
import { translate } from '@hcengineering/platform'
export let query: string = ''
export let multipleMentions: boolean = false
export let docClass: Ref<Class<Doc>> | undefined = undefined
let items: SearchItem[] = []
const dispatch = createEventDispatcher()
const client = getClient()
let list: ListView
let scrollContainer: HTMLElement
let selection = 0
const employeeSearchCategory = client
.getModel()
.findAllSync(presentation.class.ObjectSearchCategory, { classToSearch: contact.mixin.Employee })[0]
async function getMultipleEmployeeSearchItems (localQuery: string, lastIndex: number): Promise<SearchItem[]> {
if (!multipleMentions) return []
const clazz =
docClass && client.getHierarchy().hasClass(docClass) ? client.getHierarchy().getClass(docClass) : undefined
const docTitle = await translate(clazz?.label ?? core.string.Object, {})
const everyoneDescription = await translate(contact.string.EveryoneDescription, {
title: docTitle.toLowerCase()
})
const hereDescription = await translate(contact.string.HereDescription, {
title: docTitle.toLowerCase()
})
const everyoneTitle = await translate(contact.string.Everyone, {})
const hereTitle = await translate(contact.string.Here, {})
return [
{
num: 0,
category: employeeSearchCategory,
item: {
id: contact.mention.Everyone,
title: everyoneTitle,
description: everyoneDescription,
emojiIcon: '📢',
doc: {
_id: contact.mention.Everyone,
_class: contact.mixin.Employee
}
}
},
{
num: 0,
category: employeeSearchCategory,
item: {
id: contact.mention.Here,
title: hereTitle,
description: hereDescription,
emojiIcon: '📢',
doc: {
_id: contact.mention.Here,
_class: contact.mixin.Employee
}
}
}
]
.filter((it) => it.item.title.toLowerCase().includes(localQuery.toLowerCase()))
.map((it, idx) => ({ ...it, num: lastIndex + 1 + idx }))
}
async function handleSelectItem (item: SearchResultDoc): Promise<void> {
if ([contact.mention.Here, contact.mention.Everyone].includes(item.id as any)) {
dispatch('close', {
id: item.doc._id,
label: item.title?.toLowerCase() ?? '',
objectclass: item.doc._class
})
return
}
const obj = (await getReferenceObject(item.doc._class, item.doc._id)) ?? item.doc
const label = await getReferenceLabel(obj._class, obj._id)
dispatch('close', {
@@ -73,7 +145,13 @@
const updateItems = reduceCalls(async function (localQuery: string): Promise<void> {
const r = await searchFor('mention', localQuery)
if (r.query === query) {
items = r.items
const latestIndex = r.items.findLastIndex((it) => it.category.classToSearch === contact.mixin.Employee)
const multipleEmployeeSearchItems = await getMultipleEmployeeSearchItems(localQuery, latestIndex)
items =
latestIndex === -1
? [...multipleEmployeeSearchItems, ...r.items]
: [...r.items.slice(0, latestIndex + 1), ...multipleEmployeeSearchItems, ...r.items.slice(latestIndex + 1)]
}
})
$: void updateItems(query)
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Markup } from '@hcengineering/core'
import { Class, Doc, Markup, Ref } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
import textEditor, { RefAction, TextEditorHandler } from '@hcengineering/text-editor'
@@ -61,6 +61,7 @@
export let canEmbedImages = true
export let onPaste: ((view: EditorView, event: ClipboardEvent) => boolean) | undefined = undefined
export let onCancel: (() => void) | undefined = undefined
export let docClass: Ref<Class<Doc>> | undefined = undefined
const dispatch = createEventDispatcher()
const buttonSize = 'medium'
@@ -142,6 +143,8 @@
}
const completionPlugin = ReferenceExtension.configure({
...referenceConfig,
docClass,
multipleMentions: true,
showDoc (event: MouseEvent, _id: string, _class: string) {
dispatch('open-document', { event, _id, _class })
}
@@ -32,6 +32,8 @@ import workbench, { type Application } from '@hcengineering/workbench'
export interface ReferenceExtensionOptions extends ReferenceOptions {
suggestion: Omit<SuggestionOptions, 'editor'>
docClass?: Ref<Class<Doc>>
multipleMentions?: boolean
showDoc?: (event: MouseEvent, _id: string, _class: string) => void
}
@@ -96,10 +98,12 @@ export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions
this.options.HTMLAttributes,
HTMLAttributes
)
const withoutDoc = [contact.mention.Everyone, contact.mention.Here].includes(node.attrs.id)
const id = node.attrs.id
const objectclass: Ref<Class<Doc>> = node.attrs.objectclass
root.addEventListener('click', (event) => {
if (withoutDoc) return
if (event.button !== 0) return
if (broken) {
showPopup(MessageBox, {
@@ -164,7 +168,7 @@ export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions
const titleSpan = root.appendChild(document.createElement('span'))
renderLabel({ id, objectclass, label: node.attrs.label })
if (id !== undefined && objectclass !== undefined) {
if (id !== undefined && objectclass !== undefined && !withoutDoc) {
query.query(objectclass, { _id: id }, async (result) => {
const obj = result[0]
broken = obj === undefined
@@ -180,6 +184,8 @@ export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions
renderLabel({ id, objectclass, label })
}
})
} else if (withoutDoc) {
query.unsubscribe()
}
return {
@@ -228,6 +234,8 @@ export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions
return [
Suggestion({
editor: this.editor,
docClass: this.options.docClass,
multipleMentions: this.options.multipleMentions,
...this.options.suggestion
}),
// ReferenceClickHandler(this.options),
@@ -2,8 +2,8 @@ import { type Editor, type Range, escapeForRegEx } from '@tiptap/core'
import { type EditorState, Plugin, PluginKey, type Transaction } from '@tiptap/pm/state'
import { ReplaceStep } from '@tiptap/pm/transform'
import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view'
import { type ResolvedPos } from '@tiptap/pm/model'
import { type Class, type Doc, type Ref } from '@hcengineering/core'
export interface Trigger {
char: string
@@ -129,6 +129,8 @@ export interface SuggestionOptions<I = any> {
startOfLine?: boolean
decorationTag?: string
decorationClass?: string
multipleMentions?: boolean
docClass?: Ref<Class<Doc>>
command?: (props: { editor: Editor, range: Range, props: I }) => void
items?: (props: { query: string, editor: Editor }) => I[] | Promise<I[]>
render?: () => {
@@ -147,6 +149,8 @@ export interface SuggestionProps<I = any> {
range: Range
query: string
text: string
multipleMentions: boolean
docClass?: Ref<Class<Doc>>
items: I[]
command: (props: I) => void
decorationNode: Element | null
@@ -172,6 +176,8 @@ export default function Suggestion<I = any> ({
startOfLine = false,
decorationTag = 'span',
decorationClass = 'suggestion',
multipleMentions = false,
docClass,
command = () => null,
items = () => [],
render = () => ({}),
@@ -226,7 +232,9 @@ export default function Suggestion<I = any> ({
range: state.range,
query: state.query,
text: state.text,
docClass,
items: [],
multipleMentions,
command: (commandProps) => {
command({
editor,