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

Card history, link to ephemeral cards from home, misc

parent 0b96851e
Branches
Tags
No related merge requests found
......@@ -72,8 +72,8 @@ CardControllers.controller('CardList', ['$scope', '$location', 'Card', function(
}]);
CardControllers.controller('CardDetail', [
'$rootScope', '$scope', '$route', '$routeParams', '$location', '$q', '$window', '$timeout', 'Card', 'Dashboard', 'CorvusFormGenerator', 'Metabase', 'VisualizationSettings', 'QueryUtils',
function($rootScope, $scope, $route, $routeParams, $location, $q, $window, $timeout, Card, Dashboard, CorvusFormGenerator, Metabase, VisualizationSettings, QueryUtils) {
'$rootScope', '$scope', '$route', '$routeParams', '$location', '$q', '$window', '$timeout', 'Card', 'Dashboard', 'CorvusFormGenerator', 'Metabase', 'VisualizationSettings', 'QueryUtils', 'Revision',
function($rootScope, $scope, $route, $routeParams, $location, $q, $window, $timeout, Card, Dashboard, CorvusFormGenerator, Metabase, VisualizationSettings, QueryUtils, Revision) {
// promise helper
$q.resolve = function(object) {
var deferred = $q.defer();
......@@ -133,22 +133,17 @@ CardControllers.controller('CardDetail', [
fromUrl: $routeParams.from,
cardApi: Card,
dashboardApi: Dashboard,
revisionApi: Revision,
broadcastEventFn: function(eventName, value) {
$rootScope.$broadcast(eventName, value);
},
notifyCardChangedFn: function(modifiedCard) {
notifyCardChangedFn: async function(modifiedCard) {
// these are the only things we let the header change
card.name = modifiedCard.name;
card.description = modifiedCard.description;
card.public_perms = modifiedCard.public_perms;
renderAll();
// this looks a little hokey, but its preferrable to setup our functions as promises so that callers can
// be certain when they have been resolved.
var deferred = $q.defer();
deferred.resolve();
return deferred.promise;
},
notifyCardCreatedFn: function(newCard) {
setCard(newCard, { resetDirty: true, replaceState: true });
......@@ -182,9 +177,7 @@ CardControllers.controller('CardDetail', [
setCard(card, { setDirty: true, replaceState: false })
});
},
revertCardFn: function() {
revertCard();
},
reloadCardFn: reloadCard,
onChangeLocation: function(url) {
$timeout(() => $location.url(url))
},
......@@ -859,7 +852,8 @@ CardControllers.controller('CardDetail', [
savedCardSerialized = null;
}
function revertCard() {
function reloadCard() {
delete $routeParams.serializedCard;
loadAndSetCard();
}
......
......@@ -74,7 +74,7 @@ export default React.createClass({
return (
<span>
<Icon name='check' width="12px" height="12px" />
{this.props.successText}
<span className="ml1">{this.props.successText}</span>
</span>
);
} else if (this.state.result === "failed") {
......
......@@ -6,12 +6,8 @@ import ActionButton from "metabase/components/ActionButton.react";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.react";
import Modal from "metabase/components/Modal.react";
import { fetchRevisions, revertToRevision } from "../actions";
import moment from "moment";
window.moment = moment;
function formatDate(date) {
var m = moment(date);
if (m.isSame(moment(), 'day')) {
......@@ -32,7 +28,7 @@ export default class HistoryModal extends Component {
async componentDidMount() {
let { entityType, entityId } = this.props;
try {
await this.props.dispatch(fetchRevisions({ entity: entityType, id: entityId }));
await this.props.onFetchRevisions({ entity: entityType, id: entityId });
} catch (error) {
this.setState({ error: error });
}
......@@ -40,12 +36,17 @@ export default class HistoryModal extends Component {
async revert(revision) {
let { entityType, entityId } = this.props;
await this.props.dispatch(revertToRevision({ entity: entityType, id: entityId, revision_id: revision.id }));
this.props.onReverted();
try {
await this.props.onRevertToRevision({ entity: entityType, id: entityId, revision_id: revision.id });
this.props.onReverted();
} catch (e) {
console.warn("revert failed", e);
throw e;
}
}
render() {
var revisions = this.props.revisions[this.props.entityType+"-"+this.props.entityId];
var { revisions } = this.props;
return (
<Modal
title="Change History"
......@@ -91,9 +92,11 @@ export default class HistoryModal extends Component {
}
HistoryModal.propTypes = {
revisions: PropTypes.array,
entityType: PropTypes.string.isRequired,
entityId: PropTypes.number.isRequired,
revisions: PropTypes.object.isRequired,
onFetchRevisions: PropTypes.func.isRequired,
onRevertToRevision: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onReverted: PropTypes.func.isRequired
};
......@@ -8,14 +8,16 @@ import Icon from "metabase/components/Icon.react";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.react";
import AddToDashSelectQuestionModal from "./AddToDashSelectQuestionModal.react";
import DeleteDashboardModal from "./DeleteDashboardModal.react";
import HistoryModal from "./HistoryModal.react";
import HistoryModal from "metabase/components/HistoryModal.react";
import {
setEditingDashboard,
fetchDashboard,
setDashboardAttributes,
saveDashboard,
deleteDashboard
deleteDashboard,
fetchRevisions,
revertToRevision
} from '../actions';
import cx from "classnames";
......@@ -57,6 +59,14 @@ export default class DashboardHeader extends Component {
this.props.dispatch(fetchDashboard(this.props.dashboard.id));
}
onFetchRevisions({ entity, id }) {
return this.props.dispatch(fetchRevisions({ entity, id }));
}
onRevertToRevision({ entity, id, revision_id }) {
return this.props.dispatch(revertToRevision({ entity, id, revision_id }));
}
getEditingButtons() {
var editingButtons = [];
if (this.props.isDirty) {
......@@ -112,7 +122,9 @@ export default class DashboardHeader extends Component {
dispatch={this.props.dispatch}
entityType="dashboard"
entityId={dashboard.id}
revisions={this.props.revisions}
revisions={this.props.revisions["dashboard-"+dashboard.id]}
onFetchRevisions={this.onFetchRevisions.bind(this)}
onRevertToRevision={this.onRevertToRevision.bind(this)}
onClose={() => this.refs.dashboardHistory.toggleModal()}
onReverted={() => this.onRevertedRevision()}
/>
......
......@@ -46,7 +46,7 @@
<div class="Grid">
<ul class="Grid-cell">
<li ng-repeat="card in cards | filter:searchFilter">
<a href="/card/{{card.id}}" class="Entity flex align-center no-decoration mb2 py2 lg-p2 border-bottom">
<a href="/card/{{card.id}}?clone" class="Entity flex align-center no-decoration mb2 py2 lg-p2 border-bottom">
<div class="flex flex-column">
<div class="flex align-center">
<h4 class="Entity-title break-word text-brand-hover">{{card.name}}</h4>
......
......@@ -7,6 +7,7 @@ import AddToDashSelectDashModal from '../components/AddToDashSelectDashModal.rea
import CardFavoriteButton from './card_favorite_button.react';
import DeleteQuestionModal from '../components/DeleteQuestionModal.react';
import Header from "metabase/components/Header.react";
import HistoryModal from "metabase/components/HistoryModal.react";
import Icon from "metabase/components/Icon.react";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.react";
import QueryModeToggle from './query_mode_toggle.react';
......@@ -23,10 +24,11 @@ export default React.createClass({
tableMetadata: React.PropTypes.object, // can't be required, sometimes null
cardApi: React.PropTypes.func.isRequired,
dashboardApi: React.PropTypes.func.isRequired,
revisionApi: React.PropTypes.func.isRequired,
notifyCardChangedFn: React.PropTypes.func.isRequired,
notifyCardDeletedFn: React.PropTypes.func.isRequired,
notifyCardAddedToDashFn: React.PropTypes.func.isRequired,
revertCardFn: React.PropTypes.func.isRequired,
reloadCardFn: React.PropTypes.func.isRequired,
setQueryModeFn: React.PropTypes.func.isRequired,
isShowingDataReference: React.PropTypes.bool.isRequired,
toggleDataReferenceFn: React.PropTypes.func.isRequired,
......@@ -37,7 +39,8 @@ export default React.createClass({
getInitialState: function() {
return {
recentlySaved: null,
modal: null
modal: null,
revisions: null
};
},
......@@ -115,10 +118,25 @@ export default React.createClass({
this.props.onChangeLocation(this.props.fromUrl);
},
onFetchRevisions: async function({ entity, id }) {
var revisions = await this.props.revisionApi.list({ entity, id }).$promise;
this.setState({ revisions });
},
onRevertToRevision: function({ entity, id, revision_id }) {
return this.props.revisionApi.revert({ entity, id, revision_id }).$promise;
},
onRevertedRevision: function() {
this.props.reloadCardFn();
this.refs.cardHistory.toggleModal();
},
getHeaderButtons: function() {
var saveButton;
var buttons = [];
if (this.props.cardIsNewFn() && this.props.cardIsDirtyFn()) {
saveButton = (
buttons.push(
<PopoverWithTrigger
ref="saveModal"
tether={false}
......@@ -135,9 +153,28 @@ export default React.createClass({
);
}
var queryModeToggle;
if (!this.props.cardIsNewFn()) {
buttons.push(
<PopoverWithTrigger
ref="cardHistory"
tether={false}
triggerElement={<Icon name="history" width="16px" height="16px" />}
>
<HistoryModal
revisions={this.state.revisions}
entityType="card"
entityId={this.props.card.id}
onFetchRevisions={this.onFetchRevisions}
onRevertToRevision={this.onRevertToRevision}
onClose={() => this.refs.cardHistory.toggleModal()}
onReverted={this.onRevertedRevision}
/>
</PopoverWithTrigger>
);
}
if (this.props.cardIsNewFn() && !this.props.cardIsDirtyFn()) {
queryModeToggle = (
buttons.push(
<QueryModeToggle
currentQueryMode={this.props.card.dataset_query.type}
setQueryModeFn={this.setQueryMode}
......@@ -145,31 +182,6 @@ export default React.createClass({
);
}
var cloneButton;
// if (this.props.card.id) {
// cloneButton = (
// <a href="#" className="mx1 text-grey-4 text-brand-hover" title="Ask another question based on this question">
// <Icon name='clone' width="16px" height="16px" onClick={this.props.cloneCardFn}></Icon>
// </a>
// );
// }
//
var cardFavorite;
// if (this.props.card.id != undefined) {
// cardFavorite = (<CardFavoriteButton cardApi={this.props.cardApi} cardId={this.props.card.id}></CardFavoriteButton>);
// }
//
var addToDashButton;
// if (this.props.card.id != undefined) {
// addToDashButton = (
// <AddToDashboard
// card={this.props.card}
// dashboardApi={this.props.dashboardApi}
// broadcastEventFn={this.props.broadcastEventFn}
// />
// )
// }
var dataReferenceButtonClasses = cx({
'mx1': true,
'transition-color': true,
......@@ -183,11 +195,7 @@ export default React.createClass({
</a>
);
return [
[saveButton, queryModeToggle],
[cardFavorite, cloneButton, addToDashButton],
[dataReferenceButton]
].map(section => section.filter(button => !!button)).filter(section => section.length > 0);
return [buttons, [dataReferenceButton]];
},
getEditingButtons: function() {
......@@ -211,7 +219,7 @@ export default React.createClass({
}
if (this.props.cardIsDirtyFn()) {
editingButtons.push(
<a className="Button Button--small text-uppercase" href="#" onClick={this.props.revertCardFn}>Discard Changes</a>
<a className="Button Button--small text-uppercase" href="#" onClick={this.props.reloadCardFn}>Discard Changes</a>
);
}
editingButtons.push(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment