This commit is contained in:
Grégoire Cutzach 2024-10-23 14:37:06 +02:00 committed by GitHub
commit b57c2b35bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 496 additions and 61 deletions

View File

@ -29,6 +29,7 @@ import {Holder, Observable, subscribe} from 'grainjs';
import {Computed, Disposable, dom, DomArg, DomElementArg} from 'grainjs';
import {makeT} from 'app/client/lib/localization';
import {logTelemetryEvent} from 'app/client/lib/telemetry';
import {DocumentType} from 'app/common/UserAPI';
// tslint:disable:no-console
@ -87,7 +88,7 @@ export interface DocPageModel {
isTutorialTrunk: Observable<boolean>;
isTutorialFork: Observable<boolean>;
isTemplate: Observable<boolean>;
type: Observable<DocumentType|null>;
importSources: ImportSource[];
undoState: Observable<IUndoState|null>; // See UndoStack for details.
@ -147,6 +148,8 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
(use, doc) => doc ? doc.isTutorialFork : false);
public readonly isTemplate = Computed.create(this, this.currentDoc,
(use, doc) => doc ? doc.isTemplate : false);
public readonly type = Computed.create(this, this.currentDoc,
(use, doc) => doc?.type ?? null);
public readonly importSources: ImportSource[] = [];
@ -499,7 +502,8 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo {
const isFork = Boolean(idParts.forkId || idParts.snapshotId);
const isBareFork = isFork && idParts.trunkId === NEW_DOCUMENT_CODE;
const isSnapshot = Boolean(idParts.snapshotId);
const isTutorial = doc.type === 'tutorial';
const type = doc.type;
const isTutorial = type === 'tutorial';
const isTutorialTrunk = isTutorial && !isFork && mode !== 'default';
const isTutorialFork = isTutorial && isFork;
@ -511,7 +515,7 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo {
// mode. Since the document's 'openMode' has no effect, don't bother trying
// to set it here, as it'll potentially be confusing for other code reading it.
openMode = 'default';
} else if (!isFork && doc.type === 'template') {
} else if (!isFork && type === 'template') {
// Templates should always open in fork mode by default.
openMode = 'fork';
} else {
@ -521,7 +525,7 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo {
}
const isPreFork = openMode === 'fork';
const isTemplate = doc.type === 'template' && (isFork || isPreFork);
const isTemplate = type === 'template' && (isFork || isPreFork);
const isEditable = !isSnapshot && (canEdit(doc.access) || isPreFork);
return {
...doc,
@ -534,6 +538,7 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo {
isSnapshot,
isTutorialTrunk,
isTutorialFork,
type,
isTemplate,
isReadonly: !isEditable,
idParts,

View File

@ -106,7 +106,7 @@ const cssItem = styled('div', `
const cssItemShort = styled('div', `
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
align-items: center;
padding: 8px;
margin: 0 -8px;
@ -131,7 +131,7 @@ const cssItemShort = styled('div', `
`);
const cssItemName = styled('div', `
width: 150px;
width: 230px;
font-weight: bold;
display: flex;
align-items: center;
@ -159,6 +159,7 @@ const cssItemName = styled('div', `
`);
const cssItemDescription = styled('div', `
max-width: 250px;
margin-right: auto;
margin-bottom: -1px; /* aligns with the value */
`);

View File

@ -29,8 +29,19 @@ import {commonUrls, GristLoadConfig} from 'app/common/gristUrls';
import {not, propertyCompare} from 'app/common/gutil';
import {getCurrency, locales} from 'app/common/Locales';
import {isOwner, isOwnerOrEditor} from 'app/common/roles';
import {Computed, Disposable, dom, fromKo, IDisposableOwner, makeTestId, Observable, styled} from 'grainjs';
import {
Computed,
Disposable,
dom,
DomElementMethod,
fromKo,
IDisposableOwner,
makeTestId,
Observable,
styled
} from 'grainjs';
import * as moment from 'moment-timezone';
import {DocumentType} from 'app/common/UserAPI';
const t = makeT('DocumentSettings');
const testId = makeTestId('test-settings-');
@ -63,7 +74,7 @@ export class DocSettingsPage extends Disposable {
const isDocEditor = isOwnerOrEditor(docPageModel.currentDoc.get());
return cssContainer(
dom.create(AdminSection, t('Document Settings'), [
dom.create(cssAdminSection, t('Document Settings'), [
dom.create(AdminSectionItem, {
id: 'timezone',
name: t('Time Zone'),
@ -85,9 +96,25 @@ export class DocSettingsPage extends Disposable {
{defaultCurrencyLabel: t("Local currency ({{currency}})", {currency: getCurrency(l)})})
)
}),
dom.create(AdminSectionItem, {
id: 'templateMode',
name: t('Template mode'),
description: t('Special document mode'),
value: cssDocTypeContainer(
dom.create(
displayCurrentType,
docPageModel.type,
),
cssSmallButton(t('Edit'),
dom.on('click', this._doSetDocumentType.bind(this, true)),
testId('doctype-edit')
)
),
disabled: isDocOwner ? false : t('Only available to document owners'),
}),
]),
dom.create(AdminSection, t('Data Engine'), [
dom.create(cssAdminSection, t('Data Engine'), [
dom.create(AdminSectionItem, {
id: 'timings',
name: t('Formula timer'),
@ -120,7 +147,6 @@ export class DocSettingsPage extends Disposable {
)),
disabled: isDocOwner ? false : t('Only available to document owners'),
}),
dom.create(AdminSectionItem, {
id: 'reload',
name: t('Reload'),
@ -128,7 +154,6 @@ export class DocSettingsPage extends Disposable {
value: cssSmallButton(t('Reload data engine'), dom.on('click', this._reloadEngine.bind(this, true))),
disabled: isDocEditor ? false : t('Only available to document editors'),
}),
canChangeEngine ? dom.create(AdminSectionItem, {
id: 'python',
name: t('Python'),
@ -137,7 +162,7 @@ export class DocSettingsPage extends Disposable {
}) : null,
]),
dom.create(AdminSection, t('API'), [
dom.create(cssAdminSection, t('API'), [
dom.create(AdminSectionItem, {
id: 'documentId',
name: t('Document ID'),
@ -186,7 +211,6 @@ export class DocSettingsPage extends Disposable {
href: getApiConsoleLink(docPageModel),
}),
}),
dom.create(AdminSectionItem, {
id: 'webhooks',
name: t('Webhooks'),
@ -224,11 +248,11 @@ export class DocSettingsPage extends Disposable {
const docPageModel = this._gristDoc.docPageModel;
modal((ctl, owner) => {
this.onDispose(() => ctl.close());
const selected = Observable.create<Option>(owner, Option.Adhoc);
const selected = Observable.create<TimingModalOption>(owner, TimingModalOption.Adhoc);
const page = Observable.create<TimingModalPage>(owner, TimingModalPage.Start);
const startTiming = async () => {
if (selected.get() === Option.Reload) {
if (selected.get() === TimingModalOption.Reload) {
page.set(TimingModalPage.Spinner);
await this._gristDoc.docApi.startTiming();
await docPageModel.appModel.api.getDocAPI(docPageModel.currentDocId.get()!).forceReload();
@ -243,7 +267,7 @@ export class DocSettingsPage extends Disposable {
const startPage = () => [
cssRadioCheckboxOptions(
dom.style('max-width', '400px'),
radioCheckboxOption(selected, Option.Adhoc, dom('div',
radioCheckboxOption(selected, TimingModalOption.Adhoc, dom('div',
dom('div',
dom('strong', t('Start timing')),
),
@ -253,7 +277,7 @@ export class DocSettingsPage extends Disposable {
),
testId('timing-modal-option-adhoc'),
)),
radioCheckboxOption(selected, Option.Reload, dom('div',
radioCheckboxOption(selected, TimingModalOption.Reload, dom('div',
dom('div',
dom('strong', t('Time reload')),
),
@ -289,6 +313,111 @@ export class DocSettingsPage extends Disposable {
});
}
private async _doSetDocumentType() {
const docPageModel = this._gristDoc.docPageModel;
modal((ctl, owner) => {
this.onDispose(() => ctl.close());
const currentDocType = docPageModel.type.get() as string;
let currentDocTypeOption;
switch (currentDocType) {
case "template":
currentDocTypeOption = DocTypeOption.Template;
break;
case "tutorial":
currentDocTypeOption = DocTypeOption.Tutorial;
break;
default:
currentDocTypeOption = DocTypeOption.Regular;
}
const selected = Observable.create<DocTypeOption>(owner, currentDocTypeOption);
const doSetDocumentType = async () => {
const docId = docPageModel.currentDocId.get();
let docType;
if (selected.get() === DocTypeOption.Regular) {
docType = "";
} else if (selected.get() === DocTypeOption.Template) {
docType = "template";
} else {
docType = "tutorial";
}
await persistType(docType, docId);
const {trunkId} = docPageModel.currentDoc.get()!.idParts;
window.location.replace(urlState().makeUrl({
docPage: "settings",
fork: undefined, // will be automatically set once the page is reloaded
doc: trunkId,
}));
};
const docTypeOption = (
{
type,
label,
description,
itemTestId
}: {
type: DocTypeOption,
label: string | any,
description: string,
itemTestId: DomElementMethod | null
}) => {
return radioCheckboxOption(selected, type, dom('div',
dom('div',
dom('strong', label),
),
dom('div',
dom.style('margin-top', '8px'),
dom('span', description)
),
itemTestId,
));
};
const documentTypeOptions = () => [
cssRadioCheckboxOptions(
dom.style('max-width', '400px'),
docTypeOption({
type: DocTypeOption.Regular,
label: t('Regular document'),
description: t('Regular document behavior, all users work on the same copy of the document.'),
itemTestId: testId('doctype-modal-option-regular'),
}),
docTypeOption({
type: DocTypeOption.Template,
label: t('Template'),
description: t('Document automatically opens in {{fiddleModeDocUrl}}. ' +
'Any edit (open to anybody) will create a new unsaved copy.',
{
fiddleModeDocUrl: cssLink({href: commonUrls.helpAPI, target: '_blank'}, t('fiddle mode'))
}
),
itemTestId: testId('doctype-modal-option-template'),
}),
docTypeOption({
type: DocTypeOption.Tutorial,
label: t('Tutorial'),
description: t('Document automatically opens with a new copy.'),
itemTestId: testId('doctype-modal-option-tutorial'),
}),
),
cssModalButtons(
bigBasicButton(t('Cancel'), dom.on('click', () => ctl.close()), testId('doctype-modal-cancel')),
bigPrimaryButton(t(`Confirm change`),
dom.on('click', doSetDocumentType),
testId('doctype-modal-confirm'),
),
)
];
return [
cssModalTitle(t(`Change nature of document`)),
documentTypeOptions(),
testId('doctype-modal'),
];
});
}
private async _doSetEngine(val: EngineCode|undefined) {
const docPageModel = this._gristDoc.docPageModel;
if (this._engine.get() !== val) {
@ -298,7 +427,15 @@ export class DocSettingsPage extends Disposable {
}
}
function persistType(type: string|null, docId: string|undefined){
docId = docId?.split("~")[0];
return fetch(`/o/docs/api/docs/${docId}`, {
method: 'PATCH',
headers: {"Content-Type": "application/json"},
credentials: 'include',
body: JSON.stringify({type})
});
}
function getApiConsoleLink(docPageModel: DocPageModel) {
const url = new URL(location.href);
@ -343,6 +480,39 @@ function buildLocaleSelect(
);
}
type DocumentTypeItem = ACSelectItem & {type?: string};
const typeList: DocumentTypeItem[] = [{
label: t('Regular'),
type: ''
}, {
label: t('Template'),
type: 'template'
}, {
label: t('Tutorial'),
type: 'tutorial'
}].map((el) => ({
...el,
value: el.label,
cleanText: el.label.trim().toLowerCase()
}));
function displayCurrentType(
owner: IDisposableOwner,
type: Observable<DocumentType|null>,
) {
const typeObs = Computed.create(owner, use => {
const typeCode = use(type) ?? "";
const typeName = typeList.find(ty => ty.type === typeCode)?.label || typeCode;
return typeName;
});
return dom(
'div',
typeObs.get(),
testId('doctype-value')
);
}
const cssContainer = styled('div', `
overflow-y: auto;
position: relative;
@ -462,7 +632,7 @@ enum TimingModalPage {
/**
* Enum for the different options in the timing modal.
*/
enum Option {
enum TimingModalOption {
/**
* Start timing and immediately forces a reload of the document and waits for the
* document to be loaded, to show the results.
@ -474,6 +644,15 @@ enum Option {
Adhoc,
}
/**
* Enum for the different options in the document type Modal.
*/
enum DocTypeOption {
Regular,
Template,
Tutorial,
}
// A version that is not underlined, and on hover mouse pointer indicates that copy is available
const cssCopyLink = styled(cssLink, `
word-wrap: break-word;
@ -509,3 +688,17 @@ const cssWrap = styled('p', `
const cssRedText = styled('span', `
color: ${theme.errorText};
`);
const cssAdminSection = styled(AdminSection, `
max-width: 750px;
`);
const cssDocTypeContainer = styled('div', `
display: flex;
width: 172px;
align-items: center;
justify-content: space-between;
& > * {
display: inline-block;
}
`);

View File

@ -25,6 +25,8 @@ export const cssLabel = styled('label', `
margin-bottom: 0px;
flex-shrink: 0;
align-items: center;
outline: none;
user-select: none;
@ -106,7 +108,7 @@ export const cssCheckboxCircle = styled(cssCheckboxSquare, `
`);
export const cssLabelText = styled('span', `
margin-left: 8px;
margin-left: 16px;
color: ${theme.text};
font-weight: initial; /* negate bootstrap */
overflow: hidden;
@ -213,10 +215,11 @@ export function toggle(value: Observable<boolean|null>, ...domArgs: DomElementAr
// checkbox doesn't support two-way binding.
const cssBlockCheckbox = styled('div', `
display: flex;
padding: 10px 8px;
padding: 16px;
border: 1px solid ${theme.controlSecondaryDisabledFg};
border-radius: 3px;
cursor: pointer;
& input::before, & input::after {
top: unset;
left: unset;
@ -226,10 +229,21 @@ const cssBlockCheckbox = styled('div', `
}
&-block {
pointer-events: none;
border-color: ${theme.controlFg};
border-width: 2px;
}
&-block a {
pointer-events: all;
}
& input:checked::before, & input:disabled::before, & input:indeterminate::before {
background-color: ${theme.checkboxBg};
}
& input:checked::after, & input:indeterminate::after {
-webkit-mask-image: var(--icon-RadioButtonInnerCircle);
background-color: ${theme.checkboxSelectedFg};
}
`);
const cssInlineRelative = styled('div', `

View File

@ -386,6 +386,7 @@ export const theme = {
/* Checkboxes */
checkboxBg: new CustomProp('theme-checkbox-bg', undefined, colors.light),
checkboxSelectedFg: new CustomProp('theme-checkbox-selected-bg', undefined, colors.lightGreen),
checkboxDisabledBg: new CustomProp('theme-checkbox-disabled-bg', undefined, colors.darkGrey),
checkboxBorder: new CustomProp('theme-checkbox-border', undefined, colors.darkGrey),
checkboxBorderHover: new CustomProp('theme-checkbox-border-hover', undefined, colors.hover),

View File

@ -623,10 +623,11 @@ export const cssModalBody = styled('div', `
export const cssModalButtons = styled('div', `
margin: 40px 0 0 0;
text-align: right;
& > button,
& > .${cssButton.className} {
margin: 0 8px 0 0;
margin: 0 0 0 8px;
}
`);

View File

@ -122,6 +122,7 @@
--icon-PublicColor: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTQiIHdpZHRoPSIxNiI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMCAwSDE2VjE2SDB6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIC0xKSIvPjxnIG9wYWNpdHk9Ii43ODMiPjxwYXRoIGQ9Ik0gMTUuMjc1LDEwLjI5MyAxMy4wODcsOS42NjggQyAxMi43MjY0NzgsOS41NjQ5MTc4IDEyLjQ1MzkyLDkuMjY4ODEwNSAxMi4zODEsOC45MDEgTCAxMi4yNDcsOC4yMjYgQyAxMy4zMTQxMiw3LjczODMwNjEgMTMuOTk5MDAyLDYuNjczMjgxNyAxNCw1LjUgViA0LjEyNiBDIDE0LjAyNTE5OCwyLjQ3MzAwMzMgMTIuNzQyNTA2LDEuMDk0MTUzNCAxMS4wOTIsMSA5Ljg3OTM3MDksMC45NjM0MTA4MiA4Ljc2NDA2NTUsMS42NjA3NzI3IDguMjY2LDIuNzY3IDguNzQzMDQ3OCwzLjQ2MTI3ODUgOC45OTg5MTk4LDQuMjgzNjI0NyA5LDUuMTI2IFYgNi41IEMgOC45OTgyMDc3LDYuODYzNzcwNiA4Ljk0NjA0NTYsNy4yMjU1NDA0IDguODQ1LDcuNTc1IDkuMTA0MjMyNSw3Ljg0Njk0MjMgOS40MTIyMzk1LDguMDY3NzcxMSA5Ljc1Myw4LjIyNiBMIDkuNjE5LDguOSBDIDkuNTQ2MDc5Niw5LjI2NzgxMDUgOS4yNzM1MjE5LDkuNTYzOTE3OCA4LjkxMyw5LjY2NyBMIDguMDcsOS45MDggOS41NSwxMC4zMzEgYyAwLjg1Njk2NCwwLjI0NzU1NCAxLjQ0NzcwNiwxLjAzMDk5OSAxLjQ1LDEuOTIzIFYgMTQuNSBjIC0wLjAwMTcsMC4xNzA3MiAtMC4wMzI3OCwwLjMzOTg3MSAtMC4wOTIsMC41IEggMTUuNSBjIDAuMjc2MTQyLDAgMC41LC0wLjIyMzg1OCAwLjUsLTAuNSB2IC0zLjI0NiBjIC0xLjk4ZS00LC0wLjQ0NjIwOCAtMC4yOTU5OTYsLTAuODM4MjkzIC0wLjcyNSwtMC45NjEgeiIgZmlsbD0iI2U2YTExNyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtMSkiLz48cGF0aCBkPSJNIDkuMjc1LDExLjI5MyA3LjA4NywxMC42NjggQyA2LjcyNjIwMjMsMTAuNTY0NzU0IDYuNDUzNTgzLDEwLjI2ODE5NCA2LjM4MSw5LjkgTCA2LjI0Nyw5LjIyNSBDIDcuMzEzNzkxNCw4LjczNzQ1NjkgNy45OTg2MTEyLDcuNjcyOTE5NSA4LDYuNSBWIDUuMTI2IEMgOC4wMjUxOTc2LDMuNDczMDAzMyA2Ljc0MjUwNTUsMi4wOTQxNTM0IDUuMDkyLDIgNC4yODA1MzI3LDEuOTc1MTAzMiAzLjQ5MzYyMDIsMi4yODAxNDgzIDIuOTEwOTUzMiwyLjg0NTQ3ODUgMi4zMjgyODYzLDMuNDEwODA4NiAxLjk5OTYxODIsNC4xODgxNTA5IDIsNSB2IDEuNSBjIDkuOTc5ZS00LDEuMTczMjgxNyAwLjY4NTg3OTcsMi4yMzgzMDYyIDEuNzUzLDIuNzI2IEwgMy42MTksOS45IEMgMy41NDYwOCwxMC4yNjc4MTEgMy4yNzM1MjE5LDEwLjU2MzkxOCAyLjkxMywxMC42NjcgTCAwLjcyNSwxMS4yOTIgQyAwLjI5NTYzOTI2LDExLjQxNDgwOSAtMi40ODE3NzE2ZS00LDExLjgwNzQyMSAwLDEyLjI1NCBWIDE0LjUgQyAwLDE0Ljc3NjE0MiAwLjIyMzg1NzYzLDE1IDAuNSwxNSBoIDkgQyA5Ljc3NjE0MjQsMTUgMTAsMTQuNzc2MTQyIDEwLDE0LjUgViAxMi4yNTQgQyA5Ljk5OTgwMTgsMTEuODA3NzkyIDkuNzA0MDA0MywxMS40MTU3MDcgOS4yNzUsMTEuMjkzIFoiIGZpbGw9IiNmZmYiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTEpIi8+PC9nPjwvZz48L3N2Zz4=');
--icon-PublicFilled: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMCAwSDE2VjE2SDB6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIC0xKSIvPjxnIGZpbGw9IiMxNkIzNzgiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTE1LjI3NSwxMC4yOTMgTDEzLjA4Nyw5LjY2OCBDMTIuNzI2NDc4MSw5LjU2NDkxNzggMTIuNDUzOTIwNCw5LjI2ODgxMDUgMTIuMzgxLDguOTAxIEwxMi4yNDcsOC4yMjYgQzEzLjMxNDEyMDMsNy43MzgzMDYxNSAxMy45OTkwMDIxLDYuNjczMjgxNyAxNCw1LjUgTDE0LDQuMTI2IEMxNC4wMjUxOTc2LDIuNDczMDAzMjUgMTIuNzQyNTA1NSwxLjA5NDE1MzM3IDExLjA5MiwxIEM5Ljg3OTM3MDksMC45NjM0MTA4MjIgOC43NjQwNjU1LDEuNjYwNzcyNjkgOC4yNjYsMi43NjcgQzguNzQzMDQ3OCwzLjQ2MTI3ODUgOC45OTg5MTk4NCw0LjI4MzYyNDc0IDksNS4xMjYgTDksNi41IEM4Ljk5ODIwNzc0LDYuODYzNzcwNTcgOC45NDYwNDU1OCw3LjIyNTU0MDM4IDguODQ1LDcuNTc1IEM5LjEwNDIzMjUzLDcuODQ2OTQyMjYgOS40MTIyMzk1Myw4LjA2Nzc3MTA2IDkuNzUzLDguMjI2IEw5LjYxOSw4LjkgQzkuNTQ2MDc5NTcsOS4yNjc4MTA1IDkuMjczNTIxODcsOS41NjM5MTc4IDguOTEzLDkuNjY3IEw4LjA3LDkuOTA4IEw5LjU1LDEwLjMzMSBDMTAuNDA2OTY0MywxMC41Nzg1NTM2IDEwLjk5NzcwNTksMTEuMzYxOTk5MyAxMSwxMi4yNTQgTDExLDE0LjUgQzEwLjk5ODM0MjgsMTQuNjcwNzE5OCAxMC45NjcyMTg5LDE0LjgzOTg3MTUgMTAuOTA4LDE1IEwxNS41LDE1IEMxNS43NzYxNDI0LDE1IDE2LDE0Ljc3NjE0MjQgMTYsMTQuNSBMMTYsMTEuMjU0IEMxNS45OTk4MDE4LDEwLjgwNzc5MTggMTUuNzA0MDA0MywxMC40MTU3MDcyIDE1LjI3NSwxMC4yOTMgWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtMSkiLz48cGF0aCBkPSJNOS4yNzUsMTEuMjkzIEw3LjA4NywxMC42NjggQzYuNzI2MjAyMzIsMTAuNTY0NzUzOSA2LjQ1MzU4MywxMC4yNjgxOTM1IDYuMzgxLDkuOSBMNi4yNDcsOS4yMjUgQzcuMzEzNzkxNDEsOC43Mzc0NTY5MyA3Ljk5ODYxMTI1LDcuNjcyOTE5NTQgOCw2LjUgTDgsNS4xMjYgQzguMDI1MTk3NTYsMy40NzMwMDMyNSA2Ljc0MjUwNTQ3LDIuMDk0MTUzMzcgNS4wOTIsMiBDNC4yODA1MzI3LDEuOTc1MTAzMiAzLjQ5MzYyMDIsMi4yODAxNDgyNyAyLjkxMDk1MzIzLDIuODQ1NDc4NDYgQzIuMzI4Mjg2MjUsMy40MTA4MDg2NCAxLjk5OTYxODE2LDQuMTg4MTUwOTUgMiw1IEwyLDYuNSBDMi4wMDA5OTc5MSw3LjY3MzI4MTcgMi42ODU4Nzk3NCw4LjczODMwNjE1IDMuNzUzLDkuMjI2IEwzLjYxOSw5LjkgQzMuNTQ2MDc5NTcsMTAuMjY3ODEwNSAzLjI3MzUyMTg3LDEwLjU2MzkxNzggMi45MTMsMTAuNjY3IEwwLjcyNSwxMS4yOTIgQzAuMjk1NjM5MjYyLDExLjQxNDgwOTEgLTAuMDAwMjQ4MTc3MTU3LDExLjgwNzQyMTIgMCwxMi4yNTQgTDAsMTQuNSBDMy4zODE3NjU4MWUtMTcsMTQuNzc2MTQyNCAwLjIyMzg1NzYyNSwxNSAwLjUsMTUgTDkuNSwxNSBDOS43NzYxNDIzNywxNSAxMCwxNC43NzYxNDI0IDEwLDE0LjUgTDEwLDEyLjI1NCBDOS45OTk4MDE3NywxMS44MDc3OTE4IDkuNzA0MDA0MzEsMTEuNDE1NzA3MiA5LjI3NSwxMS4yOTMgWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtMSkiLz48L2c+PC9nPjwvc3ZnPg==');
--icon-Question: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iOCIgY3k9IjgiIHI9IjcuNSIgc3Ryb2tlPSIjMTZCMzc4Ii8+PHBhdGggZD0iTTcuMTI0NiAxMC4zNDA5VjEwLjI4NTVDNy4xMzA3NiA5LjY5NzY4IDcuMTkyMzEgOS4yMjk4OCA3LjMwOTI2IDguODgyMUM3LjQyNjIxIDguNTM0MzMgNy41OTI0IDguMjUyNzIgNy44MDc4NCA4LjAzNzI5QzguMDIzMjcgNy44MjE4NSA4LjI4MTggNy42MjMzNCA4LjU4MzQxIDcuNDQxNzZDOC43NjQ5OSA3LjMzMDk3IDguOTI4MSA3LjIwMDE3IDkuMDcyNzUgNy4wNDkzNkM5LjIxNzQgNi44OTU0OCA5LjMzMTI4IDYuNzE4NTEgOS40MTQzNyA2LjUxODQ3QzkuNTAwNTUgNi4zMTg0MiA5LjU0MzYzIDYuMDk2ODMgOS41NDM2MyA1Ljg1MzY5QzkuNTQzNjMgNS41NTIwOCA5LjQ3Mjg1IDUuMjkwNDggOS4zMzEyOCA1LjA2ODg5QzkuMTg5NyA0Ljg0NzMgOS4wMDA0MyA0LjY3NjQ5IDguNzYzNDUgNC41NTY0NkM4LjUyNjQ3IDQuNDM2NDMgOC4yNjMzMyA0LjM3NjQyIDcuOTc0MDMgNC4zNzY0MkM3LjcyMTY2IDQuMzc2NDIgNy40Nzg1MyA0LjQyODc0IDcuMjQ0NjMgNC41MzMzOEM3LjAxMDczIDQuNjM4MDIgNi44MTUzIDQuODAyNjggNi42NTgzNCA1LjAyNzM0QzYuNTAxMzggNS4yNTIwMSA2LjQxMDU5IDUuNTQ1OTMgNi4zODU5NiA1LjkwOTA5SDUuMjIyNjFDNS4yNDcyMyA1LjM4NTg5IDUuMzgyNjUgNC45MzgwOSA1LjYyODg2IDQuNTY1N0M1Ljg3ODE1IDQuMTkzMyA2LjIwNTkyIDMuOTA4NjIgNi42MTIxNyAzLjcxMTY1QzcuMDIxNSAzLjUxNDY4IDcuNDc1NDUgMy40MTYxOSA3Ljk3NDAzIDMuNDE2MTlDOC41MTU3IDMuNDE2MTkgOC45ODY1OCAzLjUyMzkxIDkuMzg2NjcgMy43MzkzNUM5Ljc4OTg1IDMuOTU0NzggMTAuMTAwNyA0LjI1MDI0IDEwLjMxOTIgNC42MjU3MUMxMC41NDA4IDUuMDAxMTggMTAuNjUxNiA1LjQyODk4IDEwLjY1MTYgNS45MDkwOUMxMC42NTE2IDYuMjQ3NjMgMTAuNTk5MyA2LjU1Mzg2IDEwLjQ5NDYgNi44Mjc3N0MxMC4zOTMxIDcuMTAxNjggMTAuMjQ1MyA3LjM0NjM1IDEwLjA1MTQgNy41NjE3OUM5Ljg2MDYzIDcuNzc3MjMgOS42Mjk4MSA3Ljk2ODA0IDkuMzU4OTggOC4xMzQyM0M5LjA4ODE0IDguMzAzNSA4Ljg3MTE3IDguNDgyMDEgOC43MDgwNSA4LjY2OTc0QzguNTQ0OTQgOC44NTQ0IDguNDI2NDUgOS4wNzQ0NiA4LjM1MjU4IDkuMzI5OUM4LjI3ODcyIDkuNTg1MzUgOC4yMzg3MSA5LjkwMzg4IDguMjMyNTUgMTAuMjg1NVYxMC4zNDA5SDcuMTI0NlpNNy43MTU1MSAxMy4wNzM5QzcuNDg3NzYgMTMuMDczOSA3LjI5MjMzIDEyLjk5MjMgNy4xMjkyMiAxMi44MjkyQzYuOTY2MSAxMi42NjYxIDYuODg0NTQgMTIuNDcwNiA2Ljg4NDU0IDEyLjI0MjlDNi44ODQ1NCAxMi4wMTUyIDYuOTY2MSAxMS44MTk3IDcuMTI5MjIgMTEuNjU2NkM3LjI5MjMzIDExLjQ5MzUgNy40ODc3NiAxMS40MTE5IDcuNzE1NTEgMTEuNDExOUM3Ljk0MzI2IDExLjQxMTkgOC4xMzg2OSAxMS40OTM1IDguMzAxOCAxMS42NTY2QzguNDY0OTIgMTEuODE5NyA4LjU0NjQ4IDEyLjAxNTIgOC41NDY0OCAxMi4yNDI5QzguNTQ2NDggMTIuMzkzNyA4LjUwODAxIDEyLjUzMjIgOC40MzEwNiAxMi42NTg0QzguMzU3MiAxMi43ODQ2IDguMjU3MTggMTIuODg2MSA4LjEzMDk5IDEyLjk2MzFDOC4wMDc4OSAxMy4wMzY5IDcuODY5MzkgMTMuMDczOSA3LjcxNTUxIDEzLjA3MzlaIiBmaWxsPSIjMTZCMzc4Ii8+PC9zdmc+');
--icon-RadioButtonInnerCircle: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjUgKDY3NDY5KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5JY29ucyAvIFVJIC8gUmFkaW9CdXR0b25Jbm5lckNpcmNsZTwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxnIGlkPSJJY29ucy0vLVVJLS8tTmV3Tm90aWZpY2F0aW9uIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8Y2lyY2xlIGlkPSJPdmFsLUNvcHktMyIgZmlsbD0iIzAwMDAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiBjeD0iOCIgY3k9IjgiIHI9IjUiPjwvY2lyY2xlPgogICAgPC9nPgo8L3N2Zz4K');
--icon-Redo: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIuMzU1MzI4LDUgTDYuNTAwMTAyMzgsNSBDNi43NzYyNDQ3Niw1IDcuMDAwMTAyMzgsNS4yMjM4NTc2MyA3LjAwMDEwMjM4LDUuNSBDNy4wMDAxMDIzOCw1Ljc3NjE0MjM3IDYuNzc2MjQ0NzYsNiA2LjUwMDEwMjM4LDYgTDEuNTEwMjEyMzgsNiBDMS40ODY4NTQ0NCw2LjAwMDQ5MTk4IDEuNDYzMzU4OTcsNS45OTkzNDQxNCAxLjQzOTg5NjI3LDUuOTk2NDk5MjUgQzEuMzYwMzg0NTQsNS45ODY4NzA1OCAxLjI4NjYwNzQzLDUuOTU4NjY1NjMgMS4yMjMwNzYwNCw1LjkxNjMwNDg0IEMxLjA5OTMxODAxLDUuODM0MTc1NDcgMS4wMjE5NjcwOSw1LjcwMzQ3ODggMS4wMDQwMTY1Niw1LjU2Mjg2NjI5IEMxLjAwMTkxMjc3LDUuNTQ2MDk2MDIgMS4wMDA2Mzk1Niw1LjUyOTA2NzQgMS4wMDAyMzk0Miw1LjUxMTgyMjkyIEMwLjk5OTk1NjM3MSw1LjUwNDI3MzIxIDAuOTk5OTQyNjg0LDUuNDk2NzA0NiAxLjAwMDEwMjM4LDUuNDg5MTI2OTMgTDEuMDAwMTAyMzgsMC41IEMxLjAwMDEwMjM4LDAuMjIzODU3NjI1IDEuMjIzOTYwMDEsMCAxLjUwMDEwMjM4LDAgQzEuNzc2MjQ0NzYsMCAyLjAwMDEwMjM4LDAuMjIzODU3NjI1IDIuMDAwMTAyMzgsMC41IEwyLjAwMDEwMjM4LDMuNzc0NjkzMTYgQzMuMzY4NjQzMjgsMi4wMzk0MDM0MyA1LjMyOTE3Nzk4LDEgNy41MDAxMDIzOCwxIEMxMS42NDIyNDQ4LDEgMTUuMDAwMTAyNCw0LjM1Nzg1NzYzIDE1LjAwMDEwMjQsOC41IEMxNS4wMDAxMDI0LDEyLjY0MjE0MjQgMTEuNjQyMjQ0OCwxNiA3LjUwMDEwMjM4LDE2IEM3LjIyMzk2MDAxLDE2IDcuMDAwMTAyMzgsMTUuNzc2MTQyNCA3LjAwMDEwMjM4LDE1LjUgQzcuMDAwMTAyMzgsMTUuMjIzODU3NiA3LjIyMzk2MDAxLDE1IDcuNTAwMTAyMzgsMTUgQzExLjA4OTk2LDE1IDE0LjAwMDEwMjQsMTIuMDg5ODU3NiAxNC4wMDAxMDI0LDguNSBDMTQuMDAwMTAyNCw0LjkxMDE0MjM3IDExLjA4OTk2LDIgNy41MDAxMDIzOCwyIEM1LjQxNTg0ODkyLDIgMy41NDU2NTQwMiwzLjEzMDY0MDcgMi4zNTUzMjgsNSBaIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHRyYW5zZm9ybT0ibWF0cml4KC0xIDAgMCAxIDE2IDApIi8+PC9zdmc+');
--icon-Remove: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTYsNCBMNiwyLjUgQzYsMi4yMjM4NTc2MyA2LjIyMzg1NzYzLDIgNi41LDIgTDkuNSwyIEM5Ljc3NjE0MjM3LDIgMTAsMi4yMjM4NTc2MyAxMCwyLjUgTDEwLDQgTDEzLjUsNCBDMTMuNzc2MTQyNCw0IDE0LDQuMjIzODU3NjMgMTQsNC41IEMxNCw0Ljc3NjE0MjM3IDEzLjc3NjE0MjQsNSAxMy41LDUgTDIuNSw1IEMyLjIyMzg1NzYzLDUgMiw0Ljc3NjE0MjM3IDIsNC41IEMyLDQuMjIzODU3NjMgMi4yMjM4NTc2Myw0IDIuNSw0IEw2LDQgWiBNNyw0IEw5LDQgTDksMyBMNywzIEw3LDQgWiBNMTEsNi41IEMxMSw2LjIyMzg1NzYzIDExLjIyMzg1NzYsNiAxMS41LDYgQzExLjc3NjE0MjQsNiAxMiw2LjIyMzg1NzYzIDEyLDYuNSBMMTIsMTIuNSBDMTIsMTMuMzI4NDI3MSAxMS4zMjg0MjcxLDE0IDEwLjUsMTQgTDUuNSwxNCBDNC42NzE1NzI4OCwxNCA0LDEzLjMyODQyNzEgNCwxMi41IEw0LDYuNSBDNCw2LjIyMzg1NzYzIDQuMjIzODU3NjMsNiA0LjUsNiBDNC43NzYxNDIzNyw2IDUsNi4yMjM4NTc2MyA1LDYuNSBMNSwxMi41IEM1LDEyLjc3NjE0MjQgNS4yMjM4NTc2MywxMyA1LjUsMTMgTDEwLjUsMTMgQzEwLjc3NjE0MjQsMTMgMTEsMTIuNzc2MTQyNCAxMSwxMi41IEwxMSw2LjUgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+');
--icon-RemoveBig: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTSA1LjQ4MTQ3MTEsNCA1LjQ5ODQyMDMsMS4wNTkzMjIgQyA1LjUwMDAxMTksMC43ODMxODQyMiA1LjcyMTMyOTQsMC41NTkzMjIwMyA1Ljk5NjMwMTcsMC41NTkzMjIwMyBIIDEwLjE5ODkzIGMgMC4yNzQ5NzIsMCAwLjQ5OTQ3MiwwLjIyMzg2MjE5IDAuNDk3ODgxLDAuNDk5OTk5OTcgTCAxMC42Nzk4NjIsNCAxNS4zNTI5NzcsMy45ODMwNTA4IGMgMC4yNzQ5NzEsLTkuOTczZS00IDAuNDk3ODgxLDAuMjIzODU3NiAwLjQ5Nzg4MSwwLjUgMCwwLjI3NjE0MjQgLTAuMjIyOTA5LDAuNDk5NjgzNyAtMC40OTc4ODEsMC41IEwgMC42MTk3Nzc2OCw1IEMgMC4zNDQ4MDU1OCw1LjAwMDMxNjMgMC4xMjE4OTYzOSw0Ljc3NjE0MjQgMC4xMjE4OTYzOSw0LjUgYyAwLC0wLjI3NjE0MjQgMC4yMjI5MDksLTAuNSAwLjQ5Nzg4MTI5LC0wLjUgeiBNIDYuNDc3MjMzOCw0IEggOS42ODQwOTkgTCA5LjcwMTA0ODIsMS41NTkzMjIgSCA2LjQ5NDE4MyBaIG0gNS41Mjk4NDcyLDMuNjI2OTUxNCBjIC0zLjk4ZS00LC0wLjI3NjE0MjEgMC4yMjI5MDksLTAuNSAwLjQ5Nzg4MSwtMC41IDAuMjc0OTczLDAgMC40OTc0ODIsMC4yMjM4NTggMC40OTc4ODIsMC41IGwgMC4wMDkyLDYuMzQ2MjQwNiBjIDAuMDAxMiwwLjgyODQyNiAtMC42Njg3MjgsMS41IC0xLjQ5MzY0NCwxLjUgSCA0LjQ2MjQxMjYgYyAtMC44MjQ5MTY4LDAgLTEuNDkyNDQ5NiwtMC42NzE1NzQgLTEuNDkzNjQ0LC0xLjUgbCAtMC4wMDkxNSwtNi4zNDYyNDA2IGMgLTMuOTgxZS00LC0wLjI3NjE0MjEgMC4yMjI5MDkxLC0wLjUgMC40OTc4ODEzLC0wLjUgMC4yNzQ5NzIzLDAgMC40OTc0ODMzLDAuMjIzODU3OSAwLjQ5Nzg4MTQsMC41IGwgMC4wMDkxNSw2LjM0NjI0MDYgYyAzLjk4MWUtNCwwLjI3NjE0MSAwLjIyMjkwOTEsMC41IDAuNDk3ODgxMywwLjUgaCA3LjA1NTk0MDQgYyAwLjI3NDk3MiwwIDAuNDk4Mjc5LC0wLjIyMzg1OSAwLjQ5Nzg4MSwtMC41IHoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg==');

View File

@ -354,7 +354,23 @@
"Timing is on": "Timing is on",
"You can make changes to the document, then stop timing to see the results.": "You can make changes to the document, then stop timing to see the results.",
"Only available to document editors": "Only available to document editors",
"Only available to document owners": "Only available to document owners"
"Only available to document owners": "Only available to document owners",
"Template mode": "Template mode",
"Special document mode": "Special document mode",
"Edit": "Edit",
"Once you start timing, Grist will measure the time it takes to evaluate each formula. This allows diagnosing which formulas are responsible for slow performance when a document is first opened, or when a document responds to changes.": "Once you start timing, Grist will measure the time it takes to evaluate each formula. This allows diagnosing which formulas are responsible for slow performance when a document is first opened, or when a document responds to changes.",
"Change nature of document": "Change nature of document",
"Regular document": "Regular document",
"Regular document behavior, all users work on the same copy of the document.": "Regular document behavior, all users work on the same copy of the document.",
"Regular": "Regular",
"Template": "Template",
"Document automatically opens in {{fiddleModeDocUrl}}. Any edit (open to anybody) will create a new unsaved copy.": "Document automatically opens in {{fiddleModeDocUrl}}. Any edit (open to anybody) will create a new unsaved copy.",
"fiddle mode": "fiddle mode",
"Tutorial": "Tutorial",
"Document automatically opens with a new copy.": "Document automatically opens with a new copy.",
"Confirm change": "Confirm change",
"This will perform a hard reload of the data engine. This may help if the data engine is stuck in an infinite loop, is indefinitely processing the latest change, or has crashed. No data will be lost, except possibly currently pending actions.": "This will perform a hard reload of the data engine. This may help if the data engine is stuck in an infinite loop, is indefinitely processing the latest change, or has crashed. No data will be lost, except possibly currently pending actions.",
"Once you start timing, Grist will measure the time it takes to evaluate each formula. This allows diagnosing which formulas are responsible for slow performance when a document is first opened, or when a document responds to changes.": "Once you start timing, Grist will measure the time it takes to evaluate each formula. This allows diagnosing which formulas are responsible for slow performance when a document is first opened, or when a document responds to changes."
},
"DocumentUsage": {
"Attachments Size": "Size of Attachments",
@ -966,7 +982,11 @@
"Reference": "Reference",
"Reference List": "Reference List",
"Attachment": "Attachment",
"Search columns": "Search columns"
"Search columns": "Search columns",
"By Name": "By Name",
"By Date Modified": "By date Modified",
"Light": "Light",
"Custom": "Custom"
},
"modals": {
"Cancel": "Cancel",

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
<title>Icons / UI / RadioButtonInnerCircle</title>
<desc>Created with Sketch.</desc>
<g id="Icons-/-UI-/-NewNotification" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<circle id="Oval-Copy-3" fill="#000000" fill-rule="nonzero" cx="8" cy="8" r="5"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@ -0,0 +1,178 @@
import { UserAPI } from "app/common/UserAPI";
import { assert, driver, until } from "mocha-webdriver";
import * as gu from "test/nbrowser/gristUtils";
import { setupTestSuite } from "test/nbrowser/testUtils";
import { Button, button, element, label, option} from "test/nbrowser/elementUtils";
type TypeLabels = "Regular" | "Template" | "Tutorial";
describe("Document Type Conversion", function () {
this.timeout(20000);
const cleanup = setupTestSuite();
let userApi: UserAPI;
let docId: string;
let session: gu.Session;
before(async () => {
session = await gu.session().teamSite.login();
docId = await session.tempNewDoc(cleanup);
userApi = session.createHomeApi();
});
async function assertExistsButton(button: Button, text: String) {
await gu.waitToPass(async () => {
assert.equal(await button.element().getText(), text);
});
assert.isTrue(await button.visible());
}
async function convert(from: TypeLabels, to: TypeLabels) {
await gu.openDocumentSettings();
// Ensure that initial document type is the expected one.
assert.equal(await displayedLabel.element().getText(), from);
// Click to open the modal
await editButton.click();
// Wait for modal.
await modal.wait();
// Select the desired Document type
await optionByLabel[to].click();
assert.isTrue(await optionByLabel[to]?.checked());
// Confirm the choice
await modalConfirm.click();
// Wait for the page to be reloaded
await driver.wait(until.stalenessOf(displayedLabel.element()));
await displayedLabel.wait();
// check that the displayedLabel is now equal to convert destination
assert.equal(await displayedLabel.element().getText(), to);
}
async function isRegular(){
assert.isFalse(await saveCopyButton.present());
assert.isFalse(await fiddleTag.present());
}
async function isTemplate(){
await assertExistsButton(saveCopyButton, "Save Copy");
assert.isTrue(await fiddleTag.visible());
}
async function isTutorial(){
await assertExistsButton(saveCopyButton, "Save Copy");
assert.isFalse(await fiddleTag.present());
}
it("should display the modal with only the current type selected", async function () {
await gu.openDocumentSettings();
// Make sure we see the Edit button of document type conversion.
await assertExistsButton(editButton, "Edit");
// Check that Document type is Regular before any conversion was ever apply to It.
assert.equal(await displayedLabel.element().getText(), "Regular");
await editButton.click();
// Wait for modal.
await modal.wait();
// We have three options.
assert.isTrue(await optionRegular.visible());
assert.isTrue(await optionTemplate.visible());
assert.isTrue(await optionTutorial.visible());
// Regular is selected cause its the current mode.
assert.isTrue(await optionRegular.checked());
assert.isFalse(await optionTemplate.checked());
assert.isFalse(await optionTutorial.checked());
// check that cancel works
await modalCancel.click();
assert.isFalse(await modal.present());
});
// If the next six tests succeed so each document type can properly be converted to every other
it('should convert from Regular to Template', async function() {
await convert("Regular", "Template");
await isTemplate();
});
it('should convert from Template to Tutorial', async function() {
await convert("Template", "Tutorial");
await isTutorial();
});
it('should convert from Tutorial to Regular', async function() {
await convert("Tutorial", "Regular");
await isRegular();
});
it('should convert from Regular to Tutorial', async function() {
await convert("Regular", "Tutorial");
await isTutorial();
});
it('should convert from Tutorial to Template', async function() {
await convert("Tutorial", "Template");
await isTemplate();
});
it('should convert from Template to Regular', async function() {
await convert("Template", "Regular");
await isRegular();
});
it('should be disabled for non-owners', async function() {
await userApi.updateDocPermissions(docId, {users: {
[gu.translateUser('user2').email]: 'editors',
}});
const session = await gu.session().teamSite.user('user2').login();
await session.loadDoc(`/doc/${docId}`);
await driver.sleep(500);
await gu.openDocumentSettings();
const start = driver.find('.test-settings-doctype-edit');
assert.equal(await start.isPresent(), true);
// Check that we have an informative tooltip.
await start.mouseMove();
// Note that .test-tooltip may appear blank a first time,
// hence the necessity to use waitToPass instead of findWait.
await gu.waitToPass(async () => {
assert.match(await driver.find('.test-tooltip').getText(), /Only available to document owners/);
});
// Nothing should happen on click. We click the location rather than the element, since the
// element isn't actually clickable.
await start.mouseMove();
await driver.withActions(a => a.press().release());
await driver.sleep(100);
assert.equal(await driver.find(".test-settings-doctype-modal").isPresent(), false);
});
});
const editButton = button('.test-settings-doctype-edit');
const saveCopyButton = button('.test-tb-share-action');
const displayedLabel = label('.test-settings-doctype-value');
const modal = element('.test-settings-doctype-modal');
const optionRegular = option('.test-settings-doctype-modal-option-regular');
const optionTemplate = option('.test-settings-doctype-modal-option-template');
const optionTutorial = option('.test-settings-doctype-modal-option-tutorial');
const optionByLabel = {
'Tutorial': optionTutorial,
'Template': optionTemplate,
'Regular': optionRegular
};
const modalConfirm = button('.test-settings-doctype-modal-confirm');
const modalCancel = button('.test-settings-doctype-modal-cancel');
const fiddleTag = element('.test-fiddle-tag');

View File

@ -3,6 +3,7 @@ import difference from 'lodash/difference';
import { assert, driver } from "mocha-webdriver";
import * as gu from "test/nbrowser/gristUtils";
import { setupTestSuite } from "test/nbrowser/testUtils";
import { button, element, label, option } from "test/nbrowser/elementUtils";
describe("Timing", function () {
this.timeout(20000);
@ -192,43 +193,6 @@ describe("Timing", function () {
});
});
const element = (testId: string) => ({
element() {
return driver.find(testId);
},
async wait() {
await driver.findWait(testId, 1000);
},
async visible() {
return await this.element().isDisplayed();
},
async present() {
return await this.element().isPresent();
}
});
const label = (testId: string) => ({
...element(testId),
async text() {
return this.element().getText();
},
});
const button = (testId: string) => ({
...element(testId),
async click() {
await gu.scrollIntoView(this.element());
await this.element().click();
},
});
const option = (testId: string) => ({
...button(testId),
async checked() {
return 'true' === await this.element().findClosest("label").find("input[type='checkbox']").getAttribute('checked');
}
});
const startTiming = button(".test-settings-timing-start");
const stopTiming = button(".test-settings-timing-stop");
const timingText = label(".test-settings-timing-desc");

View File

@ -0,0 +1,48 @@
import { driver, WebElementPromise } from "mocha-webdriver";
import * as gu from "test/nbrowser/gristUtils";
export interface Button {
click(): Promise<void>;
element(): WebElementPromise;
wait(): Promise<void>;
visible(): Promise<boolean>;
present(): Promise<boolean>;
}
export const element = (testId: string) => ({
element() {
return driver.find(testId);
},
async wait() {
await driver.findWait(testId, 2000);
},
async visible() {
return await this.element().isDisplayed();
},
async present() {
return await this.element().isPresent();
}
});
export const label = (testId: string) => ({
...element(testId),
async text() {
return this.element().getText();
},
});
export const button = (testId: string): Button => ({
...element(testId),
async click() {
await gu.scrollIntoView(this.element());
await this.element().click();
},
});
export const option = (testId: string) => ({
...button(testId),
async checked() {
return 'true' === await this.element().findClosest("label").find("input[type='checkbox']").getAttribute('checked');
}
});