0
0
Fork 0
mirror of https://github.com/verdigado/organization_folders.git synced 2024-12-03 18:02:50 +01:00

improved resource settings view

This commit is contained in:
Jonathan Treffler 2024-11-20 03:01:23 +01:00
parent baf5d30f8b
commit 9f14ca2179
8 changed files with 363 additions and 181 deletions

View file

@ -10,6 +10,8 @@ return [
['name' => 'resource#update', 'url' => '/resources/{resourceId}', 'verb' => 'PUT'],
['name' => 'resource_member#index', 'url' => '/resources/{resourceId}/members', 'verb' => 'GET'],
['name' => 'resource_member#create', 'url' => '/resources/{resourceId}/members', 'verb' => 'POST'],
['name' => 'resource_member#update', 'url' => '/resources/members/{id}', 'verb' => 'PUT'],
['name' => 'resource_member#destroy', 'url' => '/resources/members/{id}', 'verb' => 'DELETE'],
['name' => 'organization#getOrganizationProviders', 'url' => '/organizationProviders', 'verb' => 'GET'],
['name' => 'organization#getOrganization', 'url' => '/organizationProviders/{organizationProviderId}/organizations/{organizationId}', 'verb' => 'GET'],
['name' => 'organization#getSubOrganizations', 'url' => '/organizationProviders/{organizationProviderId}/organizations/{parentOrganizationId}/subOrganizations', 'verb' => 'GET'],

View file

@ -118,14 +118,22 @@ export default {
},
/**
* @param {number|string} resourceMemberId Resource member id
* @param {number} resourceMemberId Resource member id
* @param {{
* permissionLevel: ResourceMemberPermissionLevel
* }} createResourceMemberDto CreateResourceMemberDto
* }} updateResourceMemberDto UpdateResourceMemberDto
* @return {Promise<ResourceMember>}
*/
updateResourceMember(resourceId, createResourceMemberDto) {
return axios.post(`/resources/${resourceId}/members`, { ...createResourceMemberDto }).then((res) => res.data);
updateResourceMember(resourceMemberId, updateResourceMemberDto) {
return axios.put(`/resources/members/${resourceMemberId}`, { ...updateResourceMemberDto }).then((res) => res.data);
},
/**
* @param {number} resourceMemberId Resource member id
* @return {Promise<ResourceMember>}
*/
deleteResourceMember(resourceMemberId) {
return axios.delete(`/resources/members/${resourceMemberId}`, {}).then((res) => res.data);
},
/**

View file

@ -1,162 +0,0 @@
<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>

View file

@ -13,23 +13,23 @@ const props = defineProps({
},
});
const friendlyNameParts = computed(() => props.member.principal.split(" / "));
const friendlyNameParts = computed(() => [props.member.principal.id] /*.split(" / ")*/);
const emit = defineEmits(["update", "delete"]);
const typeOptions = [
const permissionLevelOptions = [
{ label: "Mitglied", value: 1 },
{ label: "Manager", value: 2 },
];
const onTypeSelected = (e) => {
const onPermissionLevelSelected = (e) => {
emit("update", props.member.id, {
type: e.target.value,
})
permissionLevel: parseInt(e.target.value, 10),
});
};
const onDeleteClicked = (e) => {
emit("delete", props.member.id)
emit("delete", props.member.id);
};
</script>
@ -52,8 +52,8 @@ const onDeleteClicked = (e) => {
</div>
</td>
<td>
<select :value="props.member.permissionLevel" @input="onTypeSelected">
<option v-for="{ label, value} in typeOptions" :key="value" :value="value">
<select :value="props.member.permissionLevel" @input="onPermissionLevelSelected">
<option v-for="{ label, value} in permissionLevelOptions" :key="value" :value="value">
{{ label }}
</option>
</select>

View file

@ -0,0 +1,144 @@
<template>
<div class="input-row">
<div v-for="(level, levelIndex) in levels"
:key="'level-' + levelIndex"
style="display: contents;"
@input="event => onSelection(levelIndex, event.target.value)">
<select>
<option value="" selected />
<option v-for="(item, itemIndex) in level" :key="'option-' + itemIndex + '-' + item.prefix" :value="item.type + '_' + item.id">
{{ item.friendlyName }}
</option>
</select>
<ChevronRight v-if="levelIndex !== levels.length - 1" :size="20" />
</div>
<NcButton :disabled="!validSelection"
@click="onSave">
<template #icon>
<Plus />
</template>
Hinzufügen
</NcButton>
</div>
</template>
<script>
import NcButton from "@nextcloud/vue/dist/Components/NcButton.js"
import Plus from "vue-material-design-icons/Plus.vue"
import ChevronRight from "vue-material-design-icons/ChevronRight.vue"
import api from "../../api.js"
export default {
components: {
NcButton,
Plus,
ChevronRight,
},
props: {
organizationProvider: {
type: String,
required: true,
},
},
data() {
return {
selections: [],
levels: [],
selectedRole: null,
options: [],
}
},
computed: {
validSelection() {
// valid, if not null, undefined or empty
return !!this.selectedRole
},
},
async mounted() {
// load first selection level
this.options = await this.loadSubOptions();
await this.recalculateLevels();
},
methods: {
async loadSubOptions(parent) {
const self = this.loadSubOptions;
let subOrganizations = await api.getSubOrganizations(this.organizationProvider, parent);
let roles = [];
if(parent) {
roles = await api.getRoles(this.organizationProvider, parent);
}
return [
...subOrganizations.map((subOrganization) => {
return {
type: "organization",
id: subOrganization.id,
friendlyName: subOrganization.friendlyName,
subOptions: () => new Promise((resolve, reject) => {
self(subOrganization.id).then((result) => {
resolve(result);
}).catch((err) => {
reject(err);
});
}),
};
}),
...roles.map((role) => {
return {
type: "role",
id: role.id,
friendlyName: role.friendlyName,
};
}),
];
},
async recalculateLevels() {
const levels = [this.options]
let selectedRole = null
let parent = this.options
for (let index = 0; index < this.selections.length; index++) {
const selection = this.selections[index]
const option = parent.find(option => option.type + '_' + option.id === selection);
if (option.type === "organization") {
const subOptions = await option.subOptions()
levels[index + 1] = subOptions
parent = subOptions
} else {
// reached leaf
selectedRole = option.id
break
}
}
this.selectedRole = selectedRole
this.levels = levels
},
async onSelection(level, value) {
// truncate to levels before new selection
const newSelections = this.selections.filter((_, index) => index < level);
newSelections[level] = value;
this.selections = newSelections;
this.recalculateLevels();
},
onSave() {
this.$emit("add-member", this.organizationProvider + ":" + this.selectedRole);
},
},
}
</script>
<style scoped>
.input-row {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}
</style>

View file

@ -0,0 +1,155 @@
<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 MemberListNewRole from "./MemberListNewRole.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({
resourceId: {
type: Number,
required: true,
},
members: {
type: Array,
required: true,
},
organizationProviders: {
type: Array,
required: false,
default: [],
},
});
const emit = defineEmits(["add-member", "update-member", "delete-member"]);
const loading = ref(false);
const newMemberType = ref(null);
const newMemberAdditionalParameters = ref({});
const addMenuOpen = ref(false);
const setNewMemberType = (name, additionalParameters = {}) => {
newMemberType.value = name;
newMemberAdditionalParameters.value = additionalParameters;
addMenuOpen.value = false;
};
const addMember = (principalType, principalId) => {
emit("add-member", principalType, principalId);
newMemberType.value = null;
};
const updateMember = (memberId, updateResourceMemberDto) => {
emit("update-member", memberId, updateResourceMemberDto);
};
const deleteMember = (memberId) => {
emit("delete-member", memberId);
};
</script>
<template>
<div>
<div class="title">
<h3>Mitglieder</h3>
<NcActions :disabled="!!newMemberType" type="secondary">
<template #icon>
<Plus :size="20" />
</template>
<NcActionButton icon="icon-user" close-after-click @click="setNewMemberType(api.PrincipalTypes.USER)">
Benutzer hinzufügen
</NcActionButton>
<NcActionButton icon="icon-group" close-after-click @click="setNewMemberType(api.PrincipalTypes.GROUP)">
Gruppe hinzufügen
</NcActionButton>
<NcActionButton v-for="organizationProvider of organizationProviders"
:key="organizationProvider"
icon="icon-group"
close-after-click
@click="setNewMemberType(api.PrincipalTypes.ROLE, { organizationProvider })">
{{ organizationProvider }} Organisation Rolle hinzufügen
</NcActionButton>
</NcActions>
</div>
<div v-if="newMemberType" class="new-item">
<NcButton type="tertiary" @click="setNewMemberType(null)">
<template #icon>
<Close />
</template>
</NcButton>
<!--<MemberListNewUser v-if="newMemberType === api.PrincipalTypes.USER" :resource-id="props.resourceId" @add-member="(principalId) => addMember(api.PrincipalTypes.USER, principalId)" />-->
<!--<MemberListNewGroup v-if="newMemberType === api.PrincipalTypes.GROUP" :resource-id="props.resourceId" @add-member="(principalId) => addMember(api.PrincipalTypes.GROUP, principalId)" />-->
<MemberListNewRole v-if="newMemberType === api.PrincipalTypes.ROLE" :resource-id="props.resourceId" :organization-provider="newMemberAdditionalParameters?.organizationProvider" @add-member="(principalId) => addMember(api.PrincipalTypes.ROLE, principalId)" />
</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 Manager gelten die oben ausgewählten Ordnermanager Berechtigungen, für Mitglieder die Ordnermitglieder Berechtigungen. Manager 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 Mitglieder" />
</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>

View file

@ -1 +0,0 @@
export { default } from "./MemberList.vue"

View file

@ -6,7 +6,7 @@ import { NcLoadingIcon, NcCheckboxRadioSwitch, NcButton, NcTextField } from '@ne
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 ResourceMembersList from "../components/MemberList/ResourceMembersList.vue";
import Permissions from "../components/Permissions/index.js";
import ConfirmDeleteDialog from "../components/ConfirmDeleteDialog.vue";
import ModalView from '../ModalView.vue';
@ -31,7 +31,11 @@ const resourceNameValid = computed(() => {
});
const saveName = async () => {
resource.value = await api.updateResource(resource.value.id, { name: currentResourceName.value });
resource.value = await api.updateResource(resource.value.id, { name: currentResourceName.value }, "model+members");
};
const saveInheritManagers = async (inheritManagers) => {
resource.value = await api.updateResource(resource.value.id, { inheritManagers }, "model+members");
};
watch(() => props.resourceId, async (newResourceId) => {
@ -43,28 +47,54 @@ watch(() => props.resourceId, async (newResourceId) => {
const saveActive = async (active) => {
resourceActiveLoading.value = true;
resource.value = await api.updateResource(resource.value.id, { active });
resource.value = await api.updateResource(resource.value.id, { active }, "model+members");
resourceActiveLoading.value = false;
};
const savePermission = async ({ field, value }) => {
resource.value = await api.updateResource(resource.value.id, {
[field]: value,
});
}, "model+members");
};
const switchToSnapshotRestoreView = () => {
};
const addMember = async (principalType, principalId) => {
resource.value.members.push(await api.createResourceMember(resource.value.id, {
permissionLevel: api.ResourceMemberPermissionLevels.MEMBER,
principalType,
principalId,
}));
};
const updateMember = async (memberId, updateResourceMemberDto) => {
const member = await api.updateResourceMember(memberId, updateResourceMemberDto);
resource.value.members = resource.value.members.map((m) => m.id === member.id ? member : m);
};
const deleteMember = async (memberId) => {
await api.deleteResourceMember(memberId);
resource.value.members = resource.value.members.filter((m) => m.id !== memberId);
};
const snapshotIntegrationActive = loadState('organization_folders', 'snapshot_integration_active', false);
const organizationProviders = ref([]);
api.getOrganizationProviders().then((providers) => {
organizationProviders.value = providers;
});
const validResourceMemberPrincipalTypes = api.PrincipalTypes;
</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">
<div class="resource-general-settings">
<NcTextField :value.sync="currentResourceName"
:error="!resourceNameValid"
:label-visible="!resourceNameValid"
@ -77,10 +107,16 @@ const snapshotIntegrationActive = loadState('organization_folders', 'snapshot_in
@trailing-button-click="saveName"
@blur="() => currentResourceName = currentResourceName.trim()"
@keyup.enter="saveName" />
<NcCheckboxRadioSwitch :checked="resource.inheritManagers" @update:checked="saveInheritManagers">Manager aus oberer Ebene vererben</NcCheckboxRadioSwitch>
</div>
<h3>Berechtigungen</h3>
<Permissions :resource="resource" @permissionUpdated="savePermission" />
<MemberList :members="resource?.members" />
<ResourceMembersList :resource-id="resource.id"
:members="resource?.members"
:organizationProviders="organizationProviders"
@add-member="addMember"
@update-member="updateMember"
@delete-member="deleteMember"/>
<h3>Einstellungen</h3>
<div class="settings-group">
<NcButton v-if="snapshotIntegrationActive" @click="switchToSnapshotRestoreView">