(core) Form kanban tasks

Summary:
- Open all links in a new tab
- Excluding not filled columns (to fix trigger formulas)
- Fixed Ref/RefList submission
- Removing redundant type definitions for Box
- Adding header menu item
- Default empty values in select control

Test Plan: Updated

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D4166
This commit is contained in:
Jarosław Sadziński
2024-01-23 21:52:57 +01:00
parent 007c4492dc
commit 95c0441d84
22 changed files with 363 additions and 124 deletions

View File

@@ -3,7 +3,7 @@ import {GristDoc} from 'app/client/components/GristDoc';
import {makeT} from 'app/client/lib/localization';
import {reportError} from 'app/client/models/AppModel';
import {ColumnRec, TableRec, ViewSectionRec} from 'app/client/models/DocModel';
import {GRIST_FORMS_FEATURE, PERMITTED_CUSTOM_WIDGETS} from "app/client/models/features";
import {PERMITTED_CUSTOM_WIDGETS} from "app/client/models/features";
import {GristTooltips} from 'app/client/ui/GristTooltips';
import {linkId, NoLink} from 'app/client/ui/selectBy';
import {overflowTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
@@ -98,21 +98,17 @@ export interface IOptions extends ISelectOptions {
const testId = makeTestId('test-wselect-');
function maybeForms(): Array<'form'> {
return GRIST_FORMS_FEATURE() ? ['form'] : [];
}
// The picker disables some choices that do not make much sense. This function return the list of
// compatible types given the tableId and whether user is creating a new page or not.
function getCompatibleTypes(tableId: TableRef, isNewPage: boolean|undefined): IWidgetType[] {
if (tableId !== 'New Table') {
return ['record', 'single', 'detail', 'chart', 'custom', 'custom.calendar', ...maybeForms()];
return ['record', 'single', 'detail', 'chart', 'custom', 'custom.calendar', 'form'];
} else if (isNewPage) {
// New view + new table means we'll be switching to the primary view.
return ['record', ...maybeForms()];
return ['record', 'form'];
} else {
// The type 'chart' makes little sense when creating a new table.
return ['record', 'single', 'detail', ...maybeForms()];
return ['record', 'single', 'detail', 'form'];
}
}
@@ -275,7 +271,7 @@ const permittedCustomWidgets: IAttachedCustomWidget[] = PERMITTED_CUSTOM_WIDGETS
const finalListOfCustomWidgetToShow = permittedCustomWidgets.filter(a=>
registeredCustomWidgets.includes(a));
const sectionTypes: IWidgetType[] = [
'record', 'single', 'detail', ...maybeForms(), 'chart', ...finalListOfCustomWidgetToShow, 'custom'
'record', 'single', 'detail', 'form', 'chart', ...finalListOfCustomWidgetToShow, 'custom'
];

View File

@@ -67,7 +67,8 @@ import {
MultiHolder,
Observable,
styled,
subscribe
subscribe,
toKo
} from 'grainjs';
import * as ko from 'knockout';
@@ -955,12 +956,25 @@ export class RightPanel extends Disposable {
return vsi && vsi.activeFieldBuilder();
}));
const formView = owner.autoDispose(ko.computed(() => {
// Sorry for the acrobatics below, but grainjs are not reentred when the active section changes.
const viewInstance = owner.autoDispose(ko.computed(() => {
const vsi = this._gristDoc.viewModel.activeSection?.().viewInstance();
return (vsi ?? null) as FormView|null;
if (!vsi || vsi.isDisposed() || !toKo(ko, this._isForm)) { return null; }
return vsi;
}));
const selectedBox = Computed.create(owner, (use) => use(formView) && use(use(formView)!.selectedBox));
const formView = owner.autoDispose(ko.computed(() => {
const view = viewInstance() as unknown as FormView;
if (!view || !view.selectedBox) { return null; }
return view;
}));
const selectedBox = owner.autoDispose(ko.pureComputed(() => {
const view = formView();
if (!view) { return null; }
const box = toKo(ko, view.selectedBox)();
return box;
}));
const selectedField = Computed.create(owner, (use) => {
const box = use(selectedBox);
if (!box) { return null; }
@@ -983,33 +997,38 @@ export class RightPanel extends Disposable {
}
});
return cssSection(
return domAsync(imports.loadViewPane().then(() => buildConfigContainer(cssSection(
// Field config.
dom.maybe(selectedField, (field) => {
dom.maybeOwned(selectedField, (scope, field) => {
const requiredField = field.widgetOptionsJson.prop('formRequired');
// V2 thing.
// const hiddenField = field.widgetOptionsJson.prop('formHidden');
const defaultField = field.widgetOptionsJson.prop('formDefault');
const toComputed = (obs: typeof defaultField) => {
const result = Computed.create(null, (use) => use(obs));
const result = Computed.create(scope, (use) => use(obs));
result.onWrite(val => obs.setAndSave(val));
return result;
};
const fieldTitle = field.widgetOptionsJson.prop('question');
return [
cssLabel(t("Field title")),
cssRow(
cssTextInput(
fromKo(field.label),
(val) => field.displayLabel.saveOnly(val),
fromKo(fieldTitle),
(val) => fieldTitle.saveOnly(val).catch(reportError),
dom.prop('readonly', use => use(field.disableModify)),
dom.prop('placeholder', use => use(field.displayLabel) || use(field.colId)),
testId('field-title'),
),
),
cssLabel(t("Table column name")),
cssRow(
cssTextInput(
fromKo(field.colId),
(val) => field.column().colId.saveOnly(val),
fromKo(field.displayLabel),
(val) => field.displayLabel.saveOnly(val).catch(reportError),
dom.prop('readonly', use => use(field.disableModify)),
testId('field-label'),
),
),
// TODO: this is for V1 as it requires full cell editor here.
@@ -1038,7 +1057,11 @@ export class RightPanel extends Disposable {
]),
cssSeparator(),
cssLabel(t("Field rules")),
cssRow(labeledSquareCheckbox(toComputed(requiredField), t("Required field")),),
cssRow(labeledSquareCheckbox(
toComputed(requiredField),
t("Required field"),
testId('field-required'),
)),
// V2 thing
// cssRow(labeledSquareCheckbox(toComputed(hiddenField), t("Hidden field")),),
];
@@ -1071,7 +1094,7 @@ export class RightPanel extends Disposable {
dom.maybe(u => !u(selectedColumn) && !u(selectedBox), () => [
cssLabel(t('Layout')),
])
);
))));
}
}

View File

@@ -2,7 +2,6 @@ import {hooks} from 'app/client/Hooks';
import {makeT} from 'app/client/lib/localization';
import {allCommands} from 'app/client/components/commands';
import {ViewSectionRec} from 'app/client/models/DocModel';
import {GRIST_FORMS_FEATURE} from 'app/client/models/features';
import {urlState} from 'app/client/models/gristUrlState';
import {testId} from 'app/client/ui2018/cssVars';
import {menuDivider, menuItemCmd, menuItemLink} from 'app/client/ui2018/menus';
@@ -96,7 +95,7 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool
menuItemCmd(allCommands.viewTabOpen, t("Widget options"), testId('widget-options')),
menuItemCmd(allCommands.sortFilterTabOpen, t("Advanced Sort & Filter"), dom.hide(viewSection.isRecordCard)),
menuItemCmd(allCommands.dataSelectionTabOpen, t("Data selection"), dom.hide(viewSection.isRecordCard)),
!GRIST_FORMS_FEATURE() ? null : menuItemCmd(allCommands.createForm, t("Create a form"), dom.show(isTable)),
menuItemCmd(allCommands.createForm, t("Create a form"), dom.show(isTable)),
]),
menuDivider(dom.hide(viewSection.isRecordCard)),