Skip to content
Snippets Groups Projects
Commit f264e504 authored by Sameer Al-Sakran's avatar Sameer Al-Sakran
Browse files

Merge branch 'release-0.24.0' of github.com:metabase/metabase into release-0.24.0

parents 75e07f85 8db2d6ce
No related branches found
No related tags found
No related merge requests found
......@@ -11,7 +11,7 @@ const SearchHeader = ({ searchText, setSearchText }) =>
<div className={S.searchHeader}>
<Icon className={S.searchIcon} name="search" size={18} />
<input
className={cx("input", S.searchBox)}
className={cx("input bg-transparent", S.searchBox)}
type="text"
placeholder="Filter this list..."
value={searchText}
......
......@@ -196,3 +196,4 @@
}
.text-slate { color: #606E7B; }
.bg-transparent { background-color: transparent }
......@@ -144,8 +144,10 @@ export class Dashboards extends Component {
return (
<LoadingAndErrorWrapper
style={{ backgroundColor: "#f9fbfc" }}
loading={isLoading}
className={cx("relative mx4", {"flex flex-full flex-column": noDashboardsCreated})}
className={cx("relative px4 full-height", {"flex flex-full flex-column": noDashboardsCreated})}
noBackground
>
{ modalOpen ? this.renderCreateDashboardModal() : null }
<div className="flex align-center pt4 pb1">
......
......@@ -56,7 +56,7 @@ export function isCardDirty(card, originalCard) {
}
} else {
const origCardSerialized = originalCard ? serializeCardForUrl(originalCard) : null;
const newCardSerialized = card ? serializeCardForUrl(card) : null;
const newCardSerialized = card ? serializeCardForUrl(_.omit(card, 'original_card_id')) : null;
return (newCardSerialized !== origCardSerialized);
}
}
......
import { isCardDirty, serializeCardForUrl, deserializeCardFromUrl } from "./card";
const CARD_ID = 31;
// TODO Atte Keinänen 8/5/17: Create a reusable version `getCard` for reducing test code duplication
const getCard = ({
newCard = false,
hasOriginalCard = false,
isNative = false,
database = 1,
display = "table",
queryFields = {},
table = undefined,
}) => {
const savedCardFields = {
name: "Example Saved Question",
description: "For satisfying your craving for information",
created_at: "2017-04-20T16:52:55.353Z",
id: CARD_ID
};
return {
"name": null,
"display": display,
"visualization_settings": {},
"dataset_query": {
"database": database,
"type": isNative ? "native" : "query",
...(!isNative ? {
query: {
...(table ? {"source_table": table} : {}),
...queryFields
}
} : {}),
...(isNative ? {
native: { query: "SELECT * FROM ORDERS"}
} : {})
},
...(newCard ? {} : savedCardFields),
...(hasOriginalCard ? {"original_card_id": CARD_ID} : {})
};
};
describe("browser", () => {
describe("isCardDirty", () => {
it("should consider a new card clean if no db table or native query is defined", () => {
expect(isCardDirty(
getCard({newCard: true}),
null
)).toBe(false);
});
it("should consider a new card dirty if a db table is chosen", () => {
expect(isCardDirty(
getCard({newCard: true, table: 5}),
null
)).toBe(true);
});
it("should consider a new card dirty if there is any content on the native query", () => {
expect(isCardDirty(
getCard({newCard: true, table: 5}),
null
)).toBe(true);
});
it("should consider a saved card and a matching original card identical", () => {
expect(isCardDirty(
getCard({hasOriginalCard: true}),
getCard({hasOriginalCard: false})
)).toBe(false);
});
it("should consider a saved card dirty if the current card doesn't match the last saved version", () => {
expect(isCardDirty(
getCard({hasOriginalCard: true, queryFields: [["field-id", 21]]}),
getCard({hasOriginalCard: false})
)).toBe(true);
});
});
describe("serializeCardForUrl", () => {
it("should include `original_card_id` property to the serialized URL", () => {
const cardAfterSerialization =
deserializeCardFromUrl(serializeCardForUrl(getCard({hasOriginalCard: true})));
expect(cardAfterSerialization).toHaveProperty("original_card_id", CARD_ID)
})
})
});
/*global ace*/
import React from 'react'
import { createAction } from "redux-actions";
import _ from "underscore";
import { assocIn } from "icepick";
......@@ -17,6 +17,7 @@ import { isPK, isFK } from "metabase/lib/types";
import Utils from "metabase/lib/utils";
import { getEngineNativeType, formatJsonQuery } from "metabase/lib/engine";
import { defer } from "metabase/lib/promise";
import { addUndo } from "metabase/redux/undo";
import { applyParameters, cardIsEquivalent } from "metabase/meta/Card";
import { getParameters, getTableMetadata, getNativeDatabases } from "./selectors";
......@@ -29,8 +30,7 @@ import { MetabaseApi, CardApi, UserApi } from "metabase/services";
import { parse as urlParse } from "url";
import querystring from "querystring";
export const SET_CURRENT_STATE = "metabase/qb/SET_CURRENT_STATE";
const setCurrentState = createAction(SET_CURRENT_STATE);
export const SET_CURRENT_STATE = "metabase/qb/SET_CURRENT_STATE"; const setCurrentState = createAction(SET_CURRENT_STATE);
export const POP_STATE = "metabase/qb/POP_STATE";
export const popState = createThunkAction(POP_STATE, (location) =>
......@@ -481,12 +481,20 @@ export const reloadCard = createThunkAction(RELOAD_CARD, () => {
});
// setCardAndRun
// Used when navigating browser history, when drilling through in visualizations / action widget,
// and when having the entity details view open and clicking its cells
export const SET_CARD_AND_RUN = "metabase/qb/SET_CARD_AND_RUN";
export const setCardAndRun = createThunkAction(SET_CARD_AND_RUN, (nextCard, shouldUpdateUrl = true) => {
return async (dispatch, getState) => {
// clone
const card = Utils.copy(nextCard);
const originalCard = card.original_card_id ? await loadCard(card.original_card_id) : card;
const originalCard = card.original_card_id ?
// If the original card id is present, dynamically load its information for showing lineage
await loadCard(card.original_card_id)
// Otherwise, use a current card as the original card if the card has been saved
// This is needed for checking whether the card is in dirty state or not
: (card.id ? card : null);
dispatch(loadMetadataForCard(card));
......@@ -1118,6 +1126,40 @@ export const loadObjectDetailFKReferences = createThunkAction(LOAD_OBJECT_DETAIL
};
});
// TODO - this is pretty much a duplicate of SET_ARCHIVED in questions/questions.js
// unfortunately we have to do this because that action relies on its part of the store
// for the card lookup
// A simplified version of a similar method in questions/questions.js
function createUndo(type, action) {
return {
type: type,
count: 1,
message: (undo) => // eslint-disable-line react/display-name
<div> { "Question was " + type + "."} </div>,
actions: [action]
};
}
export const ARCHIVE_QUESTION = 'metabase/qb/ARCHIVE_QUESTION';
export const archiveQuestion = createThunkAction(ARCHIVE_QUESTION, (questionId, archived = true) =>
async (dispatch, getState) => {
let card = {
...getState().qb.card, // grab the current card
archived
}
let response = await CardApi.update(card)
dispatch(addUndo(createUndo(
archived ? "archived" : "unarchived",
archiveQuestion(card.id, !archived)
)));
dispatch(push('/questions'))
return response
}
)
// these are just temporary mappings to appease the existing QB code and it's naming prefs
export const toggleDataReferenceFn = toggleDataReference;
......
......@@ -7,7 +7,6 @@ import QueryModeButton from "./QueryModeButton.jsx";
import ActionButton from 'metabase/components/ActionButton.jsx';
import AddToDashSelectDashModal from 'metabase/containers/AddToDashSelectDashModal.jsx';
import ButtonBar from "metabase/components/ButtonBar.jsx";
import DeleteQuestionModal from 'metabase/components/DeleteQuestionModal.jsx';
import HeaderBar from "metabase/components/HeaderBar.jsx";
import HistoryModal from "metabase/components/HistoryModal.jsx";
import Icon from "metabase/components/Icon.jsx";
......@@ -16,6 +15,7 @@ import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
import QuestionSavedModal from 'metabase/components/QuestionSavedModal.jsx';
import Tooltip from "metabase/components/Tooltip.jsx";
import MoveToCollection from "metabase/questions/containers/MoveToCollection.jsx";
import ArchiveQuestionModal from "metabase/query_builder/containers/ArchiveQuestionModal"
import SaveQuestionModal from 'metabase/containers/SaveQuestionModal.jsx';
......@@ -29,6 +29,7 @@ import * as Urls from "metabase/lib/urls";
import cx from "classnames";
import _ from "underscore";
export default class QueryHeader extends Component {
constructor(props, context) {
super(props, context);
......@@ -256,22 +257,7 @@ export default class QueryHeader extends Component {
// delete button
buttonSections.push([
<Tooltip key="delete" tooltip="Delete">
<ModalWithTrigger
ref="deleteModal"
triggerElement={
<span className="text-brand-hover">
<Icon name="trash" size={16} />
</span>
}
>
<DeleteQuestionModal
card={this.props.card}
deleteCardFn={this.onDelete}
onClose={() => this.refs.deleteModal.toggle()}
/>
</ModalWithTrigger>
</Tooltip>
<ArchiveQuestionModal questionId={this.props.card.id} />
]);
buttonSections.push([
......
import React, { Component } from "react"
import { connect } from "react-redux"
import Button from "metabase/components/Button"
import Icon from "metabase/components/Icon"
import ModalWithTrigger from "metabase/components/ModalWithTrigger"
import Tooltip from "metabase/components/Tooltip"
import { archiveQuestion } from "metabase/query_builder/actions"
const mapStateToProps = () => ({})
const mapDispatchToProps = {
archiveQuestion
}
@connect(mapStateToProps, mapDispatchToProps)
class ArchiveQuestionModal extends Component {
onArchive = async () => {
try {
await this.props.archiveQuestion()
this.onClose();
} catch (error) {
console.error(error)
this.setState({ error })
}
}
onClose = () => {
if (this.refs.archiveModal) {
this.refs.archiveModal.close();
}
}
render () {
return (
<ModalWithTrigger
ref="archiveModal"
triggerElement={
<Tooltip key="archive" tooltip="Archive">
<span className="text-brand-hover">
<Icon name="archive" size={16} />
</span>
</Tooltip>
}
title="Archive this question?"
footer={[
<Button key='cancel' onClick={this.onClose}>Cancel</Button>,
<Button key='archive' warning onClick={this.onArchive}>Archive</Button>
]}
>
<div className="px4 pb4">This question will be removed from any dashboards or pulses using it.</div>
</ModalWithTrigger>
)
}
}
export default ArchiveQuestionModal
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