mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-20 17:14:11 +00:00
418681915e
Summary: - Forms now have a reset button. - Choice and Reference fields in forms now have an improved select menu. - Formula and attachments column types are no longer mappable or visible in forms. - Fields in a form widget are now removed if their column is deleted. - The preview button in a published form widget has been replaced with a view button. It now opens the published form in a new tab. - A new share menu for published form widgets, with options to copy a link or embed code. - Forms can now have multiple sections. - Form widgets now indicate when publishing is unavailable (e.g. in forks or unsaved documents). - General improvements to form styling. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4203
281 lines
7.6 KiB
TypeScript
281 lines
7.6 KiB
TypeScript
import {allCommands} from 'app/client/components/commands';
|
|
import {makeT} from 'app/client/lib/localization';
|
|
import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel';
|
|
import {basicButton, cssButton, primaryButton} from 'app/client/ui2018/buttons';
|
|
import {squareCheckbox} from 'app/client/ui2018/checkbox';
|
|
import {theme, vars} from 'app/client/ui2018/cssVars';
|
|
import {cssDragger} from 'app/client/ui2018/draggableList';
|
|
import {icon} from 'app/client/ui2018/icons';
|
|
import {Computed, Disposable, dom, fromKo, makeTestId, Observable, styled} from 'grainjs';
|
|
import * as ko from 'knockout';
|
|
|
|
const testId = makeTestId('test-vfc-');
|
|
const t = makeT('VisibleFieldsConfig');
|
|
|
|
/**
|
|
* This is a component used in the RightPanel. It replaces hidden fields section on other views, and adds
|
|
* the ability to drag and drop fields onto the form.
|
|
*/
|
|
export class MappedFieldsConfig extends Disposable {
|
|
|
|
constructor(private _section: ViewSectionRec) {
|
|
super();
|
|
}
|
|
|
|
public buildDom() {
|
|
const unmappedColumns = fromKo(this.autoDispose(ko.pureComputed(() => {
|
|
if (this._section.isDisposed()) {
|
|
return [];
|
|
}
|
|
const fields = new Set(this._section.viewFields().map(f => f.colId()).all());
|
|
const cols = this._section.table().visibleColumns()
|
|
.filter(c => c.isFormCol() && !fields.has(c.colId()));
|
|
return cols.map(col => ({
|
|
col,
|
|
selected: Observable.create(null, false),
|
|
}));
|
|
})));
|
|
const mappedColumns = fromKo(this.autoDispose(ko.pureComputed(() => {
|
|
if (this._section.isDisposed()) {
|
|
return [];
|
|
}
|
|
const cols = this._section.viewFields().map(f => f.column()).all()
|
|
.filter(c => c.isFormCol());
|
|
return cols.map(col => ({
|
|
col,
|
|
selected: Observable.create(null, false),
|
|
}));
|
|
})));
|
|
|
|
const anyUnmappedSelected = Computed.create(this, use => {
|
|
return use(unmappedColumns).some(c => use(c.selected));
|
|
});
|
|
|
|
const anyMappedSelected = Computed.create(this, use => {
|
|
return use(mappedColumns).some(c => use(c.selected));
|
|
});
|
|
|
|
const mapSelected = async () => {
|
|
await allCommands.showColumns.run(
|
|
unmappedColumns.get().filter(c => c.selected.get()).map(c => c.col.colId.peek()));
|
|
};
|
|
|
|
const unMapSelected = async () => {
|
|
await allCommands.hideFields.run(
|
|
mappedColumns.get().filter(c => c.selected.get()).map(c => c.col.colId.peek()));
|
|
};
|
|
|
|
return [
|
|
cssHeader(
|
|
cssFieldListHeader(dom.text(t("Mapped"))),
|
|
selectAllLabel(
|
|
dom.on('click', () => {
|
|
mappedColumns.get().forEach((col) => col.selected.set(true));
|
|
}),
|
|
dom.show(/* any mapped columns */ use => use(mappedColumns).length > 0),
|
|
),
|
|
),
|
|
dom('div',
|
|
testId('visible-fields'),
|
|
dom.forEach(mappedColumns, (field) => {
|
|
return this._buildMappedField(field);
|
|
})
|
|
),
|
|
dom.maybe(anyMappedSelected, () =>
|
|
cssRow(
|
|
primaryButton(
|
|
dom.text(t("Unmap fields")),
|
|
dom.on('click', unMapSelected),
|
|
testId('visible-hide')
|
|
),
|
|
basicButton(
|
|
t("Clear"),
|
|
dom.on('click', () => mappedColumns.get().forEach((col) => col.selected.set(false))),
|
|
testId('visible-clear')
|
|
),
|
|
testId('visible-batch-buttons')
|
|
),
|
|
),
|
|
cssHeader(
|
|
cssFieldListHeader(t("Unmapped")),
|
|
selectAllLabel(
|
|
dom.on('click', () => {
|
|
unmappedColumns.get().forEach((col) => col.selected.set(true));
|
|
}),
|
|
dom.show(/* any unmapped columns */ use => use(unmappedColumns).length > 0),
|
|
),
|
|
),
|
|
dom('div',
|
|
testId('hidden-fields'),
|
|
dom.forEach(unmappedColumns, (field) => {
|
|
return this._buildUnmappedField(field);
|
|
})
|
|
),
|
|
dom.maybe(anyUnmappedSelected, () =>
|
|
cssRow(
|
|
primaryButton(
|
|
dom.text(t("Map fields")),
|
|
dom.on('click', mapSelected),
|
|
testId('visible-hide')
|
|
),
|
|
basicButton(
|
|
t("Clear"),
|
|
dom.on('click', () => unmappedColumns.get().forEach((col) => col.selected.set(false))),
|
|
testId('visible-clear')
|
|
),
|
|
testId('visible-batch-buttons')
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
private _buildUnmappedField(props: {col: ColumnRec, selected: Observable<boolean>}) {
|
|
const column = props.col;
|
|
return cssDragRow(
|
|
testId('hidden-field'),
|
|
{draggable: "true"},
|
|
dom.on('dragstart', (ev) => {
|
|
// Prevent propagation, as we might be in a nested editor.
|
|
ev.stopPropagation();
|
|
ev.dataTransfer?.setData('text/plain', JSON.stringify({
|
|
type: 'Field',
|
|
leaf: column.colId.peek(), // TODO: convert to Field
|
|
}));
|
|
ev.dataTransfer!.dropEffect = "move";
|
|
}),
|
|
cssSimpleDragger(),
|
|
cssFieldEntry(
|
|
cssFieldLabel(dom.text(column.label)),
|
|
cssHideIcon('EyeShow',
|
|
testId('hide'),
|
|
dom.on('click', () => {
|
|
allCommands.showColumns.run([column.colId.peek()]);
|
|
}),
|
|
),
|
|
cssSquareCheckbox(props.selected),
|
|
),
|
|
);
|
|
}
|
|
|
|
|
|
private _buildMappedField(props: {col: ColumnRec, selected: Observable<boolean>}) {
|
|
const column = props.col;
|
|
return cssDragRow(
|
|
testId('visible-field'),
|
|
cssSimpleDragger(
|
|
cssSimpleDragger.cls('-hidden'),
|
|
),
|
|
cssFieldEntry(
|
|
cssFieldLabel(dom.text(column.label)),
|
|
cssHideIcon('EyeHide',
|
|
testId('hide'),
|
|
dom.on('click', () => {
|
|
allCommands.hideFields.run([column.colId.peek()]);
|
|
}),
|
|
),
|
|
cssSquareCheckbox(props.selected),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function selectAllLabel(...args: any[]) {
|
|
return cssControlLabel(
|
|
testId('select-all'),
|
|
icon('Tick'),
|
|
dom('span', t("Select All")),
|
|
...args
|
|
);
|
|
}
|
|
|
|
const cssControlLabel = styled('div', `
|
|
--icon-color: ${theme.controlFg};
|
|
color: ${theme.controlFg};
|
|
cursor: pointer;
|
|
line-height: 16px;
|
|
`);
|
|
|
|
|
|
// TODO: reuse them
|
|
const cssDragRow = styled('div', `
|
|
display: flex !important;
|
|
align-items: center;
|
|
margin: 0 16px 0px 0px;
|
|
margin-bottom: 2px;
|
|
cursor: grab;
|
|
`);
|
|
|
|
const cssFieldEntry = styled('div', `
|
|
display: flex;
|
|
background-color: ${theme.hover};
|
|
border-radius: 2px;
|
|
margin: 0 8px 0 0;
|
|
padding: 4px 8px;
|
|
cursor: default;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
flex: 1 1 auto;
|
|
|
|
--icon-color: ${theme.lightText};
|
|
`);
|
|
|
|
const cssSimpleDragger = styled(cssDragger, `
|
|
cursor: grab;
|
|
.${cssDragRow.className}:hover & {
|
|
visibility: visible;
|
|
}
|
|
&-hidden {
|
|
visibility: hidden !important;
|
|
}
|
|
`);
|
|
|
|
const cssHideIcon = styled(icon, `
|
|
--icon-color: ${theme.lightText};
|
|
display: none;
|
|
cursor: pointer;
|
|
flex: none;
|
|
margin-right: 8px;
|
|
.${cssFieldEntry.className}:hover & {
|
|
display: block;
|
|
}
|
|
`);
|
|
|
|
const cssFieldLabel = styled('span', `
|
|
color: ${theme.text};
|
|
flex: 1 1 auto;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
`);
|
|
|
|
const cssFieldListHeader = styled('span', `
|
|
color: ${theme.text};
|
|
flex: 1 1 0px;
|
|
font-size: ${vars.xsmallFontSize};
|
|
text-transform: uppercase;
|
|
`);
|
|
|
|
const cssRow = styled('div', `
|
|
display: flex;
|
|
margin: 16px;
|
|
overflow: hidden;
|
|
--icon-color: ${theme.lightText};
|
|
& > .${cssButton.className} {
|
|
margin-right: 8px;
|
|
}
|
|
`);
|
|
|
|
const cssHeader = styled(cssRow, `
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
margin-bottom: 12px;
|
|
line-height: 1em;
|
|
& * {
|
|
line-height: 1em;
|
|
}
|
|
`);
|
|
|
|
const cssSquareCheckbox = styled(squareCheckbox, `
|
|
flex-shrink: 0;
|
|
`);
|