2022-11-17 20:17:51 +00:00
|
|
|
import {GristDoc} from 'app/client/components/GristDoc';
|
2022-10-28 16:11:08 +00:00
|
|
|
import {makeT} from 'app/client/lib/localization';
|
2022-02-07 14:02:26 +00:00
|
|
|
import {reportError} from 'app/client/models/AppModel';
|
2022-11-17 20:17:51 +00:00
|
|
|
import {DocModel, ViewSectionRec} from 'app/client/models/DocModel';
|
|
|
|
import {FilterConfig} from 'app/client/ui/FilterConfig';
|
|
|
|
import {cssLabel, cssSaveButtonsRow} from 'app/client/ui/RightPanelStyles';
|
2022-02-07 14:02:26 +00:00
|
|
|
import {hoverTooltip} from 'app/client/ui/tooltips';
|
2022-11-17 20:17:51 +00:00
|
|
|
import {SortConfig} from 'app/client/ui/SortConfig';
|
2022-02-07 14:02:26 +00:00
|
|
|
import {makeViewLayoutMenu} from 'app/client/ui/ViewLayoutMenu';
|
|
|
|
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
2022-09-06 01:51:57 +00:00
|
|
|
import {theme, vars} from 'app/client/ui2018/cssVars';
|
2022-02-07 14:02:26 +00:00
|
|
|
import {icon} from 'app/client/ui2018/icons';
|
|
|
|
import {menu} from 'app/client/ui2018/menus';
|
2022-11-17 20:17:51 +00:00
|
|
|
import {Computed, dom, IDisposableOwner, makeTestId, styled} from 'grainjs';
|
|
|
|
import {defaultMenuOptions} from 'popweasel';
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
const testId = makeTestId('test-section-menu-');
|
2022-10-28 16:11:08 +00:00
|
|
|
const t = makeT('ViewSectionMenu');
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2022-01-12 13:30:51 +00:00
|
|
|
// Handler for [Save] button.
|
2021-04-30 17:28:52 +00:00
|
|
|
async function doSave(docModel: DocModel, viewSection: ViewSectionRec): Promise<void> {
|
2022-10-28 16:11:08 +00:00
|
|
|
await docModel.docData.bundleActions(t("UpdateSortFilterSettings"), () => Promise.all([
|
2022-01-12 13:30:51 +00:00
|
|
|
viewSection.activeSortJson.save(), // Save sort
|
|
|
|
viewSection.saveFilters(), // Save filter
|
|
|
|
viewSection.activeCustomOptions.save(), // Save widget options
|
2021-04-30 17:28:52 +00:00
|
|
|
]));
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2022-01-12 13:30:51 +00:00
|
|
|
// Handler for [Revert] button.
|
2021-04-30 17:28:52 +00:00
|
|
|
function doRevert(viewSection: ViewSectionRec) {
|
2022-01-12 13:30:51 +00:00
|
|
|
viewSection.activeSortJson.revert(); // Revert sort
|
|
|
|
viewSection.revertFilters(); // Revert filter
|
|
|
|
viewSection.activeCustomOptions.revert(); // Revert widget options
|
2021-04-30 17:28:52 +00:00
|
|
|
}
|
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
// [Filter Icon] - Filter toggle and all the components in the menu.
|
|
|
|
export function viewSectionMenu(
|
|
|
|
owner: IDisposableOwner,
|
|
|
|
gristDoc: GristDoc,
|
|
|
|
viewSection: ViewSectionRec,
|
|
|
|
) {
|
|
|
|
const {docModel, isReadonly} = gristDoc;
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
// If there is any filter (should [Filter Icon] background be filled).
|
|
|
|
const anyFilter = Computed.create(owner, (use) => Boolean(use(viewSection.activeFilters).length));
|
2022-01-12 13:30:51 +00:00
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
// Should we show [Save] [Revert] buttons.
|
2021-04-30 17:28:52 +00:00
|
|
|
const displaySaveObs: Computed<boolean> = Computed.create(owner, (use) => (
|
|
|
|
use(viewSection.filterSpecChanged)
|
|
|
|
|| !use(viewSection.activeSortJson.isSaved)
|
2022-01-12 13:30:51 +00:00
|
|
|
|| !use(viewSection.activeCustomOptions.isSaved)
|
2021-04-30 17:28:52 +00:00
|
|
|
));
|
|
|
|
|
2021-05-06 12:23:50 +00:00
|
|
|
const save = () => { doSave(docModel, viewSection).catch(reportError); };
|
2021-04-30 17:28:52 +00:00
|
|
|
const revert = () => doRevert(viewSection);
|
|
|
|
|
|
|
|
return [
|
|
|
|
cssFilterMenuWrapper(
|
|
|
|
cssFixHeight.cls(''),
|
|
|
|
cssFilterMenuWrapper.cls('-unsaved', displaySaveObs),
|
|
|
|
testId('wrapper'),
|
|
|
|
cssMenu(
|
|
|
|
testId('sortAndFilter'),
|
2022-11-17 20:17:51 +00:00
|
|
|
// [Filter icon]
|
2021-04-30 17:28:52 +00:00
|
|
|
cssFilterIconWrapper(
|
|
|
|
testId('filter-icon'),
|
2022-11-17 20:17:51 +00:00
|
|
|
// Fill background when there are some filters. Ignore sort options.
|
2021-04-30 17:28:52 +00:00
|
|
|
cssFilterIconWrapper.cls('-any', anyFilter),
|
2022-10-19 23:06:05 +00:00
|
|
|
cssFilterIcon('Filter'),
|
|
|
|
hoverTooltip('Sort and filter', {key: 'sortFilterBtnTooltip'}),
|
2021-04-30 17:28:52 +00:00
|
|
|
),
|
|
|
|
),
|
2022-11-17 20:17:51 +00:00
|
|
|
// [Save] [Revert] buttons when there are unsaved options.
|
|
|
|
dom.maybe(displaySaveObs, () => cssSectionSaveButtonsWrapper(
|
|
|
|
cssSaveTextButton(
|
|
|
|
t('Save'),
|
|
|
|
cssSaveTextButton.cls('-accent'),
|
2021-04-30 17:28:52 +00:00
|
|
|
dom.on('click', save),
|
2022-10-19 23:06:05 +00:00
|
|
|
hoverTooltip('Save sort & filter settings', {key: 'sortFilterBtnTooltip'}),
|
2021-04-30 17:28:52 +00:00
|
|
|
testId('small-btn-save'),
|
2021-05-05 15:15:15 +00:00
|
|
|
dom.hide(isReadonly),
|
2021-04-30 17:28:52 +00:00
|
|
|
),
|
2022-11-17 20:17:51 +00:00
|
|
|
cssRevertIconButton(
|
|
|
|
cssRevertIcon('Revert', cssRevertIcon.cls('-normal')),
|
2021-04-30 17:28:52 +00:00
|
|
|
dom.on('click', revert),
|
2022-10-19 23:06:05 +00:00
|
|
|
hoverTooltip('Revert sort & filter settings', {key: 'sortFilterBtnTooltip'}),
|
2021-04-30 17:28:52 +00:00
|
|
|
testId('small-btn-revert'),
|
|
|
|
),
|
|
|
|
)),
|
2022-11-17 20:17:51 +00:00
|
|
|
menu(ctl => [
|
|
|
|
// Sort section.
|
|
|
|
makeSortPanel(viewSection, gristDoc),
|
|
|
|
// Filter section.
|
|
|
|
makeFilterPanel(viewSection),
|
|
|
|
// Widget options
|
|
|
|
dom.maybe(use => use(viewSection.parentKey) === 'custom', () =>
|
|
|
|
makeCustomOptions(viewSection)
|
|
|
|
),
|
|
|
|
// [Save] [Revert] buttons
|
|
|
|
dom.domComputed(displaySaveObs, displaySave => [
|
|
|
|
displaySave ? cssSaveButtonsRow(
|
|
|
|
cssSaveButton(t('Save'), testId('btn-save'),
|
|
|
|
dom.on('click', () => { ctl.close(); save(); }),
|
|
|
|
dom.boolAttr('disabled', isReadonly)),
|
|
|
|
basicButton(t('Revert'), testId('btn-revert'),
|
|
|
|
dom.on('click', () => { ctl.close(); revert(); }))
|
|
|
|
) : null,
|
|
|
|
]),
|
|
|
|
// Updates to active sort or filters can cause menu contents to grow, while
|
|
|
|
// leaving the position of the popup unchanged. This can sometimes lead to
|
|
|
|
// the menu growing beyond the boundaries of the viewport. To mitigate this,
|
|
|
|
// we subscribe to changes to the sort/filters and manually update the popup's
|
|
|
|
// position, which will re-position the popup if necessary so that it's fully
|
|
|
|
// visible.
|
|
|
|
dom.autoDispose(viewSection.activeFilters.addListener(() => ctl.update())),
|
|
|
|
dom.autoDispose(viewSection.activeSortJson.subscribe(() => ctl.update())),
|
|
|
|
], {...defaultMenuOptions, placement: 'bottom-end', trigger: [
|
|
|
|
// Toggle the menu whenever the filter icon button is clicked.
|
|
|
|
(el, ctl) => dom.onMatchElem(el, '.test-section-menu-sortAndFilter', 'click', () => {
|
|
|
|
ctl.toggle();
|
|
|
|
}),
|
|
|
|
// Close the menu whenever the save or revert button is clicked.
|
|
|
|
(el, ctl) => dom.onMatchElem(el, '.test-section-menu-small-btn-save', 'click', () => {
|
|
|
|
ctl.close();
|
|
|
|
}),
|
|
|
|
(el, ctl) => dom.onMatchElem(el, '.test-section-menu-small-btn-revert', 'click', () => {
|
|
|
|
ctl.close();
|
|
|
|
}),
|
|
|
|
]}),
|
2021-04-30 17:28:52 +00:00
|
|
|
),
|
|
|
|
cssMenu(
|
|
|
|
testId('viewLayout'),
|
|
|
|
cssFixHeight.cls(''),
|
|
|
|
cssDotsIconWrapper(cssIcon('Dots')),
|
2022-11-17 20:17:51 +00:00
|
|
|
menu(_ctl => makeViewLayoutMenu(viewSection, isReadonly.get()), {
|
|
|
|
...defaultMenuOptions,
|
|
|
|
placement: 'bottom-end',
|
|
|
|
})
|
2021-04-30 17:28:52 +00:00
|
|
|
)
|
|
|
|
];
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
function makeSortPanel(section: ViewSectionRec, gristDoc: GristDoc) {
|
2020-10-02 15:10:00 +00:00
|
|
|
return [
|
2022-11-17 20:17:51 +00:00
|
|
|
cssLabel(t('Sort'), testId('heading-sort')),
|
|
|
|
dom.create(SortConfig, section, gristDoc, {
|
|
|
|
// Attach content to triggerElem's parent, which is needed to prevent view
|
|
|
|
// section menu to close when clicking an item in the advanced sort menu.
|
|
|
|
menuOptions: {attach: null},
|
|
|
|
}),
|
2020-10-02 15:10:00 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
function makeFilterPanel(section: ViewSectionRec) {
|
2020-10-02 15:10:00 +00:00
|
|
|
return [
|
2022-11-17 20:17:51 +00:00
|
|
|
cssLabel(t('Filter'), testId('heading-filter')),
|
|
|
|
dom.create(FilterConfig, section, {
|
|
|
|
// Attach content to triggerElem's parent, which is needed to prevent view
|
|
|
|
// section menu to close when clicking an item of the add filter menu.
|
|
|
|
menuOptions: {attach: null},
|
|
|
|
}),
|
2020-10-02 15:10:00 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2022-01-12 13:30:51 +00:00
|
|
|
// Custom Options
|
|
|
|
// (empty)|(customized)|(modified) [Remove Icon]
|
|
|
|
function makeCustomOptions(section: ViewSectionRec) {
|
2022-11-17 20:17:51 +00:00
|
|
|
const color = Computed.create(null, use => use(section.activeCustomOptions.isSaved) ? "-normal" : "-accent");
|
2022-01-12 13:30:51 +00:00
|
|
|
const text = Computed.create(null, use => {
|
|
|
|
if (use(section.activeCustomOptions)) {
|
2022-10-28 16:11:08 +00:00
|
|
|
return use(section.activeCustomOptions.isSaved) ? t("Customized") : t("Modified");
|
2022-01-12 13:30:51 +00:00
|
|
|
} else {
|
2022-10-28 16:11:08 +00:00
|
|
|
return t("Empty");
|
2022-01-12 13:30:51 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return [
|
2022-10-28 16:11:08 +00:00
|
|
|
cssMenuInfoHeader(t('CustomOptions'), testId('heading-widget-options')),
|
2022-01-12 13:30:51 +00:00
|
|
|
cssMenuText(
|
|
|
|
dom.autoDispose(text),
|
|
|
|
dom.autoDispose(color),
|
|
|
|
dom.text(text),
|
|
|
|
cssMenuText.cls(color),
|
|
|
|
cssSpacer(),
|
|
|
|
dom.maybe(use => use(section.activeCustomOptions), () =>
|
|
|
|
cssMenuIconWrapper(
|
|
|
|
cssIcon('Remove', testId('btn-remove-options'), dom.on('click', () =>
|
|
|
|
section.activeCustomOptions(null)
|
|
|
|
))
|
|
|
|
),
|
|
|
|
),
|
|
|
|
testId("custom-options")
|
|
|
|
)
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
const clsOldUI = styled('div', ``);
|
|
|
|
|
2021-04-30 17:28:52 +00:00
|
|
|
const cssFixHeight = styled('div', `
|
2020-10-02 15:10:00 +00:00
|
|
|
margin-top: -3px; /* Section header is 24px, so need to move this up a little bit */
|
2021-04-30 17:28:52 +00:00
|
|
|
`);
|
|
|
|
|
|
|
|
const cssMenu = styled('div', `
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
border: 1px solid transparent;
|
|
|
|
&.${clsOldUI.className} {
|
|
|
|
margin-top: 0px;
|
|
|
|
border-radius: 0px;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:hover, &.weasel-popup-open {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.hover};
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssIconWrapper = styled('div', `
|
|
|
|
padding: 3px;
|
|
|
|
border-radius: 3px;
|
|
|
|
cursor: pointer;
|
|
|
|
user-select: none;
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssMenuIconWrapper = styled(cssIconWrapper, `
|
2021-04-14 15:17:45 +00:00
|
|
|
display: flex;
|
2020-10-02 15:10:00 +00:00
|
|
|
margin: -3px 0;
|
|
|
|
width: 22px;
|
|
|
|
height: 22px;
|
|
|
|
|
|
|
|
&:hover, &.weasel-popup-open {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.hover};
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
&-changed {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.accentIcon};
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
&-changed:hover, &-changed:hover.weasel-popup-open {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.controlHoverFg};
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
2021-04-30 17:28:52 +00:00
|
|
|
const cssFilterMenuWrapper = styled('div', `
|
|
|
|
display: inline-flex;
|
|
|
|
margin-right: 10px;
|
|
|
|
border-radius: 3px;
|
|
|
|
align-items: center;
|
|
|
|
&-unsaved {
|
2022-09-06 01:51:57 +00:00
|
|
|
border: 1px solid ${theme.accentBorder};
|
2021-04-30 17:28:52 +00:00
|
|
|
}
|
|
|
|
& .${cssMenu.className} {
|
|
|
|
border: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
`);
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
const cssIcon = styled(icon, `
|
|
|
|
flex: none;
|
|
|
|
cursor: pointer;
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.lightText};
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
.${cssMenuIconWrapper.className}-changed & {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.controlPrimaryFg};
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.${clsOldUI.className} & {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.controlPrimaryFg};
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
2021-04-14 15:17:45 +00:00
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
&-accent {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.accentIcon};
|
2021-04-14 15:17:45 +00:00
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
`);
|
|
|
|
|
|
|
|
const cssDotsIconWrapper = styled(cssIconWrapper, `
|
|
|
|
border-radius: 0px 2px 2px 0px;
|
|
|
|
|
|
|
|
.${clsOldUI.className} & {
|
|
|
|
border-radius: 0px;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssFilterIconWrapper = styled(cssIconWrapper, `
|
|
|
|
border-radius: 2px 0px 0px 2px;
|
2022-11-17 20:17:51 +00:00
|
|
|
&-any {
|
|
|
|
border-radius: 2px;
|
|
|
|
background-color: ${theme.controlSecondaryFg};
|
|
|
|
}
|
2021-04-30 17:28:52 +00:00
|
|
|
.${cssFilterMenuWrapper.className}-unsaved & {
|
2022-11-17 20:17:51 +00:00
|
|
|
background-color: ${theme.controlPrimaryBg};
|
2021-04-30 17:28:52 +00:00
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
`);
|
|
|
|
|
|
|
|
const cssFilterIcon = styled(cssIcon, `
|
2021-04-30 17:28:52 +00:00
|
|
|
.${cssFilterIconWrapper.className}-any & {
|
2022-11-17 20:17:51 +00:00
|
|
|
background-color: ${theme.controlPrimaryFg};
|
2021-04-30 17:28:52 +00:00
|
|
|
}
|
|
|
|
.${cssFilterMenuWrapper.className}-unsaved & {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.controlPrimaryFg};
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssMenuInfoHeader = styled('div', `
|
2022-09-06 01:51:57 +00:00
|
|
|
color: ${theme.menuSubheaderFg};
|
2020-10-02 15:10:00 +00:00
|
|
|
font-weight: ${vars.bigControlTextWeight};
|
|
|
|
padding: 8px 24px 8px 24px;
|
|
|
|
cursor: default;
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssMenuText = styled('div', `
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
padding: 0px 24px 8px 24px;
|
|
|
|
cursor: default;
|
2021-04-14 15:17:45 +00:00
|
|
|
white-space: nowrap;
|
2022-11-17 20:17:51 +00:00
|
|
|
&-accent {
|
2022-09-06 01:51:57 +00:00
|
|
|
color: ${theme.accentText};
|
2022-01-12 13:30:51 +00:00
|
|
|
}
|
2022-11-17 20:17:51 +00:00
|
|
|
&-normal {
|
2022-09-06 01:51:57 +00:00
|
|
|
color: ${theme.lightText};
|
2022-01-12 13:30:51 +00:00
|
|
|
}
|
2021-04-14 15:17:45 +00:00
|
|
|
`);
|
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
const cssSaveButton = styled(primaryButton, `
|
|
|
|
margin-right: 8px;
|
2020-10-02 15:10:00 +00:00
|
|
|
`);
|
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
const cssSaveTextButton = styled('div', `
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
cursor: pointer;
|
|
|
|
font-size: ${vars.mediumFontSize};
|
|
|
|
padding: 0px 5px;
|
|
|
|
border-right: 1px solid ${theme.accentBorder};
|
|
|
|
|
|
|
|
&-accent {
|
|
|
|
color: ${theme.accentText};
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
`);
|
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
const cssRevertIconButton = styled('div', `
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
align-items: center;
|
|
|
|
cursor: pointer;
|
2020-10-02 15:10:00 +00:00
|
|
|
`);
|
2021-04-30 17:28:52 +00:00
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
const cssRevertIcon = styled(icon, `
|
|
|
|
--icon-color: ${theme.accentIcon};
|
2021-05-05 15:15:15 +00:00
|
|
|
margin: 0 5px 0 5px;
|
2021-04-30 17:28:52 +00:00
|
|
|
`);
|
|
|
|
|
2022-11-17 20:17:51 +00:00
|
|
|
const cssSectionSaveButtonsWrapper = styled('div', `
|
2021-05-05 15:15:15 +00:00
|
|
|
padding: 0 1px 0 1px;
|
2021-04-30 17:28:52 +00:00
|
|
|
display: flex;
|
|
|
|
justify-content: space-between;
|
2022-11-17 20:17:51 +00:00
|
|
|
align-self: normal;
|
2021-04-30 17:28:52 +00:00
|
|
|
`);
|
2022-01-12 13:30:51 +00:00
|
|
|
|
|
|
|
const cssSpacer = styled('div', `
|
|
|
|
margin: 0 auto;
|
|
|
|
`);
|