mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) In custom widgets show placeholder content until all columns are mapped
Summary: Showing configuration screen when widget is not mapped Test Plan: New test added Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D4192
This commit is contained in:
@@ -8,3 +8,35 @@ iframe.custom_view {
|
||||
padding: 15px;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.custom_view_no_mapping {
|
||||
padding: 15px;
|
||||
margin: 15px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--grist-theme-text, var(--grist-color-dark));
|
||||
}
|
||||
|
||||
.custom_view_no_mapping h1 {
|
||||
max-width: 310px;
|
||||
margin-bottom: 24px;
|
||||
margin-top: 56px;
|
||||
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.custom_view_no_mapping p {
|
||||
max-width: 310px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ import {
|
||||
WidgetFrame
|
||||
} from 'app/client/components/WidgetFrame';
|
||||
import {CustomSectionElement, ViewProcess} from 'app/client/lib/CustomSectionElement';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {Disposable} from 'app/client/lib/dispose';
|
||||
import dom from 'app/client/lib/dom';
|
||||
import {makeTestId} from 'app/client/lib/domUtils';
|
||||
import * as kd from 'app/client/lib/koDom';
|
||||
import DataTableModel from 'app/client/models/DataTableModel';
|
||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||
@@ -28,12 +30,14 @@ import {closeRegisteredMenu} from 'app/client/ui2018/menus';
|
||||
import {AccessLevel} from 'app/common/CustomWidget';
|
||||
import {defaultLocale} from 'app/common/gutil';
|
||||
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');
|
||||
|
||||
const t = makeT('CustomView');
|
||||
const testId = makeTestId('test-custom-widget-');
|
||||
|
||||
/**
|
||||
*
|
||||
* Built in settings for a custom widget. Used when the custom
|
||||
@@ -104,6 +108,7 @@ export class CustomView extends Disposable {
|
||||
private _pluginInstance: PluginInstance|undefined;
|
||||
|
||||
private _frame: WidgetFrame; // plugin frame (holding external page)
|
||||
private _hasUnmappedColumns: ko.Computed<boolean>;
|
||||
|
||||
public create(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
|
||||
BaseView.call(this as any, gristDoc, viewSectionModel, { 'addNewRow': true });
|
||||
@@ -124,6 +129,15 @@ export class CustomView extends Disposable {
|
||||
this.autoDispose(this.customDef.sectionId.subscribe(this._updateCustomSection, this));
|
||||
this.autoDispose(commands.createGroup(CustomView._commands, this, this.viewSection.hasFocus));
|
||||
|
||||
this._hasUnmappedColumns = this.autoDispose(ko.pureComputed(() => {
|
||||
const columns = this.viewSection.columnsToMap();
|
||||
if (!columns) { return false; }
|
||||
const required = columns.filter(col => typeof col === 'string' || !(col.optional === true))
|
||||
.map(col => typeof col === 'string' ? col : col.name);
|
||||
const mapped = this.viewSection.mappedColumns() || {};
|
||||
return required.some(col => !mapped[col]) && this.customDef.mode() === "url";
|
||||
}));
|
||||
|
||||
this.viewPane = this.autoDispose(this._buildDom());
|
||||
this._updatePluginInstance();
|
||||
}
|
||||
@@ -138,10 +152,6 @@ export class CustomView extends Disposable {
|
||||
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.
|
||||
@@ -207,11 +217,21 @@ export class CustomView extends Disposable {
|
||||
dom.autoDispose(showPluginNotification),
|
||||
dom.autoDispose(showSectionNotification),
|
||||
dom.autoDispose(showPluginContent),
|
||||
|
||||
kd.maybe(this._hasUnmappedColumns, () => dom('div.custom_view_no_mapping',
|
||||
testId('not-mapped'),
|
||||
dom('img', {src: 'img/empty-widget.svg'}),
|
||||
dom('h1', kd.text(t("Some required columns aren't mapped"))),
|
||||
dom('p',
|
||||
t('To use this widget, please map all non-optional columns from the creator panel on the right.')
|
||||
),
|
||||
)),
|
||||
// todo: should display content in webview when running electron
|
||||
// prefer widgetId; spelunk in widgetDef for older docs
|
||||
kd.scope(() => [mode(), url(), access(), widgetId() || widgetDef()?.widgetId || '', pluginId()],
|
||||
([_mode, _url, _access, _widgetId, _pluginId]: string[]) =>
|
||||
_mode === "url" ?
|
||||
kd.scope(() => [
|
||||
this._hasUnmappedColumns(), mode(), url(), access(), widgetId() || widgetDef()?.widgetId || '', pluginId()
|
||||
], ([_hide, _mode, _url, _access, _widgetId, _pluginId]: string[]) =>
|
||||
_mode === "url" && !_hide ?
|
||||
this._buildIFrame({
|
||||
baseUrl: _url,
|
||||
access: builtInSettings.accessLevel || (_access as AccessLevel || AccessLevel.none),
|
||||
@@ -254,7 +274,7 @@ export class CustomView extends Disposable {
|
||||
const documentSettings = this.gristDoc.docData.docSettings();
|
||||
const readonly = this.gristDoc.isReadonly.get();
|
||||
const widgetFrame = WidgetFrame.create(null, {
|
||||
url: baseUrl || this.getEmptyWidgetPage(),
|
||||
url: baseUrl,
|
||||
widgetId,
|
||||
pluginId,
|
||||
access,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||
import {BulkColValues, fromTableDataAction, RowRecord} from 'app/common/DocActions';
|
||||
import {extractInfoFromColType, reencodeAsAny} from 'app/common/gristTypes';
|
||||
import {Theme} from 'app/common/ThemePrefs';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {
|
||||
AccessTokenOptions, CursorPos, CustomSectionAPI, FetchSelectedOptions, GristDocAPI, GristView,
|
||||
InteractionOptionsRequest, WidgetAPI, WidgetColumnMap
|
||||
@@ -45,7 +46,7 @@ export interface WidgetFrameOptions {
|
||||
/**
|
||||
* Url of external page. Iframe is rebuild each time the URL changes.
|
||||
*/
|
||||
url: string;
|
||||
url: string|null;
|
||||
/**
|
||||
* ID of widget, if known. When set, the url for the specified widget
|
||||
* in the WidgetRepository, if found, will take precedence.
|
||||
@@ -102,6 +103,12 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
private _visible = Observable.create(this, !this._options.showAfterReady);
|
||||
private readonly _widget = Observable.create<ICustomWidget|null>(this, null);
|
||||
|
||||
private _url: Observable<string>;
|
||||
/**
|
||||
* If the widget URL is empty, it also means that we are showing the empty page.
|
||||
*/
|
||||
private _isEmpty: Observable<boolean>;
|
||||
|
||||
constructor(private _options: WidgetFrameOptions) {
|
||||
super();
|
||||
_options.access = _options.access || AccessLevel.none;
|
||||
@@ -129,6 +136,22 @@ export class WidgetFrame extends DisposableWithEvents {
|
||||
_options.configure?.(this);
|
||||
|
||||
this._checkWidgetRepository().catch(reportError);
|
||||
|
||||
// Url if set.
|
||||
const maybeUrl = Computed.create(this, use => use(this._widget)?.url || this._options.url);
|
||||
|
||||
// Url to widget or empty page with access level and preferences.
|
||||
this._url = Computed.create(this, use => this._urlWithAccess(use(maybeUrl) || this._getEmptyWidgetPage()));
|
||||
|
||||
// Iframe is empty when url is not set.
|
||||
this._isEmpty = Computed.create(this, use => !use(maybeUrl));
|
||||
|
||||
// When isEmpty is switched to true, reset the ready state.
|
||||
this.autoDispose(this._isEmpty.addListener(isEmpty => {
|
||||
if (isEmpty) {
|
||||
this._readyCalled.set(false);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,30 +213,30 @@ 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._widget))),
|
||||
dom.attr('src', this._url),
|
||||
hooks.iframeAttributes,
|
||||
testId('ready', this._readyCalled),
|
||||
testId('ready', use => use(this._readyCalled) && !use(this._isEmpty)),
|
||||
self => void onElem(self),
|
||||
);
|
||||
return this._iframe;
|
||||
}
|
||||
|
||||
private _getUrl(widget: ICustomWidget|null): string {
|
||||
// Append access level to query string.
|
||||
const urlWithAccess = (url: string) => {
|
||||
if (!url) {
|
||||
return url;
|
||||
}
|
||||
const urlObj = new URL(url);
|
||||
urlObj.searchParams.append('access', this._options.access);
|
||||
urlObj.searchParams.append('readonly', String(this._options.readonly));
|
||||
// Append user and document preferences to query string.
|
||||
const settingsParams = new URLSearchParams(this._options.preferences);
|
||||
settingsParams.forEach((value, key) => urlObj.searchParams.append(key, value));
|
||||
return urlObj.href;
|
||||
};
|
||||
const url = widget?.url || this._options.url || 'about:blank';
|
||||
return urlWithAccess(url);
|
||||
// Appends access level to query string.
|
||||
private _urlWithAccess(url: string) {
|
||||
if (!url) {
|
||||
return url;
|
||||
}
|
||||
const urlObj = new URL(url);
|
||||
urlObj.searchParams.append('access', this._options.access);
|
||||
urlObj.searchParams.append('readonly', String(this._options.readonly));
|
||||
// Append user and document preferences to query string.
|
||||
const settingsParams = new URLSearchParams(this._options.preferences);
|
||||
settingsParams.forEach((value, key) => urlObj.searchParams.append(key, value));
|
||||
return urlObj.href;
|
||||
}
|
||||
|
||||
private _getEmptyWidgetPage(): string {
|
||||
return new URL("custom-widget.html", getGristConfig().homeUrl!).href;
|
||||
}
|
||||
|
||||
private _onMessage(event: MessageEvent) {
|
||||
|
||||
Reference in New Issue
Block a user