Merge pull request #704 from rehandalal/js-refactor

Implement refactored state tree
This commit is contained in:
Andy Mikulski 2017-05-05 12:35:15 -06:00 коммит произвёл GitHub
Родитель be1256eee3 f39acceb03
Коммит 186a212dec
41 изменённых файлов: 1056 добавлений и 22 удалений

Просмотреть файл

@ -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",