From 79e1a7ab6b3a3a28c59bdb4a7c1b0ff9c57e0aff Mon Sep 17 00:00:00 2001 From: Devlin Junker Date: Wed, 30 Aug 2023 22:13:58 -0700 Subject: [PATCH 1/3] fix loading bug and move to feed-display directory Signed-off-by: Devlin Junker --- .../{ => feed-display}/FeedItemDisplay.vue | 6 +++--- .../FeedItemDisplayList.vue | 2 +- .../{ => feed-display}/FeedItemRow.vue | 6 +++--- .../{ => feed-display}/ItemSkeleton.vue | 0 .../{ => feed-display}/VirtualScroll.vue | 0 src/components/routes/Feed.vue | 4 ++-- src/components/routes/Starred.vue | 2 +- src/components/routes/Unread.vue | 4 ++-- src/store/item.ts | 20 ++++++++++++++----- src/types/MutationTypes.ts | 1 + .../FeedItemDisplay.spec.ts | 4 ++-- .../FeedItemDisplayList.spec.ts | 6 +++--- .../{ => feed-display}/FeedItemRow.spec.ts | 4 ++-- 13 files changed, 35 insertions(+), 24 deletions(-) rename src/components/{ => feed-display}/FeedItemDisplay.vue (97%) rename src/components/{ => feed-display}/FeedItemDisplayList.vue (99%) rename src/components/{ => feed-display}/FeedItemRow.vue (97%) rename src/components/{ => feed-display}/ItemSkeleton.vue (100%) rename src/components/{ => feed-display}/VirtualScroll.vue (100%) rename tests/javascript/unit/components/{ => feed-display}/FeedItemDisplay.spec.ts (93%) rename tests/javascript/unit/components/{ => feed-display}/FeedItemDisplayList.spec.ts (82%) rename tests/javascript/unit/components/{ => feed-display}/FeedItemRow.spec.ts (95%) diff --git a/src/components/FeedItemDisplay.vue b/src/components/feed-display/FeedItemDisplay.vue similarity index 97% rename from src/components/FeedItemDisplay.vue rename to src/components/feed-display/FeedItemDisplay.vue index 1d7d5a843..4eab900ab 100644 --- a/src/components/FeedItemDisplay.vue +++ b/src/components/feed-display/FeedItemDisplay.vue @@ -101,9 +101,9 @@ import CloseIcon from 'vue-material-design-icons/Close.vue' import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import { Feed } from '../types/Feed' -import { FeedItem } from '../types/FeedItem' -import { ACTIONS, MUTATIONS } from '../store' +import { Feed } from '../../types/Feed' +import { FeedItem } from '../../types/FeedItem' +import { ACTIONS, MUTATIONS } from '../../store' export default Vue.extend({ name: 'FeedItemDisplay', diff --git a/src/components/FeedItemDisplayList.vue b/src/components/feed-display/FeedItemDisplayList.vue similarity index 99% rename from src/components/FeedItemDisplayList.vue rename to src/components/feed-display/FeedItemDisplayList.vue index f7ba8d3ab..8fce1fb25 100644 --- a/src/components/FeedItemDisplayList.vue +++ b/src/components/feed-display/FeedItemDisplayList.vue @@ -60,7 +60,7 @@ import VirtualScroll from './VirtualScroll.vue' import FeedItemRow from './FeedItemRow.vue' import FeedItemDisplay from './FeedItemDisplay.vue' -import { FeedItem } from '../types/FeedItem' +import { FeedItem } from '../../types/FeedItem' const DEFAULT_DISPLAY_LIST_CONFIG = { starFilter: true, diff --git a/src/components/FeedItemRow.vue b/src/components/feed-display/FeedItemRow.vue similarity index 97% rename from src/components/FeedItemRow.vue rename to src/components/feed-display/FeedItemRow.vue index 6d436ba01..03670295a 100644 --- a/src/components/FeedItemRow.vue +++ b/src/components/feed-display/FeedItemRow.vue @@ -60,9 +60,9 @@ import ShareVariant from 'vue-material-design-icons/ShareVariant.vue' import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import { Feed } from '../types/Feed' -import { FeedItem } from '../types/FeedItem' -import { ACTIONS, MUTATIONS } from '../store' +import { Feed } from '../../types/Feed' +import { FeedItem } from '../../types/FeedItem' +import { ACTIONS, MUTATIONS } from '../../store' export default Vue.extend({ name: 'FeedItemRow', diff --git a/src/components/ItemSkeleton.vue b/src/components/feed-display/ItemSkeleton.vue similarity index 100% rename from src/components/ItemSkeleton.vue rename to src/components/feed-display/ItemSkeleton.vue diff --git a/src/components/VirtualScroll.vue b/src/components/feed-display/VirtualScroll.vue similarity index 100% rename from src/components/VirtualScroll.vue rename to src/components/feed-display/VirtualScroll.vue diff --git a/src/components/routes/Feed.vue b/src/components/routes/Feed.vue index 6f8f9c922..31194eb09 100644 --- a/src/components/routes/Feed.vue +++ b/src/components/routes/Feed.vue @@ -17,7 +17,7 @@ import { mapState } from 'vuex' import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js' -import FeedItemDisplayList from '../FeedItemDisplayList.vue' +import FeedItemDisplayList from '../feed-display/FeedItemDisplayList.vue' import { FeedItem } from '../../types/FeedItem' import { ACTIONS, MUTATIONS } from '../../store' @@ -56,7 +56,7 @@ export default Vue.extend({ methods: { async fetchMore() { if (!this.$store.state.items.fetchingItems['feed-' + this.feedId]) { - this.$store.dispatch(ACTIONS.FETCH_FEED_ITEMS, { feedId: this.id, start: this.items && this.items.length > 0 ? this.items[this.items.length - 1].id : 0 }) + this.$store.dispatch(ACTIONS.FETCH_FEED_ITEMS, { feedId: this.id }) } }, }, diff --git a/src/components/routes/Starred.vue b/src/components/routes/Starred.vue index d3df5a89f..243d42c5d 100644 --- a/src/components/routes/Starred.vue +++ b/src/components/routes/Starred.vue @@ -20,7 +20,7 @@ import { mapState } from 'vuex' import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js' -import FeedItemDisplayList from '../FeedItemDisplayList.vue' +import FeedItemDisplayList from '../feed-display/FeedItemDisplayList.vue' import { FeedItem } from '../../types/FeedItem' import { ACTIONS, MUTATIONS } from '../../store' diff --git a/src/components/routes/Unread.vue b/src/components/routes/Unread.vue index 230a8ea04..b7199ea35 100644 --- a/src/components/routes/Unread.vue +++ b/src/components/routes/Unread.vue @@ -21,7 +21,7 @@ import { mapState } from 'vuex' import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js' -import FeedItemDisplayList from '../FeedItemDisplayList.vue' +import FeedItemDisplayList from '../feed-display/FeedItemDisplayList.vue' import { FeedItem } from '../../types/FeedItem' import { ACTIONS, MUTATIONS } from '../../store' @@ -68,7 +68,7 @@ export default Vue.extend({ }, async fetchMore() { if (this.unreadCache && !this.$store.state.items.fetchingItems.unread) { - this.$store.dispatch(ACTIONS.FETCH_UNREAD, { start: this.unreadCache[this.unreadCache?.length - 1]?.id }) + this.$store.dispatch(ACTIONS.FETCH_UNREAD) } }, }, diff --git a/src/store/item.ts b/src/store/item.ts index f409305f1..3b7bf0726 100644 --- a/src/store/item.ts +++ b/src/store/item.ts @@ -17,7 +17,7 @@ export const FEED_ITEM_ACTION_TYPES = { export type ItemState = { fetchingItems: { [key: string]: boolean }; allItemsLoaded: { [key: string]: boolean }; - starredLoaded: boolean; + lastItemLoaded: { [key: string]: number }; starredCount: number; unreadCount: number; @@ -30,7 +30,7 @@ export type ItemState = { const state: ItemState = { fetchingItems: {}, allItemsLoaded: {}, - starredLoaded: false, + lastItemLoaded: {}, starredCount: 0, unreadCount: 0, @@ -55,18 +55,21 @@ export const actions = { async [FEED_ITEM_ACTION_TYPES.FETCH_UNREAD]({ commit }: ActionParams, { start }: { start: number } = { start: 0 }) { commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'unread', fetching: true }) - const response = await ItemService.debounceFetchUnread(start) + const response = await ItemService.debounceFetchUnread(state.lastItemLoaded.unread || start) commit(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, response?.data.items) if (response?.data.items.length < 40) { commit(FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED, { key: 'unread', loaded: true }) } + + const lastItem = response?.data.items[response?.data.items.length - 1].id + commit(FEED_ITEM_MUTATION_TYPES.SET_LAST_ITEM_LOADED, { key: 'unread', lastItem }) commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'unread', fetching: false }) }, async [FEED_ITEM_ACTION_TYPES.FETCH_STARRED]({ commit }: ActionParams, { start }: { start: number } = { start: 0 }) { commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'starred', fetching: true }) - const response = await ItemService.debounceFetchStarred(start) + const response = await ItemService.debounceFetchStarred(state.lastItemLoaded.starred || start) commit(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, response?.data.items) if (response?.data.starred) { @@ -76,16 +79,20 @@ export const actions = { if (response?.data.items.length < 40) { commit(FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED, { key: 'starred', loaded: true }) } + const lastItem = response?.data.items[response?.data.items.length - 1].id + commit(FEED_ITEM_MUTATION_TYPES.SET_LAST_ITEM_LOADED, { key: 'starred', lastItem }) commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'starred', fetching: false }) }, async [FEED_ITEM_ACTION_TYPES.FETCH_FEED_ITEMS]({ commit }: ActionParams, { feedId, start }: { feedId: number; start: number }) { commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'feed-' + feedId, fetching: true }) - const response = await ItemService.debounceFetchFeedItems(feedId, start) + const response = await ItemService.debounceFetchFeedItems(feedId, state.lastItemLoaded['feed-' + feedId] || start) commit(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, response?.data.items) if (response?.data.items.length < 40) { commit(FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED, { key: 'feed-' + feedId, loaded: true }) } + const lastItem = response?.data.items[response?.data.items.length - 1].id + commit(FEED_ITEM_MUTATION_TYPES.SET_LAST_ITEM_LOADED, { key: 'feed-' + feedId, lastItem }) commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'feed-' + feedId, fetching: false }) }, [FEED_ITEM_ACTION_TYPES.MARK_READ]({ commit }: ActionParams, { item }: { item: FeedItem}) { @@ -151,6 +158,9 @@ export const mutations = { [FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED](state: ItemState, { loaded, key }: { loaded: boolean; key: string; }) { state.allItemsLoaded[key] = loaded }, + [FEED_ITEM_MUTATION_TYPES.SET_LAST_ITEM_LOADED](state: ItemState, { lastItem, key }: { lastItem: number; key: string; }) { + state.lastItemLoaded[key] = lastItem + }, } export default { diff --git a/src/types/MutationTypes.ts b/src/types/MutationTypes.ts index 9d725c0c3..859c47f04 100644 --- a/src/types/MutationTypes.ts +++ b/src/types/MutationTypes.ts @@ -18,4 +18,5 @@ export const FEED_ITEM_MUTATION_TYPES = { SET_FETCHING: 'SET_FETCHING', SET_ALL_LOADED: 'SET_ALL_LOADED', + SET_LAST_ITEM_LOADED: 'SET_LAST_ITEM_LOADED', } diff --git a/tests/javascript/unit/components/FeedItemDisplay.spec.ts b/tests/javascript/unit/components/feed-display/FeedItemDisplay.spec.ts similarity index 93% rename from tests/javascript/unit/components/FeedItemDisplay.spec.ts rename to tests/javascript/unit/components/feed-display/FeedItemDisplay.spec.ts index 594ccf9c1..a75119414 100644 --- a/tests/javascript/unit/components/FeedItemDisplay.spec.ts +++ b/tests/javascript/unit/components/feed-display/FeedItemDisplay.spec.ts @@ -1,7 +1,7 @@ import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils' -import FeedItemDisplay from '../../../../src/components/FeedItemDisplay.vue' -import { ACTIONS, MUTATIONS } from '../../../../src/store' +import FeedItemDisplay from '../../../../../src/components/feed-display/FeedItemDisplay.vue' +import { ACTIONS, MUTATIONS } from '../../../../../src/store' describe('FeedItemDisplay.vue', () => { 'use strict' diff --git a/tests/javascript/unit/components/FeedItemDisplayList.spec.ts b/tests/javascript/unit/components/feed-display/FeedItemDisplayList.spec.ts similarity index 82% rename from tests/javascript/unit/components/FeedItemDisplayList.spec.ts rename to tests/javascript/unit/components/feed-display/FeedItemDisplayList.spec.ts index 2b1d7e455..cd11b7ea9 100644 --- a/tests/javascript/unit/components/FeedItemDisplayList.spec.ts +++ b/tests/javascript/unit/components/feed-display/FeedItemDisplayList.spec.ts @@ -1,9 +1,9 @@ import Vuex, { Store } from 'vuex' import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils' -import FeedItemDisplayList from '../../../../src/components/FeedItemDisplayList.vue' -import VirtualScroll from '../../../../src/components/VirtualScroll.vue' -import FeedItemRow from '../../../../src/components/FeedItemRow.vue' +import FeedItemDisplayList from '../../../../../src/components/feed-display/FeedItemDisplayList.vue' +import VirtualScroll from '../../../../../src/components/feed-display/VirtualScroll.vue' +import FeedItemRow from '../../../../../src/components/feed-display/FeedItemRow.vue' jest.mock('@nextcloud/axios') diff --git a/tests/javascript/unit/components/FeedItemRow.spec.ts b/tests/javascript/unit/components/feed-display/FeedItemRow.spec.ts similarity index 95% rename from tests/javascript/unit/components/FeedItemRow.spec.ts rename to tests/javascript/unit/components/feed-display/FeedItemRow.spec.ts index dde0106a9..d6aa2cd9d 100644 --- a/tests/javascript/unit/components/FeedItemRow.spec.ts +++ b/tests/javascript/unit/components/feed-display/FeedItemRow.spec.ts @@ -1,7 +1,7 @@ import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils' -import FeedItemRow from '../../../../src/components/FeedItemRow.vue' -import { ACTIONS } from '../../../../src/store' +import FeedItemRow from '../../../../../src/components/feed-display/FeedItemRow.vue' +import { ACTIONS } from '../../../../../src/store' describe('FeedItemRow.vue', () => { 'use strict' From 66b463f60267c30e968da0a28a233c7cda5c69fd Mon Sep 17 00:00:00 2001 From: Devlin Junker Date: Wed, 30 Aug 2023 22:39:52 -0700 Subject: [PATCH 2/3] add comments and remove start parameter Signed-off-by: Devlin Junker --- src/components/routes/Starred.vue | 2 +- src/store/item.ts | 71 +++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/components/routes/Starred.vue b/src/components/routes/Starred.vue index 243d42c5d..4dac06d87 100644 --- a/src/components/routes/Starred.vue +++ b/src/components/routes/Starred.vue @@ -43,7 +43,7 @@ export default Vue.extend({ methods: { async fetchMore() { if (!this.$store.state.items.fetchingItems.starred) { - this.$store.dispatch(ACTIONS.FETCH_STARRED, { start: this.$store.getters.starred[this.$store.getters.starred?.length - 1]?.id }) + this.$store.dispatch(ACTIONS.FETCH_STARRED) } }, }, diff --git a/src/store/item.ts b/src/store/item.ts index 3b7bf0726..fe8d5b6c8 100644 --- a/src/store/item.ts +++ b/src/store/item.ts @@ -52,10 +52,18 @@ const getters = { } export const actions = { + /** + * Fetch Unread Items from Backend and call commit to update state + * + * @param param0 ActionParams + * @param param0.commit + * @param param1 ActionArgs + * @param param1.start + */ async [FEED_ITEM_ACTION_TYPES.FETCH_UNREAD]({ commit }: ActionParams, { start }: { start: number } = { start: 0 }) { commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'unread', fetching: true }) - const response = await ItemService.debounceFetchUnread(state.lastItemLoaded.unread || start) + const response = await ItemService.debounceFetchUnread(start || state.lastItemLoaded.unread) commit(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, response?.data.items) @@ -67,9 +75,18 @@ export const actions = { commit(FEED_ITEM_MUTATION_TYPES.SET_LAST_ITEM_LOADED, { key: 'unread', lastItem }) commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'unread', fetching: false }) }, + + /** + * Fetch Starred Items from Backend and call commit to update state + * + * @param param0 ActionParams + * @param param0.commit + * @param param1 ActionArgs + * @param param1.start + */ async [FEED_ITEM_ACTION_TYPES.FETCH_STARRED]({ commit }: ActionParams, { start }: { start: number } = { start: 0 }) { commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'starred', fetching: true }) - const response = await ItemService.debounceFetchStarred(state.lastItemLoaded.starred || start) + const response = await ItemService.debounceFetchStarred(start || state.lastItemLoaded.starred) commit(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, response?.data.items) if (response?.data.starred) { @@ -83,9 +100,19 @@ export const actions = { commit(FEED_ITEM_MUTATION_TYPES.SET_LAST_ITEM_LOADED, { key: 'starred', lastItem }) commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'starred', fetching: false }) }, + + /** + * Fetch All Feed Items from Backend and call commit to update state + * + * @param param0 ActionParams + * @param param0.commit + * @param param1 ActionArgs + * @param param1.start + * @param param1.feedId + */ async [FEED_ITEM_ACTION_TYPES.FETCH_FEED_ITEMS]({ commit }: ActionParams, { feedId, start }: { feedId: number; start: number }) { commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'feed-' + feedId, fetching: true }) - const response = await ItemService.debounceFetchFeedItems(feedId, state.lastItemLoaded['feed-' + feedId] || start) + const response = await ItemService.debounceFetchFeedItems(feedId, start || state.lastItemLoaded['feed-' + feedId]) commit(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, response?.data.items) if (response?.data.items.length < 40) { @@ -95,7 +122,16 @@ export const actions = { commit(FEED_ITEM_MUTATION_TYPES.SET_LAST_ITEM_LOADED, { key: 'feed-' + feedId, lastItem }) commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'feed-' + feedId, fetching: false }) }, - [FEED_ITEM_ACTION_TYPES.MARK_READ]({ commit }: ActionParams, { item }: { item: FeedItem}) { + + /** + * Sends message to Backend to mark as read, and then call commit to update state + * + * @param param0 ActionParams + * @param param0.commit + * @param param1 ActionArgs + * @param param1.item + */ + [FEED_ITEM_ACTION_TYPES.MARK_READ]({ commit }: ActionParams, { item }: { item: FeedItem }) { ItemService.markRead(item, true) if (item.unread) { @@ -104,6 +140,15 @@ export const actions = { item.unread = false commit(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item }) }, + + /** + * Sends message to Backend to mark as unread, and then call commit to update state + * + * @param param0 ActionParams + * @param param0.commit + * @param param1 ActionArgs + * @param param1.item + */ [FEED_ITEM_ACTION_TYPES.MARK_UNREAD]({ commit }: ActionParams, { item }: { item: FeedItem}) { ItemService.markRead(item, false) @@ -113,6 +158,15 @@ export const actions = { item.unread = true commit(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item }) }, + + /** + * Sends message to Backend to mark as starred, and then call commit to update state + * + * @param param0 ActionParams + * @param param0.commit + * @param param1 ActionArgs + * @param param1.item + */ [FEED_ITEM_ACTION_TYPES.STAR_ITEM]({ commit }: ActionParams, { item }: { item: FeedItem}) { ItemService.markStarred(item, true) @@ -120,6 +174,15 @@ export const actions = { commit(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item }) commit(FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT, state.starredCount + 1) }, + + /** + * Sends message to Backend to remove mark as starred, and then call commit to update state + * + * @param param0 ActionParams + * @param param0.commit + * @param param1 ActionArgs + * @param param1.item + */ [FEED_ITEM_ACTION_TYPES.UNSTAR_ITEM]({ commit }: ActionParams, { item }: { item: FeedItem}) { ItemService.markStarred(item, false) From 27dd4ca510b2be680dadc79df4f32a8a96c83336 Mon Sep 17 00:00:00 2001 From: Devlin Junker Date: Thu, 31 Aug 2023 18:36:20 -0700 Subject: [PATCH 3/3] move backend call details to services Signed-off-by: Devlin Junker --- src/dataservices/feed.service.ts | 39 ++++++++++++++++ src/dataservices/folder.service.ts | 39 ++++++++++++++++ src/store/feed.ts | 29 ++++-------- src/store/folder.ts | 10 ++--- .../unit/components/routes/Feed.spec.ts | 2 +- .../unit/components/routes/Starred.spec.ts | 2 +- .../unit/components/routes/Unread.spec.ts | 2 +- .../unit/services/feed.service.spec.ts | 34 ++++++++++++++ .../unit/services/folder.service.spec.ts | 45 +++++++++++++++++++ tests/javascript/unit/store/feed.spec.ts | 25 ++++++----- tests/javascript/unit/store/folder.spec.ts | 26 +++++------ tests/javascript/unit/store/item.spec.ts | 8 ++-- 12 files changed, 203 insertions(+), 58 deletions(-) create mode 100644 src/dataservices/feed.service.ts create mode 100644 src/dataservices/folder.service.ts create mode 100644 tests/javascript/unit/services/feed.service.spec.ts create mode 100644 tests/javascript/unit/services/folder.service.spec.ts diff --git a/src/dataservices/feed.service.ts b/src/dataservices/feed.service.ts new file mode 100644 index 000000000..f5ac032b5 --- /dev/null +++ b/src/dataservices/feed.service.ts @@ -0,0 +1,39 @@ +import { AxiosResponse } from 'axios' +import axios from '@nextcloud/axios' + +import { API_ROUTES } from '../types/ApiRoutes' + +export class FeedService { + + /** + * Retrieves all Feed info from the Nextcloud News backend + * + * @return {AxiosResponse} Feed info stored in array property data.feeds + */ + static fetchAllFeeds(): Promise { + return axios.get(API_ROUTES.FEED) + } + + /** + * Attempts to add a feed to the Nextcloud News backend + * NOTE: this can fail if feed URL is not resolvable + * + * @param param0 + * @param param0.url {String} url of the feed to add + * @param param0.folderId {number} id number of folder to add feed to + * @param param0.user {String} http auth username required for accessing feed + * @param param0.password {String} http auth password required for accessing feed + * @return {AxiosResponse} Feed info stored in data.feeds[0] property + */ + static addFeed({ url, folderId, user, password }: { url: string; folderId: number; user?: string; password?: string }): Promise { + return axios.post(API_ROUTES.FEED, { + url, + parentFolderId: folderId, + title: null, // TODO: let user define feed title on create? + user: user || null, + password: password || null, + fullDiscover: undefined, // TODO: autodiscover? + }) + } + +} diff --git a/src/dataservices/folder.service.ts b/src/dataservices/folder.service.ts new file mode 100644 index 000000000..45db7d271 --- /dev/null +++ b/src/dataservices/folder.service.ts @@ -0,0 +1,39 @@ +import { AxiosResponse } from 'axios' +import axios from '@nextcloud/axios' + +import { API_ROUTES } from '../types/ApiRoutes' + +export class FolderService { + + /** + * Retrieves all of the folders from the Nextcloud News backend + * + * @return {AxiosResponse} Folders contained in data.folders property + */ + static fetchAllFolders(): Promise { + return axios.get(API_ROUTES.FOLDER) + } + + /** + * Creates a new Folder in the Nextcloud News backend + * + * @param param0 + * @param param0.name {String} New Folder Name + * @return {AxiosResponse} Folder info from backend in data.folders[0] property + */ + static createFolder({ name }: { name: string }): Promise { + return axios.post(API_ROUTES.FOLDER, { folderName: name }) + } + + /** + * Deletes a folder in the Nextcloud News backend (by id number) + * + * @param param0 + * @param param0.id {number} id of folder to delete + * @return {AxiosResponse} + */ + static deleteFolder({ id }: { id: number }): Promise { + return axios.delete(API_ROUTES.FOLDER + '/' + id) + } + +} diff --git a/src/store/feed.ts b/src/store/feed.ts index d86604266..b66fff376 100644 --- a/src/store/feed.ts +++ b/src/store/feed.ts @@ -1,9 +1,8 @@ -import axios from '@nextcloud/axios' - import { ActionParams, AppState } from '../store' import { Feed } from '../types/Feed' -import { API_ROUTES } from '../types/ApiRoutes' import { FOLDER_MUTATION_TYPES, FEED_MUTATION_TYPES, FEED_ITEM_MUTATION_TYPES } from '../types/MutationTypes' +import { FolderService } from '../dataservices/folder.service' +import { FeedService } from '../dataservices/feed.service' export const FEED_ACTION_TYPES = { ADD_FEED: 'ADD_FEED', @@ -22,7 +21,7 @@ const getters = { export const actions = { async [FEED_ACTION_TYPES.FETCH_FEEDS]({ commit }: ActionParams) { - const feeds = await axios.get(API_ROUTES.FEED) + const feeds = await FeedService.fetchAllFeeds() commit(FEED_MUTATION_TYPES.SET_FEEDS, feeds.data.feeds) commit(FEED_ITEM_MUTATION_TYPES.SET_UNREAD_COUNT, (feeds.data.feeds.reduce((total: number, feed: Feed) => { @@ -47,30 +46,20 @@ export const actions = { let folderId if (feedReq.folder?.id === undefined && feedReq.folder?.name && feedReq.folder?.name !== '') { - const response = await axios.post(API_ROUTES.FOLDER, { folderName: feedReq.folder?.name }) + const response = await FolderService.createFolder({ name: feedReq.folder.name }) folderId = response.data.folders[0].id commit(FOLDER_MUTATION_TYPES.SET_FOLDERS, response.data.folders) } else { folderId = feedReq.folder?.id || 0 } - const feed: Feed = { - url, - folderId, - title: undefined, // TODO: let user define feed title on create? - unreadCount: 0, - autoDiscover: undefined, // TODO: autodiscover? - } - // Check that url is resolvable try { - const response = await axios.post(API_ROUTES.FEED, { - url: feed.url, - parentFolderId: feed.folderId, - title: null, - user: feedReq.user ? feedReq.user : null, - password: feedReq.password ? feedReq.password : null, - fullDiscover: feed.autoDiscover, + const response = await FeedService.addFeed({ + url, + folderId, + user: feedReq.user, + password: feedReq.password, }) commit(FEED_MUTATION_TYPES.ADD_FEED, response.data.feeds[0]) diff --git a/src/store/folder.ts b/src/store/folder.ts index bf8bc0922..760ff39c6 100644 --- a/src/store/folder.ts +++ b/src/store/folder.ts @@ -1,10 +1,8 @@ -import axios from '@nextcloud/axios' - import { AppState, ActionParams } from '../store' import { Folder } from '../types/Folder' import { Feed } from '../types/Feed' import { FOLDER_MUTATION_TYPES, FEED_MUTATION_TYPES } from '../types/MutationTypes' -import { API_ROUTES } from '../types/ApiRoutes' +import { FolderService } from '../dataservices/folder.service' export const FOLDER_ACTION_TYPES = { FETCH_FOLDERS: 'FETCH_FOLDERS', @@ -24,12 +22,12 @@ const getters = { export const actions = { async [FOLDER_ACTION_TYPES.FETCH_FOLDERS]({ commit }: ActionParams) { - const folders = await axios.get(API_ROUTES.FOLDER) + const folders = await FolderService.fetchAllFolders() commit(FOLDER_MUTATION_TYPES.SET_FOLDERS, folders.data.folders) }, async [FOLDER_ACTION_TYPES.ADD_FOLDERS]({ commit }: ActionParams, { folder }: { folder: Folder}) { - const response = await axios.post(API_ROUTES.FOLDER, { folderName: folder.name }) + const response = await FolderService.createFolder({ name: folder.name }) commit(FOLDER_MUTATION_TYPES.SET_FOLDERS, response.data.folders) }, async [FOLDER_ACTION_TYPES.DELETE_FOLDER]({ commit }: ActionParams, { folder }: { folder: Folder}) { @@ -39,7 +37,7 @@ export const actions = { promises.push(self.reversiblyDelete(feed.id, false, true)); }); */ - await axios.delete(API_ROUTES.FOLDER + '/' + folder.id) + await FolderService.deleteFolder({ id: folder.id }) commit(FOLDER_MUTATION_TYPES.DELETE_FOLDER, folder) }, } diff --git a/tests/javascript/unit/components/routes/Feed.spec.ts b/tests/javascript/unit/components/routes/Feed.spec.ts index 4e0e69f2b..dca9a02e2 100644 --- a/tests/javascript/unit/components/routes/Feed.spec.ts +++ b/tests/javascript/unit/components/routes/Feed.spec.ts @@ -2,7 +2,7 @@ import Vuex, { Store } from 'vuex' import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils' import Feed from '../../../../../src/components/routes/Feed.vue' -import FeedItemDisplayList from '../../../../../src/components/FeedItemDisplayList.vue' +import FeedItemDisplayList from '../../../../../src/components/feed-display/FeedItemDisplayList.vue' jest.mock('@nextcloud/axios') diff --git a/tests/javascript/unit/components/routes/Starred.spec.ts b/tests/javascript/unit/components/routes/Starred.spec.ts index 44c9acd75..cf77cbef2 100644 --- a/tests/javascript/unit/components/routes/Starred.spec.ts +++ b/tests/javascript/unit/components/routes/Starred.spec.ts @@ -2,7 +2,7 @@ import Vuex, { Store } from 'vuex' import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils' import Starred from '../../../../../src/components/routes/Starred.vue' -import FeedItemDisplayList from '../../../../../src/components/FeedItemDisplayList.vue' +import FeedItemDisplayList from '../../../../../src/components/feed-display/FeedItemDisplayList.vue' jest.mock('@nextcloud/axios') diff --git a/tests/javascript/unit/components/routes/Unread.spec.ts b/tests/javascript/unit/components/routes/Unread.spec.ts index 149fc015f..78f741199 100644 --- a/tests/javascript/unit/components/routes/Unread.spec.ts +++ b/tests/javascript/unit/components/routes/Unread.spec.ts @@ -2,7 +2,7 @@ import Vuex, { Store } from 'vuex' import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils' import Unread from '../../../../../src/components/routes/Unread.vue' -import FeedItemDisplayList from '../../../../../src/components/FeedItemDisplayList.vue' +import FeedItemDisplayList from '../../../../../src/components/feed-display/FeedItemDisplayList.vue' jest.mock('@nextcloud/axios') diff --git a/tests/javascript/unit/services/feed.service.spec.ts b/tests/javascript/unit/services/feed.service.spec.ts new file mode 100644 index 000000000..5f0c058cd --- /dev/null +++ b/tests/javascript/unit/services/feed.service.spec.ts @@ -0,0 +1,34 @@ +import { FeedService } from './../../../../src/dataservices/feed.service' +import axios from '@nextcloud/axios' + +jest.mock('@nextcloud/axios') + +describe('feed.service.ts', () => { + 'use strict' + + beforeEach(() => { + (axios.get as any).mockReset(); + (axios.post as any).mockReset() + }) + + describe('fetchAllFeeds', () => { + it('should call GET to retrieve all feeds', async () => { + (axios as any).get.mockResolvedValue({ data: { feeds: [] } }) + + await FeedService.fetchAllFeeds() + + expect(axios.get).toBeCalled() + }) + }) + + describe('addFeed', () => { + it('should call POST with item id in URL and read param', async () => { + await FeedService.addFeed({ url: 'http://example.com', folderId: 0 }) + + expect(axios.post).toBeCalled() + const args = (axios.post as any).mock.calls[0] + + expect(args[1].url).toEqual('http://example.com') + }) + }) +}) diff --git a/tests/javascript/unit/services/folder.service.spec.ts b/tests/javascript/unit/services/folder.service.spec.ts new file mode 100644 index 000000000..6577df2c8 --- /dev/null +++ b/tests/javascript/unit/services/folder.service.spec.ts @@ -0,0 +1,45 @@ +import { FolderService } from './../../../../src/dataservices/folder.service' +import axios from '@nextcloud/axios' + +jest.mock('@nextcloud/axios') + +describe('folder.service.ts', () => { + 'use strict' + + beforeEach(() => { + (axios.get as any).mockReset(); + (axios.post as any).mockReset() + }) + + describe('fetchAllFolders', () => { + it('should call GET to retrieve all folders', async () => { + (axios as any).get.mockResolvedValue({ data: { feeds: [] } }) + + await FolderService.fetchAllFolders() + + expect(axios.get).toBeCalled() + }) + }) + + describe('createFolder', () => { + it('should call POST with item id in URL and read param', async () => { + await FolderService.createFolder({ name: 'abc' }) + + expect(axios.post).toBeCalled() + const args = (axios.post as any).mock.calls[0] + + expect(args[1].folderName).toEqual('abc') + }) + }) + + describe('deleteFolder', () => { + it('should call POST with item id in URL and read param', async () => { + await FolderService.deleteFolder({ id: 123 }) + + expect(axios.delete).toBeCalled() + const args = (axios.delete as any).mock.calls[0] + + expect(args[0]).toContain('123') + }) + }) +}) diff --git a/tests/javascript/unit/store/feed.spec.ts b/tests/javascript/unit/store/feed.spec.ts index 3a7521f89..451d04d7a 100644 --- a/tests/javascript/unit/store/feed.spec.ts +++ b/tests/javascript/unit/store/feed.spec.ts @@ -1,41 +1,42 @@ -import axios from '@nextcloud/axios' import { Feed } from '../../../../src/types/Feed' import { AppState } from '../../../../src/store' import { FEED_ACTION_TYPES, mutations, actions } from '../../../../src/store/feed' +import { FeedService } from '../../../../src/dataservices/feed.service' import { FEED_ITEM_MUTATION_TYPES, FEED_MUTATION_TYPES } from '../../../../src/types/MutationTypes' -jest.mock('@nextcloud/axios') - describe('feed.ts', () => { 'use strict' describe('actions', () => { describe('FETCH_FEEDS', () => { - it('should call GET and commit returned feeds to state', async () => { - (axios as any).get.mockResolvedValue({ data: { feeds: [] } }) + it('should call FeedService.fetchAllFeeds and commit returned feeds to state', async () => { + FeedService.fetchAllFeeds = jest.fn(); + (FeedService.fetchAllFeeds as any).mockResolvedValue({ data: { feeds: [] } }) const commit = jest.fn() await (actions[FEED_ACTION_TYPES.FETCH_FEEDS] as any)({ commit }) - expect(axios.get).toBeCalled() + expect(FeedService.fetchAllFeeds).toBeCalled() expect(commit).toBeCalledWith(FEED_MUTATION_TYPES.SET_FEEDS, []) expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.SET_UNREAD_COUNT, 0) }) }) describe('ADD_FEED', () => { - it('should call POST and commit feed to state', async () => { - (axios as any).post.mockResolvedValue({ data: { feeds: [] } }) + it('should call FeedService.addF and commit feed to state', async () => { + FeedService.addFeed = jest.fn(); + (FeedService.addFeed as any).mockResolvedValue({ data: { feeds: [] } }) const commit = jest.fn() await actions[FEED_ACTION_TYPES.ADD_FEED]({ commit }, { feedReq: { url: '' } }) - expect(axios.post).toBeCalled() + expect(FeedService.addFeed).toBeCalled() expect(commit).toBeCalled() }) - it('should call POST and not call commit if error', async () => { - (axios as any).post.mockRejectedValue() + it('should call FeedService.addF and not call commit if error', async () => { + FeedService.addFeed = jest.fn(); + (FeedService.addFeed as any).mockRejectedValue() const commit = jest.fn() await actions[FEED_ACTION_TYPES.ADD_FEED]({ commit }, { feedReq: { url: '' } }) - expect(axios.post).toBeCalled() + expect(FeedService.addFeed).toBeCalled() expect(commit).not.toBeCalled() }) diff --git a/tests/javascript/unit/store/folder.spec.ts b/tests/javascript/unit/store/folder.spec.ts index eacfd18cb..8fd2ad34f 100644 --- a/tests/javascript/unit/store/folder.spec.ts +++ b/tests/javascript/unit/store/folder.spec.ts @@ -1,47 +1,47 @@ -import axios from '@nextcloud/axios' -import { generateUrl } from '@nextcloud/router' import { Folder } from '../../../../src/types/Folder' import { AppState } from '../../../../src/store' import { FOLDER_ACTION_TYPES, mutations, actions } from '../../../../src/store/folder' import { FOLDER_MUTATION_TYPES } from '../../../../src/types/MutationTypes' +import { FolderService } from '../../../../src/dataservices/folder.service' -jest.mock('@nextcloud/axios') jest.mock('@nextcloud/router') describe('folder.ts', () => { 'use strict' describe('actions', () => { - it('FETCH_FOLDERS should send GET and then commit folders returned to state', async () => { - (generateUrl as any).mockReturnValue(''); - (axios.get as any).mockResolvedValue({ data: { folders: [] } }) + it('FETCH_FOLDERS should call FolderService.fetchAllFolders and then commit folders returned to state', async () => { + FolderService.fetchAllFolders = jest.fn(); + (FolderService.fetchAllFolders as any).mockResolvedValue({ data: { folders: [] } }) const commit = jest.fn() await (actions[FOLDER_ACTION_TYPES.FETCH_FOLDERS] as any)({ commit }) - expect(axios.get).toBeCalled() + expect(FolderService.fetchAllFolders).toBeCalled() expect(commit).toBeCalled() }) - it('ADD_FOLDERS should send POST and then commit the folders returned to state', async () => { - (axios.post as any).mockResolvedValue({ data: { folders: [] } }) + it('ADD_FOLDERS should call FolderService.createFolder and then commit the folders returned to state', async () => { + FolderService.createFolder = jest.fn(); + (FolderService.createFolder as any).mockResolvedValue({ data: { folders: [] } }) const folder = {} as Folder const commit = jest.fn() await actions[FOLDER_ACTION_TYPES.ADD_FOLDERS]({ commit }, { folder }) - expect(axios.post).toBeCalled() + expect(FolderService.createFolder).toBeCalled() expect(commit).toBeCalled() }) - it('DELETE_FOLDER should send DELETE and then commit deleted folder to state', async () => { - (axios.delete as any).mockResolvedValue() + it('DELETE_FOLDER should call FolderService.deleteFolder and then commit deleted folder to state', async () => { + FolderService.deleteFolder = jest.fn(); + (FolderService.deleteFolder as any).mockResolvedValue() const folder = {} as Folder const commit = jest.fn() await actions[FOLDER_ACTION_TYPES.DELETE_FOLDER]({ commit }, { folder }) - expect(axios.delete).toBeCalled() + expect(FolderService.deleteFolder).toBeCalled() expect(commit).toBeCalled() }) }) diff --git a/tests/javascript/unit/store/item.spec.ts b/tests/javascript/unit/store/item.spec.ts index 84869c4d8..7866cff5b 100644 --- a/tests/javascript/unit/store/item.spec.ts +++ b/tests/javascript/unit/store/item.spec.ts @@ -11,28 +11,28 @@ describe('item.ts', () => { describe('FETCH_UNREAD', () => { it('should call ItemService and commit items to state', async () => { const fetchMock = jest.fn() - fetchMock.mockResolvedValue({ data: { items: [] } }) + fetchMock.mockResolvedValue({ data: { items: [{ id: 123 }] } }) ItemService.debounceFetchUnread = fetchMock as any const commit = jest.fn() await (actions[FEED_ITEM_ACTION_TYPES.FETCH_UNREAD] as any)({ commit }) expect(fetchMock).toBeCalled() - expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, []) + expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, [{ id: 123 }]) }) }) describe('FETCH_STARRED', () => { it('should call ItemService and commit items and starred count to state', async () => { const fetchMock = jest.fn() - fetchMock.mockResolvedValue({ data: { items: [], starred: 3 } }) + fetchMock.mockResolvedValue({ data: { items: [{ id: 123 }], starred: 3 } }) ItemService.debounceFetchStarred = fetchMock as any const commit = jest.fn() await (actions[FEED_ITEM_ACTION_TYPES.FETCH_STARRED] as any)({ commit }) expect(fetchMock).toBeCalled() - expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, []) + expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, [{ id: 123 }]) expect(commit).toBeCalledWith(FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT, 3) }) })