From c1e678df2622d9b2b29e02317b7291d512f7bf2c Mon Sep 17 00:00:00 2001 From: Aniko Litvanyi Date: Mon, 28 Aug 2017 10:10:05 +0200 Subject: [PATCH 1/9] Merge release/3.0.1 branch (#35) (#36) * fix(Epics): Fix checkoutLoginState at failure checkoutLoginState epic is now return null instead of error message when user is not authenticated * chore: Update version number --- package.json | 2 +- src/Epics.ts | 2 +- test/EpicsTests.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0758cd9..6c6edb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sn-redux", - "version": "3.0.0", + "version": "3.0.1", "description": "A set of redux actions, reducers and redux-ovbservable epics for Sense/Net ECM", "main": "dist/src/sn-redux.js", "scripts": { diff --git a/src/Epics.ts b/src/Epics.ts index 5ba2d34..72a7c94 100644 --- a/src/Epics.ts +++ b/src/Epics.ts @@ -300,7 +300,7 @@ export module Epics { return result === Authentication.LoginState.Authenticated ? Actions.UserLoginSuccess(result) : - Actions.UserLoginFailure({ message: 'Failed to log in.' }); + Actions.UserLoginFailure({ message: null }); }) }) } diff --git a/test/EpicsTests.ts b/test/EpicsTests.ts index 1171d7a..5f3dcad 100644 --- a/test/EpicsTests.ts +++ b/test/EpicsTests.ts @@ -600,7 +600,7 @@ describe('Epics', () => { store.dispatch({ type: 'CHECK_LOGIN_STATE_REQUEST' }); expect(store.getActions()).to.be.deep.eq([ { type: 'CHECK_LOGIN_STATE_REQUEST' }, - { type: 'USER_LOGIN_FAILURE', message: 'Failed to log in.' }]); + { type: 'USER_LOGIN_FAILURE', message: null }]); }) }); }); \ No newline at end of file From 454519acb2c0aa768d222547a7c2e8d0515631f7 Mon Sep 17 00:00:00 2001 From: Aniko Litvanyi Date: Thu, 31 Aug 2017 13:34:22 +0200 Subject: [PATCH 2/9] [KFI]chore: Update version number --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index 84fcf33..107f52a 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,6 @@ { "name": "sn-redux", -<<<<<<< HEAD "version": "3.1.1", -======= - "version": "3.0.1", ->>>>>>> c1e678df2622d9b2b29e02317b7291d512f7bf2c "description": "A set of redux actions, reducers and redux-ovbservable epics for Sense/Net ECM", "main": "dist/src/sn-redux.js", "scripts": { From 8107c1361484d504d97a543579773699e6a285a6 Mon Sep 17 00:00:00 2001 From: Lajos Date: Mon, 6 Nov 2017 13:42:20 +0100 Subject: [PATCH 3/9] sn-client-js update (#49) * chore(package): update @types/redux-mock-store to version 0.0.11 (#37) * chore(package): update semantic-release to version 8.0.0 (#38) * Greenkeeper/sn client js 2.4.0 (#40) * chore(package): update sn-client-js to version 2.4.0 * [KFI]chore(launch.json): fixed mocha debug profile for vs code * [KFI]test(EpicsTest): Flatterned Select assertions, added RELOAD_CONTENTFIELDS_REQUEST field list (r * Release/3.2.0 (#41) * [KFI]fix(Epics): Remove requestContent call from the initSensenetStoreEpic * [KFI]fix(Reducers): Fix userAvatarPath For now userAvatarPath contains the Avatar field's value so it doesn't matter if the avatar is a binary or a reference * [KFI]test(ReducerTests): Fix userAvatarPath test * [KFI]fix(Reducers): Fix fetch getError and order reducers * [KFI]feat(Actions): Add two new Actions for selection and deselecting a Content * [KFI]feat(Reducers): Change selected reducer to handle select and deselect actions * [KFI]test(Actions): Add tests for select and deselect actions * [KFI]test(Reducers): Add test for testing the selected Reducer handling select and deselect Actions * [KFI]feat(Actions): Add actions for getting sn Actions of a content * [KFI]test(Actions): Add tests for testing the new sn action getter Actions * [KFI]feat(Reducers): Complete the Reducer of the content items * [KFI]test(Reducers): Add test for testing the childrenactions reducer * [KFI]feat(Reducers): Add a isOpened reducer This reducer holds the id of the content where the actionmenu was opened last * [KFI]fix(Reducers): Change action in isOpened reducer to REQUEST_CONTENT_ACTION * [KFI]test(Reducers): Add test for testing isOpened reducer * [KFI]feat(Reducers): Add a function to return the currently opened items id * [KFI]refactor(Reducer): Rename getOpenedContentId to getOpenedContent * [KFI]test(Reducers): Add a test for testing getOpenedContent function * [KFI]feat(Actions): Add id as input attr to RequestContentActions Action * [KFI]feat(Reducers): Add getChildrenActions function to return actions from the state tree * [KFI]feat(Actions): Change RequestContentActions first input param to content * [KFI]feat(Epics): Add getContentActions Epic * [KFI]fix(GetActions): Fix GetActions action * [KFI]feat(Reducers): Add getCurrentContent to get the path of the current content * [KFI]fix(Epics): Fix loadContentActions epic * [KFI]test: Improve epic tests * [KFI]chore: Update version number * [KFI]fix(Login): Fix action order and subscribing in the login process * [KFI]test(Login): Add tests for testing the new login buffer action and epic * [KFI]chore: Update version number * chore(package): update mocha to version 4.0.0 (#42) * chore(package): update typedoc to version 0.9.0 (#43) * Merge release/3.2.2 (#44) * [KFI]fix(Reducers): Fix entity list after update success action * [KFI]test(Reducers): Add test for checking the entity list after update success * [KFI]chore: Update version number * [KFI]chore: Update version number * [KFI]feat(Actions): Add an action to clear the selected reducer * [KFI]test(Actions): Add test for testing the new clear selection action * [KFI]chore: Update version number to 3.2.2 * Fileupload (#45) * [KFI]feat(Actions): Add upload request, success and failure actions * [KFI]test(Actions): Add tests for testing the new upload related actions * [KFI]feat(Epics): Add new epic for upload a file * [KFI]test(Epics): Add test for testing the upload epic * [KFI]feat(Reducers): Change ids and entities reducer to handle UPLOAD_CONTENT_SUCCESS * [KFI]test(Reducers): Add upload related reducer tests * [KFI]chore: Update sn-client-js to 2.5.0 * [KFI]test(Epics): Fix epic tests related to sn-client-js upgrade * [KFI]chore: Update version number to 3.3.0 * [KFI]fix(Epics): Add rxjs mergeMap import * merge fix * [KFI]refactor(project): replaced '@reactivex/rxjs' package with 'rxjs', updated imports --- .vscode/launch.json | 54 +++-- package.json | 15 +- src/Actions.ts | 153 ++++++++++-- src/Epics.ts | 80 +++++- src/Reducers.ts | 110 ++++++--- test/ActionsTests.ts | 220 ++++++++++++++--- test/EpicsTests.ts | 547 ++++++++++++++++++++++++++---------------- test/ReducersTests.ts | 246 +++++++++++++++++-- 8 files changed, 1056 insertions(+), 369 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d6a3d68..fe89608 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,27 +1,31 @@ { - // Use IntelliSense to learn about possible Node.js debug attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Debug tests", - "runtimeExecutable": "mocha", - "windows": { - "runtimeExecutable": "mocha.cmd" + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Mocha Tests", + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "args": [ + "-u", + "tdd", + "--timeout", + "999999", + "--colors", + "-p", + "${workspaceRoot}\\tsconfig.json", + "${workspaceRoot}/dist/test/index.js" + ], + "internalConsoleOptions": "openOnSessionStart" }, - "preLaunchTask": "build", - "runtimeArgs": [ - "--debug-brk", - "./dist/test/index.js" - ], - "program": "${workspaceRoot}\\test\\index.ts", - "outFiles": [ - "${workspaceRoot}\\dist\\**\\*.js" - ], - "port": 5858 - } - ] -} \ No newline at end of file + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceRoot}\\dist\\src\\sn-redux.js", + "outFiles": [ + "${workspaceRoot}/out/**/*.js" + ] + } + ] + } \ No newline at end of file diff --git a/package.json b/package.json index 107f52a..56a35a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sn-redux", - "version": "3.1.1", + "version": "3.3.1", "description": "A set of redux actions, reducers and redux-ovbservable epics for Sense/Net ECM", "main": "dist/src/sn-redux.js", "scripts": { @@ -54,14 +54,13 @@ }, "homepage": "https://sensenet.com", "dependencies": { - "@reactivex/rxjs": "^5.4.3", "normalizr": "^3.2.3", "nyc": "^11.1.0", "redux": "^3.7.2", "redux-logger": "^3.0.6", "redux-observable": "^0.16.0", "rimraf": "^2.6.1", - "rxjs": "^5.4.3", + "rxjs": "^5.5.2", "sensenet-kfi-cz-conventional-changelog": "^1.0.0" }, "devDependencies": { @@ -69,7 +68,7 @@ "@types/mocha": "^2.2.42", "@types/nock": "^8.2.1", "@types/orchestrator": "^0.3.0", - "@types/redux-mock-store": "^0.0.10", + "@types/redux-mock-store": "^0.0.11", "chai": "^4.1.1", "codecov.io": "^0.1.6", "commitizen": "^2.9.6", @@ -78,16 +77,16 @@ "gulp": "^3.9.1", "gulp-rename": "^1.2.2", "gulp-typedoc": "^2.0.3", - "mocha": "^3.5.0", + "mocha": "^4.0.0", "nock": "^9.0.14", "normalizr": "^3.2.3", "redux": "^3.7.2", "redux-mock-store": "^1.2.3", "redux-observable": "^0.16.0", - "semantic-release": "^7.0.2", - "sn-client-js": "^2.3.0", + "semantic-release": "^8.0.0", + "sn-client-js": "^3.0.0-development.2", "tslint": "^5.6.0", - "typedoc": "^0.8.0", + "typedoc": "^0.9.0", "typedoc-md-theme": "^1.0.1", "typedoc-plugin-external-module-name": "^1.0.9", "typescript": "^2.4.2" diff --git a/src/Actions.ts b/src/Actions.ts index 49aca5d..9461fc6 100644 --- a/src/Actions.ts +++ b/src/Actions.ts @@ -1,6 +1,6 @@ import { normalize } from 'normalizr'; import { Schemas } from './Schema'; -import { Content, ODataApi, ODataHelper, Repository } from 'sn-client-js'; +import { Content, IContent, ODataApi, ODataHelper, Repository, ContentTypes } from 'sn-client-js'; /** * Module that contains the action creators. @@ -116,7 +116,7 @@ export module Actions { * @param path {string} Path of the root Content * @param options {OData.IODataParams} Represents an ODataOptions object based on the IODataOptions interface. Holds the possible url parameters as properties. */ - export const InitSensenetStore = (path?: string, options: ODataApi.IODataParams = {}) => ({ + export const InitSensenetStore = (path?: string, options: ODataApi.IODataParams = {}) => ({ type: 'INIT_SENSENET_STORE', path: path ? path : '/Root', options: options @@ -128,7 +128,7 @@ export module Actions { * @param contentType {ContentType} Content Type of the requested content. * @returns {Object} Returns a redux action with the properties type, path, options and contentType. */ - export const RequestContent = (path: string, options: ODataApi.IODataParams = {}, contentType?: { new(...args): T }) => ({ + export const RequestContent = (path: string, options: ODataApi.IODataParams = {}, contentType?: { new(...args): T }) => ({ type: 'FETCH_CONTENT_REQUEST', path, options, @@ -140,7 +140,7 @@ export module Actions { * @param params {string} String with the url params. * @returns {Object} Returns a redux action with the properties type, normalized response and params. */ - export const ReceiveContent = (response: Content[], params: any) => + export const ReceiveContent = (response: IContent[], params: any) => ({ type: 'FETCH_CONTENT_SUCCESS', response: normalize(response, Schemas.arrayOfContent), @@ -164,7 +164,7 @@ export module Actions { * @param contentType {ContentType} Content Type of the requested content. * @returns {Object} Returns a redux action with the properties id, options and contentType. */ - export const LoadContent = (id: number, options: ODataApi.IODataParams = {}, contentType?: { new(...args): T }) => ({ + export const LoadContent = (id: number, options: ODataApi.IODataParams = {}, contentType?: { new(...args): T }) => ({ type: 'LOAD_CONTENT_REQUEST', id, options: options, @@ -176,7 +176,7 @@ export module Actions { * @param params {string} String with the url params. * @returns {Object} Returns a redux action with the properties type, normalized response and params. */ - export const ReceiveLoadedContent = (response: Content, params: any) => + export const ReceiveLoadedContent = (response: Content, params: any) => ({ type: 'LOAD_CONTENT_SUCCESS', response, @@ -198,7 +198,7 @@ export module Actions { * @param content {Content} The requested Content. * @param scenario {string} The Actions should be in the given Scenario */ - export const LoadContentActions = (content: Content, scenario?: string) => ({ + export const LoadContentActions = (content: IContent, scenario?: string) => ({ type: 'LOAD_CONTENT_ACTIONS', content, scenario @@ -225,7 +225,7 @@ export module Actions { * @param actionName {string} Name of the action witch which we want to reload the content (edit, new, etc). * @returns {Object} Returns a redux action with the properties type and actionName. */ - export const ReloadContent = (content: Content, actionName: 'edit' | 'view') => ({ + export const ReloadContent = (content: Content, actionName: 'edit' | 'view') => ({ type: 'RELOAD_CONTENT_REQUEST', content, actionName @@ -255,7 +255,7 @@ export module Actions { * @param fields {any[]} List of the fields to be loaded * @returns {Object} Returns a redux action with the properties type and fields. */ - export const ReloadContentFields = (content: Content, fields: any[]) => ({ + export const ReloadContentFields = (content: Content, fields: any[]) => ({ type: 'RELOAD_CONTENTFIELDS_REQUEST', content, fields @@ -284,7 +284,7 @@ export module Actions { * @param content {Content} Content that have to be created in the Content Respository. * @returns {Object} Returns a redux action with the properties type, path of the parent and content. */ - export const CreateContent = (content: T) => ({ + export const CreateContent = (content: T) => ({ type: 'CREATE_CONTENT_REQUEST', content }); @@ -312,7 +312,7 @@ export module Actions { * @param content {Object} Content object with the field value pairs that have to be modified. * @returns {Object} Returns a redux action with the properties type, id and fields. */ - export const UpdateContent = (content: Partial) => ({ + export const UpdateContent = (content: Partial) => ({ type: 'UPDATE_CONTENT_REQUEST', content }); @@ -341,7 +341,7 @@ export module Actions { * @param permanently {boolean} Defines whether the a Content must be moved to the Trash or deleted permanently. * @returns {Object} Returns a redux action with the properties type, id and permanently. */ - export const Delete = (content: T, permanently: boolean = false) => ({ type: 'DELETE_CONTENT_REQUEST', content, permanently }); + export const Delete = (content: T, permanently: boolean = false) => ({ type: 'DELETE_CONTENT_REQUEST', content, permanently }); /** * Action creator for the step when Content deleted successfully. * @param index {number} Index of the item in the state collection. @@ -398,7 +398,7 @@ export module Actions { * @param content {number} Content that should be checked out. * @returns {Object} Returns a redux action with the properties type and id . */ - export const CheckOut = (content: T) => ({ + export const CheckOut = (content: T) => ({ type: 'CHECKOUT_CONTENT_REQUEST', content }) @@ -425,7 +425,7 @@ export module Actions { * @param content {Content} Content that should be checked in. * @returns {Object} Returns a redux action with the properties type, id and checkinComment. */ - export const CheckIn = (content: T, checkInComment: string = '') => ({ + export const CheckIn = (content: T, checkInComment: string = '') => ({ type: 'CHECKIN_CONTENT_REQUEST', content, checkInComment @@ -453,7 +453,7 @@ export module Actions { * @param content {Content} Content that should be published. * @returns {Object} Returns a redux action with the properties type and id. */ - export const Publish = (content: T) => ({ + export const Publish = (content: T) => ({ type: 'PUBLISH_CONTENT_REQUEST', content }) @@ -480,7 +480,7 @@ export module Actions { * @param content {Content} Content that should be approved. * @returns {Object} Returns a redux action with the properties type and id. */ - export const Approve = (content: T) => ({ + export const Approve = (content: T) => ({ type: 'APPROVE_CONTENT_REQUEST', content }) @@ -508,7 +508,7 @@ export module Actions { * @param rejectReason {string} Reason of rejecting. * @returns {Object} Returns a redux action with the properties type, rejectReason and id. */ - export const Reject = (content: T, rejectReason: string = '') => ({ + export const Reject = (content: T, rejectReason: string = '') => ({ type: 'REJECT_CONTENT_REQUEST', content, rejectReason @@ -536,7 +536,7 @@ export module Actions { * @param content {Content} Content that should be checked in. * @returns {Object} Returns a redux action with the properties type and id. */ - export const UndoCheckout = (content: T) => ({ + export const UndoCheckout = (content: T) => ({ type: 'UNDOCHECKOUT_CONTENT_REQUEST', content }) @@ -563,7 +563,7 @@ export module Actions { * @param content {Content} Content that should be checked in. * @returns {Object} Returns a redux action with the properties type and id. */ - export const ForceUndoCheckout = (content: T) => ({ + export const ForceUndoCheckout = (content: T) => ({ type: 'FORCEUNDOCHECKOUT_CONTENT_REQUEST', content }) @@ -591,7 +591,7 @@ export module Actions { * @param version {string} Specify which old version to restore * @returns {Object} Returns a redux action with the properties type and id. */ - export const RestoreVersion = (content: T, version: string) => ({ + export const RestoreVersion = (content: T, version: string) => ({ type: 'RESTOREVERSION_CONTENT_REQUEST', content, version @@ -633,8 +633,8 @@ export module Actions { /** * Action creator for login a user to a sensenet portal. - * @param userName {string} Login name of the user. - * @param password {string} Password of the user. + * @param {string} userName Login name of the user. + * @param {string} password Password of the user. * @returns {Object} Returns a redux action with the properties userName and password. */ export const UserLogin = (userName: string, password: string) => ({ @@ -642,21 +642,31 @@ export module Actions { userName, password }) + /** + * Action creator for handling a user login success response without a loggedin user. + * @param {boolean} response Response of the login request + * @returns {Object} Returns a redux action with the properties userName and password. + */ + export const UserLoginBuffer = (response: boolean) => ({ + type: 'USER_LOGIN_BUFFER', + response + }) /** * Action creator for the step when a User is logged in successfully. * @param response {any} JSON response of the ajax request. * @returns {Object} Returns a redux action with the user as a response. */ - export const UserLoginSuccess = (response: any) => ({ + + export const UserLoginSuccess = (content: Content) => ({ type: 'USER_LOGIN_SUCCESS', - response: response + response: content }) /** * Action creator for the step when login of a user is failed. * @param error {any} The catched error object. * @returns {Object} Returns a redux action with the properties type and the error message. */ - export const UserLoginFailure = (error: any) => ({ + export const UserLoginFailure = (error: {status?: number, message: string}) => ({ type: 'USER_LOGIN_FAILURE', message: (error.status === 403) ? 'The username or the password is not valid!' : error.message }) @@ -693,4 +703,97 @@ export module Actions { type: 'LOAD_REPOSITORY', repository: repositoryConfig }) + /** + * Action creator for selecting a Content + * @param id {number} The id of the selected Content + * @returns {Object} Returns a redux action. + */ + export const SelectContent = (id) => ({ + type: 'SELECT_CONTENT', + id + }) + /** + * Action creator for deselecting a Content + * @param id {number} The id of the deselected Content + * @returns {Object} Returns a redux action. + */ + export const DeSelectContent = (id) => ({ + type: 'DESELECT_CONTENT', + id + })/** + * Action creator for clearing the array of selected content + * @returns {Object} Returns a redux action. + */ + export const ClearSelection = () => ({ + type: 'CLEAR_SELECTION' + }) + /** + * Action creator for a request for get actions of a content by a given scenario. + * @param content {Content} The name of the scenario + * @param scenario {string} The name of the scenario + * @returns {Object} Returns a redux action. + */ + export const RequestContentActions = (content, scenario?: string) => ({ + type: 'REQUEST_CONTENT_ACTIONS', + content, + scenario + }) + /** + * Action creator for the step getting the actions of a content successfully. + * @param response {any} JSON response of the ajax request. + * @returns {Object} Returns a redux action with a response. + */ + export const RequestContentActionsSuccess = (response: any, id: number) => { + return ({ + type: 'REQUEST_CONTENT_ACTIONS_SUCCESS', + response: response, + id + }) + } + /** + * Action creator for the step when getting the actions of a content is failed + * @param error {any} JSON response of the ajax request. + * @returns {Object} Returns a redux action with a response. + */ + export const RequestContentActionsFailure = (error: any) => ({ + type: 'REQUEST_CONTENT_ACTIONS_FAILURE', + message: error.message + }) + /** + * Action creator for uploading a Content into the Content Repository. + * @param {Content} content The parent Content + * @param file The file that should be uploaded + * @param {ContentTypes.ContentType} [contentType=ContentTypes.File] ContentType of the Content that should be created with the binary (default is File) + * @param {boolean} [overwrite=true] Determines whether the existing file with a same name should be overwritten or not (default is true) + * @param {Object} [body=null] Contains extra stuff to request body + * @param {string} [propertyName='Binary'] Name of the field where the binary should be saved + * @returns {Object} Returns a redux action with the properties type, content, file, contentType, overwrite, body and propertyName. + */ + export const UploadRequest = (content: Content, file, contentType?, overwrite?: boolean, body?, propertyName?: string) => ({ + type: 'UPLOAD_CONTENT_REQUEST', + content, + file, + contentType: contentType || ContentTypes.File, + overwrite: typeof overwrite !== 'undefined' ? overwrite : true, + body: body ? body : null, + propertyName: propertyName ? propertyName : 'Binary' + }) + /** + * Action creator for the step when a content was uploaded successfully. + * @param response {any} JSON response of the ajax request. + * @returns {Object} Returns a redux action with a response. + */ + export const UploadSuccess = (response) => ({ + type: 'UPLOAD_CONTENT_SUCCESS', + response + }) + /** + * Action creator for the step when uploading a content is failed + * @param error {any} JSON response of the ajax request. + * @returns {Object} Returns a redux action with a response. + */ + export const UploadFailure = (error: any) => ({ + type: 'UPLOAD_CONTENT_FAILURE', + message: error.message + }) } \ No newline at end of file diff --git a/src/Epics.ts b/src/Epics.ts index ec82a4b..e6571b8 100644 --- a/src/Epics.ts +++ b/src/Epics.ts @@ -2,8 +2,9 @@ import { Actions } from './Actions'; import { Reducers } from './Reducers'; import { ActionsObservable, combineEpics } from 'redux-observable'; -import { Observable } from '@reactivex/rxjs'; -import { Repository, Content, Collection, ODataApi, Authentication } from 'sn-client-js'; +import { Observable } from 'rxjs/Observable'; +import { Repository, Content, ContentTypes, Collection, ODataApi, Authentication } from 'sn-client-js'; +import 'rxjs/add/operator/mergeMap'; /** * Module for redux-observable Epics of the sensenet built-in OData actions. @@ -43,14 +44,24 @@ export module Epics { export const initSensenetStoreEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { return action$.ofType('INIT_SENSENET_STORE') .mergeMap(action => { + + store.dispatch(Actions.LoadRepository(dependencies.repository.Config)) + // dependencies.repository.Authentication.State.skipWhile(state => state === Authentication.LoginState.Pending) + // .first() + // .map(result => { + dependencies.repository.GetCurrentUser().subscribe(user => { - store.dispatch(Actions.UserChanged(user)) + if (user.Name === 'Visitor') { + store.dispatch(Actions.UserLoginFailure({ message: null })) + } + else { + store.dispatch(Actions.UserChanged(user)) + store.dispatch(Actions.UserLoginSuccess(user)) + } }) - store.dispatch(Actions.CheckLoginState()) - store.dispatch(Actions.LoadRepository(dependencies.repository.Config)) + return dependencies.repository.Load(action.path, action.options) .map((response) => { - store.dispatch(Actions.RequestContent(action.path, action.options)) return Actions.ReceiveLoadedContent(response, action.options) }) .catch(error => { @@ -83,7 +94,7 @@ export module Epics { .mergeMap(action => { return dependencies.repository.Load(action.id, action.options) .map((response) => { - store.dispatch(Actions.LoadContentActions(response, action.scenario)) + //store.dispatch(Actions.LoadContentActions(response, action.scenario)) return Actions.ReceiveLoadedContent(response, action.options) }) .catch(error => { @@ -99,8 +110,9 @@ export module Epics { export const loadContentActionsEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { return action$.ofType('LOAD_CONTENT_ACTIONS') .mergeMap(action => { - return action.content.Actions(action.scenario) - .map(Actions.ReceiveContentActions) + let c = dependencies.repository.HandleLoadedContent(action.content, ContentTypes.GenericContent); + return c.Actions(action.scenario) + .map(result => Actions.ReceiveContentActions(result)) .catch(error => Observable.of(Actions.ReceiveContentActionsFailure(error))) }) } @@ -299,7 +311,7 @@ export module Epics { .first() .map(result => { return result === Authentication.LoginState.Authenticated ? - Actions.UserLoginSuccess(result) + Actions.UserLoginBuffer(true) : Actions.UserLoginFailure({ message: null }); }) @@ -314,15 +326,28 @@ export module Epics { return action$.ofType('USER_LOGIN_REQUEST') .mergeMap(action => { return dependencies.repository.Authentication.Login(action.userName, action.password) + // .combineLatest(dependencies.repository.GetCurrentUser().skipWhile(u => u.Name === 'Visitor')) + // .skipWhile(u => u instanceof ContentTypes.User) + // .first() .map(result => { return result ? - Actions.UserLoginSuccess(result) + Actions.UserLoginBuffer(result) : Actions.UserLoginFailure({ message: 'Failed to log in.' }); }) .catch(error => Observable.of(Actions.UserLoginFailure(error))) }) } + export const userLoginBufferEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { + return action$.ofType('USER_LOGIN_BUFFER') + .mergeMap(action => { + return dependencies.repository.GetCurrentUser().skipWhile(u => u.Name === 'Visitor') + .map(result => { + Actions.UserLoginSuccess(result) + }) + .catch(error => Observable.of(Actions.UserLoginFailure(error))) + }) + } /** * Epic to logout a user from a sensenet portal. It is related to three redux actions, returns ```LogoutUser``` action and sends the response to the * ```LogoutUserSuccess``` action if the ajax request ended successfully or catches the error if the request failed and sends the error message to the ```LogoutUserFailure``` action. @@ -335,6 +360,35 @@ export module Epics { .catch(error => Observable.of(Actions.UserLogoutFailure(error))) }) } + export const getContentActions = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { + return action$.ofType('REQUEST_CONTENT_ACTIONS') + .mergeMap(action => { + let c = dependencies.repository.HandleLoadedContent(action.content, ContentTypes.GenericContent); + return c.Actions(action.scenario) + .map(result => Actions.RequestContentActionsSuccess(result, action.content.Id)) + .catch(error => Observable.of(Actions.RequestContentActionsFailure(error))) + }) + } + /** + * Epic to upload a file to the Content Repository. It is related to three redux actions, returns ```UploadContent``` action and sends the response to the + * ```UploadSuccess``` action if the ajax request ended successfully or catches the error if the request failed and sends the error message to the ```UploadFailure``` action. + */ + export const uploadFileEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { + return action$.ofType('UPLOAD_CONTENT_REQUEST') + .mergeMap(action => { + return action.content.UploadFile({ + File: action.file, + ContentType: action.contentType, + OverWrite: action.overwrite, + Body: action.body, + PropertyName: action.propertyName + }) + .map((response) => { + return Actions.UploadSuccess(response) + }) + .catch(error => Observable.of(Actions.UploadFailure(error))) + }) + } /** * sn-redux root Epic, the main Epic combination that is used on a default sensenet application. Contains Epics related to CRUD operations and thr other built-in sensenet * [OData Actions and Function](http://wiki.sensenet.com/Built-in_OData_actions_and_functions). @@ -358,7 +412,9 @@ export module Epics { restoreversionContentEpic, userLoginEpic, userLogoutEpic, - checkLoginStateEpic + checkLoginStateEpic, + getContentActions, + uploadFileEpic ); } diff --git a/src/Reducers.ts b/src/Reducers.ts index 6c0caec..18d024e 100644 --- a/src/Reducers.ts +++ b/src/Reducers.ts @@ -130,14 +130,13 @@ export module Reducers { export const userAvatarPath = (state = '', action) => { switch (action.type) { case 'USER_CHANGED': - return action.user.ImageData ? action.user.ImageData.__mediaresource.media_src : '' + return action.user.Avatar ? action.user.Avatar._deferred : '' default: return state } } - /** - * Reducer combining userName, fullName and userLanguage into a single object, ```user```. + * Reducer combining userName, fullName, userLanguage, userAvatarPath into a single object, ```user```. */ const user = combineReducers({ userName, @@ -182,6 +181,11 @@ export module Reducers { return action.response.result; case 'CREATE_CONTENT_SUCCESS': return [...state, action.response.result]; + case 'UPLOAD_CONTENT_SUCCESS': + if (state.indexOf(action.response.CreatedContent.Id) === -1) + return [...state, action.response.CreatedContent.Id]; + else + return state case 'DELETE_CONTENT_SUCCESS': return [...state.slice(0, action.index), ...state.slice(action.index + 1)] default: @@ -195,7 +199,13 @@ export module Reducers { * @returns {Object} state. Returns the next state based on the action. */ export const entities = (state = {}, action) => { - if (action.response && (action.type !== 'USER_LOGIN_SUCCESS' && action.type !== 'LOAD_CONTENT_SUCCESS')) { + if (action.response && ( + action.type !== 'USER_LOGIN_SUCCESS' && + action.type !== 'USER_LOGIN_BUFFER' && + action.type !== 'LOAD_CONTENT_SUCCESS' && + action.type !== 'REQUEST_CONTENT_ACTIONS_SUCCESS' && + action.type !== 'UPDATE_CONTENT_SUCCESS' && + action.type !== 'UPLOAD_CONTENT_SUCCESS')) { return (Object).assign({}, state, action.response.entities.entities); } switch (action.type) { @@ -203,6 +213,13 @@ export module Reducers { let res = Object.assign({}, state); delete res[action.id]; return res; + case 'UPDATE_CONTENT_SUCCESS': + state[action.response.Id] = action.response + return state + case 'UPLOAD_CONTENT_SUCCESS': + if (typeof state[action.response.CreatedContent.Id] === 'undefined') + state[action.response.CreatedContent.Id] = action.response.CreatedContent + return state default: return state; } @@ -234,40 +251,17 @@ export module Reducers { switch (action.type) { case 'FETCH_CONTENT_FAILURE': return action.message; - case 'CREATE_CONTENT_FAILURE': - case 'UPDATE_CONTENT_FAILURE': - case 'DELETE_CONTENT_FAILURE': - case 'CHECKIN_CONTENT_FAILURE': - case 'CHECKOUT_CONTENT_FAILURE': - case 'PUBLISH_CONTENT_FAILURE': - case 'APPROVE_CONTENT_FAILURE': - case 'REJECT_CONTENT_FAILURE': - case 'UNDOCHECKOUT_CONTENT_FAILURE': - case 'FORCEUNDOCHECKOUT_CONTENT_FAILURE': - case 'RESTOREVERSION_CONTENT_FAILURE': - case 'FETCH_CONTENT_REQUEST': case 'FETCH_CONTENT_SUCCESS': - case 'CREATE_CONTENT_REQUEST': case 'CREATE_CONTENT_SUCCESS': - case 'UPDATE_CONTENT_REQUEST': case 'UPDATE_CONTENT_SUCCESS': - case 'DELETE_CONTENT_REQUEST': case 'DELETE_CONTENT_SUCCESS': - case 'CHECKIN_CONTENT_REQUEST': case 'CHECKIN_CONTENT_SUCCESS': - case 'CHECKOUT_CONTENT_REQUEST': case 'CHECKOUT_CONTENT_SUCCESS': - case 'APPROVE_CONTENT_REQUEST': case 'APPROVE_CONTENT_SUCCESS': - case 'PUBLISH_CONTENT_REQUEST': case 'PUBLISH_CONTENT_SUCCESS': - case 'REJECT_CONTENT_REQUEST': case 'REJECT_CONTENT_SUCCESS': - case 'UNDOCHECKOUT_CONTENT_REQUEST': case 'UNDOCHECKOUT_CONTENT_SUCCESS': - case 'FORCEUNDOCHECKOUT_CONTENT_REQUEST': case 'FORCEUNDOCHECKOUT_CONTENT_SUCCESS': - case 'RESTOREVERSION_CONTENT_REQUEST': case 'RESTOREVERSION_CONTENT_SUCCESS': return null; default: @@ -280,8 +274,13 @@ export module Reducers { * @param {Object} action Represents an action that is called. * @returns {Object} state. Returns the next state based on the action. */ - export const childrenactions = (state = {}, action) => { - return state + export const childrenactions = (state = [], action) => { + switch (action.type) { + case 'REQUEST_CONTENT_ACTIONS_SUCCESS': + return action.response + default: + return state + } } /** * Reducer to handle Actions on the top property in the children object. @@ -343,8 +342,8 @@ export module Reducers { export const order = (state = {}, action) => { switch (action.type) { case 'FETCH_CONTENT_REQUEST': - if (action.options.order) - return action.options.order + if (action.options.orderby) + return action.options.orderby else return state default: @@ -385,6 +384,20 @@ export module Reducers { return state } } + /** + * Reducer to handle Actions on the isOpened property in the children object. + * @param {Object} [state={}] Represents the current state. + * @param {Object} action Represents an action that is called. + * @returns {Object} state. Returns the next state based on the action. + */ + export const isOpened = (state = null, action) => { + switch (action.type) { + case 'REQUEST_CONTENT_ACTIONS_SUCCESS': + return action.id + default: + return state + } + } /** * Reducer combining ids, entities, isFetching, error, top, skip, query, order, filter and select into a single object, ```children```. */ @@ -393,13 +406,14 @@ export module Reducers { entities, isFetching, error: childrenerror, - // actions: childrenactions, + actions: childrenactions, top, skip, query, order, filter, - select + select, + isOpened }) /** * Reducer to handle Actions on the isSaved property in the contentState object. @@ -575,7 +589,17 @@ export module Reducers { * @returns {Object} state. Returns the next state based on the action. */ export const selected = (state = [], action) => { - return state; + switch (action.type) { + case 'SELECT_CONTENT': + return [...state, action.id] + case 'DESELECT_CONTENT': + const index = state.indexOf(action.id) + return [...state.slice(0, index), ...state.slice(index + 1)] + case 'CLEAR_SELECTION': + return [] + default: + return state + } } /** * Reducer combining session, children, currentcontent and selected into a single object, ```sensenet``` which will be the top-level one. @@ -612,7 +636,7 @@ export module Reducers { * @returns {string} Returns the error message. */ export const getError = (state: any) => { - return state.errorMessage + return state.error }; export const getAuthenticationStatus = (state) => { @@ -626,4 +650,20 @@ export module Reducers { export const getRepositoryUrl = (state) => { return state.session.repository.RepositoryUrl; } + + export const getSelectedContent = (state) => { + return state.selected + } + + export const getOpenedContent = (state) => { + return state.isOpened + } + + export const getChildrenActions = (state) => { + return state.actions + } + + export const getCurrentContent = (state) => { + return state.currentcontent.content + } } \ No newline at end of file diff --git a/test/ActionsTests.ts b/test/ActionsTests.ts index be472cc..a6a41b8 100644 --- a/test/ActionsTests.ts +++ b/test/ActionsTests.ts @@ -1,7 +1,7 @@ /// import { Actions } from '../src/Actions' import * as Chai from 'chai'; -import { Content, Mocks, IContentOptions, ContentTypes, Repository } from 'sn-client-js'; +import { Mocks, ContentTypes, Repository } from 'sn-client-js'; const expect = Chai.expect; describe('Actions', () => { @@ -31,9 +31,9 @@ describe('Actions', () => { type: 'FETCH_CONTENT_REQUEST', path: '/workspaces/project', options: {}, - contentType: Content + contentType: ContentTypes.Task } - expect(Actions.RequestContent(path, {}, Content)).to.deep.equal(expectedAction) + expect(Actions.RequestContent(path, {}, ContentTypes.Task)).to.deep.equal(expectedAction) }); it('should create an action to a fetch content request', () => { const expectedAction = { @@ -81,7 +81,7 @@ describe('Actions', () => { expect(Actions.LoadContent(123)).to.deep.equal(expectedAction) }); it('should create an action to receive a loaded content', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task); expect(Actions.ReceiveLoadedContent(content, { select: ['Id', 'DisplayName'] }).response.DisplayName).to.deep.equal('My content') }); @@ -96,7 +96,7 @@ describe('Actions', () => { }); describe('LoadContentActions', () => { it('should create an action to a load content actions request', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) const expectedAction = { type: 'LOAD_CONTENT_ACTIONS', content: content, @@ -105,7 +105,7 @@ describe('Actions', () => { expect(Actions.LoadContentActions(content, 'ListItem')).to.deep.equal(expectedAction) }); it('should create an action to receive a loaded contents actions', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) const expectedAction = { type: 'LOAD_CONTENT_ACTIONS_SUCCESS', actions: ['aa', 'bb'] @@ -122,7 +122,7 @@ describe('Actions', () => { }); describe('ReloadContent', () => { it('should create an action to a reload content request', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) const expectedAction = { type: 'RELOAD_CONTENT_REQUEST', content, @@ -131,7 +131,7 @@ describe('Actions', () => { expect(Actions.ReloadContent(content, 'edit')).to.deep.equal(expectedAction) }); it('should create an action to receive the reloaded content', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.ReceiveReloadedContent(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to content load request failure', () => { @@ -144,7 +144,7 @@ describe('Actions', () => { }); describe('ReloadContentFields', () => { it('should create an action to a reload fields of a content request', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) const expectedAction = { type: 'RELOAD_CONTENTFIELDS_REQUEST', content, @@ -153,7 +153,7 @@ describe('Actions', () => { expect(Actions.ReloadContentFields(content, ['Id', 'DisplayName'])).to.deep.equal(expectedAction) }); it('should create an action to receive the reloaded fields of a content', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.ReceiveReloadedContentFields(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to content load request failure', () => { @@ -165,10 +165,10 @@ describe('Actions', () => { }); }); describe('CreateContent', () => { - const content = Content.Create({ + const content = repo.CreateContent({ Id: 123, DisplayName: 'My Content' - }, ContentTypes.Task, repo); + }, ContentTypes.Task); it('should create an action to a create content request', () => { const expectedAction = { @@ -178,7 +178,7 @@ describe('Actions', () => { expect(Actions.CreateContent(content)).to.deep.equal(expectedAction) }); it('should create an action to a create content success', () => { - expect(Actions.CreateContentSuccess(content).response.entities.entities['123'].options.DisplayName).to.be.eq('My Content') + expect(Actions.CreateContentSuccess(content).response.entities.entities['123'].DisplayName).to.be.eq('My Content') }); it('should create an action to content creation failure', () => { const expectedAction = { @@ -199,7 +199,7 @@ describe('Actions', () => { })).to.deep.equal(expectedAction) }); it('should create an action to update content success', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.UpdateContentSuccess(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to content update request failure', () => { @@ -211,7 +211,7 @@ describe('Actions', () => { }); }); describe('DeleteContent', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) it('should create an action to a delete content request', () => { const expectedAction = { type: 'DELETE_CONTENT_REQUEST', @@ -231,7 +231,7 @@ describe('Actions', () => { it('should create an action to delete content success', () => { const expectedAction = { type: 'DELETE_CONTENT_SUCCESS', - index: 0, + index: 0, id: 123 } expect(Actions.DeleteSuccess(0, 123)).to.deep.equal(expectedAction) @@ -279,7 +279,7 @@ describe('Actions', () => { }); }); describe('CheckoutContent', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) it('should create an action to a checkout content request', () => { const expectedAction = { type: 'CHECKOUT_CONTENT_REQUEST', @@ -288,7 +288,7 @@ describe('Actions', () => { expect(Actions.CheckOut(content)).to.deep.equal(expectedAction) }); it('should create an action to checkout content success', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.CheckOutSuccess(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to checkout content failure', () => { @@ -300,7 +300,7 @@ describe('Actions', () => { }); }); describe('CheckinContent', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) it('should create an action to a checkin content request', () => { const expectedAction = { type: 'CHECKIN_CONTENT_REQUEST', @@ -318,7 +318,7 @@ describe('Actions', () => { expect(Actions.CheckIn(content)).to.deep.equal(expectedAction) }); it('should create an action to checkin content success', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.CheckInSuccess(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to checkin content failure', () => { @@ -330,7 +330,7 @@ describe('Actions', () => { }); }); describe('PublishContent', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) it('should create an action to a publish content request', () => { const expectedAction = { type: 'PUBLISH_CONTENT_REQUEST', @@ -339,7 +339,7 @@ describe('Actions', () => { expect(Actions.Publish(content)).to.deep.equal(expectedAction) }); it('should create an action to publish content success', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.PublishSuccess(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to publish content failure', () => { @@ -351,7 +351,7 @@ describe('Actions', () => { }); }); describe('ApproveContent', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) it('should create an action to an approve content request', () => { const expectedAction = { type: 'APPROVE_CONTENT_REQUEST', @@ -360,7 +360,7 @@ describe('Actions', () => { expect(Actions.Approve(content)).to.deep.equal(expectedAction) }); it('should create an action to approve content success', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.ApproveSuccess(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to approve content failure', () => { @@ -372,7 +372,7 @@ describe('Actions', () => { }); }); describe('RejectContent', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) it('should create an action to an reject content request', () => { const expectedAction = { type: 'REJECT_CONTENT_REQUEST', @@ -390,7 +390,7 @@ describe('Actions', () => { expect(Actions.Reject(content)).to.deep.equal(expectedAction) }); it('should create an action to reject content success', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.RejectSuccess(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to reject content failure', () => { @@ -402,7 +402,7 @@ describe('Actions', () => { }); }); describe('UndoCheckoutContent', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) it('should create an action to an undo-checkout content request', () => { const expectedAction = { type: 'UNDOCHECKOUT_CONTENT_REQUEST', @@ -411,7 +411,7 @@ describe('Actions', () => { expect(Actions.UndoCheckout(content)).to.deep.equal(expectedAction) }); it('should create an action to undo-checkout content success', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.UndoCheckoutSuccess(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to undo-checkout content failure', () => { @@ -423,7 +423,7 @@ describe('Actions', () => { }); }); describe('ForceUndoCheckoutContent', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) it('should create an action to a force undo-checkout content request', () => { const expectedAction = { type: 'FORCEUNDOCHECKOUT_CONTENT_REQUEST', @@ -432,7 +432,7 @@ describe('Actions', () => { expect(Actions.ForceUndoCheckout(content)).to.deep.equal(expectedAction) }); it('should create an action to force undo-checkout content success', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.ForceUndoCheckoutSuccess(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to force undo-checkout content failure', () => { @@ -444,7 +444,7 @@ describe('Actions', () => { }); }); describe('RestoreVersion', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) it('should create an action to a version restore request', () => { const expectedAction = { type: 'RESTOREVERSION_CONTENT_REQUEST', @@ -454,7 +454,7 @@ describe('Actions', () => { expect(Actions.RestoreVersion(content, 'A.1.0')).to.deep.equal(expectedAction) }); it('should create an action to a version restore success', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) expect(Actions.RestoreVersionSuccess(content).response.DisplayName).to.deep.equal('My content') }); it('should create an action to a version restore failure', () => { @@ -475,11 +475,12 @@ describe('Actions', () => { expect(Actions.UserLogin('alba', 'alba')).to.deep.equal(expectedAction) }); it('should create an action to a user login success', () => { + const user = repo.CreateContent({ Name: 'alba' }, ContentTypes.User) const expectedAction = { type: 'USER_LOGIN_SUCCESS', - response: true + response: user } - expect(Actions.UserLoginSuccess(true)).to.deep.equal(expectedAction) + expect(Actions.UserLoginSuccess(user)).to.deep.equal(expectedAction) }); it('should create an action to a user login failure', () => { const expectedAction = { @@ -496,6 +497,15 @@ describe('Actions', () => { expect(Actions.UserLoginFailure({ message: 'The username or the password is not valid!', status: 403 })).to.deep.equal(expectedAction) }); }); + describe('UserLoginBuffer', () => { + it('should create an action to a user login buffering', () => { + const expectedAction = { + type: 'USER_LOGIN_BUFFER', + response: true + } + expect(Actions.UserLoginBuffer(true)).to.deep.equal(expectedAction) + }); + }); describe('UserLogout', () => { it('should create an action to a user logout request', () => { const expectedAction = { @@ -527,7 +537,7 @@ describe('Actions', () => { }); describe('UserChanged', () => { it('should return the user changed action', () => { - const user = Content.Create({ Name: 'alba' }, ContentTypes.User, repo) + const user = repo.CreateContent({ Name: 'alba' }, ContentTypes.User) const expectedAction = { type: 'USER_CHANGED', user @@ -544,4 +554,144 @@ describe('Actions', () => { expect(Actions.LoadRepository(repo)).to.deep.equal(expectedAction) }); }); + describe('SelectContent', () => { + it('should return the select content action', () => { + const expectedAction = { + type: 'SELECT_CONTENT', + id: 1 + } + expect(Actions.SelectContent(1)).to.deep.equal(expectedAction) + }) + }) + describe('DeSelectContent', () => { + it('should return the deselect content action', () => { + const expectedAction = { + type: 'DESELECT_CONTENT', + id: 1 + } + expect(Actions.DeSelectContent(1)).to.deep.equal(expectedAction) + }) + }) + describe('ClearSelection', () => { + it('should return the clear selection action', () => { + const expectedAction = { + type: 'CLEAR_SELECTION' + } + expect(Actions.ClearSelection()).to.deep.equal(expectedAction) + }) + }) + describe('RequestContentActions', () => { + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) + + it('should return the RequestContentActions action', () => { + const expectedAction = { + type: 'REQUEST_CONTENT_ACTIONS', + content: content, + scenario: 'DMSListItem' + } + expect(Actions.RequestContentActions(content, 'DMSListItem')).to.deep.equal(expectedAction) + }) + it('should return the RequestContentActionsSuccess action', () => { + const expectedAction = { + type: 'REQUEST_CONTENT_ACTIONS_SUCCESS', + response: [ + { + ActionName: 'Rename' + } + ], + id: 1 + } + expect(Actions.RequestContentActionsSuccess([{ ActionName: 'Rename' }], 1)).to.deep.equal(expectedAction) + }) + it('should return the RequestContentActionsFailure action', () => { + const expectedAction = { + type: 'REQUEST_CONTENT_ACTIONS_FAILURE', + message: 'error' + } + expect(Actions.RequestContentActionsFailure({ message: 'error' })).to.deep.equal(expectedAction) + }); + }) + describe('UploadContentActions', () => { + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) + const file = { + lastModified: 1499931166346, + name: 'README.md', + size: 75, + type: '' + } + it('should return the upload content action set only content and file', () => { + const expectedAction = { + type: 'UPLOAD_CONTENT_REQUEST', + content, + file, + overwrite: true, + propertyName: 'Binary', + contentType: ContentTypes.File, + body: null + } + expect(Actions.UploadRequest(content, file)).to.deep.equal(expectedAction) + }) + it('should return the upload content action set content, file and contentType to Folder', () => { + const expectedAction = { + type: 'UPLOAD_CONTENT_REQUEST', + content, + contentType: ContentTypes.Folder, + file, + overwrite: true, + propertyName: 'Binary', + body: null + } + expect(Actions.UploadRequest(content, file, ContentTypes.Folder)).to.deep.equal(expectedAction) + }) + it('should return the upload content action set content, file and overwrite to false', () => { + const expectedAction = { + type: 'UPLOAD_CONTENT_REQUEST', + content, + contentType: ContentTypes.File, + file, + overwrite: false, + propertyName: 'Binary', + body: null + } + expect(Actions.UploadRequest(content, file, undefined, false)).to.deep.equal(expectedAction) + }) + it('should return the upload content action set content, file and propertyName to Avatar', () => { + const expectedAction = { + type: 'UPLOAD_CONTENT_REQUEST', + content, + contentType: ContentTypes.File, + file, + overwrite: true, + propertyName: 'Avatar', + body: null + } + expect(Actions.UploadRequest(content, file, undefined, undefined, undefined, 'Avatar')).to.deep.equal(expectedAction) + }) + it('should return the upload content action set content, file and body', () => { + const expectedAction = { + type: 'UPLOAD_CONTENT_REQUEST', + content, + contentType: ContentTypes.File, + file, + overwrite: true, + propertyName: 'Binary', + body: { vmi: 'aaa' } + } + expect(Actions.UploadRequest(content, file, undefined, undefined, { vmi: 'aaa' })).to.deep.equal(expectedAction) + }) + it('should create an action to upload content success', () => { + const expectedAction = { + type: 'UPLOAD_CONTENT_SUCCESS', + response: [] + } + expect(Actions.UploadSuccess([])).to.deep.equal(expectedAction) + }); + it('should create an action to content upload request failure', () => { + const expectedAction = { + type: 'UPLOAD_CONTENT_FAILURE', + message: 'error' + } + expect(Actions.UploadFailure({ message: 'error' })).to.deep.equal(expectedAction) + }); + }) }); \ No newline at end of file diff --git a/test/EpicsTests.ts b/test/EpicsTests.ts index 5f3dcad..f244397 100644 --- a/test/EpicsTests.ts +++ b/test/EpicsTests.ts @@ -4,26 +4,29 @@ import { createEpicMiddleware } from 'redux-observable'; import { Mocks, ContentTypes, HttpProviders, Authentication, ODataApi, Content } from 'sn-client-js'; import { Epics } from '../src/Epics' import { Actions } from '../src/Actions' +import { Store } from '../src/Store' const expect = Chai.expect; import 'rxjs'; -describe('Epics', () => { +let store, repo: Mocks.MockRepository, epicMiddleware, mockStore, content; +const initBefores = () => { + repo = new Mocks.MockRepository(); + epicMiddleware = createEpicMiddleware(Epics.fetchContentEpic, { dependencies: { repository: repo } }) + mockStore = configureMockStore([epicMiddleware]); + store = mockStore(); + content = repo.HandleLoadedContent({ DisplayName: 'My Content', Id: 123, Path: '/workspaces', Name: 'MyContent' }, ContentTypes.Task) +} - let repo: Mocks.MockRepository = new Mocks.MockRepository(); - (repo.Authentication as Mocks.MockAuthService).stateSubject.next(Authentication.LoginState.Authenticated); - (repo.httpProviderRef as Mocks.MockHttpProvider).UseTimeout = false; - beforeEach(() => { - (repo.httpProviderRef as Mocks.MockHttpProvider).setError({ message: 'XMLHttpRequest is not supported by your browser' }); +describe('Epics', () => { + beforeEach(() => { + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'XMLHttpRequest is not supported by your browser' }); }) describe('fetchContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.fetchContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); before(() => { - store = mockStore(); + initBefores() }); after(() => { @@ -37,33 +40,16 @@ describe('Epics', () => { path: '/workspaces/Project', options: { - select: [ - ['Id', 'Path', 'Name', 'Type'], - ['DisplayName', 'Description', 'Icon'] - ], + select: ['Id', 'Path', 'Name', 'Type', 'DisplayName', 'Description', 'Icon'], metadata: 'no', inlinecount: 'allpages', expand: undefined, top: 1000 } - }, - { - type: 'FETCH_CONTENT_FAILURE', - params: - { - select: [ - ['Id', 'Path', 'Name', 'Type'], - ['DisplayName', 'Description', 'Icon'] - ], - metadata: 'no', - inlinecount: 'allpages', - expand: undefined, - top: 1000 - }, - message: 'XMLHttpRequest is not supported by your browser' }]); }) }); + // describe('initSensenetStoreEpic Epic', () => { // let store; // const epicMiddleware = createEpicMiddleware(Epics.initSensenetStoreEpic, { dependencies: { repository: repo } }); @@ -112,17 +98,31 @@ describe('Epics', () => { // }]); // }) // }) + + describe('initSensenetStoreEpic Epic', () => { + before(() => { + initBefores() + }); + + after(() => { + epicMiddleware.replaceEpic(Epics.initSensenetStoreEpic); + }); + it('handles the error', () => { + const user = repo.CreateContent({ Name: 'alba', Id: 123 }, ContentTypes.User); + store.dispatch({ type: 'INIT_SENSENET_STORE', path: '/workspaces', options: {} }); + expect(store.getActions()).to.be.deep.equal( + [{ type: 'INIT_SENSENET_STORE', path: '/workspaces', options: {} }]); + }) + }) + describe('loadContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.loadContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); before(() => { - store = mockStore(); + initBefores() }); after(() => { - epicMiddleware.replaceEpic(Epics.fetchContentEpic); + epicMiddleware.replaceEpic(Epics.loadContentEpic); }); it('handles the error', () => { store.dispatch({ type: 'LOAD_CONTENT_REQUEST', path: '/workspaces/Project', options: {} }); @@ -130,49 +130,21 @@ describe('Epics', () => { [{ type: 'LOAD_CONTENT_REQUEST', path: '/workspaces/Project', - options: - { - select: [ - ['Id', 'Path', 'Name', 'Type'], - ['DisplayName', 'Description', 'Icon'] - ], - metadata: 'no', - inlinecount: 'allpages', - expand: undefined, - top: 1000 - } - }, - { - type: 'LOAD_CONTENT_FAILURE', - params: - { - select: [ - ['Id', 'Path', 'Name', 'Type'], - ['DisplayName', 'Description', 'Icon'] - ], - metadata: 'no', - inlinecount: 'allpages', - expand: undefined, - top: 1000 - }, - message: 'XMLHttpRequest is not supported by your browser' + options: {} }]); }) }); describe('reloadContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.reloadContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); before(() => { - store = mockStore(); + initBefores() }); after(() => { - epicMiddleware.replaceEpic(Epics.fetchContentEpic); + epicMiddleware.replaceEpic(Epics.reloadContentEpic); }); + it('handles the error', () => { - const content = repo.HandleLoadedContent({ DisplayName: 'My Content', Id: 123, Path: '/workspaces' }, ContentTypes.Task) content.Save('/workspaces') store.dispatch({ type: 'RELOAD_CONTENT_REQUEST', content, options: {} }); expect(store.getActions()).to.be.deep.eq([ @@ -182,71 +154,83 @@ describe('Epics', () => { options: {} }]); }) + it('handles the error', () => { + store.dispatch({ type: 'RELOAD_CONTENT_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'RELOAD_CONTENT_REQUEST', + content, + options: {} + }, + { type: 'RELOAD_CONTENT_FAILURE', error: 'error' }]); + }) }); describe('reloadContentFields Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.reloadContentFieldsEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { - epicMiddleware.replaceEpic(Epics.fetchContentEpic); + epicMiddleware.replaceEpic(Epics.reloadContentFieldsEpic); }); + it('handles the error', () => { - const content = repo.HandleLoadedContent({ DisplayName: 'My Content', Id: 123, Path: '/workspaces' }, ContentTypes.Task) - store.dispatch({ type: 'RELOAD_CONTENTFIELDS_REQUEST', content, options: {} }); + store.dispatch({ type: 'RELOAD_CONTENTFIELDS_REQUEST', content, options: {}, fields: ['DisplayName'] }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'RELOAD_CONTENTFIELDS_REQUEST', + content: content, + options: {}, + fields: ['DisplayName'] + }]); + }) + it('handles the error', () => { + store.dispatch({ type: 'RELOAD_CONTENTFIELDS_FAILURE', error: 'error' }); expect(store.getActions()).to.be.deep.eq( [{ type: 'RELOAD_CONTENTFIELDS_REQUEST', content, - options: {} + options: {}, + fields: ['DisplayName'] }, - { - type: 'RELOAD_CONTENTFIELDS_FAILURE', - message: 'XMLHttpRequest is not supported by your browser' - }]); + { type: 'RELOAD_CONTENTFIELDS_FAILURE', error: 'error' }]); }) }); describe('createContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.createContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { epicMiddleware.replaceEpic(Epics.createContentEpic); }); it('handles the error', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); - store.dispatch({ type: 'CREATE_CONTENT_REQUEST', content, contentType: ContentTypes.Task }); + store.dispatch({ type: 'CREATE_CONTENT_REQUEST', content }); expect(store.getActions()).to.be.deep.eq( [{ type: 'CREATE_CONTENT_REQUEST', - content: content, - contentType: ContentTypes.Task + content: content }]); }) + it('handles the error', () => { + store.dispatch({ type: 'CREATE_CONTENT_FAILURE', error: { message: 'error' } }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'CREATE_CONTENT_REQUEST', + content: content + }, + { type: 'CREATE_CONTENT_FAILURE', error: { message: 'error' } }]); + }) }); describe('updateContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.updateContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { epicMiddleware.replaceEpic(Epics.updateContentEpic); }); it('handles the error', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); store.dispatch({ type: 'UPDATE_CONTENT_REQUEST', content }); expect(store.getActions()).to.be.deep.eq( [{ @@ -254,21 +238,25 @@ describe('Epics', () => { content }]); }) + it('handles the error', () => { + store.dispatch({ type: 'UPDATE_CONTENT_FAILURE', error: { message: 'error' } }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'UPDATE_CONTENT_REQUEST', + content: content + }, + { type: 'UPDATE_CONTENT_FAILURE', error: { message: 'error' } }]); + }) }); describe('deleteContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.deleteContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { epicMiddleware.replaceEpic(Epics.deleteContentEpic); }); it('handles the error', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); store.dispatch({ type: 'DELETE_CONTENT_REQUEST', content, permanently: false }); expect(store.getActions()).to.be.deep.eq( [{ @@ -277,46 +265,54 @@ describe('Epics', () => { permanently: false }]); }) + it('handles the error', () => { + store.dispatch({ type: 'DELETE_CONTENT_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'DELETE_CONTENT_REQUEST', + content, + permanently: false + }, + { type: 'DELETE_CONTENT_FAILURE', error: 'error' }]); + }) }); describe('deleteBatch Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.deleteBatchEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { epicMiddleware.replaceEpic(Epics.deleteBatchEpic); }); - // it('handles the error', () => { - // store.dispatch({ type: 'DELETE_BATCH_REQUEST', ids: ['1', '2'], permanently: false }); - // expect(store.getActions()).to.be.deep.eq( - // [{ - // type: 'DELETE_BATCH_REQUEST', - // path: '/workspaces/Project', - // ids: ['1', '2'], - // permanently: false - // }]); - // }) + it('handles the error', () => { + store.dispatch({ type: 'DELETE_BATCH_REQUEST', ids: ['1', '2'], permanently: false }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'DELETE_BATCH_REQUEST', + ids: ['1', '2'], + permanently: false + }]); + }) + it('handles the error', () => { + store.dispatch({ type: 'DELETE_BATCH_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'DELETE_BATCH_REQUEST', + ids: ['1', '2'], + permanently: false + }, + { type: 'DELETE_BATCH_FAILURE', error: 'error' }]); + }) }); describe('checkoutContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.checkoutContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); - after(() => { epicMiddleware.replaceEpic(Epics.checkoutContentEpic); }); it('handles the error', () => { - - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); - (repo.httpProviderRef as Mocks.MockHttpProvider).setError({ message: 'Checkout Content failed' }); + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Checkout Content failed' }); store.dispatch({ type: 'CHECKOUT_CONTENT_REQUEST', content }); expect(store.getActions()).to.be.deep.eq( [{ @@ -324,22 +320,26 @@ describe('Epics', () => { content }]); }) + it('handles the error', () => { + store.dispatch({ type: 'CHECKOUT_CONTENT_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'CHECKOUT_CONTENT_REQUEST', + content + }, + { type: 'CHECKOUT_CONTENT_FAILURE', error: 'error' }]); + }) }); describe('checkinContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.checkinContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { epicMiddleware.replaceEpic(Epics.checkinContentEpic); }); it('handles the error', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); - (repo.httpProviderRef as Mocks.MockHttpProvider).setError({ message: 'Checkin Content failed' }); + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Checkin Content failed' }); store.dispatch({ type: 'CHECKIN_CONTENT_REQUEST', content, checkinComment: 'comment' }); expect(store.getActions()).to.be.deep.eq( @@ -349,22 +349,29 @@ describe('Epics', () => { checkinComment: 'comment' }]); }) + it('handles the error', () => { + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Checkin Content failed' }); + + store.dispatch({ type: 'CHECKIN_CONTENT_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'CHECKIN_CONTENT_REQUEST', + content, + checkinComment: 'comment' + }, + { type: 'CHECKIN_CONTENT_FAILURE', error: 'error' }]); + }) }); describe('publishContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.publishContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { epicMiddleware.replaceEpic(Epics.publishContentEpic); }); it('handles the error', () => { - (repo.httpProviderRef as Mocks.MockHttpProvider).setError({ message: 'Publish Content failed' }); - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Publish Content failed' }); store.dispatch({ type: 'PUBLISH_CONTENT_REQUEST', content }); expect(store.getActions()).to.be.deep.eq( @@ -373,22 +380,27 @@ describe('Epics', () => { content }]); }) + it('handles the error', () => { + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Publish Content failed' }); + store.dispatch({ type: 'PUBLISH_CONTENT_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'PUBLISH_CONTENT_REQUEST', + content + }, + { type: 'PUBLISH_CONTENT_FAILURE', error: 'error' }]); + }) }); describe('approveContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.approveContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { epicMiddleware.replaceEpic(Epics.approveContentEpic); }); it('handles the error', () => { - (repo.httpProviderRef as Mocks.MockHttpProvider).setError({ message: 'Approve Content failed' }); - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Approve Content failed' }); store.dispatch({ type: 'APPROVE_CONTENT_REQUEST', content }); expect(store.getActions()).to.be.deep.eq( [{ @@ -396,22 +408,27 @@ describe('Epics', () => { content }]); }) + it('handles the error', () => { + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Approve Content failed' }); + store.dispatch({ type: 'APPROVE_CONTENT_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'APPROVE_CONTENT_REQUEST', + content + }, + { type: 'APPROVE_CONTENT_FAILURE', error: 'error' }]); + }) }); describe('rejectContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.rejectContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - - beforeEach(() => { - store = mockStore(); + before(() => { + initBefores() }); afterEach(() => { epicMiddleware.replaceEpic(Epics.rejectContentEpic); }); it('handles the error', () => { - (repo.httpProviderRef as Mocks.MockHttpProvider).setError({ message: 'Reject Content failed' }); - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Reject Content failed' }); store.dispatch({ type: 'REJECT_CONTENT_REQUEST', content, rejectReason: 'reason' }); expect(store.getActions()).to.be.deep.eq( [{ @@ -420,22 +437,29 @@ describe('Epics', () => { rejectReason: 'reason' }]); }); + it('handles the error', () => { + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Reject Content failed' }); + store.dispatch({ type: 'REJECT_CONTENT_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'REJECT_CONTENT_REQUEST', + content, + rejectReason: 'reason' + }, + { type: '@@redux-observable/EPIC_END' }, + { type: 'REJECT_CONTENT_FAILURE', error: 'error' }]); + }); }); describe('undocheckoutContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.undocheckoutContentEpic, { dependencies: { repository: repo } }); - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { epicMiddleware.replaceEpic(Epics.undocheckoutContentEpic); }); it('handles the error', () => { - (repo.httpProviderRef as Mocks.MockHttpProvider).setError({ message: 'Undo Checkout failed' }); + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Undo Checkout failed' }); store.dispatch({ type: 'UNDOCHECKOUT_CONTENT_REQUEST', content }); expect(store.getActions()).to.be.deep.eq( [{ @@ -443,22 +467,27 @@ describe('Epics', () => { content }]); }) + it('handles the error', () => { + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Undo Checkout failed' }); + store.dispatch({ type: 'UNDOCHECKOUT_CONTENT_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'UNDOCHECKOUT_CONTENT_REQUEST', + content + }, + { type: 'UNDOCHECKOUT_CONTENT_FAILURE', error: 'error' }]); + }) }); describe('forceundocheckoutContent Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.forceundocheckoutContentEpic, { dependencies: { repository: repo } }); - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { epicMiddleware.replaceEpic(Epics.forceundocheckoutContentEpic); }); it('handles the error', () => { - (repo.httpProviderRef as Mocks.MockHttpProvider).setError({ message: 'ForceUndoCheckout failed' }); + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'ForceUndoCheckout failed' }); store.dispatch({ type: 'FORCEUNDOCHECKOUT_CONTENT_REQUEST', content }); expect(store.getActions()).to.be.deep.eq( [{ @@ -466,22 +495,26 @@ describe('Epics', () => { content }]); }) + it('handles the error', () => { + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'ForceUndoCheckout failed' }); + store.dispatch({ type: 'FORCEUNDOCHECKOUT_CONTENT_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'FORCEUNDOCHECKOUT_CONTENT_REQUEST', + content + }, + { type: 'FORCEUNDOCHECKOUT_CONTENT_FAILURE', error: 'error' }]); + }) }); describe('restoreVersion Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.restoreversionContentEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); - after(() => { epicMiddleware.replaceEpic(Epics.restoreversionContentEpic); }); it('handles the error', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123, Path: '/workspaces' }, ContentTypes.Task, repo); - (repo.httpProviderRef as Mocks.MockHttpProvider).setError({ message: 'Restore failed' }); + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Restore failed' }); store.dispatch({ type: 'RESTOREVERSION_CONTENT_REQUEST', content, version: 'A.1.0' }); expect(store.getActions()).to.be.deep.eq( [{ @@ -490,14 +523,21 @@ describe('Epics', () => { version: 'A.1.0' }]); }); + it('handles the error', () => { + (repo.HttpProviderRef as Mocks.MockHttpProvider).AddError({ message: 'Restore failed' }); + store.dispatch({ type: 'RESTOREVERSION_CONTENT_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'RESTOREVERSION_CONTENT_REQUEST', + content, + version: 'A.1.0' + }, + { type: 'RESTOREVERSION_CONTENT_FAILURE', error: 'error' }]); + }); }); describe('login Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.userLoginEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - - beforeEach(() => { - store = mockStore(); + before(() => { + initBefores() }); afterEach(() => { @@ -505,24 +545,26 @@ describe('Epics', () => { }); it('handles the error', () => { store.dispatch({ type: 'USER_LOGIN_REQUEST', username: 'alba', password: 'alba' }); - (repo.Authentication as Mocks.MockAuthService).stateSubject.next(Authentication.LoginState.Unauthenticated); + (repo.Authentication as Mocks.MockAuthService).StateSubject.next(Authentication.LoginState.Unauthenticated); expect(store.getActions()).to.be.deep.eq( [{ type: 'USER_LOGIN_REQUEST', username: 'alba', password: 'alba' - }, - { - type: 'USER_LOGIN_FAILURE', - message: 'Failed to log in.' }]); }) it('handles the loggedin user', () => { - const user = Content.Create({ Name: 'alba', Id: 123 }, ContentTypes.User, repo) + const user = repo.CreateContent({ Name: 'alba', Id: 123 }, ContentTypes.User); store.dispatch({ type: 'USER_LOGIN_REQUEST', username: 'user', password: 'password' }); - (repo.Authentication as Mocks.MockAuthService).stateSubject.next(Authentication.LoginState.Authenticated); + (repo.Authentication as Mocks.MockAuthService).StateSubject.next(Authentication.LoginState.Authenticated); expect(store.getActions()).to.be.deep.eq( [{ + type: 'USER_LOGIN_REQUEST', + username: 'alba', + password: 'alba' + }, + { type: '@@redux-observable/EPIC_END' }, + { type: 'USER_LOGIN_REQUEST', username: 'user', password: 'password' @@ -531,19 +573,15 @@ describe('Epics', () => { }) }); describe('logout Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.userLogoutEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - before(() => { - store = mockStore(); + initBefores() }); after(() => { epicMiddleware.replaceEpic(Epics.userLogoutEpic); }); it('handles the success', () => { - (repo.Authentication as Mocks.MockAuthService).stateSubject.next(Authentication.LoginState.Unauthenticated); + (repo.Authentication as Mocks.MockAuthService).StateSubject.next(Authentication.LoginState.Unauthenticated); store.dispatch({ type: 'USER_LOGOUT_REQUEST', id: 111, username: 'alba', password: 'alba' }); expect(store.getActions()).to.be.deep.eq( [{ @@ -551,13 +589,10 @@ describe('Epics', () => { id: 111, username: 'alba', password: 'alba' - }, - { - type: 'USER_LOGOUT_SUCCESS' }]); }) it('handles the error', () => { - (repo.Authentication as Mocks.MockAuthService).stateSubject.next(Authentication.LoginState.Authenticated); + (repo.Authentication as Mocks.MockAuthService).StateSubject.next(Authentication.LoginState.Authenticated); store.dispatch({ type: 'USER_LOGOUT_FAILURE', error: 'error' }); expect(store.getActions()).to.be.deep.eq( [{ @@ -566,41 +601,129 @@ describe('Epics', () => { username: 'alba', password: 'alba' }, - { type: 'USER_LOGOUT_SUCCESS' }, { type: 'USER_LOGOUT_FAILURE', error: 'error' }]); }) }); describe('checkLoginState Epic', () => { - let store; - const epicMiddleware = createEpicMiddleware(Epics.checkLoginStateEpic, { dependencies: { repository: repo } }); - const mockStore = configureMockStore([epicMiddleware]); - beforeEach(() => { - store = mockStore(); + initBefores() }); afterEach(() => { epicMiddleware.replaceEpic(Epics.userLoginEpic); }); - const user = Content.Create({ Name: 'alba', Id: '2' }, ContentTypes.User, repo) it('handles a loggedin user', () => { + const user = repo.CreateContent({ Name: 'alba', Id: 2, Path: '/Root' }, ContentTypes.User); store.dispatch(Actions.UserLoginSuccess(user)); - (repo.Authentication as Mocks.MockAuthService).stateSubject.next(Authentication.LoginState.Authenticated); + (repo.Authentication as Mocks.MockAuthService).StateSubject.next(Authentication.LoginState.Authenticated); store.dispatch({ type: 'CHECK_LOGIN_STATE_REQUEST' }); expect(store.getActions()).to.be.deep.eq( [{ type: 'USER_LOGIN_SUCCESS', response: user }, - { type: 'CHECK_LOGIN_STATE_REQUEST' }, - { type: 'USER_LOGIN_SUCCESS', response: 2 }]); + { type: 'CHECK_LOGIN_STATE_REQUEST' } + ]); }) it('handles an error', () => { - (repo.Authentication as Mocks.MockAuthService).stateSubject.next(Authentication.LoginState.Unauthenticated); + const user = repo.HandleLoadedContent({ Name: 'alba', Id: 65535, Path: '/Root' }, ContentTypes.User); + repo.Authentication.StateSubject.next(Authentication.LoginState.Unauthenticated); store.dispatch({ type: 'CHECK_LOGIN_STATE_REQUEST' }); - expect(store.getActions()).to.be.deep.eq([ - { type: 'CHECK_LOGIN_STATE_REQUEST' }, - { type: 'USER_LOGIN_FAILURE', message: null }]); + expect(store.getActions()).to.be.deep.eq( + [ + // { + // type: 'USER_LOGIN_SUCCESS', + // response: user.GetFields() + // }, + // { type: 'CHECK_LOGIN_STATE_REQUEST' }, + // { type: '@@redux-observable/EPIC_END' }, + { type: 'CHECK_LOGIN_STATE_REQUEST' }]); + }) + }); + describe('getContentActions Epic', () => { + before(() => { + initBefores() + }); + + after(() => { + epicMiddleware.replaceEpic(Epics.getContentActions); + }); + it('handles the success', () => { + store.dispatch({ type: 'REQUEST_CONTENT_ACTIONS', content, scenario: 'DMSDemoScenario' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'REQUEST_CONTENT_ACTIONS', + content: content, + scenario: 'DMSDemoScenario' + }]); + }) + it('handles the error', () => { + store.dispatch({ type: 'REQUEST_CONTENT_ACTIONS_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'REQUEST_CONTENT_ACTIONS', + content, + scenario: 'DMSDemoScenario' + }, + { type: 'REQUEST_CONTENT_ACTIONS_FAILURE', error: 'error' }]); + }) + }); + describe('loadContentActionsEpic Epic', () => { + before(() => { + initBefores() + }); + + after(() => { + epicMiddleware.replaceEpic(Epics.loadContentActionsEpic); + }); + it('handles the success', () => { + store.dispatch({ type: 'LOAD_CONTENT_ACTIONS', content, scenario: 'DMSDemoScenario' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'LOAD_CONTENT_ACTIONS', + content, + scenario: 'DMSDemoScenario' + }, + ]); + }) + it('handles the error', () => { + store.dispatch({ type: 'LOAD_CONTENT_ACTIONS_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'LOAD_CONTENT_ACTIONS', + content, + scenario: 'DMSDemoScenario' + }, + { type: 'LOAD_CONTENT_ACTIONS_FAILURE', error: 'error' }]); }) }); + describe('userLoginBufferEpic Epic', () => { + before(() => { + initBefores() + }); + + after(() => { + epicMiddleware.replaceEpic(Epics.userLoginBufferEpic); + }); + it('handles the success', () => { + store.dispatch({ type: 'USER_LOGIN_BUFFER', response: true }); + expect(store.getActions()).to.be.deep.eq( + [{ type: 'USER_LOGIN_BUFFER', response: true }]); + }) + }) + + describe('uploadContentEpic Epic', () => { + before(() => { + initBefores() + }); + + after(() => { + epicMiddleware.replaceEpic(Epics.uploadFileEpic); + }); + it('handles the success', () => { + store.dispatch({ type: 'UPLOAD_CONTENT_SUCCESS', response: true }); + expect(store.getActions()).to.be.deep.eq( + [{ type: 'UPLOAD_CONTENT_SUCCESS', response: true }]); + }) + }) }); \ No newline at end of file diff --git a/test/ReducersTests.ts b/test/ReducersTests.ts index 25b2cf3..7da2a46 100644 --- a/test/ReducersTests.ts +++ b/test/ReducersTests.ts @@ -2,7 +2,7 @@ import { Reducers } from '../src/Reducers'; import { Actions } from '../src/Actions'; import * as Chai from 'chai'; -import { Authentication, Content, ContentTypes, Mocks } from 'sn-client-js'; +import { Authentication, Content, ContentTypes, Mocks, Enums } from 'sn-client-js'; const expect = Chai.expect; describe('Reducers', () => { describe('country reducer', () => { @@ -95,7 +95,7 @@ describe('Reducers', () => { expect(Reducers.userAvatarPath(undefined, { type: 'USER_CHANGED', user: { DisplayName: 'Alba Monday' } })).to.be.deep.equal(''); }); it('should return the logged-in users avatars path', () => { - expect(Reducers.userAvatarPath(undefined, { type: 'USER_CHANGED', user: { ImageData: { __mediaresource: { media_src: 'Alba Monday' } } } })).to.be.deep.equal('Alba Monday'); + expect(Reducers.userAvatarPath(undefined, { type: 'USER_CHANGED', user: { Avatar: { _deferred: 'Alba Monday' } } })).to.be.deep.equal('Alba Monday'); }); }) @@ -157,6 +157,32 @@ describe('Reducers', () => { })) .to.be.deep.equal([2, 3]); }); + it('should handle UPDATE_CONTENT_SUCCESS', () => { + expect(Reducers.ids( + [1, 2, 3], + { + type: 'UPLOAD_CONTENT_SUCCESS', + response: { + CreatedContent: { + Id: 4 + } + } + })) + .to.be.deep.equal([1, 2, 3, 4]); + }); + it('should handle UPDATE_CONTENT_SUCCESS with existing id', () => { + expect(Reducers.ids( + [1, 2, 3], + { + type: 'UPLOAD_CONTENT_SUCCESS', + response: { + CreatedContent: { + Id: 3 + } + } + })) + .to.be.deep.equal([1, 2, 3]); + }); }); describe('entities reducer', () => { @@ -171,6 +197,95 @@ describe('Reducers', () => { expect(Reducers.entities({}, { response: { entities: { entities: { a: 0, b: 2 } } } })) .to.be.deep.eq({ a: 0, b: 2 }); }); + it('should handle UPDATE_CONTENT_SUCCESS', () => { + const entities = { + 5145: { + Id: 5145, + DisplayName: 'Some Article', + Status: ['Active'] + }, + 5146: { + Id: 5146, + Displayname: 'Other Article', + Status: ['Completed'] + } + }; + expect(Reducers.entities(entities, { type: 'UPDATE_CONTENT_SUCCESS', response: { Id: 5145, DisplayName: 'aaa', Status: ['Active'] } })).to.be.deep.equal( + { + 5145: { + Id: 5145, + DisplayName: 'aaa', + Status: ['Active'] + }, + 5146: { + Id: 5146, + Displayname: 'Other Article', + Status: ['Completed'] + } + } + ); + }); + it('should handle UPLOAD_CONTENT_SUCCESS', () => { + const entities = { + 5122: { + Id: 5122, + DisplayName: 'Some Article', + Status: ['Active'] + }, + 5146: { + Id: 5146, + Displayname: 'Other Article', + Status: ['Completed'] + } + }; + expect(Reducers.entities(entities, { type: 'UPLOAD_CONTENT_SUCCESS', response: { CreatedContent: { Id: 5145, DisplayName: 'aaa', Status: ['Active'] } } })).to.be.deep.equal( + { + 5122: { + Id: 5122, + DisplayName: 'Some Article', + Status: ['Active'] + }, + 5146: { + Id: 5146, + Displayname: 'Other Article', + Status: ['Completed'] + }, + 5145: { + Id: 5145, + DisplayName: 'aaa', + Status: ['Active'] + }, + } + ); + }); + it('should handle UPLOAD_CONTENT_SUCCESS with existing content', () => { + const entities = { + 5122: { + Id: 5122, + DisplayName: 'Some Article', + Status: ['Active'] + }, + 5146: { + Id: 5146, + Displayname: 'Other Article', + Status: ['Completed'] + } + }; + expect(Reducers.entities(entities, { type: 'UPLOAD_CONTENT_SUCCESS', response: { CreatedContent: { Id: 5122, DisplayName: 'Some Article', Status: ['Active'] } } })).to.be.deep.equal( + { + 5122: { + Id: 5122, + DisplayName: 'Some Article', + Status: ['Active'] + }, + 5146: { + Id: 5146, + Displayname: 'Other Article', + Status: ['Completed'] + }, + } + ); + }); }); describe('isFetching reducer', () => { @@ -303,7 +418,18 @@ describe('Reducers', () => { }); describe('childrenactions reducer', () => { it('should return the initial state', () => { - expect(Reducers.childrenactions(undefined, {})).to.be.deep.equal({}); + expect(Reducers.childrenactions(undefined, {})).to.be.deep.equal([]); + }); + it('should handle REQUEST_CONTENT_ACTIONS_SUCCESS', () => { + const action = { + type: 'REQUEST_CONTENT_ACTIONS_SUCCESS', + response: [ + { + ActionName: 'Rename' + } + ] + } + expect(Reducers.childrenactions(undefined, action)).to.be.deep.equal([{ ActionName: 'Rename' }]); }); }); describe('top reducer', () => { @@ -344,7 +470,7 @@ describe('Reducers', () => { expect(Reducers.order(undefined, {})).to.be.deep.equal({}); }); it('should return "DisplayName desc"', () => { - expect(Reducers.order(undefined, { type: 'FETCH_CONTENT_REQUEST', options: { order: 'DisplayName desc' } })).to.be.eq('DisplayName desc'); + expect(Reducers.order(undefined, { type: 'FETCH_CONTENT_REQUEST', options: { orderby: 'DisplayName desc' } })).to.be.eq('DisplayName desc'); }); it('should return initial state', () => { expect(Reducers.order(undefined, { type: 'FETCH_CONTENT_REQUEST', options: {} })).to.be.deep.equal({}); @@ -372,6 +498,18 @@ describe('Reducers', () => { expect(Reducers.select(undefined, { type: 'FETCH_CONTENT_REQUEST', options: {} })).to.be.deep.equal({}); }); }); + describe('isOpened reducer', () => { + it('should return the initial state', () => { + expect(Reducers.isOpened(undefined, {})).to.be.eq(null) + }) + it('should return 1', () => { + const action = { + type: 'REQUEST_CONTENT_ACTIONS_SUCCESS', + id: 1 + } + expect(Reducers.isOpened(undefined, action)).to.be.eq(1) + }) + }) describe('isSaved reducer', () => { it('should return the initial state', () => { expect(Reducers.isSaved(undefined, {})).to.be.deep.equal(true); @@ -551,10 +689,10 @@ describe('Reducers', () => { it('should return fields of the content', () => { let repo: Mocks.MockRepository = new Mocks.MockRepository(); - let content = Content.Create({ + let content = repo.CreateContent({ Path: '/Root/Sites/Default_Site/tasks', - Status: 'active' as any - }, ContentTypes.Task, repo) + Status: Enums.Status.active + }, ContentTypes.Task) const action = { type: 'LOAD_CONTENT_SUCCESS', response: content @@ -567,10 +705,10 @@ describe('Reducers', () => { it('should return fields of the content', () => { let repo: Mocks.MockRepository = new Mocks.MockRepository(); - let content = Content.Create({ + let content = repo.CreateContent({ Path: '/Root/Sites/Default_Site/tasks', - Status: 'active' as any - }, ContentTypes.Task, repo) + Status: Enums.Status.active + }, ContentTypes.Task) const action = { type: 'RELOAD_CONTENT_SUCCESS', response: content @@ -587,10 +725,10 @@ describe('Reducers', () => { }); it('should return a content', () => { let repo: Mocks.MockRepository = new Mocks.MockRepository(); - let content = Content.Create({ + let content = repo.CreateContent({ Path: '/Root/Sites/Default_Site/tasks', - Status: 'active' as any - }, ContentTypes.Task, repo) + Status: Enums.Status.active + }, ContentTypes.Task) const action = { type: 'LOAD_CONTENT_SUCCESS', response: content @@ -599,10 +737,10 @@ describe('Reducers', () => { }); it('should return a content', () => { let repo: Mocks.MockRepository = new Mocks.MockRepository(); - let content = Content.Create({ + let content = repo.CreateContent({ Path: '/Root/Sites/Default_Site/tasks', - Status: 'active' as any - }, ContentTypes.Task, repo) + Status: Enums.Status.active + }, ContentTypes.Task) const action = { type: 'RELOAD_CONTENT_SUCCESS', response: content @@ -614,6 +752,40 @@ describe('Reducers', () => { it('should return the initial state', () => { expect(Reducers.selected(undefined, {})).to.deep.equal([]); }); + it('should return an array with one item with the id 1', () => { + const action = { + type: 'SELECT_CONTENT', + id: 1 + } + expect(Reducers.selected(undefined, action)).to.deep.equal([1]); + }) + it('should return an array with two items with the id 1 and 2', () => { + const action = { + type: 'SELECT_CONTENT', + id: 2 + } + expect(Reducers.selected([1], action)).to.deep.equal([1, 2]); + }) + it('should return an array with one item with the id 1', () => { + const action = { + type: 'DESELECT_CONTENT', + id: 2 + } + expect(Reducers.selected([1, 2], action)).to.deep.equal([1]); + }) + it('should return an empty array', () => { + const action = { + type: 'DESELECT_CONTENT', + id: 1 + } + expect(Reducers.selected([1], action)).to.deep.equal([]); + }) + it('should return an empty array', () => { + const action = { + type: 'CLEAR_SELECTION' + } + expect(Reducers.selected([1], action)).to.deep.equal([]); + }) }) describe('getContent', () => { const state = { @@ -676,7 +848,7 @@ describe('Reducers', () => { const state = { ids: [5145, 5146], isFetching: false, - errorMessage: 'error' + error: 'error' } it('should return the value of errorMessage from the current state', () => { expect(Reducers.getError(state)).to.be.eq('error'); @@ -716,4 +888,44 @@ describe('Reducers', () => { expect(Reducers.getRepositoryUrl(state)).to.be.eq('https://dmsservice.demo.sensenet.com'); }); }); + describe('getSelectedContent', () => { + const state = { + selected: [1, 2] + } + it('should return the value of the selected reducers current state, an array with two items', () => { + expect(Reducers.getSelectedContent(state)).to.be.deep.equal([1, 2]) + }) + }) + describe('getOpenedContentId', () => { + const state = { + isOpened: 1 + } + it('should return 1 as the opened items id', () => { + expect(Reducers.getOpenedContent(state)).to.be.eq(1) + }) + }) + describe('getChildrenActions', () => { + const state = { + actions: [ + { + ActionName: 'Rename' + } + ] + } + it('should return 1 as the opened items id', () => { + expect(Reducers.getChildrenActions(state)).to.be.deep.equal([{ ActionName: 'Rename' }]) + }) + }) + describe('getCurrentContent', () => { + const state = { + currentcontent: { + content: { + DisplayName: 'my content' + } + } + } + it('should return the content', () => { + expect(Reducers.getCurrentContent(state)).to.be.deep.equal({ DisplayName: 'my content' }) + }) + }) }); \ No newline at end of file From 9f3c050586878e5b5a036b5b65c0b9d3c1f0a9db Mon Sep 17 00:00:00 2001 From: Aniko Litvanyi Date: Mon, 6 Nov 2017 15:03:55 +0100 Subject: [PATCH 4/9] [KFI]test(Epics): Fix Epic tests (#51) --- test/EpicsTests.ts | 236 ++++++++++++++++++++++----------------------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/test/EpicsTests.ts b/test/EpicsTests.ts index f244397..b235fe3 100644 --- a/test/EpicsTests.ts +++ b/test/EpicsTests.ts @@ -9,9 +9,9 @@ const expect = Chai.expect; import 'rxjs'; let store, repo: Mocks.MockRepository, epicMiddleware, mockStore, content; -const initBefores = () => { +const initBefores = (epic) => { repo = new Mocks.MockRepository(); - epicMiddleware = createEpicMiddleware(Epics.fetchContentEpic, { dependencies: { repository: repo } }) + epicMiddleware = createEpicMiddleware(epic, { dependencies: { repository: repo } }) mockStore = configureMockStore([epicMiddleware]); store = mockStore(); content = repo.HandleLoadedContent({ DisplayName: 'My Content', Id: 123, Path: '/workspaces', Name: 'MyContent' }, ContentTypes.Task) @@ -26,7 +26,7 @@ describe('Epics', () => { describe('fetchContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.fetchContentEpic) }); after(() => { @@ -49,59 +49,9 @@ describe('Epics', () => { }]); }) }); - - // describe('initSensenetStoreEpic Epic', () => { - // let store; - // const epicMiddleware = createEpicMiddleware(Epics.initSensenetStoreEpic, { dependencies: { repository: repo } }); - // const mockStore = configureMockStore([epicMiddleware]); - // before(() => { - // store = mockStore(); - // }); - - // after(() => { - // epicMiddleware.replaceEpic(Epics.initSensenetStoreEpic); - // }); - // it('handles the error', () => { - // const user = Content.Create({ Name: 'alba', Id: 123 }, ContentTypes.User, repo) - // store.dispatch({ type: 'INIT_SENSENET_STORE', path: '/workspaces', options: {} }); - // expect(store.getActions()).to.equal( - // [{ - // type: 'INIT_SENSENET_STORE', - // path: '/workspaces', - // options: - // { - // select: [['Id', 'Path', 'Name', 'Type'], - // ['DisplayName', 'Description', 'Icon']], - // metadata: 'no', - // inlinecount: 'allpages', - // expand: undefined, - // top: 1000 - // } - // }, - // { - // type: 'USER_CHANGED', - // user: user - // }, - // { type: 'CHECK_LOGIN_STATE_REQUEST' }, - // { - // type: 'LOAD_CONTENT_FAILURE', - // params: - // { - // select: [['Id', 'Path', 'Name', 'Type'], - // ['DisplayName', 'Description', 'Icon']], - // metadata: 'no', - // inlinecount: 'allpages', - // expand: undefined, - // top: 1000 - // }, - // message: 'XMLHttpRequest is not supported by your browser' - // }]); - // }) - // }) - describe('initSensenetStoreEpic Epic', () => { before(() => { - initBefores() + initBefores(Epics.initSensenetStoreEpic) }); after(() => { @@ -110,15 +60,37 @@ describe('Epics', () => { it('handles the error', () => { const user = repo.CreateContent({ Name: 'alba', Id: 123 }, ContentTypes.User); store.dispatch({ type: 'INIT_SENSENET_STORE', path: '/workspaces', options: {} }); - expect(store.getActions()).to.be.deep.equal( - [{ type: 'INIT_SENSENET_STORE', path: '/workspaces', options: {} }]); + expect(store.getActions()).to.be.deep.equal([ + { + type: 'INIT_SENSENET_STORE', + path: '/workspaces', + options: + { + select: ['Id', + 'Path', + 'Name', + 'Type', + 'DisplayName', + 'Description', + 'Icon'], + metadata: 'no', + inlinecount: 'allpages', + expand: undefined, + top: 1000 + } + }, + { + type: 'LOAD_REPOSITORY', + repository: repo.Config + }, + { type: 'USER_LOGIN_FAILURE', message: null }]); }) }) describe('loadContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.loadContentEpic) }); after(() => { @@ -130,14 +102,27 @@ describe('Epics', () => { [{ type: 'LOAD_CONTENT_REQUEST', path: '/workspaces/Project', - options: {} + options: + { + select: ['Id', + 'Path', + 'Name', + 'Type', + 'DisplayName', + 'Description', + 'Icon'], + metadata: 'no', + inlinecount: 'allpages', + expand: undefined, + top: 1000 + } }]); }) }); describe('reloadContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.reloadContentEpic) }); after(() => { @@ -167,7 +152,7 @@ describe('Epics', () => { }); describe('reloadContentFields Epic', () => { before(() => { - initBefores() + initBefores(Epics.reloadContentFieldsEpic) }); after(() => { @@ -198,7 +183,7 @@ describe('Epics', () => { }); describe('createContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.createContentEpic) }); after(() => { @@ -210,6 +195,10 @@ describe('Epics', () => { [{ type: 'CREATE_CONTENT_REQUEST', content: content + }, + { + type: 'CREATE_CONTENT_SUCCESS', + response: { entities: { entities: { '123': content } }, result: 123 } }]); }) it('handles the error', () => { @@ -219,12 +208,16 @@ describe('Epics', () => { type: 'CREATE_CONTENT_REQUEST', content: content }, + { + type: 'CREATE_CONTENT_SUCCESS', + response: { entities: { entities: { '123': content } }, result: 123 } + }, { type: 'CREATE_CONTENT_FAILURE', error: { message: 'error' } }]); }) }); describe('updateContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.updateContentEpic) }); after(() => { @@ -236,6 +229,10 @@ describe('Epics', () => { [{ type: 'UPDATE_CONTENT_REQUEST', content + }, + { + type: 'UPDATE_CONTENT_SUCCESS', + response: content }]); }) it('handles the error', () => { @@ -245,12 +242,16 @@ describe('Epics', () => { type: 'UPDATE_CONTENT_REQUEST', content: content }, + { + type: 'UPDATE_CONTENT_SUCCESS', + response: content + }, { type: 'UPDATE_CONTENT_FAILURE', error: { message: 'error' } }]); }) }); describe('deleteContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.deleteContentEpic) }); after(() => { @@ -276,37 +277,37 @@ describe('Epics', () => { { type: 'DELETE_CONTENT_FAILURE', error: 'error' }]); }) }); - describe('deleteBatch Epic', () => { - before(() => { - initBefores() - }); + // describe('deleteBatch Epic', () => { + // before(() => { + // initBefores(Epics.deleteBatchEpic) + // }); - after(() => { - epicMiddleware.replaceEpic(Epics.deleteBatchEpic); - }); - it('handles the error', () => { - store.dispatch({ type: 'DELETE_BATCH_REQUEST', ids: ['1', '2'], permanently: false }); - expect(store.getActions()).to.be.deep.eq( - [{ - type: 'DELETE_BATCH_REQUEST', - ids: ['1', '2'], - permanently: false - }]); - }) - it('handles the error', () => { - store.dispatch({ type: 'DELETE_BATCH_FAILURE', error: 'error' }); - expect(store.getActions()).to.be.deep.eq( - [{ - type: 'DELETE_BATCH_REQUEST', - ids: ['1', '2'], - permanently: false - }, - { type: 'DELETE_BATCH_FAILURE', error: 'error' }]); - }) - }); + // after(() => { + // epicMiddleware.replaceEpic(Epics.deleteBatchEpic); + // }); + // it('handles the error', () => { + // store.dispatch({ type: 'DELETE_BATCH_REQUEST', ids: [1, 2], permanently: false }); + // expect(store.getActions()).to.be.deep.eq( + // [{ + // type: 'DELETE_BATCH_REQUEST', + // ids: [1, 2], + // permanently: false + // }]); + // }) + // it('handles the error', () => { + // store.dispatch({ type: 'DELETE_BATCH_FAILURE', error: 'error' }); + // expect(store.getActions()).to.be.deep.eq( + // [{ + // type: 'DELETE_BATCH_REQUEST', + // ids: [1, 2], + // permanently: false + // }, + // { type: 'DELETE_BATCH_FAILURE', error: 'error' }]); + // }) + // }); describe('checkoutContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.checkoutContentEpic) }); after(() => { epicMiddleware.replaceEpic(Epics.checkoutContentEpic); @@ -332,7 +333,7 @@ describe('Epics', () => { }); describe('checkinContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.checkinContentEpic) }); after(() => { @@ -364,7 +365,7 @@ describe('Epics', () => { }); describe('publishContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.publishContentEpic) }); after(() => { @@ -393,7 +394,7 @@ describe('Epics', () => { }); describe('approveContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.approveContentEpic) }); after(() => { @@ -421,7 +422,7 @@ describe('Epics', () => { }); describe('rejectContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.rejectContentEpic) }); afterEach(() => { @@ -452,7 +453,7 @@ describe('Epics', () => { }); describe('undocheckoutContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.undocheckoutContentEpic) }); after(() => { @@ -480,7 +481,7 @@ describe('Epics', () => { }); describe('forceundocheckoutContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.forceundocheckoutContentEpic) }); after(() => { @@ -508,7 +509,7 @@ describe('Epics', () => { }); describe('restoreVersion Epic', () => { before(() => { - initBefores() + initBefores(Epics.restoreversionContentEpic) }); after(() => { epicMiddleware.replaceEpic(Epics.restoreversionContentEpic); @@ -537,7 +538,7 @@ describe('Epics', () => { }); describe('login Epic', () => { before(() => { - initBefores() + initBefores(Epics.userLoginEpic) }); afterEach(() => { @@ -551,7 +552,8 @@ describe('Epics', () => { type: 'USER_LOGIN_REQUEST', username: 'alba', password: 'alba' - }]); + }, + { type: 'USER_LOGIN_FAILURE', message: 'Failed to log in.' }]); }) it('handles the loggedin user', () => { const user = repo.CreateContent({ Name: 'alba', Id: 123 }, ContentTypes.User); @@ -563,6 +565,7 @@ describe('Epics', () => { username: 'alba', password: 'alba' }, + { type: 'USER_LOGIN_FAILURE', message: 'Failed to log in.' }, { type: '@@redux-observable/EPIC_END' }, { type: 'USER_LOGIN_REQUEST', @@ -574,7 +577,7 @@ describe('Epics', () => { }); describe('logout Epic', () => { before(() => { - initBefores() + initBefores(Epics.userLogoutEpic) }); after(() => { @@ -589,7 +592,8 @@ describe('Epics', () => { id: 111, username: 'alba', password: 'alba' - }]); + }, + { type: 'USER_LOGOUT_SUCCESS' }]); }) it('handles the error', () => { (repo.Authentication as Mocks.MockAuthService).StateSubject.next(Authentication.LoginState.Authenticated); @@ -601,19 +605,20 @@ describe('Epics', () => { username: 'alba', password: 'alba' }, + { type: 'USER_LOGOUT_SUCCESS' }, { type: 'USER_LOGOUT_FAILURE', error: 'error' }]); }) }); describe('checkLoginState Epic', () => { beforeEach(() => { - initBefores() + initBefores(Epics.checkLoginStateEpic) }); afterEach(() => { epicMiddleware.replaceEpic(Epics.userLoginEpic); }); it('handles a loggedin user', () => { - const user = repo.CreateContent({ Name: 'alba', Id: 2, Path: '/Root' }, ContentTypes.User); + const user = repo.CreateContent({ Name: 'alba', Id: 2, Path: '/Root' }, ContentTypes.User); store.dispatch(Actions.UserLoginSuccess(user)); (repo.Authentication as Mocks.MockAuthService).StateSubject.next(Authentication.LoginState.Authenticated); store.dispatch({ type: 'CHECK_LOGIN_STATE_REQUEST' }); @@ -622,27 +627,22 @@ describe('Epics', () => { type: 'USER_LOGIN_SUCCESS', response: user }, - { type: 'CHECK_LOGIN_STATE_REQUEST' } - ]); + { type: 'CHECK_LOGIN_STATE_REQUEST' }, + { type: 'USER_LOGIN_BUFFER', response: true } + ]); }) it('handles an error', () => { const user = repo.HandleLoadedContent({ Name: 'alba', Id: 65535, Path: '/Root' }, ContentTypes.User); repo.Authentication.StateSubject.next(Authentication.LoginState.Unauthenticated); store.dispatch({ type: 'CHECK_LOGIN_STATE_REQUEST' }); expect(store.getActions()).to.be.deep.eq( - [ - // { - // type: 'USER_LOGIN_SUCCESS', - // response: user.GetFields() - // }, - // { type: 'CHECK_LOGIN_STATE_REQUEST' }, - // { type: '@@redux-observable/EPIC_END' }, - { type: 'CHECK_LOGIN_STATE_REQUEST' }]); + [{ type: 'CHECK_LOGIN_STATE_REQUEST' }, + { type: 'USER_LOGIN_FAILURE', message: null }]); }) }); describe('getContentActions Epic', () => { before(() => { - initBefores() + initBefores(Epics.getContentActions) }); after(() => { @@ -670,7 +670,7 @@ describe('Epics', () => { }); describe('loadContentActionsEpic Epic', () => { before(() => { - initBefores() + initBefores(Epics.loadContentActionsEpic) }); after(() => { @@ -699,7 +699,7 @@ describe('Epics', () => { }); describe('userLoginBufferEpic Epic', () => { before(() => { - initBefores() + initBefores(Epics.userLoginBufferEpic) }); after(() => { @@ -714,7 +714,7 @@ describe('Epics', () => { describe('uploadContentEpic Epic', () => { before(() => { - initBefores() + initBefores(Epics.uploadFileEpic) }); after(() => { From fe5b11d26eba6135bb6cd8b28db4f73fb32f7816 Mon Sep 17 00:00:00 2001 From: Aniko Litvanyi Date: Tue, 7 Nov 2017 16:23:53 +0100 Subject: [PATCH 5/9] Feat/batchactions (#52) * [KFI]feat(BatchActions): Add new reducer to handle batch action responses * [KFI]feat(BatchActions): Add copy and move batch actions * [KFI]feat(BatchActions): Modify batchActions reducers to handle general errors also * [KFI]docs(BatchActions): Add some docs to the new reducers * [KFI]test(BatchActions): Fix deleteBatch action tests to handle the new arguments * [KFI]test(DeleteBatch): Fix tests * [KFI]test(Reducers): Add tests to test batch response related reducers * [KFI]fix(BatchActions): Add a path param to copybatch and movebatch to hold the target path * [KFI]test(BatchActions): Add tests for testing the new batch actions * [KFI]fix(BatchActions): Improve deleteBatch Epic * [KFI]feat(Selection): Change select and deselect actions to handle a content except an id * [KFI]test(Selection): Fix selection related tests * [KFI]test(Selection): Fix selected reducer tests * [KFI]feat(Selection): Add a new reducer to hold and handle selected content items for batch actions * [KFI]fix(Selection): Fix selectedContentItems reducer and its tests * [KFI]feat(Selection): Add new functions to return to value of selectedIds and selectedContentItems r * [KFI]test(Selection): Add test for testing new selection reducers * [KFI]feat(DeleteBatch): Change id param to contentItems * [KFI]test(DeleteBatch): Fix deleteBatch related tests to handle content items as a param * [KFI]feat(DeleteBatch): Complete deleteBatch functionality * [KFI]test(DeleteBatch): Fix batch delete related tests * [KFI]fix(BatchActions): Change copy and move batch actions first param to a contenlist object * [KFI]test(BatchActions): Fix tests that are related to the changed param * [KFI]feat(MoveBatch): Add move batch action to the ids and entities reducers * [KFI]test(MoveBatch): Add moveBatch action related tests * [KFI]feat(BatchActionEpics): Add move- and copyBatch epics * [KFI]test(BatchEpics): Add copy- and movebatch epic tests --- src/Actions.ts | 88 +++++++++-- src/Epics.ts | 49 +++++- src/Reducers.ts | 101 ++++++++++-- test/ActionsTests.ts | 130 ++++++++++++--- test/EpicsTests.ts | 115 ++++++++++---- test/ReducersTests.ts | 359 ++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 751 insertions(+), 91 deletions(-) diff --git a/src/Actions.ts b/src/Actions.ts index 9461fc6..fe4cab5 100644 --- a/src/Actions.ts +++ b/src/Actions.ts @@ -1,6 +1,6 @@ import { normalize } from 'normalizr'; import { Schemas } from './Schema'; -import { Content, IContent, ODataApi, ODataHelper, Repository, ContentTypes } from 'sn-client-js'; +import { Content, SavedContent, IContent, ODataApi, ContentTypes } from 'sn-client-js'; /** * Module that contains the action creators. @@ -364,25 +364,23 @@ export module Actions { }) /** * Action creator for deleting multiple Content from the Content Repository. - * @param path {string} Path of parent the Content. - * @param ids {string[]} Array of ids of the Content that should be deleted. + * @param ids {number[]} Array of ids of the Content that should be deleted. * @param permanently {boolean} Defines whether Content must be moved to the Trash or deleted permanently. * @returns {Object} Returns a redux action with the properties type, id and permanently. */ - export const DeleteBatch = (path: string, ids: string[], permanently: boolean = false) => ({ + export const DeleteBatch = (contentItems: Object, permanently: boolean = false) => ({ type: 'DELETE_BATCH_REQUEST', - path, - ids, + contentItems, permanently }) /** - * Action creator for the step when multiple Content deleted successfully. - * @param indexes {number[]} Array of indexes of the items in the state collection that should be removed. + * Action creator for the step when multiple Content was deleted successfully. + * @param response {ODataApi.ODataBatchResponse} response object contains the list of successes and/or errors. * @returns {Object} Returns a redux action with the properties type and index. */ - export const DeleteBatchSuccess = (ids: number[]) => ({ + export const DeleteBatchSuccess = (response: ODataApi.ODataBatchResponse) => ({ type: 'DELETE_BATCH_SUCCESS', - ids + response }) /** * Action creator for the step when deleting multiple Content is failed. @@ -393,6 +391,64 @@ export module Actions { type: 'DELETE_BATCH_FAILURE', message: error.message }) + /** + * Action creator for copying multiple Content in the Content Repository. + * @param ids {number[]} Array of ids of the Content that should be deleted. + * @param permanently {boolean} Defines whether Content must be moved to the Trash or deleted permanently. + * @returns {Object} Returns a redux action with the properties type, id and permanently. + */ + export const CopyBatch = (contentItems: Object, path: string) => ({ + type: 'COPY_BATCH_REQUEST', + contentItems, + path + }) + /** + * Action creator for the step when multiple Content was copied successfully. + * @param response {ODataApi.ODataBatchResponse} response object contains the list of successes and/or errors. + * @returns {Object} Returns a redux action with the properties type and index. + */ + export const CopyBatchSuccess = (response: ODataApi.ODataBatchResponse) => ({ + type: 'COPY_BATCH_SUCCESS', + response + }) + /** + * Action creator for the step when copying multiple Content is failed. + * @param error {any} The catched error object. + * @returns {Object} Returns a redux action with the properties type and the error message. + */ + export const CopyBatchFailure = (error: any) => ({ + type: 'COPY_BATCH_FAILURE', + message: error.message + }) + /** + * Action creator for moving multiple Content in the Content Repository. + * @param ids {number[]} Array of ids of the Content that should be deleted. + * @param permanently {boolean} Defines whether Content must be moved to the Trash or deleted permanently. + * @returns {Object} Returns a redux action with the properties type, id and permanently. + */ + export const MoveBatch = (contentItems = {}, path: string) => ({ + type: 'MOVE_BATCH_REQUEST', + contentItems, + path + }) + /** + * Action creator for the step when multiple Content was moved successfully. + * @param response {ODataApi.ODataBatchResponse} response object contains the list of successes and/or errors. + * @returns {Object} Returns a redux action with the properties type and index. + */ + export const MoveBatchSuccess = (response: ODataApi.ODataBatchResponse) => ({ + type: 'MOVE_BATCH_SUCCESS', + response + }) + /** + * Action creator for the step when moving multiple Content is failed. + * @param error {any} The catched error object. + * @returns {Object} Returns a redux action with the properties type and the error message. + */ + export const MoveBatchFailure = (error: any) => ({ + type: 'MOVE_BATCH_FAILURE', + message: error.message + }) /** * Action creator for checking out a Content in the Content Repository. * @param content {number} Content that should be checked out. @@ -666,7 +722,7 @@ export module Actions { * @param error {any} The catched error object. * @returns {Object} Returns a redux action with the properties type and the error message. */ - export const UserLoginFailure = (error: {status?: number, message: string}) => ({ + export const UserLoginFailure = (error: { status?: number, message: string }) => ({ type: 'USER_LOGIN_FAILURE', message: (error.status === 403) ? 'The username or the password is not valid!' : error.message }) @@ -706,20 +762,20 @@ export module Actions { /** * Action creator for selecting a Content * @param id {number} The id of the selected Content - * @returns {Object} Returns a redux action. + * @returns {Object} Returns a redux action. */ - export const SelectContent = (id) => ({ + export const SelectContent = (content) => ({ type: 'SELECT_CONTENT', - id + content }) /** * Action creator for deselecting a Content * @param id {number} The id of the deselected Content * @returns {Object} Returns a redux action. */ - export const DeSelectContent = (id) => ({ + export const DeSelectContent = (content) => ({ type: 'DESELECT_CONTENT', - id + content })/** * Action creator for clearing the array of selected content * @returns {Object} Returns a redux action. diff --git a/src/Epics.ts b/src/Epics.ts index e6571b8..15958e4 100644 --- a/src/Epics.ts +++ b/src/Epics.ts @@ -2,9 +2,10 @@ import { Actions } from './Actions'; import { Reducers } from './Reducers'; import { ActionsObservable, combineEpics } from 'redux-observable'; -import { Observable } from 'rxjs/Observable'; import { Repository, Content, ContentTypes, Collection, ODataApi, Authentication } from 'sn-client-js'; +import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/mergeMap'; +import 'rxjs/add/operator/catch' /** * Module for redux-observable Epics of the sensenet built-in OData actions. @@ -193,16 +194,50 @@ export module Epics { export const deleteBatchEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { return action$.ofType('DELETE_BATCH_REQUEST') .mergeMap(action => { - let collection = new Collection.Collection([], dependencies.repository, action.contentType); - return collection.Remove(action.ids, false) + let contentItems = Object.keys(action.contentItems).map(id => { + return dependencies.repository.HandleLoadedContent(action.contentItems[id], action.contentItems.__contentType); + }); + return dependencies.repository.DeleteBatch(contentItems, action.permanently) .map((response) => { - const state = store.getState(); - const ids = Reducers.getIds(state.collection); - return Actions.DeleteBatchSuccess(ids); + return Actions.DeleteBatchSuccess(response); }) .catch(error => Observable.of(Actions.DeleteBatchFailure(error))) }) } + /** + * Epic to copy multiple Content in the Content Repository. It is related to three redux actions, returns ```CopyBatch``` action and sends the response to the + * ```CopyBatchSuccess``` action if the ajax request ended successfully or catches the error if the request failed and sends the error message to the ```CopyBatchFailure``` action. + */ + export const copyBatchEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { + return action$.ofType('COPY_BATCH_REQUEST') + .mergeMap(action => { + let contentItems = Object.keys(action.contentItems).map(id => { + return dependencies.repository.HandleLoadedContent(action.contentItems[id], action.contentItems.__contentType); + }); + return dependencies.repository.CopyBatch(contentItems, action.path) + .map((response) => { + return Actions.CopyBatchSuccess(response); + }) + .catch(error => Observable.of(Actions.CopyBatchFailure(error))) + }) + } + /** + * Epic to move multiple Content in the Content Repository. It is related to three redux actions, returns ```MoveBatch``` action and sends the response to the + * ```MoveBatchSuccess``` action if the ajax request ended successfully or catches the error if the request failed and sends the error message to the ```MoveBatchFailure``` action. + */ + export const moveBatchEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { + return action$.ofType('MOVE_BATCH_REQUEST') + .mergeMap(action => { + let contentItems = Object.keys(action.contentItems).map(id => { + return dependencies.repository.HandleLoadedContent(action.contentItems[id], action.contentItems.__contentType); + }); + return dependencies.repository.MoveBatch(contentItems, action.path) + .map((response) => { + return Actions.MoveBatchSuccess(response); + }) + .catch(error => Observable.of(Actions.MoveBatchFailure(error))) + }) + } /** * Epic to checkout a Content in the Content Repository. It is related to three redux actions, returns ```CheckOut``` action and sends the response to the * ```CheckOutSuccess``` action if the ajax request ended successfully or catches the error if the request failed and sends the error message to the ```CheckOutFailure``` action. @@ -402,6 +437,8 @@ export module Epics { updateContentEpic, deleteContentEpic, deleteBatchEpic, + copyBatchEpic, + moveBatchEpic, checkoutContentEpic, checkinContentEpic, publishContentEpic, diff --git a/src/Reducers.ts b/src/Reducers.ts index 18d024e..903e216 100644 --- a/src/Reducers.ts +++ b/src/Reducers.ts @@ -1,4 +1,3 @@ -import { normalize } from 'normalizr'; import { combineReducers } from 'redux'; import { Authentication } from 'sn-client-js'; @@ -188,6 +187,18 @@ export module Reducers { return state case 'DELETE_CONTENT_SUCCESS': return [...state.slice(0, action.index), ...state.slice(action.index + 1)] + case 'DELETE_BATCH_SUCCESS': + case 'MOVE_BATCH_SUCCESS': + if (action.response.d.results.length > 0) { + let newIds = [] + let deletedIds = action.response.d.results.map(result => result.Id) + for (let i = 0; i < state.length; i++) { + if (deletedIds.indexOf(state[i]) === -1) { + newIds.push(state[i]) + } + } + return newIds + } default: return state; } @@ -205,7 +216,10 @@ export module Reducers { action.type !== 'LOAD_CONTENT_SUCCESS' && action.type !== 'REQUEST_CONTENT_ACTIONS_SUCCESS' && action.type !== 'UPDATE_CONTENT_SUCCESS' && - action.type !== 'UPLOAD_CONTENT_SUCCESS')) { + action.type !== 'UPLOAD_CONTENT_SUCCESS' && + action.type !== 'DELETE_BATCH_SUCCESS' && + action.type !== 'COPY_BATCH_SUCCESS' && + action.type !== 'MOVE_BATCH_SUCCESS')) { return (Object).assign({}, state, action.response.entities.entities); } switch (action.type) { @@ -213,6 +227,11 @@ export module Reducers { let res = Object.assign({}, state); delete res[action.id]; return res; + case 'DELETE_BATCH_SUCCESS': + case 'MOVE_BATCH_SUCCESS': + let resource = Object.assign({}, state); + action.response.d.results.map(result => delete resource[result.Id]) + return resource; case 'UPDATE_CONTENT_SUCCESS': state[action.response.Id] = action.response return state @@ -584,16 +603,16 @@ export module Reducers { }) /** * Reducer to handle Actions on the selected array. - * @param {Object} [state=[]] Represents the current state. + * @param {Array} [state=[]] Represents the current state. * @param {Object} action Represents an action that is called. * @returns {Object} state. Returns the next state based on the action. */ - export const selected = (state = [], action) => { + export const selectedIds = (state = [], action) => { switch (action.type) { case 'SELECT_CONTENT': - return [...state, action.id] + return [...state, action.content.Id] case 'DESELECT_CONTENT': - const index = state.indexOf(action.id) + const index = state.indexOf(action.content.Id) return [...state.slice(0, index), ...state.slice(index + 1)] case 'CLEAR_SELECTION': return [] @@ -601,6 +620,65 @@ export module Reducers { return state } } + export const selectedContentItems = (state = {}, action) => { + switch (action.type) { + case 'DESELECT_CONTENT': + let res = Object.assign({}, state); + delete res[action.content.Id]; + return res; + case 'SELECT_CONTENT': + let obj = {} + obj[action.content.Id] = action.content + return (Object).assign({}, state, obj); + case 'CLEAR_SELECTION': + return {} + default: + return state; + } + } + export const selected = combineReducers({ + ids: selectedIds, + entities: selectedContentItems + }) + /** + * Reducer to handle Actions on the OdataBatchResponse Object. + * @param {Array} state Represents the current state. + * @param {Object} action Represents an action that is called. + * @returns {Object} state. Returns the next state based on the action. + */ + export const OdataBatchResponse = (state = Object, action) => { + switch (action.type) { + case 'DELETE_BATCH_SUCCESS': + case 'COPY_BATCH_SUCCESS': + case 'MOVE_BATCH_SUCCESS': + return action.response + default: + return {} + } + } + /** + * Reducer to handle Actions on the batchResponseError Object. + * @param {string} state Represents the current state. + * @param {Object} action Represents an action that is called. + * @returns {Object} state. Returns the next state based on the action. + */ + export const batchResponseError = (state = '', action) => { + switch (action.type) { + case 'DELETE_BATCH_FAILURE': + case 'COPY_BATCH_FAILURE': + case 'MOVE_BATCH_FAILURE': + return action.message + default: + return '' + } + } + /** + * Reducer combining response and error into a single object, ```batchResponses```. + */ + export const batchResponses = combineReducers({ + response: OdataBatchResponse, + error: batchResponseError + }) /** * Reducer combining session, children, currentcontent and selected into a single object, ```sensenet``` which will be the top-level one. */ @@ -608,7 +686,8 @@ export module Reducers { session, children, currentcontent, - selected + selected, + batchResponses }) /** @@ -651,8 +730,12 @@ export module Reducers { return state.session.repository.RepositoryUrl; } - export const getSelectedContent = (state) => { - return state.selected + export const getSelectedContentIds = (state) => { + return state.selected.ids + } + + export const getSelectedContentItems = (state) => { + return state.selected.entities } export const getOpenedContent = (state) => { diff --git a/test/ActionsTests.ts b/test/ActionsTests.ts index a6a41b8..def0afc 100644 --- a/test/ActionsTests.ts +++ b/test/ActionsTests.ts @@ -1,7 +1,7 @@ /// import { Actions } from '../src/Actions' import * as Chai from 'chai'; -import { Mocks, ContentTypes, Repository } from 'sn-client-js'; +import { Mocks, ContentTypes, Repository, ODataApi } from 'sn-client-js'; const expect = Chai.expect; describe('Actions', () => { @@ -248,27 +248,36 @@ describe('Actions', () => { it('should create an action to a delete content request', () => { const expectedAction = { type: 'DELETE_BATCH_REQUEST', - path: path, - ids: ['1', '2', '3'], - permanently: false - } - expect(Actions.DeleteBatch(path, ['1', '2', '3'], false)).to.deep.equal(expectedAction) - }); - it('should create an action to a delete content request', () => { - const expectedAction = { - type: 'DELETE_BATCH_REQUEST', - path: path, - ids: ['1', '2', '3'], + contentItems: { + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + }, permanently: false } - expect(Actions.DeleteBatch(path, ['1', '2', '3'])).to.deep.equal(expectedAction) + expect(Actions.DeleteBatch({ + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + })).to.deep.equal(expectedAction) }); it('should create an action to delete content success', () => { + const response = new ODataApi.ODataBatchResponse() const expectedAction = { type: 'DELETE_BATCH_SUCCESS', - ids: [0, 1, 2] + response: response } - expect(Actions.DeleteBatchSuccess([0, 1, 2])).to.deep.equal(expectedAction) + expect(Actions.DeleteBatchSuccess(response)).to.deep.equal(expectedAction) }); it('should create an action to delete content failure', () => { const expectedAction = { @@ -278,6 +287,87 @@ describe('Actions', () => { expect(Actions.DeleteBatchFailure({ message: 'error' })).to.deep.equal(expectedAction) }); }); + describe('CopyBatchContent', () => { + it('should create an action to a copy multiple content request', () => { + const expectedAction = { + type: 'COPY_BATCH_REQUEST', + contentItems: + { + '1': { DisplaName: 'aaa', Id: 1 }, + '2': { DisplaName: 'bbb', Id: 2 } + }, + path: '/workspaces' + } + expect(Actions.CopyBatch({ + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + }, '/workspaces')).to.deep.equal(expectedAction) + }); + it('should create an action to copy multiple content success', () => { + const response = new ODataApi.ODataBatchResponse() + const expectedAction = { + type: 'COPY_BATCH_SUCCESS', + response: response + } + expect(Actions.CopyBatchSuccess(response)).to.deep.equal(expectedAction) + }); + it('should create an action to copy multiple content failure', () => { + const expectedAction = { + type: 'COPY_BATCH_FAILURE', + message: 'error' + } + expect(Actions.CopyBatchFailure({ message: 'error' })).to.deep.equal(expectedAction) + }); + }); + describe('MoveBatchContent', () => { + it('should create an action to a move multiple content request', () => { + const expectedAction = { + type: 'MOVE_BATCH_REQUEST', + contentItems: { + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + }, + path: '/workspaces' + } + expect(Actions.MoveBatch({ + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + }, '/workspaces')).to.deep.equal(expectedAction) + }); + it('should create an action to move multiple content success', () => { + const response = new ODataApi.ODataBatchResponse() + const expectedAction = { + type: 'MOVE_BATCH_SUCCESS', + response: response + } + expect(Actions.MoveBatchSuccess(response)).to.deep.equal(expectedAction) + }); + it('should create an action to move multiple content failure', () => { + const expectedAction = { + type: 'MOVE_BATCH_FAILURE', + message: 'error' + } + expect(Actions.MoveBatchFailure({ message: 'error' })).to.deep.equal(expectedAction) + }); + }); describe('CheckoutContent', () => { const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) it('should create an action to a checkout content request', () => { @@ -555,21 +645,23 @@ describe('Actions', () => { }); }); describe('SelectContent', () => { + const content = repo.CreateContent({ DisplayName: 'My content', Id: 1 }, ContentTypes.Task); it('should return the select content action', () => { const expectedAction = { type: 'SELECT_CONTENT', - id: 1 + content: content } - expect(Actions.SelectContent(1)).to.deep.equal(expectedAction) + expect(Actions.SelectContent(content)).to.deep.equal(expectedAction) }) }) describe('DeSelectContent', () => { + const content = repo.CreateContent({ DisplayName: 'My content', Id: 1 }, ContentTypes.Task); it('should return the deselect content action', () => { const expectedAction = { type: 'DESELECT_CONTENT', - id: 1 + content: content } - expect(Actions.DeSelectContent(1)).to.deep.equal(expectedAction) + expect(Actions.DeSelectContent(content)).to.deep.equal(expectedAction) }) }) describe('ClearSelection', () => { diff --git a/test/EpicsTests.ts b/test/EpicsTests.ts index b235fe3..99021a4 100644 --- a/test/EpicsTests.ts +++ b/test/EpicsTests.ts @@ -277,34 +277,93 @@ describe('Epics', () => { { type: 'DELETE_CONTENT_FAILURE', error: 'error' }]); }) }); - // describe('deleteBatch Epic', () => { - // before(() => { - // initBefores(Epics.deleteBatchEpic) - // }); - - // after(() => { - // epicMiddleware.replaceEpic(Epics.deleteBatchEpic); - // }); - // it('handles the error', () => { - // store.dispatch({ type: 'DELETE_BATCH_REQUEST', ids: [1, 2], permanently: false }); - // expect(store.getActions()).to.be.deep.eq( - // [{ - // type: 'DELETE_BATCH_REQUEST', - // ids: [1, 2], - // permanently: false - // }]); - // }) - // it('handles the error', () => { - // store.dispatch({ type: 'DELETE_BATCH_FAILURE', error: 'error' }); - // expect(store.getActions()).to.be.deep.eq( - // [{ - // type: 'DELETE_BATCH_REQUEST', - // ids: [1, 2], - // permanently: false - // }, - // { type: 'DELETE_BATCH_FAILURE', error: 'error' }]); - // }) - // }); + describe('deleteBatch Epic', () => { + before(() => { + initBefores(Epics.deleteBatchEpic) + }); + + after(() => { + epicMiddleware.replaceEpic(Epics.deleteBatchEpic); + }); + it('handles the error', () => { + store.dispatch({ + type: 'DELETE_BATCH_REQUEST', contentItems: { + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + }, permanently: false + }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'DELETE_BATCH_REQUEST', + contentItems: { + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + }, + permanently: false + }]); + }) + it('handles the error', () => { + store.dispatch({ type: 'DELETE_BATCH_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ + type: 'DELETE_BATCH_REQUEST', + contentItems: { + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + }, + permanently: false + }, + { type: 'DELETE_BATCH_FAILURE', error: 'error' }]); + }) + }); + describe('copyBatch Epic', () => { + before(() => { + initBefores(Epics.copyBatchEpic) + }); + + after(() => { + epicMiddleware.replaceEpic(Epics.copyBatchEpic); + }); + + it('handles the error', () => { + store.dispatch({ type: 'COPY_BATCH_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ type: 'COPY_BATCH_FAILURE', error: 'error' }]); + }) + }); + describe('moveBatch Epic', () => { + before(() => { + initBefores(Epics.moveBatchEpic) + }); + + after(() => { + epicMiddleware.replaceEpic(Epics.moveBatchEpic); + }); + + it('handles the error', () => { + store.dispatch({ type: 'MOVE_BATCH_FAILURE', error: 'error' }); + expect(store.getActions()).to.be.deep.eq( + [{ type: 'MOVE_BATCH_FAILURE', error: 'error' }]); + }) + }); describe('checkoutContent Epic', () => { before(() => { initBefores(Epics.checkoutContentEpic) diff --git a/test/ReducersTests.ts b/test/ReducersTests.ts index 7da2a46..efd510a 100644 --- a/test/ReducersTests.ts +++ b/test/ReducersTests.ts @@ -183,6 +183,56 @@ describe('Reducers', () => { })) .to.be.deep.equal([1, 2, 3]); }); + it('should handle DELETE_BATCH_SUCCESS', () => { + expect(Reducers.ids([1, 2, 3], { + type: 'DELETE_BATCH_SUCCESS', + response: { + 'd': { + 'results': [ + { 'Id': 1 }, + { 'Id': 2 } + ], + 'errors': [] + } + } + })).to.be.deep.equal([3]); + }); + it('should handle DELETE_BATCH_SUCCESS', () => { + expect(Reducers.ids([1, 2, 3], { + type: 'DELETE_BATCH_SUCCESS', + response: { + 'd': { + 'results': [], + 'errors': [] + } + } + })).to.be.deep.equal([1, 2, 3]); + }); + it('should handle MOVE_BATCH_SUCCESS', () => { + expect(Reducers.ids([1, 2, 3], { + type: 'MOVE_BATCH_SUCCESS', + response: { + 'd': { + 'results': [ + { 'Id': 1 }, + { 'Id': 2 } + ], + 'errors': [] + } + } + })).to.be.deep.equal([3]); + }); + it('should handle MOVE_BATCH_SUCCESS', () => { + expect(Reducers.ids([1, 2, 3], { + type: 'MOVE_BATCH_SUCCESS', + response: { + 'd': { + 'results': [], + 'errors': [] + } + } + })).to.be.deep.equal([1, 2, 3]); + }); }); describe('entities reducer', () => { @@ -286,6 +336,37 @@ describe('Reducers', () => { } ); }); + it('should handle DELETE_BATCH_SUCCESS', () => { + const entities = { + 5122: { + Id: 5122, + DisplayName: 'Some Article', + Status: ['Active'] + }, + 5146: { + Id: 5146, + Displayname: 'Other Article', + Status: ['Completed'] + } + }; + expect(Reducers.entities(entities, { + type: 'DELETE_BATCH_SUCCESS', + response: { + 'd': { + 'results': [ + { 'Id': 5122 } + ], + 'errors': [] + } + } + })).to.be.deep.equal({ + 5146: { + Id: 5146, + Displayname: 'Other Article', + Status: ['Completed'] + } + }); + }); }); describe('isFetching reducer', () => { @@ -749,42 +830,253 @@ describe('Reducers', () => { }); }) describe('selected reducer', () => { + let repo: Mocks.MockRepository = new Mocks.MockRepository(); + it('should return the initial state', () => { - expect(Reducers.selected(undefined, {})).to.deep.equal([]); + expect(Reducers.selectedIds(undefined, {})).to.deep.equal([]); }); it('should return an array with one item with the id 1', () => { + let content = repo.CreateContent({ + Path: '/Root/Sites/Default_Site/tasks', + Status: Enums.Status.active, + Id: 1 + }, ContentTypes.Task) const action = { type: 'SELECT_CONTENT', - id: 1 + content: content } - expect(Reducers.selected(undefined, action)).to.deep.equal([1]); + expect(Reducers.selectedIds(undefined, action)).to.deep.equal([1]); }) it('should return an array with two items with the id 1 and 2', () => { + let content = repo.CreateContent({ + Path: '/Root/Sites/Default_Site/tasks', + Status: Enums.Status.active, + Id: 2 + }, ContentTypes.Task) const action = { type: 'SELECT_CONTENT', - id: 2 + content: content } - expect(Reducers.selected([1], action)).to.deep.equal([1, 2]); + expect(Reducers.selectedIds([1], action)).to.deep.equal([1, 2]); }) it('should return an array with one item with the id 1', () => { + let content = repo.CreateContent({ + Path: '/Root/Sites/Default_Site/tasks', + Status: Enums.Status.active, + Id: 2 + }, ContentTypes.Task) const action = { type: 'DESELECT_CONTENT', - id: 2 + content: content } - expect(Reducers.selected([1, 2], action)).to.deep.equal([1]); + expect(Reducers.selectedIds([1, 2], action)).to.deep.equal([1]); }) it('should return an empty array', () => { + let content = repo.CreateContent({ + Path: '/Root/Sites/Default_Site/tasks', + Status: Enums.Status.active, + Id: 1 + }, ContentTypes.Task) const action = { type: 'DESELECT_CONTENT', - id: 1 + content: content } - expect(Reducers.selected([1], action)).to.deep.equal([]); + expect(Reducers.selectedIds([1], action)).to.deep.equal([]); }) it('should return an empty array', () => { const action = { type: 'CLEAR_SELECTION' } - expect(Reducers.selected([1], action)).to.deep.equal([]); + expect(Reducers.selectedIds([1], action)).to.deep.equal([]); + }) + }) + describe('selectedContent reducer', () => { + let repo: Mocks.MockRepository = new Mocks.MockRepository(); + + it('should return the initial state', () => { + expect(Reducers.selectedContentItems(undefined, {})).to.deep.equal({}); + }); + it('should return an object with one children item with the id 1', () => { + let content = repo.CreateContent({ + Path: '/Root/Sites/Default_Site/tasks', + Status: Enums.Status.active, + Id: 1 + }, ContentTypes.Task) + const action = { + type: 'SELECT_CONTENT', + content: content + } + expect(Reducers.selectedContentItems(undefined, action)).to.deep.equal({ 1: content }); + }) + it('should return an object with two items with the id 1 and 2', () => { + const entities = { + 1: { + Id: 1, + DisplayName: 'Some Article', + Status: ['Active'] + } + }; + let content = repo.CreateContent({ + Path: '/Root/Sites/Default_Site/tasks', + Status: Enums.Status.active, + Id: 2 + }, ContentTypes.Task) + const action = { + type: 'SELECT_CONTENT', + content: content + } + expect(Reducers.selectedContentItems(entities, action)).to.deep.equal( + { + 1: { + Id: 1, + DisplayName: 'Some Article', + Status: ['Active'] + }, + 2: content + } + ); + }) + it('should return an object with one item with the id 1', () => { + const entities = { + 1: { + Id: 1, + DisplayName: 'Some Article', + Status: ['Active'] + }, + 2: { + Id: 2, + DisplayName: 'Some Article', + Status: ['Active'] + } + }; + let content = repo.CreateContent({ + Path: '/Root/Sites/Default_Site/tasks', + Status: Enums.Status.active, + Id: 2 + }, ContentTypes.Task) + const action = { + type: 'DESELECT_CONTENT', + content: content + } + expect(Reducers.selectedContentItems(entities, action)).to.deep.equal( + { + 1: { + Id: 1, + DisplayName: 'Some Article', + Status: ['Active'] + } + } + ); + }) + it('should return an empty object', () => { + const entities = { + 1: { + Id: 1, + DisplayName: 'Some Article', + Status: ['Active'] + } + }; + let content = repo.CreateContent({ + Path: '/Root/Sites/Default_Site/tasks', + Status: Enums.Status.active, + Id: 1 + }, ContentTypes.Task) + const action = { + type: 'DESELECT_CONTENT', + content: content + } + expect(Reducers.selectedContentItems(entities, action)).to.deep.equal({}); + }) + it('should return an empty object', () => { + const entities = { + 1: { + Id: 1, + DisplayName: 'Some Article', + Status: ['Active'] + } + }; + const action = { + type: 'CLEAR_SELECTION' + } + expect(Reducers.selectedContentItems(entities, action)).to.deep.equal({}); + }) + }) + describe('batchResponseError reducer', () => { + it('should return the initial state', () => { + expect(Reducers.batchResponseError(undefined, {})).to.deep.equal(''); + }); + it('should return an error message', () => { + const action = { + type: 'DELETE_BATCH_FAILURE', + message: 'error' + } + expect(Reducers.batchResponseError(undefined, action)).to.deep.equal('error'); + }) + it('should return an error message', () => { + const action = { + type: 'COPY_BATCH_FAILURE', + message: 'error' + } + expect(Reducers.batchResponseError(undefined, action)).to.deep.equal('error'); + }) + it('should return an error message', () => { + const action = { + type: 'MOVE_BATCH_FAILURE', + message: 'error' + } + expect(Reducers.batchResponseError(undefined, action)).to.deep.equal('error'); + }) + it('should return an empty string', () => { + const action = { + type: 'MOVE_BATCH_SUCCESS', + response: {} + } + expect(Reducers.batchResponseError(undefined, action)).to.deep.equal(''); + }) + }) + describe('OdataBatchResponse reducer', () => { + it('should return the initial state', () => { + expect(Reducers.OdataBatchResponse(undefined, {})).to.deep.equal({}); + }); + it('should return a response object', () => { + const action = { + type: 'DELETE_BATCH_SUCCESS', + response: { + vmi: '1' + } + } + expect(Reducers.OdataBatchResponse(undefined, action)).to.deep.equal({ + vmi: '1' + }); + }) + it('should return an error message', () => { + const action = { + type: 'COPY_BATCH_SUCCESS', + response: { + vmi: '1' + } + } + expect(Reducers.OdataBatchResponse(undefined, action)).to.deep.equal({ + vmi: '1' + }); + }) + it('should return an error message', () => { + const action = { + type: 'MOVE_BATCH_SUCCESS', + response: { + vmi: '1' + } + } + expect(Reducers.OdataBatchResponse(undefined, action)).to.deep.equal({ + vmi: '1' + }); + }) + it('should return an empty string', () => { + const action = { + type: 'MOVE_BATCH_FAILURE', + message: 'error' + } + expect(Reducers.OdataBatchResponse(undefined, action)).to.deep.equal({}); }) }) describe('getContent', () => { @@ -888,12 +1180,53 @@ describe('Reducers', () => { expect(Reducers.getRepositoryUrl(state)).to.be.eq('https://dmsservice.demo.sensenet.com'); }); }); - describe('getSelectedContent', () => { + describe('getSelectedContentIds', () => { const state = { - selected: [1, 2] + selected: { + ids: [1, 2], + entities: { + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + } + } } it('should return the value of the selected reducers current state, an array with two items', () => { - expect(Reducers.getSelectedContent(state)).to.be.deep.equal([1, 2]) + expect(Reducers.getSelectedContentIds(state)).to.be.deep.equal([1, 2]) + }) + }) + describe('getSelectedContentItems', () => { + const state = { + selected: { + ids: [1, 2], + entities: { + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + } + } + } + it('should return the value of the selected reducers current state, an array with two items', () => { + expect(Reducers.getSelectedContentItems(state)).to.be.deep.equal({ + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + }) }) }) describe('getOpenedContentId', () => { From cc9a16872db52d1bafe4771defdaded7079b6344 Mon Sep 17 00:00:00 2001 From: Aniko Litvanyi Date: Tue, 14 Nov 2017 12:59:37 +0100 Subject: [PATCH 6/9] Feat/addnew upload (#53) * [KFI]feat(Actions): Add a new param to requestContentActions to make it possible to add custom actio * [KFI]test(Actions): Fix RequestContentActions tests * [KFI]feat(Epics): Change getContentActions epic to handle custom listitems * [KFI]fix: Remove unused variables and add the check into the tsconfig.json to detect them in the fut * [KFI]feat(Reducers): Add new reducer to retrieving children entities object * [KFI]test(Reducers): Add test for testing getChildren reducer --- src/Actions.ts | 7 ++++--- src/Epics.ts | 6 +++--- src/Reducers.ts | 3 +++ src/Store.ts | 1 - test/ActionsTests.ts | 15 ++++++++++++--- test/EpicsTests.ts | 21 +-------------------- test/ReducersTests.ts | 33 +++++++++++++++++++++++++++++++-- tsconfig.json | 1 + 8 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/Actions.ts b/src/Actions.ts index fe4cab5..f411c86 100644 --- a/src/Actions.ts +++ b/src/Actions.ts @@ -1,6 +1,6 @@ import { normalize } from 'normalizr'; import { Schemas } from './Schema'; -import { Content, SavedContent, IContent, ODataApi, ContentTypes } from 'sn-client-js'; +import { Content, IContent, ODataApi, ContentTypes } from 'sn-client-js'; /** * Module that contains the action creators. @@ -789,10 +789,11 @@ export module Actions { * @param scenario {string} The name of the scenario * @returns {Object} Returns a redux action. */ - export const RequestContentActions = (content, scenario?: string) => ({ + export const RequestContentActions = (content, scenario?: string, customItems?: Object[]) => ({ type: 'REQUEST_CONTENT_ACTIONS', content, - scenario + scenario, + customItems: customItems || [] }) /** * Action creator for the step getting the actions of a content successfully. diff --git a/src/Epics.ts b/src/Epics.ts index 15958e4..db0983e 100644 --- a/src/Epics.ts +++ b/src/Epics.ts @@ -1,8 +1,8 @@ import { Actions } from './Actions'; import { Reducers } from './Reducers'; -import { ActionsObservable, combineEpics } from 'redux-observable'; -import { Repository, Content, ContentTypes, Collection, ODataApi, Authentication } from 'sn-client-js'; +import { combineEpics } from 'redux-observable'; +import { Repository, ContentTypes, Collection, Authentication } from 'sn-client-js'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/catch' @@ -400,7 +400,7 @@ export module Epics { .mergeMap(action => { let c = dependencies.repository.HandleLoadedContent(action.content, ContentTypes.GenericContent); return c.Actions(action.scenario) - .map(result => Actions.RequestContentActionsSuccess(result, action.content.Id)) + .map(result => Actions.RequestContentActionsSuccess([...result, ...action.customItems], action.content.Id)) .catch(error => Observable.of(Actions.RequestContentActionsFailure(error))) }) } diff --git a/src/Reducers.ts b/src/Reducers.ts index 903e216..b0843e4 100644 --- a/src/Reducers.ts +++ b/src/Reducers.ts @@ -749,4 +749,7 @@ export module Reducers { export const getCurrentContent = (state) => { return state.currentcontent.content } + export const getChildren = (state) => { + return state.entities + } } \ No newline at end of file diff --git a/src/Store.ts b/src/Store.ts index 4f7f383..83d668a 100644 --- a/src/Store.ts +++ b/src/Store.ts @@ -3,7 +3,6 @@ import { createLogger } from 'redux-logger' import { createEpicMiddleware } from 'redux-observable'; import { Epics } from './Epics'; import { Reducers } from './Reducers'; -import { Actions } from './Actions'; import { Repository } from 'sn-client-js'; /** diff --git a/test/ActionsTests.ts b/test/ActionsTests.ts index def0afc..bc44273 100644 --- a/test/ActionsTests.ts +++ b/test/ActionsTests.ts @@ -1,7 +1,7 @@ /// import { Actions } from '../src/Actions' import * as Chai from 'chai'; -import { Mocks, ContentTypes, Repository, ODataApi } from 'sn-client-js'; +import { Mocks, ContentTypes, ODataApi } from 'sn-client-js'; const expect = Chai.expect; describe('Actions', () => { @@ -105,7 +105,6 @@ describe('Actions', () => { expect(Actions.LoadContentActions(content, 'ListItem')).to.deep.equal(expectedAction) }); it('should create an action to receive a loaded contents actions', () => { - const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) const expectedAction = { type: 'LOAD_CONTENT_ACTIONS_SUCCESS', actions: ['aa', 'bb'] @@ -679,10 +678,20 @@ describe('Actions', () => { const expectedAction = { type: 'REQUEST_CONTENT_ACTIONS', content: content, - scenario: 'DMSListItem' + scenario: 'DMSListItem', + customItems: [] } expect(Actions.RequestContentActions(content, 'DMSListItem')).to.deep.equal(expectedAction) }) + it('should return the RequestContentActions action', () => { + const expectedAction = { + type: 'REQUEST_CONTENT_ACTIONS', + content: content, + scenario: 'DMSListItem', + customItems: [{ DisplayName: 'aaa', Name: 'bbb', Icon: 'ccc' }] + } + expect(Actions.RequestContentActions(content, 'DMSListItem', [{ DisplayName: 'aaa', Name: 'bbb', Icon: 'ccc' }])).to.deep.equal(expectedAction) + }) it('should return the RequestContentActionsSuccess action', () => { const expectedAction = { type: 'REQUEST_CONTENT_ACTIONS_SUCCESS', diff --git a/test/EpicsTests.ts b/test/EpicsTests.ts index 99021a4..0b4cf7c 100644 --- a/test/EpicsTests.ts +++ b/test/EpicsTests.ts @@ -1,10 +1,9 @@ import * as Chai from 'chai'; import configureMockStore from 'redux-mock-store'; import { createEpicMiddleware } from 'redux-observable'; -import { Mocks, ContentTypes, HttpProviders, Authentication, ODataApi, Content } from 'sn-client-js'; +import { Mocks, ContentTypes, Authentication } from 'sn-client-js'; import { Epics } from '../src/Epics' import { Actions } from '../src/Actions' -import { Store } from '../src/Store' const expect = Chai.expect; import 'rxjs'; @@ -58,7 +57,6 @@ describe('Epics', () => { epicMiddleware.replaceEpic(Epics.initSensenetStoreEpic); }); it('handles the error', () => { - const user = repo.CreateContent({ Name: 'alba', Id: 123 }, ContentTypes.User); store.dispatch({ type: 'INIT_SENSENET_STORE', path: '/workspaces', options: {} }); expect(store.getActions()).to.be.deep.equal([ { @@ -615,7 +613,6 @@ describe('Epics', () => { { type: 'USER_LOGIN_FAILURE', message: 'Failed to log in.' }]); }) it('handles the loggedin user', () => { - const user = repo.CreateContent({ Name: 'alba', Id: 123 }, ContentTypes.User); store.dispatch({ type: 'USER_LOGIN_REQUEST', username: 'user', password: 'password' }); (repo.Authentication as Mocks.MockAuthService).StateSubject.next(Authentication.LoginState.Authenticated); expect(store.getActions()).to.be.deep.eq( @@ -691,7 +688,6 @@ describe('Epics', () => { ]); }) it('handles an error', () => { - const user = repo.HandleLoadedContent({ Name: 'alba', Id: 65535, Path: '/Root' }, ContentTypes.User); repo.Authentication.StateSubject.next(Authentication.LoginState.Unauthenticated); store.dispatch({ type: 'CHECK_LOGIN_STATE_REQUEST' }); expect(store.getActions()).to.be.deep.eq( @@ -770,19 +766,4 @@ describe('Epics', () => { [{ type: 'USER_LOGIN_BUFFER', response: true }]); }) }) - - describe('uploadContentEpic Epic', () => { - before(() => { - initBefores(Epics.uploadFileEpic) - }); - - after(() => { - epicMiddleware.replaceEpic(Epics.uploadFileEpic); - }); - it('handles the success', () => { - store.dispatch({ type: 'UPLOAD_CONTENT_SUCCESS', response: true }); - expect(store.getActions()).to.be.deep.eq( - [{ type: 'UPLOAD_CONTENT_SUCCESS', response: true }]); - }) - }) }); \ No newline at end of file diff --git a/test/ReducersTests.ts b/test/ReducersTests.ts index efd510a..07e56a5 100644 --- a/test/ReducersTests.ts +++ b/test/ReducersTests.ts @@ -1,8 +1,7 @@ /// import { Reducers } from '../src/Reducers'; -import { Actions } from '../src/Actions'; import * as Chai from 'chai'; -import { Authentication, Content, ContentTypes, Mocks, Enums } from 'sn-client-js'; +import { Authentication, ContentTypes, Mocks, Enums } from 'sn-client-js'; const expect = Chai.expect; describe('Reducers', () => { describe('country reducer', () => { @@ -1261,4 +1260,34 @@ describe('Reducers', () => { expect(Reducers.getCurrentContent(state)).to.be.deep.equal({ DisplayName: 'my content' }) }) }) + describe('getChildren', () => { + const state = { + entities: { + 5145: { + Id: 5145, + DisplayName: 'Some Article', + Status: ['Active'] + }, + 5146: { + Id: 5146, + Displayname: 'Other Article', + Status: ['Completed'] + } + } + } + it('should return the children object', () => { + expect(Reducers.getChildren(state)).to.be.deep.equal({ + 5145: { + Id: 5145, + DisplayName: 'Some Article', + Status: ['Active'] + }, + 5146: { + Id: 5146, + Displayname: 'Other Article', + Status: ['Completed'] + } + }) + }) + }) }); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 1b4890e..ad4671d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "preserveConstEnums": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, + "noUnusedLocals": true, "skipLibCheck": true, "outDir": "./dist" }, From 6c20ae6c18cbf9a5454eb0a2235cb68e0fe21020 Mon Sep 17 00:00:00 2001 From: Aniko Litvanyi Date: Wed, 15 Nov 2017 15:32:47 +0100 Subject: [PATCH 7/9] [KFI]feat(Authentication): Add stuff for google oauth (#54) * [KFI]feat(Authentication): Add initial stuff for google oauth * [KFI]test(Actions): Add google auth login test * [KFI]chore: Update dependencies --- package.json | 6 ++++-- src/Actions.ts | 7 +++++++ src/Epics.ts | 32 ++++++++++++++++++++++---------- test/ActionsTests.ts | 8 ++++++++ 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 56a35a6..9e0d84c 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ }, "homepage": "https://sensenet.com", "dependencies": { + "@types/mocha": "^2.2.42", "normalizr": "^3.2.3", "nyc": "^11.1.0", "redux": "^3.7.2", @@ -65,10 +66,10 @@ }, "devDependencies": { "@types/chai": "^4.0.4", - "@types/mocha": "^2.2.42", "@types/nock": "^8.2.1", "@types/orchestrator": "^0.3.0", "@types/redux-mock-store": "^0.0.11", + "@types/redux-logger": "^3.0.5", "chai": "^4.1.1", "codecov.io": "^0.1.6", "commitizen": "^2.9.6", @@ -84,7 +85,8 @@ "redux-mock-store": "^1.2.3", "redux-observable": "^0.16.0", "semantic-release": "^8.0.0", - "sn-client-js": "^3.0.0-development.2", + "sn-client-js": "^3.0.0", + "sn-client-auth-google": "^1.0.0", "tslint": "^5.6.0", "typedoc": "^0.9.0", "typedoc-md-theme": "^1.0.1", diff --git a/src/Actions.ts b/src/Actions.ts index f411c86..08a303f 100644 --- a/src/Actions.ts +++ b/src/Actions.ts @@ -726,6 +726,13 @@ export module Actions { type: 'USER_LOGIN_FAILURE', message: (error.status === 403) ? 'The username or the password is not valid!' : error.message }) + /** + * Action creator for login a user to a sensenet portal with her google account. + * @returns {Object} Returns a redux action. + */ + export const UserLoginGoogle = () => ({ + type: 'USER_LOGIN_GOOGLE' + }) /** * Action creator for logout a user from a sensenet portal. * @returns {Object} Returns a redux action. diff --git a/src/Epics.ts b/src/Epics.ts index db0983e..af171ad 100644 --- a/src/Epics.ts +++ b/src/Epics.ts @@ -1,8 +1,8 @@ import { Actions } from './Actions'; import { Reducers } from './Reducers'; - import { combineEpics } from 'redux-observable'; import { Repository, ContentTypes, Collection, Authentication } from 'sn-client-js'; +import { GoogleOauthProvider } from 'sn-client-auth-google'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/catch' @@ -194,8 +194,8 @@ export module Epics { export const deleteBatchEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { return action$.ofType('DELETE_BATCH_REQUEST') .mergeMap(action => { - let contentItems = Object.keys(action.contentItems).map(id => { - return dependencies.repository.HandleLoadedContent(action.contentItems[id], action.contentItems.__contentType); + let contentItems = Object.keys(action.contentItems).map(id => { + return dependencies.repository.HandleLoadedContent(action.contentItems[id], action.contentItems.__contentType); }); return dependencies.repository.DeleteBatch(contentItems, action.permanently) .map((response) => { @@ -211,8 +211,8 @@ export module Epics { export const copyBatchEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { return action$.ofType('COPY_BATCH_REQUEST') .mergeMap(action => { - let contentItems = Object.keys(action.contentItems).map(id => { - return dependencies.repository.HandleLoadedContent(action.contentItems[id], action.contentItems.__contentType); + let contentItems = Object.keys(action.contentItems).map(id => { + return dependencies.repository.HandleLoadedContent(action.contentItems[id], action.contentItems.__contentType); }); return dependencies.repository.CopyBatch(contentItems, action.path) .map((response) => { @@ -228,8 +228,8 @@ export module Epics { export const moveBatchEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { return action$.ofType('MOVE_BATCH_REQUEST') .mergeMap(action => { - let contentItems = Object.keys(action.contentItems).map(id => { - return dependencies.repository.HandleLoadedContent(action.contentItems[id], action.contentItems.__contentType); + let contentItems = Object.keys(action.contentItems).map(id => { + return dependencies.repository.HandleLoadedContent(action.contentItems[id], action.contentItems.__contentType); }); return dependencies.repository.MoveBatch(contentItems, action.path) .map((response) => { @@ -361,9 +361,6 @@ export module Epics { return action$.ofType('USER_LOGIN_REQUEST') .mergeMap(action => { return dependencies.repository.Authentication.Login(action.userName, action.password) - // .combineLatest(dependencies.repository.GetCurrentUser().skipWhile(u => u.Name === 'Visitor')) - // .skipWhile(u => u instanceof ContentTypes.User) - // .first() .map(result => { return result ? Actions.UserLoginBuffer(result) @@ -373,6 +370,20 @@ export module Epics { .catch(error => Observable.of(Actions.UserLoginFailure(error))) }) } + /** + * Epic to login a user to a sensenet portal. It is related to three redux actions, returns ```LoginUser``` action and sends the response to the + * ```LoginUserSuccess``` action if the ajax request ended successfully or catches the error if the request failed and sends the error message to the ```LoginUserFailure``` action. + */ + export const userLoginGoogleEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { + return action$.ofType('USER_LOGIN_GOOGLE') + .mergeMap(action => { + return Observable.of(dependencies.repository.Authentication.GetOauthProvider(GoogleOauthProvider).Login()) + .map(result => { + return Actions.UserLoginBuffer(true) + }) + .catch(error => Observable.of(Actions.UserLoginFailure(error))) + }) + } export const userLoginBufferEpic = (action$, store, dependencies?: { repository: Repository.BaseRepository }) => { return action$.ofType('USER_LOGIN_BUFFER') .mergeMap(action => { @@ -448,6 +459,7 @@ export module Epics { forceundocheckoutContentEpic, restoreversionContentEpic, userLoginEpic, + userLoginGoogleEpic, userLogoutEpic, checkLoginStateEpic, getContentActions, diff --git a/test/ActionsTests.ts b/test/ActionsTests.ts index bc44273..18881ac 100644 --- a/test/ActionsTests.ts +++ b/test/ActionsTests.ts @@ -595,6 +595,14 @@ describe('Actions', () => { expect(Actions.UserLoginBuffer(true)).to.deep.equal(expectedAction) }); }); + describe('UserLoginGoogle', () => { + it('should create an action to a user login with google', () => { + const expectedAction = { + type: 'USER_LOGIN_GOOGLE' + } + expect(Actions.UserLoginGoogle()).to.deep.equal(expectedAction) + }); + }); describe('UserLogout', () => { it('should create an action to a user logout request', () => { const expectedAction = { From 3b5d9efb20cc9d6907f7500104c93041c4616023 Mon Sep 17 00:00:00 2001 From: Aniko Litvanyi Date: Tue, 5 Dec 2017 11:46:44 +0100 Subject: [PATCH 8/9] [KFI]fix(Actions): Change Actions method to GetActions in action loader epics --- package.json | 2 +- src/Epics.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f8d50c9..768c9ec 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "redux-observable": "^0.16.0", "semantic-release": "^8.0.0", "sn-client-auth-google": "^1.0.0", - "sn-client-js": "^3.0.0", + "sn-client-js": "^3.0.1", "tslint": "^5.6.0", "typedoc": "^0.9.0", "typedoc-md-theme": "^1.0.1", diff --git a/src/Epics.ts b/src/Epics.ts index 3b09591..c7ff39b 100644 --- a/src/Epics.ts +++ b/src/Epics.ts @@ -112,7 +112,7 @@ export module Epics { return action$.ofType('LOAD_CONTENT_ACTIONS') .mergeMap(action => { let c = dependencies.repository.HandleLoadedContent(action.content, ContentTypes.GenericContent); - return c.Actions(action.scenario) + return c.GetActions(action.scenario) .map(result => Actions.ReceiveContentActions(result)) .catch(error => Observable.of(Actions.ReceiveContentActionsFailure(error))) }) @@ -433,7 +433,7 @@ export module Epics { return action$.ofType('REQUEST_CONTENT_ACTIONS') .mergeMap(action => { let c = dependencies.repository.HandleLoadedContent(action.content, ContentTypes.GenericContent); - return c.Actions(action.scenario) + return c.GetActions(action.scenario) .map(result => Actions.RequestContentActionsSuccess([...result, ...action.customItems], action.content.Id)) .catch(error => Observable.of(Actions.RequestContentActionsFailure(error))) }) From 1944589e35f1be6892e030adc4884016be9853c5 Mon Sep 17 00:00:00 2001 From: Aniko Litvanyi Date: Tue, 5 Dec 2017 11:47:17 +0100 Subject: [PATCH 9/9] [KFI]chore: Update version number to 3.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 768c9ec..2bc3f64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sn-redux", - "version": "3.4.1", + "version": "3.4.2", "description": "A set of redux actions, reducers and redux-ovbservable epics for Sense/Net ECM", "main": "dist/src/sn-redux.js", "scripts": {