Skip to content
This repository has been archived by the owner on Jun 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #27 from buttercup/groups_menu
Browse files Browse the repository at this point in the history
Groups menus (moving, renaming etc.)
  • Loading branch information
perry-mitchell authored Jan 18, 2020
2 parents b354258 + 84ef2ba commit 9df1c66
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 116 deletions.
144 changes: 72 additions & 72 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@babel/plugin-transform-spread": "^7.2.2",
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"@buttercup/facades": "^1.0.1",
"@buttercup/facades": "^1.1.0",
"@storybook/addon-actions": "^4.1.11",
"@storybook/addons": "^4.1.11",
"@storybook/react": "^4.1.11",
Expand Down Expand Up @@ -85,11 +85,12 @@
"style-loader": "^0.23.1",
"styled-components": "^4.3.2",
"terser-webpack-plugin": "^1.4.1",
"uuid": "^3.3.3",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7"
},
"peerDependencies": {
"@buttercup/facades": "^v1.0.0",
"@buttercup/facades": ">= 1.1.0",
"react": "^16.8.1",
"react-dom": "^16.8.1",
"styled-components": "^4.1.3"
Expand Down
228 changes: 194 additions & 34 deletions src/components/vault/GroupsList.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import React, { useState } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { Tree as BaseTree, Button, Tag, Intent, Alignment } from '@blueprintjs/core';
import {
Alignment,
Button,
Classes,
ContextMenu,
Dialog,
InputGroup,
Intent,
Menu,
MenuDivider,
MenuItem,
Tag,
Tree as BaseTree
} from '@blueprintjs/core';
import { GroupFacade } from './props';
import { useGroups } from './hooks/vault';
import { PaneContainer, PaneHeader, PaneContent, PaneFooter } from './Pane';
import { getThemeProp } from '../../utils';

const KEYCODE_ENTER = 13;

const Tree = styled(BaseTree)`
.node {
&[class*='node-selected'] {
Expand All @@ -31,13 +46,24 @@ const Tree = styled(BaseTree)`
`;

const GroupsList = () => {
const [groupsContextOpen, setGroupsContextOpen] = useState(false);
const [groupEditID, setGroupEditID] = useState(null);
const [parentGroupID, setParentGroupID] = useState(null);
const [newGroupName, setNewGroupName] = useState('');
const groupTitleInputRef = useRef(null);
const {
groups,
groupsRaw,
selectedGroupID,
onCreateGroup,
onMoveGroup,
onMoveGroupToTrash,
onRenameGroup,
onSelectGroup,
expandedGroups,
handleCollapseGroup,
handleExpandGroup,
handleModifyGroup,
filters,
onGroupFilterTermChange,
onGroupFilterSortModeChange,
Expand All @@ -46,40 +72,174 @@ const GroupsList = () => {
trashID
} = useGroups();

return (
<PaneContainer primary>
<PaneHeader
title="Groups"
count={groups.length}
filter={filters}
onTermChange={term => onGroupFilterTermChange(term)}
onSortModeChange={sortMode => onGroupFilterSortModeChange(sortMode)}
/>
<PaneContent>
<Tree
contents={groups}
onNodeClick={group => onSelectGroup(group.id)}
onNodeExpand={handleExpandGroup}
onNodeCollapse={handleCollapseGroup}
useEffect(() => {
if (groupTitleInputRef && groupTitleInputRef.current) {
groupTitleInputRef.current.focus();
}
}, [groupTitleInputRef.current]);

const closeEditDialog = () => {
setGroupEditID(null);
setNewGroupName('');
setParentGroupID(null);
};

const confirmEmptyTrash = () => {
// @todo empty trash
};

const editGroup = (groupFacade, parentID = null) => {
setGroupEditID(groupFacade ? groupFacade.id : -1);
setNewGroupName(groupFacade ? groupFacade.title : '');
setParentGroupID(parentID);
};

const moveGroupToGroup = (groupID, parentID) => {
onMoveGroup(groupID, parentID);
};

const moveToTrash = groupID => {
onMoveGroupToTrash(groupID);
};

const renderGroupsMenu = (items, parentNode, selectedGroupID) => (
<>
<If condition={parentNode}>
<MenuItem
text={`Move to ${parentNode.label}`}
key={parentNode.id}
icon={parentNode.icon}
onClick={() => moveGroupToGroup(selectedGroupID, parentNode.id)}
disabled={selectedGroupID === parentNode.id}
/>
</PaneContent>
<PaneFooter>
<Button
rightIcon={
<Tag round minimal intent={trashCount > 0 ? Intent.WARNING : Intent.NONE}>
{trashCount}
</Tag>
}
icon="trash"
fill
minimal
text="Trash"
alignText={Alignment.LEFT}
active={trashSelected}
onClick={() => onSelectGroup(trashID)}
<MenuDivider />
</If>
<For each="group" of={items}>
<Choose>
<When condition={group.childNodes.length > 0}>
<MenuItem
text={group.label}
key={group.id}
icon={group.icon}
onClick={() => moveGroupToGroup(selectedGroupID, group.id)}
disabled={selectedGroupID === group.id}
>
{renderGroupsMenu(group.childNodes, group, selectedGroupID)}
</MenuItem>
</When>
<Otherwise>
<MenuItem
text={group.label}
key={group.id}
icon={group.icon}
onClick={() => moveGroupToGroup(selectedGroupID, group.id)}
disabled={selectedGroupID === group.id}
/>
</Otherwise>
</Choose>
</For>
</>
);

const showGroupContextMenu = (node, nodePath, evt) => {
evt.preventDefault();
const groupFacade = groupsRaw.find(group => group.id === node.id);
setGroupsContextOpen(true);
ContextMenu.show(
<Menu>
<MenuItem text={groupFacade.title} disabled />
<MenuDivider />
<MenuItem text="Add New Group" icon="add" onClick={() => editGroup(null, groupFacade.id)} />
<MenuItem text="Rename" icon="edit" onClick={() => editGroup(groupFacade)} />
<MenuDivider />
<MenuItem text="Move to..." icon="add-to-folder">
{renderGroupsMenu(groups, null, node.id)}
</MenuItem>
<MenuItem text="Move to Trash" icon="trash" onClick={() => moveToTrash(selectedGroupID)} />
</Menu>,
{ left: evt.clientX, top: evt.clientY },
() => {
setGroupsContextOpen(false);
}
);
};

const submitGroupChange = () => {
if (groupEditID !== null && groupEditID !== -1) {
onRenameGroup(groupEditID, newGroupName);
} else {
onCreateGroup(parentGroupID, newGroupName);
}
closeEditDialog();
};

return (
<>
<PaneContainer primary>
<PaneHeader
title="Groups"
count={groups.length}
filter={filters}
onAddItem={() => editGroup()}
onTermChange={term => onGroupFilterTermChange(term)}
onSortModeChange={sortMode => onGroupFilterSortModeChange(sortMode)}
/>
</PaneFooter>
</PaneContainer>
<PaneContent>
<Tree
contents={groups}
onNodeClick={group => onSelectGroup(group.id)}
onNodeContextMenu={showGroupContextMenu}
onNodeExpand={handleExpandGroup}
onNodeCollapse={handleCollapseGroup}
/>
</PaneContent>
<PaneFooter>
<Button
rightIcon={
<Tag round minimal intent={trashCount > 0 ? Intent.WARNING : Intent.NONE}>
{trashCount}
</Tag>
}
icon="trash"
fill
minimal
text="Trash"
alignText={Alignment.LEFT}
active={trashSelected}
onClick={() => onSelectGroup(trashID)}
/>
</PaneFooter>
</PaneContainer>
<Dialog
icon="manually-entered-data"
onClose={closeEditDialog}
title={groupEditID === -1 ? 'Create Group' : 'Rename Group'}
isOpen={groupEditID !== null}
>
<div className={Classes.DIALOG_BODY}>
<p>Enter group title:</p>
<InputGroup
leftIcon={groupEditID === -1 ? 'folder-new' : 'add-to-folder'}
onChange={evt => setNewGroupName(evt.target.value)}
value={newGroupName}
inputRef={groupTitleInputRef}
onKeyDown={evt => {
if (evt.keyCode === KEYCODE_ENTER) {
submitGroupChange();
}
}}
/>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={closeEditDialog}>Cancel</Button>
<Button intent={Intent.PRIMARY} onClick={submitGroupChange}>
Save
</Button>
</div>
</div>
</Dialog>
</>
);
};

Expand Down
10 changes: 8 additions & 2 deletions src/components/vault/Pane.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
} from '@blueprintjs/core';
import { getThemeProp } from '../../utils';

const NOOP = () => {};

const createScrollShadow = color => css`
/* Show shadow on scroll: https://gist.github.com/tbmiller/6675197 */
background: linear-gradient(${color} 30%, hsla(0, 0%, 100%, 0)),
Expand Down Expand Up @@ -89,8 +91,9 @@ export const PaneHeader = ({
count,
title,
filter = null,
onTermChange = () => {},
onSortModeChange = () => {}
onAddItem = NOOP,
onTermChange = NOOP,
onSortModeChange = NOOP
}) => {
const [filterInputVisible, toggleFilter] = useState(false);
const inputRef = useRef(null);
Expand Down Expand Up @@ -161,6 +164,9 @@ export const PaneHeader = ({
</Otherwise>
</Choose>
</ListHeadingContent>
<If condition={onAddItem !== NOOP}>
<Button minimal icon="add" small onClick={onAddItem} />
</If>
<If condition={showFilter}>
<Popover content={renderMenu}>
<Button minimal icon="filter-list" small />
Expand Down
34 changes: 31 additions & 3 deletions src/components/vault/Vault.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useReducer, useState, useEffect, useRef } from 'react';
import { clone } from 'ramda';
import PropTypes from 'prop-types';
import { createEntryFacade } from '@buttercup/facades';
import { createEntryFacade, createGroupFacade } from '@buttercup/facades';
import { VaultFacade } from './props';
import { entryReducer } from './reducers/entry';
import { vaultReducer, filterReducer, defaultFilter } from './reducers/vault';
Expand All @@ -23,11 +23,9 @@ export const VaultProvider = ({ onUpdate, vault: vaultSource, children }) => {

useEffect(() => {
if (initRef.current === false) {
console.log('Init call. Do not call onUpdate');
initRef.current = true;
return;
}
console.log('Debug: Running on Update function. Yay!');
onUpdate(vault);
}, [vault]);

Expand All @@ -43,6 +41,13 @@ export const VaultProvider = ({ onUpdate, vault: vaultSource, children }) => {
entriesFilters,

// Actions
batchDeleteItems: ({ groupIDs = [], entryIDs = [] }) => {
dispatch({
type: 'batch-delete',
groups: groupIDs,
entries: entryIDs
});
},
onSelectGroup: groupID => {
setSelectedGroupID(groupID);
setSelectedEntryID(null);
Expand All @@ -53,6 +58,15 @@ export const VaultProvider = ({ onUpdate, vault: vaultSource, children }) => {
handleCollapseGroup: group => {
setExpandedGroups(expandedGroups.filter(id => id !== group.id));
},
onCreateGroup: (parentID, groupTitle) => {
const parentGroupID = parentID ? parentID : undefined;
const group = createGroupFacade(null, parentGroupID);
group.title = groupTitle;
dispatch({
type: 'create-group',
payload: group
});
},
onGroupFilterTermChange: term => {
dispatchGroupFilters({
type: 'set-term',
Expand Down Expand Up @@ -89,6 +103,20 @@ export const VaultProvider = ({ onUpdate, vault: vaultSource, children }) => {
});
}
},
onMoveGroup: (groupID, parentID) => {
dispatch({
type: 'move-group',
groupID,
parentID
});
},
onRenameGroup: (groupID, title) => {
dispatch({
type: 'rename-group',
groupID,
title
});
},
onAddEntry: type => {
const facade = createEntryFacade(null, { type });
facade.parentID = selectedGroupID;
Expand Down
Loading

0 comments on commit 9df1c66

Please sign in to comment.