Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit 7983d6f

Browse files
committed
feat(packages/core): MultiModalResponse should support modeless responses
Fixes #3082
1 parent 04c245e commit 7983d6f

File tree

9 files changed

+116
-69
lines changed

9 files changed

+116
-69
lines changed

packages/core/src/api/table-models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@
1919
*
2020
*/
2121

22-
export { Table, isTable, Row } from '../webapp/models/table'
22+
export { Table, MultiTable, Row, isTable, isMultiTable } from '../webapp/models/table'

packages/core/src/core/job.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class WatchableJob {
4141
*/
4242
public start() {
4343
this.startWatching(this.handler, this.timeout)
44-
this.tab['state'].captureJob(this)
44+
this.tab.state.captureJob(this)
4545
debug(`start job ${this._id} with timeout ${this.timeout}`)
4646
}
4747

@@ -57,7 +57,7 @@ export class WatchableJob {
5757
*/
5858
public abort() {
5959
this.stopWatching(this._id)
60-
this.tab['state'].removeJob(this)
60+
this.tab.state.removeJob(this)
6161
debug(`stop job ${this._id}`)
6262
}
6363
}

packages/core/src/models/execOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export interface ExecOptions {
6262
preserveBackButton?: boolean
6363
type?: ExecType
6464

65-
exec?: 'pexec' | 'qexec'
65+
exec?: 'pexec' | 'qexec' | 'rexec'
6666

6767
container?: Element
6868
raw?: boolean

packages/core/src/models/mmr/content-types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { Tab } from '../../webapp/tab'
1818
import { Table, MultiTable } from '../../webapp/models/table'
19-
import { MetadataBearing } from '../entity'
19+
import { Entity, MetadataBearing } from '../entity'
2020
import { CustomSpec } from '../../webapp/views/sidecar-core'
2121

2222
/**
@@ -35,10 +35,15 @@ export interface ScalarContent<T = ScalarResource> {
3535
* optionally provide a `contentType`.
3636
*
3737
*/
38-
interface StringContent<ContentType = 'text/markdown' | 'text/html'> extends ScalarContent<string> {
38+
export interface StringContent<ContentType = 'text/markdown' | 'text/html'> extends ScalarContent<string> {
3939
contentType?: ContentType
4040
}
4141

42+
export function isStringWithContentType(entity: Entity): entity is StringContent {
43+
const string = entity as StringContent
44+
return string && string.content !== undefined && string.contentType !== undefined
45+
}
46+
4247
/**
4348
* `Content` as `FunctionThatProducesContent<T>` is a function that
4449
* takes a `T` and produces either a resource or some { content,

packages/core/src/models/mmr/is.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,5 @@ import { MultiModalResponse } from './types'
1919

2020
export function isMultiModalResponse(entity: Entity): entity is MultiModalResponse {
2121
const mmr = entity as MultiModalResponse
22-
return (
23-
isMetadataBearing(mmr) &&
24-
mmr.modes &&
25-
Array.isArray(mmr.modes) &&
26-
mmr.modes[0] &&
27-
mmr.modes[0].content !== undefined
28-
)
22+
return isMetadataBearing(mmr) && mmr.modes && Array.isArray(mmr.modes)
2923
}

packages/core/src/models/mmr/show.ts

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
import { Tab } from '../../webapp/tab'
1818
import { MetadataBearing } from '../entity'
19-
import { CustomSpec, isCustomSpec, showCustom, insertView } from '../../webapp/views/sidecar'
20-
import { DirectViewControllerFunction, SidecarMode } from '../../webapp/bottom-stripe'
21-
import { isTable, isMultiTable } from '../../webapp/models/table'
19+
import { CustomSpec, isCustomSpec, showCustom } from '../../webapp/views/sidecar'
20+
import { SidecarMode, addModeButtons } from '../../webapp/bottom-stripe'
21+
import { isTable, isMultiTable, Table, MultiTable } from '../../webapp/models/table'
2222
import { formatTable } from '../../webapp/views/table'
2323

2424
import { MultiModalResponse, Button } from './types'
@@ -28,14 +28,11 @@ import {
2828
ScalarResource,
2929
ScalarContent,
3030
isCommandStringContent,
31+
isStringWithContentType,
3132
isFunctionContent
3233
} from './content-types'
3334

34-
type Viewable = CustomSpec | DirectViewControllerFunction<void | CustomSpec, HTMLElement | void>
35-
36-
function isCustom(spec: Viewable): spec is CustomSpec {
37-
return isCustomSpec(spec as CustomSpec)
38-
}
35+
type Viewable = CustomSpec | HTMLElement | Table | MultiTable
3936

4037
/**
4138
* Turn a Resource into a Viewable
@@ -59,26 +56,23 @@ async function format<T extends MetadataBearing>(
5956
} else if (isCustomSpec(resource.content)) {
6057
return resource.content
6158
} else if (isTable(resource.content) || isMultiTable(resource.content)) {
62-
const content = resource.content
63-
return (tab, entity) => {
64-
const dom1 = document.createElement('div')
65-
const dom2 = document.createElement('div')
66-
dom1.classList.add('scrollable', 'scrollable-auto')
67-
dom2.classList.add('result-as-table', 'repl-result')
68-
dom1.appendChild(dom2)
69-
formatTable(tab, content, dom2)
70-
if (entity) {
71-
insertView(tab)(dom1)
72-
} else {
73-
return dom1
74-
}
75-
}
59+
return resource.content
7660
} else {
7761
// otherwise, we have string or HTMLElement content
7862
return Object.assign({ kind: mmr.kind, metadata: mmr.metadata, version: mmr.version, type: 'custom' }, resource)
7963
}
8064
}
8165

66+
function wrapTable(tab: Tab, table: Table | MultiTable): HTMLElement {
67+
const dom1 = document.createElement('div')
68+
const dom2 = document.createElement('div')
69+
dom1.classList.add('scrollable', 'scrollable-auto')
70+
dom2.classList.add('result-as-table', 'repl-result')
71+
dom1.appendChild(dom2)
72+
formatTable(tab, table, dom2)
73+
return dom1
74+
}
75+
8276
function formatButtons(buttons: Button[]): SidecarMode[] {
8377
return buttons.map(({ mode, label, command, confirm }) => ({
8478
mode,
@@ -88,53 +82,75 @@ function formatButtons(buttons: Button[]): SidecarMode[] {
8882
}))
8983
}
9084

85+
function renderContent<T extends MetadataBearing>(tab: Tab, content: string | object) {
86+
if (isStringWithContentType(content)) {
87+
return content
88+
} else if (isTable(content) || isMultiTable(content)) {
89+
return {
90+
content: wrapTable(tab, content)
91+
}
92+
} else {
93+
return {
94+
content
95+
}
96+
}
97+
}
98+
9199
/**
92100
* Render a MultiModalResponse to the sidecar
93101
*
94102
*/
95103
export async function show(tab: Tab, mmr: MultiModalResponse) {
96-
const modes: SidecarMode<Viewable>[] = await Promise.all(
104+
const modes: SidecarMode[] = await Promise.all(
97105
mmr.modes.map(async _ => ({
98106
mode: _.mode,
99107
label: _.label || _.mode,
100-
direct: isCustomSpec(_) ? _ : await format(tab, mmr, _),
108+
direct: (tab: Tab) => {
109+
if (isCustomSpec(_)) {
110+
return _
111+
} else {
112+
return format(tab, mmr, _)
113+
}
114+
},
101115
defaultMode: _.defaultMode,
102116
leaveBottomStripeAlone: true
103117
}))
104118
)
105119

120+
addModeButtons(tab, modes, mmr)
121+
106122
const modesWithButtons: SidecarMode[] = mmr.buttons
107123
? (modes as SidecarMode[]).concat(formatButtons(mmr.buttons))
108124
: (modes as SidecarMode[])
109125

110-
modes.forEach(_ => {
111-
if (isCustom(_.direct)) {
112-
_.direct.modes = modesWithButtons
113-
}
114-
})
115-
116126
if (!modes.find(_ => _.defaultMode)) {
117127
modes[0].defaultMode = true
118128
}
119129

120130
const defaultMode = modes.find(_ => _.defaultMode) || modes[0]
121131

122-
if (isCustom(defaultMode.direct)) {
123-
return showCustom(tab, defaultMode.direct)
124-
} else {
125-
const content = await defaultMode.direct(tab)
126-
if (content) {
127-
return showCustom(tab, {
128-
type: 'custom',
129-
kind: mmr.kind,
130-
metadata: mmr.metadata,
131-
toolbarText: mmr.toolbarText,
132-
version: mmr.version,
133-
modes: modesWithButtons,
134-
content
135-
})
132+
const content = typeof defaultMode.direct === 'function' ? await defaultMode.direct(tab, mmr) : defaultMode.direct
133+
134+
if (content) {
135+
if (isCustomSpec(content)) {
136+
return showCustom(tab, Object.assign({ modes: modesWithButtons }, content))
136137
} else {
137-
console.error('empty content')
138+
return showCustom(
139+
tab,
140+
Object.assign(
141+
{
142+
type: 'custom',
143+
kind: mmr.kind,
144+
metadata: mmr.metadata,
145+
toolbarText: mmr.toolbarText,
146+
version: mmr.version,
147+
modes: modesWithButtons
148+
},
149+
renderContent(tab, content)
150+
)
151+
)
138152
}
153+
} else {
154+
console.error('empty content')
139155
}
140156
}

packages/core/src/plugins/plugins.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export const init = async (): Promise<boolean> => {
107107
prescan = unify(prescan, userInstalledPrescan)
108108
// debug('prescan', prescan)
109109
}
110-
debug('user-installed prescan loaded')
110+
debug('user-installed prescan loaded', prescan)
111111
} catch (err) {
112112
console.error('error loading user-installed prescan', err)
113113
}

packages/core/src/webapp/bottom-stripe.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ import { removeAllDomChildren } from './util/dom'
2121
import { isTable, isMultiTable, Table, MultiTable } from './models/table'
2222
import { Capturable } from './models/capturable'
2323
import { formatTable } from './views/table'
24-
import { getSidecar, showCustom, isCustomSpec, CustomSpec, insertView } from './views/sidecar'
24+
import { getSidecar, showCustom, isCustomSpec, CustomSpec, insertCustomContent } from './views/sidecar'
2525
import sidecarSelector from './views/sidecar-selector'
2626
import { ExecOptions } from '../models/execOptions'
2727
import { apply as addRelevantModes } from './views/registrar/modes'
28-
import { pexec, qexec } from '../repl/exec'
28+
import { pexec, qexec, rexec } from '../repl/exec'
2929
import { isHTML } from '../util/types'
30-
import { Entity, EntitySpec, isMetadataBearingByReference } from '../models/entity'
30+
import { Entity, EntitySpec, isMetadataBearing, isMetadataBearingByReference } from '../models/entity'
31+
import { isStringWithContentType } from '../models/mmr/content-types'
3132

3233
const debug = Debug('webapp/picture-in-picture')
3334

@@ -88,6 +89,8 @@ const callDirect = async (
8889
debug('makeView as string')
8990
if (execOptions && execOptions.exec === 'pexec') {
9091
return pexec(makeView, execOptions)
92+
} else if (execOptions && execOptions.exec === 'rexec') {
93+
return rexec(makeView, execOptions)
9194
} else {
9295
return qexec(makeView, undefined, undefined, Object.assign({}, execOptions, { rethrowErrors: true }))
9396
}
@@ -451,20 +454,30 @@ const _addModeButton = (
451454

452455
changeActiveButton()
453456
} else if (view && !actAsButton && !isToggle(view)) {
454-
if (isTable(view)) {
457+
if (isTable(view) || isStringWithContentType(view)) {
455458
changeActiveButton()
456459
}
457460

458461
if (typeof view === 'string') {
459462
const dom = document.createElement('div')
460463
dom.classList.add('padding-content', 'scrollable', 'scrollable-auto')
461464
dom.innerText = view
462-
insertView(tab)(dom)
465+
insertCustomContent(tab, dom)
466+
} else if (isStringWithContentType(view)) {
467+
showCustom(
468+
tab,
469+
Object.assign({}, entity, {
470+
type: 'custom',
471+
content: view.content,
472+
contentType: view.contentType
473+
}),
474+
{ leaveBottomStripeAlone: true }
475+
)
463476
} else if (isHTML(view)) {
464477
const dom = document.createElement('div')
465478
dom.classList.add('padding-content', 'scrollable', 'scrollable-auto')
466479
dom.appendChild(view)
467-
insertView(tab)(dom)
480+
insertCustomContent(tab, dom)
468481
} else if (isCustomSpec(view)) {
469482
showCustom(tab, view, { leaveBottomStripeAlone })
470483
} else if (isTable(view) || isMultiTable(view)) {
@@ -474,7 +487,7 @@ const _addModeButton = (
474487
dom2.classList.add('result-as-table', 'repl-result')
475488
dom1.appendChild(dom2)
476489
formatTable(tab, view, dom2, { usePip: true })
477-
insertView(tab)(dom1)
490+
insertCustomContent(tab, dom1)
478491
}
479492
} else if (!isToggle(view) && isCustomSpec(view)) {
480493
showCustom(tab, view, { leaveBottomStripeAlone })
@@ -512,7 +525,7 @@ export const addModeButton = (
512525
return _addModeButton(tab, modeStripe, bottomStripe, mode, entity, undefined)
513526
}
514527

515-
export const addModeButtons = (
528+
export const addModeButtons = <Direct = DirectViewController>(
516529
tab: Tab,
517530
modesUnsorted: SidecarMode[] = [],
518531
entity: EntitySpec | CustomSpec,
@@ -521,7 +534,9 @@ export const addModeButtons = (
521534
// consult the view registrar for registered view modes
522535
// relevant to this resource
523536
const command = ''
524-
if (isMetadataBearingByReference(entity)) {
537+
if (isMetadataBearing(entity)) {
538+
addRelevantModes(modesUnsorted, command, { resource: entity })
539+
} else if (isMetadataBearingByReference(entity)) {
525540
addRelevantModes(modesUnsorted, command, entity)
526541
}
527542

packages/core/src/webapp/views/sidecar.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,10 @@ export const showCustom = async (tab: Tab, custom: CustomSpec, options?: ExecOpt
419419
if (!options || !options.leaveBottomStripeAlone) {
420420
addModeButtons(tab, modes, custom, options)
421421
sidecar.setAttribute('class', `${sidecar.getAttribute('data-base-class')} custom-content`)
422-
setVisibleClass(sidecar)
423422
} else {
424423
sidecar.classList.add('custom-content')
425424
}
425+
setVisibleClass(sidecar)
426426

427427
if (custom.sidecarHeader === false) {
428428
// view doesn't want a sidecar header
@@ -756,3 +756,20 @@ export const insertView = (tab: Tab) => (view: HTMLElement) => {
756756

757757
presentAs(tab, Presentation.Default)
758758
}
759+
760+
/**
761+
* Update the current view into the sidecar; this is helpful for tab
762+
* mode switching.
763+
*
764+
*/
765+
export const insertCustomContent = (tab: Tab, view: HTMLElement) => {
766+
debug('insertCustomContent', view)
767+
768+
const container = getSidecar(tab).querySelector('.custom-content')
769+
debug('insertCustomContent.container', container)
770+
771+
removeAllDomChildren(container)
772+
container.appendChild(view)
773+
774+
presentAs(tab, Presentation.Default)
775+
}

0 commit comments

Comments
 (0)