diff --git a/src/app/middleware/localStorageMiddleware.ts b/src/app/middleware/localStorageMiddleware.ts index cc084bfea7..c65fd3b752 100644 --- a/src/app/middleware/localStorageMiddleware.ts +++ b/src/app/middleware/localStorageMiddleware.ts @@ -1,11 +1,12 @@ +import { collectionsCache } from '../../modules/cache/collections.cache'; import { resourcesCache } from '../../modules/cache/resources.cache'; import { samplesCache } from '../../modules/cache/samples.cache'; import { saveTheme } from '../../themes/theme-utils'; import { AppAction } from '../../types/action'; import { IResourceLink } from '../../types/resources'; -import { addResourcePaths } from '../services/actions/resource-explorer-action-creators'; +import { addResourcePaths } from '../services/actions/collections-action-creators'; import { - CHANGE_THEME_SUCCESS, FETCH_RESOURCES_ERROR, FETCH_RESOURCES_SUCCESS, + CHANGE_THEME_SUCCESS, COLLECTION_CREATE_SUCCESS, FETCH_RESOURCES_ERROR, FETCH_RESOURCES_SUCCESS, RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS, SAMPLES_FETCH_SUCCESS } from '../services/redux-constants'; @@ -20,16 +21,32 @@ const localStorageMiddleware = (store: any) => (next: any) => async (action: App break; case RESOURCEPATHS_ADD_SUCCESS: { - const navigationEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; - if(navigationEntry && navigationEntry.loadEventEnd > 0) { - await saveResourcesCollection(action.response); - } + const collections = await collectionsCache.read(); + const item = collections.find(k => k.isDefault)!; + item.paths = action.response; + await collectionsCache.update(item.id, item); break; } + case RESOURCEPATHS_DELETE_SUCCESS: { - updateResourcesCollection(action.response) + const paths = action.response; + const collections = await collectionsCache.read(); + const collection = collections.find(k => k.isDefault)!; + paths.forEach((path: IResourceLink) => { + const index = collection.paths.findIndex(k => k.key === path.key); + if (index > -1) { + collection.paths.splice(index, 1); + } + }) + await collectionsCache.update(collection.id, collection); + break; + } + + case COLLECTION_CREATE_SUCCESS: { + await collectionsCache.create(action.response); break; } + case FETCH_RESOURCES_SUCCESS: case FETCH_RESOURCES_ERROR: { resourcesCache.readCollection().then((data: IResourceLink[]) => { @@ -46,26 +63,4 @@ const localStorageMiddleware = (store: any) => (next: any) => async (action: App return next(action); }; -async function saveResourcesCollection(collection: IResourceLink[]){ - const cachedCollection = await resourcesCache.readCollection(); - let newCollection: IResourceLink[] = collection; - if(cachedCollection && cachedCollection.length > 0 ){ - newCollection = [...cachedCollection, ...collection] - } - await resourcesCache.saveCollection(newCollection); -} - -async function updateResourcesCollection(collection: IResourceLink[]){ - const cachedCollection = await resourcesCache.readCollection(); - if(cachedCollection && cachedCollection.length > 0){ - collection.forEach((path: IResourceLink) => { - const index = cachedCollection.findIndex(k => k.key === path.key); - if(index > -1){ - cachedCollection.splice(index, 1); - } - }) - await resourcesCache.saveCollection(cachedCollection); - } -} - export default localStorageMiddleware; diff --git a/src/app/services/actions/autocomplete-action-creators.spec.ts b/src/app/services/actions/autocomplete-action-creators.spec.ts index 121def1257..d523d29a0a 100644 --- a/src/app/services/actions/autocomplete-action-creators.spec.ts +++ b/src/app/services/actions/autocomplete-action-creators.spec.ts @@ -104,8 +104,7 @@ const mockState: ApplicationState = { labels: [], children: [] }, - error: null, - paths: [] + error: null }, policies: { pending: false, diff --git a/src/app/services/actions/collections-action-creators.spec.ts b/src/app/services/actions/collections-action-creators.spec.ts new file mode 100644 index 0000000000..9f2cb8ec0a --- /dev/null +++ b/src/app/services/actions/collections-action-creators.spec.ts @@ -0,0 +1,87 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import { + RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS +} from '../redux-constants'; +import { addResourcePaths, removeResourcePaths } from './collections-action-creators'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +const paths = [ + { + key: '5-{serviceHealth-id}-issues', + url: '/admin/serviceAnnouncement/healthOverviews/{serviceHealth-id}/issues', + name: 'issues (1)', + labels: [ + { name: 'v1.0', methods: ['Get', 'Post'] }, + { name: 'beta', methods: ['Get', 'Post'] } + ], + isExpanded: true, + parent: '{serviceHealth-id}', + level: 5, + paths: ['/', 'admin', 'serviceAnnouncement', 'healthOverviews', '{serviceHealth-id}'], + type: 'path', + links: [] + }, { + key: '6-issues-{serviceHealthIssue-id}', + url: '/admin/serviceAnnouncement/healthOverviews/{serviceHealth-id}/issues/{serviceHealthIssue-id}', + name: '{serviceHealthIssue-id} (1)', + labels: [ + { name: 'v1.0', methods: ['Get', 'Patch', 'Delete'] }, + { name: 'beta', methods: ['Get', 'Patch', 'Delete'] } + ], + isExpanded: true, + parent: 'issues', + level: 6, + paths: ['/', 'admin', 'serviceAnnouncement', 'healthOverviews', '{serviceHealth-id}', 'issues'], + type: 'path', + links: [] + } +]; + +describe('Collections actions', () => { + beforeEach(() => { + fetchMock.resetMocks(); + }); + + it('should dispatch RESOURCEPATHS_ADD_SUCCESS when addResourcePaths() is called with valid paths', () => { + + const expectedActions = [ + { + type: RESOURCEPATHS_ADD_SUCCESS, + response: paths + } + ]; + + const store_ = mockStore({ + resources: { + paths: [] + } + }); + + store_.dispatch(addResourcePaths(paths)); + expect(store_.getActions()).toEqual(expectedActions); + }); + + it('should dispatch RESOURCEPATHS_DELETE_SUCCESS when removeResourcePaths() is dispatched', () => { + + const expectedActions = [ + { + type: RESOURCEPATHS_DELETE_SUCCESS, + response: paths + } + ]; + + const store_ = mockStore({ + resources: { + paths + } + }); + + store_.dispatch(removeResourcePaths(paths)); + expect(store_.getActions()).toEqual(expectedActions); + }) + +}); diff --git a/src/app/services/actions/collections-action-creators.ts b/src/app/services/actions/collections-action-creators.ts new file mode 100644 index 0000000000..a8b97ece2c --- /dev/null +++ b/src/app/services/actions/collections-action-creators.ts @@ -0,0 +1,26 @@ +import { AppAction } from '../../../types/action'; +import { + COLLECTION_CREATE_SUCCESS, + RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS +} from '../redux-constants'; + +export function addResourcePaths(response: object): AppAction { + return { + type: RESOURCEPATHS_ADD_SUCCESS, + response + }; +} + +export function createCollection(response: object): AppAction { + return { + type: COLLECTION_CREATE_SUCCESS, + response + }; +} + +export function removeResourcePaths(response: object): AppAction { + return { + type: RESOURCEPATHS_DELETE_SUCCESS, + response + }; +} diff --git a/src/app/services/actions/permissions-action-creator.spec.ts b/src/app/services/actions/permissions-action-creator.spec.ts index a1824464ab..ef4199124b 100644 --- a/src/app/services/actions/permissions-action-creator.spec.ts +++ b/src/app/services/actions/permissions-action-creator.spec.ts @@ -120,8 +120,7 @@ const mockState: ApplicationState = { labels: [], children: [] }, - error: null, - paths: [] + error: null }, policies: { pending: false, diff --git a/src/app/services/actions/resource-explorer-action-creators.spec.ts b/src/app/services/actions/resource-explorer-action-creators.spec.ts index 600ada58bc..9d8ebdf848 100644 --- a/src/app/services/actions/resource-explorer-action-creators.spec.ts +++ b/src/app/services/actions/resource-explorer-action-creators.spec.ts @@ -1,12 +1,12 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { - addResourcePaths, fetchResources, fetchResourcesError, - fetchResourcesPending, fetchResourcesSuccess, removeResourcePaths + fetchResources, fetchResourcesError, + fetchResourcesPending, fetchResourcesSuccess } from '../../../app/services/actions/resource-explorer-action-creators'; import { FETCH_RESOURCES_ERROR, - FETCH_RESOURCES_PENDING, FETCH_RESOURCES_SUCCESS, RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS + FETCH_RESOURCES_PENDING, FETCH_RESOURCES_SUCCESS } from '../../../app/services/redux-constants'; import { AppAction } from '../../../types/action'; import { Mode } from '../../../types/enums'; @@ -98,8 +98,7 @@ const mockState: ApplicationState = { labels: [], children: [] }, - error: null, - paths: [] + error: null }, policies: { pending: false, @@ -157,44 +156,6 @@ describe('Resource Explorer actions', () => { expect(action).toEqual(expectedAction); }); - it('should dispatch RESOURCEPATHS_ADD_SUCCESS when addResourcePaths() is called with valid paths', () => { - - const expectedActions = [ - { - type: RESOURCEPATHS_ADD_SUCCESS, - response: paths - } - ]; - - const store_ = mockStore({ - resources: { - paths: [] - } - }); - - store_.dispatch(addResourcePaths(paths)); - expect(store_.getActions()).toEqual(expectedActions); - }); - - it('should dispatch RESOURCEPATHS_DELETE_SUCCESS when removeResourcePaths() is dispatched', () => { - - const expectedActions = [ - { - type: RESOURCEPATHS_DELETE_SUCCESS, - response: paths - } - ]; - - const store_ = mockStore({ - resources: { - paths - } - }); - - store_.dispatch(removeResourcePaths(paths)); - expect(store_.getActions()).toEqual(expectedActions); - }) - it('should dispatch FETCH_RESOURCES_ERROR when fetchResourcesError() is called', () => { // Arrange const response = {}; diff --git a/src/app/services/actions/resource-explorer-action-creators.ts b/src/app/services/actions/resource-explorer-action-creators.ts index d3e18f1af8..77904c365d 100644 --- a/src/app/services/actions/resource-explorer-action-creators.ts +++ b/src/app/services/actions/resource-explorer-action-creators.ts @@ -1,12 +1,13 @@ +import { resourcesCache } from '../../../modules/cache/resources.cache'; import { AppAction } from '../../../types/action'; -import { - FETCH_RESOURCES_SUCCESS, FETCH_RESOURCES_PENDING, - FETCH_RESOURCES_ERROR, RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS -} from '../redux-constants'; +import { IRequestOptions } from '../../../types/request'; import { IResource } from '../../../types/resources'; import { ApplicationState } from '../../../types/root'; -import { IRequestOptions } from '../../../types/request'; -import { resourcesCache } from '../../../modules/cache/resources.cache'; +import { + FETCH_RESOURCES_ERROR, + FETCH_RESOURCES_PENDING, + FETCH_RESOURCES_SUCCESS +} from '../redux-constants'; export function fetchResourcesSuccess(response: object): AppAction { return { @@ -29,20 +30,6 @@ export function fetchResourcesError(response: object): AppAction { }; } -export function addResourcePaths(response: object): AppAction { - return { - type: RESOURCEPATHS_ADD_SUCCESS, - response - }; -} - -export function removeResourcePaths(response: object): AppAction { - return { - type: RESOURCEPATHS_DELETE_SUCCESS, - response - }; -} - export function fetchResources() { return async (dispatch: Function, getState: Function) => { const { devxApi }: ApplicationState = getState(); diff --git a/src/app/services/reducers/collections-reducer.spec.ts b/src/app/services/reducers/collections-reducer.spec.ts new file mode 100644 index 0000000000..791c3ad651 --- /dev/null +++ b/src/app/services/reducers/collections-reducer.spec.ts @@ -0,0 +1,142 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import { Collection, IResourceLink, ResourceLinkType } from '../../../types/resources'; +import { addResourcePaths, removeResourcePaths } from '../actions/collections-action-creators'; +import { RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS } from '../redux-constants'; +import { collections } from './collections-reducer'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +const initialState: Collection[] = [{ + id: '1', + name: 'Test Collection', + paths: [], + isDefault: true +}]; + +const paths = [{ + key: '5-issues', + url: '/issues', + name: 'issues (1)', + labels: [ + { + name: 'v1.0', methods: [{ + name: 'Get', + documentationUrl: null + }, { + name: 'Post', + documentationUrl: null + }] + }, + { + name: 'beta', methods: [{ + name: 'Get', + documentationUrl: null + }, { + name: 'Post', + documentationUrl: null + }] + } + ], + version: 'v1.0', + methods: [{ + name: 'Get', + documentationUrl: null + }, { + name: 'Post', + documentationUrl: null + }], + isExpanded: true, + parent: '/', + level: 1, + paths: ['/'], + type: 'path', + links: [] +}]; + +const resourceLinks: IResourceLink[] = [ + { + labels: [ + { + name: 'v1.0', methods: [{ + name: 'Get', + documentationUrl: null + }, { + name: 'Post', + documentationUrl: null + }] + } + ], + key: '5-issues', + url: '/issues', + name: 'issues (1)', + icon: 'LightningBolt', + isExpanded: true, + level: 7, + parent: '/', + paths: ['/'], + type: ResourceLinkType.PATH, + links: [] + } +]; + +describe('Collections Reducer', () => { + it('should return initial state', () => { + const dummyAction = { type: 'Dummy', response: { dummy: 'Dummy' } }; + const newState = collections(initialState, dummyAction); + expect(newState).toEqual(initialState); + }); + + it('should handle RESOURCEPATHS_ADD_SUCCESS', () => { + const expectedActions = [{ response: paths, type: RESOURCEPATHS_ADD_SUCCESS }]; + const store = mockStore({ resources: {} }); + store.dispatch(addResourcePaths(paths)); + expect(store.getActions()).toEqual(expectedActions); + }); + + it('should handle RESOURCEPATHS_DELETE_SUCCESS', () => { + const expectedActions = [{ response: paths, type: RESOURCEPATHS_DELETE_SUCCESS }]; + const store = mockStore({ + resources: { + paths + } + }); + store.dispatch(removeResourcePaths(paths)); + expect(store.getActions()).toEqual(expectedActions); + }); + + it('should handle RESOURCEPATHS_ADD_SUCCESS and return new state with the paths', () => { + const newState = [...initialState]; + newState[0].paths = resourceLinks; + const action_ = { + type: RESOURCEPATHS_ADD_SUCCESS, + response: paths + } + const state_ = collections(newState, action_); + expect(state_[0].paths).toEqual(resourceLinks); + }); + + it('should handle RESOURCEPATHS_DELETE_SUCCESS and return new state with no resource paths', () => { + const newState = [...initialState]; + newState[0].paths = resourceLinks; + const action_ = { + type: RESOURCEPATHS_DELETE_SUCCESS, + response: paths + } + const state_ = collections(newState, action_); + expect(state_[0].paths).toEqual([]); + }); + + it('should return unchanged state if no relevant action is passed', () => { + const newState = [...initialState]; + const action_ = { + type: 'Dummy', + response: { dummy: 'Dummy' } + } + const state_ = collections(newState, action_); + expect(state_).toEqual(newState); + }); + +}); diff --git a/src/app/services/reducers/collections-reducer.ts b/src/app/services/reducers/collections-reducer.ts new file mode 100644 index 0000000000..25839c462b --- /dev/null +++ b/src/app/services/reducers/collections-reducer.ts @@ -0,0 +1,51 @@ +import { AppAction } from '../../../types/action'; +import { Collection, IResourceLink } from '../../../types/resources'; +import { + COLLECTION_CREATE_SUCCESS, + RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS +} from '../redux-constants'; + +const initialState: Collection[] = []; + +export function collections(state: Collection[] = initialState, action: AppAction): Collection[] { + switch (action.type) { + + case COLLECTION_CREATE_SUCCESS: + const items = [...state]; + items.push(action.response); + return items; + + case RESOURCEPATHS_ADD_SUCCESS: + const index = state.findIndex(k => k.isDefault); + if (index > -1) { + const paths: IResourceLink[] = [...state[index].paths]; + action.response.forEach((element: any) => { + const exists = !!paths.find(k => k.key === element.key); + if (!exists) { + paths.push(element); + } + }); + const context = [...state]; + context[index].paths = paths; + return context; + } + return state + + case RESOURCEPATHS_DELETE_SUCCESS: + const indexOfDefaultCollection = state.findIndex(k => k.isDefault); + if (indexOfDefaultCollection > -1) { + const list: IResourceLink[] = [...state[indexOfDefaultCollection].paths]; + action.response.forEach((path: IResourceLink) => { + const pathIndex = list.findIndex(k => k.key === path.key); + list.splice(pathIndex, 1); + }); + const newState = [...state]; + newState[indexOfDefaultCollection].paths = list; + return newState; + } + return state + + default: + return state; + } +} diff --git a/src/app/services/reducers/index.ts b/src/app/services/reducers/index.ts index c7ad134936..4e51a41053 100644 --- a/src/app/services/reducers/index.ts +++ b/src/app/services/reducers/index.ts @@ -3,6 +3,7 @@ import { combineReducers } from 'redux'; import { adaptiveCard } from './adaptive-cards-reducer'; import { authToken, consentedScopes } from './auth-reducers'; import { autoComplete } from './autocomplete-reducer'; +import { collections } from './collections-reducer'; import { devxApi } from './devxApi-reducers'; import { dimensions } from './dimensions-reducers'; import { graphExplorerMode } from './graph-explorer-mode-reducer'; @@ -27,6 +28,7 @@ export default combineReducers({ adaptiveCard, authToken, autoComplete, + collections, consentedScopes, devxApi, dimensions, diff --git a/src/app/services/reducers/resources-reducer.spec.ts b/src/app/services/reducers/resources-reducer.spec.ts index 53759c8dff..5a6fcbcab0 100644 --- a/src/app/services/reducers/resources-reducer.spec.ts +++ b/src/app/services/reducers/resources-reducer.spec.ts @@ -1,13 +1,10 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { addResourcePaths, removeResourcePaths } from '../../../app/services/actions/resource-explorer-action-creators'; import { resources } from '../../../app/services/reducers/resources-reducer'; import { FETCH_RESOURCES_ERROR, FETCH_RESOURCES_PENDING, - FETCH_RESOURCES_SUCCESS, - RESOURCEPATHS_ADD_SUCCESS, - RESOURCEPATHS_DELETE_SUCCESS + FETCH_RESOURCES_SUCCESS } from '../../../app/services/redux-constants'; import { IResource, IResourceLink, IResources, ResourceLinkType } from '../../../types/resources'; @@ -69,8 +66,7 @@ const initialState: IResources = { labels: [], segment: '' }, - error: null, - paths: [] + error: null }; const paths = [{ @@ -179,54 +175,4 @@ describe('Resources Reducer', () => { expect(state).toEqual(newState); }); - it('should handle RESOURCEPATHS_ADD_SUCCESS', () => { - const expectedActions = [{ response: paths, type: RESOURCEPATHS_ADD_SUCCESS }]; - const store = mockStore({ resources: {} }); - store.dispatch(addResourcePaths(paths)); - expect(store.getActions()).toEqual(expectedActions); - }); - - it('should handle RESOURCEPATHS_DELETE_SUCCESS', () => { - const expectedActions = [{ response: paths, type: RESOURCEPATHS_DELETE_SUCCESS }]; - const store = mockStore({ - resources: { - paths - } - }); - store.dispatch(removeResourcePaths(paths)); - expect(store.getActions()).toEqual(expectedActions); - }); - - it('should handle RESOURCEPATHS_ADD_SUCCESS and return new state with the paths', () => { - const newState = { ...initialState }; - newState.paths = resourceLinks; - const action_ = { - type: RESOURCEPATHS_ADD_SUCCESS, - response: paths - } - const state_ = resources(newState, action_); - expect(state_.paths).toEqual(resourceLinks); - }); - - it('should handle RESOURCEPATHS_DELETE_SUCCESS and return new state with no resource paths', () => { - const newState = { ...initialState }; - newState.paths = resourceLinks; - const action_ = { - type: RESOURCEPATHS_DELETE_SUCCESS, - response: paths - } - const state_ = resources(newState, action_); - expect(state_.paths).toEqual([]); - }); - - it('should return unchanged state if no relevant action is passed', () => { - const newState = { ...initialState }; - const action_ = { - type: 'Dummy', - response: { dummy: 'Dummy' } - } - const state_ = resources(newState, action_); - expect(state_).toEqual(newState); - }); - }); diff --git a/src/app/services/reducers/resources-reducer.ts b/src/app/services/reducers/resources-reducer.ts index 10c3e1f863..82ce73d738 100644 --- a/src/app/services/reducers/resources-reducer.ts +++ b/src/app/services/reducers/resources-reducer.ts @@ -1,8 +1,8 @@ import { AppAction } from '../../../types/action'; -import { IResource, IResourceLink, IResources } from '../../../types/resources'; +import { IResource, IResources } from '../../../types/resources'; import { FETCH_RESOURCES_ERROR, FETCH_RESOURCES_PENDING, - FETCH_RESOURCES_SUCCESS, RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS + FETCH_RESOURCES_SUCCESS } from '../redux-constants'; const initialState: IResources = { @@ -12,8 +12,7 @@ const initialState: IResources = { labels: [], segment: '' }, - error: null, - paths: [] + error: null }; export function resources(state: IResources = initialState, action: AppAction): IResources { @@ -22,44 +21,19 @@ export function resources(state: IResources = initialState, action: AppAction): return { pending: false, data: action.response, - error: null, - paths: state.paths + error: null }; case FETCH_RESOURCES_ERROR: return { pending: false, error: action.response, - data: {} as IResource, - paths: state.paths + data: {} as IResource }; case FETCH_RESOURCES_PENDING: return { pending: true, data: initialState.data, - error: null, - paths: state.paths - }; - case RESOURCEPATHS_ADD_SUCCESS: - const paths: IResourceLink[] = [...state.paths]; - action.response.forEach((element: any) => { - const exists = !!paths.find(k => k.key === element.key); - if (!exists) { - paths.push(element); - } - }); - return { - ...state, - paths - }; - case RESOURCEPATHS_DELETE_SUCCESS: - const list: IResourceLink[] = [...state.paths]; - action.response.forEach((path: IResourceLink) => { - const index = list.findIndex(k => k.key === path.key); - list.splice(index, 1); - }); - return { - ...state, - paths: list + error: null }; default: return state; diff --git a/src/app/services/redux-constants.ts b/src/app/services/redux-constants.ts index 760f2d443e..65cb0f06b5 100644 --- a/src/app/services/redux-constants.ts +++ b/src/app/services/redux-constants.ts @@ -60,3 +60,4 @@ export const GET_ALL_PRINCIPAL_GRANTS_ERROR = 'GET_ALL_PRINCIPAL_GRANTS_ERROR'; export const REVOKE_SCOPES_PENDING = 'REVOKE_SCOPES_PENDING'; export const REVOKE_SCOPES_SUCCESS = 'REVOKE_SCOPES_SUCCESS'; export const REVOKE_SCOPES_ERROR = 'REVOKE_SCOPES_ERROR'; +export const COLLECTION_CREATE_SUCCESS = 'COLLECTION_CREATE_SUCCESS'; diff --git a/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx b/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx index 9e1630adb2..c3e0c30271 100644 --- a/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx +++ b/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx @@ -13,8 +13,8 @@ import { AppDispatch, useAppSelector } from '../../../../store'; import { componentNames, eventTypes, telemetry } from '../../../../telemetry'; import { IQuery } from '../../../../types/query-runner'; import { IResource, IResourceLink, ResourceLinkType, ResourceOptions } from '../../../../types/resources'; +import { addResourcePaths } from '../../../services/actions/collections-action-creators'; import { setSampleQuery } from '../../../services/actions/query-input-action-creators'; -import { addResourcePaths } from '../../../services/actions/resource-explorer-action-creators'; import { GRAPH_URL } from '../../../services/graph-constants'; import { getResourcesSupportedByVersion } from '../../../utils/resources/resources-filter'; import { searchBoxStyles } from '../../../utils/searchbox.styles'; @@ -31,14 +31,13 @@ import ResourceLink from './ResourceLink'; import { navStyles } from './resources.styles'; const UnstyledResourceExplorer = (props: any) => { - const { resources } = useAppSelector( + const { resources: { data, pending }, collections } = useAppSelector( (state) => state ); const dispatch: AppDispatch = useDispatch(); const classes = classNames(props); - - const { data, pending, paths: selectedLinks } = resources; + const selectedLinks = collections ? collections.find(k => k.isDefault)!.paths : []; const versions: any[] = [ { key: 'v1.0', text: 'v1.0' }, { key: 'beta', text: 'beta' } @@ -57,7 +56,6 @@ const UnstyledResourceExplorer = (props: any) => { setItems(navigationGroup); }, [filteredPayload.length]); - const addToCollection = (item: IResourceLink) => { dispatch(addResourcePaths(getResourcePaths(item, version))); } diff --git a/src/app/views/sidebar/resource-explorer/collection/PreviewCollection.tsx b/src/app/views/sidebar/resource-explorer/collection/PreviewCollection.tsx index ec52bfad37..238cf1e71e 100644 --- a/src/app/views/sidebar/resource-explorer/collection/PreviewCollection.tsx +++ b/src/app/views/sidebar/resource-explorer/collection/PreviewCollection.tsx @@ -7,7 +7,7 @@ import { useDispatch } from 'react-redux'; import { AppDispatch, useAppSelector } from '../../../../../store'; import { IResourceLink } from '../../../../../types/resources'; -import { removeResourcePaths } from '../../../../services/actions/resource-explorer-action-creators'; +import { removeResourcePaths } from '../../../../services/actions/collections-action-creators'; import { PopupsComponent } from '../../../../services/context/popups-context'; import { translateMessage } from '../../../../utils/translate-messages'; import { downloadToLocal } from '../../../common/download'; @@ -20,9 +20,10 @@ export interface IPathsReview { const PathsReview: React.FC> = (props) => { const dispatch: AppDispatch = useDispatch(); - const { resources: { paths: items } } = useAppSelector( + const { collections } = useAppSelector( (state) => state ); + const items = collections ? collections.find(k => k.isDefault)!.paths : []; const [selectedItems, setSelectedItems] = useState([]); const columns = [ diff --git a/src/app/views/sidebar/resource-explorer/command-options/CommandOptions.tsx b/src/app/views/sidebar/resource-explorer/command-options/CommandOptions.tsx index 3cada78e27..5835b4c6cc 100644 --- a/src/app/views/sidebar/resource-explorer/command-options/CommandOptions.tsx +++ b/src/app/views/sidebar/resource-explorer/command-options/CommandOptions.tsx @@ -6,7 +6,7 @@ import { useState } from 'react'; import { useDispatch } from 'react-redux'; import { AppDispatch, useAppSelector } from '../../../../../store'; -import { removeResourcePaths } from '../../../../services/actions/resource-explorer-action-creators'; +import { removeResourcePaths } from '../../../../services/actions/collections-action-creators'; import { usePopups } from '../../../../services/hooks'; import { translateMessage } from '../../../../utils/translate-messages'; import { resourceExplorerStyles } from '../resources.styles'; @@ -23,7 +23,9 @@ const CommandOptions = (props: ICommandOptions) => { const { version } = props; const theme = getTheme(); - const { resources: { paths } } = useAppSelector((state) => state); + const { collections } = useAppSelector((state) => state); + const paths = collections ? collections.find(k => k.isDefault)!.paths : []; + const itemStyles = resourceExplorerStyles(theme).itemStyles; const commandStyles = resourceExplorerStyles(theme).commandBarStyles; const options: ICommandBarItemProps[] = [ diff --git a/src/index.tsx b/src/index.tsx index 308c2b8590..b5e123ac96 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,23 +1,26 @@ import { AuthenticationResult } from '@azure/msal-browser'; -import 'bootstrap/dist/css/bootstrap-grid.min.css'; -import '@ms-ofb/officebrowserfeedbacknpm/styles/officebrowserfeedback.css'; import { initializeIcons } from '@fluentui/react'; +import '@ms-ofb/officebrowserfeedbacknpm/styles/officebrowserfeedback.css'; +import 'bootstrap/dist/css/bootstrap-grid.min.css'; import ReactDOM from 'react-dom/client'; import { IntlProvider } from 'react-intl'; import { Provider } from 'react-redux'; import { getAuthTokenSuccess, getConsentedScopesSuccess } from './app/services/actions/auth-action-creators'; +import { createCollection } from './app/services/actions/collections-action-creators'; import { setDevxApiUrl } from './app/services/actions/devxApi-action-creators'; import { setGraphExplorerMode } from './app/services/actions/explorer-mode-action-creator'; import { getGraphProxyUrl } from './app/services/actions/proxy-action-creator'; import { bulkAddHistoryItems } from './app/services/actions/request-history-action-creators'; +import { fetchResources } from './app/services/actions/resource-explorer-action-creators'; import { changeTheme, changeThemeSuccess } from './app/services/actions/theme-action-creator'; import { isValidHttpsUrl } from './app/utils/external-link-validation'; import App from './app/views/App'; -import { historyCache } from './modules/cache/history-utils'; import { geLocale } from './appLocale'; import messages from './messages'; import { authenticationWrapper } from './modules/authentication'; +import { collectionsCache } from './modules/cache/collections.cache'; +import { historyCache } from './modules/cache/history-utils'; import { store } from './store'; import './styles/index.scss'; import { telemetry } from './telemetry'; @@ -26,7 +29,7 @@ import { loadGETheme } from './themes'; import { readTheme } from './themes/theme-utils'; import { IDevxAPI } from './types/devx-api'; import { Mode } from './types/enums'; -import { fetchResources } from './app/services/actions/resource-explorer-action-creators'; +import { Collection } from './types/resources'; const appRoot: HTMLElement = document.getElementById('root')!; @@ -138,6 +141,21 @@ historyCache.readHistoryData().then((data: any) => { } }); +collectionsCache.read().then((data: Collection[]) => { + if (!data || data.length === 0) { + appStore.dispatch(createCollection({ + id: new Date().getTime().toString(), + title: 'My Collection', + paths: [], + isDefault: true + })); + } else { + data.forEach((collection: Collection) => { + appStore.dispatch(createCollection(collection)); + }); + } +}); + function loadResources() { appStore.dispatch(fetchResources()); } diff --git a/src/modules/cache/collections.cache.ts b/src/modules/cache/collections.cache.ts new file mode 100644 index 0000000000..bf5dff02e4 --- /dev/null +++ b/src/modules/cache/collections.cache.ts @@ -0,0 +1,51 @@ +import localforage from 'localforage'; +import { Collection } from '../../types/resources'; + +const collectionsStorage = localforage.createInstance({ + storeName: 'collections', + name: 'GE_V4' +}); + +export const collectionsCache = (function () { + + const create = async (collection: Collection) => { + await collectionsStorage.setItem(collection.id, collection); + } + + const read = async (): Promise => { + let collections: Collection[] = []; + const keys = await collectionsStorage.keys(); + for (const id of keys) { + const collection = await collectionsStorage.getItem(id)! as Collection; + collections = [...collections, collection]; + } + return collections; + } + + const get = async (id: string): Promise => { + const cachedCollection = await collectionsStorage.getItem(id) as Collection; + return cachedCollection ? cachedCollection || null : null; + } + + const update = async (id: string, collection: Collection) => { + const cachedCollection = await collectionsStorage.getItem(id) as Collection; + if (cachedCollection) { + await collectionsStorage.setItem(id, collection); + } + } + + const destroy = async (id: string) => { + const cachedCollection = await collectionsStorage.getItem(id) as Collection; + if (cachedCollection) { + await collectionsStorage.removeItem(id); + } + } + + return { + create, + get, + read, + update, + destroy + } +})(); \ No newline at end of file diff --git a/src/types/resources.ts b/src/types/resources.ts index 6a36b53c9d..deca7cb9f4 100644 --- a/src/types/resources.ts +++ b/src/types/resources.ts @@ -22,7 +22,6 @@ export interface IResources { pending: boolean; data: IResource; error: Error | null; - paths: IResourceLink[]; } export interface IResourceLink extends INavLink { @@ -45,3 +44,10 @@ export enum ResourceLinkType { export enum ResourceOptions { ADD_TO_COLLECTION = 'add-to-collection' } + +export interface Collection { + id: string; + name: string; + paths: IResourceLink[], + isDefault?: boolean; +} \ No newline at end of file diff --git a/src/types/root.ts b/src/types/root.ts index 1ff95b81ff..d2a861a7fc 100644 --- a/src/types/root.ts +++ b/src/types/root.ts @@ -10,7 +10,7 @@ import { IScopes } from './permissions'; import { IUser } from './profile'; import { IGraphResponse } from './query-response'; import { IQuery, ISampleQuery } from './query-runner'; -import { IResources } from './resources'; +import { IResources, Collection } from './resources'; import { ISidebarProps } from './sidebar'; import { ISnippet } from './snippets'; import { IStatus } from './status'; @@ -42,6 +42,7 @@ export interface ApplicationState { devxApi: IDevxAPI; resources: IResources; policies: IPolicies; + collections?: Collection[]; } export interface IApiFetch {