refactor(uikit): uikit interactions (#30534)
parent
fff548fe8a
commit
7b02ca3b4d
@ -1,261 +0,0 @@ |
||||
import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; |
||||
import { UIKitInteractionTypes } from '@rocket.chat/core-typings'; |
||||
import { Emitter } from '@rocket.chat/emitter'; |
||||
import { Random } from '@rocket.chat/random'; |
||||
import { lazy } from 'react'; |
||||
|
||||
import * as banners from '../../../client/lib/banners'; |
||||
import { imperativeModal } from '../../../client/lib/imperativeModal'; |
||||
import { dispatchToastMessage } from '../../../client/lib/toast'; |
||||
import { router } from '../../../client/providers/RouterProvider'; |
||||
import { sdk } from '../../utils/client/lib/SDKClient'; |
||||
import { t } from '../../utils/lib/i18n'; |
||||
|
||||
const UiKitModal = lazy(() => import('../../../client/views/modal/uikit/UiKitModal')); |
||||
|
||||
export const events = new Emitter(); |
||||
|
||||
export const on = (...args) => { |
||||
events.on(...args); |
||||
}; |
||||
|
||||
export const off = (...args) => { |
||||
events.off(...args); |
||||
}; |
||||
|
||||
const TRIGGER_TIMEOUT = 5000; |
||||
|
||||
const TRIGGER_TIMEOUT_ERROR = 'TRIGGER_TIMEOUT_ERROR'; |
||||
|
||||
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; |
||||
}; |
||||
|
||||
export 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 ([UIKitInteractionTypes.ERRORS].includes(type)) { |
||||
events.emit(viewId, { |
||||
type, |
||||
triggerId, |
||||
viewId, |
||||
appId, |
||||
...data, |
||||
}); |
||||
return UIKitInteractionTypes.ERRORS; |
||||
} |
||||
|
||||
if ( |
||||
[UIKitInteractionTypes.BANNER_UPDATE, UIKitInteractionTypes.MODAL_UPDATE, UIKitInteractionTypes.CONTEXTUAL_BAR_UPDATE].includes(type) |
||||
) { |
||||
events.emit(viewId, { |
||||
type, |
||||
triggerId, |
||||
viewId, |
||||
appId, |
||||
...data, |
||||
}); |
||||
return type; |
||||
} |
||||
|
||||
if ([UIKitInteractionTypes.MODAL_OPEN].includes(type)) { |
||||
const instance = imperativeModal.open({ |
||||
component: UiKitModal, |
||||
props: { |
||||
triggerId, |
||||
viewId, |
||||
appId, |
||||
...data, |
||||
}, |
||||
}); |
||||
|
||||
instances.set(viewId, { |
||||
close() { |
||||
instance.close(); |
||||
instances.delete(viewId); |
||||
}, |
||||
}); |
||||
|
||||
return UIKitInteractionTypes.MODAL_OPEN; |
||||
} |
||||
|
||||
if ([UIKitInteractionTypes.CONTEXTUAL_BAR_OPEN].includes(type)) { |
||||
instances.set(viewId, { |
||||
payload: { |
||||
type, |
||||
triggerId, |
||||
appId, |
||||
viewId, |
||||
...data, |
||||
}, |
||||
close() { |
||||
instances.delete(viewId); |
||||
}, |
||||
}); |
||||
|
||||
router.navigate({ |
||||
name: router.getRouteName(), |
||||
params: { |
||||
...router.getRouteParameters(), |
||||
tab: 'app', |
||||
context: viewId, |
||||
}, |
||||
}); |
||||
|
||||
return UIKitInteractionTypes.CONTEXTUAL_BAR_OPEN; |
||||
} |
||||
|
||||
if ([UIKitInteractionTypes.BANNER_OPEN].includes(type)) { |
||||
banners.open(data); |
||||
instances.set(viewId, { |
||||
close() { |
||||
banners.closeById(viewId); |
||||
}, |
||||
}); |
||||
return UIKitInteractionTypes.BANNER_OPEN; |
||||
} |
||||
|
||||
if ([UIKitIncomingInteractionType.BANNER_CLOSE].includes(type)) { |
||||
const instance = instances.get(viewId); |
||||
|
||||
if (instance) { |
||||
instance.close(); |
||||
} |
||||
return UIKitIncomingInteractionType.BANNER_CLOSE; |
||||
} |
||||
|
||||
if ([UIKitIncomingInteractionType.CONTEXTUAL_BAR_CLOSE].includes(type)) { |
||||
const instance = instances.get(viewId); |
||||
|
||||
if (instance) { |
||||
instance.close(); |
||||
} |
||||
return UIKitIncomingInteractionType.CONTEXTUAL_BAR_CLOSE; |
||||
} |
||||
|
||||
return UIKitInteractionTypes.MODAL_ClOSE; |
||||
}; |
||||
|
||||
export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, container, tmid, ...rest }) => |
||||
new Promise(async (resolve, reject) => { |
||||
events.emit('busy', { busy: true }); |
||||
|
||||
const triggerId = generateTriggerId(appId); |
||||
|
||||
const payload = rest.payload || rest; |
||||
|
||||
setTimeout(reject, TRIGGER_TIMEOUT, [TRIGGER_TIMEOUT_ERROR, { triggerId, appId }]); |
||||
|
||||
const { type: interactionType, ...data } = await (async () => { |
||||
try { |
||||
return await sdk.rest.post(`/apps/ui.interaction/${appId}`, { |
||||
type, |
||||
actionId, |
||||
payload, |
||||
container, |
||||
mid, |
||||
rid, |
||||
tmid, |
||||
triggerId, |
||||
viewId, |
||||
}); |
||||
} catch (e) { |
||||
reject(e); |
||||
return {}; |
||||
} finally { |
||||
events.emit('busy', { busy: false }); |
||||
} |
||||
})(); |
||||
|
||||
return resolve(handlePayloadUserInteraction(interactionType, data)); |
||||
}); |
||||
|
||||
export const triggerBlockAction = (options) => triggerAction({ type: UIKitIncomingInteractionType.BLOCK, ...options }); |
||||
|
||||
export const triggerActionButtonAction = (options) => |
||||
triggerAction({ type: UIKitIncomingInteractionType.ACTION_BUTTON, ...options }).catch(async (reason) => { |
||||
if (Array.isArray(reason) && reason[0] === TRIGGER_TIMEOUT_ERROR) { |
||||
dispatchToastMessage({ |
||||
type: 'error', |
||||
message: t('UIKit_Interaction_Timeout'), |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
export const triggerSubmitView = async ({ viewId, ...options }) => { |
||||
const close = () => { |
||||
const instance = instances.get(viewId); |
||||
|
||||
if (instance) { |
||||
instance.close(); |
||||
} |
||||
}; |
||||
|
||||
try { |
||||
const result = await triggerAction({ |
||||
type: UIKitIncomingInteractionType.VIEW_SUBMIT, |
||||
viewId, |
||||
...options, |
||||
}); |
||||
if (!result || UIKitInteractionTypes.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(); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
export const getUserInteractionPayloadByViewId = (viewId) => { |
||||
if (!viewId) { |
||||
throw new Error('No viewId provided when checking for `user interaction payload`'); |
||||
} |
||||
|
||||
const instance = instances.get(viewId); |
||||
|
||||
if (!instance) { |
||||
return {}; |
||||
} |
||||
|
||||
return instance.payload; |
||||
}; |
||||
@ -0,0 +1,237 @@ |
||||
import type { DistributiveOmit, UiKit } from '@rocket.chat/core-typings'; |
||||
import { Emitter } from '@rocket.chat/emitter'; |
||||
import { Random } from '@rocket.chat/random'; |
||||
import type { ActionManagerContext, RouterContext } from '@rocket.chat/ui-contexts'; |
||||
import type { ContextType } from 'react'; |
||||
import { lazy } from 'react'; |
||||
|
||||
import * as banners from '../../../client/lib/banners'; |
||||
import { imperativeModal } from '../../../client/lib/imperativeModal'; |
||||
import { router } from '../../../client/providers/RouterProvider'; |
||||
import { sdk } from '../../utils/client/lib/SDKClient'; |
||||
import { UiKitTriggerTimeoutError } from './UiKitTriggerTimeoutError'; |
||||
|
||||
const UiKitModal = lazy(() => import('../../../client/views/modal/uikit/UiKitModal')); |
||||
|
||||
type ActionManagerType = Exclude<ContextType<typeof ActionManagerContext>, undefined>; |
||||
|
||||
export class ActionManager implements ActionManagerType { |
||||
protected static TRIGGER_TIMEOUT = 5000; |
||||
|
||||
protected static TRIGGER_TIMEOUT_ERROR = 'TRIGGER_TIMEOUT_ERROR'; |
||||
|
||||
protected events = new Emitter<{ busy: { busy: boolean }; [viewId: string]: any }>(); |
||||
|
||||
protected triggersId = new Map<string, string | undefined>(); |
||||
|
||||
protected viewInstances = new Map< |
||||
string, |
||||
{ |
||||
payload?: { |
||||
view: UiKit.ContextualBarView; |
||||
}; |
||||
close: () => void; |
||||
} |
||||
>(); |
||||
|
||||
public constructor(protected router: ContextType<typeof RouterContext>) {} |
||||
|
||||
protected invalidateTriggerId(id: string) { |
||||
const appId = this.triggersId.get(id); |
||||
this.triggersId.delete(id); |
||||
return appId; |
||||
} |
||||
|
||||
public on(viewId: string, listener: (data: any) => void): void; |
||||
|
||||
public on(eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void; |
||||
|
||||
public on(eventName: string, listener: (data: any) => void) { |
||||
return this.events.on(eventName, listener); |
||||
} |
||||
|
||||
public off(viewId: string, listener: (data: any) => any): void; |
||||
|
||||
public off(eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void; |
||||
|
||||
public off(eventName: string, listener: (data: any) => void) { |
||||
return this.events.off(eventName, listener); |
||||
} |
||||
|
||||
public generateTriggerId(appId: string | undefined) { |
||||
const triggerId = Random.id(); |
||||
this.triggersId.set(triggerId, appId); |
||||
setTimeout(() => this.invalidateTriggerId(triggerId), ActionManager.TRIGGER_TIMEOUT); |
||||
return triggerId; |
||||
} |
||||
|
||||
public async emitInteraction(appId: string, userInteraction: DistributiveOmit<UiKit.UserInteraction, 'triggerId'>) { |
||||
this.events.emit('busy', { busy: true }); |
||||
|
||||
const triggerId = this.generateTriggerId(appId); |
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined; |
||||
|
||||
await Promise.race([ |
||||
new Promise((_, reject) => { |
||||
timeout = setTimeout(() => reject(new UiKitTriggerTimeoutError('Timeout', { triggerId, appId })), ActionManager.TRIGGER_TIMEOUT); |
||||
}), |
||||
sdk.rest |
||||
.post(`/apps/ui.interaction/${appId}`, { |
||||
...userInteraction, |
||||
triggerId, |
||||
}) |
||||
.then((interaction) => this.handleServerInteraction(interaction)), |
||||
]).finally(() => { |
||||
if (timeout) clearTimeout(timeout); |
||||
this.events.emit('busy', { busy: false }); |
||||
}); |
||||
} |
||||
|
||||
public handleServerInteraction(interaction: UiKit.ServerInteraction) { |
||||
const { triggerId } = interaction; |
||||
|
||||
if (!this.triggersId.has(triggerId)) { |
||||
return; |
||||
} |
||||
|
||||
const appId = this.invalidateTriggerId(triggerId); |
||||
if (!appId) { |
||||
return; |
||||
} |
||||
|
||||
switch (interaction.type) { |
||||
case 'errors': { |
||||
const { type, triggerId, viewId, appId, errors } = interaction; |
||||
this.events.emit(interaction.viewId, { |
||||
type, |
||||
triggerId, |
||||
viewId, |
||||
appId, |
||||
errors, |
||||
}); |
||||
break; |
||||
} |
||||
|
||||
case 'modal.open': { |
||||
const { view } = interaction; |
||||
const instance = imperativeModal.open({ |
||||
component: UiKitModal, |
||||
props: { |
||||
key: view.id, |
||||
initialView: interaction.view, |
||||
}, |
||||
}); |
||||
|
||||
this.viewInstances.set(view.id, { |
||||
close: () => { |
||||
instance.close(); |
||||
this.viewInstances.delete(view.id); |
||||
}, |
||||
}); |
||||
break; |
||||
} |
||||
|
||||
case 'modal.update': |
||||
case 'contextual_bar.update': { |
||||
const { type, triggerId, appId, view } = interaction; |
||||
this.events.emit(view.id, { |
||||
type, |
||||
triggerId, |
||||
viewId: view.id, |
||||
appId, |
||||
view, |
||||
}); |
||||
break; |
||||
} |
||||
|
||||
case 'modal.close': { |
||||
break; |
||||
} |
||||
|
||||
case 'banner.open': { |
||||
const { type, triggerId, ...view } = interaction; |
||||
banners.open(view); |
||||
this.viewInstances.set(view.viewId, { |
||||
close: () => { |
||||
banners.closeById(view.viewId); |
||||
}, |
||||
}); |
||||
break; |
||||
} |
||||
|
||||
case 'banner.update': { |
||||
const { type, triggerId, appId, view } = interaction; |
||||
this.events.emit(view.viewId, { |
||||
type, |
||||
triggerId, |
||||
viewId: view.viewId, |
||||
appId, |
||||
view, |
||||
}); |
||||
break; |
||||
} |
||||
|
||||
case 'banner.close': { |
||||
const { viewId } = interaction; |
||||
this.viewInstances.get(viewId)?.close(); |
||||
|
||||
break; |
||||
} |
||||
|
||||
case 'contextual_bar.open': { |
||||
const { view } = interaction; |
||||
this.viewInstances.set(view.id, { |
||||
payload: { |
||||
view, |
||||
}, |
||||
close: () => { |
||||
this.viewInstances.delete(view.id); |
||||
}, |
||||
}); |
||||
|
||||
const routeName = this.router.getRouteName(); |
||||
const routeParams = this.router.getRouteParameters(); |
||||
|
||||
if (!routeName) { |
||||
break; |
||||
} |
||||
|
||||
this.router.navigate({ |
||||
name: routeName, |
||||
params: { |
||||
...routeParams, |
||||
tab: 'app', |
||||
context: view.id, |
||||
}, |
||||
}); |
||||
break; |
||||
} |
||||
|
||||
case 'contextual_bar.close': { |
||||
const { view } = interaction; |
||||
this.viewInstances.get(view.id)?.close(); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
return interaction.type; |
||||
} |
||||
|
||||
public getInteractionPayloadByViewId(viewId: UiKit.ContextualBarView['id']) { |
||||
if (!viewId) { |
||||
throw new Error('No viewId provided when checking for `user interaction payload`'); |
||||
} |
||||
|
||||
return this.viewInstances.get(viewId)?.payload; |
||||
} |
||||
|
||||
public disposeView(viewId: UiKit.ModalView['id'] | UiKit.BannerView['viewId'] | UiKit.ContextualBarView['id']) { |
||||
const instance = this.viewInstances.get(viewId); |
||||
instance?.close?.(); |
||||
this.viewInstances.delete(viewId); |
||||
} |
||||
} |
||||
|
||||
/** @deprecated consumer should use the context instead */ |
||||
export const actionManager = new ActionManager(router); |
||||
@ -0,0 +1,7 @@ |
||||
import { RocketChatError } from '../../../client/lib/errors/RocketChatError'; |
||||
|
||||
export class UiKitTriggerTimeoutError extends RocketChatError<'trigger-timeout'> { |
||||
constructor(message = 'Timeout', details: { triggerId: string; appId: string }) { |
||||
super('trigger-timeout', message, details); |
||||
} |
||||
} |
||||
@ -1,26 +0,0 @@ |
||||
import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; |
||||
import type { UiKitPayload, UIKitActionEvent } from '@rocket.chat/core-typings'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; |
||||
|
||||
const useUIKitHandleAction = <S extends UiKitPayload>(state: S): ((event: UIKitActionEvent) => Promise<void>) => { |
||||
const actionManager = useUiKitActionManager(); |
||||
return useMutableCallback(async ({ blockId, value, appId, actionId }) => { |
||||
if (!appId) { |
||||
throw new Error('useUIKitHandleAction - invalid appId'); |
||||
} |
||||
return actionManager.triggerBlockAction({ |
||||
container: { |
||||
type: UIKitIncomingInteractionContainerType.VIEW, |
||||
id: state.viewId || state.appId, |
||||
}, |
||||
actionId, |
||||
appId, |
||||
value, |
||||
blockId, |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
export { useUIKitHandleAction }; |
||||
@ -1,34 +0,0 @@ |
||||
import type { UIKitInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; |
||||
import type { UiKitPayload } from '@rocket.chat/core-typings'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; |
||||
|
||||
import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; |
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emptyFn = (_error: any, _result: UIKitInteractionType | void): void => undefined; |
||||
|
||||
const useUIKitHandleClose = <S extends UiKitPayload>(state: S, fn = emptyFn): (() => Promise<void | UIKitInteractionType>) => { |
||||
const actionManager = useUiKitActionManager(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
return useMutableCallback(() => |
||||
actionManager |
||||
.triggerCancel({ |
||||
appId: state.appId, |
||||
viewId: state.viewId, |
||||
view: { |
||||
...state, |
||||
id: state.viewId, |
||||
}, |
||||
isCleared: true, |
||||
}) |
||||
.then((result) => fn(undefined, result)) |
||||
.catch((error) => { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
fn(error, undefined); |
||||
return Promise.reject(error); |
||||
}), |
||||
); |
||||
}; |
||||
|
||||
export { useUIKitHandleClose }; |
||||
@ -1,36 +0,0 @@ |
||||
import type { UIKitUserInteractionResult, UiKitPayload } from '@rocket.chat/core-typings'; |
||||
import { isErrorType } from '@rocket.chat/core-typings'; |
||||
import { useSafely } from '@rocket.chat/fuselage-hooks'; |
||||
import { useEffect, useState } from 'react'; |
||||
|
||||
import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; |
||||
|
||||
const useUIKitStateManager = <S extends UiKitPayload>(initialState: S): S => { |
||||
const actionManager = useUiKitActionManager(); |
||||
const [state, setState] = useSafely(useState(initialState)); |
||||
|
||||
const { viewId } = state; |
||||
|
||||
useEffect(() => { |
||||
const handleUpdate = ({ ...data }: UIKitUserInteractionResult): void => { |
||||
if (isErrorType(data)) { |
||||
const { errors } = data; |
||||
setState((state) => ({ ...state, errors })); |
||||
return; |
||||
} |
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { type, ...rest } = data; |
||||
setState(rest as any); |
||||
}; |
||||
|
||||
actionManager.on(viewId, handleUpdate); |
||||
|
||||
return (): void => { |
||||
actionManager.off(viewId, handleUpdate); |
||||
}; |
||||
}, [setState, viewId]); |
||||
|
||||
return state; |
||||
}; |
||||
|
||||
export { useUIKitStateManager }; |
||||
@ -0,0 +1,93 @@ |
||||
import type { UiKit } from '@rocket.chat/core-typings'; |
||||
import { useSafely } from '@rocket.chat/fuselage-hooks'; |
||||
import { extractInitialStateFromLayout } from '@rocket.chat/fuselage-ui-kit'; |
||||
import type { Dispatch } from 'react'; |
||||
import { useEffect, useMemo, useReducer, useState } from 'react'; |
||||
|
||||
import { useUiKitActionManager } from './useUiKitActionManager'; |
||||
|
||||
const reduceValues = ( |
||||
values: { [actionId: string]: { value: unknown; blockId?: string } }, |
||||
{ actionId, payload }: { actionId: string; payload: { value: unknown; blockId?: string } }, |
||||
): { [actionId: string]: { value: unknown; blockId?: string } } => ({ |
||||
...values, |
||||
[actionId]: payload, |
||||
}); |
||||
|
||||
const getViewId = (view: UiKit.View): string => { |
||||
if ('id' in view && typeof view.id === 'string') { |
||||
return view.id; |
||||
} |
||||
|
||||
if ('viewId' in view && typeof view.viewId === 'string') { |
||||
return view.viewId; |
||||
} |
||||
|
||||
throw new Error('Invalid view'); |
||||
}; |
||||
|
||||
const getViewFromInteraction = (interaction: UiKit.ServerInteraction): UiKit.View | undefined => { |
||||
if ('view' in interaction && typeof interaction.view === 'object') { |
||||
return interaction.view; |
||||
} |
||||
|
||||
if (interaction.type === 'banner.open') { |
||||
return interaction; |
||||
} |
||||
|
||||
return undefined; |
||||
}; |
||||
|
||||
type UseUiKitViewReturnType<TView extends UiKit.View> = { |
||||
view: TView; |
||||
errors?: { [field: string]: string }[]; |
||||
values: { [actionId: string]: { value: unknown; blockId?: string } }; |
||||
updateValues: Dispatch<{ actionId: string; payload: { value: unknown; blockId?: string } }>; |
||||
state: { |
||||
[blockId: string]: { |
||||
[key: string]: unknown; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
export function useUiKitView<S extends UiKit.View>(initialView: S): UseUiKitViewReturnType<S> { |
||||
const [errors, setErrors] = useSafely(useState<{ [field: string]: string }[] | undefined>()); |
||||
const [values, updateValues] = useSafely(useReducer(reduceValues, initialView.blocks, extractInitialStateFromLayout)); |
||||
const [view, updateView] = useSafely(useState(initialView)); |
||||
const actionManager = useUiKitActionManager(); |
||||
|
||||
const state = useMemo(() => { |
||||
return Object.entries(values).reduce<{ [blockId: string]: { [actionId: string]: unknown } }>((obj, [key, payload]) => { |
||||
if (!payload?.blockId) { |
||||
return obj; |
||||
} |
||||
|
||||
const { blockId, value } = payload; |
||||
obj[blockId] = obj[blockId] || {}; |
||||
obj[blockId][key] = value; |
||||
|
||||
return obj; |
||||
}, {}); |
||||
}, [values]); |
||||
|
||||
const viewId = getViewId(view); |
||||
|
||||
useEffect(() => { |
||||
const handleUpdate = (interaction: UiKit.ServerInteraction): void => { |
||||
if (interaction.type === 'errors') { |
||||
setErrors(interaction.errors); |
||||
return; |
||||
} |
||||
|
||||
updateView((view) => ({ ...view, ...getViewFromInteraction(interaction) })); |
||||
}; |
||||
|
||||
actionManager.on(viewId, handleUpdate); |
||||
|
||||
return (): void => { |
||||
actionManager.off(viewId, handleUpdate); |
||||
}; |
||||
}, [actionManager, setErrors, updateView, viewId]); |
||||
|
||||
return { view, errors, values, updateValues, state }; |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
import type { SyntheticEvent } from 'react'; |
||||
|
||||
export const preventSyntheticEvent = (e: SyntheticEvent): void => { |
||||
if (e) { |
||||
(e.nativeEvent || e).stopImmediatePropagation(); |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
} |
||||
}; |
||||
@ -0,0 +1,16 @@ |
||||
if (!Promise.prototype.finally) { |
||||
// eslint-disable-next-line no-extend-native
|
||||
Promise.prototype.finally = function (callback) { |
||||
if (typeof callback !== 'function') { |
||||
return this.then(callback, callback); |
||||
} |
||||
const P = (this.constructor as PromiseConstructor) || Promise; |
||||
return this.then( |
||||
(value) => P.resolve(callback()).then(() => value), |
||||
(err) => |
||||
P.resolve(callback()).then(() => { |
||||
throw err; |
||||
}), |
||||
); |
||||
}; |
||||
} |
||||
@ -1,6 +1,6 @@ |
||||
import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; |
||||
import type { ButtonElement } from '@rocket.chat/ui-kit'; |
||||
|
||||
// TODO: Move to fuselage-ui-kit
|
||||
export const getButtonStyle = (view: IUIKitSurface): { danger: boolean } | { primary: boolean } => { |
||||
return view.submit?.style === 'danger' ? { danger: true } : { primary: true }; |
||||
export const getButtonStyle = (buttonElement: ButtonElement): { danger: boolean } | { primary: boolean } => { |
||||
return buttonElement?.style === 'danger' ? { danger: true } : { primary: true }; |
||||
}; |
||||
|
||||
@ -1,39 +0,0 @@ |
||||
import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; |
||||
import { useEffect, useState } from 'react'; |
||||
|
||||
import { useUiKitActionManager } from '../../../../hooks/useUiKitActionManager'; |
||||
|
||||
export type ActionManagerState = { |
||||
viewId: string; |
||||
type: 'errors' | string; |
||||
appId: string; |
||||
mid: string; |
||||
errors: Record<string, string>; |
||||
view: IUIKitSurface; |
||||
}; |
||||
|
||||
export const useActionManagerState = (initialState: ActionManagerState) => { |
||||
const actionManager = useUiKitActionManager(); |
||||
const [state, setState] = useState(initialState); |
||||
|
||||
const { viewId } = state; |
||||
|
||||
useEffect(() => { |
||||
const handleUpdate = ({ type, errors, ...data }: ActionManagerState) => { |
||||
if (type === 'errors') { |
||||
setState((state) => ({ ...state, errors, type })); |
||||
return; |
||||
} |
||||
|
||||
setState({ ...data, type, errors }); |
||||
}; |
||||
|
||||
actionManager.on(viewId, handleUpdate); |
||||
|
||||
return () => { |
||||
actionManager.off(viewId, handleUpdate); |
||||
}; |
||||
}, [actionManager, viewId]); |
||||
|
||||
return state; |
||||
}; |
||||
@ -1,48 +0,0 @@ |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import type { LayoutBlock } from '@rocket.chat/ui-kit'; |
||||
import { useReducer } from 'react'; |
||||
|
||||
type LayoutBlockWithElement = Extract<LayoutBlock, { element: unknown }>; |
||||
type LayoutBlockWithElements = Extract<LayoutBlock, { elements: readonly unknown[] }>; |
||||
type ElementFromLayoutBlock = LayoutBlockWithElement['element'] | LayoutBlockWithElements['elements'][number]; |
||||
|
||||
const hasElementInBlock = (block: LayoutBlock): block is LayoutBlockWithElement => 'element' in block; |
||||
const hasElementsInBlock = (block: LayoutBlock): block is LayoutBlockWithElements => 'elements' in block; |
||||
const hasInitialValueAndActionId = ( |
||||
element: ElementFromLayoutBlock, |
||||
): element is Extract<ElementFromLayoutBlock, { actionId: string }> & { initialValue: unknown } => |
||||
'initialValue' in element && 'actionId' in element && typeof element.actionId === 'string' && !!element?.initialValue; |
||||
|
||||
const extractValue = (element: ElementFromLayoutBlock, obj: Record<string, { value: unknown; blockId?: string }>, blockId?: string) => { |
||||
if (hasInitialValueAndActionId(element)) { |
||||
obj[element.actionId] = { value: element.initialValue, blockId }; |
||||
} |
||||
}; |
||||
|
||||
const reduceBlocks = (obj: Record<string, { value: unknown; blockId?: string }>, block: LayoutBlock) => { |
||||
if (hasElementInBlock(block)) { |
||||
extractValue(block.element, obj, block.blockId); |
||||
} |
||||
if (hasElementsInBlock(block)) { |
||||
for (const element of block.elements) { |
||||
extractValue(element, obj, block.blockId); |
||||
} |
||||
} |
||||
|
||||
return obj; |
||||
}; |
||||
|
||||
export const useValues = (blocks: LayoutBlock[]) => { |
||||
const reducer = useMutableCallback((values, { actionId, payload }) => ({ |
||||
...values, |
||||
[actionId]: payload, |
||||
})); |
||||
|
||||
const initializer = useMutableCallback((blocks: LayoutBlock[]) => { |
||||
const obj: Record<string, { value: unknown; blockId?: string }> = {}; |
||||
|
||||
return blocks.reduce(reduceBlocks, obj); |
||||
}); |
||||
|
||||
return useReducer(reducer, blocks, initializer); |
||||
}; |
||||
@ -1,49 +1,35 @@ |
||||
import type { IUIKitContextualBarInteraction } from '@rocket.chat/apps-engine/definition/uikit'; |
||||
import { useRouteParameter } from '@rocket.chat/ui-contexts'; |
||||
import { useEffect, useState } from 'react'; |
||||
import { useCallback } from 'react'; |
||||
import { useSyncExternalStore } from 'use-sync-external-store/shim'; |
||||
|
||||
import { useUiKitActionManager } from '../../../hooks/useUiKitActionManager'; |
||||
import { useRoom } from '../contexts/RoomContext'; |
||||
import { useUiKitActionManager } from '../../../UIKit/hooks/useUiKitActionManager'; |
||||
|
||||
type AppsContextualBarData = { |
||||
viewId: string; |
||||
roomId: string; |
||||
payload: IUIKitContextualBarInteraction; |
||||
appId: string; |
||||
}; |
||||
|
||||
export const useAppsContextualBar = (): AppsContextualBarData | undefined => { |
||||
const [payload, setPayload] = useState<IUIKitContextualBarInteraction>(); |
||||
export const useAppsContextualBar = () => { |
||||
const viewId = useRouteParameter('context'); |
||||
const actionManager = useUiKitActionManager(); |
||||
const [appId, setAppId] = useState<string>(); |
||||
|
||||
const { _id: roomId } = useRoom(); |
||||
const getSnapshot = useCallback(() => { |
||||
if (!viewId) { |
||||
return undefined; |
||||
} |
||||
|
||||
const viewId = useRouteParameter('context'); |
||||
return actionManager.getInteractionPayloadByViewId(viewId)?.view; |
||||
}, [actionManager, viewId]); |
||||
|
||||
useEffect(() => { |
||||
if (viewId) { |
||||
setPayload(actionManager.getUserInteractionPayloadByViewId(viewId) as IUIKitContextualBarInteraction); |
||||
} |
||||
const subscribe = useCallback( |
||||
(handler: () => void) => { |
||||
if (!viewId) { |
||||
return () => undefined; |
||||
} |
||||
|
||||
if (payload?.appId) { |
||||
setAppId(payload.appId); |
||||
} |
||||
actionManager.on(viewId, handler); |
||||
|
||||
return () => actionManager.off(viewId, handler); |
||||
}, |
||||
[actionManager, viewId], |
||||
); |
||||
|
||||
const view = useSyncExternalStore(subscribe, getSnapshot); |
||||
|
||||
return (): void => { |
||||
setPayload(undefined); |
||||
setAppId(undefined); |
||||
}; |
||||
}, [viewId, payload?.appId, actionManager]); |
||||
|
||||
if (viewId && payload && appId) { |
||||
return { |
||||
viewId, |
||||
roomId, |
||||
payload, |
||||
appId, |
||||
}; |
||||
} |
||||
|
||||
return undefined; |
||||
return view; |
||||
}; |
||||
|
||||
@ -1,18 +1,24 @@ |
||||
import { Banner } from '@rocket.chat/core-services'; |
||||
import type { IUiKitCoreApp } from '@rocket.chat/core-services'; |
||||
import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services'; |
||||
|
||||
export class BannerModule implements IUiKitCoreApp { |
||||
appId = 'banner-core'; |
||||
|
||||
// when banner view is closed we need to dissmiss that banner for that user
|
||||
async viewClosed(payload: any): Promise<any> { |
||||
async viewClosed(payload: UiKitCoreAppPayload) { |
||||
const { |
||||
payload: { |
||||
view: { viewId: bannerId }, |
||||
}, |
||||
user: { _id: userId }, |
||||
payload: { view: { viewId: bannerId } = {} }, |
||||
user: { _id: userId } = {}, |
||||
} = payload; |
||||
|
||||
if (!userId) { |
||||
throw new Error('invalid user'); |
||||
} |
||||
|
||||
if (!bannerId) { |
||||
throw new Error('invalid banner'); |
||||
} |
||||
|
||||
return Banner.dismiss(userId, bannerId); |
||||
} |
||||
} |
||||
|
||||
@ -1,16 +1,55 @@ |
||||
import type { IUser } from '@rocket.chat/core-typings'; |
||||
|
||||
import type { IServiceClass } from './ServiceClass'; |
||||
|
||||
export type UiKitCoreAppPayload = { |
||||
appId: string; |
||||
type: 'blockAction' | 'viewClosed' | 'viewSubmit'; |
||||
actionId?: string; |
||||
triggerId?: string; |
||||
container?: { |
||||
id: string; |
||||
[key: string]: unknown; |
||||
}; |
||||
message?: unknown; |
||||
payload: { |
||||
blockId?: string; |
||||
value?: unknown; |
||||
view?: { |
||||
viewId?: string; |
||||
id?: string; |
||||
state?: { [blockId: string]: { [key: string]: unknown } }; |
||||
[key: string]: unknown; |
||||
}; |
||||
isCleared?: unknown; |
||||
}; |
||||
user?: IUser; |
||||
visitor?: { |
||||
id: string; |
||||
username: string; |
||||
name?: string; |
||||
department?: string; |
||||
updatedAt?: Date; |
||||
token: string; |
||||
phone?: { phoneNumber: string }[] | null; |
||||
visitorEmails?: { address: string }[]; |
||||
livechatData?: Record<string, unknown>; |
||||
status?: 'online' | 'away' | 'offline' | 'busy' | 'disabled'; |
||||
}; |
||||
room?: unknown; |
||||
}; |
||||
|
||||
export interface IUiKitCoreApp { |
||||
appId: string; |
||||
|
||||
blockAction?(payload: any): Promise<any>; |
||||
viewClosed?(payload: any): Promise<any>; |
||||
viewSubmit?(payload: any): Promise<any>; |
||||
blockAction?(payload: UiKitCoreAppPayload): Promise<unknown>; |
||||
viewClosed?(payload: UiKitCoreAppPayload): Promise<unknown>; |
||||
viewSubmit?(payload: UiKitCoreAppPayload): Promise<unknown>; |
||||
} |
||||
|
||||
export interface IUiKitCoreAppService extends IServiceClass { |
||||
isRegistered(appId: string): Promise<boolean>; |
||||
blockAction(payload: any): Promise<any>; |
||||
viewClosed(payload: any): Promise<any>; |
||||
viewSubmit(payload: any): Promise<any>; |
||||
blockAction(payload: UiKitCoreAppPayload): Promise<unknown>; |
||||
viewClosed(payload: UiKitCoreAppPayload): Promise<unknown>; |
||||
viewSubmit(payload: UiKitCoreAppPayload): Promise<unknown>; |
||||
} |
||||
|
||||
@ -1,9 +1,26 @@ |
||||
export type Serialized<T> = T extends Date |
||||
? Exclude<T, Date> | string |
||||
: T extends boolean | number | string | null | undefined |
||||
/* eslint-disable @typescript-eslint/ban-types */ |
||||
|
||||
type SerializablePrimitive = boolean | number | string | null; |
||||
|
||||
type UnserializablePrimitive = Function | bigint | symbol | undefined; |
||||
|
||||
type CustomSerializable<T> = { |
||||
toJSON(key: string): T; |
||||
}; |
||||
|
||||
/** |
||||
* The type of a value that was serialized via `JSON.stringify` and then deserialized via `JSON.parse`. |
||||
*/ |
||||
export type Serialized<T> = T extends CustomSerializable<infer U> |
||||
? Serialized<U> |
||||
: T extends [any, ...any] // is T a tuple?
|
||||
? { [K in keyof T]: T extends UnserializablePrimitive ? null : Serialized<T[K]> } |
||||
: T extends any[] |
||||
? Serialized<T[number]>[] |
||||
: T extends object |
||||
? { [K in keyof T]: Serialized<T[K]> } |
||||
: T extends SerializablePrimitive |
||||
? T |
||||
: T extends {} |
||||
? { |
||||
[K in keyof T]: Serialized<T[K]>; |
||||
} |
||||
: T extends UnserializablePrimitive |
||||
? undefined |
||||
: null; |
||||
|
||||
@ -1,60 +0,0 @@ |
||||
import { UIKitInteractionType as UIKitInteractionTypeApi } from '@rocket.chat/apps-engine/definition/uikit'; |
||||
import type { |
||||
IDividerBlock, |
||||
ISectionBlock, |
||||
IActionsBlock, |
||||
IContextBlock, |
||||
IInputBlock, |
||||
} from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; |
||||
|
||||
enum UIKitInteractionTypeExtended { |
||||
BANNER_OPEN = 'banner.open', |
||||
BANNER_UPDATE = 'banner.update', |
||||
BANNER_CLOSE = 'banner.close', |
||||
} |
||||
|
||||
export type UIKitInteractionType = UIKitInteractionTypeApi | UIKitInteractionTypeExtended; |
||||
|
||||
export const UIKitInteractionTypes = { |
||||
...UIKitInteractionTypeApi, |
||||
...UIKitInteractionTypeExtended, |
||||
}; |
||||
|
||||
export type UiKitPayload = { |
||||
viewId: string; |
||||
appId: string; |
||||
blocks: (IDividerBlock | ISectionBlock | IActionsBlock | IContextBlock | IInputBlock)[]; |
||||
}; |
||||
|
||||
export type UiKitBannerPayload = UiKitPayload & { |
||||
inline?: boolean; |
||||
variant?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; |
||||
icon?: string; |
||||
title?: string; |
||||
}; |
||||
|
||||
export type UIKitUserInteraction = { |
||||
type: UIKitInteractionType; |
||||
} & UiKitPayload; |
||||
|
||||
export type UiKitBannerProps = { |
||||
payload: UiKitBannerPayload; |
||||
}; |
||||
|
||||
export type UIKitUserInteractionResult = UIKitUserInteractionResultError | UIKitUserInteraction; |
||||
|
||||
type UIKitUserInteractionResultError = UIKitUserInteraction & { |
||||
type: UIKitInteractionTypeApi.ERRORS; |
||||
errors?: Array<{ [key: string]: string }>; |
||||
}; |
||||
|
||||
export const isErrorType = (result: UIKitUserInteractionResult): result is UIKitUserInteractionResultError => |
||||
result.type === UIKitInteractionTypeApi.ERRORS; |
||||
|
||||
export type UIKitActionEvent = { |
||||
blockId: string; |
||||
value?: unknown; |
||||
appId: string; |
||||
actionId: string; |
||||
viewId: string; |
||||
}; |
||||
@ -0,0 +1,16 @@ |
||||
import type { Keys as IconName } from '@rocket.chat/icons'; |
||||
import type { BannerSurfaceLayout } from '@rocket.chat/ui-kit'; |
||||
|
||||
import type { View } from './View'; |
||||
|
||||
/** |
||||
* A view that is displayed as a banner. |
||||
*/ |
||||
export type BannerView = View & { |
||||
viewId: string; |
||||
inline?: boolean; |
||||
variant?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; |
||||
icon?: IconName; |
||||
title?: string; // TODO: change to plain_text block in the future
|
||||
blocks: BannerSurfaceLayout; |
||||
}; |
||||
@ -0,0 +1,14 @@ |
||||
import type { ButtonElement, ContextualBarSurfaceLayout, TextObject } from '@rocket.chat/ui-kit'; |
||||
|
||||
import type { View } from './View'; |
||||
|
||||
/** |
||||
* A view that is displayed as a contextual bar. |
||||
*/ |
||||
export type ContextualBarView = View & { |
||||
id: string; |
||||
title: TextObject; |
||||
close?: ButtonElement; |
||||
submit?: ButtonElement; |
||||
blocks: ContextualBarSurfaceLayout; |
||||
}; |
||||
@ -0,0 +1,15 @@ |
||||
import type { ButtonElement, ModalSurfaceLayout, TextObject } from '@rocket.chat/ui-kit'; |
||||
|
||||
import type { View } from './View'; |
||||
|
||||
/** |
||||
* A view that is displayed as a modal dialog. |
||||
*/ |
||||
export type ModalView = View & { |
||||
id: string; |
||||
showIcon?: boolean; |
||||
title: TextObject; |
||||
close?: ButtonElement; |
||||
submit?: ButtonElement; |
||||
blocks: ModalSurfaceLayout; |
||||
}; |
||||
@ -0,0 +1,84 @@ |
||||
import type { BannerView } from './BannerView'; |
||||
import type { ContextualBarView } from './ContextualBarView'; |
||||
import type { ModalView } from './ModalView'; |
||||
|
||||
type OpenModalServerInteraction = { |
||||
type: 'modal.open'; |
||||
triggerId: string; |
||||
appId: string; |
||||
view: ModalView; |
||||
}; |
||||
|
||||
type UpdateModalServerInteraction = { |
||||
type: 'modal.update'; |
||||
triggerId: string; |
||||
appId: string; |
||||
view: ModalView; |
||||
}; |
||||
|
||||
type CloseModalServerInteraction = { |
||||
type: 'modal.close'; |
||||
triggerId: string; |
||||
appId: string; |
||||
}; |
||||
|
||||
type OpenBannerServerInteraction = { |
||||
type: 'banner.open'; |
||||
triggerId: string; |
||||
appId: string; |
||||
} & BannerView; |
||||
|
||||
type UpdateBannerServerInteraction = { |
||||
type: 'banner.update'; |
||||
triggerId: string; |
||||
appId: string; |
||||
view: BannerView; |
||||
}; |
||||
|
||||
type CloseBannerServerInteraction = { |
||||
type: 'banner.close'; |
||||
triggerId: string; |
||||
appId: string; |
||||
viewId: BannerView['viewId']; |
||||
}; |
||||
|
||||
type OpenContextualBarServerInteraction = { |
||||
type: 'contextual_bar.open'; |
||||
triggerId: string; |
||||
appId: string; |
||||
view: ContextualBarView; |
||||
}; |
||||
|
||||
type UpdateContextualBarServerInteraction = { |
||||
type: 'contextual_bar.update'; |
||||
triggerId: string; |
||||
appId: string; |
||||
view: ContextualBarView; |
||||
}; |
||||
|
||||
type CloseContextualBarServerInteraction = { |
||||
type: 'contextual_bar.close'; |
||||
triggerId: string; |
||||
appId: string; |
||||
view: ContextualBarView; |
||||
}; |
||||
|
||||
type ReportErrorsServerInteraction = { |
||||
type: 'errors'; |
||||
triggerId: string; |
||||
appId: string; |
||||
viewId: ModalView['id'] | BannerView['viewId'] | ContextualBarView['id']; |
||||
errors: { [field: string]: string }[]; |
||||
}; |
||||
|
||||
export type ServerInteraction = |
||||
| OpenModalServerInteraction |
||||
| UpdateModalServerInteraction |
||||
| CloseModalServerInteraction |
||||
| OpenBannerServerInteraction |
||||
| UpdateBannerServerInteraction |
||||
| CloseBannerServerInteraction |
||||
| OpenContextualBarServerInteraction |
||||
| UpdateContextualBarServerInteraction |
||||
| CloseContextualBarServerInteraction |
||||
| ReportErrorsServerInteraction; |
||||
@ -0,0 +1,122 @@ |
||||
import type { IMessage } from '../IMessage'; |
||||
import type { IRoom } from '../IRoom'; |
||||
import type { View } from './View'; |
||||
|
||||
export type MessageBlockActionUserInteraction = { |
||||
type: 'blockAction'; |
||||
actionId: string; |
||||
payload: { |
||||
blockId: string; |
||||
value: unknown; |
||||
}; |
||||
container: { |
||||
type: 'message'; |
||||
id: IMessage['_id']; |
||||
}; |
||||
mid: IMessage['_id']; |
||||
tmid?: IMessage['_id']; |
||||
rid: IRoom['_id']; |
||||
triggerId: string; |
||||
}; |
||||
|
||||
export type ViewBlockActionUserInteraction = { |
||||
type: 'blockAction'; |
||||
actionId: string; |
||||
payload: { |
||||
blockId: string; |
||||
value: unknown; |
||||
}; |
||||
container: { |
||||
type: 'view'; |
||||
id: string; |
||||
}; |
||||
triggerId: string; |
||||
}; |
||||
|
||||
export type ViewClosedUserInteraction = { |
||||
type: 'viewClosed'; |
||||
payload: { |
||||
viewId: string; |
||||
view: View & { |
||||
id: string; |
||||
state: { [blockId: string]: { [key: string]: unknown } }; |
||||
}; |
||||
isCleared?: boolean; |
||||
}; |
||||
triggerId: string; |
||||
}; |
||||
|
||||
export type ViewSubmitUserInteraction = { |
||||
type: 'viewSubmit'; |
||||
actionId?: undefined; |
||||
payload: { |
||||
view: View & { |
||||
id: string; |
||||
state: { [blockId: string]: { [key: string]: unknown } }; |
||||
}; |
||||
}; |
||||
triggerId: string; |
||||
viewId: string; |
||||
}; |
||||
|
||||
export type MessageBoxActionButtonUserInteraction = { |
||||
type: 'actionButton'; |
||||
actionId: string; |
||||
payload: { |
||||
context: 'messageBoxAction'; |
||||
message: string; |
||||
}; |
||||
mid?: undefined; |
||||
tmid?: IMessage['_id']; |
||||
rid: IRoom['_id']; |
||||
triggerId: string; |
||||
}; |
||||
|
||||
export type UserDropdownActionButtonUserInteraction = { |
||||
type: 'actionButton'; |
||||
actionId: string; |
||||
payload: { |
||||
context: 'userDropdownAction'; |
||||
message?: undefined; |
||||
}; |
||||
mid?: undefined; |
||||
tmid?: undefined; |
||||
rid?: undefined; |
||||
triggerId: string; |
||||
}; |
||||
|
||||
export type MesssageActionButtonUserInteraction = { |
||||
type: 'actionButton'; |
||||
actionId: string; |
||||
payload: { |
||||
context: 'messageAction'; |
||||
message?: undefined; |
||||
}; |
||||
mid: IMessage['_id']; |
||||
tmid?: IMessage['_id']; |
||||
rid: IRoom['_id']; |
||||
triggerId: string; |
||||
}; |
||||
|
||||
export type RoomActionButtonUserInteraction = { |
||||
type: 'actionButton'; |
||||
actionId: string; |
||||
payload: { |
||||
context: 'roomAction'; |
||||
message?: undefined; |
||||
}; |
||||
mid?: undefined; |
||||
tmid?: undefined; |
||||
rid: IRoom['_id']; |
||||
triggerId: string; |
||||
}; |
||||
|
||||
export type UserInteraction = |
||||
| MessageBlockActionUserInteraction |
||||
| ViewBlockActionUserInteraction |
||||
| ViewClosedUserInteraction |
||||
| ViewSubmitUserInteraction |
||||
| MessageBoxActionButtonUserInteraction |
||||
| UserDropdownActionButtonUserInteraction |
||||
| MesssageActionButtonUserInteraction |
||||
| RoomActionButtonUserInteraction; |
||||
@ -0,0 +1,9 @@ |
||||
import type { LayoutBlock } from '@rocket.chat/ui-kit'; |
||||
|
||||
/** |
||||
* An instance of a UiKit surface and its metadata. |
||||
*/ |
||||
export type View = { |
||||
appId: string; |
||||
blocks: LayoutBlock[]; |
||||
}; |
||||
@ -0,0 +1,17 @@ |
||||
export * from '@rocket.chat/ui-kit'; |
||||
export type { |
||||
UserInteraction, |
||||
MessageBlockActionUserInteraction, |
||||
ViewBlockActionUserInteraction, |
||||
ViewClosedUserInteraction, |
||||
ViewSubmitUserInteraction, |
||||
MessageBoxActionButtonUserInteraction, |
||||
UserDropdownActionButtonUserInteraction, |
||||
MesssageActionButtonUserInteraction, |
||||
RoomActionButtonUserInteraction, |
||||
} from './UserInteraction'; |
||||
export type { View } from './View'; |
||||
export type { BannerView } from './BannerView'; |
||||
export type { ContextualBarView } from './ContextualBarView'; |
||||
export type { ModalView } from './ModalView'; |
||||
export type { ServerInteraction } from './ServerInteraction'; |
||||
@ -0,0 +1,90 @@ |
||||
import type * as UiKit from '@rocket.chat/ui-kit'; |
||||
|
||||
type Value = { value: unknown; blockId?: string }; |
||||
|
||||
type LayoutBlockWithElement = Extract< |
||||
UiKit.LayoutBlock, |
||||
{ element: UiKit.BlockElement | UiKit.TextObject } |
||||
>; |
||||
type LayoutBlockWithElements = Extract< |
||||
UiKit.LayoutBlock, |
||||
{ elements: readonly (UiKit.BlockElement | UiKit.TextObject)[] } |
||||
>; |
||||
|
||||
const hasElement = ( |
||||
block: UiKit.LayoutBlock |
||||
): block is LayoutBlockWithElement => 'element' in block; |
||||
|
||||
const hasElements = ( |
||||
block: UiKit.LayoutBlock |
||||
): block is LayoutBlockWithElements => |
||||
'elements' in block && Array.isArray(block.elements); |
||||
|
||||
const isActionableElement = ( |
||||
element: UiKit.BlockElement | UiKit.TextObject |
||||
): element is UiKit.ActionableElement => |
||||
'actionId' in element && typeof element.actionId === 'string'; |
||||
|
||||
const hasInitialValue = ( |
||||
element: UiKit.ActionableElement |
||||
): element is UiKit.ActionableElement & { initialValue: number | string } => |
||||
'initialValue' in element; |
||||
|
||||
const hasInitialTime = ( |
||||
element: UiKit.ActionableElement |
||||
): element is UiKit.ActionableElement & { initialTime: string } => |
||||
'initialTime' in element; |
||||
|
||||
const hasInitialDate = ( |
||||
element: UiKit.ActionableElement |
||||
): element is UiKit.ActionableElement & { initialDate: string } => |
||||
'initialDate' in element; |
||||
|
||||
const hasInitialOption = ( |
||||
element: UiKit.ActionableElement |
||||
): element is UiKit.ActionableElement & { initialOption: UiKit.Option } => |
||||
'initialOption' in element; |
||||
|
||||
const hasInitialOptions = ( |
||||
element: UiKit.ActionableElement |
||||
): element is UiKit.ActionableElement & { initialOptions: UiKit.Option[] } => |
||||
'initialOptions' in element; |
||||
|
||||
const getInitialValue = (element: UiKit.ActionableElement) => |
||||
(hasInitialValue(element) && element.initialValue) || |
||||
(hasInitialTime(element) && element.initialTime) || |
||||
(hasInitialDate(element) && element.initialDate) || |
||||
(hasInitialOption(element) && element.initialOption.value) || |
||||
(hasInitialOptions(element) && |
||||
element.initialOptions.map((option) => option.value)) || |
||||
undefined; |
||||
|
||||
const reduceInitialValuesFromLayoutBlock = ( |
||||
state: { [actionId: string]: Value }, |
||||
block: UiKit.LayoutBlock |
||||
) => { |
||||
if (hasElement(block)) { |
||||
if (isActionableElement(block.element)) { |
||||
state[block.element.actionId] = { |
||||
value: getInitialValue(block.element), |
||||
blockId: block.blockId, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
if (hasElements(block)) { |
||||
for (const element of block.elements) { |
||||
if (isActionableElement(element)) { |
||||
state[element.actionId] = { |
||||
value: getInitialValue(element), |
||||
blockId: block.blockId, |
||||
}; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return state; |
||||
}; |
||||
|
||||
export const extractInitialStateFromLayout = (blocks: UiKit.LayoutBlock[]) => |
||||
blocks.reduce(reduceInitialValuesFromLayoutBlock, {}); |
||||
@ -1,5 +0,0 @@ |
||||
import { useContext } from 'react'; |
||||
|
||||
import { UiKitContext } from '../contexts/UiKitContext'; |
||||
|
||||
export const useUiKitContext = () => useContext(UiKitContext); |
||||
@ -1,18 +0,0 @@ |
||||
import { useUiKitContext } from './useUiKitContext'; |
||||
|
||||
export const useUiKitStateValue = < |
||||
T extends string | string[] | number | undefined |
||||
>( |
||||
actionId: string, |
||||
initialValue: T |
||||
): { |
||||
value: T; |
||||
error: string | undefined; |
||||
} => { |
||||
const { values, errors } = useUiKitContext(); |
||||
|
||||
return { |
||||
value: (values && (values[actionId]?.value as T)) ?? initialValue, |
||||
error: errors?.[actionId], |
||||
}; |
||||
}; |
||||
@ -1,45 +1,20 @@ |
||||
import type { DistributiveOmit, UiKit } from '@rocket.chat/core-typings'; |
||||
import { createContext } from 'react'; |
||||
|
||||
type ActionManagerContextValue = { |
||||
on: (...args: any[]) => void; |
||||
off: (...args: any[]) => void; |
||||
generateTriggerId: (appId: any) => string; |
||||
handlePayloadUserInteraction: ( |
||||
type: any, |
||||
{ |
||||
triggerId, |
||||
...data |
||||
}: { |
||||
[x: string]: any; |
||||
triggerId: any; |
||||
}, |
||||
) => any; |
||||
triggerAction: ({ |
||||
type, |
||||
actionId, |
||||
appId, |
||||
rid, |
||||
mid, |
||||
viewId, |
||||
container, |
||||
tmid, |
||||
...rest |
||||
}: { |
||||
[x: string]: any; |
||||
type: any; |
||||
actionId: any; |
||||
appId: any; |
||||
rid: any; |
||||
mid: any; |
||||
viewId: any; |
||||
container: any; |
||||
tmid: any; |
||||
}) => Promise<any>; |
||||
triggerBlockAction: (options: any) => Promise<any>; |
||||
triggerActionButtonAction: (options: any) => Promise<any>; |
||||
triggerSubmitView: ({ viewId, ...options }: { [x: string]: any; viewId: any }) => Promise<void>; |
||||
triggerCancel: ({ view, ...options }: { [x: string]: any; view: any }) => Promise<void>; |
||||
getUserInteractionPayloadByViewId: (viewId: any) => any; |
||||
type ActionManager = { |
||||
on(viewId: string, listener: (data: any) => void): void; |
||||
on(eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void; |
||||
off(viewId: string, listener: (data: any) => any): void; |
||||
off(eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void; |
||||
generateTriggerId(appId: string | undefined): string; |
||||
emitInteraction(appId: string, userInteraction: DistributiveOmit<UiKit.UserInteraction, 'triggerId'>): Promise<unknown>; |
||||
handleServerInteraction(interaction: UiKit.ServerInteraction): UiKit.ServerInteraction['type'] | undefined; |
||||
getInteractionPayloadByViewId(viewId: UiKit.ContextualBarView['id']): |
||||
| { |
||||
view: UiKit.ContextualBarView; |
||||
} |
||||
| undefined; |
||||
disposeView(viewId: UiKit.ModalView['id'] | UiKit.BannerView['viewId'] | UiKit.ContextualBarView['id']): void; |
||||
}; |
||||
|
||||
export const ActionManagerContext = createContext<ActionManagerContextValue | undefined>(undefined); |
||||
export const ActionManagerContext = createContext<ActionManager | undefined>(undefined); |
||||
|
||||
Loading…
Reference in new issue