From f19158d3a3f94e576e662512b8d92af73761d3c5 Mon Sep 17 00:00:00 2001 From: Jeremy Press Date: Thu, 3 Jan 2019 11:26:59 -0800 Subject: [PATCH] feat(openwith): Box Edit SFC support --- src/api/OpenWith.js | 32 ++- src/api/__tests__/MockOpenWithData.json | 187 ++++++++++++++---- src/api/__tests__/OpenWith-test.js | 38 +++- .../ContentOpenWith/ContentOpenWith.js | 39 +++- .../__tests__/ContentOpenWith-test.js | 19 +- src/constants.js | 2 + 6 files changed, 259 insertions(+), 58 deletions(-) diff --git a/src/api/OpenWith.js b/src/api/OpenWith.js index 29d0a71277..ecd43f95fc 100644 --- a/src/api/OpenWith.js +++ b/src/api/OpenWith.js @@ -5,7 +5,13 @@ */ import Base from './Base'; -import { HEADER_ACCEPT_LANGUAGE, DEFAULT_LOCALE, ERROR_CODE_FETCH_INTEGRATIONS } from '../constants'; +import { + HEADER_ACCEPT_LANGUAGE, + DEFAULT_LOCALE, + ERROR_CODE_FETCH_INTEGRATIONS, + BOX_EDIT_INTEGRATION_ID, + BOX_EDIT_SFC_INTEGRATION_ID, +} from '../constants'; class OpenWith extends Base { /** @@ -49,12 +55,34 @@ class OpenWith extends Base { params, successCallback: openWithIntegrations => { const formattedOpenWithData = this.formatOpenWithData(openWithIntegrations); - successCallback(formattedOpenWithData); + const consolidatedOpenWithIntegrations = this.consolidateBoxEditIntegrations(formattedOpenWithData); + successCallback(consolidatedOpenWithIntegrations); }, errorCallback, }); } + /** + * Removes the Box Edit SFC integration if the higher scoped Box Edit integration is present. + * Box Edit and SFC Box Edit are considered separate integrations by the API. We only want to show one, + * even if both are enabled and returned from the API. + * + * @param {Array} integrations - List of integrations + * @return {Array} Integrations with only one Box Edit integration + */ + consolidateBoxEditIntegrations(integrations: Array): Array { + let consolidatedIntegrations = [...integrations]; + const boxEditIntegration = integrations.some(item => item.appIntegrationId === BOX_EDIT_INTEGRATION_ID); + + if (boxEditIntegration) { + consolidatedIntegrations = integrations.filter( + item => item.appIntegrationId !== BOX_EDIT_SFC_INTEGRATION_ID, + ); + } + + return consolidatedIntegrations; + } + /** * Formats Open With data conveniently for the client * diff --git a/src/api/__tests__/MockOpenWithData.json b/src/api/__tests__/MockOpenWithData.json index ab506e3755..b42fa91bf2 100644 --- a/src/api/__tests__/MockOpenWithData.json +++ b/src/api/__tests__/MockOpenWithData.json @@ -1,47 +1,160 @@ { - "is_disabled": false, - "icon": "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAzMiAzMiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgcm9sZT0iaW1nIiA+ICAgICAgIDxwYXRoIGQ9Ik0yNSAyN0g3VjVoMTNsNSA1djE3eiIgZmlsbD0iI0ZGRkZGRiIvPiAgICA8cGF0aCBkPSJNMjAgNEg3Yy0uNiAwLTEgLjQtMSAxdjIyYzAgLjYuNCAxIDEgMWgxOGMuNiAwIDEtLjQgMS0xVjkuOUwyMCA0em01IDIzSDdWNWgxM3Y0YzAgLjYuNCAxIDEgMWg0djE3eiIgZmlsbD0iIzQwODNGNyIvPiAgICA8cGF0aCBkPSJNMTAgMTVoMTF2MUgxMHYtMXptMCAyaDExdjFIMTB2LTF6bTAgMmgxMXYxSDEwdi0xem0wIDJoOHYxaC04di0xeiIgZmlsbD0iIzQwODNGNyIvPjwvc3ZnPg==", - "disabled_reasons": [], - "default_app_integration": { - "id": "3", - "type": "app_integration" + "default": { + "is_disabled": false, + "icon": "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAzMiAzMiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgcm9sZT0iaW1nIiA+ICAgICAgIDxwYXRoIGQ9Ik0yNSAyN0g3VjVoMTNsNSA1djE3eiIgZmlsbD0iI0ZGRkZGRiIvPiAgICA8cGF0aCBkPSJNMjAgNEg3Yy0uNiAwLTEgLjQtMSAxdjIyYzAgLjYuNCAxIDEgMWgxOGMuNiAwIDEtLjQgMS0xVjkuOUwyMCA0em01IDIzSDdWNWgxM3Y0YzAgLjYuNCAxIDEgMWg0djE3eiIgZmlsbD0iIzQwODNGNyIvPiAgICA8cGF0aCBkPSJNMTAgMTVoMTF2MUgxMHYtMXptMCAyaDExdjFIMTB2LTF6bTAgMmgxMXYxSDEwdi0xem0wIDJoOHYxaC04di0xeiIgZmlsbD0iIzQwODNGNyIvPjwvc3ZnPg==", + "disabled_reasons": [], + "default_app_integration": { + "id": "3", + "type": "app_integration" + }, + "items": [ + { + "display_name": "Adobe Sign", + "display_description": "Send for Signature", + "display_order": 1, + "is_disabled": false, + "should_show_consent_popup": false, + "disabled_reasons": [], + "app_integration": { + "id": "3282", + "type": "app_integration" + } + }, + { + "display_name": "Google Docs", + "display_description": "Open with Google Docs", + "display_order": 3, + "is_disabled": true, + "disabled_reasons": ["manually disabled"], + "should_show_consent_popup": false, + "app_integration": { + "id": "10897", + "type": "app_integration" + } + }, + { + "display_name": "Google Slides", + "display_description": "Open with Google Slides", + "display_order": 2, + "is_disabled": false, + "disabled_reasons": [], + "should_show_consent_popup": false, + "app_integration": { + "id": "10897", + "type": "app_integration" + } + } + ] }, - "items": [ - { - "display_name": "Adobe Sign", - "display_description": "Send for Signature", - "display_order": 1, - "is_disabled": false, - "should_show_consent_popup": false, - "disabled_reasons": [], - "app_integration": { - "id": "3282", + "defaultAsIntegrations": [ + [ + { + "displayName": "Adobe Sign", + "displayDescription": "Send for Signature", + "displayOrder": 1, + "isDefault": true, + "isDisabled": false, + "requiresConsent": false, + "DisabledReasons": [], + "appIntegrationId": "3282", + "type": "app_integration" + }, + { + "displayName": "Google Docs", + "displayDescription": "Open with Google Docs", + "displayOrder": 3, + "isDisabled": true, + "DisabledReasons": ["manually disabled"], + "requiresConsent": false, + "appIntegrationId": "10897", + "type": "app_integration" + }, + { + "displayName": "Google Slides", + "displayDescription": "Open with Google Slides", + "displayOrder": 2, + "isDisabled": false, + "DisabledReasons": [], + "requiresConsent": false, + "appIntegrationId": "10897", "type": "app_integration" } + ] + ], + "boxEdit": [ + { + "displayName": "Open", + "displayDescription": "Open this file on your desktop", + "displayOrder": 2, + "isDisabled": false, + "requiresConsent": false, + "DisabledReasons": [], + "appIntegrationId": "1338", + "type": "app_integration" }, { - "display_name": "Google Docs", - "display_description": "Open with Google Docs", - "display_order": 3, - "is_disabled": true, - "disabled_reasons": ["manually disabled"], - "should_show_consent_popup": false, - "app_integration": { - "id": "10897", - "type": "app_integration" - } + "displayName": "Google Docs", + "displayDescription": "Open with Google Docs", + "displayOrder": 3, + "isDisabled": true, + "DisabledReasons": ["manually disabled"], + "requiresConsent": false, + "appIntegrationId": "10897", + "type": "app_integration" + } + ], + "boxEditSFC": [ + { + "displayName": "Open", + "displayDescription": "Open this file on your desktop", + "displayOrder": 1, + "isDisabled": false, + "requiresConsent": false, + "DisabledReasons": [], + "appIntegrationId": "13418", + "type": "app_integration" }, { - "display_name": "Google Slides", - "display_description": "Open with Google Slides", - "display_order": 2, - "is_disabled": false, - "disabled_reasons": [], - "should_show_consent_popup": false, - "app_integration": { - "id": "10897", - "type": "app_integration" - } + "displayName": "Google Docs", + "displayDescription": "Open with Google Docs", + "displayOrder": 3, + "isDisabled": true, + "DisabledReasons": ["manually disabled"], + "requiresConsent": false, + "appIntegrationId": "10897", + "type": "app_integration" + } + ], + "boxEditAndSFC": [ + { + "displayName": "Open", + "displayDescription": "Open this file on your desktop", + "displayOrder": 1, + "isDisabled": false, + "requiresConsent": false, + "DisabledReasons": [], + "appIntegrationId": "13418", + "type": "app_integration" + }, + { + "displayName": "Open", + "displayDescription": "Open this file on your desktop", + "displayOrder": 2, + "isDisabled": false, + "requiresConsent": false, + "DisabledReasons": [], + "appIntegrationId": "1338", + "type": "app_integration" + }, + { + "displayName": "Google Docs", + "displayDescription": "Open with Google Docs", + "displayOrder": 3, + "isDisabled": true, + "DisabledReasons": ["manually disabled"], + "requiresConsent": false, + "appIntegrationId": "10897", + "type": "app_integration" } ] -} \ No newline at end of file +} diff --git a/src/api/__tests__/OpenWith-test.js b/src/api/__tests__/OpenWith-test.js index e391cf5f36..f763c7f32b 100644 --- a/src/api/__tests__/OpenWith-test.js +++ b/src/api/__tests__/OpenWith-test.js @@ -10,22 +10,50 @@ describe('api/ContentOpenWith', () => { describe('getOpenWithIntegrations()', () => { test('should format data on success', () => { + const data = mockOpenWithData.default; const successFn = jest.fn(); const errorFn = jest.fn(); openWith.get = ({ successCallback }) => { - successCallback(mockOpenWithData); + successCallback(data); }; openWith.formatOpenWithData = jest.fn(); + openWith.consolidateBoxEditIntegrations = jest.fn(); openWith.getOpenWithIntegrations('123', successFn, errorFn); - expect(openWith.formatOpenWithData).toBeCalledWith(mockOpenWithData); + expect(openWith.formatOpenWithData).toBeCalledWith(data); + expect(openWith.consolidateBoxEditIntegrations).toBeCalled(); + }); + }); + + describe('consolidateBoxEditIntegrations()', () => { + test('should do nothing if no Box Edit integrations are present', () => { + const data = mockOpenWithData.default.items; + const result = openWith.consolidateBoxEditIntegrations(data); + expect(result).toEqual(data); + }); + + test('should do nothing if only Box Edit is present', () => { + const data = mockOpenWithData.boxEdit; + const result = openWith.consolidateBoxEditIntegrations(data); + expect(result).toEqual(data); + }); + test('should do nothing if only Box Edit SFC is present', () => { + const data = mockOpenWithData.boxEditSFC; + const result = openWith.consolidateBoxEditIntegrations(data); + expect(result).toEqual(data); + }); + test('should only return Box Edit if both Box Edit AND Box Edit SFC are present', () => { + const data = mockOpenWithData.boxEditAndSFC; + const result = openWith.consolidateBoxEditIntegrations(data); + expect(result).toEqual(mockOpenWithData.boxEdit); }); }); describe('formatOpenWithData()', () => { + const data = mockOpenWithData.default; test('should add a flattened and complete Integration', () => { - const formatedOpenWithIntegrations = openWith.formatOpenWithData(mockOpenWithData); + const formatedOpenWithIntegrations = openWith.formatOpenWithData(data); formatedOpenWithIntegrations.forEach(integration => { expect(typeof integration.appIntegrationId).toBe('string'); expect(typeof integration.displayDescription).toBe('string'); @@ -39,14 +67,14 @@ describe('api/ContentOpenWith', () => { }); test('should add isDefault to all items', () => { - const formatedOpenWithIntegrations = openWith.formatOpenWithData(mockOpenWithData); + const formatedOpenWithIntegrations = openWith.formatOpenWithData(data); formatedOpenWithIntegrations.forEach(integration => { expect(typeof integration.isDefault).toBe('boolean'); }); }); test('should return items sorted by displayOrder', () => { - const formatedOpenWithIntegrations = openWith.formatOpenWithData(mockOpenWithData); + const formatedOpenWithIntegrations = openWith.formatOpenWithData(data); formatedOpenWithIntegrations.forEach((integration, idx) => { // displayOrder is 1 indexed const expectedOrder = idx + 1; diff --git a/src/components/ContentOpenWith/ContentOpenWith.js b/src/components/ContentOpenWith/ContentOpenWith.js index abd6651f42..5783edc973 100644 --- a/src/components/ContentOpenWith/ContentOpenWith.js +++ b/src/components/ContentOpenWith/ContentOpenWith.js @@ -19,15 +19,22 @@ import OpenWithButton from './OpenWithButton'; import ExecuteForm from './ExecuteForm'; import '../base.scss'; import './ContentOpenWith.scss'; - -import { CLIENT_NAME_OPEN_WITH, DEFAULT_HOSTNAME_API, HTTP_GET, HTTP_POST } from '../../constants'; +import { + BOX_EDIT_INTEGRATION_ID, + BOX_EDIT_SFC_INTEGRATION_ID, + CLIENT_NAME_OPEN_WITH, + DEFAULT_HOSTNAME_API, + HTTP_GET, + HTTP_POST, + TYPE_FILE, + TYPE_FOLDER, +} from '../../constants'; const WINDOW_OPEN_BLOCKED_ERROR = 'Unable to open integration in new window'; const UNSUPPORTED_INVOCATION_METHOD_TYPE = 'Integration invocation using this HTTP method type is not supported'; const BLACKLISTED_ERROR_MESSAGE_KEY = 'boxToolsBlacklistedError'; const UNINSTALLED_ERROR_MESSAGE_KEY = 'boxToolsUninstalledErrorMessage'; const GENERIC_EXECUTE_MESSAGE_KEY = 'executeIntegrationOpenWithErrorHeader'; -const BOX_EDIT_INTEGRATION_ID = '1338'; const AUTH_CODE = 'auth_code'; type ExternalProps = { @@ -188,7 +195,17 @@ class ContentOpenWith extends PureComponent { * @return {boolean} */ isBoxEditIntegration(integrationId: ?string): boolean { - return integrationId === BOX_EDIT_INTEGRATION_ID; + return integrationId === BOX_EDIT_INTEGRATION_ID || this.isBoxEditSFCIntegration(integrationId); + } + + /** + * Checks if a given integration is a Box Edit integration. + * + * @param {string} [integrationId] - The integration ID + * @return {boolean} + */ + isBoxEditSFCIntegration(integrationId: ?string): boolean { + return integrationId === BOX_EDIT_SFC_INTEGRATION_ID; } /** @@ -349,18 +366,18 @@ class ContentOpenWith extends PureComponent { * Opens the integration in a new tab based on the API data * * @private - * @param {string} integrationID - The integration that was executed + * @param {string} integrationId - The integration that was executed * @param {ExecuteAPI} executeData - API response on how to open an executed integration * @return {void} */ - executeIntegrationSuccessHandler = (integrationID: string, executeData: ExecuteAPI): void => { - if (this.isBoxEditIntegration(integrationID)) { - this.executeBoxEditSuccessHandler(executeData); + executeIntegrationSuccessHandler = (integrationId: string, executeData: ExecuteAPI): void => { + if (this.isBoxEditIntegration(integrationId)) { + this.executeBoxEditSuccessHandler(integrationId, executeData); } else { this.executeOnlineIntegrationSuccessHandler(executeData); } - this.onExecute(integrationID); + this.onExecute(integrationId); }; /** @@ -403,15 +420,17 @@ class ContentOpenWith extends PureComponent { * @return {void} */ - executeBoxEditSuccessHandler = ({ url }: ExecuteAPI): void => { + executeBoxEditSuccessHandler = (integrationId: string, { url }: ExecuteAPI): void => { const { fileId, token } = this.props; const queryParams = queryString.parse(url); const authCode = queryParams[AUTH_CODE]; + const isFileScoped = this.isBoxEditSFCIntegration(integrationId); this.api.getBoxEditAPI().openFile(fileId, { data: { auth_code: authCode, token, + token_scope: isFileScoped ? TYPE_FILE : TYPE_FOLDER, }, }); }; diff --git a/src/components/ContentOpenWith/__tests__/ContentOpenWith-test.js b/src/components/ContentOpenWith/__tests__/ContentOpenWith-test.js index 3e94f30ee5..06acd77b8f 100644 --- a/src/components/ContentOpenWith/__tests__/ContentOpenWith-test.js +++ b/src/components/ContentOpenWith/__tests__/ContentOpenWith-test.js @@ -2,11 +2,11 @@ import React from 'react'; import { shallow } from 'enzyme'; import { FormattedMessage } from 'react-intl'; import ContentOpenWith from '../ContentOpenWith'; +import { BOX_EDIT_INTEGRATION_ID, BOX_EDIT_SFC_INTEGRATION_ID } from '../../../constants'; import messages from '../../messages'; jest.mock('lodash/uniqueId', () => () => 'uniqueId'); -const BOX_EDIT_INTEGRATION_ID = '1338'; const ADOBE_INTEGRATION_ID = '1234'; const BLACKLISTED_ERROR_MESSAGE_KEY = 'boxToolsBlacklistedError'; const UNINSTALLED_ERROR_MESSAGE_KEY = 'boxToolsUninstalledErrorMessage'; @@ -49,9 +49,18 @@ describe('components/ContentOpenWith/ContentOpenWith', () => { }); describe('isBoxEditIntegration()', () => { - test('should determine if the integration is a box edit integration', () => { + test('should determine if the integration is a Box Edit integration', () => { expect(instance.isBoxEditIntegration(ADOBE_INTEGRATION_ID)).toBe(false); expect(instance.isBoxEditIntegration(BOX_EDIT_INTEGRATION_ID)).toBe(true); + expect(instance.isBoxEditIntegration(BOX_EDIT_SFC_INTEGRATION_ID)).toBe(true); + }); + }); + + describe('isBoxEditSFCIntegration()', () => { + test('should determine if the integration is a Box Edit SFC integration', () => { + expect(instance.isBoxEditSFCIntegration(ADOBE_INTEGRATION_ID)).toBe(false); + expect(instance.isBoxEditSFCIntegration(BOX_EDIT_INTEGRATION_ID)).toBe(false); + expect(instance.isBoxEditSFCIntegration(BOX_EDIT_SFC_INTEGRATION_ID)).toBe(true); }); }); @@ -338,7 +347,7 @@ describe('components/ContentOpenWith/ContentOpenWith', () => { instance.executeIntegrationSuccessHandler(id, executeData); - expect(instance.executeBoxEditSuccessHandler).toBeCalledWith(executeData); + expect(instance.executeBoxEditSuccessHandler).toBeCalledWith(id, executeData); }); test('should invoke the online success handler if we executed an online integration', () => { @@ -415,17 +424,19 @@ describe('components/ContentOpenWith/ContentOpenWith', () => { instance.api = { getBoxEditAPI: () => ({ openFile: openFileStub }), }; + instance.isBoxEditSFCIntegration = jest.fn().mockReturnValue(true); const executeData = { url: `www.box.com/execute?file_id=1&auth_code=${authCode}&other_param=foo`, }; - instance.executeBoxEditSuccessHandler(executeData); + instance.executeBoxEditSuccessHandler('1234', executeData); expect(openFileStub).toBeCalledWith(fileId, { data: { auth_code: authCode, token, + token_scope: 'file', }, }); }); diff --git a/src/constants.js b/src/constants.js index 4c9b09a3b0..27c77eb2a0 100644 --- a/src/constants.js +++ b/src/constants.js @@ -306,6 +306,8 @@ export const PLACEHOLDER_USER = { type: 'user', id: '2', name: '' }; /* ------------------ Integrations ------------------------- */ export const APP_INTEGRATION = 'app_integration'; +export const BOX_EDIT_INTEGRATION_ID = '1338'; +export const BOX_EDIT_SFC_INTEGRATION_ID = '13418'; /* ------------------ Task Assignment Statuses ----------------- */ export const TASK_APPROVED: 'approved' = 'approved';