mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
some cleanup
This commit is contained in:
parent
fd1734de69
commit
4f3d0d41a0
@ -1,49 +1,8 @@
|
||||
// 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 {
|
||||
/*
|
||||
public override create(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
|
||||
super.create(gristDoc, viewSectionModel);
|
||||
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.name == this.getWidgetName());
|
||||
if (widget && this.customDef.url.peek() !== widget.url) {
|
||||
await this.customDef.url.setAndSave(widget.url);
|
||||
}
|
||||
}).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.
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
// protected abstract getWidgetName(): string;
|
||||
|
||||
// }
|
||||
|
||||
export class CustomCalendarView extends CustomView {
|
||||
protected getInitialSettings(): CustomViewSettings {
|
||||
protected getBuiltInSettings(): CustomViewSettings {
|
||||
return {
|
||||
widgetId: '@gristlabs/widget-calendar',
|
||||
accessLevel: AccessLevel.full,
|
||||
|
@ -32,6 +32,13 @@ import {dom as grains} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import defaults = require('lodash/defaults');
|
||||
|
||||
/**
|
||||
*
|
||||
* Built in settings for a custom widget. Used when the custom
|
||||
* widget is the implementation of a native-looking widget,
|
||||
* for example the calendar widget.
|
||||
*
|
||||
*/
|
||||
export interface CustomViewSettings {
|
||||
widgetId?: string;
|
||||
accessLevel?: AccessLevel;
|
||||
@ -106,42 +113,6 @@ 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() {
|
||||
@ -150,10 +121,10 @@ export class CustomView extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected getInitialSettings(): CustomViewSettings {
|
||||
protected getBuiltInSettings(): CustomViewSettings {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
protected getEmptyWidgetPage(): string {
|
||||
return new URL("custom-widget.html", getGristConfig().homeUrl!).href;
|
||||
}
|
||||
@ -201,10 +172,7 @@ export class CustomView extends Disposable {
|
||||
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; }
|
||||
if (!url() && !widgetId()) { return true; }
|
||||
|
||||
return renderAfterReady();
|
||||
};
|
||||
@ -216,19 +184,25 @@ export class CustomView extends Disposable {
|
||||
// For the view to update when switching from one section to another one, the computed
|
||||
// observable must always notify.
|
||||
.extend({notify: 'always'});
|
||||
// Some widgets have built-in settings that should override anything
|
||||
// that is in the rest of the view options. Ideally, everything would
|
||||
// be consistent. We could fix inconsistencies if we find them, but
|
||||
// we are not guaranteed to have write privileges at this point.
|
||||
const builtInSettings = this.getBuiltInSettings();
|
||||
return dom('div.flexauto.flexvbox.custom_view_container',
|
||||
dom.autoDispose(showPlugin),
|
||||
dom.autoDispose(showPluginNotification),
|
||||
dom.autoDispose(showSectionNotification),
|
||||
dom.autoDispose(showPluginContent),
|
||||
// todo: should display content in webview when running electron
|
||||
kd.scope(() => [mode(), url(), access(), widgetId(), pluginId()], ([_mode, _url, _access, _widgetId, _pluginId]: 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),
|
||||
access: builtInSettings.accessLevel || (_access as AccessLevel || AccessLevel.none),
|
||||
showAfterReady: showAfterReady(),
|
||||
widgetId: _widgetId,
|
||||
widgetId: builtInSettings.widgetId || _widgetId,
|
||||
pluginId: _pluginId,
|
||||
})
|
||||
: null
|
||||
@ -267,7 +241,6 @@ export class CustomView extends Disposable {
|
||||
url: baseUrl || this.getEmptyWidgetPage(),
|
||||
widgetId,
|
||||
pluginId,
|
||||
emptyUrl: this.getEmptyWidgetPage(),
|
||||
access,
|
||||
readonly: this.gristDoc.isReadonly.get(),
|
||||
showAfterReady,
|
||||
|
@ -6,7 +6,8 @@ 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, ICustomWidget, isSatisfied, matchWidget } from 'app/common/CustomWidget';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
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,7 +20,6 @@ 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-');
|
||||
|
||||
@ -44,9 +44,15 @@ export interface WidgetFrameOptions {
|
||||
* Url of external page. Iframe is rebuild each time the URL changes.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* ID of widget, if known. When set, the url for the specified widget
|
||||
* in the WidgetRepository, if found, will take precedence.
|
||||
*/
|
||||
widgetId?: string|null;
|
||||
/**
|
||||
* ID of the plugin that provided the widget (if it came from a plugin).
|
||||
*/
|
||||
pluginId?: string;
|
||||
emptyUrl: string;
|
||||
/**
|
||||
* Assigned access level. Iframe is rebuild each time access level is changed.
|
||||
*/
|
||||
@ -77,7 +83,9 @@ export interface WidgetFrameOptions {
|
||||
* Optional handler to modify the iframe.
|
||||
*/
|
||||
onElem?: (iframe: HTMLIFrameElement) => void;
|
||||
|
||||
/**
|
||||
* The containing document.
|
||||
*/
|
||||
gristDoc: GristDoc;
|
||||
}
|
||||
|
||||
@ -93,7 +101,8 @@ 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, []);
|
||||
// An entry for this widget in the WidgetRepository, if available.
|
||||
public readonly _widget = Observable.create<ICustomWidget|null>(this, null);
|
||||
|
||||
constructor(private _options: WidgetFrameOptions) {
|
||||
super();
|
||||
@ -121,7 +130,7 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
// Call custom configuration handler.
|
||||
_options.configure?.(this);
|
||||
|
||||
this._fetchWidgets().catch(reportError);
|
||||
this._checkWidgetRepository().catch(reportError);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,16 +193,14 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
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,
|
||||
},
|
||||
dom.attr('src', use => this._getUrl(use(this._widget))),
|
||||
hooks.iframeAttributes,
|
||||
testId('ready', this._readyCalled),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
private _getUrl(widgets: ICustomWidget[]): string {
|
||||
private _getUrl(widget: ICustomWidget|null): string {
|
||||
// Append access level to query string.
|
||||
const urlWithAccess = (url: string) => {
|
||||
if (!url) {
|
||||
@ -204,20 +211,8 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
urlObj.searchParams.append('readonly', String(this._options.readonly));
|
||||
return urlObj.href;
|
||||
};
|
||||
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;
|
||||
const url = widget?.url || this._options.url || 'about:blank';
|
||||
return urlWithAccess(url);
|
||||
}
|
||||
|
||||
private _onMessage(event: MessageEvent) {
|
||||
@ -245,12 +240,17 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchWidgets() {
|
||||
if (this.isDisposed()) { return; }
|
||||
/**
|
||||
* If we have a widgetId, look it up in the WidgetRepository and
|
||||
* get the best URL we can for it.
|
||||
*/
|
||||
private async _checkWidgetRepository() {
|
||||
const {widgetId, pluginId} = this._options;
|
||||
if (this.isDisposed() || !widgetId) { return; }
|
||||
const widgets = await this._options.gristDoc.app.topAppModel.getWidgets();
|
||||
if (this.isDisposed()) { return; }
|
||||
this._widgets.set(widgets);
|
||||
console.log("SAVED", {widgets});
|
||||
const widget = matchWidget(widgets, {widgetId, pluginId});
|
||||
this._widget.set(widget || null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,6 +78,10 @@ export interface TopAppModel {
|
||||
*/
|
||||
fetchUsersAndOrgs(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Enumerate the widgets in the WidgetRepository for this installation
|
||||
* of Grist.
|
||||
*/
|
||||
getWidgets(): Promise<ICustomWidget[]>;
|
||||
}
|
||||
|
||||
@ -147,6 +151,9 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
|
||||
public readonly users = Observable.create<FullUser[]>(this, []);
|
||||
public readonly plugins: LocalPlugin[] = [];
|
||||
private readonly _gristConfig?: GristLoadConfig;
|
||||
// Keep a list of available widgets, once requested, so we don't have to
|
||||
// keep reloading it. Downside: browser page will need reloading to pick
|
||||
// up new widgets - that seems ok.
|
||||
private readonly _widgets: AsyncCreate<ICustomWidget[]>;
|
||||
|
||||
constructor(
|
||||
|
@ -254,11 +254,7 @@ export interface CustomViewSectionDef {
|
||||
* widgets available.
|
||||
*/
|
||||
widgetId: modelUtil.KoSaveableObservable<string|null>;
|
||||
/**
|
||||
* Custom widget information.
|
||||
*/
|
||||
// widgetDef: modelUtil.KoSaveableObservable<ICustomWidget|null>;
|
||||
/**
|
||||
/**
|
||||
* Custom widget options.
|
||||
*/
|
||||
widgetOptions: modelUtil.KoSaveableObservable<Record<string, any>|null>;
|
||||
@ -333,7 +329,6 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
const customViewDefaults = {
|
||||
mode: 'url',
|
||||
url: null,
|
||||
// widgetDef: null,
|
||||
access: '',
|
||||
pluginId: '',
|
||||
sectionId: '',
|
||||
@ -346,7 +341,6 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
mode: customDefObj.prop('mode'),
|
||||
url: customDefObj.prop('url'),
|
||||
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, matchWidget } 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,8 +322,7 @@ export class CustomSectionConfig extends Disposable {
|
||||
|
||||
// Test if we can offer widget list.
|
||||
const gristConfig: GristLoadConfig = (window as any).gristConfig || {};
|
||||
console.log("Ignoring gristConfig now", {gristConfig});
|
||||
this._canSelect = true; // gristConfig.enableWidgetRepository ?? true;
|
||||
this._canSelect = gristConfig.enableWidgetRepository ?? true;
|
||||
|
||||
// Array of available widgets - will be updated asynchronously.
|
||||
this._widgets = Observable.create(this, []);
|
||||
@ -335,13 +334,12 @@ export class CustomSectionConfig extends Disposable {
|
||||
const widgetId = use(_section.customDef.widgetId);
|
||||
const pluginId = use(_section.customDef.pluginId);
|
||||
if (widgetId) {
|
||||
console.log("_selectedId", {widgetId, pluginId});
|
||||
return (pluginId||'') + ':' + widgetId;
|
||||
// selection id is "pluginId:widgetId"
|
||||
return (pluginId || '') + ':' + widgetId;
|
||||
}
|
||||
return CUSTOM_ID;
|
||||
});
|
||||
this._selectedId.onWrite(async value => {
|
||||
console.log("_selectedId onWrite", {value});
|
||||
if (value === CUSTOM_ID) {
|
||||
// Select Custom URL
|
||||
bundleChanges(() => {
|
||||
@ -351,9 +349,8 @@ export class CustomSectionConfig extends Disposable {
|
||||
_section.customDef.url(null);
|
||||
// Clear widgetId
|
||||
_section.customDef.widgetId(null);
|
||||
// Clear pluginId
|
||||
_section.customDef.pluginId('');
|
||||
// Clear widget definition.
|
||||
// _section.customDef.widgetDef(null);
|
||||
// Reset access level to none.
|
||||
_section.customDef.access(AccessLevel.none);
|
||||
// Clear all saved options.
|
||||
@ -369,13 +366,10 @@ export class CustomSectionConfig extends Disposable {
|
||||
} else {
|
||||
const [pluginId, widgetId] = value?.split(':') || [];
|
||||
// Select Widget
|
||||
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');
|
||||
@ -383,12 +377,6 @@ export class CustomSectionConfig extends Disposable {
|
||||
// If user selected the same one, do nothing.
|
||||
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(() => {
|
||||
@ -398,18 +386,11 @@ export class CustomSectionConfig extends Disposable {
|
||||
_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);
|
||||
// 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.
|
||||
// Update pluginId.
|
||||
_section.customDef.pluginId(selectedWidget.source?.pluginId || '');
|
||||
// Update widget URL. Leave blank when widgetId is set.
|
||||
_section.customDef.url(null);
|
||||
// Clear options.
|
||||
_section.customDef.widgetOptions(null);
|
||||
@ -420,7 +401,6 @@ export class CustomSectionConfig extends Disposable {
|
||||
_section.columnsToMap(null);
|
||||
});
|
||||
await _section.saveCustomDef();
|
||||
console.log("CustomSectionConfig saved");
|
||||
}
|
||||
});
|
||||
|
||||
@ -431,11 +411,12 @@ export class CustomSectionConfig extends Disposable {
|
||||
bundleChanges(() => {
|
||||
_section.customDef.renderAfterReady(false);
|
||||
if (newUrl) {
|
||||
console.log("ZAP widgetId and pluginId");
|
||||
// When a URL is set explicitly, make sure widgetId/pluginId
|
||||
// is empty.
|
||||
_section.customDef.widgetId(null);
|
||||
_section.customDef.pluginId('');
|
||||
}
|
||||
//_section.customDef.url(newUrl);
|
||||
_section.customDef.url(newUrl);
|
||||
});
|
||||
await _section.saveCustomDef();
|
||||
});
|
||||
@ -460,11 +441,6 @@ 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)!));
|
||||
@ -476,7 +452,8 @@ export class CustomSectionConfig extends Disposable {
|
||||
const options = Computed.create(holder, use => [
|
||||
{label: 'Custom URL', value: 'custom'},
|
||||
...use(this._widgets).map(w => ({
|
||||
label: makeLabel(w), value: ((w.fromPlugin||'') + ':' + w.widgetId)
|
||||
label: w.source?.name ? `${w.name} (${w.source.name})` : w.name,
|
||||
value: (w.source?.pluginId || '') + ':' + w.widgetId,
|
||||
})),
|
||||
]);
|
||||
function buildPrompt(level: AccessLevel|null) {
|
||||
@ -583,21 +560,6 @@ export class CustomSectionConfig extends Disposable {
|
||||
|
||||
protected async _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()!);
|
||||
// }
|
||||
}
|
||||
this._widgets.set(widgets);
|
||||
}
|
||||
|
||||
|
@ -220,7 +220,6 @@ 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(
|
||||
|
@ -31,7 +31,13 @@ export interface ICustomWidget {
|
||||
*/
|
||||
renderAfterReady?: boolean;
|
||||
|
||||
fromPlugin?: string;
|
||||
/**
|
||||
* If the widget came from a plugin, we track that here.
|
||||
*/
|
||||
source?: {
|
||||
pluginId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,64 +70,20 @@ export function isSatisfied(current: AccessLevel, minimum: AccessLevel) {
|
||||
return ordered(current) >= ordered(minimum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best match for a widgetId/pluginId combination among the
|
||||
* given widgets. An exact widgetId match is required. A pluginId match
|
||||
* is preferred but not required.
|
||||
*/
|
||||
export function matchWidget(widgets: ICustomWidget[], options: {
|
||||
widgetId?: string,
|
||||
widgetId: string,
|
||||
pluginId?: string,
|
||||
}): ICustomWidget|undefined {
|
||||
console.log("MATCHING", {
|
||||
widgets,
|
||||
options,
|
||||
});
|
||||
const prefs = sortBy(widgets, (w) => {
|
||||
return [w.widgetId !== options.widgetId,
|
||||
(w.fromPlugin||'') !== options.pluginId]
|
||||
(w.source?.pluginId||'') !== options.pluginId]
|
||||
});
|
||||
if (prefs.length === 0) { return; }
|
||||
if (options.widgetId && prefs[0].widgetId !== options.widgetId) {
|
||||
return;
|
||||
}
|
||||
console.log("ORDERED", prefs);
|
||||
console.log("MATCHED", prefs[0]);
|
||||
if (prefs[0].widgetId !== options.widgetId) { return };
|
||||
return prefs[0];
|
||||
}
|
||||
|
||||
export function filterWidgets(widgets: ICustomWidget[], options: {
|
||||
preferPlugin?: boolean,
|
||||
keepWidgetIdUnique?: boolean,
|
||||
}) {
|
||||
const folders = new Map<string, ICustomWidget[]>();
|
||||
for (const widget of widgets) {
|
||||
const widgetId = widget.widgetId;
|
||||
if (!folders.has(widgetId)) { folders.set(widgetId, []); }
|
||||
const widgetFolder = folders.get(widgetId)!;
|
||||
widgetFolder.push(widget);
|
||||
}
|
||||
let finalResults: ICustomWidget[] = widgets;
|
||||
if (options.preferPlugin !== undefined) {
|
||||
const results = [];
|
||||
const seen = new Set<string>();
|
||||
for (const widget of widgets) {
|
||||
const folder = folders.get(widget.widgetId)!;
|
||||
if (folder.length === 1) {
|
||||
results.push(widget);
|
||||
continue;
|
||||
}
|
||||
if (seen.has(widget.widgetId)) { continue; }
|
||||
seen.add(widget.widgetId);
|
||||
const folderSorted = sortBy(folder, (w) => Boolean(w.fromPlugin) !== options.preferPlugin);
|
||||
results.push(folderSorted[0]!);
|
||||
}
|
||||
finalResults = results;
|
||||
}
|
||||
if (options.keepWidgetIdUnique) {
|
||||
const results = [];
|
||||
const seen = new Set<string>();
|
||||
for (const widget of widgets) {
|
||||
if (seen.has(widget.widgetId)) { continue; }
|
||||
seen.add(widget.widgetId);
|
||||
results.push(widget);
|
||||
}
|
||||
finalResults = results;
|
||||
}
|
||||
return finalResults;
|
||||
}
|
||||
|
@ -35,6 +35,11 @@ export interface PublishedPlugin extends BarePlugin {
|
||||
* as those being developed).
|
||||
*/
|
||||
export interface BarePlugin {
|
||||
/**
|
||||
* An optional human-readable name.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Components describe how the plugin runs. A plugin may provide UI and behavior that runs in
|
||||
* the browser, Python code that runs in a secure sandbox, and arbitrary code that runs in Node.
|
||||
@ -82,6 +87,11 @@ export interface BarePlugin {
|
||||
*/
|
||||
unsafeNode?: string;
|
||||
|
||||
/**
|
||||
* Relative path to a specialized manifest of custom widgets.
|
||||
* I'm unsure how this fits into components and contributions,
|
||||
* this seemed the least-worst spot for it.
|
||||
*/
|
||||
widgets?: string;
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {ApiError} from 'app/common/ApiError';
|
||||
import { ICustomWidget } from 'app/common/CustomWidget';
|
||||
import {ICustomWidget} from 'app/common/CustomWidget';
|
||||
import {delay} from 'app/common/delay';
|
||||
import {DocCreationInfo} from 'app/common/DocListAPI';
|
||||
import {encodeUrl, getSlugIfNeeded, GristDeploymentType, GristDeploymentTypes,
|
||||
@ -66,7 +66,7 @@ import {getTelemetryPrefs, ITelemetry} from 'app/server/lib/Telemetry';
|
||||
import {startTestingHooks} from 'app/server/lib/TestingHooks';
|
||||
import {getTestLoginSystem} from 'app/server/lib/TestLogin';
|
||||
import {addUploadRoute} from 'app/server/lib/uploads';
|
||||
import { buildWidgetRepository, getWidgetPlaces, IWidgetRepository} from 'app/server/lib/WidgetRepository';
|
||||
import {buildWidgetRepository, getWidgetsInPlugins, IWidgetRepository} from 'app/server/lib/WidgetRepository';
|
||||
import {setupLocale} from 'app/server/localization';
|
||||
import axios from 'axios';
|
||||
import * as bodyParser from 'body-parser';
|
||||
@ -129,8 +129,8 @@ export class FlexServer implements GristServer {
|
||||
private _dbManager: HomeDBManager;
|
||||
private _defaultBaseDomain: string|undefined;
|
||||
private _pluginUrl: string|undefined;
|
||||
private _pluginUrlSet: boolean = false;
|
||||
private _willServePlugins?: boolean;
|
||||
private _pluginUrlReady: boolean = false;
|
||||
private _servesPlugins?: boolean;
|
||||
private _bundledWidgets?: ICustomWidget[];
|
||||
private _billing: IBilling;
|
||||
private _instanceRoot: string;
|
||||
@ -174,11 +174,30 @@ export class FlexServer implements GristServer {
|
||||
private _getLogoutRedirectUrl: (req: express.Request, nextUrl: URL) => Promise<string>;
|
||||
private _sendAppPage: (req: express.Request, resp: express.Response, options: ISendAppPageOptions) => Promise<void>;
|
||||
private _getLoginSystem?: () => Promise<GristLoginSystem>;
|
||||
// Called by ready() to allow requests to be served.
|
||||
private _ready: () => void;
|
||||
// Set once ready() is called
|
||||
private _isReady: boolean = false;
|
||||
|
||||
constructor(public port: number, public name: string = 'flexServer',
|
||||
public readonly options: FlexServerOptions = {}) {
|
||||
this.app = express();
|
||||
this.app.set('port', port);
|
||||
|
||||
// Before doing anything, we pause any request handling to wait
|
||||
// for the server being entirely ready. The specific reason to do
|
||||
// so is because, if we are serving plugins, and using an
|
||||
// OS-assigned port to do so, we won't know the URL to use for
|
||||
// plugins until quite late. But it seems a nice thing to
|
||||
// guarantee in general.
|
||||
const readyPromise = new Promise(resolve => {
|
||||
this._ready = () => resolve(undefined);
|
||||
});
|
||||
this.app.use(async (_req, _res, next) => {
|
||||
await readyPromise;
|
||||
next();
|
||||
});
|
||||
|
||||
this.appRoot = getAppRoot();
|
||||
this.host = process.env.GRIST_HOST || "localhost";
|
||||
log.info(`== Grist version is ${version.version} (commit ${version.gitcommit})`);
|
||||
@ -508,12 +527,6 @@ export class FlexServer implements GristServer {
|
||||
public addStaticAndBowerDirectories() {
|
||||
if (this._check('static_and_bower', 'dir')) { return; }
|
||||
this.addTagChecker();
|
||||
// Allow static files to be requested from any origin.
|
||||
const options: serveStatic.ServeStaticOptions = {
|
||||
setHeaders: (res, filepath, stat) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
}
|
||||
};
|
||||
// Grist has static help files, which may be useful for standalone app,
|
||||
// but for hosted grist the latest help is at support.getgrist.com. Redirect
|
||||
// to this page for the benefit of crawlers which currently rank the static help
|
||||
@ -526,11 +539,11 @@ export class FlexServer implements GristServer {
|
||||
// as an Electron app.
|
||||
const staticExtDir = getAppPathTo(this.appRoot, 'static') + '_ext';
|
||||
const staticExtApp = fse.existsSync(staticExtDir) ?
|
||||
express.static(staticExtDir, options) : null;
|
||||
const staticApp = express.static(getAppPathTo(this.appRoot, 'static'), options);
|
||||
const bowerApp = express.static(getAppPathTo(this.appRoot, 'bower_components'), options);
|
||||
express.static(staticExtDir, serveAnyOrigin) : null;
|
||||
const staticApp = express.static(getAppPathTo(this.appRoot, 'static'), serveAnyOrigin);
|
||||
const bowerApp = express.static(getAppPathTo(this.appRoot, 'bower_components'), serveAnyOrigin);
|
||||
if (process.env.GRIST_LOCALES_DIR) {
|
||||
const locales = express.static(process.env.GRIST_LOCALES_DIR, options);
|
||||
const locales = express.static(process.env.GRIST_LOCALES_DIR, serveAnyOrigin);
|
||||
this.app.use("/locales", this.tagChecker.withTag(locales));
|
||||
}
|
||||
if (staticExtApp) { this.app.use(this.tagChecker.withTag(staticExtApp)); }
|
||||
@ -561,20 +574,13 @@ export class FlexServer implements GristServer {
|
||||
res.sendFile(req.params[0], {root: getAppPathTo(this.appRoot, 'static')})));
|
||||
this.addOrg();
|
||||
addPluginEndpoints(this, await this._addPluginManager());
|
||||
const places = getWidgetPlaces(this, 'wotnot');
|
||||
// Allow static files to be requested from any origin.
|
||||
const options: serveStatic.ServeStaticOptions = {
|
||||
setHeaders: (res, filepath, stat) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
}
|
||||
};
|
||||
for (const place of places) {
|
||||
this.app.use(
|
||||
'/widgets/' + place.name,
|
||||
this.tagChecker.withTag(
|
||||
limitToPlugins(this,
|
||||
express.static(place.fileDir, options)
|
||||
)
|
||||
|
||||
// Serve bundled custom widgets on the plugin endpoint.
|
||||
const places = getWidgetsInPlugins(this, '');
|
||||
for (const place of places) {
|
||||
this.app.use(
|
||||
'/widgets/' + place.pluginId, this.tagChecker.withTag(
|
||||
limitToPlugins(this, express.static(place.dir, serveAnyOrigin))
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -1445,25 +1451,29 @@ export class FlexServer implements GristServer {
|
||||
});
|
||||
}
|
||||
|
||||
public setPluginPort(port: number) {
|
||||
const url = new URL(this.getOwnUrl());
|
||||
url.port = String(port);
|
||||
this._pluginUrl = url.href;
|
||||
}
|
||||
|
||||
public willServePlugins() {
|
||||
if (this._willServePlugins === undefined) {
|
||||
throw new Error('do not know if will serve plugins');
|
||||
/**
|
||||
* Check whether there's a local plugin port.
|
||||
*/
|
||||
public servesPlugins() {
|
||||
if (this._servesPlugins === undefined) {
|
||||
throw new Error('do not know if server will serve plugins');
|
||||
}
|
||||
return this._willServePlugins;
|
||||
}
|
||||
|
||||
public setWillServePlugins(flag: boolean) {
|
||||
this._willServePlugins = flag;
|
||||
return this._servesPlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare that there will be a local plugin port.
|
||||
*/
|
||||
public setServesPlugins(flag: boolean) {
|
||||
this._servesPlugins = flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL for plugins. Throws an error if the URL is not
|
||||
* yet available.
|
||||
*/
|
||||
public getPluginUrl() {
|
||||
if (!this._pluginUrlSet) {
|
||||
if (!this._pluginUrlReady) {
|
||||
throw new Error('looked at plugin url too early');
|
||||
}
|
||||
return this._pluginUrl;
|
||||
@ -1476,14 +1486,24 @@ export class FlexServer implements GristServer {
|
||||
return this._pluginManager.getPlugins();
|
||||
}
|
||||
|
||||
public async prepareSummary() {
|
||||
// Add some information that isn't guaranteed set until the end.
|
||||
|
||||
public async finishPluginSetup(userPort: number|null) {
|
||||
// If plugin content is served from same host but on different port,
|
||||
// run webserver on that port
|
||||
if (userPort !== null) {
|
||||
const ports = await this.startCopy('pluginServer', userPort);
|
||||
// If Grist is running on a desktop, directly on the host, it
|
||||
// can be convenient to leave the user port free for the OS to
|
||||
// allocate by using GRIST_UNTRUSTED_PORT=0. But we do need to
|
||||
// remember how to contact it.
|
||||
if (process.env.APP_UNTRUSTED_URL === undefined) {
|
||||
const url = new URL(this.getOwnUrl());
|
||||
url.port = String(userPort || ports.serverPort);
|
||||
this._pluginUrl = url.href;
|
||||
}
|
||||
}
|
||||
this.info.push(['pluginUrl', this._pluginUrl]);
|
||||
// plugin url should be finalized by now.
|
||||
this._pluginUrlSet = true;
|
||||
this.info.push(['willServePlugins', this._willServePlugins]);
|
||||
|
||||
this._pluginUrlReady = true;
|
||||
this.info.push(['willServePlugins', this._servesPlugins]);
|
||||
const repo = buildWidgetRepository(this, { localOnly: true });
|
||||
this._bundledWidgets = await repo.getWidgets();
|
||||
}
|
||||
@ -1509,6 +1529,12 @@ export class FlexServer implements GristServer {
|
||||
}
|
||||
}
|
||||
|
||||
public ready() {
|
||||
if (this._isReady) { return; }
|
||||
this._isReady = true;
|
||||
this._ready();
|
||||
}
|
||||
|
||||
public checkOptionCombinations() {
|
||||
// Check for some bad combinations we should warn about.
|
||||
const allowedWebhookDomains = appSettings.section('integrations').flag('allowedWebhookDomains').readString({
|
||||
@ -1983,15 +2009,18 @@ export class FlexServer implements GristServer {
|
||||
private async _startServers(server: http.Server, httpsServer: https.Server|undefined,
|
||||
name: string, port: number, verbose: boolean) {
|
||||
await listenPromise(server.listen(port, this.host));
|
||||
if (verbose) { log.info(`${name} available at ${this.host}:${port}`); }
|
||||
const serverPort = (server.address() as AddressInfo).port;
|
||||
if (verbose) { log.info(`${name} available at ${this.host}:${serverPort}`); }
|
||||
let httpsServerPort: number|undefined;
|
||||
if (TEST_HTTPS_OFFSET && httpsServer) {
|
||||
const httpsPort = port + TEST_HTTPS_OFFSET;
|
||||
await listenPromise(httpsServer.listen(httpsPort, this.host));
|
||||
if (verbose) { log.info(`${name} available at https://${this.host}:${httpsPort}`); }
|
||||
if (port === 0) { throw new Error('cannot use https with OS-assigned port'); }
|
||||
httpsServerPort = port + TEST_HTTPS_OFFSET;
|
||||
await listenPromise(httpsServer.listen(httpsServerPort, this.host));
|
||||
if (verbose) { log.info(`${name} available at https://${this.host}:${httpsServerPort}`); }
|
||||
}
|
||||
return {
|
||||
serverPort: (server.address() as AddressInfo).port,
|
||||
httpsServerPort: (server.address() as AddressInfo)?.port,
|
||||
serverPort,
|
||||
httpsServerPort,
|
||||
};
|
||||
}
|
||||
|
||||
@ -2247,3 +2276,10 @@ export interface ElectronServerMethods {
|
||||
updateUserConfig(obj: any): Promise<void>;
|
||||
onBackupMade(cb: () => void): void;
|
||||
}
|
||||
|
||||
// Allow static files to be requested from any origin.
|
||||
const serveAnyOrigin: serveStatic.ServeStaticOptions = {
|
||||
setHeaders: (res, filepath, stat) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
}
|
||||
};
|
||||
|
@ -57,7 +57,7 @@ export interface GristServer {
|
||||
resolveLoginSystem(): Promise<GristLoginSystem>;
|
||||
getPluginUrl(): string|undefined;
|
||||
getPlugins(): LocalPlugin[];
|
||||
willServePlugins(): boolean;
|
||||
servesPlugins(): boolean;
|
||||
getBundledWidgets(): ICustomWidget[];
|
||||
}
|
||||
|
||||
@ -142,7 +142,7 @@ export function createDummyGristServer(): GristServer {
|
||||
getAccessTokens() { throw new Error('no access tokens'); },
|
||||
resolveLoginSystem() { throw new Error('no login system'); },
|
||||
getPluginUrl() { return undefined; },
|
||||
willServePlugins() { return false; },
|
||||
servesPlugins() { return false; },
|
||||
getPlugins() { return []; },
|
||||
getBundledWidgets() { return []; },
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ export function getUntrustedContentHost(origin: string|undefined): string|undefi
|
||||
|
||||
// Add plugin endpoints to be served on untrusted host
|
||||
export function addPluginEndpoints(server: FlexServer, pluginManager: PluginManager) {
|
||||
if (server.willServePlugins()) {
|
||||
if (server.servesPlugins()) {
|
||||
server.app.get(/^\/plugins\/(installed|builtIn)\/([^/]+)\/(.+)/, (req, res) =>
|
||||
servePluginContent(req, res, pluginManager, server));
|
||||
}
|
||||
|
@ -131,7 +131,6 @@ export class PluginManager {
|
||||
|
||||
|
||||
async function scanDirectory(dir: string, kind: "installed"|"builtIn"): Promise<DirectoryScanEntry[]> {
|
||||
console.log("SCAN", {dir, kind});
|
||||
const plugins: DirectoryScanEntry[] = [];
|
||||
let listDir;
|
||||
|
||||
|
@ -4,11 +4,14 @@ import * as fse from 'fs-extra';
|
||||
import fetch from 'node-fetch';
|
||||
import * as path from 'path';
|
||||
import {ApiError} from 'app/common/ApiError';
|
||||
import {removeTrailingSlash} from 'app/common/gutil';
|
||||
import {GristServer} from 'app/server/lib/GristServer';
|
||||
import LRUCache from 'lru-cache';
|
||||
import * as url from 'url';
|
||||
import { removeTrailingSlash } from 'app/common/gutil';
|
||||
import { GristServer } from './GristServer';
|
||||
// import { LocalPlugin } from 'app/common/plugin';
|
||||
import { AsyncCreate } from 'app/common/AsyncCreate';
|
||||
|
||||
// Static url for UrlWidgetRepository
|
||||
const STATIC_URL = process.env.GRIST_WIDGET_LIST_URL;
|
||||
|
||||
/**
|
||||
* Widget Repository returns list of available Custom Widgets.
|
||||
@ -17,67 +20,66 @@ export interface IWidgetRepository {
|
||||
getWidgets(): Promise<ICustomWidget[]>;
|
||||
}
|
||||
|
||||
// Static url for StaticWidgetRepository
|
||||
const STATIC_URL = process.env.GRIST_WIDGET_LIST_URL;
|
||||
|
||||
export class FileWidgetRepository implements IWidgetRepository {
|
||||
constructor(private _widgetFileName: string,
|
||||
/**
|
||||
*
|
||||
* A widget repository that lives on disk.
|
||||
*
|
||||
* The _widgetFile should point to a json file containing a
|
||||
* list of custom widgets, in the format used by the grist-widget
|
||||
* repo:
|
||||
* https://github.com/gristlabs/grist-widget
|
||||
*
|
||||
* The file can use relative URLs. The URLs will be interpreted
|
||||
* as relative to the _widgetBaseUrl.
|
||||
*
|
||||
* If a _source is provided, it will be passed along in the
|
||||
* widget listings.
|
||||
*
|
||||
*/
|
||||
export class DiskWidgetRepository implements IWidgetRepository {
|
||||
constructor(private _widgetFile: string,
|
||||
private _widgetBaseUrl: string,
|
||||
private _pluginId?: string) {}
|
||||
private _source?: any) {}
|
||||
|
||||
public async getWidgets(): Promise<ICustomWidget[]> {
|
||||
const txt = await fse.readFile(this._widgetFileName, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const txt = await fse.readFile(this._widgetFile, { encoding: 'utf8' });
|
||||
const widgets: ICustomWidget[] = JSON.parse(txt);
|
||||
fixUrls(widgets, this._widgetBaseUrl);
|
||||
if (this._pluginId) {
|
||||
if (this._source) {
|
||||
for (const widget of widgets) {
|
||||
widget.fromPlugin = this._pluginId;
|
||||
widget.source = this._source;
|
||||
}
|
||||
}
|
||||
console.log("FileWidget", {widgets});
|
||||
return widgets;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
export class NestedWidgetRepository implements IWidgetRepository {
|
||||
constructor(private _widgetDir: string,
|
||||
private _widgetBaseUrl: string) {}
|
||||
|
||||
public async getWidgets(): Promise<ICustomWidget[]> {
|
||||
const listDir = await fse.readdir(this._widgetDir,
|
||||
{ withFileTypes: true });
|
||||
const fileName = 'manifest.json';
|
||||
const allWidgets: ICustomWidget[] = [];
|
||||
for (const dir of listDir) {
|
||||
if (!dir.isDirectory()) { continue; }
|
||||
const fullPath = path.join(this._widgetDir, dir.name, fileName);
|
||||
if (!await fse.pathExists(fullPath)) { continue; }
|
||||
const txt = await fse.readFile(fullPath, 'utf8');
|
||||
const widgets = JSON.parse(txt);
|
||||
fixUrls(
|
||||
widgets,
|
||||
removeTrailingSlash(this._widgetBaseUrl) + '/' + dir.name + '/'
|
||||
);
|
||||
allWidgets.push(...widgets);
|
||||
}
|
||||
return allWidgets;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* A wrapper around a widget repository that delays creating it
|
||||
* until the first call to getWidgets().
|
||||
*
|
||||
*/
|
||||
export class DelayedWidgetRepository implements IWidgetRepository {
|
||||
constructor(private _makeRepo: () => Promise<IWidgetRepository|undefined>) {}
|
||||
private _repo: AsyncCreate<IWidgetRepository|undefined>;
|
||||
|
||||
constructor(_makeRepo: () => Promise<IWidgetRepository|undefined>) {
|
||||
this._repo = new AsyncCreate(_makeRepo);
|
||||
}
|
||||
|
||||
public async getWidgets(): Promise<ICustomWidget[]> {
|
||||
const repo = await this._makeRepo();
|
||||
const repo = await this._repo.get();
|
||||
if (!repo) { return []; }
|
||||
return repo.getWidgets();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* A wrapper around a list of widget repositories that concatenates
|
||||
* their results.
|
||||
*
|
||||
*/
|
||||
export class CombinedWidgetRepository implements IWidgetRepository {
|
||||
constructor(private _repos: IWidgetRepository[]) {}
|
||||
|
||||
@ -86,13 +88,12 @@ export class CombinedWidgetRepository implements IWidgetRepository {
|
||||
for (const repo of this._repos) {
|
||||
allWidgets.push(...await repo.getWidgets());
|
||||
}
|
||||
console.log("COMBINED", {allWidgets});
|
||||
return allWidgets;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository that gets list of available widgets from a static URL.
|
||||
* Repository that gets a list of widgets from a URL.
|
||||
*/
|
||||
export class UrlWidgetRepository implements IWidgetRepository {
|
||||
constructor(private _staticUrl = STATIC_URL) {}
|
||||
@ -134,13 +135,14 @@ export class UrlWidgetRepository implements IWidgetRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Default repository that gets list of available widgets from a static URL.
|
||||
* Default repository that gets list of available widgets from multiple
|
||||
* sources.
|
||||
*/
|
||||
export class WidgetRepositoryImpl implements IWidgetRepository {
|
||||
protected _staticUrl: string|undefined;
|
||||
private _diskWidgets?: IWidgetRepository;
|
||||
private _urlWidgets: UrlWidgetRepository;
|
||||
private _combinedWidgets: CombinedWidgetRepository;
|
||||
private _dirWidgets?: IWidgetRepository;
|
||||
|
||||
constructor(_options: {
|
||||
staticUrl?: string,
|
||||
@ -148,12 +150,16 @@ export class WidgetRepositoryImpl implements IWidgetRepository {
|
||||
}) {
|
||||
const {staticUrl, gristServer} = _options;
|
||||
if (gristServer) {
|
||||
this._dirWidgets = new DelayedWidgetRepository(async () => {
|
||||
const places = getWidgetPlaces(gristServer);
|
||||
console.log("PLACES!", places);
|
||||
const files = places.map(place => new FileWidgetRepository(place.fileBase,
|
||||
place.urlBase,
|
||||
place.pluginId));
|
||||
this._diskWidgets = new DelayedWidgetRepository(async () => {
|
||||
const places = getWidgetsInPlugins(gristServer);
|
||||
const files = places.map(
|
||||
place => new DiskWidgetRepository(
|
||||
place.file,
|
||||
place.urlBase,
|
||||
{
|
||||
pluginId: place.pluginId,
|
||||
name: place.name
|
||||
}));
|
||||
return new CombinedWidgetRepository(files);
|
||||
});
|
||||
}
|
||||
@ -174,7 +180,7 @@ export class WidgetRepositoryImpl implements IWidgetRepository {
|
||||
this._urlWidgets = new UrlWidgetRepository(this._staticUrl);
|
||||
repos.push(this._urlWidgets);
|
||||
}
|
||||
if (this._dirWidgets) { repos.push(this._dirWidgets); }
|
||||
if (this._diskWidgets) { repos.push(this._diskWidgets); }
|
||||
this._combinedWidgets = new CombinedWidgetRepository(repos);
|
||||
}
|
||||
|
||||
@ -200,7 +206,6 @@ class CachedWidgetRepository extends WidgetRepositoryImpl {
|
||||
const list = await super.getWidgets();
|
||||
// Cache only if there are some widgets.
|
||||
if (list.length) { this._cache.set(1, list); }
|
||||
console.log("CACHABLE RESULT", {list});
|
||||
return list;
|
||||
}
|
||||
|
||||
@ -217,20 +222,15 @@ export function buildWidgetRepository(gristServer: GristServer,
|
||||
options?: {
|
||||
localOnly: boolean
|
||||
}) {
|
||||
if (options?.localOnly) {
|
||||
return new WidgetRepositoryImpl({
|
||||
gristServer,
|
||||
staticUrl: ''
|
||||
});
|
||||
}
|
||||
return new CachedWidgetRepository({
|
||||
gristServer,
|
||||
...(options?.localOnly ? { staticUrl: '' } : undefined)
|
||||
});
|
||||
}
|
||||
|
||||
function fixUrls(widgets: ICustomWidget[], baseUrl: string) {
|
||||
// If URLs are relative, make them absolute, interpreting them
|
||||
// relative to the manifest file.
|
||||
// relative to the supplied base.
|
||||
for (const widget of widgets) {
|
||||
if (!(url.parse(widget.url).protocol)) {
|
||||
widget.url = new URL(widget.url, baseUrl).href;
|
||||
@ -238,41 +238,40 @@ function fixUrls(widgets: ICustomWidget[], baseUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CustomWidgetPlace {
|
||||
urlBase: string,
|
||||
fileBase: string,
|
||||
fileDir: string,
|
||||
name: string,
|
||||
/**
|
||||
* Information about widgets in a plugin. We need to coordinate
|
||||
* URLs with location on disk.
|
||||
*/
|
||||
export interface CustomWidgetsInPlugin {
|
||||
pluginId: string,
|
||||
urlBase: string,
|
||||
dir: string,
|
||||
file: string,
|
||||
name: string,
|
||||
}
|
||||
|
||||
export function getWidgetPlaces(gristServer: GristServer,
|
||||
pluginUrl?: string) {
|
||||
const places: CustomWidgetPlace[] = [];
|
||||
/**
|
||||
* Get a list of widgets available locally via plugins.
|
||||
*/
|
||||
export function getWidgetsInPlugins(gristServer: GristServer,
|
||||
pluginUrl?: string) {
|
||||
const places: CustomWidgetsInPlugin[] = [];
|
||||
const plugins = gristServer.getPlugins();
|
||||
console.log("PLUGINS", plugins);
|
||||
pluginUrl = pluginUrl || gristServer.getPluginUrl();
|
||||
if (!pluginUrl) { return []; }
|
||||
pluginUrl = pluginUrl ?? gristServer.getPluginUrl();
|
||||
if (pluginUrl === undefined) { return []; }
|
||||
for (const plugin of plugins) {
|
||||
console.log("PLUGIN", plugin);
|
||||
const components = plugin.manifest.components;
|
||||
if (!components.widgets) { continue; }
|
||||
console.log("GOT SOMETHING", {
|
||||
name: plugin.id,
|
||||
path: plugin.path,
|
||||
widgets: components.widgets
|
||||
});
|
||||
const urlBase =
|
||||
removeTrailingSlash(pluginUrl) + '/v/' +
|
||||
gristServer.getTag() + '/widgets/' + plugin.id + '/';
|
||||
places.push({
|
||||
urlBase,
|
||||
fileBase: path.join(plugin.path, components.widgets),
|
||||
fileDir: plugin.path,
|
||||
name: plugin.id,
|
||||
dir: plugin.path,
|
||||
file: path.join(plugin.path, components.widgets),
|
||||
name: plugin.manifest.name || plugin.id,
|
||||
pluginId: plugin.id,
|
||||
});
|
||||
}
|
||||
console.log("PLACES", places);
|
||||
return places;
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ export function makeGristConfig(options: MakeGristConfigOptions): GristLoadConfi
|
||||
maxUploadSizeImport: (Number(process.env.GRIST_MAX_UPLOAD_IMPORT_MB) * 1024 * 1024) || undefined,
|
||||
maxUploadSizeAttachment: (Number(process.env.GRIST_MAX_UPLOAD_ATTACHMENT_MB) * 1024 * 1024) || undefined,
|
||||
timestampMs: Date.now(),
|
||||
enableWidgetRepository: Boolean(process.env.GRIST_WIDGET_LIST_URL),
|
||||
enableWidgetRepository: Boolean(process.env.GRIST_WIDGET_LIST_URL) || ((server?.getBundledWidgets().length || 0) > 0),
|
||||
survey: Boolean(process.env.DOC_ID_NEW_USER_INFO),
|
||||
tagManagerId: process.env.GOOGLE_TAG_MANAGER_ID,
|
||||
activation: getActivation(req as RequestWithLogin | undefined),
|
||||
|
@ -72,11 +72,12 @@ export async function main(port: number, serverTypes: ServerType[],
|
||||
|
||||
const server = new FlexServer(port, `server(${serverTypes.join(",")})`, options);
|
||||
|
||||
// We need to know early on whether we will be serving plugins or not.
|
||||
if (includeHome) {
|
||||
const userPort = checkUserContentPort();
|
||||
server.setWillServePlugins(userPort !== undefined);
|
||||
server.setServesPlugins(userPort !== undefined);
|
||||
} else {
|
||||
server.setWillServePlugins(false);
|
||||
server.setServesPlugins(false);
|
||||
}
|
||||
|
||||
if (options.loginSystem) {
|
||||
@ -171,26 +172,14 @@ export async function main(port: number, serverTypes: ServerType[],
|
||||
server.finalize();
|
||||
|
||||
if (includeHome) {
|
||||
// If plugin content is served from same host but on different port,
|
||||
// run webserver on that port
|
||||
const userPort = checkUserContentPort();
|
||||
if (userPort !== null) {
|
||||
const ports = await server.startCopy('pluginServer', userPort);
|
||||
// If Grist is running on a desktop, directly on the host, it
|
||||
// can be convenient to leave the user port free for the OS to
|
||||
// allocate by using GRIST_UNTRUSTED_PORT=0. But we do need to
|
||||
// remember how to contact it.
|
||||
if (userPort === 0) {
|
||||
server.setPluginPort(ports.serverPort);
|
||||
} else if (process.env.APP_UNTRUSTED_URL === undefined) {
|
||||
server.setPluginPort(userPort);
|
||||
}
|
||||
}
|
||||
await server.finishPluginSetup(checkUserContentPort());
|
||||
} else {
|
||||
await server.finishPluginSetup(null);
|
||||
}
|
||||
|
||||
server.checkOptionCombinations();
|
||||
await server.prepareSummary();
|
||||
server.summary();
|
||||
server.ready();
|
||||
return server;
|
||||
} catch(e) {
|
||||
await server.close();
|
||||
|
@ -51,7 +51,7 @@
|
||||
"@types/double-ended-queue": "2.1.0",
|
||||
"@types/express": "4.16.0",
|
||||
"@types/form-data": "2.2.1",
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/fs-extra": "5.0.4",
|
||||
"@types/http-proxy": "1.17.9",
|
||||
"@types/i18next-fs-backend": "1.1.2",
|
||||
"@types/image-size": "0.0.29",
|
||||
@ -139,7 +139,7 @@
|
||||
"exceljs": "4.2.1",
|
||||
"express": "4.16.4",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "11.1.1",
|
||||
"fs-extra": "7.0.0",
|
||||
"grain-rpc": "0.1.7",
|
||||
"grainjs": "1.0.2",
|
||||
"handlebars": "4.7.7",
|
||||
|
66
yarn.lock
66
yarn.lock
@ -721,12 +721,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/fs-extra@11.0.2":
|
||||
version "11.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.2.tgz#23dc1ed7b2eba8ccd75568ac34e7a4e48aa2d087"
|
||||
integrity sha512-c0hrgAOVYr21EX8J0jBMXGLMgJqVf/v6yxi0dLaJboW9aQPh16Id+z6w2Tx1hm+piJOLv8xPfVKZCLfjPw/IMQ==
|
||||
"@types/fs-extra@5.0.4":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.4.tgz"
|
||||
integrity sha512-DsknoBvD8s+RFfSGjmERJ7ZOP1HI0UZRA3FSI+Zakhrc/Gy26YQsLI+m5V5DHxroHRJqCDLKJp7Hixn8zyaF7g==
|
||||
dependencies:
|
||||
"@types/jsonfile" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/http-cache-semantics@*":
|
||||
@ -786,13 +785,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/jsonfile@*":
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.2.tgz#d3b8a3536c5bb272ebee0f784180e456b7691c8f"
|
||||
integrity sha512-8t92P+oeW4d/CRQfJaSqEwXujrhH4OEeHRjGU3v1Q8mUS8GPF3yiX26sw4svv6faL2HfBtGTe2xWIoVgN3dy9w==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/jsonwebtoken@7.2.8":
|
||||
version "7.2.8"
|
||||
resolved "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-7.2.8.tgz"
|
||||
@ -865,9 +857,9 @@
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node@*":
|
||||
version "20.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.7.2.tgz#0bdc211f8c2438cfadad26dc8c040a874d478aed"
|
||||
integrity sha512-RcdC3hOBOauLP+r/kRt27NrByYtDjsXyAuSbR87O6xpsvi763WI+5fbSIvYJrXnt9w4RuxhV6eAXfIs7aaf/FQ==
|
||||
version "14.0.1"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz"
|
||||
integrity sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA==
|
||||
|
||||
"@types/node@^14":
|
||||
version "14.18.21"
|
||||
@ -3917,14 +3909,14 @@ fs-constants@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs-extra@11.1.1:
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
|
||||
integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==
|
||||
fs-extra@7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz"
|
||||
integrity sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
graceful-fs "^4.1.2"
|
||||
jsonfile "^4.0.0"
|
||||
universalify "^0.1.0"
|
||||
|
||||
fs-extra@^4.0.2, fs-extra@^4.0.3:
|
||||
version "4.0.3"
|
||||
@ -4331,12 +4323,12 @@ graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6,
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
|
||||
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
|
||||
|
||||
graceful-fs@^4.1.2, graceful-fs@^4.1.6:
|
||||
version "4.2.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
graceful-fs@^4.1.2:
|
||||
version "4.2.4"
|
||||
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz"
|
||||
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
||||
|
||||
graceful-fs@^4.2.0, graceful-fs@^4.2.2:
|
||||
graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2:
|
||||
version "4.2.6"
|
||||
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz"
|
||||
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
|
||||
@ -5255,17 +5247,8 @@ json5@^2.2.1:
|
||||
|
||||
jsonfile@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
|
||||
integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
||||
dependencies:
|
||||
universalify "^2.0.0"
|
||||
resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz"
|
||||
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
@ -8401,7 +8384,7 @@ unique-string@^2.0.0:
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"
|
||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||
|
||||
universalify@^0.2.0:
|
||||
@ -8409,11 +8392,6 @@ universalify@^0.2.0:
|
||||
resolved "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz"
|
||||
integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user