зеркало из https://github.com/mozilla/normandy.git
Merge pull request #704 from rehandalal/js-refactor
Implement refactored state tree
This commit is contained in:
Коммит
186a212dec
|
@ -12,6 +12,7 @@
|
|||
"classnames": "2.2.5",
|
||||
"cssmin": "0.4.3",
|
||||
"font-awesome": "4.6.3",
|
||||
"immutable": "3.8.1",
|
||||
"jexl": "1.1.4",
|
||||
"jquery": "3.1.0",
|
||||
"json-editor": "0.7.28",
|
||||
|
@ -62,6 +63,7 @@
|
|||
"file-loader": "0.9.0",
|
||||
"imports-loader": "0.6.5",
|
||||
"jasmine-core": "2.5.0",
|
||||
"jasmine-immutable-matchers": "1.0.1",
|
||||
"jasmine-promises": "0.4.1",
|
||||
"karma": "1.2.0",
|
||||
"karma-firefox-launcher": "1.0.0",
|
||||
|
|
|
@ -12,6 +12,8 @@ import recipesReducer from 'control/reducers/RecipesReducer';
|
|||
import notificationReducer from 'control/reducers/NotificationReducer';
|
||||
import userReducer from 'control/reducers/UserReducer';
|
||||
|
||||
import newState from '../state';
|
||||
|
||||
export default combineReducers({
|
||||
columns: columnReducer,
|
||||
controlApp: controlAppReducer,
|
||||
|
@ -21,4 +23,5 @@ export default combineReducers({
|
|||
user: userReducer,
|
||||
recipes: recipesReducer,
|
||||
routing: routerReducer,
|
||||
newState,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/* Keep this list alphabetized */
|
||||
|
||||
export const ACTION_RECEIVE = 'ACTION_RECEIVE';
|
||||
export const APPROVAL_REQUEST_RECEIVE = 'APPROVAL_REQUEST_RECEIVE';
|
||||
export const RECIPE_RECEIVE = 'RECIPE_RECEIVE';
|
||||
export const RECIPE_FILTERS_RECEIVE = 'RECIPE_FILTERS_RECEIVE';
|
||||
export const RECIPE_HISTORY_RECEIVE = 'RECIPE_HISTORY_RECEIVE';
|
||||
export const REQUEST_FAILURE = 'REQUEST_FAILURE';
|
||||
export const REQUEST_SEND = 'REQUEST_SEND';
|
||||
export const REQUEST_SUCCESS = 'REQUEST_SUCCESS';
|
||||
export const REVISION_RECEIVE = 'REVISION_RECEIVE';
|
|
@ -0,0 +1,35 @@
|
|||
import {
|
||||
ACTION_RECEIVE,
|
||||
} from '../action-types';
|
||||
|
||||
import {
|
||||
makeApiRequest,
|
||||
} from '../requests/actions';
|
||||
|
||||
|
||||
export function fetchAction(name) {
|
||||
return async dispatch => {
|
||||
const requestId = `fetch-action-${name}`;
|
||||
const action = await dispatch(makeApiRequest(requestId, `action/${name}/`));
|
||||
|
||||
dispatch({
|
||||
type: ACTION_RECEIVE,
|
||||
action,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function fetchAllActions() {
|
||||
return async dispatch => {
|
||||
const requestId = 'fetch-all-actions';
|
||||
const actions = await dispatch(makeApiRequest(requestId, 'action/'));
|
||||
|
||||
actions.forEach(action => {
|
||||
dispatch({
|
||||
type: ACTION_RECEIVE,
|
||||
action,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { fromJS, Map } from 'immutable';
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import {
|
||||
ACTION_RECEIVE,
|
||||
} from '../action-types';
|
||||
|
||||
|
||||
function items(state = new Map(), action) {
|
||||
switch (action.type) {
|
||||
case ACTION_RECEIVE:
|
||||
return state.set(action.action.id, fromJS(action.action));
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
items,
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
/* eslint import/prefer-default-export: "off" */
|
||||
|
||||
export function getAction(state, id, defaultsTo) {
|
||||
return state.newState.actions.items.get(id, defaultsTo);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import {
|
||||
APPROVAL_REQUEST_RECEIVE,
|
||||
} from '../action-types';
|
||||
|
||||
import {
|
||||
makeApiRequest,
|
||||
} from '../requests/actions';
|
||||
|
||||
|
||||
export function fetchApprovalRequest(pk) {
|
||||
return async dispatch => {
|
||||
const requestId = `fetch-approval-request-${pk}`;
|
||||
const response = dispatch(makeApiRequest(requestId, `approval_request/${pk}/`));
|
||||
const approvalRequest = await response;
|
||||
|
||||
dispatch({
|
||||
type: APPROVAL_REQUEST_RECEIVE,
|
||||
approvalRequest,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function fetchAllApprovalRequests() {
|
||||
return async dispatch => {
|
||||
const requestId = 'fetch-all-approval-requests';
|
||||
const approvalRequests = await dispatch(makeApiRequest(requestId, 'approval_request/'));
|
||||
|
||||
approvalRequests.forEach(approvalRequest => {
|
||||
dispatch({
|
||||
type: APPROVAL_REQUEST_RECEIVE,
|
||||
approvalRequest,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { fromJS, Map } from 'immutable';
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import {
|
||||
APPROVAL_REQUEST_RECEIVE,
|
||||
} from '../action-types';
|
||||
|
||||
|
||||
function items(state = new Map(), action) {
|
||||
switch (action.type) {
|
||||
case APPROVAL_REQUEST_RECEIVE:
|
||||
return state.set(action.approvalRequest.id, fromJS(action.approvalRequest));
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
items,
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
/* eslint import/prefer-default-export: "off" */
|
||||
|
||||
export function getApprovalRequest(state, id, defaultsTo) {
|
||||
return state.newState.approvalRequests.items.get(id, defaultsTo);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/* eslint import/prefer-default-export: "off" */
|
||||
|
||||
import { Map } from 'immutable';
|
||||
|
||||
|
||||
export const DEFAULT_REQUEST = new Map({
|
||||
inProgress: false,
|
||||
error: null,
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
import { routerReducer as routing } from 'react-router-redux';
|
||||
import { combineReducers } from 'redux';
|
||||
import { reducer as form } from 'redux-form';
|
||||
|
||||
import actions from './actions/reducers';
|
||||
import approvalRequests from './approvalRequests/reducers';
|
||||
import recipes from './recipes/reducers';
|
||||
import requests from './requests/reducers';
|
||||
import revisions from './revisions/reducers';
|
||||
|
||||
|
||||
const reducer = combineReducers({
|
||||
actions,
|
||||
approvalRequests,
|
||||
form,
|
||||
recipes,
|
||||
requests,
|
||||
revisions,
|
||||
routing,
|
||||
});
|
||||
|
||||
export default reducer;
|
|
@ -0,0 +1,72 @@
|
|||
import {
|
||||
RECIPE_RECEIVE,
|
||||
RECIPE_FILTERS_RECEIVE,
|
||||
RECIPE_HISTORY_RECEIVE,
|
||||
REVISION_RECEIVE,
|
||||
} from '../action-types';
|
||||
|
||||
import {
|
||||
makeApiRequest,
|
||||
} from '../requests/actions';
|
||||
|
||||
|
||||
export function fetchRecipe(pk) {
|
||||
return async dispatch => {
|
||||
const requestId = `fetch-recipe-${pk}`;
|
||||
const recipe = await dispatch(makeApiRequest(requestId, `recipe/${pk}/`));
|
||||
|
||||
dispatch({
|
||||
type: RECIPE_RECEIVE,
|
||||
recipe,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function fetchAllRecipes() {
|
||||
return async dispatch => {
|
||||
const requestId = 'fetch-all-recipes';
|
||||
const recipes = await dispatch(makeApiRequest(requestId, 'recipe/'));
|
||||
|
||||
recipes.forEach(recipe => {
|
||||
dispatch({
|
||||
type: RECIPE_RECEIVE,
|
||||
recipe,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function fetchRecipeHistory(pk) {
|
||||
return async dispatch => {
|
||||
const requestId = `fetch-recipe-history-${pk}`;
|
||||
const revisions = await dispatch(makeApiRequest(requestId, `recipe/${pk}/history/`));
|
||||
|
||||
dispatch({
|
||||
type: RECIPE_HISTORY_RECEIVE,
|
||||
recipeId: pk,
|
||||
revisions,
|
||||
});
|
||||
|
||||
revisions.forEach(revision => {
|
||||
dispatch({
|
||||
type: REVISION_RECEIVE,
|
||||
revision,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function fetchRecipeFilters() {
|
||||
return async dispatch => {
|
||||
const requestId = 'fetch-recipe-filters';
|
||||
const filters = await dispatch(makeApiRequest(requestId, 'filters/'));
|
||||
|
||||
dispatch({
|
||||
type: RECIPE_FILTERS_RECEIVE,
|
||||
filters,
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { fromJS, Map } from 'immutable';
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import {
|
||||
RECIPE_RECEIVE,
|
||||
RECIPE_FILTERS_RECEIVE,
|
||||
RECIPE_HISTORY_RECEIVE,
|
||||
} from '../action-types';
|
||||
|
||||
|
||||
function filters(state = new Map(), action) {
|
||||
switch (action.type) {
|
||||
case RECIPE_FILTERS_RECEIVE:
|
||||
return fromJS(action.filters);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function history(state = new Map(), action) {
|
||||
switch (action.type) {
|
||||
case RECIPE_HISTORY_RECEIVE:
|
||||
return state.set(action.recipeId, fromJS(action.revisions.map(revision => revision.id)));
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function items(state = new Map(), action) {
|
||||
switch (action.type) {
|
||||
case RECIPE_RECEIVE:
|
||||
return state.set(action.recipe.id, fromJS(action.recipe));
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
filters,
|
||||
history,
|
||||
items,
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { List } from 'immutable';
|
||||
|
||||
import { getRevision } from '../revisions/selectors';
|
||||
|
||||
|
||||
export function getRecipe(state, id, defaultsTo) {
|
||||
return state.newState.recipes.items.get(id, defaultsTo);
|
||||
}
|
||||
|
||||
|
||||
export function getRecipeHistory(state, id) {
|
||||
const history = state.newState.recipes.history.get(id, new List([]));
|
||||
return history.map(revisionId => getRevision(state, revisionId));
|
||||
}
|
||||
|
||||
|
||||
export function getRecipeFilters(state) {
|
||||
return state.newState.recipes.filters;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint import/prefer-default-export: "off" */
|
||||
|
||||
import { getRequest } from './selectors';
|
||||
|
||||
import {
|
||||
REQUEST_FAILURE,
|
||||
REQUEST_SEND,
|
||||
REQUEST_SUCCESS,
|
||||
} from '../action-types';
|
||||
|
||||
import apiFetch from '../../utils/apiFetch';
|
||||
|
||||
|
||||
export function makeApiRequest(requestId, endpoint, options = {}) {
|
||||
return async (dispatch, getState) => {
|
||||
const request = getRequest(getState(), requestId);
|
||||
|
||||
if (request.inProgress) { return true; }
|
||||
|
||||
dispatch({
|
||||
type: REQUEST_SEND,
|
||||
requestId,
|
||||
});
|
||||
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = await apiFetch(endpoint, options);
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: REQUEST_FAILURE,
|
||||
requestId,
|
||||
error,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: REQUEST_SUCCESS,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { fromJS, Map } from 'immutable';
|
||||
|
||||
import {
|
||||
REQUEST_FAILURE,
|
||||
REQUEST_SEND,
|
||||
REQUEST_SUCCESS,
|
||||
} from '../action-types';
|
||||
|
||||
|
||||
export default function requests(state = new Map(), action) {
|
||||
switch (action.type) {
|
||||
case REQUEST_SEND:
|
||||
return state.set(action.requestId, new Map({
|
||||
inProgress: true,
|
||||
error: null,
|
||||
}));
|
||||
|
||||
case REQUEST_SUCCESS:
|
||||
return state.set(action.requestId, new Map({
|
||||
inProgress: false,
|
||||
error: null,
|
||||
}));
|
||||
|
||||
case REQUEST_FAILURE:
|
||||
return state.set(action.requestId, new Map({
|
||||
inProgress: false,
|
||||
error: fromJS(action.error),
|
||||
}));
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/* eslint import/prefer-default-export: "off" */
|
||||
|
||||
import { DEFAULT_REQUEST } from '../constants';
|
||||
|
||||
|
||||
export function getRequest(state, id, defaultsTo = DEFAULT_REQUEST) {
|
||||
return state.newState.requests.get(id, defaultsTo);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import {
|
||||
REVISION_RECEIVE,
|
||||
} from '../action-types';
|
||||
|
||||
import {
|
||||
makeApiRequest,
|
||||
} from '../requests/actions';
|
||||
|
||||
|
||||
export function fetchRevision(pk) {
|
||||
return async dispatch => {
|
||||
const requestId = `fetch-revision-${pk}`;
|
||||
const revision = await dispatch(makeApiRequest(requestId, `recipe_revision/${pk}/`));
|
||||
|
||||
dispatch({
|
||||
type: REVISION_RECEIVE,
|
||||
revision,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function fetchAllRevisions() {
|
||||
return async dispatch => {
|
||||
const requestId = 'fetch-all-revisions';
|
||||
const revisions = await dispatch(makeApiRequest(requestId, 'recipe_revision/'));
|
||||
|
||||
revisions.forEach(revision => {
|
||||
dispatch({
|
||||
type: REVISION_RECEIVE,
|
||||
revision,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { fromJS, Map } from 'immutable';
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import {
|
||||
REVISION_RECEIVE,
|
||||
} from '../action-types';
|
||||
|
||||
|
||||
function items(state = new Map(), action) {
|
||||
switch (action.type) {
|
||||
case REVISION_RECEIVE:
|
||||
return state.set(action.revision.id, fromJS(action.revision));
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
items,
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
/* eslint import/prefer-default-export: "off" */
|
||||
|
||||
export function getRevision(state, id, defaultsTo) {
|
||||
return state.newState.revisions.items.get(id, defaultsTo);
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
import { Map } from 'immutable';
|
||||
|
||||
|
||||
export const fixtureRecipes = [
|
||||
{
|
||||
id: 1,
|
||||
|
@ -152,6 +155,27 @@ export const initialState = {
|
|||
routing: {
|
||||
locationBeforeTransitions: null,
|
||||
},
|
||||
newState: {
|
||||
actions: {
|
||||
items: new Map(),
|
||||
},
|
||||
approvalRequests: {
|
||||
items: new Map(),
|
||||
},
|
||||
form: {},
|
||||
recipes: {
|
||||
filters: new Map(),
|
||||
history: new Map(),
|
||||
items: new Map(),
|
||||
},
|
||||
requests: new Map(),
|
||||
revisions: {
|
||||
items: new Map(),
|
||||
},
|
||||
routing: {
|
||||
locationBeforeTransitions: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const fixtureRevisions = [
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { Map } from 'immutable';
|
||||
|
||||
|
||||
export const INITIAL_STATE = {
|
||||
items: new Map(),
|
||||
};
|
||||
|
||||
|
||||
export const ACTION = {
|
||||
id: 1,
|
||||
name: 'test-action',
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
ACTION_RECEIVE,
|
||||
} from 'control/state/action-types';
|
||||
import actionsReducer from 'control/state/actions/reducers';
|
||||
|
||||
import {
|
||||
INITIAL_STATE,
|
||||
ACTION,
|
||||
} from '.';
|
||||
|
||||
|
||||
describe('Actions reducer', () => {
|
||||
it('should return initial state by default', () => {
|
||||
expect(actionsReducer(undefined, {})).toEqual(INITIAL_STATE);
|
||||
});
|
||||
|
||||
it('should handle ACTION_RECEIVE', () => {
|
||||
expect(actionsReducer(undefined, {
|
||||
type: ACTION_RECEIVE,
|
||||
action: ACTION,
|
||||
})).toEqual({
|
||||
...INITIAL_STATE,
|
||||
items: INITIAL_STATE.items.set(ACTION.id, fromJS(ACTION)),
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import { getAction } from 'control/state/actions/selectors';
|
||||
|
||||
import {
|
||||
ACTION,
|
||||
} from '.';
|
||||
|
||||
import {
|
||||
INITIAL_STATE,
|
||||
} from '..';
|
||||
|
||||
|
||||
describe('getAction', () => {
|
||||
const STATE = {
|
||||
...INITIAL_STATE,
|
||||
newState: {
|
||||
...INITIAL_STATE.newState,
|
||||
actions: {
|
||||
...INITIAL_STATE.newState.actions,
|
||||
items: INITIAL_STATE.newState.actions.items.set(ACTION.id, fromJS(ACTION)),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should return the action', () => {
|
||||
expect(getAction(STATE, ACTION.id)).toEqual(fromJS(ACTION));
|
||||
});
|
||||
|
||||
it('should return `undefined` for invalid ID', () => {
|
||||
expect(getAction(STATE, 0)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return default value for invalid ID with default provided', () => {
|
||||
expect(getAction(STATE, 0, 'default')).toEqual('default');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import { Map } from 'immutable';
|
||||
|
||||
|
||||
export const INITIAL_STATE = {
|
||||
items: new Map(),
|
||||
};
|
||||
|
||||
|
||||
export const APPROVAL_REQUEST = {
|
||||
id: 1,
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
APPROVAL_REQUEST_RECEIVE,
|
||||
} from 'control/state/action-types';
|
||||
import approvalRequestsReducer from 'control/state/approvalRequests/reducers';
|
||||
|
||||
import {
|
||||
INITIAL_STATE,
|
||||
APPROVAL_REQUEST,
|
||||
} from '.';
|
||||
|
||||
|
||||
describe('Approval requests reducer', () => {
|
||||
it('should return initial state by default', () => {
|
||||
expect(approvalRequestsReducer(undefined, {})).toEqual(INITIAL_STATE);
|
||||
});
|
||||
|
||||
it('should handle APPROVAL_REQUEST_RECEIVE', () => {
|
||||
expect(approvalRequestsReducer(undefined, {
|
||||
type: APPROVAL_REQUEST_RECEIVE,
|
||||
approvalRequest: APPROVAL_REQUEST,
|
||||
})).toEqual({
|
||||
...INITIAL_STATE,
|
||||
items: INITIAL_STATE.items.set(APPROVAL_REQUEST.id, fromJS(APPROVAL_REQUEST)),
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import { getApprovalRequest } from 'control/state/approvalRequests/selectors';
|
||||
|
||||
import {
|
||||
APPROVAL_REQUEST,
|
||||
} from '.';
|
||||
|
||||
import {
|
||||
INITIAL_STATE,
|
||||
} from '..';
|
||||
|
||||
|
||||
describe('getApprovalRequest', () => {
|
||||
const STATE = {
|
||||
...INITIAL_STATE,
|
||||
newState: {
|
||||
...INITIAL_STATE.newState,
|
||||
approvalRequests: {
|
||||
...INITIAL_STATE.newState.approvalRequests,
|
||||
items: INITIAL_STATE.newState.approvalRequests.items.set(
|
||||
APPROVAL_REQUEST.id, fromJS(APPROVAL_REQUEST)
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should return the approval request', () => {
|
||||
expect(getApprovalRequest(STATE, APPROVAL_REQUEST.id)).toEqual(fromJS(APPROVAL_REQUEST));
|
||||
});
|
||||
|
||||
it('should return `undefined` for invalid ID', () => {
|
||||
expect(getApprovalRequest(STATE, 0)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return default value for invalid ID with default provided', () => {
|
||||
expect(getApprovalRequest(STATE, 0, 'default')).toEqual('default');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
/* eslint import/prefer-default-export: "off" */
|
||||
|
||||
import { INITIAL_STATE as actions } from './actions';
|
||||
import { INITIAL_STATE as approvalRequests } from './approvalRequests';
|
||||
import { INITIAL_STATE as recipes } from './recipes';
|
||||
import { INITIAL_STATE as requests } from './requests';
|
||||
import { INITIAL_STATE as revisions } from './revisions';
|
||||
|
||||
|
||||
export const INITIAL_STATE = {
|
||||
newState: {
|
||||
actions,
|
||||
approvalRequests,
|
||||
recipes,
|
||||
requests,
|
||||
revisions,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import { Map } from 'immutable';
|
||||
|
||||
|
||||
export const INITIAL_STATE = {
|
||||
filters: new Map(),
|
||||
history: new Map(),
|
||||
items: new Map(),
|
||||
};
|
||||
|
||||
|
||||
export const RECIPE = {
|
||||
id: 1,
|
||||
};
|
||||
|
||||
export const FILTERS = {
|
||||
status: [
|
||||
{
|
||||
key: 'enabled',
|
||||
value: 'Enabled',
|
||||
},
|
||||
{
|
||||
key: 'disabled',
|
||||
value: 'Disabled',
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
import { fromJS, List } from 'immutable';
|
||||
|
||||
import {
|
||||
RECIPE_RECEIVE,
|
||||
RECIPE_FILTERS_RECEIVE,
|
||||
RECIPE_HISTORY_RECEIVE,
|
||||
} from 'control/state/action-types';
|
||||
import recipesReducer from 'control/state/recipes/reducers';
|
||||
|
||||
import {
|
||||
FILTERS,
|
||||
INITIAL_STATE,
|
||||
RECIPE,
|
||||
} from '.';
|
||||
|
||||
import {
|
||||
REVISION,
|
||||
} from '../revisions';
|
||||
|
||||
|
||||
describe('Recipes reducer', () => {
|
||||
it('should return initial state by default', () => {
|
||||
expect(recipesReducer(undefined, {})).toEqual(INITIAL_STATE);
|
||||
});
|
||||
|
||||
it('should handle RECIPE_RECEIVE', () => {
|
||||
expect(recipesReducer(undefined, {
|
||||
type: RECIPE_RECEIVE,
|
||||
recipe: RECIPE,
|
||||
})).toEqual({
|
||||
...INITIAL_STATE,
|
||||
items: INITIAL_STATE.items.set(RECIPE.id, fromJS(RECIPE)),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle RECIPE_FILTERS_RECEIVE', () => {
|
||||
expect(recipesReducer(undefined, {
|
||||
type: RECIPE_FILTERS_RECEIVE,
|
||||
filters: FILTERS,
|
||||
})).toEqual({
|
||||
...INITIAL_STATE,
|
||||
filters: INITIAL_STATE.filters.merge(fromJS(FILTERS)),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle RECIPE_HISTORY_RECEIVE', () => {
|
||||
expect(recipesReducer(undefined, {
|
||||
type: RECIPE_HISTORY_RECEIVE,
|
||||
recipeId: RECIPE.id,
|
||||
revisions: [REVISION],
|
||||
})).toEqual({
|
||||
...INITIAL_STATE,
|
||||
history: INITIAL_STATE.history.set(RECIPE.id, new List([REVISION.id])),
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
import { fromJS } from 'immutable';
|
||||
import * as matchers from 'jasmine-immutable-matchers';
|
||||
|
||||
import {
|
||||
getRecipe,
|
||||
getRecipeFilters,
|
||||
getRecipeHistory,
|
||||
} from 'control/state/recipes/selectors';
|
||||
|
||||
import {
|
||||
FILTERS,
|
||||
RECIPE,
|
||||
} from '.';
|
||||
|
||||
import {
|
||||
INITIAL_STATE,
|
||||
} from '..';
|
||||
|
||||
import {
|
||||
REVISION,
|
||||
} from '../revisions';
|
||||
|
||||
|
||||
describe('getRecipe', () => {
|
||||
const STATE = {
|
||||
...INITIAL_STATE,
|
||||
newState: {
|
||||
...INITIAL_STATE.newState,
|
||||
recipes: {
|
||||
...INITIAL_STATE.newState.recipes,
|
||||
items: INITIAL_STATE.newState.recipes.items.set(RECIPE.id, fromJS(RECIPE)),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should return the recipe', () => {
|
||||
expect(getRecipe(STATE, RECIPE.id)).toEqual(fromJS(RECIPE));
|
||||
});
|
||||
|
||||
it('should return `undefined` for invalid ID', () => {
|
||||
expect(getRecipe(STATE, 'invalid')).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return default value for invalid ID with default provided', () => {
|
||||
expect(getRecipe(STATE, 'invalid', 'default')).toEqual('default');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('getRecipeFilters', () => {
|
||||
const STATE = {
|
||||
...INITIAL_STATE,
|
||||
newState: {
|
||||
...INITIAL_STATE.newState,
|
||||
recipes: {
|
||||
...INITIAL_STATE.newState.recipes,
|
||||
filters: fromJS(FILTERS),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.addMatchers(matchers);
|
||||
});
|
||||
|
||||
it('should return the list of filters', () => {
|
||||
expect(getRecipeFilters(STATE)).toEqualImmutable(fromJS(FILTERS));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('getRecipeHistory', () => {
|
||||
const STATE = {
|
||||
...INITIAL_STATE,
|
||||
newState: {
|
||||
...INITIAL_STATE.newState,
|
||||
revisions: {
|
||||
...INITIAL_STATE.newState.revisions,
|
||||
items: INITIAL_STATE.newState.revisions.items.set(REVISION.id, fromJS(REVISION)),
|
||||
},
|
||||
recipes: {
|
||||
...INITIAL_STATE.newState.recipes,
|
||||
history: INITIAL_STATE.newState.recipes.history.set(
|
||||
REVISION.recipe.id,
|
||||
fromJS([REVISION.id])
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.addMatchers(matchers);
|
||||
});
|
||||
|
||||
it('should return the list of revisions', () => {
|
||||
expect(getRecipeHistory(STATE, REVISION.recipe.id)).toEqualImmutable(fromJS([REVISION]));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
/* eslint import/prefer-default-export: "off" */
|
||||
|
||||
import { Map } from 'immutable';
|
||||
|
||||
|
||||
export const INITIAL_STATE = new Map();
|
|
@ -0,0 +1,51 @@
|
|||
import { Map } from 'immutable';
|
||||
import * as matchers from 'jasmine-immutable-matchers';
|
||||
|
||||
import {
|
||||
REQUEST_FAILURE,
|
||||
REQUEST_SEND,
|
||||
REQUEST_SUCCESS,
|
||||
} from 'control/state/action-types';
|
||||
import {
|
||||
DEFAULT_REQUEST,
|
||||
} from 'control/state/constants';
|
||||
import requestsReducer from 'control/state/requests/reducers';
|
||||
|
||||
import {
|
||||
INITIAL_STATE,
|
||||
} from '.';
|
||||
|
||||
|
||||
describe('Requests reducer', () => {
|
||||
beforeEach(() => {
|
||||
jasmine.addMatchers(matchers);
|
||||
});
|
||||
|
||||
it('should return initial state by default', () => {
|
||||
expect(requestsReducer(undefined, {})).toEqual(INITIAL_STATE);
|
||||
});
|
||||
|
||||
it('should handle REQUEST_SEND', () => {
|
||||
expect(requestsReducer(undefined, {
|
||||
type: REQUEST_SEND,
|
||||
requestId: 'test',
|
||||
})).toEqualImmutable(INITIAL_STATE.set('test', DEFAULT_REQUEST.set('inProgress', true)));
|
||||
});
|
||||
|
||||
it('should handle REQUEST_SUCCESS', () => {
|
||||
expect(requestsReducer(undefined, {
|
||||
type: REQUEST_SUCCESS,
|
||||
requestId: 'test',
|
||||
})).toEqualImmutable(INITIAL_STATE.set('test', DEFAULT_REQUEST));
|
||||
});
|
||||
|
||||
const ERROR = { message: 'test message' };
|
||||
|
||||
it('should handle REQUEST_FAILURE', () => {
|
||||
expect(requestsReducer(undefined, {
|
||||
type: REQUEST_FAILURE,
|
||||
error: ERROR,
|
||||
requestId: 'test',
|
||||
})).toEqualImmutable(INITIAL_STATE.set('test', DEFAULT_REQUEST.set('error', new Map(ERROR))));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import * as matchers from 'jasmine-immutable-matchers';
|
||||
|
||||
import { DEFAULT_REQUEST } from 'control/state/constants';
|
||||
import { getRequest } from 'control/state/requests/selectors';
|
||||
|
||||
import {
|
||||
INITIAL_STATE,
|
||||
} from '..';
|
||||
|
||||
|
||||
describe('getRequest', () => {
|
||||
const REQUEST = DEFAULT_REQUEST.set('inProgress', true);
|
||||
const STATE = {
|
||||
...INITIAL_STATE,
|
||||
newState: {
|
||||
...INITIAL_STATE.newState,
|
||||
requests: INITIAL_STATE.newState.requests.set('test', REQUEST),
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.addMatchers(matchers);
|
||||
});
|
||||
|
||||
it('should return the request', () => {
|
||||
expect(getRequest(STATE, 'test')).toEqualImmutable(REQUEST);
|
||||
});
|
||||
|
||||
it('should return the DEFAULT_REQUEST object for invalid ID', () => {
|
||||
expect(getRequest(STATE, 'invalid')).toEqualImmutable(DEFAULT_REQUEST);
|
||||
});
|
||||
|
||||
it('should return default value for invalid ID with default provided', () => {
|
||||
expect(getRequest(STATE, 'invalid', 'default')).toEqual('default');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import { Map } from 'immutable';
|
||||
|
||||
|
||||
export const INITIAL_STATE = {
|
||||
items: new Map(),
|
||||
};
|
||||
|
||||
|
||||
export const REVISION = {
|
||||
id: '9f86d081',
|
||||
recipe: {
|
||||
id: 1,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
REVISION_RECEIVE,
|
||||
} from 'control/state/action-types';
|
||||
import revisionsReducer from 'control/state/revisions/reducers';
|
||||
|
||||
import {
|
||||
INITIAL_STATE,
|
||||
REVISION,
|
||||
} from '.';
|
||||
|
||||
|
||||
describe('Revisions reducer', () => {
|
||||
it('should return initial state by default', () => {
|
||||
expect(revisionsReducer(undefined, {})).toEqual(INITIAL_STATE);
|
||||
});
|
||||
|
||||
it('should handle REVISION_RECEIVE', () => {
|
||||
expect(revisionsReducer(undefined, {
|
||||
type: REVISION_RECEIVE,
|
||||
revision: REVISION,
|
||||
})).toEqual({
|
||||
...INITIAL_STATE,
|
||||
items: INITIAL_STATE.items.set(REVISION.id, fromJS(REVISION)),
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import { getRevision } from 'control/state/revisions/selectors';
|
||||
|
||||
import {
|
||||
REVISION,
|
||||
} from '.';
|
||||
|
||||
import {
|
||||
INITIAL_STATE,
|
||||
} from '..';
|
||||
|
||||
|
||||
describe('getRevision', () => {
|
||||
const STATE = {
|
||||
...INITIAL_STATE,
|
||||
newState: {
|
||||
...INITIAL_STATE.newState,
|
||||
revisions: {
|
||||
...INITIAL_STATE.newState.revisions,
|
||||
items: INITIAL_STATE.newState.revisions.items.set(REVISION.id, fromJS(REVISION)),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should return the revision', () => {
|
||||
expect(getRevision(STATE, REVISION.id)).toEqual(fromJS(REVISION));
|
||||
});
|
||||
|
||||
it('should return `undefined` for invalid ID', () => {
|
||||
expect(getRevision(STATE, 'invalid')).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return default value for invalid ID with default provided', () => {
|
||||
expect(getRevision(STATE, 'invalid', 'default')).toEqual('default');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
const API_ROOT = '/api/v1/';
|
||||
|
||||
export default async function apiFetch(url, options = {}) {
|
||||
let queryString = '';
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append('Accept', 'application/json');
|
||||
headers.append('Content-Type', 'application/json');
|
||||
headers.append('X-CSRFToken', document.getElementsByTagName('html')[0].dataset.csrf);
|
||||
|
||||
const settings = {
|
||||
headers,
|
||||
credentials: 'same-origin',
|
||||
method: 'GET',
|
||||
...options,
|
||||
};
|
||||
|
||||
// Convert `data` to `body` or querystring if necessary.
|
||||
if ('data' in settings) {
|
||||
if ('body' in settings) {
|
||||
throw new Error('Only pass one of `settings.data` and `settings.body`.');
|
||||
}
|
||||
|
||||
if (['GET', 'HEAD'].includes(settings.method.toUpperCase())) {
|
||||
queryString = '?';
|
||||
Object.keys(settings.data).forEach(key => {
|
||||
queryString += `${key}=${encodeURIComponent(settings.data[key])}&`;
|
||||
});
|
||||
queryString.slice(0, -1);
|
||||
} else {
|
||||
settings.body = JSON.stringify(settings.data);
|
||||
}
|
||||
|
||||
delete settings.data;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_ROOT}${url}${queryString}`, settings);
|
||||
return response.json();
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
from pyjexl import JEXL
|
||||
from rest_framework import serializers
|
||||
from reversion.models import Version
|
||||
|
||||
from normandy.base.api.serializers import UserSerializer
|
||||
from normandy.recipes.api.fields import ActionImplementationHyperlinkField
|
||||
|
@ -159,15 +158,10 @@ class RecipeSerializer(serializers.ModelSerializer):
|
|||
return value
|
||||
|
||||
|
||||
class ClientSerializer(serializers.Serializer):
|
||||
country = serializers.CharField()
|
||||
request_time = serializers.DateTimeField()
|
||||
|
||||
|
||||
class RecipeRevisionSerializer(serializers.ModelSerializer):
|
||||
date_created = serializers.DateTimeField(source='created', read_only=True)
|
||||
comment = serializers.CharField(read_only=True)
|
||||
recipe = RecipeSerializer(source='serializable_recipe', read_only=True)
|
||||
comment = serializers.CharField(read_only=True)
|
||||
approval_request = ApprovalRequestSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
@ -181,20 +175,9 @@ class RecipeRevisionSerializer(serializers.ModelSerializer):
|
|||
]
|
||||
|
||||
|
||||
class RecipeVersionSerializer(serializers.ModelSerializer):
|
||||
date_created = serializers.DateTimeField(source='revision.date_created', read_only=True)
|
||||
comment = serializers.CharField(source='revision.comment', read_only=True)
|
||||
recipe = RecipeSerializer(source='_object_version.object', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Version
|
||||
fields = [
|
||||
'id',
|
||||
'date_created',
|
||||
'recipe',
|
||||
'comment',
|
||||
'approval_status'
|
||||
]
|
||||
class ClientSerializer(serializers.Serializer):
|
||||
country = serializers.CharField()
|
||||
request_time = serializers.DateTimeField()
|
||||
|
||||
|
||||
class SignatureSerializer(serializers.ModelSerializer):
|
||||
|
|
|
@ -753,7 +753,8 @@ class TestApprovalFlow(object):
|
|||
api_client.force_authenticate(user2)
|
||||
res = api_client.post('/api/v1/approval_request/{}/reject/'.format(approval_data['id']),
|
||||
{'comment': 'r-'})
|
||||
recipe_data_2['approval_request'] = res.json()
|
||||
approval_data = res.json()
|
||||
recipe_data_2['approval_request'] = approval_data
|
||||
assert res.status_code == 200
|
||||
|
||||
# The change should not be visible yet, since it isn't approved
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"classnames": "2.2.5",
|
||||
"cssmin": "0.4.3",
|
||||
"font-awesome": "4.6.3",
|
||||
"immutable": "3.8.1",
|
||||
"jexl": "1.1.4",
|
||||
"jquery": "3.1.0",
|
||||
"json-editor": "0.7.28",
|
||||
|
@ -69,6 +70,7 @@
|
|||
"file-loader": "0.9.0",
|
||||
"imports-loader": "0.6.5",
|
||||
"jasmine-core": "2.5.0",
|
||||
"jasmine-immutable-matchers": "1.0.1",
|
||||
"jasmine-promises": "0.4.1",
|
||||
"karma": "1.2.0",
|
||||
"karma-firefox-launcher": "1.0.0",
|
||||
|
|
Загрузка…
Ссылка в новой задаче