Skip to content
Snippets Groups Projects
Commit 09e8ca8f authored by Tom Robinson's avatar Tom Robinson
Browse files

Add undo + misc cleanup

parent a0060e3a
No related branches found
No related tags found
No related merge requests found
......@@ -25,7 +25,7 @@ export default ComposedComponent => class extends Component {
}
_render() {
this._element.className = this.props.className;
this._element.className = this.props.className || "";
ReactDOM.render(<ComposedComponent {...this.props} className={undefined} />, this._element);
}
......
......@@ -3,6 +3,10 @@
--brand-light-color: #CDE3F8;
--base-grey: #f8f8f8;
--grey-1: color(var(--base-grey) shade(10%));
--grey-2: color(var(--base-grey) shade(20%));
--grey-3: color(var(--base-grey) shade(30%));
--grey-4: color(var(--base-grey) shade(40%));
--grey-text-color: #797979;
--alt-color: #F5F7F9;
......@@ -130,22 +134,22 @@
/* grey */
.text-grey-1,
.text-grey-1-hover:hover { color: color(var(--base-grey) shade(10%)) }
.text-grey-1-hover:hover { color: var(--grey-1) }
.text-grey-2,
.text-grey-2-hover:hover { color: color(var(--base-grey) shade(20%)) }
.text-grey-2-hover:hover { color: var(--grey-2)) }
.text-grey-3,
.text-grey-3-hover:hover { color: color(var(--base-grey) shade(30%)) }
.text-grey-3-hover:hover { color: cvar(--grey-3) }
.text-grey-4,
.text-grey-4-hover:hover { color: color(var(--base-grey) shade(40%)) }
.text-grey-4-hover:hover { color: var(--grey-4) }
.bg-grey-0 { background-color: var(--base-grey) }
.bg-grey-1 { background-color: color(var(--base-grey) shade(10%)) }
.bg-grey-2 { background-color: color(var(--base-grey) shade(20%)) }
.bg-grey-3 { background-color: color(var(--base-grey) shade(30%)) }
.bg-grey-4 { background-color: color(var(--base-grey) shade(40%)) }
.bg-grey-1 { background-color: var(--grey-1) }
.bg-grey-2 { background-color: var(--grey-2) }
.bg-grey-3 { background-color: var(--grey-3) }
.bg-grey-4 { background-color: var(--grey-4) }
.text-dark { color: var(--dark-color); }
......
:root {
--default-border-radius: 4px;
}
.rounded {
.rounded, :local(.rounded) {
border-radius: var(--default-border-radius);
}
......
:root {
--shadow-color: rgba(0, 0, 0, .08);
}
.shadowed {
.shadowed, :local(.shadowed) {
box-shadow: 0 2px 2px var(--shadow-color);
}
......@@ -9,7 +9,7 @@ import LabelPopover from "../containers/LabelPopover.jsx";
import cx from "classnames";
const ActionHeader = ({ selectedCount, allAreSelected, setAllSelected, setArchived, labels }) =>
const ActionHeader = ({ selectedCount, allAreSelected, sectionIsArchive, setAllSelected, setArchived, labels }) =>
<div className={S.actionHeader}>
<Tooltip tooltip="Select all" isEnabled={!allAreSelected}>
<StackedCheckBox
......@@ -34,9 +34,9 @@ const ActionHeader = ({ selectedCount, allAreSelected, setAllSelected, setArchiv
}
labels={labels}
/>
<span className={S.archiveButton} onClick={() => setArchived()}>
<span className={S.archiveButton} onClick={() => setArchived(undefined, !sectionIsArchive)}>
<Icon name="grid" />
Archive
{ sectionIsArchive ? "Unarchive" : "Archive" }
</span>
</span>
</div>
......
......@@ -32,15 +32,16 @@ const Item = ({ id, name, created, by, selected, favorite, archived, icon, label
<div className={S.rightIcons}>
<LabelPopover
triggerElement={<Icon className={S.tagIcon} name="grid" width={20} height={20} />}
triggerClasses={S.trigger}
triggerClassesOpen={S.open}
item={{ id, labels }}
/>
<Tooltip tooltip="Favorite">
<Tooltip tooltip={favorite ? "Unfavorite" : "Favorite"}>
<Icon className={S.favoriteIcon} name="star" width={20} height={20} onClick={() => setFavorited(id, !favorite) }/>
</Tooltip>
</div>
<div className={S.extraIcons}>
<Tooltip tooltip="Archive">
<Tooltip tooltip={archived ? "Unarchive" : "Archive"}>
<Icon className={S.archiveIcon} name="grid" width={20} height={20} onClick={() => setArchived(id, !archived)} />
</Tooltip>
</div>
......
......@@ -116,7 +116,7 @@
/* FAVORITE */
:local(.favoriteIcon) {
composes: icon;
composes: mr1 from "style/spacing";
composes: mx1 from "style/spacing";
}
:local(.item.favorite) :local(.favoriteIcon) {
visibility: visible;
......@@ -131,3 +131,7 @@
:local(.item.archived) :local(.archiveIcon) {
color: var(--blue-color);
}
:local(.trigger) {
line-height: 0;
}
......@@ -6,9 +6,10 @@ import S from "../components/List.css";
import List from "../components/List.jsx";
import SearchHeader from "../components/SearchHeader.jsx";
import ActionHeader from "../components/ActionHeader.jsx";
import UndoListing from "./UndoListing.jsx";
import { setSearchText, setItemSelected, setAllSelected, setArchived } from "../questions";
import { getSearchText, getEntityType, getEntityIds, getSectionName, getSelectedCount, getAllAreSelected, getLabelsWithSelectedState } from "../selectors";
import { getSearchText, getEntityType, getEntityIds, getSectionName, getVisibleCount, getSelectedCount, getAllAreSelected, getSectionIsArchive, getLabelsWithSelectedState } from "../selectors";
const mapStateToProps = (state, props) => {
return {
......@@ -18,8 +19,10 @@ const mapStateToProps = (state, props) => {
searchText: getSearchText(state),
name: getSectionName(state),
visibleCount: getVisibleCount(state),
selectedCount: getSelectedCount(state),
allAreSelected: getAllAreSelected(state),
sectionIsArchive: getSectionIsArchive(state),
labels: getLabelsWithSelectedState(state)
}
......@@ -35,7 +38,7 @@ const mapDispatchToProps = {
@connect(mapStateToProps, mapDispatchToProps)
export default class EntityList extends Component {
render() {
const { style, name, selectedCount, allAreSelected, labels, searchText, setSearchText, entityType, entityIds, setItemSelected, setAllSelected, setArchived } = this.props;
const { style, name, visibleCount, selectedCount, allAreSelected, sectionIsArchive, labels, searchText, setSearchText, entityType, entityIds, setItemSelected, setAllSelected, setArchived } = this.props;
return (
<div style={style} className={S.list}>
<div className={S.header}>
......@@ -43,8 +46,10 @@ export default class EntityList extends Component {
</div>
{ selectedCount > 0 ?
<ActionHeader
visibleCount={visibleCount}
selectedCount={selectedCount}
allAreSelected={allAreSelected}
sectionIsArchive={sectionIsArchive}
setAllSelected={setAllSelected}
setArchived={setArchived}
labels={labels}
......@@ -53,6 +58,7 @@ export default class EntityList extends Component {
<SearchHeader searchText={searchText} setSearchText={setSearchText} />
}
<List entityType={entityType} entityIds={entityIds} setItemSelected={setItemSelected} />
<UndoListing />
</div>
);
}
......
@import '../Questions.css';
:local(.listing) {
composes: m2 from "style/spacing";
position: fixed;
left: 0;
bottom: 0;
z-index: 99;
}
:local(.undo) {
composes: mx2 mt2 p2 from "style/spacing";
composes: bordered from "style/bordered";
composes: rounded from "style/rounded";
composes: shadowed from "style/shadow";
position: relative;
background-color: white;
width: 300px;
}
:local(.actions) {
composes: flex align-center from "style/flex";
float: right;
}
:local(.message) {
}
:local(.undoButton) {
composes: mx2 from "style/spacing";
color: var(--blue-color);
text-transform: uppercase;
}
:local(.dismissButton) {
composes: cursor-pointer from "style/cursor";
color: var(--grey-1);
}
:local(.dismissButton):hover {
color: var(--grey-3);
}
.UndoListing-enter {
}
.UndoListing-enter.UndoListing-enter-active {
}
.UndoListing-leave {
opacity: 1;
}
.UndoListing-leave.UndoListing-leave-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}
import React, { Component, PropTypes } from "react";
import { connect } from "react-redux";
import S from "./UndoListing.css";
import { dismissUndo, performUndo } from "../questions";
import { getUndos } from "../selectors";
import Icon from "metabase/components/Icon";
import BodyComponent from "metabase/components/BodyComponent";
import ReactCSSTransitionGroup from "react-addons-css-transition-group";
const mapStateToProps = (state, props) => {
return {
undos: getUndos(state)
}
}
const mapDispatchToProps = {
dismissUndo,
performUndo
}
@connect(mapStateToProps, mapDispatchToProps)
@BodyComponent
export default class UndoListing extends Component {
constructor(props, context) {
super(props, context);
this.state = {};
}
static propTypes = {};
static defaultProps = {};
render() {
const { undos, performUndo, dismissUndo } = this.props;
return (
<ul className={S.listing}>
<ReactCSSTransitionGroup
transitionName="UndoListing"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}
>
{ undos.map(undo =>
<li key={undo.id} className={S.undo}>
<span className={S.message}>{undo.message}</span>
<span className={S.actions}>
<a className={S.undoButton} onClick={() => performUndo(undo.id)}>Undo</a>
<Icon className={S.dismissButton} name="close" onClick={() => dismissUndo(undo.id)} />
</span>
</li>
)}
</ReactCSSTransitionGroup>
</ul>
);
}
}
......@@ -22,6 +22,9 @@ const SET_ALL_SELECTED = 'metabase/questions/SET_ALL_SELECTED';
const SET_FAVORITED = 'metabase/questions/SET_FAVORITED';
const SET_ARCHIVED = 'metabase/questions/SET_ARCHIVED';
const SET_LABELED = 'metabase/questions/SET_LABELED';
const ADD_UNDO = 'metabase/questions/ADD_UNDO';
const DISMISS_UNDO = 'metabase/questions/DISMISS_UNDO';
const PERFORM_UNDO = 'metabase/questions/PERFORM_UNDO';
export const selectSection = createThunkAction(SELECT_SECTION, (section = "all", slug = null, type = "cards") => {
return async (dispatch, getState) => {
......@@ -72,19 +75,29 @@ export const setFavorited = createThunkAction(SET_FAVORITED, (cardId, favorited)
}
});
export const setArchived = createThunkAction(SET_ARCHIVED, (cardId, archived) => {
export const setArchived = createThunkAction(SET_ARCHIVED, (cardId, archived, showUndo = true) => {
return async (dispatch, getState) => {
if (cardId == null) {
// bulk archive
let selected = getSelectedEntities(getState());
selected.map(item => dispatch(setArchived(item.id, !selected[0].archived)));
let selected = getSelectedEntities(getState()).filter(item => item.archived !== archived);
selected.map(item => dispatch(setArchived(item.id, archived, false)));
// TODO: errors
dispatch(addUndo(
selected.length + " question were " + (archived ? "archived" : "unarchived"),
selected.map(item => setArchived(cardId, !archived, false))
));
} else {
let card = {
...getState().questions.entities.cards[cardId],
archived: archived
};
return await CardApi.update(card);
let response = await CardApi.update(card);
if (showUndo) {
dispatch(addUndo("Question was " + (archived ? "archived" : "unarchived"), [
setArchived(cardId, !archived, false)
]));
}
return response;
}
}
});
......@@ -116,6 +129,30 @@ export const setSearchText = createAction(SET_SEARCH_TEXT);
export const setItemSelected = createAction(SET_ITEM_SELECTED);
export const setAllSelected = createAction(SET_ALL_SELECTED);
let nextUndoId = 0;
export const addUndo = createThunkAction(ADD_UNDO, (message, actions) => {
return (dispatch, getState) => {
let id = nextUndoId++;
setTimeout(() => dispatch(dismissUndo(id)), 5000);
return { id, message, actions };
};
});
export const dismissUndo = createAction(DISMISS_UNDO);
export const performUndo = createThunkAction(PERFORM_UNDO, (undoId) => {
return (dispatch, getState) => {
let undo = _.findWhere(getState().questions.undos, { id: undoId });
if (undo) {
undo.actions.map(action =>
dispatch(action)
);
dispatch(dismissUndo(undoId));
}
};
});
const initialState = {
entities: {},
type: "cards",
......@@ -123,7 +160,8 @@ const initialState = {
itemsBySection: {},
searchText: "",
selectedIds: {},
allSelected: false
allSelected: false,
undos: []
};
export default function(state = initialState, { type, payload, error }) {
......@@ -189,13 +227,17 @@ export default function(state = initialState, { type, payload, error }) {
...state.entities.cards[payload.id],
...payload
});
console.log("payload", payload, state)
if (payload._changedLabeled) {
state = addToSection(state, "cards", "label-" + payload._changedLabelSlug, payload.id);
} else {
state = removeFromSection(state, "cards", "label-" + payload._changedLabelSlug, payload.id);
}
return state;
}
case ADD_UNDO:
return { ...state, undos: state.undos.concat(payload) };
case DISMISS_UNDO:
return { ...state, undos: state.undos.filter(undo => undo.id !== payload) };
default:
return state;
}
......
......@@ -76,7 +76,7 @@ export const getSelectedEntities = createSelector(
export const getVisibleCount = createSelector(
[getVisibleEntities],
(visibleEntities) => visibleEntities.length
)
);
export const getSelectedCount = createSelector(
[getSelectedEntities],
......@@ -87,7 +87,13 @@ export const getAllAreSelected = createSelector(
[getSelectedCount, getVisibleCount],
(selectedCount, visibleCount) =>
selectedCount === visibleCount && visibleCount > 0
)
);
export const getSectionIsArchive = createSelector(
[getSection],
(section) =>
section === "archived"
);
const sections = [
{ id: "all", name: "All questions", icon: "star" },
......@@ -137,14 +143,22 @@ export const getLabelsWithSelectedState = createSelector(
export const getSectionName = createSelector(
[getSection, getSections, getLabels],
(sectionId, sections, labels) => {
console.log("sectionId", sectionId)
let match = sectionId && sectionId.match(/^section-(.*)/);
let match = sectionId && sectionId.match(/^(.*)-(.*)/);
if (match) {
let label = _.findWhere(labels, { slug: match[1] });
return label && label.name
if (match[1] === "label") {
let label = _.findWhere(labels, { slug: match[2] });
if (label && label.name) {
return label.name;
}
}
} else {
let section = _.findWhere(sections, { id: sectionId });
return section && section.name
if (section && section.name) {
return section.name;
}
}
return "";
}
);
export const getUndos = (state) => state.questions.undos;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment