/* * Copyright (c) 2020. The Nextcloud Bookmarks contributors. * * This file is licensed under the Affero General Public License version 3 or later. See the COPYING file. */ import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import { loadState } from '@nextcloud/initial-state' import AppGlobal from '../mixins/AppGlobal.js' import { mutations } from './mutations.js' import * as Parallel from 'async-parallel' import uniq from 'lodash/uniq.js' import difference from 'lodash/difference.js' const BATCH_SIZE = 42 export const actions = { ADD_ALL_BOOKMARKS: 'ADD_ALL_BOOKMARKS', COUNT_BOOKMARKS: 'COUNT_BOOKMARKS', COUNT_UNAVAILABLE: 'COUNT_UNAVAILABLE', COUNT_ARCHIVED: 'COUNT_ARCHIVED', COUNT_DUPLICATED: 'COUNT_DUPLICATED', CREATE_BOOKMARK: 'CREATE_BOOKMARK', FIND_BOOKMARK: 'FIND_BOOKMARK', LOAD_BOOKMARK: 'LOAD_BOOKMARK', DELETE_BOOKMARK: 'DELETE_BOOKMARK', OPEN_BOOKMARK: 'OPEN_BOOKMARK', SAVE_BOOKMARK: 'SAVE_BOOKMARK', MOVE_BOOKMARK: 'MOVE_BOOKMARK', COPY_BOOKMARK: 'COPY_BOOKMARK', CLICK_BOOKMARK: 'CLICK_BOOKMARK', IMPORT_BOOKMARKS: 'IMPORT_BOOKMARKS', DELETE_BOOKMARKS: 'DELETE_BOOKMARKS', LOAD_TAGS: 'LOAD_TAGS', RENAME_TAG: 'RENAME_TAG', DELETE_TAG: 'DELETE_TAG', LOAD_FOLDERS: 'LOAD_FOLDERS', CREATE_FOLDER: 'CREATE_FOLDER', SAVE_FOLDER: 'SAVE_FOLDER', DELETE_FOLDER: 'DELETE_FOLDER', LOAD_FOLDER_CHILDREN_ORDER: 'LOAD_FOLDER_CHILDREN_ORDER', OPEN_FOLDER_DETAILS: 'OPEN_FOLDER_DETAILS', OPEN_FOLDER_SHARING: 'OPEN_FOLDER_SHARING', MOVE_SELECTION: 'MOVE_SELECTION', COPY_SELECTION: 'COPY_SELECTION', DELETE_SELECTION: 'DELETE_SELECTION', TAG_SELECTION: 'TAG_SELECTION', RELOAD_VIEW: 'RELOAD_VIEW', NO_FILTER: 'NO_FILTER', FILTER_BY_RECENT: 'FILTER_BY_RECENT', FILTER_BY_FREQUENT: 'FILTER_BY_FREQUENT', FILTER_BY_UNTAGGED: 'FILTER_BY_UNTAGGED', FILTER_BY_UNAVAILABLE: 'FILTER_BY_UNAVAILABLE', FILTER_BY_ARCHIVED: 'FILTER_BY_ARCHIVED', FILTER_BY_DUPLICATED: 'FILTER_BY_DUPLICATED', FILTER_BY_TAGS: 'FILTER_BY_TAGS', FILTER_BY_FOLDER: 'FILTER_BY_FOLDER', FILTER_BY_SHARED_FOLDERS: 'FILTER_BY_SHARED_FOLDERS', FILTER_BY_SEARCH: 'FILTER_BY_SEARCH', FETCH_PAGE: 'FETCH_PAGE', FETCH_ALL: 'FETCH_ALL', SET_SETTING: 'SET_SETTING', LOAD_SETTING: 'LOAD_SETTING', LOAD_SETTINGS: 'LOAD_SETTINGS', LOAD_SHARES: 'LOAD_SHARES', LOAD_SHARES_OF_FOLDER: 'LOAD_SHARES_OF_FOLDER', CREATE_SHARE: 'CREATE_SHARE', EDIT_SHARE: 'EDIT_SHARE', DELETE_SHARE: 'DELETE_SHARE', LOAD_PUBLIC_LINK: 'LOAD_PUBLIC_LINK', CREATE_PUBLIC_LINK: 'CREATE_PUBLIC_LINK', DELETE_PUBLIC_LINK: 'DELETE_PUBLIC_LINK', LOAD_SHARED_FOLDERS: 'LOAD_SHARED_FOLDERS', } export default { [actions.ADD_ALL_BOOKMARKS]({ commit }, bookmarks) { for (const bookmark of bookmarks) { commit(mutations.ADD_BOOKMARK, bookmark) } }, async [actions.COUNT_UNAVAILABLE]({ commit, dispatch, state }, folderId) { try { const response = await axios.get( url(state, '/bookmark/unavailable') ) const { data: { item: count, data, status }, } = response if (status !== 'success') { throw new Error(data) } commit(mutations.SET_UNAVAILABLE_COUNT, count) } catch (err) { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to count unavailable bookmarks' ) ) throw err } }, async [actions.COUNT_ARCHIVED]({ commit, dispatch, state }, folderId) { try { const response = await axios.get(url(state, '/bookmark/archived')) const { data: { item: count, data, status }, } = response if (status !== 'success') { throw new Error(data) } commit(mutations.SET_ARCHIVED_COUNT, count) } catch (err) { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to count archived bookmarks' ) ) throw err } }, async [actions.COUNT_DUPLICATED]({ commit, dispatch, state }) { try { const response = await axios.get(url(state, '/bookmark/duplicated')) const { data: { item: count, data, status }, } = response if (status !== 'success') { throw new Error(data) } commit(mutations.SET_DUPLICATED_COUNT, count) } catch (err) { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to count duplicated bookmarks' ) ) throw err } }, async [actions.COUNT_BOOKMARKS]({ commit, dispatch, state }, folderId) { try { const response = await axios.get( url(state, `/folder/${folderId}/count`) ) const { data: { item: count, data, status }, } = response if (status !== 'success') { throw new Error(data) } commit(mutations.SET_BOOKMARK_COUNT, { folderId, count }) } catch (err) { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to count bookmarks') ) throw err } }, async [actions.LOAD_BOOKMARK]({ commit, dispatch, state }, id) { try { const response = await axios.get(url(state, `/bookmark/${id}`)) const { data: { item: bookmark, status }, } = response if (status !== 'success') { throw new Error(response.data) } commit(mutations.ADD_BOOKMARK, bookmark) return bookmark } catch (err) { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to load bookmark') ) throw err } }, async [actions.FIND_BOOKMARK]({ commit, dispatch, state }, link) { if (state.loading.bookmarks) return try { const response = await axios.get(url(state, '/bookmark'), { params: { url: link, }, }) const { data: { data: bookmarks, status }, } = response if (status !== 'success') { throw new Error(response.data) } if (!bookmarks.length) return commit(mutations.ADD_BOOKMARK, bookmarks[0]) return bookmarks[0] } catch (err) { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to find existing bookmark' ) ) throw err } }, async [actions.CREATE_BOOKMARK]({ commit, dispatch, state }, data) { if (state.loading.bookmarks) return commit(mutations.FETCH_START, { type: 'createBookmark' }) commit(mutations.DISPLAY_NEW_BOOKMARK, false) // Insert a dummy bookmark const currentTimestamp = Math.round(Date.now() / 1000) const prelimBookmark = { id: 'preliminary-' + Math.random(), title: data.url, folders: [-1], tags: [], added: currentTimestamp, lastmodified: currentTimestamp, clickcount: 0, ...data, preliminary: true, } commit(mutations.ADD_BOOKMARK, prelimBookmark) commit(mutations.SORT_BOOKMARKS, state.settings.sorting) if (data.folders) { for (const folderId of data.folders) { commit(mutations.SET_FOLDER_CHILDREN_ORDER, { folderId, children: [...this.getters.getFolderChildren(folderId), { type: 'bookmark', id: prelimBookmark.id }], }) } } else { commit(mutations.SET_FOLDER_CHILDREN_ORDER, { folderId: -1, children: [...this.getters.getFolderChildren(-1), { type: 'bookmark', id: prelimBookmark.id }], }) } try { const response = await axios.post(url(state, '/bookmark'), { url: data.url, title: data.title, description: data.description, folders: data.folders && data.folders.map(parseInt), tags: data.tags, }) const { data: { item: bookmark, status }, } = response if (status !== 'success') { throw new Error(response.data.data.join('\n')) } commit(mutations.FETCH_END, 'createBookmark') commit(mutations.REMOVE_BOOKMARK, prelimBookmark.id) commit(mutations.ADD_BOOKMARK, bookmark) commit(mutations.SORT_BOOKMARKS, state.settings.sorting) // Update other displays commit(mutations.SET_BOOKMARK_COUNT, { folderId: -1, count: state.countsByFolder[-1] + 1, }) if (data.folders) { for (const folderId of data.folders) { commit(mutations.SET_FOLDER_CHILDREN_ORDER, { folderId, children: [...this.getters.getFolderChildren(folderId), { type: 'bookmark', id: bookmark.id }], }) commit(mutations.SET_BOOKMARK_COUNT, { folderId, count: state.countsByFolder[folderId] + 1, }) dispatch(actions.LOAD_FOLDER_CHILDREN_ORDER, folderId) } } else { commit(mutations.SET_FOLDER_CHILDREN_ORDER, { folderId: -1, children: [...this.getters.getFolderChildren(-1), { type: 'bookmark', id: bookmark.id }], }) dispatch(actions.LOAD_FOLDER_CHILDREN_ORDER, -1) } } catch (err) { console.error(err) commit(mutations.FETCH_END, 'createBookmark') commit(mutations.REMOVE_BOOKMARK, prelimBookmark.id) commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to create bookmark') ) throw err } }, async [actions.SAVE_BOOKMARK]({ commit, dispatch, state }, id) { commit(mutations.FETCH_START, { type: 'saveBookmark' }) try { const response = await axios.put( url(state, `/bookmark/${id}`), this.getters.getBookmark(id) ) const { data: { status }, } = response if (status !== 'success') { throw new Error(response.data) } commit(mutations.FETCH_END, 'saveBookmark') } catch (err) { console.error(err) commit(mutations.FETCH_END, 'saveBookmark') commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to save bookmark') ) throw err } }, async [actions.MOVE_BOOKMARK]( { commit, dispatch, state }, { bookmark, oldFolder, newFolder } ) { if (Number(oldFolder) === Number(newFolder)) { return } commit(mutations.FETCH_START, { type: 'moveBookmark' }) try { const response = await axios.post( url(state, `/folder/${newFolder}/bookmarks/${bookmark}`) ) if (response.data.status !== 'success') { throw new Error(response.data) } const response2 = await axios.delete( url(state, `/folder/${oldFolder}/bookmarks/${bookmark}`) ) if (response2.data.status !== 'success') { throw new Error(response2.data) } commit(mutations.FETCH_END, 'moveBookmark') dispatch(actions.LOAD_FOLDER_CHILDREN_ORDER, oldFolder) dispatch(actions.LOAD_FOLDER_CHILDREN_ORDER, newFolder) } catch (err) { console.error(err) commit(mutations.FETCH_END, 'moveBookmark') commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to move bookmark') ) throw err } }, async [actions.COPY_BOOKMARK]( { commit, dispatch, state }, { bookmark, oldFolder, newFolder } ) { if (Number(oldFolder) === Number(newFolder)) { return } commit(mutations.FETCH_START, { type: 'copyBookmark' }) try { const response = await axios.post( url(state, `/folder/${newFolder}/bookmarks/${bookmark}`) ) if (response.data.status !== 'success') { throw new Error(response.data) } commit(mutations.FETCH_END, 'copyBookmark') dispatch(actions.LOAD_FOLDER_CHILDREN_ORDER, newFolder) } catch (err) { console.error(err) commit(mutations.FETCH_END, 'copyBookmark') commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to copy bookmark') ) throw err } }, async [actions.CLICK_BOOKMARK]({ commit, dispatch, state }, bookmark) { commit(mutations.FETCH_START, { type: 'clickBookmark' }) try { const response = await axios.post(url(state, '/bookmark/click'), { url: bookmark.url, }) if (response.data.status !== 'success') { throw new Error(response.data) } commit(mutations.FETCH_END, 'clickBookmark') } catch (err) { console.error(err) commit(mutations.FETCH_END, 'clickBookmark') // Don't bother the user } }, [actions.OPEN_BOOKMARK]({ commit }, id) { commit(mutations.SET_SIDEBAR, { type: 'bookmark', id }) }, async [actions.DELETE_BOOKMARK]( { commit, dispatch, state }, { id, folder, avoidReload } ) { if (folder) { try { const response = await axios.delete( url(state, `/folder/${folder}/bookmarks/${id}`) ) if (response.data.status !== 'success') { throw new Error(response.data) } commit(mutations.REMOVE_BOOKMARK, id) if (!avoidReload) { await dispatch(actions.COUNT_BOOKMARKS, -1) await dispatch(actions.LOAD_FOLDER_CHILDREN_ORDER, folder) } } catch (err) { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to delete bookmark' ) ) throw err } return } try { const response = await axios.delete(url(state, `/bookmark/${id}`)) if (response.data.status !== 'success') { throw new Error(response.data) } await dispatch(actions.COUNT_BOOKMARKS, -1) await commit(mutations.REMOVE_BOOKMARK, id) } catch (err) { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to delete bookmark') ) throw err } }, async [actions.IMPORT_BOOKMARKS]( { commit, dispatch, state }, { file, folder } ) { commit(mutations.FETCH_START, { type: 'importBookmarks' }) const data = new FormData() data.append('bm_import', file) try { const response = await axios.post( url(state, `/folder/${folder || -1}/import`), data ) if (!response.data || response.data.status !== 'success') { if (response.status === 413) { throw new Error('Selected file is too large') } console.error('Failed to import bookmarks', response) throw new Error( Array.isArray(response.data.data) ? response.data.data.join('. ') : response.data.data ) } await dispatch(actions.COUNT_BOOKMARKS, -1) await dispatch(actions.LOAD_FOLDER_CHILDREN_ORDER, -1) await dispatch(actions.RELOAD_VIEW) commit(mutations.FETCH_END, 'importBookmarks') return commit( mutations.SET_NOTIFICATION, AppGlobal.methods.t('bookmarks', 'Import successful') ) } catch (err) { console.error(err) commit(mutations.FETCH_END, 'importBookmarks') commit(mutations.SET_ERROR, err.message) throw err } }, [actions.DELETE_BOOKMARKS]({ commit, dispatch, state }) { commit(mutations.FETCH_START, { type: 'deleteBookmarks' }) return axios .delete(url(state, '/bookmark')) .then(response => { const { data: { status }, } = response if (status !== 'success') { throw new Error(response.data) } dispatch(actions.COUNT_BOOKMARKS, -1) dispatch(actions.LOAD_FOLDER_CHILDREN_ORDER, -1) commit(mutations.FETCH_END, 'deleteBookmarks') return dispatch(actions.RELOAD_VIEW) }) .catch(err => { console.error(err) commit(mutations.FETCH_END, 'deleteBookmarks') commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', err.message) ) throw err }) }, async [actions.RENAME_TAG]( { commit, dispatch, state }, { oldName, newName } ) { commit(mutations.FETCH_START, { type: 'tag' }) try { const response = await axios.put(url(state, `/tag/${oldName}`), { name: newName, }) const { data: { status }, } = response if (status !== 'success') { throw new Error(response.data) } commit(mutations.RENAME_TAG, { oldName, newName }) commit(mutations.FETCH_END, 'tag') return dispatch(actions.LOAD_TAGS) } catch (err) { console.error(err) commit(mutations.FETCH_END, 'tag') commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to rename tag') ) throw err } }, [actions.LOAD_TAGS]({ commit, dispatch, state }) { commit(mutations.FETCH_START, { type: 'tags' }) return axios .get(url(state, '/tag'), { params: { count: true } }) .then(response => { const { data: tags } = response commit(mutations.FETCH_END, 'tags') return commit(mutations.SET_TAGS, tags) }) .catch(err => { console.error(err) commit(mutations.FETCH_END, 'tags') commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to load tags') ) throw err }) }, [actions.DELETE_TAG]({ commit, dispatch, state }, tag) { return axios .delete(url(state, `/tag/${tag}`)) .then(response => { const { data: { status }, } = response if (status !== 'success') { throw new Error(response.data) } commit(mutations.REMOVE_TAG, tag) dispatch(actions.LOAD_TAGS) }) .catch(err => { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to delete bookmark' ) ) throw err }) }, async [actions.LOAD_FOLDERS]({ commit, dispatch, state }, force) { if (!state.folders.length && !force) { try { const folders = loadState('bookmarks', 'folders') return commit(mutations.SET_FOLDERS, folders) } catch (e) { console.warn( 'Could not load initial folder state, continuing with HTTP request' ) } } let canceled = false commit(mutations.FETCH_START, { type: 'folders', cancel: () => { canceled = true }, }) try { const response = await axios.get(url(state, '/folder'), { params: {}, }) if (canceled) return const { data: { data, status }, } = response if (status !== 'success') throw new Error(data) const folders = data commit(mutations.FETCH_END, 'folders') return commit(mutations.SET_FOLDERS, folders) } catch (err) { console.error(err) commit(mutations.FETCH_END, 'folders') commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to load folders') ) throw err } }, async [actions.DELETE_FOLDER]( { commit, dispatch, state }, { id, avoidReload } ) { try { const response = await axios.delete(url(state, `/folder/${id}`)) const { data: { status }, } = response if (status !== 'success') { throw new Error(response.data) } const parentFolder = this.getters.getFolder(id)[0].parent_folder if (!avoidReload) { await dispatch( actions.LOAD_FOLDER_CHILDREN_ORDER, parentFolder ) await dispatch(actions.LOAD_FOLDERS) } } catch (err) { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to delete folder') ) throw err } }, async [actions.CREATE_FOLDER]( { commit, dispatch, state }, { parentFolder, title } ) { try { const response = await axios.post(url(state, '/folder'), { parent_folder: parentFolder, title, }) const { data: { status }, } = response if (status !== 'success') { throw new Error(response.data) } commit(mutations.DISPLAY_NEW_FOLDER, false) await dispatch( actions.LOAD_FOLDER_CHILDREN_ORDER, parentFolder || -1 ) await dispatch(actions.LOAD_FOLDERS, /* force: */ true) } catch (err) { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to create folder') ) throw err } }, async [actions.SAVE_FOLDER]({ commit, dispatch, state }, id) { const folder = this.getters.getFolder(id)[0] commit(mutations.FETCH_START, { type: 'saveFolder' }) try { const response = await axios.put(url(state, `/folder/${id}`), { parent_folder: folder.parent_folder, title: folder.title, }) const { data: { status }, } = response if (status !== 'success') { throw new Error(response.data) } await dispatch( actions.LOAD_FOLDER_CHILDREN_ORDER, folder.parent_folder ) commit(mutations.FETCH_END, 'saveFolder') } catch (err) { console.error(err) commit(mutations.FETCH_END, 'saveFolder') commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to create folder') ) throw err } }, async [actions.LOAD_FOLDER_CHILDREN_ORDER]( { commit, dispatch, state }, id ) { commit(mutations.FETCH_START, { type: 'childrenOrder' }) try { const response = await axios.get( url(state, `/folder/${id}/childorder`) ) const { data: { status }, } = response if (status !== 'success') { throw new Error(response.data) } await commit(mutations.FETCH_END, 'childrenOrder') await commit(mutations.SET_FOLDER_CHILDREN_ORDER, { folderId: id, children: response.data.data, }) } catch (err) { console.error(err) commit(mutations.FETCH_END, 'childrenOrder') commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to load children order' ) ) throw err } }, [actions.OPEN_FOLDER_DETAILS]({ commit }, id) { commit(mutations.SET_SIDEBAR, { type: 'folder', id }) }, [actions.OPEN_FOLDER_SHARING]({ commit }, id) { commit(mutations.SET_SIDEBAR, { type: 'folder', id, tab: 'folder-sharing' }) }, async [actions.MOVE_SELECTION]({ commit, dispatch, state }, folderId) { commit(mutations.FETCH_START, { type: 'moveSelection' }) try { await Parallel.each( state.selection.folders, async folder => { if (folderId === folder.id) { throw new Error('Cannot move folder into itself') } commit(mutations.MOVE_FOLDER, { folder: folder.id, target: folderId }) const oldParent = folder.parent_folder folder.parent_folder = folderId await dispatch(actions.SAVE_FOLDER, folder.id) // reloads children order for new parent dispatch( actions.LOAD_FOLDER_CHILDREN_ORDER, oldParent ) }, 10 ) await Parallel.each( state.selection.bookmarks, bookmark => { commit(mutations.REMOVE_BOOKMARK, bookmark.id) return dispatch(actions.MOVE_BOOKMARK, { oldFolder: bookmark.folders[bookmark.folders.length - 1], // FIXME This is veeeery ugly and will cause issues. Inevitably. newFolder: folderId, bookmark: bookmark.id, }) }, 10 ) // Because we're possibly moving across share boundaries we need to recount dispatch(actions.COUNT_BOOKMARKS, -1) commit(mutations.FETCH_END, 'moveSelection') } catch (err) { console.error(err) console.error(err.list) commit(mutations.FETCH_END, 'moveSelection') commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to move parts of selection' ) ) throw err } }, async [actions.COPY_SELECTION]({ commit, dispatch, state }, folderId) { commit(mutations.FETCH_START, { type: 'copySelection' }) try { await Parallel.each( state.selection.folders, async folder => { if (folder) { throw new Error('Cannot copy folders') } }, 10 ) await Promise.all([ dispatch(actions.LOAD_FOLDERS), Parallel.each( state.selection.bookmarks, bookmark => { return dispatch(actions.COPY_BOOKMARK, { newFolder: folderId, bookmark: bookmark.id, }) }, 10 ), ]) commit(mutations.FETCH_END, 'copySelection') } catch (err) { console.error(err) commit(mutations.FETCH_END, 'copySelection') commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to copy parts of selection' ) ) throw err } }, async [actions.DELETE_SELECTION]({ commit, dispatch, state }, { folder }) { commit(mutations.FETCH_START, { type: 'deleteSelection' }) try { await Parallel.each( state.selection.folders, folder => dispatch(actions.DELETE_FOLDER, { id: folder.id, avoidReload: true, }), 10 ) await Parallel.each( state.selection.bookmarks, bookmark => dispatch(actions.DELETE_BOOKMARK, { id: bookmark.id, folder, avoidReload: true, }), 10 ) dispatch(actions.RELOAD_VIEW) commit(mutations.FETCH_END, 'deleteSelection') } catch (err) { console.error(err) commit(mutations.FETCH_END, 'deleteSelection') commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to delete parts of selection' ) ) throw err } }, async [actions.TAG_SELECTION]({ commit, dispatch, state }, { tags, originalTags }) { commit(mutations.FETCH_START, { type: 'tagSelection' }) try { const removed = difference(originalTags, tags) await Parallel.each( state.selection.bookmarks, bookmark => { const originalTags = bookmark.tags bookmark.tags = uniq(tags.concat(bookmark.tags)) .filter(tag => !removed.includes(tag)) if (originalTags.join(',') !== bookmark.tags.join(',')) { return dispatch(actions.SAVE_BOOKMARK, bookmark.id) } }, 10 ) await dispatch(actions.LOAD_TAGS) commit(mutations.FETCH_END, 'tagSelection') } catch (err) { console.error(err) commit(mutations.FETCH_END, 'tagSelection') commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to tag parts of selection' ) ) throw err } }, [actions.RELOAD_VIEW]({ state, dispatch, commit }) { commit(mutations.SET_QUERY, state.fetchState.query) dispatch(actions.FETCH_PAGE) dispatch(actions.LOAD_FOLDERS) dispatch(actions.LOAD_TAGS) dispatch(actions.COUNT_BOOKMARKS, -1) dispatch(actions.COUNT_UNAVAILABLE) dispatch(actions.COUNT_ARCHIVED) }, [actions.NO_FILTER]({ dispatch, commit }) { commit(mutations.SET_QUERY, {}) return dispatch(actions.FETCH_PAGE) }, [actions.FILTER_BY_RECENT]({ dispatch, commit }, search) { commit(mutations.SET_QUERY, { sortby: 'added' }) return dispatch(actions.FETCH_PAGE) }, [actions.FILTER_BY_FREQUENT]({ dispatch, commit }, search) { commit(mutations.SET_QUERY, { sortby: 'clickcount' }) return dispatch(actions.FETCH_PAGE) }, [actions.FILTER_BY_SEARCH]({ dispatch, commit }, search) { commit(mutations.SET_QUERY, { search: search.split(' '), conjunction: 'and' }) return dispatch(actions.FETCH_PAGE) }, [actions.FILTER_BY_TAGS]({ dispatch, commit }, tags) { commit(mutations.SET_QUERY, { tags, conjunction: 'and' }) return dispatch(actions.FETCH_PAGE) }, [actions.FILTER_BY_UNTAGGED]({ dispatch, commit }) { commit(mutations.SET_QUERY, { untagged: true }) return dispatch(actions.FETCH_PAGE) }, [actions.FILTER_BY_UNAVAILABLE]({ dispatch, commit }) { commit(mutations.SET_QUERY, { unavailable: true }) return dispatch(actions.FETCH_PAGE) }, [actions.FILTER_BY_ARCHIVED]({ dispatch, commit }) { commit(mutations.SET_QUERY, { archived: true }) return dispatch(actions.FETCH_PAGE) }, [actions.FILTER_BY_DUPLICATED]({ dispatch, commit }) { commit(mutations.SET_QUERY, { duplicated: true }) return dispatch(actions.FETCH_PAGE) }, [actions.FILTER_BY_FOLDER]({ dispatch, commit, state }, folder) { commit(mutations.SET_QUERY, { folder }) if (state.settings.sorting === 'index') { dispatch(actions.LOAD_FOLDER_CHILDREN_ORDER, folder) } return dispatch(actions.FETCH_PAGE) }, [actions.FETCH_PAGE]({ dispatch, commit, state }) { if (state.fetchState.reachedEnd) return if (state.loading.bookmarks) return let canceled = false const fetchedPage = state.fetchState.page commit(mutations.FETCH_START, { type: 'bookmarks', cancel() { canceled = true }, }) axios .get(url(state, '/bookmark'), { params: { limit: BATCH_SIZE, page: state.fetchState.page, sortby: state.settings.sorting, ...state.fetchState.query, }, }) .then(response => { if (canceled) return const { data: { data, status }, } = response if (status !== 'success') throw new Error(data) const bookmarks = data commit(mutations.INCREMENT_PAGE) if (bookmarks.length < BATCH_SIZE) { commit(mutations.REACHED_END) } commit(mutations.FETCH_END, 'bookmarks') if (fetchedPage === 0) { commit(mutations.REMOVE_ALL_BOOKMARKS) } return dispatch(actions.ADD_ALL_BOOKMARKS, bookmarks) }) .catch(err => { console.error(err) commit(mutations.FETCH_END, 'bookmarks') commit( mutations.SET_ERROR, AppGlobal.t('bookmarks', 'Failed to fetch bookmarks.') ) throw err }) }, async [actions.FETCH_ALL]({ dispatch, commit, state }) { if (state.fetchState.reachedEnd) return if (state.loading.bookmarks) return let canceled = false commit(mutations.FETCH_START, { type: 'bookmarks', cancel() { canceled = true }, }) try { const response = await axios.get(url(state, '/bookmark'), { params: { page: -1, sortby: state.settings.sorting, ...state.fetchState.query, }, }) if (canceled) return const { data: { data, status }, } = response if (status !== 'success') throw new Error(data) const bookmarks = data commit(mutations.REACHED_END) commit(mutations.FETCH_END, 'bookmarks') return dispatch(actions.ADD_ALL_BOOKMARKS, bookmarks) } catch (err) { console.error(err) commit(mutations.FETCH_END, 'bookmarks') commit( mutations.SET_ERROR, AppGlobal.t('bookmarks', 'Failed to fetch bookmarks.') ) throw err } }, async [actions.SET_SETTING]({ commit, dispatch, state }, { key, value }) { await commit(mutations.SET_SETTING, { key, value }) if (key === 'viewMode') { await commit(mutations.SET_VIEW_MODE, value) } if (key === 'sorting') { await commit(mutations.RESET_PAGE) } if (state.public) { return } return axios .post(url(state, `/settings/${key}`), { [key]: value, }) .catch(err => { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t('bookmarks', 'Failed to change setting') ) throw err }) }, [actions.LOAD_SETTING]({ commit, dispatch, state }, key) { return axios .get(url(state, `/settings/${key}`)) .then(async response => { const { data: { [key]: value }, } = response await commit(mutations.SET_SETTING, { key, value }) switch (key) { case 'viewMode': await commit(mutations.SET_VIEW_MODE, value) break case 'sorting': await commit(mutations.RESET_PAGE) break } }) .catch(err => { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to load setting {key}', { key } ) ) throw err }) }, [actions.LOAD_SETTINGS]({ commit, dispatch, state }) { const settings = loadState('bookmarks', 'settings') for (const setting in settings) { const key = setting let value = settings[setting] switch (key) { case 'viewMode': value = value || state.settings.viewMode commit(mutations.SET_VIEW_MODE, value) break case 'sorting': value = value || state.settings.sorting commit(mutations.RESET_PAGE) break } commit(mutations.SET_SETTING, { key, value }) } ['archivePath', 'backupPath', 'backupEnabled', 'limit'].forEach(key => dispatch(actions.LOAD_SETTING, key) ) }, [actions.LOAD_SHARES]({ commit, dispatch, state }) { return axios .get(url(state, '/share')) .then(async response => { const { data: { data, status }, } = response if (status !== 'success') throw new Error(data) const shares = data for (const share of shares) { await commit(mutations.ADD_SHARE, share) } }) .catch(err => { console.error(err) // Don't set a notification as this is expected to happen for subfolders of shares that we don't have a RESHAR permission for throw err }) }, [actions.LOAD_SHARED_FOLDERS]({ commit, dispatch, state }) { return axios .get(url(state, '/folder/shared')) .then(async response => { const { data: { data, status }, } = response if (status !== 'success') throw new Error(data) const folders = data for (const folder of folders) { await commit(mutations.ADD_SHARED_FOLDER, folder) } }) .catch(err => { console.error(err) // Don't set a notification as this is expected to happen for subfolders of shares that we don't have a RESHAR permission for throw err }) }, [actions.LOAD_SHARES_OF_FOLDER]({ commit, dispatch, state }, folderId) { if (folderId === -1 || folderId === '-1') { return Promise.resolve() } return axios .get(url(state, `/folder/${folderId}/shares`)) .then(async response => { const { data: { data, status }, } = response if (status !== 'success') throw new Error(data) const shares = data for (const share of shares) { await commit(mutations.ADD_SHARE, share) } }) .catch(err => { console.error(err) // Don't set a notification as this is expected to happen for subfolders of shares that we don't have a RESHAR permission for throw err }) }, [actions.CREATE_SHARE]( { commit, dispatch, state }, { folderId, type, participant } ) { return axios .post(url(state, `/folder/${folderId}/shares`), { folderId, participant, type, }) .then(async response => { const { data: { item, data, status }, } = response if (status !== 'success') throw new Error(data) await commit(mutations.ADD_SHARE, item) }) .catch(err => { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to create share for folder {folderId}', { folderId } ) ) throw err }) }, [actions.EDIT_SHARE]( { commit, dispatch, state }, { shareId, canWrite, canShare } ) { return axios .put(url(state, `/share/${shareId}`), { canWrite, canShare, }) .then(async response => { const { data: { item, data, status }, } = response if (status !== 'success') throw new Error(data) await commit(mutations.ADD_SHARE, item) }) .catch(err => { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to update share {shareId}', { shareId } ) ) throw err }) }, [actions.DELETE_SHARE]({ commit, dispatch, state }, shareId) { return axios .delete(url(state, `/share/${shareId}`)) .then(async response => { const { data: { data, status }, } = response if (status !== 'success') throw new Error(data) await commit(mutations.REMOVE_SHARE, shareId) }) .catch(err => { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to delete share {shareId}', { shareId } ) ) throw err }) }, [actions.LOAD_PUBLIC_LINK]({ commit, dispatch, state }, folderId) { return axios .get(url(state, `/folder/${folderId}/publictoken`), { validateStatus: status => status === 404 || status === 200, }) .then(async response => { const { data: { item, data, status }, } = response if (response.status === 404) { return } if (status !== 'success') throw new Error(data) const token = item await commit(mutations.ADD_PUBLIC_TOKEN, { folderId, token }) }) .catch(err => { console.error(err) // Not sending a notification because we might just not have enough permissions to see this }) }, [actions.CREATE_PUBLIC_LINK]({ commit, dispatch, state }, folderId) { return axios .post(url(state, `/folder/${folderId}/publictoken`)) .then(async response => { const { data: { item, data, status }, } = response if (status !== 'success') throw new Error(data) const token = item await commit(mutations.ADD_PUBLIC_TOKEN, { folderId, token }) }) .catch(err => { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to create public link for folder {folderId}', { folderId } ) ) throw err }) }, [actions.DELETE_PUBLIC_LINK]({ commit, dispatch, state }, folderId) { return axios .delete(url(state, `/folder/${folderId}/publictoken`)) .then(async response => { const { data: { data, status }, } = response if (status !== 'success') throw new Error(data) await commit(mutations.REMOVE_PUBLIC_TOKEN, { folderId }) }) .catch(err => { console.error(err) commit( mutations.SET_ERROR, AppGlobal.methods.t( 'bookmarks', 'Failed to delete public link for folder {folderId}', { folderId } ) ) throw err }) }, } /** * @param state * @param url */ function url(state, url) { if (state.public) { url = `/apps/bookmarks/public/rest/v2${url}` } else { url = `/apps/bookmarks${url}` } return generateUrl(url) }