(core) passing language as a query parameter to custom widgets

Summary: to allow custom widget having optional translations, lagunage seeted in user profile is passed as query parameter to custom widget

Test Plan: test added to check if query parameter is existing in url when settings is changed in profile

Reviewers: georgegevoian

Reviewed By: georgegevoian

Subscribers: jarek, paulfitz

Differential Revision: https://phab.getgrist.com/D4045
pull/691/head
Jakub Serafin 8 months ago
parent fbae81648c
commit 498ad07d38

@ -25,6 +25,7 @@ import {UserError} from 'app/client/models/errors';
import {SortedRowSet} from 'app/client/models/rowset';
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';
@ -213,9 +214,17 @@ export class CustomView extends Disposable {
showAfterReady?: boolean,
}) {
const {baseUrl, access, showAfterReady} = options;
const documentSettings = this.gristDoc.docData.docSettings();
return grains.create(WidgetFrame, {
url: baseUrl || this.getEmptyWidgetPage(),
access,
preferences:
{
culture: documentSettings.locale?? defaultLocale,
language: this.gristDoc.appModel.currentUser?.locale ?? defaultLocale,
timeZone: this.gristDoc.docInfo.timezone() ?? "UTC",
currency: documentSettings.currency?? "USD",
},
readonly: this.gristDoc.isReadonly.get(),
showAfterReady,
onSettingsInitialized: async () => {

@ -73,6 +73,10 @@ export interface WidgetFrameOptions {
* Optional handler to modify the iframe.
*/
onElem?: (iframe: HTMLIFrameElement) => void;
/**
* Optional language to use for the widget.
*/
preferences: {language?: string, timeZone?: any, currency?: string, culture?: string};
}
/**
@ -175,6 +179,9 @@ export class WidgetFrame extends DisposableWithEvents {
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 fullUrl = urlWithAccess(this._options.url);

@ -38,6 +38,11 @@ const widgetFull = fromAccess(AccessLevel.full);
// Holds widgets manifest content.
let widgets: ICustomWidget[] = [];
// Helper function to get iframe with custom widget.
function getCustomWidgetFrame() {
return driver.findWait('iframe', 500);
}
describe('CustomWidgets', function () {
this.timeout(20000);
const cleanup = setupTestSuite();
@ -50,6 +55,8 @@ describe('CustomWidgets', function () {
return server.testingHooks.setWidgetRepositoryUrl(url ? `${widgetServerUrl}${url}` : '');
}
before(async function () {
if (server.isExternalServer()) {
this.skip();
@ -100,8 +107,14 @@ describe('CustomWidgets', function () {
// Add custom section.
await gu.addNewSection(/Custom/, /Table1/, {selectBy: /TABLE1/});
// Override gristConfig to enable widget list.
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
});
after(async function() {
await server.testingHooks.setWidgetRepositoryUrl('');
});
after(async function() {
await server.testingHooks.setWidgetRepositoryUrl('');
});
after(async function() {
@ -109,7 +122,7 @@ describe('CustomWidgets', function () {
});
// Open or close widget menu.
const toggle = () => driver.find('.test-config-widget-select .test-select-open').click();
const toggle = async () => await driver.findWait('.test-config-widget-select .test-select-open', 1000).click();
// Get current value from widget menu.
const current = () => driver.find('.test-config-widget-select .test-select-open').getText();
// Get options from widget menu (must be first opened).
@ -121,19 +134,17 @@ describe('CustomWidgets', function () {
};
// Get rendered content from custom section.
const content = async () => {
const iframe = driver.find('iframe');
await driver.switchTo().frame(iframe);
return gu.doInIframe(await getCustomWidgetFrame(), async ()=>{
const text = await driver.find('body').getText();
await driver.switchTo().defaultContent();
return text;
});
};
async function execute(
op: (table: TableOperations) => Promise<any>,
tableSelector: (grist: any) => TableOperations = (grist) => grist.selectedTable
) {
const iframe = await driver.find('iframe');
await driver.switchTo().frame(iframe);
try {
return gu.doInIframe(await getCustomWidgetFrame(), async ()=> {
const harness = async (done: any) => {
const grist = (window as any).grist;
grist.ready();
@ -157,9 +168,7 @@ describe('CustomWidgets', function () {
const result = await driver.executeAsyncScript(cmd);
// done callback will return null instead of undefined
return result === "__undefined__" ? undefined : result;
} finally {
await driver.switchTo().defaultContent();
}
});
}
// Replace url for the Custom URL widget.
const setUrl = async (url: string) => {
@ -206,6 +215,17 @@ describe('CustomWidgets', function () {
// Rejects new access level.
const reject = () => driver.find(".test-config-widget-access-reject").click();
describe('RightWidgetMenu', () => {
beforeEach(async function () {
// Override gristConfig to enable widget list.
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
// We need to be sure that widget configuration panel is open all the time.
await gu.toggleSidePanel('right', 'open');
await recreatePanel();
await driver.findWait('.test-right-tab-pagewidget', 100).click();
});
it('should show widgets in dropdown', async () => {
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-pagewidget').click();
@ -360,6 +380,8 @@ describe('CustomWidgets', function () {
});
it('should switch access level to none on new widget', async () => {
widgets = [widget1, widget2];
await recreatePanel();
await toggle();
await select(widget1.name);
assert.equal(await access(), AccessLevel.none);
@ -533,6 +555,62 @@ describe('CustomWidgets', function () {
assert.equal(await content(), AccessLevel.none);
});
it('should offer only custom url when disabled', async () => {
await toggle();
await select(CUSTOM_URL);
await driver.executeScript('window.gristConfig.enableWidgetRepository = false;');
await recreatePanel();
assert.isTrue(await driver.find('.test-config-widget-url').isDisplayed());
assert.isFalse(await driver.find('.test-config-widget-select').isPresent());
});
});
describe('gristApiSupport', async ()=>{
beforeEach(async function () {
// Override gristConfig to enable widget list.
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
// We need to be sure that widget configuration panel is open all the time.
await gu.toggleSidePanel('right', 'open');
await recreatePanel();
await driver.findWait('.test-right-tab-pagewidget', 100).click();
});
it('should set language in widget url', async () => {
function languageMenu() {
return gu.currentDriver().find('.test-account-page-language .test-select-open');
}
async function language() {
return await gu.doInIframe(await getCustomWidgetFrame(), async ()=>{
const urlText = await driver.executeScript<string>('return document.location.href');
const url = new URL(urlText);
return url.searchParams.get('language');
});
}
async function switchLanguage(lang: string) {
await gu.openProfileSettingsPage();
await gu.waitForServer();
await languageMenu().click();
await driver.findContentWait('.test-select-menu li', lang, 100).click();
await gu.waitForServer();
await driver.navigate().back();
await gu.waitForServer();
}
widgets = [widget1];
await useManifest(manifestEndpoint);
await gu.openWidgetPanel();
await toggle();
await select(widget1.name);
//Switch language to Polish
await switchLanguage('Polski');
//Check if widgets have "pl" in url
assert.equal(await language(), 'pl');
//Switch back to English
await switchLanguage('English');
//Check if widgets have "en" in url
assert.equal(await language(), 'en');
});
it("should support grist.selectedTable", async () => {
// Open a custom widget with full access.
await gu.toggleSidePanel('right', 'open');
@ -633,26 +711,14 @@ describe('CustomWidgets', function () {
});
it("should support grist.getAccessTokens", async () => {
const iframe = await driver.find('iframe');
await driver.switchTo().frame(iframe);
try {
return await gu.doInIframe(await getCustomWidgetFrame(), async ()=>{
const tokenResult: AccessTokenResult = await driver.executeAsyncScript(
(done: any) => (window as any).grist.getAccessToken().then(done)
);
assert.sameMembers(Object.keys(tokenResult), ['ttlMsecs', 'token', 'baseUrl']);
const result = await fetch(tokenResult.baseUrl + `/tables/Table1/records?auth=${tokenResult.token}`);
assert.sameMembers(Object.keys(await result.json()), ['records']);
} finally {
await driver.switchTo().defaultContent();
}
});
it('should offer only custom url when disabled', async () => {
await toggle();
await select(CUSTOM_URL);
await driver.executeScript('window.gristConfig.enableWidgetRepository = false;');
await recreatePanel();
assert.isTrue(await driver.find('.test-config-widget-url').isDisplayed());
assert.isFalse(await driver.find('.test-config-widget-select').isPresent());
});
});
});

@ -859,6 +859,19 @@ export async function importUrlDialog(url: string): Promise<void> {
await driver.switchTo().defaultContent();
}
/**
* Executed passed function in the context of given iframe, and then switching back to original context
*
*/
export async function doInIframe<T>(iframe: WebElement, func: () => Promise<T>) {
try {
await driver.switchTo().frame(iframe);
return await func();
} finally {
await driver.switchTo().defaultContent();
}
}
/**
* Starts or resets the collections of UserActions. This should be followed some time later by
* a call to userActionsVerify() to check which UserActions were sent to the server. If the

Loading…
Cancel
Save