mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move client code to core
Summary: This moves all client code to core, and makes minimal fix-ups to get grist and grist-core to compile correctly. The client works in core, but I'm leaving clean-up around the build and bundles to follow-up. Test Plan: existing tests pass; server-dev bundle looks sane Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2627
This commit is contained in:
686
app/client/ui/RightPanel.ts
Normal file
686
app/client/ui/RightPanel.ts
Normal file
@@ -0,0 +1,686 @@
|
||||
/**
|
||||
* Builds the structure of the right-side panel containing configuration and assorted tools.
|
||||
* It includes the regular tabs, to configure the Page (including several sub-tabs), and Field;
|
||||
* and allows other tools, such as Activity Feed, to be rendered temporarily in its place.
|
||||
*
|
||||
* A single RightPanel object is created in AppUI for a document page, and attached to PagePanels.
|
||||
* GristDoc registers callbacks with it to create various standard tabs. These are created as
|
||||
* needed, and destroyed when hidden.
|
||||
*
|
||||
* In addition, tools such as "Activity Feed" may use openTool() to replace the panel header and
|
||||
* content. The user may dismiss this panel.
|
||||
*
|
||||
* All methods above return an object which may be disposed to close and dispose that specific
|
||||
* tab from the outside (e.g. when GristDoc is disposed).
|
||||
*/
|
||||
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import * as FieldConfigTab from 'app/client/components/FieldConfigTab';
|
||||
import {GristDoc, IExtraTool, TabContent} from 'app/client/components/GristDoc';
|
||||
import * as ViewConfigTab from 'app/client/components/ViewConfigTab';
|
||||
import * as imports from 'app/client/lib/imports';
|
||||
import {createSessionObs} from 'app/client/lib/sessionObs';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {GridOptions} from 'app/client/ui/GridOptions';
|
||||
import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
||||
import {linkFromId, linkId, selectBy} from 'app/client/ui/selectBy';
|
||||
import {VisibleFieldsConfig} from 'app/client/ui/VisibleFieldsConfig';
|
||||
import {IWidgetType, widgetTypes} from 'app/client/ui/widgetTypes';
|
||||
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
|
||||
import {textInput} from 'app/client/ui2018/editableLabel';
|
||||
import {IconName} from 'app/client/ui2018/IconList';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {select} from 'app/client/ui2018/menus';
|
||||
import {StringUnion} from 'app/common/StringUnion';
|
||||
import {bundleChanges, Computed, Disposable, dom, DomArg, domComputed, DomContents,
|
||||
DomElementArg, DomElementMethod, IDomComponent} from 'grainjs';
|
||||
import {MultiHolder, Observable, styled, subscribe} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Represents a top tab of the right side-pane.
|
||||
const TopTab = StringUnion("pageWidget", "field");
|
||||
|
||||
// Represents a subtab of pageWidget in the right side-pane.
|
||||
const PageSubTab = StringUnion("widget", "sortAndFilter", "data");
|
||||
|
||||
// A map of widget type to the icon and label to use for a field of that widget.
|
||||
const fieldTypes = new Map<IWidgetType, {label: string, icon: IconName, pluralLabel: string}>([
|
||||
['record', {label: 'Column', icon: 'TypeCell', pluralLabel: 'Columns'}],
|
||||
['detail', {label: 'Field', icon: 'TypeCell', pluralLabel: 'Fields'}],
|
||||
['single', {label: 'Field', icon: 'TypeCell', pluralLabel: 'Fields'}],
|
||||
['chart', {label: 'Series', icon: 'ChartLine', pluralLabel: 'Series'}],
|
||||
['custom', {label: 'Column', icon: 'TypeCell', pluralLabel: 'Columns'}],
|
||||
]);
|
||||
|
||||
// Returns the icon and label of a type, default to those associate to 'record' type.
|
||||
export function getFieldType(widgetType: IWidgetType|null) {
|
||||
return fieldTypes.get(widgetType || 'record') || fieldTypes.get('record')!;
|
||||
}
|
||||
|
||||
export class RightPanel extends Disposable {
|
||||
public readonly header: DomContents;
|
||||
public readonly content: DomContents;
|
||||
|
||||
// If the panel is showing a tool, such as Action Log, instead of the usual section/field
|
||||
// configuration, this will be set to the tool's header and content.
|
||||
private _extraTool: Observable<IExtraTool|null>;
|
||||
|
||||
// Which of the two standard top tabs (page widget or field) is selected, or was last selected.
|
||||
private _topTab = createSessionObs(this, "rightTopTab", "pageWidget", TopTab.guard);
|
||||
|
||||
// Which subtab is open for configuring page widget.
|
||||
private _subTab = createSessionObs(this, "rightPageSubTab", "widget", PageSubTab.guard);
|
||||
|
||||
// Which type of page widget is active, e.g. "record" or "chart". This affects the names and
|
||||
// icons in the top tab.
|
||||
private _pageWidgetType = Computed.create<IWidgetType|null>(this, (use) => {
|
||||
const section: ViewSectionRec = use(this._gristDoc.viewModel.activeSection);
|
||||
return (use(section.parentKey) || null) as IWidgetType;
|
||||
});
|
||||
|
||||
// Returns the active section if it's valid, null otherwise.
|
||||
private _validSection = Computed.create(this, (use) => {
|
||||
const sec = use(this._gristDoc.viewModel.activeSection);
|
||||
return sec.getRowId() ? sec : null;
|
||||
});
|
||||
|
||||
constructor(private _gristDoc: GristDoc, private _isOpen: Observable<boolean>) {
|
||||
super();
|
||||
this._extraTool = _gristDoc.rightPanelTool;
|
||||
this.autoDispose(subscribe(this._extraTool, (_use, tool) => tool && _isOpen.set(true)));
|
||||
this.header = this._buildHeaderDom();
|
||||
this.content = this._buildContentDom();
|
||||
|
||||
this.autoDispose(commands.createGroup({
|
||||
fieldTabOpen: () => this._openFieldTab(),
|
||||
viewTabOpen: () => this._openViewTab(),
|
||||
sortFilterTabOpen: () => this._openSortFilter(),
|
||||
dataSelectionTabOpen: () => this._openDataSelection()
|
||||
}, this, true));
|
||||
}
|
||||
|
||||
private _openFieldTab() {
|
||||
this._open('field');
|
||||
}
|
||||
|
||||
private _openViewTab() {
|
||||
this._open('pageWidget', 'widget');
|
||||
}
|
||||
|
||||
private _openSortFilter() {
|
||||
this._open('pageWidget', 'sortAndFilter');
|
||||
}
|
||||
|
||||
private _openDataSelection() {
|
||||
this._open('pageWidget', 'data');
|
||||
}
|
||||
|
||||
private _open(topTab: typeof TopTab.type, subTab?: typeof PageSubTab.type) {
|
||||
bundleChanges(() => {
|
||||
this._isOpen.set(true);
|
||||
this._topTab.set(topTab);
|
||||
if (subTab) {
|
||||
this._subTab.set(subTab);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _buildHeaderDom() {
|
||||
return dom.domComputed((use) => {
|
||||
if (!use(this._isOpen)) { return null; }
|
||||
const tool = use(this._extraTool);
|
||||
return tool ? this._buildToolHeader(tool) : this._buildStandardHeader();
|
||||
});
|
||||
}
|
||||
|
||||
private _buildToolHeader(tool: IExtraTool) {
|
||||
return cssTopBarItem(cssTopBarIcon(tool.icon), tool.label,
|
||||
cssHoverCircle(cssHoverIcon("CrossBig"),
|
||||
dom.on('click', () => this._gristDoc.showTool('none')),
|
||||
testId('right-tool-close'),
|
||||
),
|
||||
cssTopBarItem.cls('-selected', true)
|
||||
);
|
||||
}
|
||||
|
||||
private _buildStandardHeader() {
|
||||
return dom.maybe(this._pageWidgetType, (type) => {
|
||||
const widgetInfo = widgetTypes.get(type) || {label: 'Table', icon: 'TypeTable'};
|
||||
const fieldInfo = getFieldType(type);
|
||||
return [
|
||||
cssTopBarItem(cssTopBarIcon(widgetInfo.icon), widgetInfo.label,
|
||||
cssTopBarItem.cls('-selected', (use) => use(this._topTab) === 'pageWidget'),
|
||||
dom.on('click', () => this._topTab.set("pageWidget")),
|
||||
testId('right-tab-pagewidget')),
|
||||
cssTopBarItem(cssTopBarIcon(fieldInfo.icon), fieldInfo.label,
|
||||
cssTopBarItem.cls('-selected', (use) => use(this._topTab) === 'field'),
|
||||
dom.on('click', () => this._topTab.set("field")),
|
||||
testId('right-tab-field')),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private _buildContentDom() {
|
||||
return dom.domComputed((use) => {
|
||||
if (!use(this._isOpen)) { return null; }
|
||||
const tool = use(this._extraTool);
|
||||
if (tool) { return tabContentToDom(tool.content); }
|
||||
|
||||
const topTab = use(this._topTab);
|
||||
if (topTab === 'field') {
|
||||
return dom.create(this._buildFieldContent.bind(this));
|
||||
}
|
||||
if (topTab === 'pageWidget' && use(this._pageWidgetType)) {
|
||||
return dom.create(this._buildPageWidgetContent.bind(this));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private _buildFieldContent(owner: MultiHolder) {
|
||||
const obs: Observable<null|FieldConfigTab> = Observable.create(owner, null);
|
||||
const fieldBuilder = this.autoDispose(ko.computed(() => {
|
||||
const vsi = this._gristDoc.viewModel.activeSection().viewInstance();
|
||||
return vsi && vsi.activeFieldBuilder();
|
||||
}));
|
||||
const gristDoc = this._gristDoc;
|
||||
const content = Observable.create<TabContent[]>(owner, []);
|
||||
const contentCallback = (tabs: TabContent[]) => content.set(tabs);
|
||||
imports.loadViewPane()
|
||||
.then(ViewPane => {
|
||||
if (owner.isDisposed()) { return; }
|
||||
const fct = owner.autoDispose(ViewPane.FieldConfigTab.create({gristDoc, fieldBuilder, contentCallback}));
|
||||
obs.set(fct);
|
||||
})
|
||||
.catch(reportError);
|
||||
return dom.maybe(obs, (fct) =>
|
||||
buildConfigContainer(
|
||||
cssLabel('COLUMN TITLE'),
|
||||
fct._buildNameDom(),
|
||||
fct._buildFormulaDom(),
|
||||
cssSeparator(),
|
||||
cssLabel('COLUMN TYPE'),
|
||||
fct._buildFormatDom(),
|
||||
cssSeparator(),
|
||||
dom.maybe(fct.isForeignRefCol, () => [
|
||||
cssLabel('Add referenced columns'),
|
||||
cssRow(fct.refSelect.buildDom()),
|
||||
cssSeparator()
|
||||
]),
|
||||
cssLabel('TRANSFORM'),
|
||||
fct._buildTransformDom(),
|
||||
this._disableIfReadonly(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _buildPageWidgetContent(_owner: MultiHolder) {
|
||||
return [
|
||||
cssSubTabContainer(
|
||||
cssSubTab('Widget',
|
||||
cssSubTab.cls('-selected', (use) => use(this._subTab) === 'widget'),
|
||||
dom.on('click', () => this._subTab.set("widget")),
|
||||
testId('config-widget')),
|
||||
cssSubTab('Sort & Filter',
|
||||
cssSubTab.cls('-selected', (use) => use(this._subTab) === 'sortAndFilter'),
|
||||
dom.on('click', () => this._subTab.set("sortAndFilter")),
|
||||
testId('config-sortAndFilter')),
|
||||
cssSubTab('Data',
|
||||
cssSubTab.cls('-selected', (use) => use(this._subTab) === 'data'),
|
||||
dom.on('click', () => this._subTab.set("data")),
|
||||
testId('config-data')),
|
||||
),
|
||||
dom.domComputed(this._subTab, (subTab) => (
|
||||
dom.maybe(this._validSection, (activeSection) => (
|
||||
buildConfigContainer(
|
||||
subTab === 'widget' ? dom.create(this._buildPageWidgetConfig.bind(this), activeSection) :
|
||||
subTab === 'sortAndFilter' ? dom.create(this._buildPageSortFilterConfig.bind(this)) :
|
||||
subTab === 'data' ? dom.create(this._buildPageDataConfig.bind(this), activeSection) :
|
||||
null
|
||||
)
|
||||
))
|
||||
))
|
||||
];
|
||||
}
|
||||
|
||||
private _createViewConfigTab(owner: MultiHolder): Observable<null|ViewConfigTab> {
|
||||
const viewConfigTab = Observable.create<null|ViewConfigTab>(owner, null);
|
||||
const gristDoc = this._gristDoc;
|
||||
imports.loadViewPane()
|
||||
.then(ViewPane => {
|
||||
if (owner.isDisposed()) { return; }
|
||||
viewConfigTab.set(owner.autoDispose(
|
||||
ViewPane.ViewConfigTab.create({gristDoc, viewModel: gristDoc.viewModel, skipDomBuild: true})));
|
||||
})
|
||||
.catch(reportError);
|
||||
return viewConfigTab;
|
||||
}
|
||||
|
||||
private _buildPageWidgetConfig(owner: MultiHolder, activeSection: ViewSectionRec) {
|
||||
// TODO: This uses private methods from ViewConfigTab. These methods are likely to get
|
||||
// refactored, but if not, should be made public.
|
||||
const viewConfigTab = this._createViewConfigTab(owner);
|
||||
return dom.maybe(viewConfigTab, (vct) => [
|
||||
this._disableIfReadonly(),
|
||||
cssLabel('WIDGET TITLE',
|
||||
dom.style('margin-bottom', '14px')),
|
||||
cssRow(cssTextInput(
|
||||
Computed.create(owner, (use) => use(activeSection.titleDef)),
|
||||
val => activeSection.titleDef.saveOnly(val),
|
||||
testId('right-widget-title')
|
||||
)),
|
||||
|
||||
cssRow(primaryButton('Change Widget', this._createPageWidgetPicker()),
|
||||
cssRow.cls('-top-space')),
|
||||
cssSeparator(),
|
||||
|
||||
dom.maybe((use) => ['detail', 'single'].includes(use(this._pageWidgetType)!), () => [
|
||||
cssLabel('Theme'),
|
||||
dom('div',
|
||||
vct._buildThemeDom(),
|
||||
vct._buildLayoutDom())
|
||||
]),
|
||||
|
||||
domComputed((use) => {
|
||||
if (use(this._pageWidgetType) !== 'record') { return null; }
|
||||
return dom.create(GridOptions, activeSection);
|
||||
}),
|
||||
|
||||
dom.maybe((use) => use(this._pageWidgetType) === 'chart', () => [
|
||||
cssLabel('CHART TYPE'),
|
||||
vct._buildChartConfigDom(),
|
||||
]),
|
||||
|
||||
dom.maybe((use) => use(this._pageWidgetType) === 'custom', () => [
|
||||
cssLabel('CUSTOM'),
|
||||
() => {
|
||||
const parts = vct._buildCustomTypeItems() as any[];
|
||||
return [
|
||||
// If 'customViewPlugin' feature is on, show the toggle that allows switching to
|
||||
// plugin mode. Note that the default mode for a new 'custom' view is 'url', so that's
|
||||
// the only one that will be shown without the feature flag.
|
||||
dom.maybe((use) => use(this._gristDoc.app.features).customViewPlugin,
|
||||
() => dom('div', parts[0].buildDom())),
|
||||
dom.maybe(use => use(activeSection.customDef.mode) === 'plugin',
|
||||
() => dom('div', parts[2].buildDom())),
|
||||
// In the default url mode, allow picking a url and granting/forbidding
|
||||
// access to data.
|
||||
dom.maybe(use => use(activeSection.customDef.mode) === 'url',
|
||||
() => dom('div', parts[1].buildDom())),
|
||||
];
|
||||
}
|
||||
]),
|
||||
|
||||
cssSeparator(),
|
||||
dom.create(VisibleFieldsConfig, this._gristDoc, activeSection, true)
|
||||
]);
|
||||
}
|
||||
|
||||
private _buildPageSortFilterConfig(owner: MultiHolder) {
|
||||
const viewConfigTab = this._createViewConfigTab(owner);
|
||||
return [
|
||||
cssLabel('SORT'),
|
||||
dom.maybe(viewConfigTab, (vct) => vct.buildSortDom()),
|
||||
cssSeparator(),
|
||||
|
||||
cssLabel('FILTER'),
|
||||
dom.maybe(viewConfigTab, (vct) => dom('div', vct._buildFilterDom())),
|
||||
];
|
||||
}
|
||||
|
||||
private _buildPageDataConfig(owner: MultiHolder, activeSection: ViewSectionRec) {
|
||||
const viewConfigTab = this._createViewConfigTab(owner);
|
||||
const viewModel = this._gristDoc.viewModel;
|
||||
const table = activeSection.table;
|
||||
const groupedBy = Computed.create(owner, (use) => use(use(table).groupByColumns));
|
||||
const link = Computed.create(owner, (use) => {
|
||||
return linkId({
|
||||
srcSectionRef: use(activeSection.linkSrcSectionRef),
|
||||
srcColRef: use(activeSection.linkSrcColRef),
|
||||
targetColRef: use(activeSection.linkTargetColRef)
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: this computed is not enough to make sure that the linkOptions are up to date. Indeed
|
||||
// the selectBy function depends on a much greater number of observables. Creating that many
|
||||
// dependencies does not seem a better approach. Instead, we could refresh the list of
|
||||
// linkOptions only when the user clicks the dropdown. Such behaviour is not supported by the
|
||||
// weasel select function as of writing and would require a custom implementation.
|
||||
const linkOptions = Computed.create(owner, (use) =>
|
||||
selectBy(
|
||||
this._gristDoc.docModel,
|
||||
use(viewModel.viewSections).peek(),
|
||||
activeSection,
|
||||
)
|
||||
);
|
||||
|
||||
link.onWrite((val) => this._gristDoc.saveLink(linkFromId(val)));
|
||||
return [
|
||||
this._disableIfReadonly(),
|
||||
cssLabel('DATA TABLE'),
|
||||
cssRow(
|
||||
cssIcon('TypeTable'), cssDataLabel('SOURCE DATA'),
|
||||
cssContent(dom.text((use) => use(use(table).primaryTableId)),
|
||||
testId('pwc-table'))
|
||||
),
|
||||
dom(
|
||||
'div',
|
||||
cssRow(cssIcon('Pivot'), cssDataLabel('GROUPED BY')),
|
||||
cssRow(domComputed(groupedBy, (cols) => cssList(cols.map((c) => (
|
||||
cssListItem(dom.text(c.label),
|
||||
testId('pwc-groupedBy-col'))
|
||||
))))),
|
||||
|
||||
testId('pwc-groupedBy'),
|
||||
// hide if not a summary table
|
||||
dom.hide((use) => !use(use(table).summarySourceTable)),
|
||||
),
|
||||
|
||||
cssButtonRow(primaryButton('Edit Data Selection', this._createPageWidgetPicker(),
|
||||
testId('pwc-editDataSelection')),
|
||||
dom.maybe(
|
||||
use => Boolean(use(use(activeSection.table).summarySourceTable)),
|
||||
() => basicButton(
|
||||
'Detach',
|
||||
dom.on('click', () => this._gristDoc.docData.sendAction(
|
||||
["DetachSummaryViewSection", activeSection.getRowId()])),
|
||||
testId('detach-button'),
|
||||
)),
|
||||
cssRow.cls('-top-space'),
|
||||
),
|
||||
|
||||
// TODO: "Advanced settings" is for "on-demand" marking of tables. This should only be shown
|
||||
// for raw data tables (once that's supported), should have updated UI, and should possibly
|
||||
// be hidden for free plans.
|
||||
dom.maybe(viewConfigTab, (vct) => cssRow(
|
||||
dom('div', vct._buildAdvancedSettingsDom()),
|
||||
)),
|
||||
cssSeparator(),
|
||||
|
||||
cssLabel('SELECT BY'),
|
||||
cssRow(
|
||||
select(link, linkOptions, {defaultLabel: 'Select Widget'}),
|
||||
testId('right-select-by')
|
||||
),
|
||||
|
||||
domComputed((use) => {
|
||||
const activeSectionRef = activeSection.getRowId();
|
||||
const allViewSections = use(use(viewModel.viewSections).getObservable());
|
||||
const selectorFor = allViewSections.filter((sec) => use(sec.linkSrcSectionRef) === activeSectionRef);
|
||||
// TODO: sections should be listed following the order of appearance in the view layout (ie:
|
||||
// left/right - top/bottom);
|
||||
return selectorFor.length ? [
|
||||
cssLabel('SELECTOR FOR', testId('selector-for')),
|
||||
cssRow(cssList(selectorFor.map((sec) => this._buildSectionItem(sec))))
|
||||
] : null;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _createPageWidgetPicker(): DomElementMethod {
|
||||
const gristDoc = this._gristDoc;
|
||||
const section = gristDoc.viewModel.activeSection;
|
||||
const onSave = (val: IPageWidget) => gristDoc.saveViewSection(section.peek(), val);
|
||||
return (elem) => { attachPageWidgetPicker(elem, gristDoc.docModel, onSave, {
|
||||
buttonLabel: 'Save',
|
||||
value: () => toPageWidget(section.peek()),
|
||||
selectBy: (val) => gristDoc.selectBy(val),
|
||||
}); };
|
||||
}
|
||||
|
||||
// Returns dom for a section item.
|
||||
private _buildSectionItem(sec: ViewSectionRec) {
|
||||
return cssListItem(
|
||||
dom.text(sec.titleDef),
|
||||
testId('selector-for-entry')
|
||||
);
|
||||
}
|
||||
|
||||
// Returns a DomArg that disables the content of the panel by adding a transparent overlay on top
|
||||
// of it.
|
||||
private _disableIfReadonly() {
|
||||
if (this._gristDoc.docPageModel) {
|
||||
return dom.maybe(this._gristDoc.docPageModel.isReadonly, () => (
|
||||
cssOverlay(
|
||||
testId('disable-overlay'),
|
||||
cssBottomText('You do not have edit access to this document'),
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildConfigContainer(...args: DomElementArg[]): DomArg {
|
||||
return cssConfigContainer(
|
||||
// The `position: relative;` style is needed for the overlay for the readonly mode. Note that
|
||||
// we cannot set it on the cssConfigContainer directly because it conflicts with how overflow
|
||||
// works. `padding-top: 1px;` prevents collapsing the top margins for the container and the
|
||||
// first child.
|
||||
dom('div', {style: 'position: relative; padding-top: 1px;'}, ...args),
|
||||
);
|
||||
}
|
||||
|
||||
// This logic is copied from SidePane.js for building DOM from TabContent.
|
||||
// TODO It may not be needed after new-ui refactoring of the side-pane content.
|
||||
function tabContentToDom(content: Observable<TabContent[]>|TabContent[]|IDomComponent) {
|
||||
function buildItemDom(item: any) {
|
||||
return dom('div.config_item',
|
||||
dom.show(item.showObs || true),
|
||||
item.buildDom()
|
||||
);
|
||||
}
|
||||
|
||||
if ("buildDom" in content) {
|
||||
return content.buildDom();
|
||||
}
|
||||
|
||||
return cssTabContents(
|
||||
dom.forEach(content, itemOrHeader => {
|
||||
if (itemOrHeader.header) {
|
||||
return dom('div.config_group',
|
||||
dom.show(itemOrHeader.showObs || true),
|
||||
itemOrHeader.label ? dom('div.config_header', itemOrHeader.label) : null,
|
||||
dom.forEach(itemOrHeader.items, item => buildItemDom(item)),
|
||||
);
|
||||
} else {
|
||||
return buildItemDom(itemOrHeader);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const cssOverlay = styled('div', `
|
||||
background-color: white;
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
`);
|
||||
|
||||
const cssBottomText = styled('span', `
|
||||
position: absolute;
|
||||
bottom: -40px;
|
||||
padding: 4px 16px;
|
||||
`);
|
||||
|
||||
export const cssLabel = styled('div', `
|
||||
text-transform: uppercase;
|
||||
margin: 16px 16px 12px 16px;
|
||||
font-size: ${vars.xsmallFontSize};
|
||||
`);
|
||||
|
||||
export const cssRow = styled('div', `
|
||||
display: flex;
|
||||
margin: 8px 16px;
|
||||
align-items: center;
|
||||
&-top-space {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssButtonRow = styled(cssRow, `
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
& > button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssIcon = styled(icon, `
|
||||
flex: 0 0 auto;
|
||||
background-color: ${colors.slate};
|
||||
`);
|
||||
|
||||
const cssTopBarItem = styled('div', `
|
||||
flex: 1 1 0px;
|
||||
height: 100%;
|
||||
background-color: ${colors.lightGrey};
|
||||
font-weight: ${vars.headerControlTextWeight};
|
||||
color: ${colors.dark};
|
||||
--icon-color: ${colors.slate};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
|
||||
&-selected {
|
||||
background-color: ${colors.lightGreen};
|
||||
font-weight: initial;
|
||||
color: ${colors.light};
|
||||
--icon-color: ${colors.light};
|
||||
}
|
||||
&:not(&-selected):hover {
|
||||
background-color: ${colors.mediumGrey};
|
||||
--icon-color: ${colors.lightGreen};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssTopBarIcon = styled(icon, `
|
||||
flex: none;
|
||||
margin: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
background-color: vars(--icon-color);
|
||||
`);
|
||||
|
||||
const cssHoverCircle = styled('div', `
|
||||
margin-left: auto;
|
||||
margin-right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: none;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background-color: ${colors.darkGreen};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssHoverIcon = styled(icon, `
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
background-color: vars(--icon-color);
|
||||
`);
|
||||
|
||||
export const cssSubTabContainer = styled('div', `
|
||||
height: 48px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`);
|
||||
|
||||
export const cssSubTab = styled('div', `
|
||||
color: ${colors.lightGreen};
|
||||
flex: auto;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
text-align: center;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid ${colors.mediumGrey};
|
||||
cursor: default;
|
||||
|
||||
&-selected {
|
||||
color: ${colors.dark};
|
||||
border-bottom: 1px solid ${colors.lightGreen};
|
||||
}
|
||||
&:not(&-selected):hover {
|
||||
color: ${colors.darkGreen};
|
||||
}
|
||||
&:hover {
|
||||
border-bottom: 1px solid ${colors.lightGreen};
|
||||
}
|
||||
.${cssSubTabContainer.className}:hover > &-selected:not(:hover) {
|
||||
border-bottom: 1px solid ${colors.mediumGrey};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssTabContents = styled('div', `
|
||||
padding: 16px 8px;
|
||||
overflow: auto;
|
||||
`);
|
||||
|
||||
const cssSeparator = styled('div', `
|
||||
border-bottom: 1px solid ${colors.mediumGrey};
|
||||
margin-top: 24px;
|
||||
`);
|
||||
|
||||
const cssConfigContainer = styled('div', `
|
||||
overflow: auto;
|
||||
--color-list-item: none;
|
||||
--color-list-item-hover: none;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 40px;
|
||||
}
|
||||
& .fieldbuilder_settings {
|
||||
margin: 16px 0 0 0;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssDataLabel = styled('div', `
|
||||
flex: 0 0 81px;
|
||||
color: ${colors.slate};
|
||||
font-size: ${vars.xsmallFontSize};
|
||||
margin-left: 4px;
|
||||
margin-top: 2px;
|
||||
`);
|
||||
|
||||
const cssContent = styled('div', `
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 1em;
|
||||
`);
|
||||
|
||||
const cssList = styled('div', `
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
`);
|
||||
|
||||
|
||||
const cssListItem = styled('li', `
|
||||
background-color: ${colors.mediumGrey};
|
||||
border-radius: 2px;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
`);
|
||||
|
||||
const cssTextInput = styled(textInput, `
|
||||
flex: 1 0 auto;
|
||||
`);
|
||||
Reference in New Issue
Block a user