[NEW] UiKit - Interactive UI elements for Rocket.Chat Apps (#16048)
parent
e6a8821a8d
commit
1932254338
@ -0,0 +1,13 @@ |
|||||||
|
import { Notifications } from '../../../notifications/server'; |
||||||
|
|
||||||
|
export class UiInteractionBridge { |
||||||
|
constructor(orch) { |
||||||
|
this.orch = orch; |
||||||
|
} |
||||||
|
|
||||||
|
async notifyUser(user, interaction, appId) { |
||||||
|
this.orch.debugLog(`The App ${ appId } is sending an interaction to user.`); |
||||||
|
|
||||||
|
Notifications.notifyUser(user.id, 'uiInteraction', interaction); |
||||||
|
} |
||||||
|
} |
@ -1,11 +1,6 @@ |
|||||||
import { AppMethods } from './methods'; |
import { AppMethods } from './methods'; |
||||||
import { AppsRestApi } from './rest'; |
import { AppsRestApi } from './rest'; |
||||||
import { AppEvents, AppServerNotifier, AppServerListener } from './websockets'; |
import { AppUIKitInteractionApi } from './uikit'; |
||||||
|
import { AppEvents, AppServerListener, AppServerNotifier } from './websockets'; |
||||||
|
|
||||||
export { |
export { AppUIKitInteractionApi, AppMethods, AppsRestApi, AppEvents, AppServerNotifier, AppServerListener }; |
||||||
AppMethods, |
|
||||||
AppsRestApi, |
|
||||||
AppEvents, |
|
||||||
AppServerNotifier, |
|
||||||
AppServerListener, |
|
||||||
}; |
|
||||||
|
@ -0,0 +1,159 @@ |
|||||||
|
import express from 'express'; |
||||||
|
import { WebApp } from 'meteor/webapp'; |
||||||
|
import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; |
||||||
|
|
||||||
|
import { Users } from '../../../models/server'; |
||||||
|
|
||||||
|
const apiServer = express(); |
||||||
|
|
||||||
|
apiServer.disable('x-powered-by'); |
||||||
|
|
||||||
|
WebApp.connectHandlers.use(apiServer); |
||||||
|
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const router = express.Router(); |
||||||
|
|
||||||
|
const unauthorized = (res) => |
||||||
|
res.status(401).send({ |
||||||
|
status: 'error', |
||||||
|
message: 'You must be logged in to do this.', |
||||||
|
}); |
||||||
|
|
||||||
|
router.use((req, res, next) => { |
||||||
|
const { |
||||||
|
'x-user-id': userId, |
||||||
|
'x-auth-token': authToken, |
||||||
|
} = req.headers; |
||||||
|
|
||||||
|
if (!userId || !authToken) { |
||||||
|
return unauthorized(res); |
||||||
|
} |
||||||
|
|
||||||
|
const user = Users.findOneByIdAndLoginToken(userId, authToken); |
||||||
|
if (!user) { |
||||||
|
return unauthorized(res); |
||||||
|
} |
||||||
|
|
||||||
|
req.user = user; |
||||||
|
req.userId = user._id; |
||||||
|
|
||||||
|
next(); |
||||||
|
}); |
||||||
|
|
||||||
|
apiServer.use('/api/apps/ui.interaction/', router); |
||||||
|
|
||||||
|
export class AppUIKitInteractionApi { |
||||||
|
constructor(orch) { |
||||||
|
this.orch = orch; |
||||||
|
|
||||||
|
router.post('/:appId', (req, res) => { |
||||||
|
const { |
||||||
|
appId, |
||||||
|
} = req.params; |
||||||
|
|
||||||
|
const { |
||||||
|
type, |
||||||
|
} = req.body; |
||||||
|
|
||||||
|
switch (type) { |
||||||
|
case UIKitIncomingInteractionType.BLOCK: { |
||||||
|
const { |
||||||
|
type, |
||||||
|
actionId, |
||||||
|
triggerId, |
||||||
|
mid, |
||||||
|
rid, |
||||||
|
payload, |
||||||
|
} = req.body; |
||||||
|
|
||||||
|
const room = this.orch.getConverters().get('rooms').convertById(rid); |
||||||
|
const user = this.orch.getConverters().get('users').convertToApp(req.user); |
||||||
|
const message = mid && this.orch.getConverters().get('messages').convertById(mid); |
||||||
|
|
||||||
|
const action = { |
||||||
|
type, |
||||||
|
appId, |
||||||
|
actionId, |
||||||
|
message, |
||||||
|
triggerId, |
||||||
|
payload, |
||||||
|
user, |
||||||
|
room, |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
const result = Promise.await(this.orch.getBridges().getListenerBridge().uiKitInteractionEvent('IUIKitInteractionHandler', action)); |
||||||
|
|
||||||
|
res.send(result); |
||||||
|
} catch (e) { |
||||||
|
res.status(500).send(e.message); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case UIKitIncomingInteractionType.VIEW_CLOSED: { |
||||||
|
const { |
||||||
|
type, |
||||||
|
actionId, |
||||||
|
view, |
||||||
|
isCleared, |
||||||
|
} = req.body; |
||||||
|
|
||||||
|
const user = this.orch.getConverters().get('users').convertToApp(req.user); |
||||||
|
|
||||||
|
const action = { |
||||||
|
type, |
||||||
|
appId, |
||||||
|
actionId, |
||||||
|
user, |
||||||
|
payload: { |
||||||
|
view, |
||||||
|
isCleared, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
Promise.await(this.orch.getBridges().getListenerBridge().uiKitInteractionEvent('IUIKitInteractionHandler', action)); |
||||||
|
|
||||||
|
res.send(200); |
||||||
|
} catch (e) { |
||||||
|
console.log(e); |
||||||
|
res.status(500).send(e.message); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case UIKitIncomingInteractionType.VIEW_SUBMIT: { |
||||||
|
const { |
||||||
|
type, |
||||||
|
actionId, |
||||||
|
triggerId, |
||||||
|
payload, |
||||||
|
} = req.body; |
||||||
|
|
||||||
|
const user = this.orch.getConverters().get('users').convertToApp(req.user); |
||||||
|
|
||||||
|
const action = { |
||||||
|
type, |
||||||
|
appId, |
||||||
|
actionId, |
||||||
|
triggerId, |
||||||
|
payload, |
||||||
|
user, |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
const result = Promise.await(this.orch.getBridges().getListenerBridge().uiKitInteractionEvent('IUIKitInteractionHandler', action)); |
||||||
|
|
||||||
|
res.send(result); |
||||||
|
} catch (e) { |
||||||
|
res.status(500).send(e.message); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: validate payloads per type
|
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,154 @@ |
|||||||
|
import { UIKitInteractionType, UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; |
||||||
|
import { Meteor } from 'meteor/meteor'; |
||||||
|
import { Random } from 'meteor/random'; |
||||||
|
import EventEmitter from 'wolfy87-eventemitter'; |
||||||
|
|
||||||
|
import Notifications from '../../notifications/client/lib/Notifications'; |
||||||
|
import { CachedCollectionManager } from '../../ui-cached-collection'; |
||||||
|
import { modal } from '../../ui-utils/client/lib/modal'; |
||||||
|
import { APIClient } from '../../utils'; |
||||||
|
|
||||||
|
const events = new EventEmitter(); |
||||||
|
|
||||||
|
export const on = (...args) => { |
||||||
|
events.on(...args); |
||||||
|
}; |
||||||
|
|
||||||
|
export const off = (...args) => { |
||||||
|
events.off(...args); |
||||||
|
}; |
||||||
|
|
||||||
|
const TRIGGER_TIMEOUT = 5000; |
||||||
|
|
||||||
|
const triggersId = new Map(); |
||||||
|
|
||||||
|
const instances = new Map(); |
||||||
|
|
||||||
|
const invalidateTriggerId = (id) => { |
||||||
|
const appId = triggersId.get(id); |
||||||
|
triggersId.delete(id); |
||||||
|
return appId; |
||||||
|
}; |
||||||
|
|
||||||
|
export const generateTriggerId = (appId) => { |
||||||
|
const triggerId = Random.id(); |
||||||
|
triggersId.set(triggerId, appId); |
||||||
|
setTimeout(invalidateTriggerId, TRIGGER_TIMEOUT, triggerId); |
||||||
|
return triggerId; |
||||||
|
}; |
||||||
|
|
||||||
|
const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...data }) => { |
||||||
|
if (!triggersId.has(triggerId)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
const appId = invalidateTriggerId(triggerId); |
||||||
|
if (!appId) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const { view } = data; |
||||||
|
let { viewId } = data; |
||||||
|
|
||||||
|
if (view && view.id) { |
||||||
|
viewId = view.id; |
||||||
|
} |
||||||
|
|
||||||
|
if (!viewId) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ([UIKitInteractionType.ERRORS].includes(type)) { |
||||||
|
events.emit(viewId, { |
||||||
|
type, |
||||||
|
triggerId, |
||||||
|
viewId, |
||||||
|
appId, |
||||||
|
...data, |
||||||
|
}); |
||||||
|
return UIKitInteractionType.ERRORS; |
||||||
|
} |
||||||
|
|
||||||
|
if ([UIKitInteractionType.MODAL_UPDATE].includes(type)) { |
||||||
|
events.emit(viewId, { |
||||||
|
type, |
||||||
|
triggerId, |
||||||
|
viewId, |
||||||
|
appId, |
||||||
|
...data, |
||||||
|
}); |
||||||
|
return UIKitInteractionType.MODAL_UPDATE; |
||||||
|
} |
||||||
|
|
||||||
|
if ([UIKitInteractionType.MODAL_OPEN].includes(type)) { |
||||||
|
const instance = modal.push({ |
||||||
|
template: 'ModalBlock', |
||||||
|
modifier: 'uikit', |
||||||
|
closeOnEscape: false, |
||||||
|
data: { |
||||||
|
triggerId, |
||||||
|
viewId, |
||||||
|
appId, |
||||||
|
...data, |
||||||
|
}, |
||||||
|
}); |
||||||
|
instances.set(viewId, instance); |
||||||
|
return UIKitInteractionType.MODAL_OPEN; |
||||||
|
} |
||||||
|
|
||||||
|
return UIKitInteractionType.MODAL_ClOSE; |
||||||
|
}; |
||||||
|
|
||||||
|
export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, ...rest }) => new Promise(async (resolve, reject) => { |
||||||
|
const triggerId = generateTriggerId(appId); |
||||||
|
|
||||||
|
const payload = rest.payload || rest; |
||||||
|
|
||||||
|
setTimeout(reject, TRIGGER_TIMEOUT, triggerId); |
||||||
|
|
||||||
|
const { type: interactionType, ...data } = await APIClient.post( |
||||||
|
`apps/ui.interaction/${ appId }`, |
||||||
|
{ type, actionId, payload, mid, rid, triggerId, viewId }, |
||||||
|
); |
||||||
|
|
||||||
|
return resolve(handlePayloadUserInteraction(interactionType, data)); |
||||||
|
}); |
||||||
|
|
||||||
|
export const triggerBlockAction = (options) => triggerAction({ type: UIKitIncomingInteractionType.BLOCK, ...options }); |
||||||
|
export const triggerSubmitView = async ({ viewId, ...options }) => { |
||||||
|
const close = () => { |
||||||
|
const instance = instances.get(viewId); |
||||||
|
|
||||||
|
if (instance) { |
||||||
|
instance.close(); |
||||||
|
instances.delete(viewId); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
const result = await triggerAction({ type: UIKitIncomingInteractionType.VIEW_SUBMIT, viewId, ...options }); |
||||||
|
if (!result || UIKitInteractionType.MODAL_CLOSE === result) { |
||||||
|
close(); |
||||||
|
} |
||||||
|
} catch { |
||||||
|
close(); |
||||||
|
} |
||||||
|
}; |
||||||
|
export const triggerCancel = async ({ view, ...options }) => { |
||||||
|
const instance = instances.get(view.id); |
||||||
|
try { |
||||||
|
await triggerAction({ type: UIKitIncomingInteractionType.VIEW_CLOSED, view, ...options }); |
||||||
|
} finally { |
||||||
|
if (instance) { |
||||||
|
instance.close(); |
||||||
|
instances.delete(view.id); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
Meteor.startup(() => |
||||||
|
CachedCollectionManager.onLogin(() => |
||||||
|
Notifications.onUser('uiInteraction', ({ type, ...data }) => { |
||||||
|
handlePayloadUserInteraction(type, data); |
||||||
|
}), |
||||||
|
), |
||||||
|
); |
@ -0,0 +1,3 @@ |
|||||||
|
<template name="Blocks"> |
||||||
|
<div class="js-block-wrapper"></div> |
||||||
|
</template> |
@ -0,0 +1,35 @@ |
|||||||
|
import { Template } from 'meteor/templating'; |
||||||
|
import { ReactiveVar } from 'meteor/reactive-var'; |
||||||
|
|
||||||
|
import { messageBlockWithContext } from './MessageBlock'; |
||||||
|
import './Blocks.html'; |
||||||
|
import * as ActionManager from '../ActionManager'; |
||||||
|
|
||||||
|
Template.Blocks.onRendered(async function() { |
||||||
|
const React = await import('react'); |
||||||
|
const ReactDOM = await import('react-dom'); |
||||||
|
const state = new ReactiveVar(); |
||||||
|
this.autorun(() => { |
||||||
|
state.set(Template.currentData()); |
||||||
|
}); |
||||||
|
|
||||||
|
ReactDOM.render( |
||||||
|
React.createElement(messageBlockWithContext({ |
||||||
|
action: (options) => { |
||||||
|
const { actionId, value, blockId, mid = this.data.mid } = options; |
||||||
|
ActionManager.triggerBlockAction({ actionId, appId: this.data.blocks[1].appId, value, blockId, rid: this.data.rid, mid }); |
||||||
|
}, |
||||||
|
// state: alert,
|
||||||
|
appId: this.data.appId, |
||||||
|
rid: this.data.rid, |
||||||
|
}), { data: () => state.get() }), |
||||||
|
this.firstNode, |
||||||
|
); |
||||||
|
const event = new Event('rendered'); |
||||||
|
this.firstNode.dispatchEvent(event); |
||||||
|
}); |
||||||
|
|
||||||
|
Template.Blocks.onDestroyed(async function() { |
||||||
|
const ReactDOM = await import('react-dom'); |
||||||
|
this.firstNode && ReactDOM.unmountComponentAtNode(this.firstNode); |
||||||
|
}); |
@ -0,0 +1,3 @@ |
|||||||
|
<template name="ButtonElement"> |
||||||
|
<button form="{{form}}" value="{{value}}" id="{{id}}" type="{{type}}" class="rc-button rc-button--primary"> {{>TextBlock text}} </button> |
||||||
|
</template> |
@ -0,0 +1,164 @@ |
|||||||
|
import React, { useRef, useEffect, useCallback, useMemo } from 'react'; |
||||||
|
import { UiKitMessage as uiKitMessage, kitContext, UiKitModal as uiKitModal, messageParser, modalParser, UiKitComponent } from '@rocket.chat/fuselage-ui-kit'; |
||||||
|
import { uiKitText } from '@rocket.chat/ui-kit'; |
||||||
|
import { Modal, AnimatedVisibility, ButtonGroup, Button, Box } from '@rocket.chat/fuselage'; |
||||||
|
import { useUniqueId } from '@rocket.chat/fuselage-hooks'; |
||||||
|
|
||||||
|
import { renderMessageBody } from '../../../ui-utils/client'; |
||||||
|
import { useReactiveValue } from '../../../../client/hooks/useReactiveValue'; |
||||||
|
|
||||||
|
|
||||||
|
const focusableElementsString = 'a[href]:not([tabindex="-1"]), area[href]:not([tabindex="-1"]), input:not([disabled]):not([tabindex="-1"]), select:not([disabled]):not([tabindex="-1"]), textarea:not([disabled]):not([tabindex="-1"]), button:not([disabled]):not([tabindex="-1"]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable]'; |
||||||
|
|
||||||
|
messageParser.text = ({ text, type } = {}) => { |
||||||
|
if (type !== 'mrkdwn') { |
||||||
|
return text; |
||||||
|
} |
||||||
|
|
||||||
|
return <span dangerouslySetInnerHTML={{ __html: renderMessageBody({ msg: text }) }} />; |
||||||
|
}; |
||||||
|
|
||||||
|
modalParser.text = messageParser.text; |
||||||
|
|
||||||
|
const contextDefault = { |
||||||
|
action: console.log, |
||||||
|
state: (data) => { |
||||||
|
console.log('state', data); |
||||||
|
}, |
||||||
|
}; |
||||||
|
export const messageBlockWithContext = (context) => (props) => { |
||||||
|
const data = useReactiveValue(props.data); |
||||||
|
return ( |
||||||
|
<kitContext.Provider value={context}> |
||||||
|
{uiKitMessage(data.blocks)} |
||||||
|
</kitContext.Provider> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const textParser = uiKitText(new class { |
||||||
|
plain_text({ text }) { |
||||||
|
return text; |
||||||
|
} |
||||||
|
|
||||||
|
text({ text }) { |
||||||
|
return text; |
||||||
|
} |
||||||
|
}()); |
||||||
|
const thumb = 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=='; |
||||||
|
|
||||||
|
// https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html
|
||||||
|
|
||||||
|
export const modalBlockWithContext = ({ |
||||||
|
view: { |
||||||
|
title, |
||||||
|
close, |
||||||
|
submit, |
||||||
|
}, |
||||||
|
onSubmit, |
||||||
|
onClose, |
||||||
|
onCancel, |
||||||
|
...context |
||||||
|
}) => (props) => { |
||||||
|
const id = `modal_id_${ useUniqueId() }`; |
||||||
|
|
||||||
|
const { view, ...data } = useReactiveValue(props.data); |
||||||
|
const ref = useRef(); |
||||||
|
|
||||||
|
// Auto focus
|
||||||
|
useEffect(() => ref.current && ref.current.querySelector(focusableElementsString).focus(), [ref.current]); |
||||||
|
// save fovus to restore after close
|
||||||
|
const previousFocus = useMemo(() => document.activeElement, []); |
||||||
|
// restore the focus after the component unmount
|
||||||
|
useEffect(() => () => previousFocus && previousFocus.focus(), []); |
||||||
|
// Handle Tab, Shift + Tab, Enter and Escape
|
||||||
|
const handleKeyUp = useCallback((event) => { |
||||||
|
if (event.keyCode === 13) { // ENTER
|
||||||
|
return onSubmit(); |
||||||
|
} |
||||||
|
|
||||||
|
if (event.keyCode === 27) { // ESC
|
||||||
|
event.stopPropagation(); |
||||||
|
event.preventDefault(); |
||||||
|
onClose(); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (event.keyCode === 9) { // TAB
|
||||||
|
const elements = Array.from(ref.current.querySelectorAll(focusableElementsString)); |
||||||
|
const [first] = elements; |
||||||
|
const last = elements.pop(); |
||||||
|
|
||||||
|
if (!ref.current.contains(document.activeElement)) { |
||||||
|
return first.focus(); |
||||||
|
} |
||||||
|
|
||||||
|
if (event.shiftKey) { |
||||||
|
if (!first || first === document.activeElement) { |
||||||
|
last.focus(); |
||||||
|
event.stopPropagation(); |
||||||
|
event.preventDefault(); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!last || last === document.activeElement) { |
||||||
|
first.focus(); |
||||||
|
event.stopPropagation(); |
||||||
|
event.preventDefault(); |
||||||
|
} |
||||||
|
} |
||||||
|
}, [onSubmit]); |
||||||
|
// Clean the events
|
||||||
|
useEffect(() => { |
||||||
|
const close = (e) => { |
||||||
|
e.preventDefault(); |
||||||
|
e.stopPropagation(); |
||||||
|
onClose(); |
||||||
|
return false; |
||||||
|
}; |
||||||
|
const element = document.querySelector('.rc-modal-wrapper'); |
||||||
|
document.addEventListener('keydown', handleKeyUp); |
||||||
|
element.addEventListener('click', close); |
||||||
|
return () => { |
||||||
|
document.removeEventListener('keydown', handleKeyUp); |
||||||
|
element.removeEventListener('click', close); |
||||||
|
}; |
||||||
|
}, handleKeyUp); |
||||||
|
|
||||||
|
return ( |
||||||
|
<kitContext.Provider value={{ ...context, ...data }}> |
||||||
|
<AnimatedVisibility visibility={AnimatedVisibility.UNHIDING}> |
||||||
|
<Modal open id={id} ref={ref}> |
||||||
|
<Modal.Header> |
||||||
|
{/* <Modal.Thumb url={`api/apps/${ context.appId }/icon`} /> */} |
||||||
|
<Modal.Thumb url={thumb} /> |
||||||
|
<Modal.Title>{textParser([title])}</Modal.Title> |
||||||
|
<Modal.Close tabIndex={-1} onClick={onClose} /> |
||||||
|
</Modal.Header> |
||||||
|
<Modal.Content> |
||||||
|
<Box |
||||||
|
is='form' |
||||||
|
method='post' |
||||||
|
action='#' |
||||||
|
onSubmit={onSubmit} |
||||||
|
> |
||||||
|
<UiKitComponent render={uiKitModal} blocks={view.blocks} /> |
||||||
|
</Box> |
||||||
|
</Modal.Content> |
||||||
|
<Modal.Footer> |
||||||
|
<ButtonGroup align='end'> |
||||||
|
<Button onClick={onCancel}>{textParser([close.text])}</Button> |
||||||
|
<Button primary onClick={onSubmit}>{textParser([submit.text])}</Button> |
||||||
|
</ButtonGroup> |
||||||
|
</Modal.Footer> |
||||||
|
</Modal> |
||||||
|
</AnimatedVisibility> |
||||||
|
</kitContext.Provider> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export const MessageBlock = ({ blocks }, context = contextDefault) => ( |
||||||
|
<kitContext.Provider value={context}> |
||||||
|
{uiKitMessage(blocks)} |
||||||
|
</kitContext.Provider> |
||||||
|
); |
@ -0,0 +1,3 @@ |
|||||||
|
<template name="ModalBlock"> |
||||||
|
<div class="js-modal-block"></div> |
||||||
|
</template> |
@ -0,0 +1,103 @@ |
|||||||
|
import { Template } from 'meteor/templating'; |
||||||
|
import { ReactiveDict } from 'meteor/reactive-dict'; |
||||||
|
import { ReactiveVar } from 'meteor/reactive-var'; |
||||||
|
|
||||||
|
import { modalBlockWithContext } from './MessageBlock'; |
||||||
|
import './ModalBlock.html'; |
||||||
|
import * as ActionManager from '../ActionManager'; |
||||||
|
|
||||||
|
Template.ModalBlock.onRendered(async function() { |
||||||
|
const React = await import('react'); |
||||||
|
const ReactDOM = await import('react-dom'); |
||||||
|
const state = new ReactiveVar(); |
||||||
|
|
||||||
|
const { viewId, appId } = this.data; |
||||||
|
|
||||||
|
this.autorun(() => { |
||||||
|
state.set(Template.currentData()); |
||||||
|
}); |
||||||
|
|
||||||
|
const handleUpdate = ({ type, ...data }) => { |
||||||
|
if (type === 'errors') { |
||||||
|
return state.set({ ...state.get(), errors: data.errors }); |
||||||
|
} |
||||||
|
return state.set(data); |
||||||
|
}; |
||||||
|
|
||||||
|
this.cancel = () => { |
||||||
|
ActionManager.off(viewId, handleUpdate); |
||||||
|
}; |
||||||
|
|
||||||
|
this.node = this.find('.js-modal-block').parentElement; |
||||||
|
ActionManager.on(viewId, handleUpdate); |
||||||
|
|
||||||
|
const filterInputFields = ({ type, element }) => type === 'input' && element.initialValue; |
||||||
|
const mapElementToState = ({ element, blockId }) => [element.actionId, { value: element.initialValue, blockId }]; |
||||||
|
const groupStateByBlockIdMap = (obj, [key, { blockId, value }]) => { |
||||||
|
obj[blockId] = obj[blockId] || {}; |
||||||
|
obj[blockId][key] = value; |
||||||
|
return obj; |
||||||
|
}; |
||||||
|
const groupStateByBlockId = (obj) => Object.entries(obj).reduce(groupStateByBlockIdMap, {}); |
||||||
|
|
||||||
|
this.state = new ReactiveDict(Object.fromEntries(this.data.view.blocks.filter(filterInputFields).map(mapElementToState))); |
||||||
|
|
||||||
|
ReactDOM.render( |
||||||
|
React.createElement( |
||||||
|
modalBlockWithContext({ |
||||||
|
onCancel: () => ActionManager.triggerCancel({ |
||||||
|
appId, |
||||||
|
viewId, |
||||||
|
view: { |
||||||
|
...this.data.view, |
||||||
|
id: viewId, |
||||||
|
state: groupStateByBlockId(this.state.all()), |
||||||
|
}, |
||||||
|
}), |
||||||
|
onClose: () => ActionManager.triggerCancel({ |
||||||
|
appId, |
||||||
|
viewId, |
||||||
|
view: { |
||||||
|
...this.data.view, |
||||||
|
id: viewId, |
||||||
|
state: groupStateByBlockId(this.state.all()), |
||||||
|
}, |
||||||
|
isCleared: true, |
||||||
|
}), |
||||||
|
onSubmit: () => ActionManager.triggerSubmitView({ |
||||||
|
viewId, |
||||||
|
appId, |
||||||
|
payload: { |
||||||
|
view: { |
||||||
|
...this.data.view, |
||||||
|
id: viewId, |
||||||
|
state: groupStateByBlockId(this.state.all()), |
||||||
|
}, |
||||||
|
}, |
||||||
|
}), |
||||||
|
action: ({ actionId, appId, value, blockId, mid = this.data.mid }) => { |
||||||
|
ActionManager.triggerBlockAction({ |
||||||
|
actionId, |
||||||
|
appId, |
||||||
|
value, |
||||||
|
blockId, |
||||||
|
mid, |
||||||
|
}); |
||||||
|
}, |
||||||
|
state: ({ actionId, value, /* ,appId, */ blockId = 'default' }) => { |
||||||
|
this.state.set(actionId, { |
||||||
|
blockId, |
||||||
|
value, |
||||||
|
}); |
||||||
|
}, |
||||||
|
...this.data, |
||||||
|
}), |
||||||
|
{ data: () => state.get() }, |
||||||
|
), |
||||||
|
this.node, |
||||||
|
); |
||||||
|
}); |
||||||
|
Template.ModalBlock.onDestroyed(async function() { |
||||||
|
const ReactDOM = await import('react-dom'); |
||||||
|
this.node && ReactDOM.unmountComponentAtNode(this.node); |
||||||
|
}); |
@ -0,0 +1,12 @@ |
|||||||
|
<!-- { |
||||||
|
"type": "button", |
||||||
|
"text": { |
||||||
|
"type": "plain_text", |
||||||
|
"text": "Button", |
||||||
|
"emoji": true |
||||||
|
} |
||||||
|
}, |
||||||
|
--> |
||||||
|
<template name="TextBlock" arguments="type text emoji"> |
||||||
|
{{text.text}} |
||||||
|
</template> |
@ -0,0 +1,5 @@ |
|||||||
|
|
||||||
|
|
||||||
|
// import { Template } from 'meteor/templating';
|
||||||
|
|
||||||
|
import './TextBlock.html'; |
@ -0,0 +1,5 @@ |
|||||||
|
import './styles.css'; |
||||||
|
import './Blocks.js'; |
||||||
|
import './ModalBlock'; |
||||||
|
import './TextBlock'; |
||||||
|
import './ButtonElement.html'; |
@ -0,0 +1,18 @@ |
|||||||
|
.block-kit-debug.debug { |
||||||
|
padding: 1rem; |
||||||
|
|
||||||
|
border: 1px solid; |
||||||
|
} |
||||||
|
|
||||||
|
.block-kit-debug legend { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
.block-kit-debug.debug legend { |
||||||
|
display: initial; |
||||||
|
} |
||||||
|
|
||||||
|
.rc-modal--uikit { |
||||||
|
width: 680px; |
||||||
|
max-width: 100%; |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue