mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +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:
parent
ca990bbfe6
commit
42d7e31d27
@ -8,3 +8,35 @@ iframe.custom_view {
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin: 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
|
WidgetFrame
|
||||||
} from 'app/client/components/WidgetFrame';
|
} from 'app/client/components/WidgetFrame';
|
||||||
import {CustomSectionElement, ViewProcess} from 'app/client/lib/CustomSectionElement';
|
import {CustomSectionElement, ViewProcess} from 'app/client/lib/CustomSectionElement';
|
||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {Disposable} from 'app/client/lib/dispose';
|
import {Disposable} from 'app/client/lib/dispose';
|
||||||
import dom from 'app/client/lib/dom';
|
import dom from 'app/client/lib/dom';
|
||||||
|
import {makeTestId} from 'app/client/lib/domUtils';
|
||||||
import * as kd from 'app/client/lib/koDom';
|
import * as kd from 'app/client/lib/koDom';
|
||||||
import DataTableModel from 'app/client/models/DataTableModel';
|
import DataTableModel from 'app/client/models/DataTableModel';
|
||||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
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 {AccessLevel} from 'app/common/CustomWidget';
|
||||||
import {defaultLocale} from 'app/common/gutil';
|
import {defaultLocale} from 'app/common/gutil';
|
||||||
import {PluginInstance} from 'app/common/PluginInstance';
|
import {PluginInstance} from 'app/common/PluginInstance';
|
||||||
import {getGristConfig} from 'app/common/urlUtils';
|
|
||||||
import {Events as BackboneEvents} from 'backbone';
|
import {Events as BackboneEvents} from 'backbone';
|
||||||
import {dom as grains} from 'grainjs';
|
import {dom as grains} from 'grainjs';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
import defaults = require('lodash/defaults');
|
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
|
* 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 _pluginInstance: PluginInstance|undefined;
|
||||||
|
|
||||||
private _frame: WidgetFrame; // plugin frame (holding external page)
|
private _frame: WidgetFrame; // plugin frame (holding external page)
|
||||||
|
private _hasUnmappedColumns: ko.Computed<boolean>;
|
||||||
|
|
||||||
public create(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
|
public create(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
|
||||||
BaseView.call(this as any, gristDoc, viewSectionModel, { 'addNewRow': true });
|
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(this.customDef.sectionId.subscribe(this._updateCustomSection, this));
|
||||||
this.autoDispose(commands.createGroup(CustomView._commands, this, this.viewSection.hasFocus));
|
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.viewPane = this.autoDispose(this._buildDom());
|
||||||
this._updatePluginInstance();
|
this._updatePluginInstance();
|
||||||
}
|
}
|
||||||
@ -138,10 +152,6 @@ export class CustomView extends Disposable {
|
|||||||
return {};
|
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 plugin instance that matches the plugin id, update the `found` observables, then tries to
|
||||||
* find a matching section.
|
* find a matching section.
|
||||||
@ -207,11 +217,21 @@ export class CustomView extends Disposable {
|
|||||||
dom.autoDispose(showPluginNotification),
|
dom.autoDispose(showPluginNotification),
|
||||||
dom.autoDispose(showSectionNotification),
|
dom.autoDispose(showSectionNotification),
|
||||||
dom.autoDispose(showPluginContent),
|
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
|
// todo: should display content in webview when running electron
|
||||||
// prefer widgetId; spelunk in widgetDef for older docs
|
// prefer widgetId; spelunk in widgetDef for older docs
|
||||||
kd.scope(() => [mode(), url(), access(), widgetId() || widgetDef()?.widgetId || '', pluginId()],
|
kd.scope(() => [
|
||||||
([_mode, _url, _access, _widgetId, _pluginId]: string[]) =>
|
this._hasUnmappedColumns(), mode(), url(), access(), widgetId() || widgetDef()?.widgetId || '', pluginId()
|
||||||
_mode === "url" ?
|
], ([_hide, _mode, _url, _access, _widgetId, _pluginId]: string[]) =>
|
||||||
|
_mode === "url" && !_hide ?
|
||||||
this._buildIFrame({
|
this._buildIFrame({
|
||||||
baseUrl: _url,
|
baseUrl: _url,
|
||||||
access: builtInSettings.accessLevel || (_access as AccessLevel || AccessLevel.none),
|
access: builtInSettings.accessLevel || (_access as AccessLevel || AccessLevel.none),
|
||||||
@ -254,7 +274,7 @@ export class CustomView extends Disposable {
|
|||||||
const documentSettings = this.gristDoc.docData.docSettings();
|
const documentSettings = this.gristDoc.docData.docSettings();
|
||||||
const readonly = this.gristDoc.isReadonly.get();
|
const readonly = this.gristDoc.isReadonly.get();
|
||||||
const widgetFrame = WidgetFrame.create(null, {
|
const widgetFrame = WidgetFrame.create(null, {
|
||||||
url: baseUrl || this.getEmptyWidgetPage(),
|
url: baseUrl,
|
||||||
widgetId,
|
widgetId,
|
||||||
pluginId,
|
pluginId,
|
||||||
access,
|
access,
|
||||||
|
@ -12,6 +12,7 @@ import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
|||||||
import {BulkColValues, fromTableDataAction, RowRecord} from 'app/common/DocActions';
|
import {BulkColValues, fromTableDataAction, RowRecord} from 'app/common/DocActions';
|
||||||
import {extractInfoFromColType, reencodeAsAny} from 'app/common/gristTypes';
|
import {extractInfoFromColType, reencodeAsAny} from 'app/common/gristTypes';
|
||||||
import {Theme} from 'app/common/ThemePrefs';
|
import {Theme} from 'app/common/ThemePrefs';
|
||||||
|
import {getGristConfig} from 'app/common/urlUtils';
|
||||||
import {
|
import {
|
||||||
AccessTokenOptions, CursorPos, CustomSectionAPI, FetchSelectedOptions, GristDocAPI, GristView,
|
AccessTokenOptions, CursorPos, CustomSectionAPI, FetchSelectedOptions, GristDocAPI, GristView,
|
||||||
InteractionOptionsRequest, WidgetAPI, WidgetColumnMap
|
InteractionOptionsRequest, WidgetAPI, WidgetColumnMap
|
||||||
@ -45,7 +46,7 @@ export interface WidgetFrameOptions {
|
|||||||
/**
|
/**
|
||||||
* Url of external page. Iframe is rebuild each time the URL changes.
|
* 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
|
* ID of widget, if known. When set, the url for the specified widget
|
||||||
* in the WidgetRepository, if found, will take precedence.
|
* 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 _visible = Observable.create(this, !this._options.showAfterReady);
|
||||||
private readonly _widget = Observable.create<ICustomWidget|null>(this, null);
|
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) {
|
constructor(private _options: WidgetFrameOptions) {
|
||||||
super();
|
super();
|
||||||
_options.access = _options.access || AccessLevel.none;
|
_options.access = _options.access || AccessLevel.none;
|
||||||
@ -129,6 +136,22 @@ export class WidgetFrame extends DisposableWithEvents {
|
|||||||
_options.configure?.(this);
|
_options.configure?.(this);
|
||||||
|
|
||||||
this._checkWidgetRepository().catch(reportError);
|
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.style('visibility', use => use(this._visible) ? 'visible' : 'hidden'),
|
||||||
dom.cls('clipboard_focus'),
|
dom.cls('clipboard_focus'),
|
||||||
dom.cls('custom_view'),
|
dom.cls('custom_view'),
|
||||||
dom.attr('src', use => this._getUrl(use(this._widget))),
|
dom.attr('src', this._url),
|
||||||
hooks.iframeAttributes,
|
hooks.iframeAttributes,
|
||||||
testId('ready', this._readyCalled),
|
testId('ready', use => use(this._readyCalled) && !use(this._isEmpty)),
|
||||||
self => void onElem(self),
|
self => void onElem(self),
|
||||||
);
|
);
|
||||||
return this._iframe;
|
return this._iframe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getUrl(widget: ICustomWidget|null): string {
|
// Appends access level to query string.
|
||||||
// Append access level to query string.
|
private _urlWithAccess(url: string) {
|
||||||
const urlWithAccess = (url: string) => {
|
if (!url) {
|
||||||
if (!url) {
|
return url;
|
||||||
return url;
|
}
|
||||||
}
|
const urlObj = new URL(url);
|
||||||
const urlObj = new URL(url);
|
urlObj.searchParams.append('access', this._options.access);
|
||||||
urlObj.searchParams.append('access', this._options.access);
|
urlObj.searchParams.append('readonly', String(this._options.readonly));
|
||||||
urlObj.searchParams.append('readonly', String(this._options.readonly));
|
// Append user and document preferences to query string.
|
||||||
// Append user and document preferences to query string.
|
const settingsParams = new URLSearchParams(this._options.preferences);
|
||||||
const settingsParams = new URLSearchParams(this._options.preferences);
|
settingsParams.forEach((value, key) => urlObj.searchParams.append(key, value));
|
||||||
settingsParams.forEach((value, key) => urlObj.searchParams.append(key, value));
|
return urlObj.href;
|
||||||
return urlObj.href;
|
}
|
||||||
};
|
|
||||||
const url = widget?.url || this._options.url || 'about:blank';
|
private _getEmptyWidgetPage(): string {
|
||||||
return urlWithAccess(url);
|
return new URL("custom-widget.html", getGristConfig().homeUrl!).href;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onMessage(event: MessageEvent) {
|
private _onMessage(event: MessageEvent) {
|
||||||
|
128
static/img/empty-widget.svg
Normal file
128
static/img/empty-widget.svg
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<svg width="225" height="138" viewBox="0 0 225 138" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1161_12522)">
|
||||||
|
<path
|
||||||
|
d="M175.131 97.5045C175.43 97.2931 175.675 97.0226 175.851 96.7122C176.027 96.4017 176.128 96.0588 176.147 95.7077C176.166 95.3565 176.104 95.0056 175.963 94.6797C175.823 94.3538 175.608 94.0608 175.335 93.8214L179 86L174.343 86.7421L171.577 94.0557C171.179 94.5149 170.975 95.0953 171.002 95.6867C171.03 96.2782 171.287 96.8396 171.725 97.2647C172.163 97.6898 172.752 97.9491 173.379 97.9932C174.007 98.0374 174.63 97.8635 175.131 97.5045Z"
|
||||||
|
fill="#FFF3DE" />
|
||||||
|
<path
|
||||||
|
d="M182 63.2566L180.701 63C180.701 63 178.883 63.7699 179.143 66.0796C179.403 68.3894 175.506 80.708 175.506 80.708L172 90.9734L176.935 92L180.182 80.1947L182 63.2566Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M187.924 102H160.076C159.526 101.999 158.998 101.775 158.609 101.377C158.22 100.979 158.001 100.438 158 99.875V97.125C158.001 96.5616 158.22 96.0215 158.609 95.6231C158.998 95.2247 159.526 95.0006 160.076 95H187.924C188.474 95.0007 189.002 95.2248 189.391 95.6231C189.78 96.0215 189.999 96.5616 190 97.125V99.875C189.999 100.438 189.78 100.978 189.391 101.377C189.002 101.775 188.474 101.999 187.924 102Z"
|
||||||
|
fill="#16B378" />
|
||||||
|
<path d="M186 130.952L183.241 132L178 121.547L182.072 120L186 130.952Z" fill="#FFF3DE" />
|
||||||
|
<path
|
||||||
|
d="M180.261 131.792L185.44 129.874L186.821 133.628L178.381 136.754C178.2 136.261 178.104 135.742 178.101 135.225C178.097 134.709 178.185 134.207 178.36 133.746C178.534 133.285 178.792 132.875 179.118 132.54C179.444 132.205 179.833 131.951 180.261 131.792Z"
|
||||||
|
fill="#494949" />
|
||||||
|
<path d="M190.526 136L187.815 135.861L187 124L191 124.205L190.526 136Z" fill="#FFF3DE" />
|
||||||
|
<path
|
||||||
|
d="M185.477 134H191V138H182C182 137.475 182.09 136.955 182.265 136.469C182.439 135.984 182.696 135.543 183.018 135.172C183.341 134.8 183.725 134.505 184.147 134.304C184.568 134.103 185.021 134 185.477 134Z"
|
||||||
|
fill="#494949" />
|
||||||
|
<path
|
||||||
|
d="M177.866 88.5146C175.543 91.1819 174.178 94.555 173.988 98.0942L173 116.561L181.707 132L186.573 129.427L180.683 117.848L184.268 106.784L194 99.0643L193.232 88L177.866 88.5146Z"
|
||||||
|
fill="#494949" />
|
||||||
|
<path d="M191.368 98L194 99.0142L191.895 134L185.842 133.24L184 106.62L191.368 98Z"
|
||||||
|
fill="#494949" />
|
||||||
|
<path
|
||||||
|
d="M186.041 94.2158C186.103 93.8788 186.24 93.5591 186.443 93.2793C186.646 92.9994 186.91 92.7662 187.215 92.5961C187.521 92.4259 187.862 92.323 188.213 92.2945C188.564 92.266 188.917 92.3127 189.248 92.4313L194.684 86L196 90.1311L190.636 95.605C190.394 96.1314 189.962 96.5533 189.422 96.7908C188.881 97.0283 188.27 97.0649 187.703 96.8937C187.137 96.7225 186.655 96.3553 186.348 95.8617C186.042 95.3682 185.932 94.7825 186.041 94.2158Z"
|
||||||
|
fill="#FFF3DE" />
|
||||||
|
<path
|
||||||
|
d="M188 59C191.314 59 194 56.3137 194 53C194 49.6863 191.314 47 188 47C184.686 47 182 49.6863 182 53C182 56.3137 184.686 59 188 59Z"
|
||||||
|
fill="#FFF3DE" />
|
||||||
|
<path
|
||||||
|
d="M196.721 64.3993C196.721 64.3993 190.023 57.9036 180.845 62.8709C180.845 62.8709 178.488 87.4527 177 90C177 90 189.899 88.2169 193.372 89.2358C193.372 89.2358 193.372 78.537 195.605 74.4613C197.837 70.3855 196.721 64.3993 196.721 64.3993Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M194.5 64.5242L197.243 64C198.612 64.8234 199.672 66.1017 200.25 67.627C201.25 70.2125 204 82.882 204 82.882L193.25 94L191 90.6387L197.75 81.3306L194.25 73.5738L194.5 64.5242Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M193.136 55.5336C193.161 55.4579 193.168 55.3772 193.159 55.2979C193.149 55.2185 193.122 55.1425 193.079 55.0757C193.037 55.0089 192.98 54.953 192.913 54.9124C192.847 54.8718 192.772 54.8475 192.695 54.8413C192.537 54.8502 192.383 54.8921 192.241 54.9645C192.099 55.0369 191.973 55.1382 191.871 55.2625C191.66 55.5141 191.414 55.7323 191.142 55.91C190.86 56.0571 190.478 56.0361 190.317 55.7557C190.166 55.4925 190.27 55.1399 190.361 54.8341C190.593 54.056 190.724 53.2491 190.748 52.435C190.774 51.5264 190.65 50.5782 190.164 49.8695C189.537 48.9548 188.416 48.6076 187.354 48.6328C186.293 48.658 185.249 48.9971 184.197 49.2115C183.835 49.2855 183.408 49.3233 183.165 49.0322C182.907 48.7226 183.002 48.2215 183.112 47.8035C183.397 46.7188 183.699 45.5973 184.361 44.7212C185.076 43.8109 186.099 43.2152 187.222 43.0539C188.3 42.9156 189.394 43.0443 190.415 43.4292C192.079 43.9718 193.542 45.0272 194.61 46.4541C195.711 47.9715 196.187 49.8785 195.933 51.758C195.686 53.395 194.848 54.874 193.588 55.8967"
|
||||||
|
fill="#494949" />
|
||||||
|
<path
|
||||||
|
d="M130.828 103H2.17241C1.59645 102.999 1.04427 102.768 0.637002 102.356C0.229736 101.944 0.000649097 101.386 0 100.803V2.19668C0.000668861 1.61429 0.229762 1.05595 0.637024 0.64414C1.04429 0.232329 1.59646 0.000676333 2.17241 0H130.828C131.404 0.000656348 131.956 0.232306 132.363 0.644122C132.77 1.05594 132.999 1.61429 133 2.19668V100.803C132.999 101.386 132.77 101.944 132.363 102.356C131.956 102.768 131.404 102.999 130.828 103ZM2.17241 0.488152C1.72444 0.488659 1.29497 0.668828 0.978202 0.98913C0.661439 1.30943 0.483261 1.74371 0.482759 2.19668V100.803C0.483281 101.256 0.661462 101.691 0.97822 102.011C1.29498 102.331 1.72445 102.511 2.17241 102.512H130.828C131.276 102.511 131.705 102.331 132.022 102.011C132.339 101.691 132.517 101.256 132.517 100.803V2.19668C132.517 1.74371 132.339 1.30945 132.022 0.989152C131.705 0.668855 131.276 0.48868 130.828 0.488152H2.17241Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M118.826 23H15.1745C14.598 22.9993 14.0453 22.7669 13.6376 22.3537C13.23 21.9405 13.0007 21.3803 13 20.7959V13.2041C13.0007 12.6197 13.23 12.0595 13.6376 11.6463C14.0453 11.2331 14.598 11.0007 15.1745 11H118.826C119.402 11.0007 119.955 11.2331 120.362 11.6463C120.77 12.0595 120.999 12.6197 121 13.2041V20.7959C120.999 21.3803 120.77 21.9405 120.362 22.3537C119.955 22.7669 119.402 22.9993 118.826 23ZM15.1745 11.4898C14.7261 11.4903 14.2962 11.6711 13.9791 11.9925C13.6621 12.3138 13.4837 12.7496 13.4832 13.2041V20.7959C13.4837 21.2504 13.6621 21.6862 13.9791 22.0075C14.2962 22.3289 14.7261 22.5097 15.1745 22.5102H118.826C119.274 22.5097 119.704 22.3289 120.021 22.0075C120.338 21.6861 120.516 21.2504 120.517 20.7959V13.2041C120.516 12.7496 120.338 12.3139 120.021 11.9925C119.704 11.6711 119.274 11.4903 118.826 11.4898H15.1745Z"
|
||||||
|
fill="#262633" />
|
||||||
|
<path
|
||||||
|
d="M56.7979 44H15.2021C14.6183 43.9993 14.0586 43.7669 13.6457 43.3537C13.2329 42.9405 13.0007 42.3803 13 41.7959V34.2041C13.0007 33.6197 13.2329 33.0595 13.6457 32.6463C14.0586 32.2331 14.6183 32.0007 15.2021 32H56.7979C57.3817 32.0007 57.9414 32.2331 58.3543 32.6463C58.7671 33.0595 58.9993 33.6197 59 34.2041V41.7959C58.9993 42.3803 58.7671 42.9405 58.3543 43.3537C57.9414 43.7669 57.3817 43.9993 56.7979 44ZM15.2021 32.4898C14.748 32.4903 14.3127 32.6711 13.9916 32.9925C13.6705 33.3138 13.4899 33.7496 13.4894 34.2041V41.7959C13.4899 42.2504 13.6705 42.6862 13.9916 43.0075C14.3127 43.3289 14.748 43.5097 15.2021 43.5102H56.7979C57.252 43.5097 57.6873 43.3289 58.0084 43.0075C58.3295 42.6862 58.5101 42.2504 58.5106 41.7959V34.2041C58.5101 33.7496 58.3295 33.3138 58.0084 32.9925C57.6873 32.6711 57.252 32.4903 56.7979 32.4898H15.2021Z"
|
||||||
|
fill="#262633" />
|
||||||
|
<path
|
||||||
|
d="M118.798 44H77.2021C76.6183 43.9993 76.0586 43.7669 75.6457 43.3537C75.2329 42.9405 75.0007 42.3803 75 41.7959V34.2041C75.0007 33.6197 75.2329 33.0595 75.6457 32.6463C76.0586 32.2331 76.6183 32.0007 77.2021 32H118.798C119.382 32.0007 119.941 32.2331 120.354 32.6463C120.767 33.0595 120.999 33.6197 121 34.2041V41.7959C120.999 42.3803 120.767 42.9405 120.354 43.3537C119.941 43.7669 119.382 43.9993 118.798 44ZM77.2021 32.4898C76.748 32.4903 76.3127 32.6711 75.9916 32.9925C75.6705 33.3138 75.4899 33.7496 75.4894 34.2041V41.7959C75.4899 42.2504 75.6705 42.6862 75.9916 43.0075C76.3127 43.3289 76.748 43.5097 77.2021 43.5102H118.798C119.252 43.5097 119.687 43.3289 120.008 43.0075C120.329 42.6861 120.51 42.2504 120.511 41.7959V34.2041C120.51 33.7496 120.329 33.3139 120.008 32.9925C119.687 32.6711 119.252 32.4903 118.798 32.4898H77.2021Z"
|
||||||
|
fill="#262633" />
|
||||||
|
<path
|
||||||
|
d="M118.924 91H91.0763C90.5259 90.9994 89.9981 90.7753 89.6088 90.3769C89.2196 89.9785 89.0006 89.4384 89 88.875V86.125C89.0006 85.5616 89.2196 85.0215 89.6088 84.6231C89.9981 84.2247 90.5259 84.0006 91.0763 84H118.924C119.474 84.0007 120.002 84.2248 120.391 84.6231C120.78 85.0215 120.999 85.5616 121 86.125V88.875C120.999 89.4384 120.78 89.9785 120.391 90.3769C120.002 90.7752 119.474 90.9993 118.924 91Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M19 56C18.6044 56 18.2178 55.8827 17.8889 55.6629C17.56 55.4432 17.3036 55.1308 17.1522 54.7654C17.0009 54.3999 16.9613 53.9978 17.0384 53.6098C17.1156 53.2219 17.3061 52.8655 17.5858 52.5858C17.8655 52.3061 18.2219 52.1156 18.6098 52.0384C18.9978 51.9613 19.3999 52.0009 19.7654 52.1522C20.1308 52.3036 20.4432 52.56 20.6629 52.8889C20.8827 53.2178 21 53.6044 21 54C20.9994 54.5302 20.7885 55.0386 20.4135 55.4135C20.0386 55.7885 19.5302 55.9994 19 56ZM19 52.5C18.7033 52.5 18.4133 52.588 18.1666 52.7528C17.92 52.9176 17.7277 53.1519 17.6142 53.426C17.5006 53.7001 17.4709 54.0017 17.5288 54.2926C17.5867 54.5836 17.7296 54.8509 17.9393 55.0607C18.1491 55.2704 18.4164 55.4133 18.7074 55.4712C18.9983 55.5291 19.2999 55.4993 19.574 55.3858C19.8481 55.2723 20.0824 55.08 20.2472 54.8334C20.412 54.5867 20.5 54.2967 20.5 54C20.4996 53.6023 20.3414 53.221 20.0602 52.9398C19.779 52.6586 19.3977 52.5004 19 52.5Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M19 63C18.6044 63 18.2178 62.8827 17.8889 62.6629C17.56 62.4432 17.3036 62.1308 17.1522 61.7654C17.0009 61.3999 16.9613 60.9978 17.0384 60.6098C17.1156 60.2219 17.3061 59.8655 17.5858 59.5858C17.8655 59.3061 18.2219 59.1156 18.6098 59.0384C18.9978 58.9613 19.3999 59.0009 19.7654 59.1522C20.1308 59.3036 20.4432 59.56 20.6629 59.8889C20.8827 60.2178 21 60.6044 21 61C20.9994 61.5302 20.7885 62.0386 20.4135 62.4135C20.0386 62.7885 19.5302 62.9994 19 63ZM19 59.5C18.7033 59.5 18.4133 59.588 18.1666 59.7528C17.92 59.9176 17.7277 60.1519 17.6142 60.426C17.5006 60.7001 17.4709 61.0017 17.5288 61.2926C17.5867 61.5836 17.7296 61.8509 17.9393 62.0607C18.1491 62.2704 18.4164 62.4133 18.7074 62.4712C18.9983 62.5291 19.2999 62.4993 19.574 62.3858C19.8481 62.2723 20.0824 62.08 20.2472 61.8334C20.412 61.5867 20.5 61.2967 20.5 61C20.4996 60.6023 20.3414 60.221 20.0602 59.9398C19.779 59.6586 19.3977 59.5004 19 59.5Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M19 69C18.6044 69 18.2178 68.8827 17.8889 68.6629C17.56 68.4432 17.3036 68.1308 17.1522 67.7654C17.0009 67.3999 16.9613 66.9978 17.0384 66.6098C17.1156 66.2219 17.3061 65.8655 17.5858 65.5858C17.8655 65.3061 18.2219 65.1156 18.6098 65.0384C18.9978 64.9613 19.3999 65.0009 19.7654 65.1522C20.1308 65.3036 20.4432 65.56 20.6629 65.8889C20.8827 66.2178 21 66.6044 21 67C20.9994 67.5302 20.7885 68.0386 20.4135 68.4135C20.0386 68.7885 19.5302 68.9994 19 69ZM19 65.5C18.7033 65.5 18.4133 65.588 18.1666 65.7528C17.92 65.9176 17.7277 66.1519 17.6142 66.426C17.5006 66.7001 17.4709 67.0017 17.5288 67.2926C17.5867 67.5836 17.7296 67.8509 17.9393 68.0607C18.1491 68.2704 18.4164 68.4133 18.7074 68.4712C18.9983 68.5291 19.2999 68.4993 19.574 68.3858C19.8481 68.2723 20.0824 68.08 20.2472 67.8334C20.412 67.5867 20.5 67.2967 20.5 67C20.4996 66.6023 20.3414 66.221 20.0602 65.9398C19.779 65.6586 19.3977 65.5004 19 65.5Z"
|
||||||
|
fill="#262633" />
|
||||||
|
<path
|
||||||
|
d="M28.5943 53C28.1715 53 27.766 53.158 27.467 53.4393C27.168 53.7206 27 54.1022 27 54.5C27 54.8978 27.168 55.2794 27.467 55.5607C27.766 55.842 28.1715 56 28.5943 56H51.4057C51.8285 56 52.234 55.842 52.533 55.5607C52.832 55.2794 53 54.8978 53 54.5C53 54.1022 52.832 53.7206 52.533 53.4393C52.234 53.158 51.8285 53 51.4057 53H28.5943Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M28.5943 59C28.1715 59 27.766 59.158 27.467 59.4393C27.168 59.7206 27 60.1022 27 60.5C27 60.8978 27.168 61.2794 27.467 61.5607C27.766 61.842 28.1715 62 28.5943 62H51.4057C51.8285 62 52.234 61.842 52.533 61.5607C52.832 61.2794 53 60.8978 53 60.5C53 60.1022 52.832 59.7206 52.533 59.4393C52.234 59.158 51.8285 59 51.4057 59H28.5943Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M28.5943 65C28.1715 65 27.766 65.158 27.467 65.4393C27.168 65.7206 27 66.1022 27 66.5C27 66.8978 27.168 67.2794 27.467 67.5607C27.766 67.842 28.1715 68 28.5943 68H51.4057C51.8285 68 52.234 67.842 52.533 67.5607C52.832 67.2794 53 66.8978 53 66.5C53 66.1022 52.832 65.7206 52.533 65.4393C52.234 65.158 51.8285 65 51.4057 65H28.5943Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M82 56C81.6044 56 81.2178 55.8827 80.8889 55.6629C80.56 55.4432 80.3036 55.1308 80.1522 54.7654C80.0009 54.3999 79.9613 53.9978 80.0384 53.6098C80.1156 53.2219 80.3061 52.8655 80.5858 52.5858C80.8655 52.3061 81.2219 52.1156 81.6098 52.0384C81.9978 51.9613 82.3999 52.0009 82.7654 52.1522C83.1308 52.3036 83.4432 52.56 83.6629 52.8889C83.8827 53.2178 84 53.6044 84 54C83.9994 54.5302 83.7885 55.0386 83.4135 55.4135C83.0386 55.7885 82.5302 55.9994 82 56ZM82 52.5C81.7033 52.5 81.4133 52.588 81.1666 52.7528C80.92 52.9176 80.7277 53.1519 80.6142 53.426C80.5007 53.7001 80.4709 54.0017 80.5288 54.2926C80.5867 54.5836 80.7296 54.8509 80.9393 55.0607C81.1491 55.2704 81.4164 55.4133 81.7074 55.4712C81.9983 55.5291 82.2999 55.4993 82.574 55.3858C82.8481 55.2723 83.0824 55.08 83.2472 54.8334C83.412 54.5867 83.5 54.2967 83.5 54C83.4996 53.6023 83.3414 53.221 83.0602 52.9398C82.779 52.6586 82.3977 52.5004 82 52.5Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M82 63C81.6044 63 81.2178 62.8827 80.8889 62.6629C80.56 62.4432 80.3036 62.1308 80.1522 61.7654C80.0009 61.3999 79.9613 60.9978 80.0384 60.6098C80.1156 60.2219 80.3061 59.8655 80.5858 59.5858C80.8655 59.3061 81.2219 59.1156 81.6098 59.0384C81.9978 58.9613 82.3999 59.0009 82.7654 59.1522C83.1308 59.3036 83.4432 59.56 83.6629 59.8889C83.8827 60.2178 84 60.6044 84 61C83.9994 61.5302 83.7885 62.0386 83.4135 62.4135C83.0386 62.7885 82.5302 62.9994 82 63ZM82 59.5C81.7033 59.5 81.4133 59.588 81.1666 59.7528C80.92 59.9176 80.7277 60.1519 80.6142 60.426C80.5007 60.7001 80.4709 61.0017 80.5288 61.2926C80.5867 61.5836 80.7296 61.8509 80.9393 62.0607C81.1491 62.2704 81.4164 62.4133 81.7074 62.4712C81.9983 62.5291 82.2999 62.4993 82.574 62.3858C82.8481 62.2723 83.0824 62.08 83.2472 61.8334C83.412 61.5867 83.5 61.2967 83.5 61C83.4996 60.6023 83.3414 60.221 83.0602 59.9398C82.779 59.6586 82.3977 59.5004 82 59.5Z"
|
||||||
|
fill="#262633" />
|
||||||
|
<path
|
||||||
|
d="M82 69C81.6044 69 81.2178 68.8827 80.8889 68.6629C80.56 68.4432 80.3036 68.1308 80.1522 67.7654C80.0009 67.3999 79.9613 66.9978 80.0384 66.6098C80.1156 66.2219 80.3061 65.8655 80.5858 65.5858C80.8655 65.3061 81.2219 65.1156 81.6098 65.0384C81.9978 64.9613 82.3999 65.0009 82.7654 65.1522C83.1308 65.3036 83.4432 65.56 83.6629 65.8889C83.8827 66.2178 84 66.6044 84 67C83.9994 67.5302 83.7885 68.0386 83.4135 68.4135C83.0386 68.7885 82.5302 68.9994 82 69ZM82 65.5C81.7033 65.5 81.4133 65.588 81.1666 65.7528C80.92 65.9176 80.7277 66.1519 80.6142 66.426C80.5007 66.7001 80.4709 67.0017 80.5288 67.2926C80.5867 67.5836 80.7296 67.8509 80.9393 68.0607C81.1491 68.2704 81.4164 68.4133 81.7074 68.4712C81.9983 68.5291 82.2999 68.4993 82.574 68.3858C82.8481 68.2723 83.0824 68.08 83.2472 67.8334C83.412 67.5867 83.5 67.2967 83.5 67C83.4996 66.6023 83.3414 66.221 83.0602 65.9398C82.779 65.6586 82.3977 65.5004 82 65.5Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M82 75C81.6044 75 81.2178 74.8827 80.8889 74.6629C80.56 74.4432 80.3036 74.1308 80.1522 73.7654C80.0009 73.3999 79.9613 72.9978 80.0384 72.6098C80.1156 72.2219 80.3061 71.8655 80.5858 71.5858C80.8655 71.3061 81.2219 71.1156 81.6098 71.0384C81.9978 70.9613 82.3999 71.0009 82.7654 71.1522C83.1308 71.3036 83.4432 71.56 83.6629 71.8889C83.8827 72.2178 84 72.6044 84 73C83.9994 73.5302 83.7885 74.0386 83.4135 74.4135C83.0386 74.7885 82.5302 74.9994 82 75ZM82 71.5C81.7033 71.5 81.4133 71.588 81.1666 71.7528C80.92 71.9176 80.7277 72.1519 80.6142 72.426C80.5007 72.7001 80.4709 73.0017 80.5288 73.2926C80.5867 73.5836 80.7296 73.8509 80.9393 74.0607C81.1491 74.2704 81.4164 74.4133 81.7074 74.4712C81.9983 74.5291 82.2999 74.4993 82.574 74.3858C82.8481 74.2723 83.0824 74.08 83.2472 73.8334C83.412 73.5867 83.5 73.2967 83.5 73C83.4996 72.6023 83.3414 72.221 83.0602 71.9398C82.779 71.6586 82.3977 71.5004 82 71.5Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M91.5943 53C91.1715 53 90.766 53.158 90.467 53.4393C90.168 53.7206 90 54.1022 90 54.5C90 54.8978 90.168 55.2794 90.467 55.5607C90.766 55.842 91.1715 56 91.5943 56H114.406C114.829 56 115.234 55.842 115.533 55.5607C115.832 55.2794 116 54.8978 116 54.5C116 54.1022 115.832 53.7206 115.533 53.4393C115.234 53.158 114.829 53 114.406 53H91.5943Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M91.5943 59C91.1715 59 90.766 59.158 90.467 59.4393C90.168 59.7206 90 60.1022 90 60.5C90 60.8978 90.168 61.2794 90.467 61.5607C90.766 61.842 91.1715 62 91.5943 62H114.406C114.829 62 115.234 61.842 115.533 61.5607C115.832 61.2794 116 60.8978 116 60.5C116 60.1022 115.832 59.7206 115.533 59.4393C115.234 59.158 114.829 59 114.406 59H91.5943Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M91.5943 65C91.1715 65 90.766 65.158 90.467 65.4393C90.168 65.7206 90 66.1022 90 66.5C90 66.8978 90.168 67.2794 90.467 67.5607C90.766 67.842 91.1715 68 91.5943 68H114.406C114.829 68 115.234 67.842 115.533 67.5607C115.832 67.2794 116 66.8978 116 66.5C116 66.1022 115.832 65.7206 115.533 65.4393C115.234 65.158 114.829 65 114.406 65H91.5943Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M91.5943 72C91.1715 72 90.766 72.158 90.467 72.4393C90.168 72.7206 90 73.1022 90 73.5C90 73.8978 90.168 74.2794 90.467 74.5607C90.766 74.842 91.1715 75 91.5943 75H114.406C114.829 75 115.234 74.842 115.533 74.5607C115.832 74.2794 116 73.8978 116 73.5C116 73.1022 115.832 72.7206 115.533 72.4393C115.234 72.158 114.829 72 114.406 72H91.5943Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M19.5726 36C19.1555 36 18.7555 36.158 18.4606 36.4393C18.1657 36.7206 18 37.1022 18 37.5C18 37.8978 18.1657 38.2794 18.4606 38.5607C18.7555 38.842 19.1555 39 19.5726 39H46.4274C46.8445 39 47.2445 38.842 47.5394 38.5607C47.8343 38.2794 48 37.8978 48 37.5C48 37.1022 47.8343 36.7206 47.5394 36.4393C47.2445 36.158 46.8445 36 46.4274 36H19.5726Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M19.5726 16C19.1555 16 18.7555 16.158 18.4606 16.4393C18.1657 16.7206 18 17.1022 18 17.5C18 17.8978 18.1657 18.2794 18.4606 18.5607C18.7555 18.842 19.1555 19 19.5726 19H46.4274C46.8445 19 47.2445 18.842 47.5394 18.5607C47.8343 18.2794 48 17.8978 48 17.5C48 17.1022 47.8343 16.7206 47.5394 16.4393C47.2445 16.158 46.8445 16 46.4274 16H19.5726Z"
|
||||||
|
fill="#16B378" />
|
||||||
|
<path
|
||||||
|
d="M51.1054 37C51.0869 37 51.0687 37.0055 51.0527 37.016C51.0367 37.0265 51.0234 37.0417 51.0141 37.0599C51.0049 37.0781 51 37.0987 51 37.1197C51 37.1408 51.0049 37.1614 51.0141 37.1796L51.9087 38.9401C51.918 38.9583 51.9313 38.9734 51.9473 38.984C51.9633 38.9945 51.9815 39 52 39C52.0185 39 52.0367 38.9945 52.0527 38.984C52.0687 38.9734 52.082 38.9583 52.0913 38.9401L52.9859 37.1796C52.9951 37.1614 53 37.1408 53 37.1197C53 37.0987 52.9951 37.0781 52.9859 37.0599C52.9766 37.0417 52.9633 37.0265 52.9473 37.016C52.9313 37.0055 52.9131 37 52.8946 37H51.1054Z"
|
||||||
|
fill="#16B378" />
|
||||||
|
<path
|
||||||
|
d="M82.5726 36C82.1555 36 81.7555 36.158 81.4606 36.4393C81.1657 36.7206 81 37.1022 81 37.5C81 37.8978 81.1657 38.2794 81.4606 38.5607C81.7555 38.842 82.1555 39 82.5726 39H109.427C109.844 39 110.244 38.842 110.539 38.5607C110.834 38.2794 111 37.8978 111 37.5C111 37.1022 110.834 36.7206 110.539 36.4393C110.244 36.158 109.844 36 109.427 36H82.5726Z"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
<path
|
||||||
|
d="M113.105 37C113.087 37 113.069 37.0055 113.053 37.016C113.037 37.0265 113.023 37.0417 113.014 37.0599C113.005 37.0781 113 37.0987 113 37.1197C113 37.1408 113.005 37.1614 113.014 37.1796L113.909 38.9401C113.918 38.9583 113.931 38.9734 113.947 38.984C113.963 38.9945 113.981 39 114 39C114.018 39 114.037 38.9945 114.053 38.984C114.069 38.9734 114.082 38.9583 114.091 38.9401L114.986 37.1796C114.995 37.1614 115 37.1408 115 37.1197C115 37.0987 114.995 37.0781 114.986 37.0599C114.977 37.0417 114.963 37.0265 114.947 37.016C114.931 37.0055 114.913 37 114.895 37H113.105Z"
|
||||||
|
fill="#16B378" />
|
||||||
|
<path
|
||||||
|
d="M82 62C82.5523 62 83 61.5523 83 61C83 60.4477 82.5523 60 82 60C81.4477 60 81 60.4477 81 61C81 61.5523 81.4477 62 82 62Z"
|
||||||
|
fill="#16B378" />
|
||||||
|
<path
|
||||||
|
d="M19 68C19.5523 68 20 67.5523 20 67C20 66.4477 19.5523 66 19 66C18.4477 66 18 66.4477 18 67C18 67.5523 18.4477 68 19 68Z"
|
||||||
|
fill="#16B378" />
|
||||||
|
<path
|
||||||
|
d="M224.712 138H147.288C147.211 138 147.138 137.947 147.084 137.854C147.03 137.76 147 137.633 147 137.5C147 137.367 147.03 137.24 147.084 137.146C147.138 137.053 147.211 137 147.288 137H224.712C224.789 137 224.862 137.053 224.916 137.146C224.97 137.24 225 137.367 225 137.5C225 137.633 224.97 137.76 224.916 137.854C224.862 137.947 224.789 138 224.712 138Z"
|
||||||
|
fill="#262633" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1161_12522">
|
||||||
|
<rect width="225" height="138" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
After Width: | Height: | Size: 21 KiB |
@ -40,9 +40,9 @@ describe('AttachedCustomWidget', function () {
|
|||||||
.header('Content-Type', 'text/html')
|
.header('Content-Type', 'text/html')
|
||||||
.send('<html><head><script src="/grist-plugin-api.js"></script></head><body>\n' +
|
.send('<html><head><script src="/grist-plugin-api.js"></script></head><body>\n' +
|
||||||
(req.query.name || req.query.access) + // send back widget name from query string or access level
|
(req.query.name || req.query.access) + // send back widget name from query string or access level
|
||||||
'</body>'+
|
'</body>' +
|
||||||
"<script>grist.ready({requiredAccess: 'full', columns: [{name: 'Content', type: 'Text'}],"+
|
"<script>grist.ready({requiredAccess: 'full', columns: [{name: 'Content', type: 'Text', optional: true}]," +
|
||||||
" onEditOptions(){}})</script>"+
|
" onEditOptions(){}})</script>" +
|
||||||
'</html>\n')
|
'</html>\n')
|
||||||
.end()
|
.end()
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {assert, driver, Key} from 'mocha-webdriver';
|
import {addToRepl, assert, driver, Key} from 'mocha-webdriver';
|
||||||
import * as gu from 'test/nbrowser/gristUtils';
|
import * as gu from 'test/nbrowser/gristUtils';
|
||||||
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
|
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
|
||||||
import {addStatic, serveSomething} from 'test/server/customUtil';
|
import {addStatic, serveSomething} from 'test/server/customUtil';
|
||||||
@ -15,6 +15,7 @@ const NORMAL_WIDGET = 'Normal';
|
|||||||
const READ_WIDGET = 'Read';
|
const READ_WIDGET = 'Read';
|
||||||
const FULL_WIDGET = 'Full';
|
const FULL_WIDGET = 'Full';
|
||||||
const COLUMN_WIDGET = 'COLUMN_WIDGET';
|
const COLUMN_WIDGET = 'COLUMN_WIDGET';
|
||||||
|
const REQUIRED_WIDGET = 'REQUIRED_WIDGET';
|
||||||
// Custom URL label in selectbox.
|
// Custom URL label in selectbox.
|
||||||
const CUSTOM_URL = 'Custom URL';
|
const CUSTOM_URL = 'Custom URL';
|
||||||
// Holds url for sample widget server.
|
// Holds url for sample widget server.
|
||||||
@ -129,6 +130,9 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
let mainSession: gu.Session;
|
let mainSession: gu.Session;
|
||||||
gu.bigScreen();
|
gu.bigScreen();
|
||||||
|
|
||||||
|
|
||||||
|
addToRepl('getOptions', getOptions);
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
if (server.isExternalServer()) {
|
if (server.isExternalServer()) {
|
||||||
this.skip();
|
this.skip();
|
||||||
@ -164,9 +168,15 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
{
|
{
|
||||||
// Widget with column mapping
|
// Widget with column mapping
|
||||||
name: COLUMN_WIDGET,
|
name: COLUMN_WIDGET,
|
||||||
url: createConfigUrl({requiredAccess: AccessLevel.read_table, columns: ['Column']}),
|
url: createConfigUrl({requiredAccess: AccessLevel.read_table, columns: [{name:'Column', optional: true}]}),
|
||||||
widgetId: 'tester5',
|
widgetId: 'tester5',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Widget with required column mapping
|
||||||
|
name: REQUIRED_WIDGET,
|
||||||
|
url: createConfigUrl({requiredAccess: AccessLevel.read_table, columns: [{name:'Column', optional: false}]}),
|
||||||
|
widgetId: 'tester6',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
addStatic(app);
|
addStatic(app);
|
||||||
@ -188,146 +198,6 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
await server.testingHooks.setWidgetRepositoryUrl('');
|
await server.testingHooks.setWidgetRepositoryUrl('');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Poor man widget rpc. Class that invokes various parts in the tester widget.
|
|
||||||
class Widget {
|
|
||||||
constructor() {}
|
|
||||||
// Wait for a frame.
|
|
||||||
public async waitForFrame() {
|
|
||||||
await driver.findWait(`iframe.test-custom-widget-ready`, 1000);
|
|
||||||
await driver.wait(async () => await driver.find('iframe').isDisplayed(), 1000);
|
|
||||||
await widget.waitForPendingRequests();
|
|
||||||
}
|
|
||||||
public async waitForPendingRequests() {
|
|
||||||
await this._inWidgetIframe(async () => {
|
|
||||||
await driver.executeScript('grist.testWaitForPendingRequests();');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
public async content() {
|
|
||||||
return await this._read('body');
|
|
||||||
}
|
|
||||||
public async readonly() {
|
|
||||||
const text = await this._read('#readonly');
|
|
||||||
return text === 'true';
|
|
||||||
}
|
|
||||||
public async access() {
|
|
||||||
const text = await this._read('#access');
|
|
||||||
return text as AccessLevel;
|
|
||||||
}
|
|
||||||
public async onRecordMappings() {
|
|
||||||
const text = await this._read('#onRecordMappings');
|
|
||||||
return JSON.parse(text || 'null');
|
|
||||||
}
|
|
||||||
public async onRecords() {
|
|
||||||
const text = await this._read('#onRecords');
|
|
||||||
return JSON.parse(text || 'null');
|
|
||||||
}
|
|
||||||
public async onRecord() {
|
|
||||||
const text = await this._read('#onRecord');
|
|
||||||
return JSON.parse(text || 'null');
|
|
||||||
}
|
|
||||||
public async onRecordsMappings() {
|
|
||||||
const text = await this._read('#onRecordsMappings');
|
|
||||||
return JSON.parse(text || 'null');
|
|
||||||
}
|
|
||||||
public async log() {
|
|
||||||
const text = await this._read('#log');
|
|
||||||
return text || '';
|
|
||||||
}
|
|
||||||
// Wait for frame to close.
|
|
||||||
public async waitForClose() {
|
|
||||||
await driver.wait(async () => !(await driver.find('iframe').isPresent()), 3000);
|
|
||||||
}
|
|
||||||
// Wait for the onOptions event, and return its value.
|
|
||||||
public async onOptions() {
|
|
||||||
const text = await this._inWidgetIframe(async () => {
|
|
||||||
// Wait for options to get filled, initially this div is empty,
|
|
||||||
// as first message it should get at least null as an options.
|
|
||||||
await driver.wait(async () => await driver.find('#onOptions').getText(), 3000);
|
|
||||||
return await driver.find('#onOptions').getText();
|
|
||||||
});
|
|
||||||
return JSON.parse(text);
|
|
||||||
}
|
|
||||||
public async wasConfigureCalled() {
|
|
||||||
const text = await this._read('#configure');
|
|
||||||
return text === 'called';
|
|
||||||
}
|
|
||||||
public async setOptions(options: any) {
|
|
||||||
return await this.invokeOnWidget('setOptions', [options]);
|
|
||||||
}
|
|
||||||
public async setOption(key: string, value: any) {
|
|
||||||
return await this.invokeOnWidget('setOption', [key, value]);
|
|
||||||
}
|
|
||||||
public async getOption(key: string) {
|
|
||||||
return await this.invokeOnWidget('getOption', [key]);
|
|
||||||
}
|
|
||||||
public async clearOptions() {
|
|
||||||
return await this.invokeOnWidget('clearOptions');
|
|
||||||
}
|
|
||||||
public async getOptions() {
|
|
||||||
return await this.invokeOnWidget('getOptions');
|
|
||||||
}
|
|
||||||
public async mappings() {
|
|
||||||
return await this.invokeOnWidget('mappings');
|
|
||||||
}
|
|
||||||
public async clearLog() {
|
|
||||||
return await this.invokeOnWidget('clearLog');
|
|
||||||
}
|
|
||||||
// Invoke method on a Custom Widget.
|
|
||||||
// Each method is available as a button with content that is equal to the method name.
|
|
||||||
// It accepts single argument, that we pass by serializing it to #input textbox. Widget invokes
|
|
||||||
// the method and serializes its return value to #output div. When there is an error, it is also
|
|
||||||
// serialized to the #output div.
|
|
||||||
public async invokeOnWidget(name: string, input?: any[]) {
|
|
||||||
// Switch to frame.
|
|
||||||
const iframe = driver.find('iframe');
|
|
||||||
await driver.switchTo().frame(iframe);
|
|
||||||
// Clear input box that holds arguments.
|
|
||||||
await driver.find('#input').click();
|
|
||||||
await gu.clearInput();
|
|
||||||
// Serialize argument to the textbox (or leave empty).
|
|
||||||
if (input !== undefined) {
|
|
||||||
await driver.sendKeys(JSON.stringify(input));
|
|
||||||
}
|
|
||||||
// Find button that is responsible for invoking method.
|
|
||||||
await driver.findContent('button', gu.exactMatch(name)).click();
|
|
||||||
// Wait for the #output div to be filled with a result. Custom Widget will set it to
|
|
||||||
// "waiting..." before invoking the method.
|
|
||||||
await driver.wait(async () => (await driver.find('#output').value()) !== 'waiting...');
|
|
||||||
// Read the result.
|
|
||||||
const text = await driver.find('#output').getText();
|
|
||||||
// Switch back to main window.
|
|
||||||
await driver.switchTo().defaultContent();
|
|
||||||
// If the method was a void method, the output will be "undefined".
|
|
||||||
if (text === 'undefined') {
|
|
||||||
return; // Simulate void method.
|
|
||||||
}
|
|
||||||
// Result will always be parsed json.
|
|
||||||
const parsed = JSON.parse(text);
|
|
||||||
// All exceptions will be serialized to { error : <<Error.message>> }
|
|
||||||
if (parsed?.error) {
|
|
||||||
// Rethrow the error.
|
|
||||||
throw new Error(parsed.error);
|
|
||||||
} else {
|
|
||||||
// Or return result.
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _read(selector: string) {
|
|
||||||
return this._inWidgetIframe(() => driver.find(selector).getText());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _inWidgetIframe<T>(callback: () => Promise<T>) {
|
|
||||||
const iframe = driver.find('iframe');
|
|
||||||
await driver.switchTo().frame(iframe);
|
|
||||||
const retVal = await callback();
|
|
||||||
await driver.switchTo().defaultContent();
|
|
||||||
return retVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Rpc for main widget (Custom Widget).
|
|
||||||
const widget = new Widget();
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Before each test, we will switch to Custom Url (to cleanup the widget)
|
// Before each test, we will switch to Custom Url (to cleanup the widget)
|
||||||
// and then back to the Tester widget.
|
// and then back to the Tester widget.
|
||||||
@ -337,6 +207,47 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
}
|
}
|
||||||
await toggleWidgetMenu();
|
await toggleWidgetMenu();
|
||||||
await clickOption(TESTER_WIDGET);
|
await clickOption(TESTER_WIDGET);
|
||||||
|
await widget.waitForFrame();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide widget when some columns are not mapped', async () => {
|
||||||
|
// Reset the widget to the one that has a column mapping requirements.
|
||||||
|
await widget.resetWidget();
|
||||||
|
|
||||||
|
// Since the widget was reset, we don't have .test-custom-widget-ready element.
|
||||||
|
assert.isFalse(await driver.find('.test-custom-widget-ready').isPresent());
|
||||||
|
|
||||||
|
// Now select the widget that requires a column.
|
||||||
|
await toggleWidgetMenu();
|
||||||
|
await clickOption(REQUIRED_WIDGET);
|
||||||
|
await gu.acceptAccessRequest();
|
||||||
|
|
||||||
|
// The widget iframe should be covered with a text explaining that the widget is not configured.
|
||||||
|
assert.isTrue(await driver.findWait('.test-custom-widget-not-mapped', 1000).isDisplayed());
|
||||||
|
|
||||||
|
// The content should at least have those words:
|
||||||
|
assert.include(await driver.find('.test-custom-widget-not-mapped').getText(),
|
||||||
|
"Some required columns aren't mapped");
|
||||||
|
|
||||||
|
// Make sure that the iframe is not displayed.
|
||||||
|
assert.isFalse(await driver.find('.test-custom-widget-ready').isPresent());
|
||||||
|
|
||||||
|
// Now map the column.
|
||||||
|
await toggleDrop(pickerDrop('Column'));
|
||||||
|
|
||||||
|
// Map it to A.
|
||||||
|
await clickOption('A');
|
||||||
|
|
||||||
|
// Make sure that the text is gone.
|
||||||
|
await gu.waitToPass(async () => {
|
||||||
|
assert.isFalse(await driver.find('.test-config-widget-not-mapped').isPresent());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make sure the widget is now visible.
|
||||||
|
assert.isTrue(await driver.find('.test-custom-widget-ready').isDisplayed());
|
||||||
|
|
||||||
|
// And we see widget with info about mapped columns, Column to A.
|
||||||
|
assert.deepEqual(await widget.onRecordsMappings(), {Column: 'A'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hide mappings when there is no good column', async () => {
|
it('should hide mappings when there is no good column', async () => {
|
||||||
@ -346,7 +257,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
}
|
}
|
||||||
await gu.setWidgetUrl(
|
await gu.setWidgetUrl(
|
||||||
createConfigUrl({
|
createConfigUrl({
|
||||||
columns: [{name: 'M2', type: 'Date'}],
|
columns: [{name: 'M2', type: 'Date', optional: true}],
|
||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -382,7 +293,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
|
|
||||||
// Now expand the drop again and make sure we can't clear it.
|
// Now expand the drop again and make sure we can't clear it.
|
||||||
await toggleDrop(pickerDrop('M2'));
|
await toggleDrop(pickerDrop('M2'));
|
||||||
assert.deepEqual(await getOptions(), ['NewCol']);
|
assert.deepEqual(await getOptions(), ['NewCol', 'Clear selection']);
|
||||||
|
|
||||||
// Now remove the column, and make sure that the drop is disabled again.
|
// Now remove the column, and make sure that the drop is disabled again.
|
||||||
await driver.sendKeys(Key.ESCAPE);
|
await driver.sendKeys(Key.ESCAPE);
|
||||||
@ -485,11 +396,8 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await widget.waitForFrame();
|
|
||||||
await gu.acceptAccessRequest();
|
await gu.acceptAccessRequest();
|
||||||
await widget.waitForPendingRequests();
|
await widget.waitForPlaceholder();
|
||||||
// Mappings should be empty
|
|
||||||
assert.isNull(await widget.onRecordsMappings());
|
|
||||||
// We should see 4 pickers
|
// We should see 4 pickers
|
||||||
assert.isTrue(await driver.find(pickerLabel('M1')).isPresent());
|
assert.isTrue(await driver.find(pickerLabel('M1')).isPresent());
|
||||||
assert.isTrue(await driver.find(pickerLabel('M2')).isPresent());
|
assert.isTrue(await driver.find(pickerLabel('M2')).isPresent());
|
||||||
@ -508,27 +416,20 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
// Should be able to select column A for all options
|
// Should be able to select column A for all options
|
||||||
await toggleDrop(pickerDrop('M1'));
|
await toggleDrop(pickerDrop('M1'));
|
||||||
await clickOption('A');
|
await clickOption('A');
|
||||||
await widget.waitForPendingRequests();
|
|
||||||
const empty = {M1: null, M2: null, M3: null, M4: null};
|
|
||||||
assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A'});
|
|
||||||
await toggleDrop(pickerDrop('M2'));
|
await toggleDrop(pickerDrop('M2'));
|
||||||
await clickOption('A');
|
await clickOption('A');
|
||||||
await widget.waitForPendingRequests();
|
|
||||||
assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A', M2: 'A'});
|
|
||||||
await toggleDrop(pickerDrop('M3'));
|
await toggleDrop(pickerDrop('M3'));
|
||||||
await clickOption('A');
|
await clickOption('A');
|
||||||
await widget.waitForPendingRequests();
|
|
||||||
assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A', M2: 'A', M3: 'A'});
|
|
||||||
await toggleDrop(pickerDrop('M4'));
|
await toggleDrop(pickerDrop('M4'));
|
||||||
await clickOption('A');
|
await clickOption('A');
|
||||||
|
await widget.waitForFrame();
|
||||||
await widget.waitForPendingRequests();
|
await widget.waitForPendingRequests();
|
||||||
assert.deepEqual(await widget.onRecordsMappings(), {M1: 'A', M2: 'A', M3: 'A', M4: 'A'});
|
assert.deepEqual(await widget.onRecordsMappings(), {M1: 'A', M2: 'A', M3: 'A', M4: 'A'});
|
||||||
// Single record should also receive update.
|
// Single record should also receive update.
|
||||||
assert.deepEqual(await widget.onRecordMappings(), {M1: 'A', M2: 'A', M3: 'A', M4: 'A'});
|
assert.deepEqual(await widget.onRecordMappings(), {M1: 'A', M2: 'A', M3: 'A', M4: 'A'});
|
||||||
// Undo should revert mappings - there should be only 3 operations to revert to first mapping.
|
// Undo should revert mappings - there should be only 3 operations to revert to first mapping.
|
||||||
await gu.undo(3);
|
await gu.undo(3);
|
||||||
await widget.waitForPendingRequests();
|
await widget.waitForPlaceholder();
|
||||||
assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A'});
|
|
||||||
// Add another columns, numeric B and any C.
|
// Add another columns, numeric B and any C.
|
||||||
await gu.selectSectionByTitle('Table');
|
await gu.selectSectionByTitle('Table');
|
||||||
await gu.addColumn('B');
|
await gu.addColumn('B');
|
||||||
@ -541,10 +442,6 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
assert.deepEqual(await getOptions(), ['A', 'B', 'C']);
|
assert.deepEqual(await getOptions(), ['A', 'B', 'C']);
|
||||||
await toggleDrop(pickerDrop('M4'));
|
await toggleDrop(pickerDrop('M4'));
|
||||||
assert.deepEqual(await getOptions(), ['A', 'C']);
|
assert.deepEqual(await getOptions(), ['A', 'C']);
|
||||||
await toggleDrop(pickerDrop('M1'));
|
|
||||||
await clickOption('B');
|
|
||||||
await widget.waitForPendingRequests();
|
|
||||||
assert.deepEqual(await widget.onRecordsMappings(), {...empty, M1: 'B'});
|
|
||||||
await revert();
|
await revert();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -602,8 +499,8 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
await gu.setWidgetUrl(
|
await gu.setWidgetUrl(
|
||||||
createConfigUrl({
|
createConfigUrl({
|
||||||
columns: [
|
columns: [
|
||||||
{name: 'M1', allowMultiple: true},
|
{name: 'M1', allowMultiple: true, optional: true},
|
||||||
{name: 'M2', type: 'Text', allowMultiple: true},
|
{name: 'M2', type: 'Text', allowMultiple: true, optional: true},
|
||||||
],
|
],
|
||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
@ -686,8 +583,8 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
await gu.setWidgetUrl(
|
await gu.setWidgetUrl(
|
||||||
createConfigUrl({
|
createConfigUrl({
|
||||||
columns: [
|
columns: [
|
||||||
{name: 'M1', type: 'Date,DateTime'},
|
{name: 'M1', type: 'Date,DateTime', optional: true},
|
||||||
{name: 'M2', type: 'Date, DateTime ', allowMultiple: true},
|
{name: 'M2', type: 'Date, DateTime ', allowMultiple: true, optional: true},
|
||||||
],
|
],
|
||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
@ -747,10 +644,10 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
await gu.setWidgetUrl(
|
await gu.setWidgetUrl(
|
||||||
createConfigUrl({
|
createConfigUrl({
|
||||||
columns: [
|
columns: [
|
||||||
{name: 'Any', type: 'Any', strictType: true},
|
{name: 'Any', type: 'Any', strictType: true, optional: true},
|
||||||
{name: 'Date_Numeric', type: 'Date, Numeric', strictType: true},
|
{name: 'Date_Numeric', type: 'Date, Numeric', strictType: true, optional: true},
|
||||||
{name: 'Date_Any', type: 'Date, Any', strictType: true},
|
{name: 'Date_Any', type: 'Date, Any', strictType: true, optional: true},
|
||||||
{name: 'Date', type: 'Date', strictType: true},
|
{name: 'Date', type: 'Date', strictType: true, optional: true},
|
||||||
],
|
],
|
||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
@ -791,7 +688,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
await gu.setWidgetUrl(
|
await gu.setWidgetUrl(
|
||||||
createConfigUrl({
|
createConfigUrl({
|
||||||
columns: [
|
columns: [
|
||||||
{name: 'Choice', type: 'Choice', strictType: true},
|
{name: 'Choice', type: 'Choice', strictType: true, optional: true},
|
||||||
],
|
],
|
||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
@ -839,7 +736,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
await clickOption(CUSTOM_URL);
|
await clickOption(CUSTOM_URL);
|
||||||
await gu.setWidgetUrl(
|
await gu.setWidgetUrl(
|
||||||
createConfigUrl({
|
createConfigUrl({
|
||||||
columns: [{name: 'M1'}, {name: 'M2', allowMultiple: true}],
|
columns: [{name: 'M1', optional: true}, {name: 'M2', allowMultiple: true, optional: true}],
|
||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -905,7 +802,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
// Add B column as a new one.
|
// Add B column as a new one.
|
||||||
await toggleDrop(pickerDrop('M1'));
|
await toggleDrop(pickerDrop('M1'));
|
||||||
// Make sure it is there to select.
|
// Make sure it is there to select.
|
||||||
assert.deepEqual(await getOptions(), ['A', 'C', 'B']);
|
assert.deepEqual(await getOptions(), ['A', 'C', 'B', 'Clear selection']);
|
||||||
await clickOption('B');
|
await clickOption('B');
|
||||||
await widget.waitForPendingRequests();
|
await widget.waitForPendingRequests();
|
||||||
await click(pickerAdd('M2'));
|
await click(pickerAdd('M2'));
|
||||||
@ -928,7 +825,10 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
await clickOption(CUSTOM_URL);
|
await clickOption(CUSTOM_URL);
|
||||||
await gu.setWidgetUrl(
|
await gu.setWidgetUrl(
|
||||||
createConfigUrl({
|
createConfigUrl({
|
||||||
columns: [{name: 'M1', type: 'Text'}, {name: 'M2', type: 'Text', allowMultiple: true}],
|
columns: [
|
||||||
|
{name: 'M1', type: 'Text', optional: true},
|
||||||
|
{name: 'M2', type: 'Text', allowMultiple: true, optional: true}
|
||||||
|
],
|
||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -1220,3 +1120,152 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
await refresh();
|
await refresh();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Poor man widget rpc. Class that invokes various parts in the tester widget.
|
||||||
|
const widget = {
|
||||||
|
async waitForPlaceholder() {
|
||||||
|
assert.isTrue(await driver.findWait('.test-custom-widget-not-mapped', 1000).isDisplayed());
|
||||||
|
},
|
||||||
|
// Wait for a frame.
|
||||||
|
async waitForFrame() {
|
||||||
|
await driver.findWait(`iframe.test-custom-widget-ready`, 1000);
|
||||||
|
await driver.wait(async () => await driver.find('iframe').isDisplayed(), 1000);
|
||||||
|
await widget.waitForPendingRequests();
|
||||||
|
},
|
||||||
|
async waitForPendingRequests() {
|
||||||
|
await this._inWidgetIframe(async () => {
|
||||||
|
await driver.executeScript('grist.testWaitForPendingRequests();');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async content() {
|
||||||
|
return await this._read('body');
|
||||||
|
},
|
||||||
|
async readonly() {
|
||||||
|
const text = await this._read('#readonly');
|
||||||
|
return text === 'true';
|
||||||
|
},
|
||||||
|
async access() {
|
||||||
|
const text = await this._read('#access');
|
||||||
|
return text as AccessLevel;
|
||||||
|
},
|
||||||
|
async onRecordMappings() {
|
||||||
|
const text = await this._read('#onRecordMappings');
|
||||||
|
return JSON.parse(text || 'null');
|
||||||
|
},
|
||||||
|
async onRecords() {
|
||||||
|
const text = await this._read('#onRecords');
|
||||||
|
return JSON.parse(text || 'null');
|
||||||
|
},
|
||||||
|
async onRecord() {
|
||||||
|
const text = await this._read('#onRecord');
|
||||||
|
return JSON.parse(text || 'null');
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Reads last mapping parameter received by the widget as part of onRecords call.
|
||||||
|
*/
|
||||||
|
async onRecordsMappings() {
|
||||||
|
const text = await this._read('#onRecordsMappings');
|
||||||
|
return JSON.parse(text || 'null');
|
||||||
|
},
|
||||||
|
async log() {
|
||||||
|
const text = await this._read('#log');
|
||||||
|
return text || '';
|
||||||
|
},
|
||||||
|
// Wait for frame to close.
|
||||||
|
async waitForClose() {
|
||||||
|
await driver.wait(async () => !(await driver.find('iframe').isPresent()), 3000);
|
||||||
|
},
|
||||||
|
// Wait for the onOptions event, and return its value.
|
||||||
|
async onOptions() {
|
||||||
|
const text = await this._inWidgetIframe(async () => {
|
||||||
|
// Wait for options to get filled, initially this div is empty,
|
||||||
|
// as first message it should get at least null as an options.
|
||||||
|
await driver.wait(async () => await driver.find('#onOptions').getText(), 3000);
|
||||||
|
return await driver.find('#onOptions').getText();
|
||||||
|
});
|
||||||
|
return JSON.parse(text);
|
||||||
|
},
|
||||||
|
async wasConfigureCalled() {
|
||||||
|
const text = await this._read('#configure');
|
||||||
|
return text === 'called';
|
||||||
|
},
|
||||||
|
async setOptions(options: any) {
|
||||||
|
return await this.invokeOnWidget('setOptions', [options]);
|
||||||
|
},
|
||||||
|
async setOption(key: string, value: any) {
|
||||||
|
return await this.invokeOnWidget('setOption', [key, value]);
|
||||||
|
},
|
||||||
|
async getOption(key: string) {
|
||||||
|
return await this.invokeOnWidget('getOption', [key]);
|
||||||
|
},
|
||||||
|
async clearOptions() {
|
||||||
|
return await this.invokeOnWidget('clearOptions');
|
||||||
|
},
|
||||||
|
async getOptions() {
|
||||||
|
return await this.invokeOnWidget('getOptions');
|
||||||
|
},
|
||||||
|
async mappings() {
|
||||||
|
return await this.invokeOnWidget('mappings');
|
||||||
|
},
|
||||||
|
async clearLog() {
|
||||||
|
return await this.invokeOnWidget('clearLog');
|
||||||
|
},
|
||||||
|
// Invoke method on a Custom Widget.
|
||||||
|
// Each method is available as a button with content that is equal to the method name.
|
||||||
|
// It accepts single argument, that we pass by serializing it to #input textbox. Widget invokes
|
||||||
|
// the method and serializes its return value to #output div. When there is an error, it is also
|
||||||
|
// serialized to the #output div.
|
||||||
|
async invokeOnWidget(name: string, input?: any[]) {
|
||||||
|
// Switch to frame.
|
||||||
|
const iframe = driver.find('iframe');
|
||||||
|
await driver.switchTo().frame(iframe);
|
||||||
|
// Clear input box that holds arguments.
|
||||||
|
await driver.find('#input').click();
|
||||||
|
await gu.clearInput();
|
||||||
|
// Serialize argument to the textbox (or leave empty).
|
||||||
|
if (input !== undefined) {
|
||||||
|
await driver.sendKeys(JSON.stringify(input));
|
||||||
|
}
|
||||||
|
// Find button that is responsible for invoking method.
|
||||||
|
await driver.findContent('button', gu.exactMatch(name)).click();
|
||||||
|
// Wait for the #output div to be filled with a result. Custom Widget will set it to
|
||||||
|
// "waiting..." before invoking the method.
|
||||||
|
await driver.wait(async () => (await driver.find('#output').value()) !== 'waiting...');
|
||||||
|
// Read the result.
|
||||||
|
const text = await driver.find('#output').getText();
|
||||||
|
// Switch back to main window.
|
||||||
|
await driver.switchTo().defaultContent();
|
||||||
|
// If the method was a void method, the output will be "undefined".
|
||||||
|
if (text === 'undefined') {
|
||||||
|
return; // Simulate void method.
|
||||||
|
}
|
||||||
|
// Result will always be parsed json.
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
// All exceptions will be serialized to { error : <<Error.message>> }
|
||||||
|
if (parsed?.error) {
|
||||||
|
// Rethrow the error.
|
||||||
|
throw new Error(parsed.error);
|
||||||
|
} else {
|
||||||
|
// Or return result.
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async _read(selector: string) {
|
||||||
|
return this._inWidgetIframe(() => driver.find(selector).getText());
|
||||||
|
},
|
||||||
|
async _inWidgetIframe<T>(callback: () => Promise<T>) {
|
||||||
|
const iframe = driver.find('iframe');
|
||||||
|
await driver.switchTo().frame(iframe);
|
||||||
|
const retVal = await callback();
|
||||||
|
await driver.switchTo().defaultContent();
|
||||||
|
return retVal;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Resets the widget by first selecting Custom URL option from the menu, which clearOptions
|
||||||
|
* any existing widget state (even if the Custom URL was already selected).
|
||||||
|
*/
|
||||||
|
async resetWidget() {
|
||||||
|
await toggleWidgetMenu();
|
||||||
|
await clickOption(CUSTOM_URL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -259,6 +259,8 @@ describe('GridViewNewColumnMenu', function () {
|
|||||||
await gu.waitForServer();
|
await gu.waitForServer();
|
||||||
//discard rename menu
|
//discard rename menu
|
||||||
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
||||||
|
// Wait for the sidepanel animation.
|
||||||
|
await gu.waitForSidePanel();
|
||||||
//check if right menu is opened on column section
|
//check if right menu is opened on column section
|
||||||
assert.isTrue(await driver.findWait('.test-right-tab-field', 1000).isDisplayed());
|
assert.isTrue(await driver.findWait('.test-right-tab-field', 1000).isDisplayed());
|
||||||
await gu.toggleSidePanel("right", "close");
|
await gu.toggleSidePanel("right", "close");
|
||||||
|
@ -3393,7 +3393,7 @@ export async function hasAccessPrompt() {
|
|||||||
* Accepts new access level.
|
* Accepts new access level.
|
||||||
*/
|
*/
|
||||||
export async function acceptAccessRequest() {
|
export async function acceptAccessRequest() {
|
||||||
await driver.find('.test-config-widget-access-accept').click();
|
await driver.findWait('.test-config-widget-access-accept', 1000).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user