2020-10-02 15:10:00 +00:00
|
|
|
var _ = require('underscore');
|
|
|
|
var ko = require('knockout');
|
|
|
|
var dispose = require('../lib/dispose');
|
|
|
|
var dom = require('../lib/dom');
|
|
|
|
var kd = require('../lib/koDom');
|
|
|
|
var kf = require('../lib/koForm');
|
|
|
|
var koArray = require('../lib/koArray');
|
|
|
|
var commands = require('./commands');
|
|
|
|
var {CustomSectionElement} = require('../lib/CustomSectionElement');
|
2021-09-15 08:51:18 +00:00
|
|
|
const {ChartConfig} = require('./ChartView');
|
2023-11-20 00:46:32 +00:00
|
|
|
const {Computed, dom: grainjsDom, makeTestId, Holder} = require('grainjs');
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
const {cssRow} = require('app/client/ui/RightPanelStyles');
|
|
|
|
const {SortFilterConfig} = require('app/client/ui/SortFilterConfig');
|
|
|
|
const {primaryButton} = require('app/client/ui2018/buttons');
|
|
|
|
const {select} = require('app/client/ui2018/menus');
|
2020-11-20 03:49:33 +00:00
|
|
|
const {confirmModal} = require('app/client/ui2018/modals');
|
2022-10-28 16:11:08 +00:00
|
|
|
const {makeT} = require('app/client/lib/localization');
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
const testId = makeTestId('test-vconfigtab-');
|
|
|
|
|
2022-12-09 15:46:03 +00:00
|
|
|
const t = makeT('ViewConfigTab');
|
2022-10-28 16:11:08 +00:00
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
/**
|
|
|
|
* Helper class that combines one ViewSection's data for building dom.
|
|
|
|
*/
|
|
|
|
function ViewSectionData(section) {
|
|
|
|
this.section = section;
|
|
|
|
|
|
|
|
// A koArray reflecting the columns (RowModels) that are not present in the current view.
|
|
|
|
this.hiddenFields = this.autoDispose(koArray.syncedKoArray(section.hiddenColumns));
|
|
|
|
}
|
|
|
|
dispose.makeDisposable(ViewSectionData);
|
|
|
|
|
|
|
|
|
|
|
|
function ViewConfigTab(options) {
|
|
|
|
var self = this;
|
|
|
|
this.gristDoc = options.gristDoc;
|
|
|
|
this.viewModel = options.viewModel;
|
2023-11-20 00:46:32 +00:00
|
|
|
this._viewSectionDataHolder = Holder.create(this);
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
// viewModel may point to different views, but viewSectionData is a single koArray reflecting
|
|
|
|
// the sections of the current view.
|
|
|
|
this.viewSectionData = this.autoDispose(
|
|
|
|
koArray.syncedKoArray(this.viewModel.viewSections, function(section) {
|
|
|
|
return ViewSectionData.create(section);
|
|
|
|
})
|
|
|
|
.setAutoDisposeValues()
|
|
|
|
);
|
|
|
|
|
|
|
|
this.isDetail = this.autoDispose(ko.computed(function() {
|
2021-04-26 21:54:09 +00:00
|
|
|
return ['detail', 'single'].includes(this.viewModel.activeSection().parentKey());
|
2020-10-02 15:10:00 +00:00
|
|
|
}, this));
|
|
|
|
this.isChart = this.autoDispose(ko.computed(function() {
|
2022-11-17 20:17:51 +00:00
|
|
|
return this.viewModel.activeSection().parentKey() === 'chart';}, this));
|
2020-10-02 15:10:00 +00:00
|
|
|
this.isGrid = this.autoDispose(ko.computed(function() {
|
2022-11-17 20:17:51 +00:00
|
|
|
return this.viewModel.activeSection().parentKey() === 'record';}, this));
|
2020-10-02 15:10:00 +00:00
|
|
|
this.isCustom = this.autoDispose(ko.computed(function() {
|
2022-11-17 20:17:51 +00:00
|
|
|
return this.viewModel.activeSection().parentKey() === 'custom';}, this));
|
|
|
|
this.isRaw = this.autoDispose(ko.computed(function() {
|
|
|
|
return this.viewModel.activeSection().isRaw();}, this));
|
2023-11-20 00:46:32 +00:00
|
|
|
this.isRecordCard = this.autoDispose(ko.computed(function() {
|
|
|
|
return this.viewModel.activeSection().isRecordCard();}, this));
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2023-11-20 00:46:32 +00:00
|
|
|
this.activeRawOrRecordCardSectionData = this.autoDispose(ko.computed(function() {
|
|
|
|
return self.isRaw() || self.isRecordCard()
|
|
|
|
? self._viewSectionDataHolder.autoDispose(ViewSectionData.create(self.viewModel.activeSection()))
|
|
|
|
: null;
|
2022-11-17 20:17:51 +00:00
|
|
|
}));
|
|
|
|
this.activeSectionData = this.autoDispose(ko.computed(function() {
|
|
|
|
return (
|
|
|
|
_.find(self.viewSectionData.all(), function(sectionData) {
|
|
|
|
return sectionData.section &&
|
|
|
|
sectionData.section.getRowId() === self.viewModel.activeSectionId();
|
|
|
|
})
|
2023-11-20 00:46:32 +00:00
|
|
|
|| self.activeRawOrRecordCardSectionData()
|
2022-11-17 20:17:51 +00:00
|
|
|
|| self.viewSectionData.at(0)
|
2020-10-02 15:10:00 +00:00
|
|
|
);
|
2022-11-17 20:17:51 +00:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
dispose.makeDisposable(ViewConfigTab);
|
2021-11-03 11:44:28 +00:00
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
ViewConfigTab.prototype.buildSortFilterDom = function() {
|
|
|
|
return grainjsDom.maybe(this.activeSectionData, ({section}) => {
|
|
|
|
return grainjsDom.create(SortFilterConfig, section, this.gristDoc);
|
2021-11-03 11:44:28 +00:00
|
|
|
});
|
2020-10-02 15:10:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
ViewConfigTab.prototype._makeOnDemand = function(table) {
|
|
|
|
// After saving the changed setting, force the reload of the document.
|
|
|
|
const onConfirm = () => {
|
|
|
|
return table.onDemand.saveOnly(!table.onDemand.peek())
|
|
|
|
.then(() => {
|
|
|
|
return this.gristDoc.docComm.reloadDoc()
|
|
|
|
.catch((err) => {
|
|
|
|
// Ignore the expected error from the socket shutdown that we asked for.
|
|
|
|
if (!err.message.includes('GristWSConnection disposed')) {
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (table.onDemand()) {
|
2023-04-28 09:20:28 +00:00
|
|
|
confirmModal('Unmark table On-Demand?', 'Unmark On-Demand', onConfirm, {
|
|
|
|
explanation: dom('div', 'If you unmark table ', dom('b', table), ' as On-Demand, ' +
|
2020-10-02 15:10:00 +00:00
|
|
|
'its data will be loaded into the calculation engine and will be available ' +
|
|
|
|
'for use in formulas. For a big table, this may greatly increase load times.',
|
2023-04-28 09:20:28 +00:00
|
|
|
dom('br'), dom('br'), 'Changing this setting will reload the document for all users.'),
|
|
|
|
});
|
2020-10-02 15:10:00 +00:00
|
|
|
} else {
|
2023-04-28 09:20:28 +00:00
|
|
|
confirmModal('Make table On-Demand?', 'Make On-Demand', onConfirm, {
|
|
|
|
explanation: dom('div', 'If you make table ', dom('b', table), ' On-Demand, ' +
|
2020-10-02 15:10:00 +00:00
|
|
|
'its data will no longer be loaded into the calculation engine and will not be available ' +
|
|
|
|
'for use in formulas. It will remain available for viewing and editing.',
|
2023-04-28 09:20:28 +00:00
|
|
|
dom('br'), dom('br'), 'Changing this setting will reload the document for all users.'),
|
|
|
|
});
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
ViewConfigTab.prototype._buildAdvancedSettingsDom = function() {
|
|
|
|
return kd.maybe(() => {
|
|
|
|
const s = this.activeSectionData();
|
|
|
|
return s && !s.section.table().summarySourceTable() ? s : null;
|
|
|
|
}, (sectionData) => {
|
|
|
|
|
|
|
|
const table = sectionData.section.table();
|
|
|
|
const isCollapsed = ko.observable(true);
|
|
|
|
return [
|
2022-12-06 13:40:02 +00:00
|
|
|
kf.collapserLabel(isCollapsed, t("Advanced settings"), dom.testId('ViewConfig_advanced')),
|
2020-10-02 15:10:00 +00:00
|
|
|
kf.helpRow(kd.hide(isCollapsed),
|
2022-12-06 13:40:02 +00:00
|
|
|
t("Big tables may be marked as \"on-demand\" to avoid loading them into the data engine."),
|
2020-10-02 15:10:00 +00:00
|
|
|
kd.style('text-align', 'left'),
|
|
|
|
kd.style('margin-top', '1.5rem')
|
|
|
|
),
|
|
|
|
kf.row(kd.hide(isCollapsed),
|
2023-09-21 16:57:58 +00:00
|
|
|
dom('div', primaryButton(
|
2022-12-06 13:40:02 +00:00
|
|
|
kd.text(() => table.onDemand() ? t("Unmark On-Demand") : t("Make On-Demand")),
|
2023-09-21 16:57:58 +00:00
|
|
|
kd.style('margin-top', '1rem'),
|
|
|
|
dom.on('click', () => this._makeOnDemand(table)),
|
|
|
|
dom.testId('ViewConfig_onDemandBtn'),
|
|
|
|
)),
|
2020-10-02 15:10:00 +00:00
|
|
|
),
|
|
|
|
];
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
ViewConfigTab.prototype._buildThemeDom = function() {
|
|
|
|
return kd.maybe(this.activeSectionData, (sectionData) => {
|
|
|
|
var section = sectionData.section;
|
|
|
|
if (this.isDetail()) {
|
|
|
|
const theme = Computed.create(null, (use) => use(section.themeDef));
|
|
|
|
theme.onWrite(val => section.themeDef.setAndSave(val));
|
|
|
|
return cssRow(
|
|
|
|
dom.autoDispose(theme),
|
|
|
|
select(theme, [
|
2022-12-06 13:40:02 +00:00
|
|
|
{label: t("Form"), value: 'form' },
|
|
|
|
{label: t("Compact"), value: 'compact'},
|
|
|
|
{label: t("Blocks"), value: 'blocks' },
|
2020-10-02 15:10:00 +00:00
|
|
|
]),
|
|
|
|
testId('detail-theme')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
ViewConfigTab.prototype._buildChartConfigDom = function() {
|
2021-09-15 08:51:18 +00:00
|
|
|
return grainjsDom.maybe(this.viewModel.activeSection, (section) => grainjsDom.create(ChartConfig, this.gristDoc, section));
|
2020-10-02 15:10:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
ViewConfigTab.prototype._buildLayoutDom = function() {
|
|
|
|
return kd.maybe(this.activeSectionData, (sectionData) => {
|
|
|
|
if (this.isDetail()) {
|
|
|
|
const view = sectionData.section.viewInstance.peek();
|
|
|
|
const layoutEditorObs = ko.computed(() => view && view.recordLayout && view.recordLayout.layoutEditor());
|
|
|
|
return cssRow({style: 'margin-top: 16px;'},
|
|
|
|
kd.maybe(layoutEditorObs, (editor) => editor.buildFinishButtons()),
|
2022-12-06 13:40:02 +00:00
|
|
|
primaryButton(t("Edit Card Layout"),
|
2020-10-02 15:10:00 +00:00
|
|
|
dom.autoDispose(layoutEditorObs),
|
|
|
|
dom.on('click', () => commands.allCommands.editLayout.run()),
|
|
|
|
grainjsDom.hide(layoutEditorObs),
|
2022-12-20 02:06:39 +00:00
|
|
|
grainjsDom.cls('behavioral-prompt-edit-card-layout'),
|
|
|
|
testId('detail-edit-layout'),
|
2020-10-02 15:10:00 +00:00
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Builds the three items for configuring a `Custom View`:
|
|
|
|
* 1) Mode picker: let user choose between 'url' and 'plugin' mode
|
|
|
|
* 2) Show if 'url' mode: let user enter the url
|
|
|
|
* 3) Show if 'plugin' mode: let user pick a plugin and a section from the list of available plugin.
|
|
|
|
*/
|
|
|
|
ViewConfigTab.prototype._buildCustomTypeItems = function() {
|
|
|
|
const docPluginManager = this.gristDoc.docPluginManager;
|
|
|
|
const activeSection = this.viewModel.activeSection;
|
|
|
|
|
|
|
|
// all available custom sections grouped by their plugin id
|
|
|
|
const customSections = _.groupBy(CustomSectionElement.getSections(docPluginManager.pluginsList), s => s.pluginId);
|
|
|
|
|
|
|
|
// all plugin ids which have custom sections
|
|
|
|
const allPlugins = Object.keys(customSections);
|
|
|
|
|
|
|
|
// the list of customSections of the selected plugin (computed)
|
|
|
|
const customSectionIds = ko.pureComputed(() => {
|
|
|
|
const sections = customSections[this.viewModel.activeSection().customDef.pluginId()] || [];
|
|
|
|
return sections.map(({sectionId}) => sectionId);
|
|
|
|
});
|
|
|
|
|
|
|
|
return [{
|
|
|
|
|
|
|
|
// 1)
|
|
|
|
buildDom: () => kd.scope(activeSection, ({customDef}) => kf.buttonSelect(customDef.mode,
|
|
|
|
kf.optionButton('url', 'URL', dom.testId('ViewConfigTab_customView_url')),
|
|
|
|
kf.optionButton('plugin', 'Plugin', dom.testId('ViewConfigTab_customView_plugin'))))
|
|
|
|
}, {
|
|
|
|
|
|
|
|
// 2)
|
2021-11-26 10:43:55 +00:00
|
|
|
// TODO: refactor this part, Custom Widget moved to separate file.
|
2020-10-02 15:10:00 +00:00
|
|
|
}, {
|
|
|
|
|
|
|
|
// 3)
|
|
|
|
showObs: () => activeSection().customDef.mode() === "plugin",
|
|
|
|
buildDom: () => kd.scope(activeSection, ({customDef}) => dom('div',
|
2022-12-06 13:40:02 +00:00
|
|
|
kf.row(5, t("Plugin: "), 13, kf.text(customDef.pluginId, {}, {list: "list_plugin"}, dom.testId('ViewConfigTab_customView_pluginId'))),
|
|
|
|
kf.row(5, t("Section: "), 13, kf.text(customDef.sectionId, {}, {list: "list_section"}, dom.testId('ViewConfigTab_customView_sectionId'))),
|
2020-10-02 15:10:00 +00:00
|
|
|
// For both `customPlugin` and `selectedSection` it is possible for the value not to be in the
|
|
|
|
// list of options. Combining <datalist> and <input> allows both to freely edit the value with
|
|
|
|
// keyboard and to select it from a list. Although the content of the list seems to be
|
|
|
|
// filtered by the current value, which could confuse user into thinking that there are no
|
|
|
|
// available options. I think it would be better to have the full list always, but it seems
|
|
|
|
// harder to accomplish and is left as a TODO.
|
|
|
|
dom('datalist#list_plugin', kd.foreach(koArray(allPlugins), value => dom('option', {value}))),
|
|
|
|
dom('datalist#list_section', kd.scope(customSectionIds, sections => kd.foreach(koArray(sections), (value) => dom('option', {value}))))
|
|
|
|
))
|
|
|
|
}];
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = ViewConfigTab;
|