mirror of
https://github.com/verdigado/organization_folders.git
synced 2024-11-23 05:00:27 +01:00
initial commit of GUI
This commit is contained in:
parent
b64ae41cd0
commit
f07b9953e3
20 changed files with 1416 additions and 0 deletions
70
img/deny.svg
Normal file
70
img/deny.svg
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
viewbox="0 0 16 16"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
id="svg3791"
|
||||||
|
sodipodi:docname="deny.svg"
|
||||||
|
inkscape:version="0.92.4 5da689c313, 2019-01-14">
|
||||||
|
<metadata
|
||||||
|
id="metadata3797">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs3795" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
id="namedview3793"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="18.296388"
|
||||||
|
inkscape:cx="-13.154265"
|
||||||
|
inkscape:cy="1.9505725"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="26"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="g3789" />
|
||||||
|
<g
|
||||||
|
stroke-width="2"
|
||||||
|
stroke="#000"
|
||||||
|
stroke-linecap="round"
|
||||||
|
fill="none"
|
||||||
|
id="g3789">
|
||||||
|
<path
|
||||||
|
d="M 11.678082,4.5280679 4.5924509,11.613699"
|
||||||
|
id="path3787"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||||
|
<ellipse
|
||||||
|
id="path3993"
|
||||||
|
cx="7.9999995"
|
||||||
|
cy="8"
|
||||||
|
rx="5.9096346"
|
||||||
|
ry="5.9096351"
|
||||||
|
style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
59
src/Header.vue
Normal file
59
src/Header.vue
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject, watch, computed, nextTick } from "vue";
|
||||||
|
import NcButton from "@nextcloud/vue/dist/Components/NcButton.js";
|
||||||
|
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon.js";
|
||||||
|
import FolderCog from "vue-material-design-icons/FolderCog.vue";
|
||||||
|
|
||||||
|
import router from "./router.js";
|
||||||
|
import { useCurrentDirStore } from "./stores/current-dir.js";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
|
||||||
|
console.log("router loggg", router, router.currentRoute);
|
||||||
|
|
||||||
|
const currentDir = useCurrentDirStore();
|
||||||
|
|
||||||
|
const modalOpen = ref(false);
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
if(currentDir.userManagerPermissions) {
|
||||||
|
if(currentDir.organizationFolderResourceId) {
|
||||||
|
router.push({
|
||||||
|
path: '/resource/' + currentDir.organizationFolderResourceId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
router.push({
|
||||||
|
path: '/organizationFolder/' + currentDir.organizationFolderId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
modalOpen.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="currentDir.userManagerPermissions" class="toolbar">
|
||||||
|
<NcButton :disabled="currentDir.loading"
|
||||||
|
type="primary"
|
||||||
|
@click="openModal">
|
||||||
|
<template #icon>
|
||||||
|
<NcLoadingIcon v-if="currentDir.loading" />
|
||||||
|
<FolderCog v-else :size="20" />
|
||||||
|
</template>
|
||||||
|
Ordner und Berechtigungen Verwalten
|
||||||
|
</NcButton>
|
||||||
|
<Modal :open.sync="modalOpen" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
margin: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
114
src/Modal.vue
Normal file
114
src/Modal.vue
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, ref } from "vue";
|
||||||
|
import { useRouter, useRoute } from 'vue2-helpers/vue-router';
|
||||||
|
|
||||||
|
import NcModal from "@nextcloud/vue/dist/Components/NcModal.js";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:open"]);
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
emit("update:open", false)
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const currentView = ref(null);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<NcModal v-if="props.open"
|
||||||
|
size="large"
|
||||||
|
class="organizationfolders-dialog"
|
||||||
|
label-id="Organization Folder Management"
|
||||||
|
:out-transition="true"
|
||||||
|
:has-next="false"
|
||||||
|
:has-previous="false"
|
||||||
|
@close="closeDialog">
|
||||||
|
<router-view />
|
||||||
|
</NcModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.organizationfolders-dialog .modal-container {
|
||||||
|
width: unset !important;
|
||||||
|
height: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material_you .list-item, .material_you.app-navigation-entry > .app-navigation-entry-button {
|
||||||
|
margin-bottom: 6px !important;
|
||||||
|
border-radius: 24px !important;
|
||||||
|
background-color: var(--color-background-dark)
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-navigation-entry.material_you > .app-navigation-entry-button {
|
||||||
|
line-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-navigation-entry.material_you > .app-navigation-entry-button > .app-navigation-entry-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
flex-basis: 60px;
|
||||||
|
background-size: 44px 44px;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-navigation-entry.material_you > .app-navigation-entry-button > .app-navigation-new-item__name {
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material_you .list-item {
|
||||||
|
--default-clickable-area: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material_you .list-item:hover, .material_you .list-item:focus, .material_you.app-navigation-entry:hover > .app-navigation-entry-button, .material_you.app-navigation-entry:focus > .app-navigation-entry-button {
|
||||||
|
background-color: var(--color-primary-light-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material_you .list-item {
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material_you.list-item__wrapper.listItemSelectable:not(.selected) .list-item {
|
||||||
|
border: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.material_you.list-item__wrapper.listItemSelectable.selected .list-item {
|
||||||
|
/*background-color: var(--color-primary-light) !important;*/
|
||||||
|
border: 3px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.material_you .list-item:hover .list-item-content__main .list-item-content__name {
|
||||||
|
color: var(--color-primary-light-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.material_you .list-item:hover .list-item__anchor > .material-design-icon svg {
|
||||||
|
fill: var(--color-primary-light-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.material_you .app-navigation-entry-div {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material_you.app-navigation-entry:hover {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material_you .app-navigation-new-item__title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: var(--default-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For divs required for vue, but irrelevant in the layout */
|
||||||
|
.ignoreForLayout {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
</style>
|
147
src/ModalView.vue
Normal file
147
src/ModalView.vue
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, reactive, nextTick } from "vue";
|
||||||
|
|
||||||
|
import NcButton from "@nextcloud/vue/dist/Components/NcButton.js";
|
||||||
|
import KeyboardBackspace from "vue-material-design-icons/KeyboardBackspace.vue";
|
||||||
|
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon.js";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
hasBackButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
hasNextStepButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
hasLastStepButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
nextStepButtonEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
lastStepButtonEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["back-button-pressed", "next-step-button-pressed", "last-step-button-pressed"]);
|
||||||
|
|
||||||
|
const backButtonPressed = () => {
|
||||||
|
emit("back-button-pressed");
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStepButtonPressed = () => {
|
||||||
|
emit("next-step-button-pressed");
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastStepButtonPressed = () => {
|
||||||
|
emit("last-step-button-pressed");
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal__content">
|
||||||
|
<div class="modal__title">
|
||||||
|
<NcButton
|
||||||
|
type="secondary"
|
||||||
|
class="btn-back"
|
||||||
|
aria-label="Zurück"
|
||||||
|
@click="backButtonPressed">
|
||||||
|
<template #icon>
|
||||||
|
<KeyboardBackspace />
|
||||||
|
</template>
|
||||||
|
</NcButton>
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="modal__loading">
|
||||||
|
<NcLoadingIcon :size="64" />
|
||||||
|
</div>
|
||||||
|
<div v-if="!loading" class="modal__main ignoreForLayout">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="!loading && (hasLastStepButton || nextStepButtonEnabled)" class="modal__footer">
|
||||||
|
<NcButton
|
||||||
|
:style="{visibility: hasLastStepButton ? 'visible' : 'hidden'}"
|
||||||
|
type="secondary"
|
||||||
|
:disabled="!lastStepButtonEnabled"
|
||||||
|
aria-label="Zurück"
|
||||||
|
@click="lastStepButtonPressed">
|
||||||
|
Zurück
|
||||||
|
</NcButton>
|
||||||
|
<NcButton
|
||||||
|
v-if="hasNextStepButton"
|
||||||
|
type="primary"
|
||||||
|
:disabled="!nextStepButtonEnabled"
|
||||||
|
aria-label="Weiter"
|
||||||
|
@click="nextStepButtonPressed">
|
||||||
|
Weiter
|
||||||
|
</NcButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal__title {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__title h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__footer{
|
||||||
|
margin-top: 16px;
|
||||||
|
height: 50px;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__footer__restore {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__content {
|
||||||
|
padding: 50px;
|
||||||
|
min-width: 75vw;
|
||||||
|
height: calc(100% - 100px);
|
||||||
|
overflow: scroll;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.modal__loading {
|
||||||
|
padding: 50px;
|
||||||
|
min-width: 75vw;
|
||||||
|
height: calc(100% - 100px);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.organizationfolders-dialog .modal-container {
|
||||||
|
width: unset !important;
|
||||||
|
height: 90%;
|
||||||
|
}
|
||||||
|
</style>
|
73
src/api.js
Normal file
73
src/api.js
Normal file
|
@ -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<Resource>}
|
||||||
|
*/
|
||||||
|
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<Resource>}
|
||||||
|
*/
|
||||||
|
updateResource(resourceId, updateGroupDto) {
|
||||||
|
return axios.put(`/resources/${resourceId}`, { ...updateGroupDto }).then((res) => res.data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number|string} resourceId Resource id
|
||||||
|
* @return {Promise<Array<ResourceMember>>}
|
||||||
|
*/
|
||||||
|
getResourceMembers(resourceId) {
|
||||||
|
return axios.get(`/resources/${resourceId}/members`, {}).then((res) => res.data)
|
||||||
|
},
|
||||||
|
}
|
92
src/components/ConfirmDeleteDialog.vue
Normal file
92
src/components/ConfirmDeleteDialog.vue
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import NcTextField from "@nextcloud/vue/dist/Components/NcTextField.js";
|
||||||
|
import NcModal from "@nextcloud/vue/dist/Components/NcModal.js";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: "Löschen",
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
matchText: {
|
||||||
|
type: String,
|
||||||
|
default: "löschen",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
const confirmText = ref("");
|
||||||
|
|
||||||
|
const openDialog = () => {
|
||||||
|
open.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot name="activator" :open="openDialog">
|
||||||
|
<button type="button" @click="openDialog">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</slot>
|
||||||
|
<NcModal v-if="open"
|
||||||
|
class="modal"
|
||||||
|
:out-transition="true"
|
||||||
|
:has-next="false"
|
||||||
|
:has-previous="false"
|
||||||
|
@close="closeDialog">
|
||||||
|
<div class="modal__content">
|
||||||
|
<div class="modal__title">
|
||||||
|
<h1>
|
||||||
|
{{ props.title }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<slot name="content" />
|
||||||
|
<p>
|
||||||
|
Gib hier als Bestätigung "<span style="user-select: all;">{{ props.matchText }}</span>" ein.
|
||||||
|
</p>
|
||||||
|
<NcTextField class="confirmText"
|
||||||
|
:value.sync="confirmText"
|
||||||
|
style=" --color-border-maxcontrast: #949494;" />
|
||||||
|
<slot name="delete-button" :close="closeDialog" :disabled="confirmText !== props.matchText">
|
||||||
|
<button type="button">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NcModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.confirmText {
|
||||||
|
margin: 1rem 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__title {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__title h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__content {
|
||||||
|
margin: 50px;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
162
src/components/MemberList/MemberList.vue
Normal file
162
src/components/MemberList/MemberList.vue
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
<script setup>
|
||||||
|
import NcEmptyContent from "@nextcloud/vue/dist/Components/NcEmptyContent.js"
|
||||||
|
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon.js"
|
||||||
|
import NcActions from "@nextcloud/vue/dist/Components/NcActions.js"
|
||||||
|
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton.js"
|
||||||
|
import NcButton from "@nextcloud/vue/dist/Components/NcButton.js"
|
||||||
|
import { showError } from "@nextcloud/dialogs"
|
||||||
|
//import MemberListNewItem from "./MemberListNewItem.vue"
|
||||||
|
import MemberListItem from "./MemberListItem.vue"
|
||||||
|
import Plus from "vue-material-design-icons/Plus.vue"
|
||||||
|
import Close from "vue-material-design-icons/Close.vue"
|
||||||
|
import HelpCircle from "vue-material-design-icons/HelpCircle.vue"
|
||||||
|
import api from "../../api.js"
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
members: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref(undefined);
|
||||||
|
const newItemComponent = ref(null);
|
||||||
|
const addMenuOpen = ref(false);
|
||||||
|
|
||||||
|
const setNewItemComponent = (name) => {
|
||||||
|
this.newItemComponent.value = name
|
||||||
|
this.addMenuOpen.value = false
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMember = async (memberId) => {
|
||||||
|
this.loading.value = true
|
||||||
|
try {
|
||||||
|
api.deleteGroupMember(this.groupId, memberId)
|
||||||
|
//this.members.value = this.members.filter((m) => m.id !== memberId)
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message)
|
||||||
|
} finally {
|
||||||
|
this.loading.value = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMember = async (memberId, changes) => {
|
||||||
|
this.loading.value = true
|
||||||
|
try {
|
||||||
|
const member = await api.updateGroupMember(this.groupId, memberId, changes)
|
||||||
|
this.members = this.members.map((m) => m.id === member.id ? member : m)
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message)
|
||||||
|
} finally {
|
||||||
|
this.loading.value = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMember = async ({ mappingId, mappingType }) => {
|
||||||
|
this.loading.value = true
|
||||||
|
try {
|
||||||
|
const _member = await api.addGroupMember(this.groupId, {
|
||||||
|
mappingType,
|
||||||
|
mappingId,
|
||||||
|
type: "member",
|
||||||
|
})
|
||||||
|
this.members.push(_member)
|
||||||
|
this.setNewItemComponent(null)
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="title">
|
||||||
|
<h3>Mitglieder</h3>
|
||||||
|
<!--<NcActions :disabled="!!newItemComponent" type="secondary">
|
||||||
|
<template #icon>
|
||||||
|
<Plus :size="20" />
|
||||||
|
</template>
|
||||||
|
<NcActionButton icon="icon-group" close-after-click @click="setNewItemComponent('new_item')">
|
||||||
|
Benutzer/Gruppe hinzufügen
|
||||||
|
</NcActionButton>
|
||||||
|
<NcActionButton icon="icon-group" close-after-click @click="setNewItemComponent('new_role_item')">
|
||||||
|
Organisation Rolle hinzufügen
|
||||||
|
</NcActionButton>
|
||||||
|
</NcActions>-->
|
||||||
|
</div>
|
||||||
|
<!--<div v-if="newItemComponent" class="new-item">
|
||||||
|
<NcButton type="tertiary" @click="setNewItemComponent(null)">
|
||||||
|
<template #icon>
|
||||||
|
<Close />
|
||||||
|
</template>
|
||||||
|
</NcButton>
|
||||||
|
<MemberListNewItem v-if="newItemComponent === 'new_item'" :group-id="groupId" @selected="addMember" />
|
||||||
|
</div>-->
|
||||||
|
<table>
|
||||||
|
<thead style="display: contents;">
|
||||||
|
<tr>
|
||||||
|
<th />
|
||||||
|
<th>Name</th>
|
||||||
|
<th>
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span>Typ</span>
|
||||||
|
<HelpCircle v-tooltip="'Für Admins gelten die oben ausgewählten Ordneradministrator*innen Berechtigungen, für Mitglieder die Ordnermitglieder Berechtigungen. Admins haben auf diese Einstellungen Zugriff.'" style="margin-left: 5px;" :size="15" />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>Aktion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody style="display: contents">
|
||||||
|
<tr v-if="loading">
|
||||||
|
<td colspan="4" style="grid-column-start: 1; grid-column-end: 5">
|
||||||
|
<NcLoadingIcon :size="50" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!loading && !members.length">
|
||||||
|
<td colspan="4" style="grid-column-start: 1; grid-column-end: 5">
|
||||||
|
<NcEmptyContent title="Keine Gruppenmitglieder" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<MemberListItem v-for="member in members"
|
||||||
|
:key="member.id"
|
||||||
|
:member="member"
|
||||||
|
@update="updateMember"
|
||||||
|
@delete="deleteMember" />
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content minmax(30px, auto) max-content max-content;
|
||||||
|
}
|
||||||
|
table tr {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
table td, table th {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
.new-item {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
</style>
|
95
src/components/MemberList/MemberListItem.vue
Normal file
95
src/components/MemberList/MemberListItem.vue
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<script setup>
|
||||||
|
import Delete from "vue-material-design-icons/Delete.vue"
|
||||||
|
import NcButton from "@nextcloud/vue/dist/Components/NcButton.js"
|
||||||
|
import NcAvatar from "@nextcloud/vue/dist/Components/NcAvatar.js"
|
||||||
|
import ChevronRight from "vue-material-design-icons/ChevronRight.vue"
|
||||||
|
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
member: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const friendlyNameParts = computed(() => props.member.principal.split(" / "));
|
||||||
|
|
||||||
|
const emit = defineEmits(["update", "delete"]);
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{ label: "Mitglied", value: 1 },
|
||||||
|
{ label: "Manager", value: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const onTypeSelected = (e) => {
|
||||||
|
emit("update", props.member.id, {
|
||||||
|
type: e.target.value,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteClicked = (e) => {
|
||||||
|
emit("delete", props.member.id)
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<NcAvatar :user="props.member.type === 1 ? props.member.principal : undefined"
|
||||||
|
:disabled-menu="true"
|
||||||
|
:disabled-tooltip="true"
|
||||||
|
:icon-class="props.member.type === 2 ? 'icon-group' : undefined" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="friendlyNameParts">
|
||||||
|
<div v-for="(friendlyNamePart, index) of friendlyNameParts" :key="'breadcrumb-' + friendlyNamePart" class="friendlyNamePartDiv">
|
||||||
|
<p v-tooltip="friendlyNamePart" class="friendlyNamePartP">
|
||||||
|
{{ friendlyNamePart }}
|
||||||
|
</p>
|
||||||
|
<ChevronRight v-if="index !== friendlyNameParts.length - 1" :size="20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select :value="props.member.permissionLevel" @input="onTypeSelected">
|
||||||
|
<option v-for="{ label, value} in typeOptions" :key="value" :value="value">
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<NcButton type="tertiary-no-background" @click="onDeleteClicked">
|
||||||
|
<template #icon>
|
||||||
|
<Delete :size="20" />
|
||||||
|
</template>
|
||||||
|
</NcButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
td {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.friendlyNameParts {
|
||||||
|
display: inline-flex;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: clip;
|
||||||
|
}
|
||||||
|
.friendlyNamePartP {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
}
|
||||||
|
.friendlyNamePartP:not(:last-child) {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.friendlyNamePartDiv {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
.friendlyNamePartDiv:last-child {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
1
src/components/MemberList/index.js
Normal file
1
src/components/MemberList/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "./MemberList.vue"
|
93
src/components/Permissions/Permissions.vue
Normal file
93
src/components/Permissions/Permissions.vue
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import PermissionsInputRow from "./PermissionsInputRow.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
resource: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(["permissionUpdated"]);
|
||||||
|
|
||||||
|
const permissionGroups = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: "managersAclPermission",
|
||||||
|
label: "Resourcenadministrator*innen",
|
||||||
|
value: props.resource.managersAclPermission,
|
||||||
|
mask: 31,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "membersAclPermission",
|
||||||
|
label: "Resourcenmitglieder",
|
||||||
|
value: props.resource.membersAclPermission,
|
||||||
|
mask: 31,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "inheritedAclPermission",
|
||||||
|
label: "Vererbte Berechtigungen",
|
||||||
|
value: props.resource.inheritedAclPermission,
|
||||||
|
mask: 31,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const permissionUpdated = async (field, value) => {
|
||||||
|
emit("permissionUpdated", { field, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th />
|
||||||
|
<th v-tooltip="t('groupfolders', 'Read')" class="state-column">
|
||||||
|
{{ t('groupfolders', 'Read') }}
|
||||||
|
</th>
|
||||||
|
<th v-tooltip="t('groupfolders', 'Write')" class="state-column">
|
||||||
|
{{ t('groupfolders', 'Write') }}
|
||||||
|
</th>
|
||||||
|
<th v-tooltip="t('groupfolders', 'Create')" class="state-column">
|
||||||
|
{{ t('groupfolders', 'Create') }}
|
||||||
|
</th>
|
||||||
|
<th v-tooltip="t('groupfolders', 'Delete')" class="state-column">
|
||||||
|
{{ t('groupfolders', 'Delete') }}
|
||||||
|
</th>
|
||||||
|
<th v-tooltip="t('groupfolders', 'Share')" class="state-column">
|
||||||
|
{{ t('groupfolders', 'Share') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<PermissionsInputRow v-for="{ field, label, mask, value} in permissionGroups"
|
||||||
|
:key="field"
|
||||||
|
:label="label"
|
||||||
|
:mask="mask"
|
||||||
|
:value="value"
|
||||||
|
@change="(val) => permissionUpdated(field, val)" />
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
table td, table th {
|
||||||
|
padding: 0
|
||||||
|
}
|
||||||
|
.state-column {
|
||||||
|
text-align: center;
|
||||||
|
width: 44px !important;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
thead .state-column {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
82
src/components/Permissions/PermissionsInputRow.vue
Normal file
82
src/components/Permissions/PermissionsInputRow.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { calcBits, toggleBit } from "../../helpers/permission-helpers.js";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
mask: {
|
||||||
|
type: Number,
|
||||||
|
default: 31,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(["change"]);
|
||||||
|
|
||||||
|
const calcBitButtonProps = (bitName, bitState) => {
|
||||||
|
const states = {
|
||||||
|
INHERIT_DENY: {
|
||||||
|
tooltipText: t("groupfolders", "Denied (Inherited permission)"),
|
||||||
|
className: "icon-deny inherited",
|
||||||
|
},
|
||||||
|
INHERIT_ALLOW: {
|
||||||
|
tooltipText: t("groupfolders", "Allowed (Inherited permission)"),
|
||||||
|
className: "icon-checkmark inherited",
|
||||||
|
},
|
||||||
|
SELF_DENY: {
|
||||||
|
tooltipText: t("groupfolders", "Denied"),
|
||||||
|
className: "icon-deny",
|
||||||
|
},
|
||||||
|
SELF_ALLOW: {
|
||||||
|
tooltipText: t("groupfolders", "Allowed"),
|
||||||
|
className: "icon-checkmark",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...states[bitState],
|
||||||
|
bitName,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bitButtonProps = computed(() => Object.entries(calcBits(props.value, props.mask)).map(([bitName, { state }]) => calcBitButtonProps(bitName, state)));
|
||||||
|
|
||||||
|
const onClick = (bitName) => emit("change", toggleBit(props.value, bitName));
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr>
|
||||||
|
<td v-tooltip="props.label">
|
||||||
|
{{ props.label }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td v-for="({ bitName, className, tooltipText }) in bitButtonProps" :key="bitName">
|
||||||
|
<button v-tooltip="tooltipText"
|
||||||
|
:class="className"
|
||||||
|
@click="() => onClick(bitName)" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
button {
|
||||||
|
height: 24px;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
height: 24px;
|
||||||
|
border-color: var(--color-primary, #0082c9);
|
||||||
|
}
|
||||||
|
.icon-deny {
|
||||||
|
background-image: url('../../../img/deny.svg');
|
||||||
|
}
|
||||||
|
.inherited {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
1
src/components/Permissions/index.js
Normal file
1
src/components/Permissions/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default } from "./Permissions.vue"
|
69
src/header.js
Normal file
69
src/header.js
Normal file
|
@ -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;
|
12
src/helpers/file-size-helpers.js
Normal file
12
src/helpers/file-size-helpers.js
Normal file
|
@ -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]
|
||||||
|
}
|
||||||
|
|
52
src/helpers/permission-helpers.js
Normal file
52
src/helpers/permission-helpers.js
Normal file
|
@ -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),
|
||||||
|
}])))
|
||||||
|
}
|
10
src/helpers/validation.js
Normal file
10
src/helpers/validation.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
*/
|
||||||
|
export function validResourceName(str) {
|
||||||
|
/* eslint-disable no-useless-escape */
|
||||||
|
const specialChars = /[`!@#$%^()+=\[\]{};'"\\|,.<>\/?~]/
|
||||||
|
return !specialChars.test(str)
|
||||||
|
}
|
||||||
|
|
40
src/main.js
Normal file
40
src/main.js
Normal file
|
@ -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);
|
24
src/router.js
Normal file
24
src/router.js
Normal file
|
@ -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;
|
55
src/stores/current-dir.js
Normal file
55
src/stores/current-dir.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
165
src/views/ResourceSettings.vue
Normal file
165
src/views/ResourceSettings.vue
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, computed } from "vue";
|
||||||
|
import { loadState } from '@nextcloud/initial-state';
|
||||||
|
import { NcLoadingIcon, NcCheckboxRadioSwitch, NcButton, NcTextField } from '@nextcloud/vue';
|
||||||
|
|
||||||
|
import BackupRestore from "vue-material-design-icons/BackupRestore.vue";
|
||||||
|
import Delete from "vue-material-design-icons/Delete.vue";
|
||||||
|
|
||||||
|
import MemberList from "../components/MemberList/index.js";
|
||||||
|
import Permissions from "../components/Permissions/index.js";
|
||||||
|
import ConfirmDeleteDialog from "../components/ConfirmDeleteDialog.vue";
|
||||||
|
import ModalView from '../ModalView.vue';
|
||||||
|
import api from "../api.js";
|
||||||
|
import { validResourceName } from "../helpers/validation.js";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
resourceId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const resource = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const resourceActiveLoading = ref(false);
|
||||||
|
|
||||||
|
const currentResourceName = ref(false);
|
||||||
|
|
||||||
|
const resourceNameValid = computed(() => {
|
||||||
|
return validResourceName(currentResourceName.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveName = async () => {
|
||||||
|
resource.value = await api.updateResource(resource.value.id, { name: currentResourceName.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.resourceId, async (newResourceId) => {
|
||||||
|
loading.value = true;
|
||||||
|
resource.value = await api.getResource(newResourceId, "model+members");
|
||||||
|
currentResourceName.value = resource.value.name;
|
||||||
|
loading.value = false;
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const saveActive = async (active) => {
|
||||||
|
resourceActiveLoading.value = true;
|
||||||
|
resource.value = await api.updateResource(resource.value.id, { active });
|
||||||
|
resourceActiveLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const savePermission = async ({ field, value }) => {
|
||||||
|
resource.value = await api.updateResource(resource.value.id, {
|
||||||
|
[field]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchToSnapshotRestoreView = () => {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapshotIntegrationActive = loadState('organization_folders', 'snapshot_integration_active', false);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalView :has-back-button="true" :has-next-step-button="false" :has-last-step-button="false" :title="'Resource Settings'" :loading="loading" v-slot="">
|
||||||
|
<h3>Eigenschaften</h3>
|
||||||
|
<div class="name-input-container">
|
||||||
|
<NcTextField :value.sync="currentResourceName"
|
||||||
|
:error="!resourceNameValid"
|
||||||
|
:label-visible="!resourceNameValid"
|
||||||
|
:label-outside="true"
|
||||||
|
:helper-text="resourceNameValid ? '' : 'Ungültiger Name'"
|
||||||
|
label="Name"
|
||||||
|
:show-trailing-button="currentResourceName !== resource.name"
|
||||||
|
trailing-button-icon="arrowRight"
|
||||||
|
style=" --color-border-maxcontrast: #949494;"
|
||||||
|
@trailing-button-click="saveName"
|
||||||
|
@blur="() => currentResourceName = currentResourceName.trim()"
|
||||||
|
@keyup.enter="saveName" />
|
||||||
|
</div>
|
||||||
|
<h3>Berechtigungen</h3>
|
||||||
|
<Permissions :resource="resource" @permissionUpdated="savePermission" />
|
||||||
|
<MemberList :members="resource?.members" />
|
||||||
|
<h3>Einstellungen</h3>
|
||||||
|
<div class="settings-group">
|
||||||
|
<NcButton v-if="snapshotIntegrationActive" @click="switchToSnapshotRestoreView">
|
||||||
|
<template #icon>
|
||||||
|
<BackupRestore />
|
||||||
|
</template>
|
||||||
|
Aus Backup wiederherstellen
|
||||||
|
</NcButton>
|
||||||
|
<div class="resource-active-button">
|
||||||
|
<NcCheckboxRadioSwitch :checked="resource.active"
|
||||||
|
:loading="resourceActiveLoading"
|
||||||
|
type="checkbox"
|
||||||
|
@update:checked="saveActive">
|
||||||
|
Resource aktiv
|
||||||
|
</NcCheckboxRadioSwitch>
|
||||||
|
</div>
|
||||||
|
<ConfirmDeleteDialog title="Ordner löschen"
|
||||||
|
:loading="loading"
|
||||||
|
button-label="Ordner löschen"
|
||||||
|
:match-text="resource.name">
|
||||||
|
<template #activator="{ open }">
|
||||||
|
<NcButton v-tooltip="resource.active ? 'Nur deaktivierte Resourcen können gelöscht werden' : undefined"
|
||||||
|
style="height: 52px;"
|
||||||
|
:disabled="resource.active"
|
||||||
|
type="error"
|
||||||
|
@click="open">
|
||||||
|
Gruppe löschen
|
||||||
|
</NcButton>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<p style="margin: 1rem 0 1rem 0">
|
||||||
|
Du bist dabei den Ordner {{ resource.name }} und den Inhalt komplett zu löschen.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template #delete-button="{ close, disabled }">
|
||||||
|
<NcButton type="warning"
|
||||||
|
:disabled="disabled || loading"
|
||||||
|
:loading="loading"
|
||||||
|
@click="() => deleteResource(close)">
|
||||||
|
<template #icon>
|
||||||
|
<NcLoadingIcon v-if="loading" />
|
||||||
|
<Delete v-else :size="20" />
|
||||||
|
</template>
|
||||||
|
Gruppe löschen
|
||||||
|
</NcButton>
|
||||||
|
</template>
|
||||||
|
</ConfirmDeleteDialog>
|
||||||
|
</div>
|
||||||
|
</ModalView>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.name-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group > :not(:last-child) {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-active-button >>> .checkbox-radio-switch__label {
|
||||||
|
/* Add primary background color like other buttons */
|
||||||
|
background-color: var(--color-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue