From 215fcb6fb6e58632fde0af5c0b04025681c13c17 Mon Sep 17 00:00:00 2001 From: Aniko Litvanyi Date: Fri, 24 Nov 2017 11:23:48 +0100 Subject: [PATCH] Merge release/3.4.0 (#55) * 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 * [KFI]chore: Update version number * 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 * [KFI]test(Epics): Fix Epic tests (#51) * 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 * 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 * [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 * [KFI]chore: Update version number * [KFI]style: Add missing space and a equal sign * [KFI]style: Add missing semicolons * [KFI]feat(Upload): Add scenario as param of the upload request action * [KFI]test(Upload): Fix tests with the new scenario param * [KFI]feat(Upload): Add ODataOptions param with scenario to the upload epic * [KFI]fix(Epics): Fix deleteContent epic at its success branch with correct handling of the id of the * [KFI]chore: Update sn-client-js and sn-client-auth-google --- gulpfile.js | 70 +++---- package.json | 13 +- src/Actions.ts | 147 ++++++++++----- src/Epics.ts | 83 +++++++-- src/Reducers.ts | 104 ++++++++++- src/Store.ts | 1 - test/ActionsTests.ts | 245 +++++++++++++++++++------ test/EpicsTests.ts | 225 ++++++++++++++++------- test/ReducersTests.ts | 416 +++++++++++++++++++++++++++++++++++++++--- tsconfig.json | 1 + 10 files changed, 1047 insertions(+), 258 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 252eb56..a3900cc 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,7 +2,7 @@ const gulp = require('gulp'); const typedoc = require("gulp-typedoc"); var rename = require("gulp-rename"); const del = require('del'); - + const __coverageThreshold = 60; gulp.task("typedoc", function () { @@ -14,21 +14,21 @@ gulp.task("typedoc", function () { "!./src/SN.d.ts" ]) .pipe(typedoc({ - module: "commonjs", - target: "es2015", - includeDeclarations: false, - out: "./documentation/html", - name: "sn-client-js", - theme: "default", - ignoreCompilerErrors: true, - version: true, - mode: "modules", - readme: "sn-client-js/README.md", - excludeExternals: true, - excludePrivate: true, - includes: "docs", - experimentalDecorators: true - })); + module: "commonjs", + target: "es2015", + includeDeclarations: false, + out: "./documentation/html", + name: "sn-client-js", + theme: "default", + ignoreCompilerErrors: true, + version: true, + mode: "modules", + readme: "sn-client-js/README.md", + excludeExternals: true, + excludePrivate: true, + includes: "docs", + experimentalDecorators: true + })); }); @@ -41,26 +41,26 @@ gulp.task("typedoc:md:generate", function () { "!./src/SN.d.ts" ]) .pipe(typedoc({ - module: "commonjs", - target: "es2015", - includeDeclarations: false, - out: "./documentation/markdown", - name: "sn-client-js", - theme: "node_modules/typedoc-md-theme/bin", - ignoreCompilerErrors: true, - version: true, - mode: "modules", - readme: "sn-client-js/README.md", - excludeExternals: true, - excludePrivate: true, - includes: "docs" - })); + module: "commonjs", + target: "es2015", + includeDeclarations: false, + out: "./documentation/markdown", + name: "sn-client-js", + theme: "node_modules/typedoc-md-theme/bin", + ignoreCompilerErrors: true, + version: true, + mode: "modules", + readme: "sn-client-js/README.md", + excludeExternals: true, + excludePrivate: true, + includes: "docs" + })); }); -gulp.task('typedoc:md', ['typedoc:md:generate'], ()=>{ +gulp.task('typedoc:md', ['typedoc:md:generate'], () => { gulp.src('./documentation/markdown/**/*.*') - .pipe(rename((path)=>{ - path.extname= path.extname == '.html' ? '.md' : path.extname - })) - .pipe(gulp.dest('documentation/markdown_renamed')) + .pipe(rename((path) => { + path.extname = path.extname === '.html' ? '.md' : path.extname; + })) + .pipe(gulp.dest('documentation/markdown_renamed')); }); \ No newline at end of file diff --git a/package.json b/package.json index b6fc847..5f95746 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sn-redux", - "version": "3.3.1", + "version": "3.4.0", "description": "A set of redux actions, reducers and redux-ovbservable epics for Sense/Net ECM", "main": "dist/src/sn-redux.js", "scripts": { @@ -54,21 +54,21 @@ }, "homepage": "https://sensenet.com", "dependencies": { - "@reactivex/rxjs": "^5.4.3", + "@types/mocha": "^2.2.42", "normalizr": "^3.2.3", "nyc": "^11.1.0", "redux": "^3.7.2", "redux-logger": "^3.0.6", "redux-observable": "^0.17.0", "rimraf": "^2.6.1", - "rxjs": "^5.4.3", + "rxjs": "^5.5.2", "sensenet-kfi-cz-conventional-changelog": "^1.0.0" }, "devDependencies": { "@types/chai": "^4.0.4", - "@types/mocha": "^2.2.42", "@types/nock": "^8.2.1", "@types/orchestrator": "^0.3.0", + "@types/redux-logger": "^3.0.5", "@types/redux-mock-store": "^0.0.12", "chai": "^4.1.1", "codecov.io": "^0.1.6", @@ -85,12 +85,13 @@ "redux-mock-store": "^1.2.3", "redux-observable": "^0.16.0", "semantic-release": "^8.0.0", - "sn-client-js": "^2.5.0", + "sn-client-auth-google": "^1.0.0", + "sn-client-js": "^3.0.0", "tslint": "^5.6.0", "typedoc": "^0.9.0", "typedoc-md-theme": "^1.0.1", "typedoc-plugin-external-module-name": "^1.0.9", - "typescript": "^2.4.2" + "typescript": "^2.6.1" }, "czConfig": { "path": "node_modules/sensenet-kfi-cz-conventional-changelog" diff --git a/src/Actions.ts b/src/Actions.ts index 3c86952..131576f 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, ContentTypes } from 'sn-client-js'; +import { Content, IContent, ODataApi, 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. @@ -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,12 +391,70 @@ 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. * @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 +481,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 +509,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 +536,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 +564,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 +592,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 +619,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 +647,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 @@ -656,19 +712,26 @@ export module Actions { * @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: Content) => ({ + 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 }) + /** + * 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. @@ -705,20 +768,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. @@ -732,10 +795,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. @@ -768,14 +832,15 @@ export module Actions { * @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) => ({ + export const UploadRequest = (content: Content, file, contentType?, overwrite?: boolean, body?, propertyName?: string, scenario?: 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' + propertyName: propertyName ? propertyName : 'Binary', + scenario: scenario || 'ListItems' }) /** * Action creator for the step when a content was uploaded successfully. diff --git a/src/Epics.ts b/src/Epics.ts index 63b92a1..1599d2a 100644 --- a/src/Epics.ts +++ b/src/Epics.ts @@ -1,10 +1,11 @@ import { Actions } from './Actions'; import { Reducers } from './Reducers'; - -import { ActionsObservable, combineEpics } from 'redux-observable'; -import { Observable } from '@reactivex/rxjs'; -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 { GoogleOauthProvider } from 'sn-client-auth-google'; +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. @@ -177,11 +178,11 @@ export module Epics { export const deleteContentEpic = (action$, store) => { return action$.ofType('DELETE_CONTENT_REQUEST') .mergeMap(action => { - return action.content.Delete(action.id, action.permanently) + return action.content.Delete(action.content, action.permanently) .map((response) => { const state = store.getState(); - const ids = Reducers.getIds(state.collection); - return Actions.DeleteSuccess(ids.indexOf(action.id), action.id); + const ids = Reducers.getIds(state.sensenet.children); + return Actions.DeleteSuccess(ids.indexOf(action.content.Id), action.content.Id); }) .catch(error => Observable.of(Actions.DeleteFailure(error))) }) @@ -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. @@ -338,6 +373,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 => { @@ -365,7 +414,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))) }) } @@ -381,7 +430,10 @@ export module Epics { ContentType: action.contentType, OverWrite: action.overwrite, Body: action.body, - PropertyName: action.propertyName + PropertyName: action.propertyName, + OdataOptions: { + Scenario: action.scenario + } }) .map((response) => { return Actions.UploadSuccess(response) @@ -402,6 +454,8 @@ export module Epics { updateContentEpic, deleteContentEpic, deleteBatchEpic, + copyBatchEpic, + moveBatchEpic, checkoutContentEpic, checkinContentEpic, publishContentEpic, @@ -411,6 +465,7 @@ export module Epics { forceundocheckoutContentEpic, restoreversionContentEpic, userLoginEpic, + userLoginGoogleEpic, userLogoutEpic, checkLoginStateEpic, getContentActions, diff --git a/src/Reducers.ts b/src/Reducers.ts index 18d024e..b0843e4 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) => { @@ -666,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 ba72943..f00b7f3 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, ODataApi } 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,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 = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) const expectedAction = { type: 'LOAD_CONTENT_ACTIONS_SUCCESS', actions: ['aa', 'bb'] @@ -122,7 +121,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 +130,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 +143,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 +152,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 +164,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 +177,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 +198,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 +210,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', @@ -248,27 +247,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,8 +286,89 @@ 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 = 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 +377,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 +389,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 +407,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 +419,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 +428,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 +440,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 +449,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 +461,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 +479,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 +491,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 +500,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 +512,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 +521,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 +533,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 +543,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,7 +564,7 @@ describe('Actions', () => { expect(Actions.UserLogin('alba', 'alba')).to.deep.equal(expectedAction) }); it('should create an action to a user login success', () => { - const user = Content.Create({ Name: 'alba' }, ContentTypes.User, repo) + const user = repo.CreateContent({ Name: 'alba' }, ContentTypes.User) const expectedAction = { type: 'USER_LOGIN_SUCCESS', response: user @@ -506,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 = { @@ -537,7 +634,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 @@ -555,21 +652,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', () => { @@ -581,16 +680,26 @@ describe('Actions', () => { }) }) describe('RequestContentActions', () => { + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) it('should return the RequestContentActions action', () => { 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', @@ -612,7 +721,7 @@ describe('Actions', () => { }); }) describe('UploadContentActions', () => { - const content = Content.Create({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task, repo) + const content = repo.CreateContent({ DisplayName: 'My content', Id: 123 }, ContentTypes.Task) const file = { lastModified: 1499931166346, name: 'README.md', @@ -627,7 +736,8 @@ describe('Actions', () => { overwrite: true, propertyName: 'Binary', contentType: ContentTypes.File, - body: null + body: null, + scenario: 'ListItems' } expect(Actions.UploadRequest(content, file)).to.deep.equal(expectedAction) }) @@ -639,7 +749,8 @@ describe('Actions', () => { file, overwrite: true, propertyName: 'Binary', - body: null + body: null, + scenario: 'ListItems' } expect(Actions.UploadRequest(content, file, ContentTypes.Folder)).to.deep.equal(expectedAction) }) @@ -651,7 +762,8 @@ describe('Actions', () => { file, overwrite: false, propertyName: 'Binary', - body: null + body: null, + scenario: 'ListItems' } expect(Actions.UploadRequest(content, file, undefined, false)).to.deep.equal(expectedAction) }) @@ -663,7 +775,8 @@ describe('Actions', () => { file, overwrite: true, propertyName: 'Avatar', - body: null + body: null, + scenario: 'ListItems' } expect(Actions.UploadRequest(content, file, undefined, undefined, undefined, 'Avatar')).to.deep.equal(expectedAction) }) @@ -675,10 +788,24 @@ describe('Actions', () => { file, overwrite: true, propertyName: 'Binary', - body: { vmi: 'aaa' } + body: { vmi: 'aaa' }, + scenario: 'ListItems' } expect(Actions.UploadRequest(content, file, undefined, undefined, { vmi: 'aaa' })).to.deep.equal(expectedAction) }) + it('should return the upload content action set content, file and scenario', () => { + const expectedAction = { + type: 'UPLOAD_CONTENT_REQUEST', + content, + contentType: ContentTypes.File, + file, + overwrite: true, + propertyName: 'Binary', + body: null, + scenario: 'DMSListItems' + } + expect(Actions.UploadRequest(content, file, undefined, undefined, undefined, null, 'DMSListItems')).to.deep.equal(expectedAction) + }) it('should create an action to upload content success', () => { const expectedAction = { type: 'UPLOAD_CONTENT_SUCCESS', diff --git a/test/EpicsTests.ts b/test/EpicsTests.ts index cceb628..0b4cf7c 100644 --- a/test/EpicsTests.ts +++ b/test/EpicsTests.ts @@ -1,22 +1,22 @@ 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'; 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' }, ContentTypes.Task) + content = repo.HandleLoadedContent({ DisplayName: 'My Content', Id: 123, Path: '/workspaces', Name: 'MyContent' }, ContentTypes.Task) } + describe('Epics', () => { beforeEach(() => { @@ -25,7 +25,7 @@ describe('Epics', () => { describe('fetchContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.fetchContentEpic) }); after(() => { @@ -50,23 +50,45 @@ describe('Epics', () => { }); describe('initSensenetStoreEpic Epic', () => { before(() => { - initBefores() + initBefores(Epics.initSensenetStoreEpic) }); 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.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(() => { @@ -78,14 +100,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(() => { @@ -115,7 +150,7 @@ describe('Epics', () => { }); describe('reloadContentFields Epic', () => { before(() => { - initBefores() + initBefores(Epics.reloadContentFieldsEpic) }); after(() => { @@ -146,7 +181,7 @@ describe('Epics', () => { }); describe('createContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.createContentEpic) }); after(() => { @@ -158,6 +193,10 @@ describe('Epics', () => { [{ type: 'CREATE_CONTENT_REQUEST', content: content + }, + { + type: 'CREATE_CONTENT_SUCCESS', + response: { entities: { entities: { '123': content } }, result: 123 } }]); }) it('handles the error', () => { @@ -167,12 +206,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(() => { @@ -184,6 +227,10 @@ describe('Epics', () => { [{ type: 'UPDATE_CONTENT_REQUEST', content + }, + { + type: 'UPDATE_CONTENT_SUCCESS', + response: content }]); }) it('handles the error', () => { @@ -193,12 +240,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(() => { @@ -226,18 +277,38 @@ describe('Epics', () => { }); describe('deleteBatch Epic', () => { before(() => { - initBefores() + initBefores(Epics.deleteBatchEpic) }); after(() => { epicMiddleware.replaceEpic(Epics.deleteBatchEpic); }); it('handles the error', () => { - store.dispatch({ type: 'DELETE_BATCH_REQUEST', ids: ['1', '2'], permanently: false }); + 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', - ids: ['1', '2'], + contentItems: { + 1: { + DisplaName: 'aaa', + Id: 1 + }, + 2: { + DisplaName: 'bbb', + Id: 2 + } + }, permanently: false }]); }) @@ -246,15 +317,54 @@ describe('Epics', () => { expect(store.getActions()).to.be.deep.eq( [{ type: 'DELETE_BATCH_REQUEST', - ids: ['1', '2'], + 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() + initBefores(Epics.checkoutContentEpic) }); after(() => { epicMiddleware.replaceEpic(Epics.checkoutContentEpic); @@ -280,7 +390,7 @@ describe('Epics', () => { }); describe('checkinContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.checkinContentEpic) }); after(() => { @@ -312,7 +422,7 @@ describe('Epics', () => { }); describe('publishContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.publishContentEpic) }); after(() => { @@ -341,7 +451,7 @@ describe('Epics', () => { }); describe('approveContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.approveContentEpic) }); after(() => { @@ -369,7 +479,7 @@ describe('Epics', () => { }); describe('rejectContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.rejectContentEpic) }); afterEach(() => { @@ -400,7 +510,7 @@ describe('Epics', () => { }); describe('undocheckoutContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.undocheckoutContentEpic) }); after(() => { @@ -428,7 +538,7 @@ describe('Epics', () => { }); describe('forceundocheckoutContent Epic', () => { before(() => { - initBefores() + initBefores(Epics.forceundocheckoutContentEpic) }); after(() => { @@ -456,7 +566,7 @@ describe('Epics', () => { }); describe('restoreVersion Epic', () => { before(() => { - initBefores() + initBefores(Epics.restoreversionContentEpic) }); after(() => { epicMiddleware.replaceEpic(Epics.restoreversionContentEpic); @@ -485,7 +595,7 @@ describe('Epics', () => { }); describe('login Epic', () => { before(() => { - initBefores() + initBefores(Epics.userLoginEpic) }); afterEach(() => { @@ -499,10 +609,10 @@ 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 = Content.Create({ Name: 'alba', Id: 123 }, ContentTypes.User, repo) 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( @@ -511,6 +621,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', @@ -522,7 +633,7 @@ describe('Epics', () => { }); describe('logout Epic', () => { before(() => { - initBefores() + initBefores(Epics.userLogoutEpic) }); after(() => { @@ -537,7 +648,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); @@ -549,19 +661,20 @@ describe('Epics', () => { username: 'alba', password: 'alba' }, + { type: 'USER_LOGOUT_SUCCESS' }, { type: 'USER_LOGOUT_FAILURE', error: 'error' }]); }) }); describe('checkLoginState Epic', () => { - before(() => { - initBefores() + beforeEach(() => { + initBefores(Epics.checkLoginStateEpic) }); 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); store.dispatch({ type: 'CHECK_LOGIN_STATE_REQUEST' }); @@ -570,30 +683,26 @@ 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', () => { - (repo.Authentication as Mocks.MockAuthService).StateSubject.next(Authentication.LoginState.Unauthenticated); + 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 - }, - { 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(() => { epicMiddleware.replaceEpic(Epics.getContentActions); }); - const content = Content.Create({ Name: 'alba', Id: '2' }, ContentTypes.Task, repo) it('handles the success', () => { store.dispatch({ type: 'REQUEST_CONTENT_ACTIONS', content, scenario: 'DMSDemoScenario' }); expect(store.getActions()).to.be.deep.eq( @@ -616,13 +725,12 @@ describe('Epics', () => { }); describe('loadContentActionsEpic Epic', () => { before(() => { - initBefores() + initBefores(Epics.loadContentActionsEpic) }); after(() => { epicMiddleware.replaceEpic(Epics.loadContentActionsEpic); }); - const content = Content.Create({ Name: 'alba', Id: '2' }, ContentTypes.Task, repo) it('handles the success', () => { store.dispatch({ type: 'LOAD_CONTENT_ACTIONS', content, scenario: 'DMSDemoScenario' }); expect(store.getActions()).to.be.deep.eq( @@ -646,7 +754,7 @@ describe('Epics', () => { }); describe('userLoginBufferEpic Epic', () => { before(() => { - initBefores() + initBefores(Epics.userLoginBufferEpic) }); after(() => { @@ -658,19 +766,4 @@ describe('Epics', () => { [{ 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 9f1ac5e..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 } from 'sn-client-js'; +import { Authentication, ContentTypes, Mocks, Enums } from 'sn-client-js'; const expect = Chai.expect; describe('Reducers', () => { describe('country reducer', () => { @@ -183,6 +182,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 +335,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', () => { @@ -689,10 +769,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 @@ -705,10 +785,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 @@ -725,10 +805,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 @@ -737,10 +817,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 @@ -749,42 +829,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 +1179,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', () => { @@ -928,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" },