(core) Form Publishing

Summary:
Adds initial implementation of form publishing, built upon WYSIWYS shares.

A simple UI for publishing and unpublishing forms is included.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Subscribers: paulfitz, jarek

Differential Revision: https://phab.getgrist.com/D4154
This commit is contained in:
George Gevoian
2024-01-12 09:35:24 -08:00
parent 8ddcff4310
commit e12471347b
17 changed files with 634 additions and 85 deletions

View File

@@ -34,6 +34,7 @@ import {ColumnRec, createColumnRec} from 'app/client/models/entities/ColumnRec';
import {createDocInfoRec, DocInfoRec} from 'app/client/models/entities/DocInfoRec';
import {createFilterRec, FilterRec} from 'app/client/models/entities/FilterRec';
import {createPageRec, PageRec} from 'app/client/models/entities/PageRec';
import {createShareRec, ShareRec} from 'app/client/models/entities/ShareRec';
import {createTabBarRec, TabBarRec} from 'app/client/models/entities/TabBarRec';
import {createTableRec, TableRec} from 'app/client/models/entities/TableRec';
import {createValidationRec, ValidationRec} from 'app/client/models/entities/ValidationRec';
@@ -127,6 +128,7 @@ export class DocModel {
public tabBar: MTM<TabBarRec> = this._metaTableModel("_grist_TabBar", createTabBarRec);
public validations: MTM<ValidationRec> = this._metaTableModel("_grist_Validations", createValidationRec);
public pages: MTM<PageRec> = this._metaTableModel("_grist_Pages", createPageRec);
public shares: MTM<ShareRec> = this._metaTableModel("_grist_Shares", createShareRec);
public rules: MTM<ACLRuleRec> = this._metaTableModel("_grist_ACLRules", createACLRuleRec);
public filters: MTM<FilterRec> = this._metaTableModel("_grist_Filters", createFilterRec);
public cells: MTM<CellRec> = this._metaTableModel("_grist_Cells", createCellRec);
@@ -149,6 +151,7 @@ export class DocModel {
public allTabs: KoArray<TabBarRec> = this.tabBar.createAllRowsModel('tabPos');
public allPages: ko.Computed<PageRec[]>;
/** Pages that are shown in the menu. These can include censored pages if they have children. */
public menuPages: ko.Computed<PageRec[]>;
// Excludes pages hidden by ACL rules or other reasons (e.g. doc-tour)
@@ -217,8 +220,9 @@ export class DocModel {
// Get a list of only the visible pages.
const allPages = this.pages.createAllRowsModel('pagePos');
this.allPages = ko.computed(() => allPages.all());
this.menuPages = ko.computed(() => {
const pagesToShow = allPages.all().filter(p => !p.isSpecial()).sort((a, b) => a.pagePos() - b.pagePos());
const pagesToShow = this.allPages().filter(p => !p.isSpecial()).sort((a, b) => a.pagePos() - b.pagePos());
// Helper to find all children of a page.
const children = memoize((page: PageRec) => {
const following = pagesToShow.slice(pagesToShow.indexOf(page) + 1);
@@ -230,7 +234,7 @@ export class DocModel {
const hide = memoize((page: PageRec): boolean => page.isCensored() && children(page).every(p => hide(p)));
return pagesToShow.filter(p => !hide(p));
});
this.visibleDocPages = ko.computed(() => allPages.all().filter(p => !p.isHidden()));
this.visibleDocPages = ko.computed(() => this.allPages().filter(p => !p.isHidden()));
this.hasDocTour = ko.computed(() => this.visibleTableIds.all().includes('GristDocTour'));

View File

@@ -1,4 +1,5 @@
import {DocModel, IRowModel, refRecord, ViewRec} from 'app/client/models/DocModel';
import {ShareRec} from 'app/client/models/entities/ShareRec';
import * as ko from 'knockout';
// Represents a page entry in the tree of pages.
@@ -7,6 +8,7 @@ export interface PageRec extends IRowModel<"_grist_Pages"> {
isHidden: ko.Computed<boolean>;
isCensored: ko.Computed<boolean>;
isSpecial: ko.Computed<boolean>;
share: ko.Computed<ShareRec>;
}
export function createPageRec(this: PageRec, docModel: DocModel): void {
@@ -36,4 +38,5 @@ export function createPageRec(this: PageRec, docModel: DocModel): void {
this.isHidden = ko.pureComputed(() => {
return this.isCensored() || this.isSpecial();
});
this.share = refRecord(docModel.shares, this.shareRef);
}

View File

@@ -0,0 +1,10 @@
import {IRowModel} from 'app/client/models/DocModel';
import * as modelUtil from 'app/client/models/modelUtil';
export interface ShareRec extends IRowModel<"_grist_Shares"> {
optionsObj: modelUtil.SaveableObjObservable<any>;
}
export function createShareRec(this: ShareRec): void {
this.optionsObj = modelUtil.jsonObservable(this.options);
}

View File

@@ -1,7 +1,7 @@
import {BoxSpec} from 'app/client/components/Layout';
import {KoArray} from 'app/client/lib/koArray';
import * as koUtil from 'app/client/lib/koUtil';
import {DocModel, IRowModel, recordSet, refRecord} from 'app/client/models/DocModel';
import {DocModel, IRowModel, PageRec, recordSet, refRecord} from 'app/client/models/DocModel';
import {TabBarRec, ViewSectionRec} from 'app/client/models/DocModel';
import * as modelUtil from 'app/client/models/modelUtil';
import * as ko from 'knockout';
@@ -30,6 +30,8 @@ export interface ViewRec extends IRowModel<"_grist_Views"> {
// If the active section is removed, set the next active section to be the default.
_isActiveSectionGone: ko.Computed<boolean>;
page: ko.Computed<PageRec|null>;
}
export function createViewRec(this: ViewRec, docModel: DocModel): void {
@@ -76,6 +78,11 @@ export function createViewRec(this: ViewRec, docModel: DocModel): void {
this.activeSectionId(0);
}
}));
this.page = this.autoDispose(ko.pureComputed(() => {
const viewRef = this.id();
return docModel.allPages().find(p => p.viewRef() === viewRef) ?? null;
}));
}
function getFirstLeaf(layoutSpec: BoxSpec|undefined): BoxSpec['leaf'] {

View File

@@ -71,6 +71,7 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO
columns: ko.Computed<ColumnRec[]>;
optionsObj: modelUtil.SaveableObjObservable<any>;
shareOptionsObj: modelUtil.SaveableObjObservable<any>;
customDef: CustomViewSectionDef;
@@ -380,6 +381,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
};
this.optionsObj = modelUtil.jsonObservable(this.options,
(obj: any) => defaults(obj || {}, defaultOptions));
this.shareOptionsObj = modelUtil.jsonObservable(this.shareOptions);
const customViewDefaults = {
mode: 'url',