mirror of
https://github.com/verdigado/organization_folders.git
synced 2024-11-23 13:10:28 +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