mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
bundling experiments (WIP)
Looking at ways to bundle custom widgets with Grist. WIP, experimental, everything will need rewrite.
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import {AccessLevel} from "app/common/CustomWidget";
|
||||
import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
|
||||
import {CustomView} from "app/client/components/CustomView";
|
||||
import {GristDoc} from "app/client/components/GristDoc";
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
// import {AccessLevel} from "app/common/CustomWidget";
|
||||
// import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
|
||||
import { CustomView, CustomViewSettings } from "app/client/components/CustomView";
|
||||
import { AccessLevel } from "app/common/CustomWidget";
|
||||
// import {GristDoc} from "app/client/components/GristDoc";
|
||||
// import {reportError} from 'app/client/models/errors';
|
||||
|
||||
//Abstract class for more future inheritances
|
||||
abstract class CustomAttachedView extends CustomView {
|
||||
// abstract class CustomAttachedView extends CustomView {
|
||||
/*
|
||||
public override create(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
|
||||
super.create(gristDoc, viewSectionModel);
|
||||
if (viewSectionModel.customDef.access.peek() !== AccessLevel.full) {
|
||||
@@ -18,7 +20,7 @@ abstract class CustomAttachedView extends CustomView {
|
||||
});
|
||||
}
|
||||
|
||||
const widgetsApi = this.gristDoc.app.topAppModel.api;
|
||||
const widgetsApi = this.gristDoc.app.topAppModel;
|
||||
widgetsApi.getWidgets().then(async result=>{
|
||||
const widget = result.find(w=>w.name == this.getWidgetName());
|
||||
if (widget && this.customDef.url.peek() !== widget.url) {
|
||||
@@ -34,13 +36,17 @@ abstract class CustomAttachedView extends CustomView {
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
protected abstract getWidgetName(): string;
|
||||
// protected abstract getWidgetName(): string;
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
export class CustomCalendarView extends CustomAttachedView {
|
||||
protected getWidgetName(): string {
|
||||
return "Calendar";
|
||||
export class CustomCalendarView extends CustomView {
|
||||
protected getInitialSettings(): CustomViewSettings {
|
||||
return {
|
||||
widgetId: '@gristlabs/widget-calendar',
|
||||
accessLevel: AccessLevel.full,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ import {dom as grains} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import defaults = require('lodash/defaults');
|
||||
|
||||
export interface CustomViewSettings {
|
||||
widgetId?: string;
|
||||
accessLevel?: AccessLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* CustomView components displays arbitrary html. There are two modes available, in the "url" mode
|
||||
@@ -81,11 +85,10 @@ export class CustomView extends Disposable {
|
||||
|
||||
private _frame: WidgetFrame; // plugin frame (holding external page)
|
||||
|
||||
|
||||
public create(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
|
||||
BaseView.call(this as any, gristDoc, viewSectionModel, { 'addNewRow': true });
|
||||
|
||||
this.customDef = this.viewSection.customDef;
|
||||
this.customDef = this.viewSection.customDef;
|
||||
|
||||
this.autoDisposeCallback(() => {
|
||||
if (this._customSection) {
|
||||
@@ -103,8 +106,43 @@ export class CustomView extends Disposable {
|
||||
|
||||
this.viewPane = this.autoDispose(this._buildDom());
|
||||
this._updatePluginInstance();
|
||||
|
||||
this.dealWithBundledWidgets(gristDoc, viewSectionModel);
|
||||
}
|
||||
|
||||
public dealWithBundledWidgets(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
|
||||
const settings = this.getInitialSettings();
|
||||
console.log("dealWith!", {settings});
|
||||
if (!settings.widgetId) { return; }
|
||||
if (viewSectionModel.customDef.access.peek() !== AccessLevel.full) {
|
||||
void viewSectionModel.customDef.access.setAndSave(AccessLevel.full).catch((err)=>{
|
||||
if (err?.code === "ACL_DENY") {
|
||||
// do nothing, we might be in a readonly mode.
|
||||
return;
|
||||
}
|
||||
reportError(err);
|
||||
});
|
||||
}
|
||||
|
||||
const widgetsApi = this.gristDoc.app.topAppModel;
|
||||
widgetsApi.getWidgets().then(async result=>{
|
||||
const widget = result.find(w => w.widgetId === settings.widgetId);
|
||||
console.log("FOUND", {widget});
|
||||
if (widget && this.customDef.widgetId.peek() !== widget.widgetId) {
|
||||
console.log("SET!!");
|
||||
await this.customDef.widgetId.setAndSave(widget.widgetId);
|
||||
await this.customDef.pluginId.setAndSave(widget.fromPlugin||'');
|
||||
}
|
||||
}).catch((err)=>{
|
||||
if (err?.code !== "ACL_DENY") {
|
||||
// TODO: revisit it later. getWidgets() is async call, and non of the code
|
||||
// above is checking if we are still alive.
|
||||
console.error(err);
|
||||
} else {
|
||||
// do nothing, we might be in a readonly mode.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async triggerPrint() {
|
||||
if (!this.isDisposed() && this._frame) {
|
||||
@@ -112,9 +150,14 @@ export class CustomView extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected getInitialSettings(): CustomViewSettings {
|
||||
return {};
|
||||
}
|
||||
|
||||
protected getEmptyWidgetPage(): string {
|
||||
return new URL("custom-widget.html", getGristConfig().homeUrl!).href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a plugin instance that matches the plugin id, update the `found` observables, then tries to
|
||||
* find a matching section.
|
||||
@@ -154,13 +197,16 @@ export class CustomView extends Disposable {
|
||||
}
|
||||
|
||||
private _buildDom() {
|
||||
const {mode, url, access, renderAfterReady} = this.customDef;
|
||||
const {mode, url, access, renderAfterReady, widgetId, pluginId} = this.customDef;
|
||||
const showPlugin = ko.pureComputed(() => this.customDef.mode() === "plugin");
|
||||
const showAfterReady = () => {
|
||||
// The empty widget page calls `grist.ready()`.
|
||||
// Pending: URLs set now only when user actually enters a URL,
|
||||
// so this could be breaking pages without grist.ready() call
|
||||
// added to manifests.
|
||||
if (!url()) { return true; }
|
||||
|
||||
return this.customDef.widgetDef()?.renderAfterReady ?? renderAfterReady();
|
||||
return renderAfterReady();
|
||||
};
|
||||
|
||||
// When both plugin and section are not found, let's show only plugin notification.
|
||||
@@ -176,12 +222,14 @@ export class CustomView extends Disposable {
|
||||
dom.autoDispose(showSectionNotification),
|
||||
dom.autoDispose(showPluginContent),
|
||||
// todo: should display content in webview when running electron
|
||||
kd.scope(() => [mode(), url(), access()], ([_mode, _url, _access]: string[]) =>
|
||||
kd.scope(() => [mode(), url(), access(), widgetId(), pluginId()], ([_mode, _url, _access, _widgetId, _pluginId]: string[]) =>
|
||||
_mode === "url" ?
|
||||
this._buildIFrame({
|
||||
baseUrl: _url,
|
||||
access: (_access as AccessLevel || AccessLevel.none),
|
||||
showAfterReady: showAfterReady(),
|
||||
widgetId: _widgetId,
|
||||
pluginId: _pluginId,
|
||||
})
|
||||
: null
|
||||
),
|
||||
@@ -211,10 +259,15 @@ export class CustomView extends Disposable {
|
||||
baseUrl: string|null,
|
||||
access: AccessLevel,
|
||||
showAfterReady?: boolean,
|
||||
widgetId?: string|null,
|
||||
pluginId?: string,
|
||||
}) {
|
||||
const {baseUrl, access, showAfterReady} = options;
|
||||
const {baseUrl, access, showAfterReady, widgetId, pluginId} = options;
|
||||
return grains.create(WidgetFrame, {
|
||||
url: baseUrl || this.getEmptyWidgetPage(),
|
||||
widgetId,
|
||||
pluginId,
|
||||
emptyUrl: this.getEmptyWidgetPage(),
|
||||
access,
|
||||
readonly: this.gristDoc.isReadonly.get(),
|
||||
showAfterReady,
|
||||
@@ -265,7 +318,8 @@ export class CustomView extends Disposable {
|
||||
}
|
||||
// allow menus to close if any
|
||||
closeRegisteredMenu();
|
||||
})
|
||||
}),
|
||||
gristDoc: this.gristDoc,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {hooks} from 'app/client/Hooks';
|
||||
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
||||
import {makeTestId} from 'app/client/lib/domUtils';
|
||||
import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {AccessLevel, isSatisfied} from 'app/common/CustomWidget';
|
||||
import {AccessLevel, ICustomWidget, isSatisfied, matchWidget } from 'app/common/CustomWidget';
|
||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||
import {BulkColValues, fromTableDataAction, RowRecord} from 'app/common/DocActions';
|
||||
import {extractInfoFromColType, reencodeAsAny} from 'app/common/gristTypes';
|
||||
@@ -19,6 +19,7 @@ import noop = require('lodash/noop');
|
||||
import debounce = require('lodash/debounce');
|
||||
import isEqual = require('lodash/isEqual');
|
||||
import flatMap = require('lodash/flatMap');
|
||||
import { reportError } from '../models/errors';
|
||||
|
||||
const testId = makeTestId('test-custom-widget-');
|
||||
|
||||
@@ -43,6 +44,9 @@ export interface WidgetFrameOptions {
|
||||
* Url of external page. Iframe is rebuild each time the URL changes.
|
||||
*/
|
||||
url: string;
|
||||
widgetId?: string|null;
|
||||
pluginId?: string;
|
||||
emptyUrl: string;
|
||||
/**
|
||||
* Assigned access level. Iframe is rebuild each time access level is changed.
|
||||
*/
|
||||
@@ -73,6 +77,8 @@ export interface WidgetFrameOptions {
|
||||
* Optional handler to modify the iframe.
|
||||
*/
|
||||
onElem?: (iframe: HTMLIFrameElement) => void;
|
||||
|
||||
gristDoc: GristDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +93,7 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
private _readyCalled = Observable.create(this, false);
|
||||
// Whether the iframe is visible.
|
||||
private _visible = Observable.create(this, !this._options.showAfterReady);
|
||||
public readonly _widgets = Observable.create<ICustomWidget[]>(this, []);
|
||||
|
||||
constructor(private _options: WidgetFrameOptions) {
|
||||
super();
|
||||
@@ -113,7 +120,10 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
|
||||
// Call custom configuration handler.
|
||||
_options.configure?.(this);
|
||||
|
||||
this._fetchWidgets().catch(reportError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an EventSource with desired access level.
|
||||
*/
|
||||
@@ -167,6 +177,23 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
const onElem = this._options.onElem ?? ((el: HTMLIFrameElement) => el);
|
||||
return onElem(
|
||||
(this._iframe = dom(
|
||||
'iframe',
|
||||
dom.style('visibility', use => use(this._visible) ? 'visible' : 'hidden'),
|
||||
dom.cls('clipboard_focus'),
|
||||
dom.cls('custom_view'),
|
||||
dom.attr('src', use => this._getUrl(use(this._widgets))),
|
||||
{
|
||||
...hooks.iframeAttributes,
|
||||
},
|
||||
testId('ready', this._readyCalled),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
private _getUrl(widgets: ICustomWidget[]): string {
|
||||
// Append access level to query string.
|
||||
const urlWithAccess = (url: string) => {
|
||||
if (!url) {
|
||||
@@ -177,19 +204,20 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
urlObj.searchParams.append('readonly', String(this._options.readonly));
|
||||
return urlObj.href;
|
||||
};
|
||||
const fullUrl = urlWithAccess(this._options.url);
|
||||
const onElem = this._options.onElem ?? ((el: HTMLIFrameElement) => el);
|
||||
return onElem(
|
||||
(this._iframe = dom('iframe',
|
||||
dom.style('visibility', use => use(this._visible) ? 'visible' : 'hidden'),
|
||||
dom.cls('clipboard_focus'),
|
||||
dom.cls('custom_view'), {
|
||||
src: fullUrl,
|
||||
...hooks.iframeAttributes,
|
||||
},
|
||||
testId('ready', this._readyCalled),
|
||||
))
|
||||
);
|
||||
const {widgetId, pluginId} = this._options;
|
||||
let url = this._options.url;
|
||||
if (widgetId) {
|
||||
console.log("Iframe match starting");
|
||||
const widget = matchWidget(widgets, {widgetId, pluginId});
|
||||
console.log("Iframe match done");
|
||||
if (widget) {
|
||||
url = widget.url;
|
||||
} else {
|
||||
return 'about:blank';
|
||||
}
|
||||
}
|
||||
const fullUrl = urlWithAccess(url);
|
||||
return fullUrl;
|
||||
}
|
||||
|
||||
private _onMessage(event: MessageEvent) {
|
||||
@@ -216,6 +244,14 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
this._rpc.receiveMessage(event.data);
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchWidgets() {
|
||||
if (this.isDisposed()) { return; }
|
||||
const widgets = await this._options.gristDoc.app.topAppModel.getWidgets();
|
||||
if (this.isDisposed()) { return; }
|
||||
this._widgets.set(widgets);
|
||||
console.log("SAVED", {widgets});
|
||||
}
|
||||
}
|
||||
|
||||
const throwError = (access: AccessLevel) => {
|
||||
|
||||
@@ -27,6 +27,8 @@ import {getOrgName, isTemplatesOrg, Organization, OrgError, UserAPI, UserAPIImpl
|
||||
import {getUserPrefObs, getUserPrefsObs, markAsSeen, markAsUnSeen} from 'app/client/models/UserPrefs';
|
||||
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { ICustomWidget } from 'app/common/CustomWidget';
|
||||
import { AsyncCreate } from 'app/common/AsyncCreate';
|
||||
|
||||
const t = makeT('AppModel');
|
||||
|
||||
@@ -75,6 +77,8 @@ export interface TopAppModel {
|
||||
* Reloads orgs and accounts for current user.
|
||||
*/
|
||||
fetchUsersAndOrgs(): Promise<void>;
|
||||
|
||||
getWidgets(): Promise<ICustomWidget[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,6 +147,7 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
|
||||
public readonly users = Observable.create<FullUser[]>(this, []);
|
||||
public readonly plugins: LocalPlugin[] = [];
|
||||
private readonly _gristConfig?: GristLoadConfig;
|
||||
private readonly _widgets: AsyncCreate<ICustomWidget[]>;
|
||||
|
||||
constructor(
|
||||
window: {gristConfig?: GristLoadConfig},
|
||||
@@ -153,6 +158,7 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
|
||||
this.isSingleOrg = Boolean(window.gristConfig && window.gristConfig.singleOrg);
|
||||
this.productFlavor = getFlavor(window.gristConfig && window.gristConfig.org);
|
||||
this._gristConfig = window.gristConfig;
|
||||
this._widgets = new AsyncCreate<ICustomWidget[]>(() => this.api.getWidgets());
|
||||
|
||||
// Initially, and on any change to subdomain, call initialize() to get the full Organization
|
||||
// and the FullUser to use for it (the user may change when switching orgs).
|
||||
@@ -175,6 +181,10 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
public async getWidgets(): Promise<ICustomWidget[]> {
|
||||
return this._widgets.get();
|
||||
}
|
||||
|
||||
public getUntrustedContentOrigin() {
|
||||
if (G.window.isRunningUnderElectron) {
|
||||
// when loaded within webviews it is safe to serve plugin's content from the same domain
|
||||
|
||||
@@ -21,7 +21,7 @@ import {removeRule, RuleOwner} from 'app/client/models/RuleOwner';
|
||||
import {LinkConfig} from 'app/client/ui/selectBy';
|
||||
import {getWidgetTypes} from "app/client/ui/widgetTypesMap";
|
||||
import {FilterColValues} from "app/common/ActiveDocAPI";
|
||||
import {AccessLevel, ICustomWidget} from 'app/common/CustomWidget';
|
||||
import {AccessLevel} from 'app/common/CustomWidget';
|
||||
import {UserAction} from 'app/common/DocActions';
|
||||
import {arrayRepeat} from 'app/common/gutil';
|
||||
import {Sort} from 'app/common/SortSpec';
|
||||
@@ -245,10 +245,19 @@ export interface CustomViewSectionDef {
|
||||
* The url.
|
||||
*/
|
||||
url: modelUtil.KoSaveableObservable<string|null>;
|
||||
/**
|
||||
* A widgetId, if available. Preferred to url.
|
||||
* For bundled custom widgets, it is important to refer
|
||||
* to them by something other than url, since url will
|
||||
* vary with deployment, and it should be possible to move
|
||||
* documents between deployments if they have compatible
|
||||
* widgets available.
|
||||
*/
|
||||
widgetId: modelUtil.KoSaveableObservable<string|null>;
|
||||
/**
|
||||
* Custom widget information.
|
||||
*/
|
||||
widgetDef: modelUtil.KoSaveableObservable<ICustomWidget|null>;
|
||||
// widgetDef: modelUtil.KoSaveableObservable<ICustomWidget|null>;
|
||||
/**
|
||||
* Custom widget options.
|
||||
*/
|
||||
@@ -324,7 +333,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
const customViewDefaults = {
|
||||
mode: 'url',
|
||||
url: null,
|
||||
widgetDef: null,
|
||||
// widgetDef: null,
|
||||
access: '',
|
||||
pluginId: '',
|
||||
sectionId: '',
|
||||
@@ -336,7 +345,8 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
this.customDef = {
|
||||
mode: customDefObj.prop('mode'),
|
||||
url: customDefObj.prop('url'),
|
||||
widgetDef: customDefObj.prop('widgetDef'),
|
||||
widgetId: customDefObj.prop('widgetId'),
|
||||
// widgetDef: customDefObj.prop('widgetDef'),
|
||||
widgetOptions: customDefObj.prop('widgetOptions'),
|
||||
columnsMapping: customDefObj.prop('columnsMapping'),
|
||||
access: customDefObj.prop('access'),
|
||||
|
||||
@@ -15,7 +15,7 @@ import {IconName} from 'app/client/ui2018/IconList';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {cssLink} from 'app/client/ui2018/links';
|
||||
import {IOptionFull, menu, menuItem, menuText, select} from 'app/client/ui2018/menus';
|
||||
import {AccessLevel, ICustomWidget, isSatisfied} from 'app/common/CustomWidget';
|
||||
import { AccessLevel, ICustomWidget, isSatisfied, matchWidget } from 'app/common/CustomWidget';
|
||||
import {GristLoadConfig} from 'app/common/gristUrls';
|
||||
import {unwrap} from 'app/common/gutil';
|
||||
import {
|
||||
@@ -322,7 +322,8 @@ export class CustomSectionConfig extends Disposable {
|
||||
|
||||
// Test if we can offer widget list.
|
||||
const gristConfig: GristLoadConfig = (window as any).gristConfig || {};
|
||||
this._canSelect = gristConfig.enableWidgetRepository ?? true;
|
||||
console.log("Ignoring gristConfig now", {gristConfig});
|
||||
this._canSelect = true; // gristConfig.enableWidgetRepository ?? true;
|
||||
|
||||
// Array of available widgets - will be updated asynchronously.
|
||||
this._widgets = Observable.create(this, []);
|
||||
@@ -331,12 +332,16 @@ export class CustomSectionConfig extends Disposable {
|
||||
|
||||
// Selected value from the dropdown (contains widgetId or "custom" string for Custom URL)
|
||||
this._selectedId = Computed.create(this, use => {
|
||||
if (use(_section.customDef.widgetDef)) {
|
||||
return _section.customDef.widgetDef.peek()!.widgetId;
|
||||
const widgetId = use(_section.customDef.widgetId);
|
||||
const pluginId = use(_section.customDef.pluginId);
|
||||
if (widgetId) {
|
||||
console.log("_selectedId", {widgetId, pluginId});
|
||||
return (pluginId||'') + ':' + widgetId;
|
||||
}
|
||||
return CUSTOM_ID;
|
||||
});
|
||||
this._selectedId.onWrite(async value => {
|
||||
console.log("_selectedId onWrite", {value});
|
||||
if (value === CUSTOM_ID) {
|
||||
// Select Custom URL
|
||||
bundleChanges(() => {
|
||||
@@ -344,8 +349,11 @@ export class CustomSectionConfig extends Disposable {
|
||||
_section.customDef.renderAfterReady(false);
|
||||
// Clear url.
|
||||
_section.customDef.url(null);
|
||||
// Clear widgetId
|
||||
_section.customDef.widgetId(null);
|
||||
_section.customDef.pluginId('');
|
||||
// Clear widget definition.
|
||||
_section.customDef.widgetDef(null);
|
||||
// _section.customDef.widgetDef(null);
|
||||
// Reset access level to none.
|
||||
_section.customDef.access(AccessLevel.none);
|
||||
// Clear all saved options.
|
||||
@@ -359,27 +367,50 @@ export class CustomSectionConfig extends Disposable {
|
||||
});
|
||||
await _section.saveCustomDef();
|
||||
} else {
|
||||
const [pluginId, widgetId] = value?.split(':') || [];
|
||||
// Select Widget
|
||||
const selectedWidget = this._widgets.get().find(w => w.widgetId === value);
|
||||
console.log("Start match");
|
||||
const selectedWidget = matchWidget(this._widgets.get(), {
|
||||
widgetId,
|
||||
pluginId,
|
||||
});
|
||||
console.log("Started match");
|
||||
console.log("SETTING", {pluginId, widgetId, selectedWidget});
|
||||
if (!selectedWidget) {
|
||||
// should not happen
|
||||
throw new Error('Error accessing widget from the list');
|
||||
}
|
||||
// If user selected the same one, do nothing.
|
||||
if (_section.customDef.widgetDef.peek()?.widgetId === value) {
|
||||
if (_section.customDef.widgetId.peek() === widgetId &&
|
||||
_section.customDef.pluginId.peek() === pluginId) {
|
||||
console.log("DO NOTHING", {
|
||||
widgetId,
|
||||
pluginId,
|
||||
owidgetId: _section.customDef.widgetId.peek(),
|
||||
opluginId: _section.customDef.pluginId.peek(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
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.
|
||||
this._desiredAccess.set(selectedWidget.accessLevel || AccessLevel.none);
|
||||
// Update widget definition.
|
||||
_section.customDef.widgetDef(selectedWidget);
|
||||
// _section.customDef.widgetDef(selectedWidget);
|
||||
// Update widgetId.
|
||||
_section.customDef.widgetId(selectedWidget.widgetId);
|
||||
_section.customDef.pluginId(selectedWidget.fromPlugin || '');
|
||||
console.log({
|
||||
setty: 1,
|
||||
widgetId: selectedWidget.widgetId,
|
||||
pluginId: selectedWidget.fromPlugin || '',
|
||||
selectedWidget
|
||||
});
|
||||
// Update widget URL.
|
||||
_section.customDef.url(selectedWidget.url);
|
||||
_section.customDef.url(null);
|
||||
// Clear options.
|
||||
_section.customDef.widgetOptions(null);
|
||||
// Clear has custom configuration.
|
||||
@@ -389,6 +420,7 @@ export class CustomSectionConfig extends Disposable {
|
||||
_section.columnsToMap(null);
|
||||
});
|
||||
await _section.saveCustomDef();
|
||||
console.log("CustomSectionConfig saved");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -398,7 +430,12 @@ export class CustomSectionConfig extends Disposable {
|
||||
this._url.onWrite(async newUrl => {
|
||||
bundleChanges(() => {
|
||||
_section.customDef.renderAfterReady(false);
|
||||
_section.customDef.url(newUrl);
|
||||
if (newUrl) {
|
||||
console.log("ZAP widgetId and pluginId");
|
||||
_section.customDef.widgetId(null);
|
||||
_section.customDef.pluginId('');
|
||||
}
|
||||
//_section.customDef.url(newUrl);
|
||||
});
|
||||
await _section.saveCustomDef();
|
||||
});
|
||||
@@ -423,6 +460,11 @@ export class CustomSectionConfig extends Disposable {
|
||||
const holder = new MultiHolder();
|
||||
|
||||
// Show prompt, when desired access level is different from actual one.
|
||||
function makeLabel(widget: ICustomWidget) {
|
||||
if (!widget.fromPlugin) { return widget.name; }
|
||||
const group = widget.fromPlugin.replace('builtIn/', '');
|
||||
return `${widget.name} (${group})`;
|
||||
}
|
||||
const prompt = Computed.create(holder, use =>
|
||||
use(this._desiredAccess)
|
||||
&& !isSatisfied(use(this._currentAccess), use(this._desiredAccess)!));
|
||||
@@ -433,7 +475,9 @@ export class CustomSectionConfig extends Disposable {
|
||||
// Options for the select-box (all widgets definitions and Custom URL)
|
||||
const options = Computed.create(holder, use => [
|
||||
{label: 'Custom URL', value: 'custom'},
|
||||
...use(this._widgets).map(w => ({label: w.name, value: w.widgetId})),
|
||||
...use(this._widgets).map(w => ({
|
||||
label: makeLabel(w), value: ((w.fromPlugin||'') + ':' + w.widgetId)
|
||||
})),
|
||||
]);
|
||||
function buildPrompt(level: AccessLevel|null) {
|
||||
if (!level) {
|
||||
@@ -469,7 +513,7 @@ export class CustomSectionConfig extends Disposable {
|
||||
testId('select')
|
||||
)
|
||||
: null,
|
||||
dom.maybe(isCustom && this.shouldRenderWidgetSelector(), () => [
|
||||
dom.maybe((use) => use(isCustom) && this.shouldRenderWidgetSelector(), () => [
|
||||
cssRow(
|
||||
cssTextInput(
|
||||
this._url,
|
||||
@@ -538,17 +582,23 @@ export class CustomSectionConfig extends Disposable {
|
||||
}
|
||||
|
||||
protected async _getWidgets() {
|
||||
const api = this._gristDoc.app.topAppModel.api;
|
||||
const wigets = await api.getWidgets();
|
||||
const widgets = await this._gristDoc.app.topAppModel.getWidgets();
|
||||
/*
|
||||
const widgets = filterWidgets(widgets1, {
|
||||
keepWidgetIdUnique: true,
|
||||
preferPlugin: false,
|
||||
});
|
||||
*/
|
||||
// const wigets = 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()!);
|
||||
}
|
||||
// if (this._section.customDef.widgetDef.peek()) {
|
||||
// wigets.push(this._section.customDef.widgetDef.peek()!);
|
||||
// }
|
||||
}
|
||||
this._widgets.set(wigets);
|
||||
this._widgets.set(widgets);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -220,6 +220,7 @@ export function multiSelect<T>(selectedOptions: MutableObsArray<T>,
|
||||
},
|
||||
dom.domComputed(selectedOptionsSet, selectedOpts => {
|
||||
return dom.forEach(availableOptions, option => {
|
||||
console.log(">>> option", {availableOptions});
|
||||
const fullOption = weasel.getOptionFull(option);
|
||||
return cssCheckboxLabel(
|
||||
cssCheckboxSquare(
|
||||
|
||||
Reference in New Issue
Block a user