platform/plugins/view-resources/src/actionImpl.ts
Andrey Sobolev 7621bccf22
UBER-1074: Svelte 4 (#4014)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
2023-11-20 17:01:43 +07:00

497 lines
13 KiB
TypeScript

import {
type Class,
type Doc,
type DocumentQuery,
Hierarchy,
type Ref,
type Space,
type TxResult
} from '@hcengineering/core'
import { type Asset, type IntlString, type Resource, getResource } from '@hcengineering/platform'
import { MessageBox, getClient, updateAttribute, type ContextStore, contextStore } from '@hcengineering/presentation'
import {
type AnyComponent,
type AnySvelteComponent,
type PopupAlignment,
type PopupPosAlignment,
closeTooltip,
isPopupPosAlignment,
navigate,
showPanel,
showPopup
} from '@hcengineering/ui'
import MoveView from './components/Move.svelte'
import view from './plugin'
import {
type FocusSelection,
type SelectDirection,
type SelectionStore,
focusStore,
previewDocument,
selectionStore
} from './selection'
import { deleteObjects, getObjectLinkFragment } from './utils'
import contact from '@hcengineering/contact'
/**
* Action to be used for copying text to clipboard.
* In Safari a request to write to the clipboard must be triggered during a user gesture.
* A call to clipboard.write or clipboard.writeText outside the scope of a user
* gesture(such as "click" or "touch" event handlers) will result in the immediate
* rejection of the promise returned by the API call.
* https://webkit.org/blog/10855/async-clipboard-api/
*
* * Require props:
* - textProvider - a function that provides text to be copied.
* - props - additional text provider props.
*/
async function CopyTextToClipboard (
doc: Doc | Doc[],
evt: Event,
props: {
textProvider: Resource<(doc: Doc, props?: Record<string, any>) => Promise<string>>
props?: Record<string, any>
}
): Promise<void> {
const getText = await getResource(props.textProvider)
try {
// Safari specific behavior
// see https://bugs.webkit.org/show_bug.cgi?id=222262
const text = Array.isArray(doc)
? (await Promise.all(doc.map(async (d) => await getText(d, props.props)))).join(',')
: await getText(doc, props.props)
const clipboardItem = new ClipboardItem({
'text/plain': text
})
await navigator.clipboard.write([clipboardItem])
} catch {
// Fallback to default clipboard API implementation
const text = Array.isArray(doc)
? (await Promise.all(doc.map(async (d) => await getText(d, props.props)))).join(',')
: await getText(doc, props.props)
await navigator.clipboard.writeText(text)
}
}
function Delete (object: Doc | Doc[]): void {
showPopup(
contact.component.DeleteConfirmationPopup,
{
object,
deleteAction: async () => {
const objs = Array.isArray(object) ? object : [object]
await deleteObjects(getClient(), objs).catch((err) => {
console.error(err)
})
}
},
undefined
)
}
function Archive (object: Space | Space[]): void {
showPopup(
MessageBox,
{
label: view.string.Archive,
message: view.string.ArchiveConfirm,
params: { count: Array.isArray(object) ? object.length : 1 },
action: async () => {
const objs = Array.isArray(object) ? object : [object]
const client = getClient()
const promises: Array<Promise<TxResult>> = []
for (const obj of objs) {
promises.push(client.update(obj, { archived: true }))
}
await Promise.all(promises)
}
},
undefined
)
}
async function Move (docs: Doc | Doc[]): Promise<void> {
showPopup(MoveView, { selected: docs })
}
let $focusStore: FocusSelection
focusStore.subscribe((it) => {
$focusStore = it
})
let $contextStore: ContextStore
contextStore.subscribe((it) => {
$contextStore = it
})
let $selectionStore: SelectionStore
selectionStore.subscribe((it) => {
$selectionStore = it
})
export function select (
evt: Event | undefined,
offset: 1 | -1 | 0,
of?: Doc,
direction?: SelectDirection,
noScroll?: boolean
): void {
closeTooltip()
if ($focusStore.provider?.select !== undefined) {
$focusStore.provider?.select(offset, of, direction, noScroll)
evt?.preventDefault()
previewDocument.update((old) => {
if (old !== undefined) {
return $focusStore.focus
}
})
}
}
function SelectItem (doc: Doc | Doc[] | undefined, evt: Event): void {
const focus = $focusStore.focus
const provider = $selectionStore.provider ?? $focusStore.provider
if (focus !== undefined) {
provider?.selection.update((selection) => {
const ind = selection.findIndex((it) => it._id === focus._id)
if (ind === -1) {
selection.push(focus)
} else {
selection.splice(ind, 1)
}
return selection
})
}
evt.preventDefault()
}
function SelectItemNone (doc: Doc | undefined, evt: Event): void {
const provider = $selectionStore.provider ?? $focusStore.provider
if (provider !== undefined) {
provider.selection.set([])
previewDocument.set(undefined)
evt.preventDefault()
}
}
function SelectItemAll (doc: Doc | undefined, evt: Event): void {
const provider = $selectionStore.provider ?? $focusStore.provider
if (provider !== undefined) {
const docs = provider.docs() ?? []
provider.selection.set(docs)
previewDocument.set(undefined)
evt.preventDefault()
}
}
const MoveUp = (doc: Doc | undefined, evt: Event): void => {
select(evt, -1, $focusStore.focus, 'vertical')
}
const MoveDown = (doc: Doc | undefined, evt: Event): void => {
select(evt, 1, $focusStore.focus, 'vertical')
}
const MoveLeft = (doc: Doc | undefined, evt: Event): void => {
select(evt, -1, $focusStore.focus, 'horizontal')
}
const MoveRight = (doc: Doc | undefined, evt: Event): void => {
select(evt, 1, $focusStore.focus, 'horizontal')
}
function ShowActions (doc: Doc | Doc[] | undefined, evt: Event): void {
evt.preventDefault()
showPopup(view.component.ActionsPopup, { viewContext: $contextStore.getLastContext() }, 'top')
}
function ShowPreview (doc: Doc | Doc[] | undefined, evt: Event): void {
previewDocument.update((old) => {
const d = Array.isArray(doc) ? doc[0] : doc
if (old?._id === d?._id) {
return undefined
}
return d
})
evt.preventDefault()
}
async function Open (
doc: Doc,
evt: Event,
props:
| {
component?: AnyComponent
}
| undefined
): Promise<void> {
evt.preventDefault()
const d = Array.isArray(doc) ? doc[0] : doc
const client = getClient()
const hierarchy = client.getHierarchy()
const panelComponent = hierarchy.classHierarchyMixin(d._class, view.mixin.ObjectPanel)
const component = props?.component ?? panelComponent?.component ?? view.component.EditDoc
const loc = await getObjectLinkFragment(hierarchy, d, {}, component)
navigate(loc)
}
/**
* Quick action for show panel
* Require props:
* - component - view.component.EditDoc or another component
* - element - position
* - right - some right component
*/
function ShowPanel (
doc: Doc | Doc[],
evt: Event,
props: {
component?: AnyComponent
element: PopupPosAlignment
rightSection?: AnyComponent
}
): void {
const d = Array.isArray(doc) ? doc[0] : doc
evt.preventDefault()
showPanel(
props.component ?? view.component.EditDoc,
d._id,
Hierarchy.mixinOrClass(d),
props.element ?? 'content',
props.rightSection
)
}
/**
* Quick action for show popup
* Props:
* - _id - object id will be placed into
* - _class - object _class will be placed into
* - value - object itself will be placed into
* - values - all docs will be placed into
* - props - some basic props, will be merged with key, _class, value, values
*/
async function ShowPopup (
doc: Doc | Doc[],
evt: Event,
props: {
component: AnyComponent
element?: PopupPosAlignment | Resource<(e?: Event) => PopupAlignment | undefined>
_id?: string
_class?: string
_space?: string
value?: string
values?: string
props?: Record<string, any>
fillProps?: Record<string, string>
}
): Promise<void> {
const docs = Array.isArray(doc) ? doc : doc !== undefined ? [doc] : []
const element = await getPopupAlignment(props.element, evt)
evt.preventDefault()
const cprops = {
...(props?.props ?? {})
}
for (const [docKey, propKey] of Object.entries(props.fillProps ?? {})) {
for (const dv of docs) {
const dvv = (dv as any)[docKey]
if (dvv !== undefined) {
;(cprops as any)[propKey] = dvv
}
}
if (docKey === '_object') {
;(cprops as any)[propKey] = docs[0]
} else if (docKey === '_objects') {
;(cprops as any)[propKey] = docs.length === 1 ? docs[0] : docs
}
}
showPopup(props.component, cprops, element)
}
/**
* Quick action for show popup
* Props:
* - attribute - to show editor for specific attribute
* - props - some basic props, will be merged with key, _class, value, values
*/
async function ShowEditor (
doc: Doc | Doc[],
evt: Event,
props: {
element?: PopupPosAlignment | Resource<(e?: Event) => PopupAlignment | undefined>
attribute: string
props?: Record<string, any>
}
): Promise<void> {
const docs = Array.isArray(doc) ? doc : doc !== undefined ? [doc] : []
evt.preventDefault()
let cprops = {
...(props?.props ?? {})
}
if (docs.length === 1) {
const client = getClient()
const hierarchy = client.getHierarchy()
const doc: Doc = docs[0]
const attribute = hierarchy.getAttribute(doc._class, props.attribute)
const typeClass = hierarchy.getClass(attribute.type._class)
const attributeEditorMixin = hierarchy.as(typeClass, view.mixin.AttributeEditor)
if (attributeEditorMixin?.popup === undefined) {
throw new Error(`failed to find editor popup for ${typeClass._id}`)
}
const editor: AnySvelteComponent = await getResource(attributeEditorMixin.popup)
cprops = {
...cprops,
...{
value: (doc as any)[props.attribute]
}
}
if (editor !== undefined) {
console.log('EVT', evt)
showPopup(
editor,
cprops,
{
getBoundingClientRect: () => new DOMRect((evt as MouseEvent).clientX, (evt as MouseEvent).clientY)
},
(result) => {
if (result != null) {
void updateAttribute(client, doc, doc._class, { key: props.attribute, attr: attribute }, result)
}
}
)
}
}
}
function UpdateDocument (doc: Doc | Doc[], evt: Event, props: Record<string, any>): void {
async function update (): Promise<void> {
if (props?.key !== undefined && props?.value !== undefined) {
if (Array.isArray(doc)) {
for (const d of doc) {
await getClient().update(d, { [props.key]: props.value })
}
} else {
await getClient().update(doc, { [props.key]: props.value })
}
}
}
if (props?.ask === true) {
showPopup(
MessageBox,
{
label: props.label ?? view.string.LabelYes,
message: props.message ?? view.string.LabelYes
},
undefined,
(result: boolean) => {
if (result) {
void update()
}
}
)
} else {
void update()
}
}
function ValueSelector (
doc: Doc | Doc[],
evt: Event,
props: {
actionPopup: AnyComponent
attribute: string
// Class object finder
_class?: Ref<Class<Doc>>
query?: DocumentQuery<Doc>
// Will copy values from selection document to query
// If set of docs passed, will do $in for values.
fillQuery?: Record<string, string>
// A list of fields with matched values to perform action.
docMatches?: string[]
searchField?: string
// Or list of values to select from
values?: Array<{ icon?: Asset, label: IntlString, id: number | string }>
placeholder?: IntlString
}
): void {
showPopup(view.component.ValueSelector, { ...props, value: doc, width: 'large' }, 'top')
}
function AttributeSelector (
doc: Doc | Doc[],
evt: Event,
props: {
actionPopup: AnyComponent
attribute: string
values?: Array<{ icon?: Asset, label: IntlString, id: number | string }>
isAction?: boolean
valueKey?: string
}
): void {
const client = getClient()
const hierarchy = client.getHierarchy()
const docArray = Array.isArray(doc) ? doc : [doc]
const attribute = hierarchy.getAttribute(docArray[0]._class, props.attribute)
showPopup(props.actionPopup, { ...props, [props.valueKey ?? 'value']: docArray, width: 'large' }, 'top', (result) => {
if (result != null) {
for (const docEl of docArray) {
void updateAttribute(client, docEl, docEl._class, { key: props.attribute, attr: attribute }, result)
}
}
})
}
async function getPopupAlignment (
element?: PopupPosAlignment | Resource<(e?: Event) => PopupAlignment | undefined>,
evt?: Event
): Promise<PopupAlignment | undefined> {
if (element === undefined) {
return undefined
}
if (isPopupPosAlignment(element)) {
return element
}
try {
const alignmentGetter: (e?: Event) => PopupAlignment | undefined = await getResource(element)
return alignmentGetter(evt)
} catch (e) {
return element as PopupAlignment
}
}
/**
* @public
*/
export const actionImpl = {
CopyTextToClipboard,
Delete,
Archive,
Move,
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
SelectItem,
SelectItemNone,
SelectItemAll,
ShowActions,
ShowPreview,
Open,
UpdateDocument,
ShowPanel,
ShowPopup,
ShowEditor,
ValueSelector,
AttributeSelector
}