From 9bf87929d849dc6700e1dc3ccba307ba431b15e3 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Sun, 19 Nov 2017 01:56:16 -0500 Subject: [PATCH] add private media library for integrations --- src/actions/mediaLibrary.js | 48 +++++++++++-------- src/components/MediaLibrary/MediaLibrary.css | 22 +++++++++ src/components/MediaLibrary/MediaLibrary.js | 29 +++++++---- src/components/Widgets/FileControl.js | 2 +- src/components/Widgets/ImageControl.js | 2 +- .../providers/assetStore/implementation.js | 8 ++-- src/reducers/mediaLibrary.js | 36 +++++++++++++- 7 files changed, 109 insertions(+), 38 deletions(-) diff --git a/src/actions/mediaLibrary.js b/src/actions/mediaLibrary.js index 0045f9abcd76..805065e34d3e 100644 --- a/src/actions/mediaLibrary.js +++ b/src/actions/mediaLibrary.js @@ -33,7 +33,7 @@ export function insertMedia(mediaPath) { } export function loadMedia(opts = {}) { - const { delay = 0, query = '', page = 1 } = opts; + const { delay = 0, query = '', page = 1, privateUpload } = opts; return async (dispatch, getState) => { const state = getState(); const backend = currentBackend(state.config); @@ -42,17 +42,18 @@ export function loadMedia(opts = {}) { const provider = getIntegrationProvider(state.integrations, backend.getToken, integration); dispatch(mediaLoading(page)); try { - const files = await provider.retrieve(query, page); + const files = await provider.retrieve(query, page, privateUpload); const mediaLoadedOpts = { page, canPaginate: true, dynamicSearch: true, - dynamicSearchQuery: query + dynamicSearchQuery: query, + privateUpload, }; return dispatch(mediaLoaded(files, mediaLoadedOpts)); } catch(error) { - return dispatch(mediaLoadFailed()); + return dispatch(mediaLoadFailed({ privateUpload })); } } dispatch(mediaLoading(page)); @@ -66,7 +67,8 @@ export function loadMedia(opts = {}) { }; } -export function persistMedia(file, privateUpload) { +export function persistMedia(file, opts = {}) { + const { privateUpload } = opts; return async (dispatch, getState) => { const state = getState(); const backend = currentBackend(state.config); @@ -81,7 +83,7 @@ export function persistMedia(file, privateUpload) { const asset = await backend.persistMedia(assetProxy); return dispatch(mediaPersisted(asset)); } - return dispatch(mediaPersisted(assetProxy.asset)); + return dispatch(mediaPersisted(assetProxy.asset, { privateUpload })); } catch(error) { console.error(error); @@ -90,12 +92,13 @@ export function persistMedia(file, privateUpload) { kind: 'danger', dismissAfter: 8000, })); - return dispatch(mediaPersistFailed()); + return dispatch(mediaPersistFailed({ privateUpload })); } }; } -export function deleteMedia(file) { +export function deleteMedia(file, opts = {}) { + const { privateUpload } = opts; return (dispatch, getState) => { const state = getState(); const backend = currentBackend(state.config); @@ -105,7 +108,7 @@ export function deleteMedia(file) { dispatch(mediaDeleting()); return provider.delete(file.id) .then(() => { - return dispatch(mediaDeleted(file)); + return dispatch(mediaDeleted(file, { privateUpload })); }) .catch(error => { console.error(error); @@ -114,7 +117,7 @@ export function deleteMedia(file) { kind: 'danger', dismissAfter: 8000, })); - return dispatch(mediaDeleteFailed()); + return dispatch(mediaDeleteFailed({ privateUpload })); }); } dispatch(mediaDeleting()); @@ -148,36 +151,41 @@ export function mediaLoaded(files, opts = {}) { }; } -export function mediaLoadFailed(error) { - return { type: MEDIA_LOAD_FAILURE }; +export function mediaLoadFailed(error, opts = {}) { + const { privateUpload } = opts; + return { type: MEDIA_LOAD_FAILURE, payload: { privateUpload } }; } export function mediaPersisting() { return { type: MEDIA_PERSIST_REQUEST }; } -export function mediaPersisted(asset) { +export function mediaPersisted(asset, opts = {}) { + const { privateUpload } = opts; return { type: MEDIA_PERSIST_SUCCESS, - payload: { file: asset }, + payload: { file: asset, privateUpload }, }; } -export function mediaPersistFailed(error) { - return { type: MEDIA_PERSIST_FAILURE }; +export function mediaPersistFailed(error, opts = {}) { + const { privateUpload } = opts; + return { type: MEDIA_PERSIST_FAILURE, payload: { privateUpload } }; } export function mediaDeleting() { return { type: MEDIA_DELETE_REQUEST }; } -export function mediaDeleted(file) { +export function mediaDeleted(file, opts = {}) { + const { privateUpload } = opts; return { type: MEDIA_DELETE_SUCCESS, - payload: { file }, + payload: { file, privateUpload }, }; } -export function mediaDeleteFailed(error) { - return { type: MEDIA_DELETE_FAILURE }; +export function mediaDeleteFailed(error, opts = {}) { + const { privateUpload } = opts; + return { type: MEDIA_DELETE_FAILURE, payload: { privateUpload } }; } diff --git a/src/components/MediaLibrary/MediaLibrary.css b/src/components/MediaLibrary/MediaLibrary.css index 0a175226550c..8c8b27da5368 100644 --- a/src/components/MediaLibrary/MediaLibrary.css +++ b/src/components/MediaLibrary/MediaLibrary.css @@ -96,3 +96,25 @@ overflow-wrap: break-word; line-height: 1.3 !important; } + +.nc-mediaLibrary-dialogPrivate { + background-color: var(--backgroundAltColor); + + & .nc-mediaLibrary-title, + & .nc-mediaLibrary-emptyMessage, + & .nc-mediaLibrary-paginatingMessage, + & h1 { + color: var(--textFieldBorderColor); + } + + & .nc-mediaLibrary-card, + & .nc-mediaLibrary-searchInput { + background-color: var(--textFieldBorderColor); + } + + & button:disabled, + & label[disabled] { + background-color: rgba(217, 217, 217, 0.15); + } +} + diff --git a/src/components/MediaLibrary/MediaLibrary.js b/src/components/MediaLibrary/MediaLibrary.js index 76c5b6880f7a..82199e9acc2f 100644 --- a/src/components/MediaLibrary/MediaLibrary.js +++ b/src/components/MediaLibrary/MediaLibrary.js @@ -48,6 +48,10 @@ class MediaLibrary extends React.Component { if (isOpening) { this.setState({ selectedFile: {}, query: '' }); } + + if (isOpening && (this.props.privateUpload !== nextProps.privateUpload)) { + this.props.loadMedia({ privateUpload: nextProps.privateUpload }); + } } /** @@ -111,7 +115,7 @@ class MediaLibrary extends React.Component { */ event.stopPropagation(); event.preventDefault(); - const { loadMedia, persistMedia, privateUpload } = this.props; + const { persistMedia, privateUpload } = this.props; const { files: fileList } = event.dataTransfer || event.target; const files = [...fileList]; const file = files[0]; @@ -121,7 +125,7 @@ class MediaLibrary extends React.Component { * improved in the future, but isn't currently resulting in noticeable * performance/load time issues. */ - await persistMedia(file, privateUpload); + await persistMedia(file, { privateUpload }); this.scrollToTop(); }; @@ -143,20 +147,20 @@ class MediaLibrary extends React.Component { */ handleDelete = () => { const { selectedFile } = this.state; - const { files, deleteMedia } = this.props; + const { files, deleteMedia, privateUpload } = this.props; if (!window.confirm('Are you sure you want to delete selected media?')) { return; } const file = files.find(file => selectedFile.key === file.key); - deleteMedia(file) + deleteMedia(file, { privateUpload }) .then(() => { this.setState({ selectedFile: {} }); }); }; handleLoadMore = () => { - const { loadMedia, dynamicSearchQuery, page } = this.props; - loadMedia({ query: dynamicSearchQuery, page: page + 1 }); + const { loadMedia, dynamicSearchQuery, page, privateUpload } = this.props; + loadMedia({ query: dynamicSearchQuery, page: page + 1, privateUpload }); }; /** @@ -167,8 +171,9 @@ class MediaLibrary extends React.Component { * so this handler has no impact. */ handleSearchKeyDown = async (event) => { - if (event.key === 'Enter' && this.props.dynamicSearch) { - await this.props.loadMedia({ query: this.state.query }) + const { dynamicSearch, loadMedia, privateUpload } = this.props; + if (event.key === 'Enter' && dynamicSearch) { + await loadMedia({ query: this.state.query, privateUpload }) this.scrollToTop(); } }; @@ -216,6 +221,7 @@ class MediaLibrary extends React.Component { hasNextPage, page, isPaginating, + privateUpload, } = this.props; const { query, selectedFile } = this.state; const filteredFiles = forImage ? this.filterImages(files) : files; @@ -236,7 +242,7 @@ class MediaLibrary extends React.Component { } > -

{forImage ? 'Images' : 'Assets'}

+

+ {privateUpload ? 'Private ' : null} + {forImage ? 'Images' : 'Assets'} +

{ const { field, onOpenMediaLibrary } = this.props; - return onOpenMediaLibrary({ controlID: this.controlID, privateUpload: field.private }); + return onOpenMediaLibrary({ controlID: this.controlID, privateUpload: field.get('private') }); }; renderFileName = () => { diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index a8406ce06181..92a9f11611c2 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -43,7 +43,7 @@ export default class ImageControl extends React.Component { handleClick = (e) => { const { field, onOpenMediaLibrary } = this.props; - return onOpenMediaLibrary({ controlID: this.controlID, forImage: true, privateUpload: field.private }); + return onOpenMediaLibrary({ controlID: this.controlID, forImage: true, privateUpload: field.get('private') }); }; renderFileName = () => { diff --git a/src/integrations/providers/assetStore/implementation.js b/src/integrations/providers/assetStore/implementation.js index b2399aa3eb19..dbac1c114cad 100644 --- a/src/integrations/providers/assetStore/implementation.js +++ b/src/integrations/providers/assetStore/implementation.js @@ -1,4 +1,4 @@ -import { pickBy } from 'lodash'; +import { pickBy, trimEnd } from 'lodash'; import { addParams } from '../../../lib/urlHelper'; export default class AssetStore { @@ -10,7 +10,7 @@ export default class AssetStore { this.getToken = getToken; this.shouldConfirmUpload = config.get('shouldConfirmUpload', false); - this.getSignedFormURL = config.get('getSignedFormURL'); + this.getSignedFormURL = trimEnd(config.get('getSignedFormURL'), '/'); } parseJsonResponse(response) { @@ -65,8 +65,8 @@ export default class AssetStore { return content; } - async retrieve(query, page) { - const params = pickBy({ search: query, page }, val => !!val); + async retrieve(query, page, privateUpload) { + const params = pickBy({ search: query, page, filter: privateUpload ? 'private' : 'public' }, val => !!val); const url = addParams(this.getSignedFormURL, params); const token = await this.getToken(); const headers = { diff --git a/src/reducers/mediaLibrary.js b/src/reducers/mediaLibrary.js index 17aeafaeba94..b00f001cd82c 100644 --- a/src/reducers/mediaLibrary.js +++ b/src/reducers/mediaLibrary.js @@ -17,14 +17,26 @@ import { } from '../actions/mediaLibrary'; const mediaLibrary = (state = Map({ isVisible: false, controlMedia: Map() }), action) => { + const privateUploadChanged = state.get('privateUpload') !== get(action, ['payload', 'privateUpload']); switch (action.type) { case MEDIA_LIBRARY_OPEN: { - const { controlID, forImage } = action.payload || {}; + const { controlID, forImage, privateUpload } = action.payload || {}; + if (privateUploadChanged) { + return Map({ + isVisible: true, + forImage, + controlID, + canInsert: !!controlID, + privateUpload, + controlMedia: Map(), + }); + } return state.withMutations(map => { map.set('isVisible', true); map.set('forImage', forImage); map.set('controlID', controlID); map.set('canInsert', !!controlID); + map.set('privateUpload', privateUpload); }); } case MEDIA_LIBRARY_CLOSE: @@ -40,7 +52,12 @@ const mediaLibrary = (state = Map({ isVisible: false, controlMedia: Map() }), ac map.set('isPaginating', action.payload.page > 1); }); case MEDIA_LOAD_SUCCESS: { - const { files = [], page, canPaginate, dynamicSearch, dynamicSearchQuery } = action.payload; + const { files = [], page, canPaginate, dynamicSearch, dynamicSearchQuery, privateUpload } = action.payload; + + if (privateUploadChanged) { + return state; + } + const filesWithKeys = files.map(file => ({ ...file, key: uuid() })); return state.withMutations(map => { map.set('isLoading', false); @@ -59,11 +76,17 @@ const mediaLibrary = (state = Map({ isVisible: false, controlMedia: Map() }), ac }); } case MEDIA_LOAD_FAILURE: + if (privateUploadChanged) { + return state; + } return state.set('isLoading', false); case MEDIA_PERSIST_REQUEST: return state.set('isPersisting', true); case MEDIA_PERSIST_SUCCESS: { const { file } = action.payload; + if (privateUploadChanged) { + return state; + } return state.withMutations(map => { const fileWithKey = { ...file, key: uuid() }; const updatedFiles = [fileWithKey, ...map.get('files')]; @@ -72,11 +95,17 @@ const mediaLibrary = (state = Map({ isVisible: false, controlMedia: Map() }), ac }); } case MEDIA_PERSIST_FAILURE: + if (privateUploadChanged) { + return state; + } return state.set('isPersisting', false); case MEDIA_DELETE_REQUEST: return state.set('isDeleting', true); case MEDIA_DELETE_SUCCESS: { const { key } = action.payload.file; + if (privateUploadChanged) { + return state; + } return state.withMutations(map => { const updatedFiles = map.get('files').filter(file => file.key !== key); map.set('files', updatedFiles); @@ -84,6 +113,9 @@ const mediaLibrary = (state = Map({ isVisible: false, controlMedia: Map() }), ac }); } case MEDIA_DELETE_FAILURE: + if (privateUploadChanged) { + return state; + } return state.set('isDeleting', false); default: return state;