mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) custom widget appear as build-in widget
Summary: Added boilerplate code needed to create new wigets in "Add new" menu, that are wrapped around existing custom widgets. More details can be found here: https://grist.quip.com/larhAGRKyl6Z/Custom-widgets-in-Add-Widget-menu Test Plan: nbowser tests added to verify if item in menu exits, if widget is rendered, and right side menu has widget selection and read access selection hided. Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3994
This commit is contained in:
31
app/client/components/CustomCalendarView.ts
Normal file
31
app/client/components/CustomCalendarView.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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";
|
||||
|
||||
//Abstract class for more future inheritances
|
||||
abstract class CustomAttachedView extends CustomView {
|
||||
public override create(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
|
||||
super.create(gristDoc, viewSectionModel);
|
||||
void viewSectionModel.customDef.access.setAndSave(AccessLevel.full);
|
||||
|
||||
const widgetsApi = this.gristDoc.app.topAppModel.api;
|
||||
widgetsApi.getWidgets().then(async result=>{
|
||||
const widget = result.find(w=>w.name == this.getWidgetName());
|
||||
if(widget) {
|
||||
await this.customDef.url.setAndSave(widget.url);
|
||||
}
|
||||
}).catch(()=>{
|
||||
//do nothing
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract getWidgetName(): string;
|
||||
|
||||
}
|
||||
|
||||
export class CustomCalendarView extends CustomAttachedView {
|
||||
protected getWidgetName(): string {
|
||||
return "Calendar";
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
import BaseView from 'app/client/components/BaseView';
|
||||
import {Cursor} from 'app/client/components/Cursor';
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import {Cursor} from 'app/client/components/Cursor';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {ConfigNotifier, CustomSectionAPIImpl, GristDocAPIImpl, GristViewImpl,
|
||||
MinimumLevel, RecordNotifier, TableNotifier, WidgetAPIImpl,
|
||||
WidgetFrame} from 'app/client/components/WidgetFrame';
|
||||
import {
|
||||
ConfigNotifier,
|
||||
CustomSectionAPIImpl,
|
||||
GristDocAPIImpl,
|
||||
GristViewImpl,
|
||||
MinimumLevel,
|
||||
RecordNotifier,
|
||||
TableNotifier,
|
||||
WidgetAPIImpl,
|
||||
WidgetFrame
|
||||
} from 'app/client/components/WidgetFrame';
|
||||
import {CustomSectionElement, ViewProcess} from 'app/client/lib/CustomSectionElement';
|
||||
import {Disposable} from 'app/client/lib/dispose';
|
||||
import dom from 'app/client/lib/dom';
|
||||
@@ -14,15 +22,16 @@ import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {CustomViewSectionDef} from 'app/client/models/entities/ViewSectionRec';
|
||||
import {UserError} from 'app/client/models/errors';
|
||||
import {SortedRowSet} from 'app/client/models/rowset';
|
||||
import {PluginInstance} from 'app/common/PluginInstance';
|
||||
import {AccessLevel} from 'app/common/CustomWidget';
|
||||
import {closeRegisteredMenu} from 'app/client/ui2018/menus';
|
||||
import {AccessLevel} from 'app/common/CustomWidget';
|
||||
import {PluginInstance} from 'app/common/PluginInstance';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {Events as BackboneEvents} from 'backbone';
|
||||
import {dom as grains} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import defaults = require('lodash/defaults');
|
||||
|
||||
|
||||
/**
|
||||
* CustomView components displays arbitrary html. There are two modes available, in the "url" mode
|
||||
* the content is hosted by a third-party (for instance a github page), as opposed to the "plugin"
|
||||
@@ -60,7 +69,7 @@ export class CustomView extends Disposable {
|
||||
protected gristDoc: GristDoc;
|
||||
protected cursor: Cursor;
|
||||
|
||||
private _customDef: CustomViewSectionDef;
|
||||
protected customDef: CustomViewSectionDef;
|
||||
|
||||
// state of the component
|
||||
private _foundPlugin: ko.Observable<boolean>;
|
||||
@@ -70,14 +79,12 @@ export class CustomView extends Disposable {
|
||||
private _pluginInstance: PluginInstance|undefined;
|
||||
|
||||
private _frame: WidgetFrame; // plugin frame (holding external page)
|
||||
private _emptyWidgetPage: string;
|
||||
|
||||
|
||||
public create(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
|
||||
BaseView.call(this as any, gristDoc, viewSectionModel, { 'addNewRow': true });
|
||||
|
||||
this._customDef = this.viewSection.customDef;
|
||||
|
||||
this._emptyWidgetPage = new URL("custom-widget.html", getGristConfig().homeUrl!).href;
|
||||
this.customDef = this.viewSection.customDef;
|
||||
|
||||
this.autoDisposeCallback(() => {
|
||||
if (this._customSection) {
|
||||
@@ -89,27 +96,31 @@ export class CustomView extends Disposable {
|
||||
// Ensure that selecting another section in same plugin update the view.
|
||||
this._foundSection.extend({notify: 'always'});
|
||||
|
||||
this.autoDispose(this._customDef.pluginId.subscribe(this._updatePluginInstance, this));
|
||||
this.autoDispose(this._customDef.sectionId.subscribe(this._updateCustomSection, this));
|
||||
this.autoDispose(this.customDef.pluginId.subscribe(this._updatePluginInstance, this));
|
||||
this.autoDispose(this.customDef.sectionId.subscribe(this._updateCustomSection, this));
|
||||
this.autoDispose(commands.createGroup(CustomView._commands, this, this.viewSection.hasFocus));
|
||||
|
||||
this.viewPane = this.autoDispose(this._buildDom());
|
||||
this._updatePluginInstance();
|
||||
}
|
||||
|
||||
|
||||
public async triggerPrint() {
|
||||
if (!this.isDisposed() && this._frame) {
|
||||
return await this._frame.callRemote('print');
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
private _updatePluginInstance() {
|
||||
|
||||
const pluginId = this._customDef.pluginId();
|
||||
const pluginId = this.customDef.pluginId();
|
||||
this._pluginInstance = this.gristDoc.docPluginManager.pluginsList.find(p => p.definition.id === pluginId);
|
||||
|
||||
if (this._pluginInstance) {
|
||||
@@ -129,7 +140,7 @@ export class CustomView extends Disposable {
|
||||
|
||||
if (!this._pluginInstance) { return; }
|
||||
|
||||
const sectionId = this._customDef.sectionId();
|
||||
const sectionId = this.customDef.sectionId();
|
||||
this._customSection = CustomSectionElement.find(this._pluginInstance, sectionId);
|
||||
|
||||
if (this._customSection) {
|
||||
@@ -142,8 +153,8 @@ export class CustomView extends Disposable {
|
||||
}
|
||||
|
||||
private _buildDom() {
|
||||
const {mode, url, access} = this._customDef;
|
||||
const showPlugin = ko.pureComputed(() => this._customDef.mode() === "plugin");
|
||||
const {mode, url, access} = this.customDef;
|
||||
const showPlugin = ko.pureComputed(() => this.customDef.mode() === "plugin");
|
||||
|
||||
// When both plugin and section are not found, let's show only plugin notification.
|
||||
const showPluginNotification = ko.pureComputed(() => showPlugin() && !this._foundPlugin());
|
||||
@@ -161,12 +172,12 @@ export class CustomView extends Disposable {
|
||||
kd.scope(() => [mode(), url(), access()], ([_mode, _url, _access]: string[]) =>
|
||||
_mode === "url" ? this._buildIFrame(_url, (_access || AccessLevel.none) as AccessLevel) : null),
|
||||
kd.maybe(showPluginNotification, () => buildNotification('Plugin ',
|
||||
dom('strong', kd.text(this._customDef.pluginId)), ' was not found',
|
||||
dom('strong', kd.text(this.customDef.pluginId)), ' was not found',
|
||||
dom.testId('customView_notification_plugin')
|
||||
)),
|
||||
kd.maybe(showSectionNotification, () => buildNotification('Section ',
|
||||
dom('strong', kd.text(this._customDef.sectionId)), ' was not found in plugin ',
|
||||
dom('strong', kd.text(this._customDef.pluginId)),
|
||||
dom('strong', kd.text(this.customDef.sectionId)), ' was not found in plugin ',
|
||||
dom('strong', kd.text(this.customDef.pluginId)),
|
||||
dom.testId('customView_notification_section')
|
||||
)),
|
||||
// When showPluginContent() is true then _foundSection() is also and _customSection is not
|
||||
@@ -184,7 +195,7 @@ export class CustomView extends Disposable {
|
||||
|
||||
private _buildIFrame(baseUrl: string, access: AccessLevel) {
|
||||
return grains.create(WidgetFrame, {
|
||||
url: baseUrl || this._emptyWidgetPage,
|
||||
url: baseUrl || this.getEmptyWidgetPage(),
|
||||
access,
|
||||
readonly: this.gristDoc.isReadonly.get(),
|
||||
configure: (frame) => {
|
||||
|
||||
@@ -47,7 +47,7 @@ import {IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
||||
import {linkFromId, selectBy} from 'app/client/ui/selectBy';
|
||||
import {WebhookPage} from 'app/client/ui/WebhookPage';
|
||||
import {startWelcomeTour} from 'app/client/ui/WelcomeTour';
|
||||
import {IWidgetType} from 'app/client/ui/widgetTypes';
|
||||
import {IWidgetType} from 'app/common/widgetTypes';
|
||||
import {PlayerState, YouTubePlayer} from 'app/client/ui/YouTubePlayer';
|
||||
import {isNarrowScreen, mediaSmall, mediaXSmall, testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import {IconName} from 'app/client/ui2018/IconList';
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import BaseView from 'app/client/components/BaseView';
|
||||
import {buildViewSectionDom} from 'app/client/components/buildViewSectionDom';
|
||||
import {ChartView} from 'app/client/components/ChartView';
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import {CustomCalendarView} from "app/client/components/CustomCalendarView";
|
||||
import {CustomView} from 'app/client/components/CustomView';
|
||||
import * as DetailView from 'app/client/components/DetailView';
|
||||
import * as GridView from 'app/client/components/GridView';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {BoxSpec, Layout} from 'app/client/components/Layout';
|
||||
import {LayoutEditor} from 'app/client/components/LayoutEditor';
|
||||
import {LayoutTray} from 'app/client/components/LayoutTray';
|
||||
import {printViewSection} from 'app/client/components/Printing';
|
||||
import {Delay} from 'app/client/lib/Delay';
|
||||
import {createObsArray} from 'app/client/lib/koArrayWrap';
|
||||
@@ -15,14 +18,23 @@ import {reportError} from 'app/client/models/errors';
|
||||
import {isNarrowScreen, mediaSmall, testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||
import {LayoutTray} from 'app/client/components/LayoutTray';
|
||||
import {buildViewSectionDom} from 'app/client/components/buildViewSectionDom';
|
||||
import {mod} from 'app/common/gutil';
|
||||
import {
|
||||
Computed,
|
||||
computedArray,
|
||||
Disposable,
|
||||
dom,
|
||||
fromKo,
|
||||
Holder,
|
||||
IDomComponent,
|
||||
MultiHolder,
|
||||
Observable,
|
||||
styled,
|
||||
subscribe
|
||||
} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import * as _ from 'underscore';
|
||||
import debounce from 'lodash/debounce';
|
||||
import {Computed, computedArray, Disposable, dom, fromKo, Holder,
|
||||
IDomComponent, MultiHolder, Observable, styled, subscribe} from 'grainjs';
|
||||
import * as _ from 'underscore';
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
@@ -32,6 +44,7 @@ const viewSectionTypes: {[key: string]: any} = {
|
||||
chart: ChartView,
|
||||
single: DetailView,
|
||||
custom: CustomView,
|
||||
'custom.calendar': CustomCalendarView,
|
||||
};
|
||||
|
||||
function getInstanceConstructor(parentKey: string) {
|
||||
|
||||
@@ -89,7 +89,6 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
// Call custom configuration handler.
|
||||
_options.configure?.(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an EventSource with desired access level.
|
||||
*/
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import BaseView from 'app/client/components/BaseView';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {filterBar} from 'app/client/ui/FilterBar';
|
||||
import {cssIcon} from 'app/client/ui/RightPanelStyles';
|
||||
import {makeCollapsedLayoutMenu} from 'app/client/ui/ViewLayoutMenu';
|
||||
import {cssDotsIconWrapper, cssMenu, viewSectionMenu} from 'app/client/ui/ViewSectionMenu';
|
||||
import {buildWidgetTitle} from 'app/client/ui/WidgetTitle';
|
||||
import {getWidgetTypes} from 'app/client/ui/widgetTypes';
|
||||
import {colors, isNarrowScreenObs, mediaSmall, testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {menu} from 'app/client/ui2018/menus';
|
||||
import {getWidgetTypes} from "app/client/ui/widgetTypesMap";
|
||||
import {Computed, dom, DomElementArg, Observable, styled} from 'grainjs';
|
||||
import {defaultMenuOptions} from 'popweasel';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user