(core) Move theme from ConfigNotifier to ThemeNotifier

Summary:
This reverts the behavior of onOptions, which had unintentionally
changed recently and no longer matched the API documentation.

Test Plan: Existing tests.

Reviewers: jarek

Reviewed By: jarek

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D4064
This commit is contained in:
George Gevoian
2023-10-06 09:17:39 -04:00
parent a101337213
commit 1be1e5f647
12 changed files with 127 additions and 127 deletions

View File

@@ -11,6 +11,7 @@ import {
MinimumLevel,
RecordNotifier,
TableNotifier,
ThemeNotifier,
WidgetAPIImpl,
WidgetFrame
} from 'app/client/components/WidgetFrame';
@@ -215,6 +216,7 @@ export class CustomView extends Disposable {
}) {
const {baseUrl, access, showAfterReady} = options;
const documentSettings = this.gristDoc.docData.docSettings();
const readonly = this.gristDoc.isReadonly.get();
return grains.create(WidgetFrame, {
url: baseUrl || this.getEmptyWidgetPage(),
access,
@@ -225,13 +227,8 @@ export class CustomView extends Disposable {
timeZone: this.gristDoc.docInfo.timezone() ?? "UTC",
currency: documentSettings.currency?? "USD",
},
readonly: this.gristDoc.isReadonly.get(),
readonly,
showAfterReady,
onSettingsInitialized: async () => {
if (!this.customDef.renderAfterReady.peek()) {
await this.customDef.renderAfterReady.setAndSave(true);
}
},
configure: (frame) => {
this._frame = frame;
// Need to cast myself to a BaseView
@@ -266,6 +263,9 @@ export class CustomView extends Disposable {
theme: this.gristDoc.currentTheme,
}),
new MinimumLevel(AccessLevel.none)); // none access is enough
frame.useEvents(
ThemeNotifier.create(frame, this.gristDoc.currentTheme),
new MinimumLevel(AccessLevel.none));
},
onElem: (iframe) => onFrameFocus(iframe, () => {
if (this.isDisposed()) { return; }

View File

@@ -54,17 +54,10 @@ export interface WidgetFrameOptions {
/**
* If set, show the iframe after `grist.ready()`.
*
* Currently, this is only used to defer showing a widget until it has had
* a chance to apply the Grist theme.
* This is used to defer showing a widget on initial load until it has finished
* applying the Grist theme.
*/
showAfterReady?: boolean;
/**
* Handler for the settings initialized message.
*
* Currently, this is only used to defer showing a widget until it has had
* a chance to apply the Grist theme.
*/
onSettingsInitialized: () => void;
/**
* Optional callback to configure exposed API.
*/
@@ -216,9 +209,12 @@ export class WidgetFrame extends DisposableWithEvents {
this.trigger('ready', this);
this._readyCalled.set(true);
}
if (event.data.data?.settings?.status === 'initialized') {
if (
event.data.data?.message === 'themeInitialized' ||
// DEPRECATED: remove once the plugin API starts sending the message above.
event.data.data?.settings?.status === 'initialized'
) {
this._visible.set(true);
this._options.onSettingsInitialized();
}
this._rpc.receiveMessage(event.data);
}
@@ -580,56 +576,79 @@ export interface ConfigNotifierOptions {
}
/**
* Notifies about options change. Exposed in the API as a onOptions handler.
* Notifies about options changes. Exposed in the API as `onOptions`.
*/
export class ConfigNotifier extends BaseEventSource {
private _accessLevel = this._options.access;
private _theme = this._options.theme;
private _currentConfig: Computed<any | null>;
private _currentConfig = Computed.create(this, use => {
const options = use(this._section.activeCustomOptions);
return options;
});
// Debounced call to let the view know linked cursor changed.
private _debounced: (fromReady?: boolean) => void;
private _debounced = debounce((options?: {fromReady?: boolean}) => this._update(options), 0);
constructor(private _section: ViewSectionRec, private _options: ConfigNotifierOptions) {
super();
this._currentConfig = Computed.create(this, use => {
const options = use(this._section.activeCustomOptions);
return options;
});
this._debounced = debounce((fromReady?: boolean) => this._update(fromReady), 0);
const subscribe = (...observables: Observable<any>[]) => {
for (const obs of observables) {
this.autoDispose(
obs.addListener((cur, prev) => {
if (isEqual(prev, cur)) {
return;
}
this._debounced();
})
);
}
};
subscribe(this._currentConfig, this._theme);
this.autoDispose(
this._currentConfig.addListener((newConfig, oldConfig) => {
if (isEqual(newConfig, oldConfig)) { return; }
this._debounced();
})
);
}
protected _ready() {
// On ready, send initial configuration.
this._debounced(true);
this._debounced({fromReady: true});
}
private _update(fromReady = false) {
if (this.isDisposed()) {
return;
}
private _update({fromReady}: {fromReady?: boolean} = {}) {
if (this.isDisposed()) { return; }
this._notify({
options: this._currentConfig.get(),
settings: {
accessLevel: this._accessLevel,
theme: this._theme.get(),
// DEPRECATED: remove once the plugin API includes the `onThemeChange` handler.
...(fromReady ? {theme: this._theme.get()} : undefined),
},
fromReady,
});
}
}
/**
* Notifies about theme changes. Exposed in the API as `onThemeChange`.
*/
export class ThemeNotifier extends BaseEventSource {
constructor(private _theme: Computed<Theme>) {
super();
this.autoDispose(
this._theme.addListener((newTheme, oldTheme) => {
if (isEqual(newTheme, oldTheme)) { return; }
this._update();
})
);
}
protected _ready() {
this._update({fromReady: true});
}
private _update({fromReady}: {fromReady?: boolean} = {}) {
if (this.isDisposed()) { return; }
this._notify({
theme: this._theme.get(),
fromReady,
});
}
}
/**
* Notifies about cursor table data or structure change.
* Exposed in the API as a onRecords handler.

View File

@@ -2,6 +2,8 @@ import {ClientScope} from 'app/client/components/ClientScope';
import {SafeBrowser} from 'app/client/lib/SafeBrowser';
import {LocalPlugin} from 'app/common/plugin';
import {createRpcLogger, PluginInstance} from 'app/common/PluginInstance';
import {Theme} from 'app/common/ThemePrefs';
import {Computed} from 'grainjs';
/**
* Home plugins are all plugins that contributes to a general Grist management tasks.
@@ -13,9 +15,13 @@ export class HomePluginManager {
public pluginsList: PluginInstance[];
constructor(localPlugins: LocalPlugin[],
untrustedContentOrigin: string,
clientScope: ClientScope) {
constructor(options: {
localPlugins: LocalPlugin[],
untrustedContentOrigin: string,
clientScope: ClientScope,
theme: Computed<Theme>,
}) {
const {localPlugins, untrustedContentOrigin, clientScope, theme} = options;
this.pluginsList = [];
for (const plugin of localPlugins) {
try {
@@ -35,6 +41,7 @@ export class HomePluginManager {
clientScope,
untrustedContentOrigin,
mainPath: components.safeBrowser,
theme,
});
if (components.safeBrowser) {
pluginInstance.rpc.registerForwarder(components.safeBrowser, safeBrowser);

View File

@@ -73,7 +73,7 @@ export class SafeBrowser extends BaseComponent {
new IframeProcess(safeBrowser, rpc, src);
}
public theme? = this._options.theme;
public theme = this._options.theme;
// All view processes. This is not used anymore to dispose all processes on deactivation (this is
// now achieved using `this._mainProcess.autoDispose(...)`) but rather to be able to dispatch
@@ -94,10 +94,10 @@ export class SafeBrowser extends BaseComponent {
pluginInstance: PluginInstance,
clientScope: ClientScope,
untrustedContentOrigin: string,
theme: Computed<Theme>,
mainPath?: string,
baseLogger?: BaseLogger,
rpcLogger?: IRpcLogger,
theme?: Computed<Theme>,
}) {
super(
_options.pluginInstance.definition.manifest,
@@ -292,8 +292,8 @@ class WorkerProcess extends ClientProcess {
export class ViewProcess extends ClientProcess {
public element: HTMLElement;
// Set once all of the plugin's onOptions handlers have been called.
protected _optionsInitialized: Observable<boolean>;
// Set once all of the plugin's onThemeChange handlers have been called.
protected _themeInitialized: Observable<boolean>;
}
/**
@@ -302,21 +302,21 @@ export class ViewProcess extends ClientProcess {
class IframeProcess extends ViewProcess {
public create(safeBrowser: SafeBrowser, rpc: Rpc, src: string) {
super.create(safeBrowser, rpc, src);
this._optionsInitialized = Observable.create(this, false);
this._themeInitialized = Observable.create(this, false);
const iframe = this.element = this.autoDispose(
grainjsDom(`iframe.safe_browser_process.clipboard_focus`,
{src},
grainjsDom.style('visibility', use => use(this._optionsInitialized) ? 'visible' : 'hidden'),
grainjsDom.style('visibility', use => use(this._themeInitialized) ? 'visible' : 'hidden'),
) as HTMLIFrameElement
);
const listener = async (event: MessageEvent) => {
if (event.source === iframe.contentWindow) {
if (event.data.mtype === MsgType.Ready) {
await this._sendSettings({theme: safeBrowser.theme?.get()}, true);
await this._sendTheme({theme: safeBrowser.theme.get(), fromReady: true});
}
if (event.data.data?.settings?.status === 'initialized') {
this._optionsInitialized.set(true);
if (event.data.data?.message === 'themeInitialized') {
this._themeInitialized.set(true);
}
this.rpc.receiveMessage(event.data);
@@ -333,14 +333,14 @@ class IframeProcess extends ViewProcess {
safeBrowser.theme.addListener(async (newTheme, oldTheme) => {
if (isEqual(newTheme, oldTheme)) { return; }
await this._sendSettings({theme: safeBrowser.theme?.get()});
await this._sendTheme({theme: newTheme});
})
);
}
}
private async _sendSettings(settings: {theme?: Theme}, fromReady = false) {
await this.rpc.postMessage({settings, fromReady});
private async _sendTheme({theme, fromReady = false}: {theme: Theme, fromReady?: boolean}) {
await this.rpc.postMessage({theme, fromReady});
}
}

View File

@@ -186,10 +186,12 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
this._updateWorkspaces().catch(reportError)));
// Defer home plugin initialization
const pluginManager = new HomePluginManager(
_app.topAppModel.plugins,
_app.topAppModel.getUntrustedContentOrigin()!,
clientScope);
const pluginManager = new HomePluginManager({
localPlugins: _app.topAppModel.plugins,
untrustedContentOrigin: _app.topAppModel.getUntrustedContentOrigin()!,
clientScope,
theme: _app.currentTheme,
});
const importSources = ImportSourceElement.fromArray(pluginManager.pluginsList);
this.importSources.set(importSources);

View File

@@ -272,8 +272,8 @@ export interface CustomViewSectionDef {
/**
* If set, render the widget after `grist.ready()`.
*
* Currently, this is only used to defer rendering a widget until it has had
* a chance to apply the Grist theme.
* This is used to defer showing a widget on initial load until it has finished
* applying the Grist theme.
*/
renderAfterReady: modelUtil.KoSaveableObservable<boolean>;
}

View File

@@ -459,7 +459,7 @@ export class CustomSectionConfig extends Disposable {
}
bundleChanges(() => {
// Reset whether widget should render after `grist.ready()`.
_section.customDef.renderAfterReady(false);
_section.customDef.renderAfterReady(selectedWidget.renderAfterReady ?? false);
// Clear access level
_section.customDef.access(AccessLevel.none);
// When widget wants some access, set desired access level.
@@ -627,16 +627,16 @@ export class CustomSectionConfig extends Disposable {
protected async _getWidgets() {
const api = this._gristDoc.app.topAppModel.api;
const wigets = await api.getWidgets();
const widgets = await api.getWidgets();
// Request for rest of the widgets.
if (this._canSelect) {
// From the start we will provide single widget definition
// that was chosen previously.
if (this._section.customDef.widgetDef.peek()) {
wigets.push(this._section.customDef.widgetDef.peek()!);
widgets.push(this._section.customDef.widgetDef.peek()!);
}
}
this._widgets.set(wigets);
this._widgets.set(widgets);
}
private _accept() {