Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New ViewManager for persisting named bundles of grid and other component state #3774

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9b02ead
Persistence Manager
Ryanseanlee Sep 4, 2024
09db08d
Merge branch 'develop' into persistenceManager
Ryanseanlee Sep 4, 2024
b4b177c
Greg CR
Ryanseanlee Sep 5, 2024
b91d766
- Save and manage dialog model created once and open with isOpen state
Ryanseanlee Sep 6, 2024
8b1eb9c
Merge branch 'develop' into persistenceManager
Ryanseanlee Sep 6, 2024
e99cd7c
Remove new and add code default state
Ryanseanlee Sep 9, 2024
e58e1be
Add view tree getter for app specific custom component.
Ryanseanlee Sep 10, 2024
0bf7539
Favorites feature
Ryanseanlee Sep 11, 2024
b9b9447
Fix Favorites feature
Ryanseanlee Sep 12, 2024
78d95fc
Merge remote-tracking branch 'origin/develop' into persistenceManager
ghsolomon Sep 16, 2024
becede0
Merge branch 'develop' into persistenceManager
Ryanseanlee Sep 17, 2024
103dafa
Greg CR
Ryanseanlee Sep 17, 2024
0c591d2
Merge remote-tracking branch 'origin/persistenceManager' into persist…
Ryanseanlee Sep 17, 2024
9ca0fab
Favorites
Ryanseanlee Sep 20, 2024
c71ebfe
Grid popover
Ryanseanlee Sep 24, 2024
2982458
Remove persistence grid
Ryanseanlee Sep 27, 2024
afd6f7e
Update type
Ryanseanlee Sep 27, 2024
3ee7367
Remove ID and remove PersistenceGridPopover
Ryanseanlee Sep 27, 2024
bacb760
Merge branch 'develop' into persistenceManager
Ryanseanlee Sep 30, 2024
ee1beff
Add comments
Ryanseanlee Sep 30, 2024
a01060e
Replace Persistence -> View
Ryanseanlee Oct 3, 2024
bb518f5
Replace Persistence -> View
Ryanseanlee Oct 3, 2024
7ae18ec
add isWriteStateImmediately getter on persistenceProvider
Ryanseanlee Oct 7, 2024
9391c64
Merge branch 'develop' into persistenceManager
Ryanseanlee Oct 7, 2024
ac6e397
greg CR
Ryanseanlee Oct 16, 2024
c5536e2
greg CR
Ryanseanlee Oct 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cmp/grid/GridModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,8 +629,10 @@ export class GridModel extends HoistModel {
*
* @returns true if defaults were restored
*/
async restoreDefaultsAsync(): Promise<boolean> {
if (this.restoreDefaultsWarning) {
async restoreDefaultsAsync(
Ryanseanlee marked this conversation as resolved.
Show resolved Hide resolved
{skipWarning}: {skipWarning: boolean} = {skipWarning: !!this.restoreDefaultsWarning}
): Promise<boolean> {
if (this.restoreDefaultsWarning && !skipWarning) {
const confirmed = await XH.confirm({
message: this.restoreDefaultsWarning,
confirmProps: {
Expand Down
30 changes: 30 additions & 0 deletions desktop/cmp/persistenceManager/PersistenceManager.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.xh-persistence-manager {
align-items: center;

// Save Button
& > .xh-button {
margin-right: var(--xh-pad-half-px);
}

// Dialogs
&__manage-dialog,
&__save-as-dialog {
&__form {
padding: var(--xh-pad-px);

.xh-form-field.xh-form-field-readonly .xh-form-field-readonly-display {
padding: 0;
}

.xh-form-field .xh-form-field-info {
line-height: 1.5em;
margin-top: var(--xh-pad-half-px);
white-space: unset;

.xh-icon {
margin-right: 2px;
}
}
}
}
}
166 changes: 166 additions & 0 deletions desktop/cmp/persistenceManager/PersistenceManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import {div, fragment, hbox} from '@xh/hoist/cmp/layout';
import {hoistCmp, HoistProps, uses} from '@xh/hoist/core';
import {button} from '@xh/hoist/desktop/cmp/button';
import {PersistenceViewTree} from '@xh/hoist/desktop/cmp/persistenceManager/Types';
import {Icon} from '@xh/hoist/icon/Icon';
import {menu, menuDivider, menuItem, popover} from '@xh/hoist/kit/blueprint';
import {capitalize} from 'lodash';
import {ReactNode} from 'react';
import {manageDialog} from './impl/ManageDialog';
import {saveDialog} from './impl/SaveDialog';
import './PersistenceManager.scss';
import {PersistenceManagerModel} from './PersistenceManagerModel';
import {pluralize} from '@xh/hoist/utils/js';

interface PersistenceManagerProps extends HoistProps<PersistenceManagerModel> {
/** True to disable options for saving/managing items. */
minimal?: boolean;
/** True (default) to render a save button alongside the primary menu button when dirty. */
enableTopLevelSaveButton?: boolean;
/** True to omit the default menu component. Should be used when creating custom app-specific component */
omitDefaultMenuComponent?: boolean;
Ryanseanlee marked this conversation as resolved.
Show resolved Hide resolved
}

export const [PersistenceManager, persistenceManager] =
hoistCmp.withFactory<PersistenceManagerProps>({
displayName: 'PersistenceManager',
model: uses(PersistenceManagerModel),

render({model, ...props}) {
return fragment(defaultMenu({...props}), manageDialog(), saveDialog());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To discuss - either exporting the dialogs, exposing more props for customizing, or making them pluggable (i.e. PersistenceManagerProps could accept a manageDialogCmp and saveDialogCmp that default to our Hoist implementations)

}
});

//------------------------
// Implementation
//------------------------

const defaultMenu = hoistCmp.factory<PersistenceManagerModel>({
Ryanseanlee marked this conversation as resolved.
Show resolved Hide resolved
render({
model,
omitDefaultMenuComponent = false,
minimal = false,
enableTopLevelSaveButton = true
}) {
const {selectedView, isShared, entity} = model,
Ryanseanlee marked this conversation as resolved.
Show resolved Hide resolved
displayName = entity.displayName;
return hbox({
className: 'xh-persistence-manager',
items: [
popover({
omit: omitDefaultMenuComponent,
item: button({
text: model.getHierarchyDisplayName(selectedView?.name) ?? `-`,
icon: isShared ? Icon.users() : Icon.bookmark(),
rightIcon: Icon.chevronDown(),
outlined: true
}),
content: div(
div({
className: 'xh-popup__title',
item: capitalize(pluralize(displayName))
}),
objMenu({minimal})
),
placement: 'bottom-start'
}),
saveButton({omit: !enableTopLevelSaveButton || !model.canSave})
]
});
}
});

const saveButton = hoistCmp.factory<PersistenceManagerModel>({
render({model}) {
return button({
icon: Icon.save(),
tooltip: `Save changes to this ${model.entity.displayName}`,
intent: 'primary',
onClick: () => model.saveAsync(false).linkTo(model.loadModel)
});
}
});

const objMenu = hoistCmp.factory<PersistenceManagerProps>({
render({model, minimal}) {
const {loadModel, entity} = model,
items = [];

if (model.favoritedViews.length > 0) {
items.push(menuDivider({title: 'Favorites'}));
items.push(
...model.favoritedViews.map(it => {
return menuItem({
key: `${it.id}-isFavorite`,
icon: model.selectedId === it.id ? Icon.check() : Icon.placeholder(),
text: model.getHierarchyDisplayName(it.name),
onClick: () => model.selectAsync(it.id).linkTo(model.loadModel)
});
})
);
}

model.viewTree.forEach(it => {
if (it.type === 'divider') items.push(menuDivider({title: it.text}));
items.push(buildView(it, model));
});

if (minimal) return menu({items});
return menu({
Ryanseanlee marked this conversation as resolved.
Show resolved Hide resolved
items: [
...items,
menuDivider(),
menuItem({
icon: Icon.save(),
text: 'Save',
disabled: !model.canSave,
onClick: () => model.saveAsync(false).linkTo(loadModel)
}),
menuItem({
icon: Icon.copy(),
text: 'Save as...',
onClick: () => model.saveAsAsync().linkTo(loadModel)
}),
menuItem({
icon: Icon.reset(),
text: 'Revert View',
disabled: !model.isDirty,
onClick: () => model.resetAsync().linkTo(loadModel)
}),
menuItem({
icon: Icon.refresh(),
text: 'Reset Default View',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discuss today - Reset vs Restore. Or maybe there’s a better phrasing entirely?

omit: !model.isAllowEmpty,
onClick: () => model.selectAsync(null).linkTo(loadModel)
}),
menuDivider(),
menuItem({
icon: Icon.gear(),
text: `Manage ${pluralize(entity.displayName)}...`,
onClick: () => model.openManageDialog()
})
]
});
}
});

function buildView(view: PersistenceViewTree, model: PersistenceManagerModel): ReactNode {
const {type, text, selected} = view,
icon = selected ? Icon.check() : Icon.placeholder();
switch (type) {
case 'directory':
return menuItem({
text,
icon,
shouldDismissPopover: false,
children: view.items ? view.items.map(child => buildView(child, model)) : []
});
case 'view':
return menuItem({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a little favorite star icon to the right of the view?

key: view.isFavorite ? `${view.id}-isFavorite` : view.id,
icon,
text,
onClick: () => model.selectAsync(view.id).linkTo(model.loadModel)
});
}
}
Loading