Refactor vuex store

This commit is contained in:
Marcel Klehr 2019-08-29 11:05:29 +02:00
parent 60cdafb5e0
commit b0751b7dad
19 changed files with 800 additions and 792 deletions

View File

@ -3,7 +3,7 @@
</template>
<script>
import { mutations } from './store';
import { mutations } from './store/';
export default {
name: 'App',
computed: {

View File

@ -22,7 +22,7 @@
import Vue from 'vue';
import { Tooltip } from 'nextcloud-vue';
import App from './components/ViewAdmin';
import store from './store';
import store from './store/';
import AppGlobal from './mixins/AppGlobal';
Vue.mixin(AppGlobal);

View File

@ -63,7 +63,7 @@
import Vue from 'vue';
import { Actions, ActionButton } from 'nextcloud-vue';
import { generateUrl } from 'nextcloud-router';
import { actions, mutations } from '../store';
import { actions, mutations } from '../store/';
import TagLine from './TagLine';
export default {

View File

@ -49,7 +49,7 @@
</template>
<script>
import { Multiselect, Actions, ActionButton } from 'nextcloud-vue';
import { mutations } from '../store';
import { mutations } from '../store/';
export default {
name: 'Breadcrumbs',

View File

@ -23,7 +23,7 @@
</template>
<script>
import { Actions, ActionButton } from 'nextcloud-vue';
import { actions } from '../store';
import { actions } from '../store/';
export default {
name: 'CreateBookmark',
components: { Actions, ActionButton },

View File

@ -23,7 +23,7 @@
</template>
<script>
import { Actions, ActionButton } from 'nextcloud-vue';
import { actions } from '../store';
import { actions } from '../store/';
export default {
name: 'CreateFolder',

View File

@ -38,7 +38,7 @@
<script>
import Vue from 'vue';
import { Actions, ActionButton } from 'nextcloud-vue';
import { actions, mutations } from '../store';
import { actions, mutations } from '../store/';
export default {
name: 'Folder',

View File

@ -15,7 +15,7 @@
</template>
<script>
import { Modal } from 'nextcloud-vue';
import { actions, mutations } from '../store';
import { actions, mutations } from '../store/';
import TreeFolder from './TreeFolder';
export default {

View File

@ -21,7 +21,7 @@ import {
AppNavigationSettings
} from 'nextcloud-vue';
import Settings from './Settings';
import { actions, mutations } from '../store';
import { actions, mutations } from '../store/';
export default {
name: 'App',

View File

@ -74,7 +74,7 @@
</template>
<script>
import { generateUrl } from 'nextcloud-router';
import { actions } from '../store';
import { actions } from '../store/';
export default {
name: 'Settings',
components: {},

View File

@ -47,7 +47,7 @@
import { AppSidebar, AppSidebarTab, Multiselect } from 'nextcloud-vue';
import { generateUrl } from 'nextcloud-router';
import humanizeDuration from 'humanize-duration';
import { actions, mutations } from '../store';
import { actions, mutations } from '../store/';
const MAX_RELATIVE_DATE = 1000 * 60 * 60 * 24 * 7; // one week

View File

@ -41,7 +41,7 @@
<script>
import { Content, Multiselect } from 'nextcloud-vue';
import { actions } from '../store';
import { actions } from '../store/';
export default {
name: 'ViewBookmarklet',

View File

@ -17,7 +17,7 @@ import BookmarksList from './BookmarksList';
import Breadcrumbs from './Breadcrumbs';
import SidebarBookmark from './SidebarBookmark';
import MoveDialog from './MoveDialog';
import { actions } from '../store';
import { actions } from '../store/';
export default {
name: 'App',

View File

@ -23,7 +23,7 @@ import Vue from 'vue';
import { Tooltip } from 'nextcloud-vue';
import App from './App';
import router from './router';
import store from './store';
import store from './store/';
import AppGlobal from './mixins/AppGlobal';
Vue.mixin(AppGlobal);

View File

@ -1,776 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'nextcloud-axios';
import { generateUrl } from 'nextcloud-router';
import AppGlobal from './mixins/AppGlobal';
Vue.use(Vuex);
const BATCH_SIZE = 42;
export const mutations = {
DISPLAY_NEW_BOOKMARK: 'DISPLAY_NEW_BOOKMARK',
DISPLAY_NEW_FOLDER: 'DISPLAY_NEW_FOLDER',
DISPLAY_MOVE_DIALOG: 'DISPLAY_MOVE_DIALOG',
RESET_SELECTION: 'RESET_SELECTION',
REMOVE_SELECTION_BOOKMARK: 'REMOVE_SELECTION_BOOKMARK',
ADD_SELECTION_BOOKMARK: 'ADD_SELECTION_BOOKMARK',
REMOVE_SELECTION_FOLDER: 'REMOVE_SELECTION_FOLDER',
ADD_SELECTION_FOLDER: 'ADD_SELECTION_FOLDER',
ADD_BOOKMARK: 'ADD_BOOKMARK',
REMOVE_BOOKMARK: 'REMOVE_BOOKMARK',
REMOVE_ALL_BOOKMARK: 'REMOVE_ALL_BOOKMARK',
SET_TAGS: 'SET_TAGS',
INCREMENT_PAGE: 'INCREMENT_PAGE',
SET_QUERY: 'SET_QUERY',
SET_SORTBY: 'SET_SORTBY',
FETCH_START: 'FETCH_START',
FETCH_END: 'FETCH_END',
REACHED_END: 'REACHED_END',
SET_ERROR: 'SET_ERROR',
SET_FOLDERS: 'SET_FOLDERS',
SET_SIDEBAR: 'SET_SIDEBAR',
SET_SETTING: 'SET_SETTING',
SET_VIEW_MODE: 'SET_VIEW_MODE'
};
export const actions = {
ADD_ALL_BOOKMARKS: 'ADD_ALL_BOOKMARKS',
CREATE_BOOKMARK: 'CREATE_BOOKMARK',
FIND_BOOKMARK: 'FIND_BOOKMARK',
DELETE_BOOKMARK: 'DELETE_BOOKMARK',
OPEN_BOOKMARK: 'OPEN_BOOKMARK',
SAVE_BOOKMARK: 'SAVE_BOOKMARK',
MOVE_BOOKMARK: 'MOVE_BOOKMARK',
IMPORT_BOOKMARKS: 'IMPORT_BOOKMARKS',
DELETE_BOOKMARKS: 'IMPORT_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',
MOVE_SELECTION: 'MOVE_SELECTION',
RELOAD_VIEW: 'RELOAD_VIEW',
NO_FILTER: 'NO_FILTER',
FILTER_BY_RECENT: 'FILTER_BY_RECENT',
FILTER_BY_UNTAGGED: 'FILTER_BY_UNTAGGED',
FILTER_BY_TAGS: 'FILTER_BY_TAGS',
FILTER_BY_FOLDER: 'FILTER_BY_FOLDER',
FILTER_BY_SEARCH: 'FILTER_BY_SEARCH',
FETCH_PAGE: 'FETCH_PAGE',
SET_SETTING: 'SET_SETTING',
LOAD_SETTING: 'LOAD_SETTING',
LOAD_SETTINGS: 'SLOAD_SETTINGS'
};
export default new Vuex.Store({
state: {
fetchState: {
page: 0,
query: {},
reachedEnd: false
},
loading: {
tags: false,
folders: false,
bookmarks: false,
createBookmark: false,
saveBookmark: false,
createFolder: false,
saveFolder: false
},
error: null,
settings: {
viewMode: 'list',
sorting: 'lastmodified'
},
bookmarks: [],
bookmarksById: {},
tags: [],
folders: [],
foldersById: {},
selection: {
folders: [],
bookmarks: []
},
displayNewBookmark: false,
displayNewFolder: false,
displayMoveDialog: false,
sidebar: null,
viewMode: 'list'
},
getters: {
getBookmark: state => id => {
return state.bookmarksById[id];
},
getFolder: state => id => {
if (Number(id) === -1) {
return [{ id: '-1', children: state.folders }];
}
return findFolder(id, state.folders);
}
},
mutations: {
[mutations.SET_VIEW_MODE](state, viewMode) {
state.viewMode = viewMode;
},
[mutations.SET_ERROR](state, error) {
state.error = error;
},
[mutations.SET_SETTING](state, { key, value }) {
Vue.set(state.settings, key, value);
},
[mutations.SET_FOLDERS](state, folders) {
state.folders = folders;
},
[mutations.SET_TAGS](state, tags) {
state.tags = tags;
},
[mutations.DISPLAY_NEW_BOOKMARK](state, display) {
state.displayNewBookmark = display;
if (display) {
state.displayNewFolder = false;
}
},
[mutations.DISPLAY_NEW_FOLDER](state, display) {
state.displayNewFolder = display;
if (display) {
state.displayNewBookmark = false;
}
},
[mutations.DISPLAY_MOVE_DIALOG](state, display) {
state.displayMoveDialog = display;
},
[mutations.RESET_SELECTION](state) {
state.selection = { folders: [], bookmarks: [] };
},
[mutations.ADD_SELECTION_BOOKMARK](state, item) {
state.selection.bookmarks.push(item);
},
[mutations.REMOVE_SELECTION_BOOKMARK](state, item) {
Vue.set(
state.selection,
'bookmarks',
state.selection.bookmarks.filter(s => !(s.id === item.id))
);
},
[mutations.ADD_SELECTION_FOLDER](state, item) {
state.selection.folders.push(item);
},
[mutations.REMOVE_SELECTION_FOLDER](state, item) {
Vue.set(
state.selection,
'folders',
state.selection.folders.filter(s => !(s.id === item.id))
);
},
[mutations.ADD_BOOKMARK](state, bookmark) {
const existingBookmark = state.bookmarksById[bookmark.id];
if (!existingBookmark) {
state.bookmarks.push(bookmark);
Vue.set(state.bookmarksById, bookmark.id, bookmark);
}
},
[mutations.REMOVE_BOOKMARK](state, id) {
const index = state.bookmarks.findIndex(bookmark => bookmark.id === id);
if (index !== -1) {
state.bookmarks.splice(index, 1);
Vue.delete(state.bookmarksById, id);
}
},
[mutations.REMOVE_ALL_BOOKMARKS](state) {
state.bookmarks = [];
state.bookmarksById = {};
},
[mutations.SET_SIDEBAR](state, sidebar) {
state.sidebar = sidebar;
},
[mutations.INCREMENT_PAGE](state) {
Vue.set(state.fetchState, 'page', state.fetchState.page + 1);
},
[mutations.SET_QUERY](state, query) {
state.bookmarks = [];
state.bookmarksById = {};
Vue.set(state.fetchState, 'page', 0);
Vue.set(state.fetchState, 'reachedEnd', false);
Vue.set(state.fetchState, 'query', query);
},
[mutations.FETCH_START](state, type) {
Vue.set(state.loading, type, true);
},
[mutations.FETCH_END](state, type) {
Vue.set(state.loading, type, false);
},
[mutations.REACHED_END](state) {
Vue.set(state.fetchState, 'reachedEnd', true);
}
},
actions: {
[actions.ADD_ALL_BOOKMARKS]({ commit }, bookmarks) {
for (const bookmark of bookmarks) {
commit(mutations.ADD_BOOKMARK, bookmark);
}
},
async [actions.FIND_BOOKMARK]({ commit, dispatch, state }, link) {
if (state.loading.bookmarks) return;
try {
const response = await axios
.get(url('/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;
}
},
[actions.CREATE_BOOKMARK]({ commit, dispatch, state }, data) {
if (state.loading.bookmarks) return;
commit(mutations.FETCH_START, 'createBookmark');
return axios
.post(url('/bookmark'), {
url: data.url,
title: data.title,
description: data.description,
folders: data.folders,
tags: data.tags
})
.then(response => {
const {
data: { item: bookmark, status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
commit(mutations.DISPLAY_NEW_BOOKMARK, false);
commit(mutations.ADD_BOOKMARK, bookmark);
return dispatch(actions.OPEN_BOOKMARK, bookmark.id);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to create bookmark')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'createBookmark');
});
},
[actions.SAVE_BOOKMARK]({ commit, dispatch, state }, id) {
commit(mutations.FETCH_START, 'saveBookmark');
return axios
.put(url(`/bookmark/${id}`), this.getters.getBookmark(id))
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to save bookmark')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'saveBookmark');
});
},
async [actions.MOVE_BOOKMARK](
{ commit, dispatch, state },
{ bookmark, oldFolder, newFolder }
) {
commit(mutations.FETCH_START, 'moveBookmark');
try {
let response = await axios.post(
url(`/folder/${newFolder}/bookmarks/${bookmark}`)
);
if (response.data.status !== 'success') {
throw new Error(response.data);
}
let response2 = await axios.delete(
url(`/folder/${oldFolder}/bookmarks/${bookmark}`)
);
if (response2.data.status !== 'success') {
throw new Error(response2.data);
}
} catch (err) {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to move bookmark')
);
throw err;
} finally {
commit(mutations.FETCH_END, 'moveBookmark');
}
},
[actions.OPEN_BOOKMARK]({ commit }, id) {
commit(mutations.SET_SIDEBAR, { type: 'bookmark', id });
},
async [actions.DELETE_BOOKMARK](
{ commit, dispatch, state },
{ id, folder }
) {
if (folder) {
try {
const response = await axios.delete(
url(`/folder/${folder}/bookmarks/${id}`)
);
if (response.data.status !== 'success') {
throw new Error(response.data);
}
commit(mutations.REMOVE_BOOKMARK, id);
} 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(`/bookmark/${id}`));
if (response.data.status !== 'success') {
throw new Error(response.data);
}
commit(mutations.REMOVE_BOOKMARK, id);
} catch (err) {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to delete bookmark')
);
throw err;
}
},
[actions.IMPORT_BOOKMARKS]({ commit, dispatch, state }, file) {
var data = new FormData();
data.append('bm_import', file);
return axios
.post(url(`/bookmark/import`), data)
.then(response => {
if (!response.ok) {
if (response.status === 413) {
throw new Error('Selected file is too large');
}
throw new Error(response.statusText);
} else {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
}
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', err.message)
);
throw err;
});
},
[actions.DELETE_BOOKMARKS]({ commit, dispatch, state }) {
return axios
.delete(url(`/bookmark`))
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
return dispatch(actions.LOAD_FOLDERS);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', err.message)
);
throw err;
});
},
[actions.RENAME_TAG]({ commit, dispatch, state }, { oldName, newName }) {
commit(mutations.FETCH_START, 'tag');
return axios
.put(url(`/tag/${oldName}`), {
name: newName
})
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
return dispatch(actions.LOAD_TAGS);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to create bookmark')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'tag');
});
},
[actions.LOAD_TAGS]({ commit, dispatch, state }, link) {
if (state.loading.bookmarks) return;
commit(mutations.FETCH_START, 'tags');
return axios
.get(url('/tag'), { params: { count: true } })
.then(response => {
const { data: tags } = response;
return commit(mutations.SET_TAGS, tags);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to load tags')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'tags');
});
},
[actions.DELETE_TAG]({ commit, dispatch, state }, tag) {
return axios
.delete(url(`/tag/${tag}`))
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
dispatch(actions.LOAD_TAGS);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to delete bookmark')
);
throw err;
});
},
[actions.LOAD_FOLDERS]({ commit, dispatch, state }) {
if (state.loading.bookmarks) return;
commit(mutations.FETCH_START, 'folders');
return axios
.get(url('/folder'), { params: {} })
.then(response => {
const {
data: { data, status }
} = response;
if (status !== 'success') throw new Error(data);
const folders = data;
return commit(mutations.SET_FOLDERS, folders);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to load folders')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'folders');
});
},
[actions.DELETE_FOLDER]({ commit, dispatch, state }, id) {
return axios
.delete(url(`/folder/${id}`))
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
dispatch(actions.LOAD_FOLDERS);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to delete folder')
);
throw err;
});
},
[actions.CREATE_FOLDER](
{ commit, dispatch, state },
{ parentFolder, title }
) {
return axios
.post(url(`/folder`), {
parent_folder: parentFolder,
title
})
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
commit(mutations.DISPLAY_NEW_FOLDER, false);
dispatch(actions.LOAD_FOLDERS);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to create folder')
);
throw err;
});
},
[actions.SAVE_FOLDER]({ commit, dispatch, state }, id) {
const folder = this.getters.getFolder(id)[0];
commit(mutations.FETCH_START, 'saveFolder');
return axios
.put(url(`/folder/${id}`), {
parent_folder: folder.parent_folder,
title: folder.title
})
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to create folder')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'saveFolder');
});
},
async [actions.MOVE_SELECTION]({ commit, dispatch, state }, folderId) {
commit(mutations.FETCH_START, 'moveSelection');
try {
for (const folder of state.selection.folders) {
if (folderId === folder.id) {
throw new Error('Cannot move folder into itself');
}
folder.parent_folder = folderId;
await dispatch(actions.SAVE_FOLDER, folder.id);
}
for (const bookmark of state.selection.bookmarks) {
await 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
});
}
} catch (err) {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to move parts of selection')
);
throw err;
} finally {
commit(mutations.FETCH_END, 'moveSelection');
}
},
[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);
},
[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: 'lastmodified' });
return dispatch(actions.FETCH_PAGE);
},
[actions.FILTER_BY_SEARCH]({ dispatch, commit }, search) {
commit(mutations.SET_QUERY, { search: search.split(' ') });
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_FOLDER]({ dispatch, commit }, folder) {
commit(mutations.SET_QUERY, { folder });
return dispatch(actions.FETCH_PAGE);
},
[actions.FETCH_PAGE]({ dispatch, commit, state }) {
if (state.loading.bookmarks) return;
if (state.fetchState.reachedEnd) return;
commit(mutations.FETCH_START, 'bookmarks');
return axios
.get(url('/bookmark'), {
params: {
limit: BATCH_SIZE,
page: state.fetchState.page,
sortby: state.settings.sorting,
...state.fetchState.query
}
})
.then(response => {
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);
}
return dispatch(actions.ADD_ALL_BOOKMARKS, bookmarks);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.t('bookmarks', 'Failed to fetch bookmarks.')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'bookmarks');
});
},
[actions.SET_SETTING]({ commit, dispatch, state }, { key, value }) {
return axios
.post(url(`/settings/${key}`), {
[key]: value
})
.then(response => {
commit(mutations.SET_SETTING, key, value);
if (key === 'viewMode' && state.viewMode !== value) {
commit(mutations.SET_VIEW_MODE, 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(`/settings/${key}`))
.then(response => {
const {
data: { [key]: value }
} = response;
commit(mutations.SET_SETTING, { key, value });
if (key === 'viewMode' && state.viewMode !== value) {
commit(mutations.SET_VIEW_MODE, value);
}
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to load setting ' + key)
);
throw err;
});
},
[actions.LOAD_SETTINGS]({ commit, dispatch, state }) {
return Promise.all(
['sorting', 'viewMode'].map(key => dispatch(actions.LOAD_SETTING, key))
);
}
}
});
function url(url) {
url = `/apps/bookmarks${url}`;
return generateUrl(url);
}
function findFolder(id, children) {
if (!children || !children.length) return [];
let folders = children.filter(folder => Number(folder.id) === Number(id));
if (folders.length) {
return folders;
} else {
for (let child of children) {
let folders = findFolder(id, child.children);
if (folders.length) {
folders.push(child);
return folders;
}
}
return [];
}
}

580
src/store/actions.js Normal file
View File

@ -0,0 +1,580 @@
import axios from 'nextcloud-axios';
import { generateUrl } from 'nextcloud-router';
import AppGlobal from '../mixins/AppGlobal';
import { mutations } from './mutations';
const BATCH_SIZE = 42;
export const actions = {
ADD_ALL_BOOKMARKS: 'ADD_ALL_BOOKMARKS',
CREATE_BOOKMARK: 'CREATE_BOOKMARK',
FIND_BOOKMARK: 'FIND_BOOKMARK',
DELETE_BOOKMARK: 'DELETE_BOOKMARK',
OPEN_BOOKMARK: 'OPEN_BOOKMARK',
SAVE_BOOKMARK: 'SAVE_BOOKMARK',
MOVE_BOOKMARK: 'MOVE_BOOKMARK',
IMPORT_BOOKMARKS: 'IMPORT_BOOKMARKS',
DELETE_BOOKMARKS: 'IMPORT_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',
MOVE_SELECTION: 'MOVE_SELECTION',
RELOAD_VIEW: 'RELOAD_VIEW',
NO_FILTER: 'NO_FILTER',
FILTER_BY_RECENT: 'FILTER_BY_RECENT',
FILTER_BY_UNTAGGED: 'FILTER_BY_UNTAGGED',
FILTER_BY_TAGS: 'FILTER_BY_TAGS',
FILTER_BY_FOLDER: 'FILTER_BY_FOLDER',
FILTER_BY_SEARCH: 'FILTER_BY_SEARCH',
FETCH_PAGE: 'FETCH_PAGE',
SET_SETTING: 'SET_SETTING',
LOAD_SETTING: 'LOAD_SETTING',
LOAD_SETTINGS: 'SLOAD_SETTINGS'
};
export default {
[actions.ADD_ALL_BOOKMARKS]({ commit }, bookmarks) {
for (const bookmark of bookmarks) {
commit(mutations.ADD_BOOKMARK, bookmark);
}
},
async [actions.FIND_BOOKMARK]({ commit, dispatch, state }, link) {
if (state.loading.bookmarks) return;
try {
const response = await axios
.get(url('/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;
}
},
[actions.CREATE_BOOKMARK]({ commit, dispatch, state }, data) {
if (state.loading.bookmarks) return;
commit(mutations.FETCH_START, 'createBookmark');
return axios
.post(url('/bookmark'), {
url: data.url,
title: data.title,
description: data.description,
folders: data.folders,
tags: data.tags
})
.then(response => {
const {
data: { item: bookmark, status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
commit(mutations.DISPLAY_NEW_BOOKMARK, false);
commit(mutations.ADD_BOOKMARK, bookmark);
return dispatch(actions.OPEN_BOOKMARK, bookmark.id);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to create bookmark')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'createBookmark');
});
},
[actions.SAVE_BOOKMARK]({ commit, dispatch, state }, id) {
commit(mutations.FETCH_START, 'saveBookmark');
return axios
.put(url(`/bookmark/${id}`), this.getters.getBookmark(id))
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to save bookmark')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'saveBookmark');
});
},
async [actions.MOVE_BOOKMARK](
{ commit, dispatch, state },
{ bookmark, oldFolder, newFolder }
) {
commit(mutations.FETCH_START, 'moveBookmark');
try {
let response = await axios.post(
url(`/folder/${newFolder}/bookmarks/${bookmark}`)
);
if (response.data.status !== 'success') {
throw new Error(response.data);
}
let response2 = await axios.delete(
url(`/folder/${oldFolder}/bookmarks/${bookmark}`)
);
if (response2.data.status !== 'success') {
throw new Error(response2.data);
}
} catch (err) {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to move bookmark')
);
throw err;
} finally {
commit(mutations.FETCH_END, 'moveBookmark');
}
},
[actions.OPEN_BOOKMARK]({ commit }, id) {
commit(mutations.SET_SIDEBAR, { type: 'bookmark', id });
},
async [actions.DELETE_BOOKMARK](
{ commit, dispatch, state },
{ id, folder }
) {
if (folder) {
try {
const response = await axios.delete(
url(`/folder/${folder}/bookmarks/${id}`)
);
if (response.data.status !== 'success') {
throw new Error(response.data);
}
commit(mutations.REMOVE_BOOKMARK, id);
} 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(`/bookmark/${id}`));
if (response.data.status !== 'success') {
throw new Error(response.data);
}
commit(mutations.REMOVE_BOOKMARK, id);
} catch (err) {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to delete bookmark')
);
throw err;
}
},
[actions.IMPORT_BOOKMARKS]({ commit, dispatch, state }, file) {
var data = new FormData();
data.append('bm_import', file);
return axios
.post(url(`/bookmark/import`), data)
.then(response => {
if (!response.ok) {
if (response.status === 413) {
throw new Error('Selected file is too large');
}
throw new Error(response.statusText);
} else {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
}
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', err.message)
);
throw err;
});
},
[actions.DELETE_BOOKMARKS]({ commit, dispatch, state }) {
return axios
.delete(url(`/bookmark`))
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
return dispatch(actions.LOAD_FOLDERS);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', err.message)
);
throw err;
});
},
[actions.RENAME_TAG]({ commit, dispatch, state }, { oldName, newName }) {
commit(mutations.FETCH_START, 'tag');
return axios
.put(url(`/tag/${oldName}`), {
name: newName
})
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
return dispatch(actions.LOAD_TAGS);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to create bookmark')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'tag');
});
},
[actions.LOAD_TAGS]({ commit, dispatch, state }, link) {
if (state.loading.bookmarks) return;
commit(mutations.FETCH_START, 'tags');
return axios
.get(url('/tag'), { params: { count: true } })
.then(response => {
const { data: tags } = response;
return commit(mutations.SET_TAGS, tags);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to load tags')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'tags');
});
},
[actions.DELETE_TAG]({ commit, dispatch, state }, tag) {
return axios
.delete(url(`/tag/${tag}`))
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
dispatch(actions.LOAD_TAGS);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to delete bookmark')
);
throw err;
});
},
[actions.LOAD_FOLDERS]({ commit, dispatch, state }) {
if (state.loading.bookmarks) return;
commit(mutations.FETCH_START, 'folders');
return axios
.get(url('/folder'), { params: {} })
.then(response => {
const {
data: { data, status }
} = response;
if (status !== 'success') throw new Error(data);
const folders = data;
return commit(mutations.SET_FOLDERS, folders);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to load folders')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'folders');
});
},
[actions.DELETE_FOLDER]({ commit, dispatch, state }, id) {
return axios
.delete(url(`/folder/${id}`))
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
dispatch(actions.LOAD_FOLDERS);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to delete folder')
);
throw err;
});
},
[actions.CREATE_FOLDER](
{ commit, dispatch, state },
{ parentFolder, title }
) {
return axios
.post(url(`/folder`), {
parent_folder: parentFolder,
title
})
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
commit(mutations.DISPLAY_NEW_FOLDER, false);
dispatch(actions.LOAD_FOLDERS);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to create folder')
);
throw err;
});
},
[actions.SAVE_FOLDER]({ commit, dispatch, state }, id) {
const folder = this.getters.getFolder(id)[0];
commit(mutations.FETCH_START, 'saveFolder');
return axios
.put(url(`/folder/${id}`), {
parent_folder: folder.parent_folder,
title: folder.title
})
.then(response => {
const {
data: { status }
} = response;
if (status !== 'success') {
throw new Error(response.data);
}
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to create folder')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'saveFolder');
});
},
async [actions.MOVE_SELECTION]({ commit, dispatch, state }, folderId) {
commit(mutations.FETCH_START, 'moveSelection');
try {
for (const folder of state.selection.folders) {
if (folderId === folder.id) {
throw new Error('Cannot move folder into itself');
}
folder.parent_folder = folderId;
await dispatch(actions.SAVE_FOLDER, folder.id);
}
for (const bookmark of state.selection.bookmarks) {
await 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
});
}
} catch (err) {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to move parts of selection')
);
throw err;
} finally {
commit(mutations.FETCH_END, 'moveSelection');
}
},
[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);
},
[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: 'lastmodified' });
return dispatch(actions.FETCH_PAGE);
},
[actions.FILTER_BY_SEARCH]({ dispatch, commit }, search) {
commit(mutations.SET_QUERY, { search: search.split(' ') });
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_FOLDER]({ dispatch, commit }, folder) {
commit(mutations.SET_QUERY, { folder });
return dispatch(actions.FETCH_PAGE);
},
[actions.FETCH_PAGE]({ dispatch, commit, state }) {
if (state.loading.bookmarks) return;
if (state.fetchState.reachedEnd) return;
commit(mutations.FETCH_START, 'bookmarks');
return axios
.get(url('/bookmark'), {
params: {
limit: BATCH_SIZE,
page: state.fetchState.page,
sortby: state.settings.sorting,
...state.fetchState.query
}
})
.then(response => {
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);
}
return dispatch(actions.ADD_ALL_BOOKMARKS, bookmarks);
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.t('bookmarks', 'Failed to fetch bookmarks.')
);
throw err;
})
.finally(() => {
commit(mutations.FETCH_END, 'bookmarks');
});
},
[actions.SET_SETTING]({ commit, dispatch, state }, { key, value }) {
return axios
.post(url(`/settings/${key}`), {
[key]: value
})
.then(response => {
commit(mutations.SET_SETTING, key, value);
if (key === 'viewMode' && state.viewMode !== value) {
commit(mutations.SET_VIEW_MODE, 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(`/settings/${key}`))
.then(response => {
const {
data: { [key]: value }
} = response;
commit(mutations.SET_SETTING, { key, value });
if (key === 'viewMode' && state.viewMode !== value) {
commit(mutations.SET_VIEW_MODE, value);
}
})
.catch(err => {
console.error(err);
commit(
mutations.SET_ERROR,
AppGlobal.methods.t('bookmarks', 'Failed to load setting ' + key)
);
throw err;
});
},
[actions.LOAD_SETTINGS]({ commit, dispatch, state }) {
return Promise.all(
['sorting', 'viewMode'].map(key => dispatch(actions.LOAD_SETTING, key))
);
}
}
;
function url(url) {
url = `/apps/bookmarks${url}`;
return generateUrl(url);
}

79
src/store/index.js Normal file
View File

@ -0,0 +1,79 @@
import Vue from 'vue';
import Vuex, { Store } from 'vuex';
import Mutations from './mutations';
import Actions from './actions';
Vue.use(Vuex);
export { mutations } from './mutations';
export { actions } from './actions';
export default new Store({
mutations: Mutations,
actions: Actions,
state: {
fetchState: {
page: 0,
query: {},
reachedEnd: false
},
loading: {
tags: false,
folders: false,
bookmarks: false,
createBookmark: false,
saveBookmark: false,
createFolder: false,
saveFolder: false
},
error: null,
settings: {
viewMode: 'list',
sorting: 'lastmodified'
},
bookmarks: [],
bookmarksById: {},
tags: [],
folders: [],
foldersById: {},
selection: {
folders: [],
bookmarks: []
},
displayNewBookmark: false,
displayNewFolder: false,
displayMoveDialog: false,
sidebar: null,
viewMode: 'list'
},
getters: {
getBookmark: state => id => {
return state.bookmarksById[id];
},
getFolder: state => id => {
if (Number(id) === -1) {
return [{ id: '-1', children: state.folders }];
}
return findFolder(id, state.folders);
}
}
});
function findFolder(id, children) {
if (!children || !children.length) return [];
let folders = children.filter(folder => Number(folder.id) === Number(id));
if (folders.length) {
return folders;
} else {
for (let child of children) {
let folders = findFolder(id, child.children);
if (folders.length) {
folders.push(child);
return folders;
}
}
return [];
}
}

127
src/store/mutations.js Normal file
View File

@ -0,0 +1,127 @@
import Vue from 'vue';
export const mutations = {
DISPLAY_NEW_BOOKMARK: 'DISPLAY_NEW_BOOKMARK',
DISPLAY_NEW_FOLDER: 'DISPLAY_NEW_FOLDER',
DISPLAY_MOVE_DIALOG: 'DISPLAY_MOVE_DIALOG',
RESET_SELECTION: 'RESET_SELECTION',
REMOVE_SELECTION_BOOKMARK: 'REMOVE_SELECTION_BOOKMARK',
ADD_SELECTION_BOOKMARK: 'ADD_SELECTION_BOOKMARK',
REMOVE_SELECTION_FOLDER: 'REMOVE_SELECTION_FOLDER',
ADD_SELECTION_FOLDER: 'ADD_SELECTION_FOLDER',
ADD_BOOKMARK: 'ADD_BOOKMARK',
REMOVE_BOOKMARK: 'REMOVE_BOOKMARK',
REMOVE_ALL_BOOKMARK: 'REMOVE_ALL_BOOKMARK',
SET_TAGS: 'SET_TAGS',
INCREMENT_PAGE: 'INCREMENT_PAGE',
SET_QUERY: 'SET_QUERY',
SET_SORTBY: 'SET_SORTBY',
FETCH_START: 'FETCH_START',
FETCH_END: 'FETCH_END',
REACHED_END: 'REACHED_END',
SET_ERROR: 'SET_ERROR',
SET_FOLDERS: 'SET_FOLDERS',
SET_SIDEBAR: 'SET_SIDEBAR',
SET_SETTING: 'SET_SETTING',
SET_VIEW_MODE: 'SET_VIEW_MODE'
};
export default {
[mutations.SET_VIEW_MODE](state, viewMode) {
state.viewMode = viewMode;
},
[mutations.SET_ERROR](state, error) {
state.error = error;
},
[mutations.SET_SETTING](state, { key, value }) {
Vue.set(state.settings, key, value);
},
[mutations.SET_FOLDERS](state, folders) {
state.folders = folders;
},
[mutations.SET_TAGS](state, tags) {
state.tags = tags;
},
[mutations.DISPLAY_NEW_BOOKMARK](state, display) {
state.displayNewBookmark = display;
if (display) {
state.displayNewFolder = false;
}
},
[mutations.DISPLAY_NEW_FOLDER](state, display) {
state.displayNewFolder = display;
if (display) {
state.displayNewBookmark = false;
}
},
[mutations.DISPLAY_MOVE_DIALOG](state, display) {
state.displayMoveDialog = display;
},
[mutations.RESET_SELECTION](state) {
state.selection = { folders: [], bookmarks: [] };
},
[mutations.ADD_SELECTION_BOOKMARK](state, item) {
state.selection.bookmarks.push(item);
},
[mutations.REMOVE_SELECTION_BOOKMARK](state, item) {
Vue.set(
state.selection,
'bookmarks',
state.selection.bookmarks.filter(s => !(s.id === item.id))
);
},
[mutations.ADD_SELECTION_FOLDER](state, item) {
state.selection.folders.push(item);
},
[mutations.REMOVE_SELECTION_FOLDER](state, item) {
Vue.set(
state.selection,
'folders',
state.selection.folders.filter(s => !(s.id === item.id))
);
},
[mutations.ADD_BOOKMARK](state, bookmark) {
const existingBookmark = state.bookmarksById[bookmark.id];
if (!existingBookmark) {
state.bookmarks.push(bookmark);
Vue.set(state.bookmarksById, bookmark.id, bookmark);
}
},
[mutations.REMOVE_BOOKMARK](state, id) {
const index = state.bookmarks.findIndex(bookmark => bookmark.id === id);
if (index !== -1) {
state.bookmarks.splice(index, 1);
Vue.delete(state.bookmarksById, id);
}
},
[mutations.REMOVE_ALL_BOOKMARKS](state) {
state.bookmarks = [];
state.bookmarksById = {};
},
[mutations.SET_SIDEBAR](state, sidebar) {
state.sidebar = sidebar;
},
[mutations.INCREMENT_PAGE](state) {
Vue.set(state.fetchState, 'page', state.fetchState.page + 1);
},
[mutations.SET_QUERY](state, query) {
state.bookmarks = [];
state.bookmarksById = {};
Vue.set(state.fetchState, 'page', 0);
Vue.set(state.fetchState, 'reachedEnd', false);
Vue.set(state.fetchState, 'query', query);
},
[mutations.FETCH_START](state, type) {
Vue.set(state.loading, type, true);
},
[mutations.FETCH_END](state, type) {
Vue.set(state.loading, type, false);
},
[mutations.REACHED_END](state) {
Vue.set(state.fetchState, 'reachedEnd', true);
}
};

View File

@ -1,6 +1,4 @@
<?php
/** @var $l \OCP\IL10N */
/** @var $_ array */
script('bookmarks', 'bookmarks.admin');
?>
<div id="bookmarks" class="section"></div>