From f07b9953e32328235cafc4503884f8474c949770 Mon Sep 17 00:00:00 2001 From: Jonathan Treffler Date: Mon, 18 Nov 2024 18:32:34 +0100 Subject: [PATCH] initial commit of GUI --- img/deny.svg | 70 ++++++++ src/Header.vue | 59 +++++++ src/Modal.vue | 114 ++++++++++++ src/ModalView.vue | 147 ++++++++++++++++ src/api.js | 73 ++++++++ src/components/ConfirmDeleteDialog.vue | 92 ++++++++++ src/components/MemberList/MemberList.vue | 162 +++++++++++++++++ src/components/MemberList/MemberListItem.vue | 95 ++++++++++ src/components/MemberList/index.js | 1 + src/components/Permissions/Permissions.vue | 93 ++++++++++ .../Permissions/PermissionsInputRow.vue | 82 +++++++++ src/components/Permissions/index.js | 1 + src/header.js | 69 ++++++++ src/helpers/file-size-helpers.js | 12 ++ src/helpers/permission-helpers.js | 52 ++++++ src/helpers/validation.js | 10 ++ src/main.js | 40 +++++ src/router.js | 24 +++ src/stores/current-dir.js | 55 ++++++ src/views/ResourceSettings.vue | 165 ++++++++++++++++++ 20 files changed, 1416 insertions(+) create mode 100644 img/deny.svg create mode 100644 src/Header.vue create mode 100644 src/Modal.vue create mode 100644 src/ModalView.vue create mode 100644 src/api.js create mode 100644 src/components/ConfirmDeleteDialog.vue create mode 100644 src/components/MemberList/MemberList.vue create mode 100644 src/components/MemberList/MemberListItem.vue create mode 100644 src/components/MemberList/index.js create mode 100644 src/components/Permissions/Permissions.vue create mode 100644 src/components/Permissions/PermissionsInputRow.vue create mode 100644 src/components/Permissions/index.js create mode 100644 src/header.js create mode 100644 src/helpers/file-size-helpers.js create mode 100644 src/helpers/permission-helpers.js create mode 100644 src/helpers/validation.js create mode 100644 src/main.js create mode 100644 src/router.js create mode 100644 src/stores/current-dir.js create mode 100644 src/views/ResourceSettings.vue diff --git a/img/deny.svg b/img/deny.svg new file mode 100644 index 0000000..b7d67b1 --- /dev/null +++ b/img/deny.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Header.vue b/src/Header.vue new file mode 100644 index 0000000..7e5b2f9 --- /dev/null +++ b/src/Header.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/src/Modal.vue b/src/Modal.vue new file mode 100644 index 0000000..9bbf777 --- /dev/null +++ b/src/Modal.vue @@ -0,0 +1,114 @@ + + + + \ No newline at end of file diff --git a/src/ModalView.vue b/src/ModalView.vue new file mode 100644 index 0000000..5863f85 --- /dev/null +++ b/src/ModalView.vue @@ -0,0 +1,147 @@ + + + + + + + \ No newline at end of file diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..9e380b3 --- /dev/null +++ b/src/api.js @@ -0,0 +1,73 @@ +import axios from "@nextcloud/axios" +import { generateUrl } from "@nextcloud/router" + +/** + * @typedef {{ + * id: number + * type: string + * organizationFolderId: number + * name: string + * parentResource: number + * active: bool + * inheritManagers: bool + * membersAclPermission: number + * managersAclPermission: number + * inheritedAclPermission: number + * }} FolderResource + * + * @typedef {(FolderResource)} Resource + * + * @typedef {{ + * type: number, + * id: string, + * }} Principal + * + * @typedef {{ + * id: number + * resourceId: number + * permissionLevel: number + * principal: Principal, + * createdTimestamp: number, + * lastUpdatedTimestamp: number, + * }} ResourceMember + * + */ + +axios.defaults.baseURL = generateUrl("/apps/organization_folders") + +export default { + /** + * + * @param {number|string} resourceId Resource id + * @param {string} include + * @return {Promise} + */ + getResource(resourceId, include = "model") { + return axios.get(`/resources/${resourceId}`, { params: { include } }).then((res) => res.data) + }, + + /** + * @param {number|string} resourceId Resource id + * @param {{ + * name: string|undefined + * active: boolean|undefined + * inheritManagers: boolean|undefined + * membersAclPermission: number|undefined + * managersAclPermission: number|undefined + * inheritedAclPermission: number|undefined + * }} updateResourceDto UpdateResourceDto + * @return {Promise} + */ + updateResource(resourceId, updateGroupDto) { + return axios.put(`/resources/${resourceId}`, { ...updateGroupDto }).then((res) => res.data) + }, + + /** + * + * @param {number|string} resourceId Resource id + * @return {Promise>} + */ + getResourceMembers(resourceId) { + return axios.get(`/resources/${resourceId}/members`, {}).then((res) => res.data) + }, +} diff --git a/src/components/ConfirmDeleteDialog.vue b/src/components/ConfirmDeleteDialog.vue new file mode 100644 index 0000000..ae02152 --- /dev/null +++ b/src/components/ConfirmDeleteDialog.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/components/MemberList/MemberList.vue b/src/components/MemberList/MemberList.vue new file mode 100644 index 0000000..e5ea008 --- /dev/null +++ b/src/components/MemberList/MemberList.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/src/components/MemberList/MemberListItem.vue b/src/components/MemberList/MemberListItem.vue new file mode 100644 index 0000000..5159991 --- /dev/null +++ b/src/components/MemberList/MemberListItem.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/MemberList/index.js b/src/components/MemberList/index.js new file mode 100644 index 0000000..a7bf749 --- /dev/null +++ b/src/components/MemberList/index.js @@ -0,0 +1 @@ +export { default } from "./MemberList.vue" diff --git a/src/components/Permissions/Permissions.vue b/src/components/Permissions/Permissions.vue new file mode 100644 index 0000000..876fb30 --- /dev/null +++ b/src/components/Permissions/Permissions.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/src/components/Permissions/PermissionsInputRow.vue b/src/components/Permissions/PermissionsInputRow.vue new file mode 100644 index 0000000..1913a6a --- /dev/null +++ b/src/components/Permissions/PermissionsInputRow.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/src/components/Permissions/index.js b/src/components/Permissions/index.js new file mode 100644 index 0000000..7f34305 --- /dev/null +++ b/src/components/Permissions/index.js @@ -0,0 +1 @@ +export { default } from "./Permissions.vue" diff --git a/src/header.js b/src/header.js new file mode 100644 index 0000000..9d27d02 --- /dev/null +++ b/src/header.js @@ -0,0 +1,69 @@ +import Vue from "vue"; +import { Header } from '@nextcloud/files'; +import { createPinia } from "pinia"; +import { subscribe } from '@nextcloud/event-bus'; + +import router from "./router.js"; +import HeaderComponent from "./Header.vue"; + +import { useCurrentDirStore } from "./stores/current-dir.js"; + +let vm = null; +let currentFolderFileid = null; + +const pinia = createPinia(); + +const OrganizationFoldersHeader = new Header({ + id: 'organization_folders', + order: 2, + + enabled(_, view) { + return view.id === 'files' || view.id === 'favorites'; + }, + + async render(el, folder, view) { + if(!vm) { + el.id = "organization_folders"; + vm = new Vue({ + el, + router, + pinia, + render: h => h(HeaderComponent), + }); + } else { + // the outer vue instance calling will have replaced el, so we need to "re-mount" our vue instance + el.replaceWith(vm.$el); + } + + const currentDir = useCurrentDirStore(); + currentDir.updatePath(folder?.path); + currentFolderFileid = folder?.fileid; + }, + + updated(folder) { + const currentDir = useCurrentDirStore(); + currentDir.updatePath(folder?.path); + currentFolderFileid = folder?.fileid; + }, +}) + +// Handle empty folders seperately, because Headers are not rendered in this case :/ +subscribe("files:list:updated", ({view, folder, contents}) => { + if(contents.length === 0) { + // only re-render, if open folder has changed + if(folder && currentFolderFileid !== folder.fileid) { + const fileListHeader = document.querySelector(".app-files .files-list__header"); + + const vueContainer = document.createElement("div"); + vueContainer.style.width = "100%"; + + console.log("vueContainer.nextElementSibling", fileListHeader.nextElementSibling); + + fileListHeader.parentNode.insertBefore(vueContainer, fileListHeader.nextElementSibling); + + OrganizationFoldersHeader.render(vueContainer, folder, view); + } + } +}); + +export default OrganizationFoldersHeader; \ No newline at end of file diff --git a/src/helpers/file-size-helpers.js b/src/helpers/file-size-helpers.js new file mode 100644 index 0000000..958f589 --- /dev/null +++ b/src/helpers/file-size-helpers.js @@ -0,0 +1,12 @@ +/** + * + * @param {number} bytes filesize in bytes + * @return {string} file size in appropriate unit + */ +export function bytesToSize(bytes) { + const sizes = ["Bytes", "KB", "MB", "GB", "TB"] + if (bytes === 0) return "0 Byte" + const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))) + return Math.round(bytes / Math.pow(1024, i), 2) + " " + sizes[i] + } + \ No newline at end of file diff --git a/src/helpers/permission-helpers.js b/src/helpers/permission-helpers.js new file mode 100644 index 0000000..c2a35ec --- /dev/null +++ b/src/helpers/permission-helpers.js @@ -0,0 +1,52 @@ +/** + * bit names from least to most significant bit + */ +const bitNames = ["READ", "UPDATE", "CREATE", "DELETE", "SHARE"] + +/** + * mask value + * 0 0 INHERIT_DENY: Denied (Inherited permission) + * 0 1 INHERIT_ALLOW: Allowed (Inherited permission) + * 1 0 SELF_DENY: Denied + * 1 1 SELF_ALLOW: Allowed + */ +const bitStates = ["INHERIT_DENY", "INHERIT_ALLOW", "SELF_DENY", "SELF_ALLOW"] + +/** + * @param {number} value integer value + * @param {number} bit the nth bit to check, zero indexed + * @return {boolean} if bit is set + */ +const bitValue = (value, bit) => (value >> bit) % 2 !== 0 + +const bitState = (value, mask, bit) => { + const valueBit = bitValue(value, bit) + const maskBit = bitValue(mask, bit) + const i = valueBit | (maskBit << 1) + return bitStates[i] +} + +/** + * + * @param {number} value permission value 0 - 31 + * @param {string} bitName READ UPDATE CREATE DELETE SHARE + */ +export function toggleBit(value, bitName) { + return value ^ (1 << bitNames.indexOf(bitName)) +} + +export const isBitSet = (value, bitName) => { + return bitValue(value, bitNames.indexOf(bitName)) +} + +/** + * @param {number} value + * @param {number} mask + */ +export function calcBits(value, mask) { + const maskedValue = value & mask + return Object.fromEntries(bitNames.map((key, index) => ([key, { + value: bitValue(maskedValue, index), + state: bitState(value, mask, index), + }]))) +} diff --git a/src/helpers/validation.js b/src/helpers/validation.js new file mode 100644 index 0000000..92cae29 --- /dev/null +++ b/src/helpers/validation.js @@ -0,0 +1,10 @@ +/** + * + * @param {string} str + */ +export function validResourceName(str) { + /* eslint-disable no-useless-escape */ + const specialChars = /[`!@#$%^()+=\[\]{};'"\\|,.<>\/?~]/ + return !specialChars.test(str) + } + \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..f928be0 --- /dev/null +++ b/src/main.js @@ -0,0 +1,40 @@ +import Vue from "vue"; +import { PiniaVuePlugin } from "pinia"; +import { registerFileListHeaders } from '@nextcloud/files'; +import { translate as t, translatePlural as n } from "@nextcloud/l10n"; +import { generateFilePath } from "@nextcloud/router"; +import Tooltip from "@nextcloud/vue/dist/Directives/Tooltip.js"; + +import { initFilesClient } from "./davClient.js"; +import Header from "./header.js"; +import api from "./api.js"; + +import '@nextcloud/dialogs/style.css'; + +// eslint-disable-next-line +__webpack_public_path__ = generateFilePath(appName, '', 'js/'); + +// Adding translations to the whole app +Vue.mixin({ + methods: { + t, + n, + }, +}); + +// Recommendation by @nextcloud/vue +Vue.prototype.OC = window.OC; +Vue.prototype.OCA = window.OCA; + +Vue.directive("tooltip", Tooltip); + +// Pinia +Vue.use(PiniaVuePlugin); + +window.api = api; + +window.addEventListener('DOMContentLoaded', () => { + initFilesClient(OC.Files.getClient()); +}); + +registerFileListHeaders(Header); \ No newline at end of file diff --git a/src/router.js b/src/router.js new file mode 100644 index 0000000..60cff1e --- /dev/null +++ b/src/router.js @@ -0,0 +1,24 @@ +import Vue from "vue"; +import Router from "vue-router"; + +import ResourceSettings from "./views/ResourceSettings.vue"; + +Vue.use(Router); + +const router = new Router({ + mode: 'abstract', + routes: [ + { + path: "/resource/:resourceId", + name: "resource-settings", + component: ResourceSettings, + props: (route) => ( + { + resourceId: Number.parseInt(route.params.resourceId, 10) || undefined, + } + ), + }, + ], +}); + +export default router; \ No newline at end of file diff --git a/src/stores/current-dir.js b/src/stores/current-dir.js new file mode 100644 index 0000000..b814a43 --- /dev/null +++ b/src/stores/current-dir.js @@ -0,0 +1,55 @@ +import { defineStore } from "pinia"; +import { computed } from "vue"; +import { getFolderProperties } from "../davClient.js"; +import api from "../api.js"; + +export const useCurrentDirStore = defineStore("currentDir", { + state: () => ({ + loading: false, + path: "", + organizationFolderId: null, + organizationFolderResourceId: null, + userManagerPermissions: null, + }), + actions: { + /** + * set the path of the current directory and fetch organization folders info from dav api + * + * @param {string} path current path + */ + async updatePath(path) { + this.loading = true; + this.path = path + + let { fileInfo } = await getFolderProperties(path) + .catch(() => { + this.organizationFolderId = false; + this.organizationFolderResourceId = false; + this.userManagerPermissions = false; + this.loading = false; + }); + + console.log("fileInfo", fileInfo); + + if(fileInfo) { + this.organizationFolderId = fileInfo.organizationFolderId; + this.organizationFolderResourceId = fileInfo.organizationFolderResourceId; + this.userManagerPermissions = fileInfo.userManagerPermissions; + } else { + this.organizationFolderId = false; + this.organizationFolderResourceId = false; + this.userManagerPermissions = false; + } + + this.loading = false; + }, + + async fetchCurrentResource() { + if(this.organizationFolderResourceId) { + return await api.getResource(this.organizationFolderResourceId); + } else { + return false; + } + } + }, +}) diff --git a/src/views/ResourceSettings.vue b/src/views/ResourceSettings.vue new file mode 100644 index 0000000..5936d32 --- /dev/null +++ b/src/views/ResourceSettings.vue @@ -0,0 +1,165 @@ + + + + + \ No newline at end of file