mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Permissions (#451)
* Start on permissions setup widget * Calculate toggle states * Render out statuses details * Render all sub-toggles * Rough in grid styling * Tweak type / structure of permissions array * Add inline saving for status checkboxes * Fix popper reference * Add sub-indicators + add active to list item * Finish status toggles * Add saving on column level * Add border to system section * Add field blacklist modal * Make field blacklist work on collection level * Add indeterminate state to field checkboxes * Add hover / active state to activator * Save all statuses to create on collection level too * Visually align icons * Account for status mapping without value key * Style loader to match height of permissions * Add status modal * Allow status modal for create permission * Add collections header * Increase font-weight of permissions header
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
role="checkbox"
|
||||
:aria-pressed="isChecked ? 'true' : 'false'"
|
||||
:disabled="disabled"
|
||||
:class="{ checked: isChecked }"
|
||||
:class="{ checked: isChecked, indeterminate }"
|
||||
>
|
||||
<div class="prepend" v-if="$scopedSlots.prepend"><slot name="prepend" /></div>
|
||||
<v-icon class="checkbox" :name="icon" />
|
||||
@@ -57,14 +57,14 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const icon = computed<string>(() => {
|
||||
if (props.indeterminate) return 'indeterminate_check_box';
|
||||
if (props.indeterminate === true) return 'indeterminate_check_box';
|
||||
return isChecked.value ? 'check_box' : 'check_box_outline_blank';
|
||||
});
|
||||
|
||||
return { isChecked, toggleInput, icon };
|
||||
|
||||
function toggleInput(): void {
|
||||
if (props.indeterminate) {
|
||||
if (props.indeterminate === true) {
|
||||
emit('update:indeterminate', false);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled).checked {
|
||||
&:not(:disabled):not(.indeterminate).checked {
|
||||
.checkbox {
|
||||
--v-icon-color: var(--v-checkbox-color);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
<footer class="footer" v-if="$slots.footer">
|
||||
<footer class="footer" v-if="$slots.footer || $scopedSlots.footer">
|
||||
<slot name="footer" v-bind="{ close: () => $emit('toggle', false) }" />
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
@@ -23,6 +23,10 @@ export function useCollection(collection: Ref<string>) {
|
||||
);
|
||||
});
|
||||
|
||||
const ownerField = computed(() => {
|
||||
return fields.value?.find((field) => field.type === 'owner');
|
||||
});
|
||||
|
||||
const statusField = computed(() => {
|
||||
return fields.value?.find((field) => field.type === 'status');
|
||||
});
|
||||
@@ -50,5 +54,5 @@ export function useCollection(collection: Ref<string>) {
|
||||
);
|
||||
});
|
||||
|
||||
return { info, fields, primaryKeyField, statusField, softDeleteStatus };
|
||||
return { info, fields, primaryKeyField, ownerField, statusField, softDeleteStatus };
|
||||
}
|
||||
|
||||
@@ -251,6 +251,27 @@
|
||||
"add_new_folder": "Add New Folder",
|
||||
"folder_name": "Folder Name...",
|
||||
|
||||
"saves_automatically": "Saves Automatically",
|
||||
|
||||
"show_system_collections": "Show System Collections",
|
||||
"hide_system_collections": "Hide System Collections",
|
||||
|
||||
"always": "Always",
|
||||
"create": "Create",
|
||||
"full": "All",
|
||||
"mine": "Mine Only",
|
||||
"on_create": "On Creation",
|
||||
"on_update": "On Update",
|
||||
"read": "Read",
|
||||
"role": "Role Only",
|
||||
"update": "Update",
|
||||
|
||||
"select_fields": "Select Fields",
|
||||
"readable_fields": "Readable Fields",
|
||||
"writable_fields": "Writable Fields",
|
||||
|
||||
"select_statuses": "Select Statuses",
|
||||
|
||||
"about_directus": "About Directus",
|
||||
"activity_log": "Activity Log",
|
||||
"add_field_filter": "Add a field filter",
|
||||
@@ -353,7 +374,6 @@
|
||||
"contains": "Contains",
|
||||
"continue": "Continue",
|
||||
"continue_as": "<b>{name}</b> is already authenticated for this project. If you recognize this account, please press continue.",
|
||||
"create": "Create",
|
||||
"create_collection": "Create Collection",
|
||||
"create_field": "Create Field",
|
||||
"create_role": "Create Role",
|
||||
@@ -594,18 +614,6 @@
|
||||
"otp": "One-Time Password",
|
||||
"password": "Password",
|
||||
"password_reset_sending": "Sending email...",
|
||||
"permission_states": {
|
||||
"always": "Always",
|
||||
"create": "Create",
|
||||
"full": "All",
|
||||
"mine": "Mine Only",
|
||||
"none": "None",
|
||||
"on_create": "On Creation",
|
||||
"on_update": "On Update",
|
||||
"read": "Readonly",
|
||||
"role": "Role Only",
|
||||
"update": "Update"
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"permissions_admin": "Admins have access to all managed data within the system by default.",
|
||||
"permissions_no_collections": "This project does not have any collections yet.",
|
||||
@@ -617,8 +625,6 @@
|
||||
"project_key": "Project Key",
|
||||
"project_name": "Project Name",
|
||||
"project_not_configured": "Project Not Configured",
|
||||
"read": "Read",
|
||||
"readable_fields": "Readable Fields",
|
||||
"readable_fields_copy": "Select the fields that the user can view",
|
||||
"regex": "RegEx",
|
||||
"related_collection": "Related Collection",
|
||||
@@ -647,11 +653,9 @@
|
||||
"search_interface": "Search for an interface...",
|
||||
"select_existing": "Select Existing",
|
||||
"select_field": "Select a Field",
|
||||
"select_fields": "Select Fields",
|
||||
"select_from_device": "Select from device",
|
||||
"select_interface": "Select an interface",
|
||||
"select_interface_below": "Select an interface below",
|
||||
"select_statuses": "Select Statuses",
|
||||
"select_statuses_copy": "Select the statuses the user can use",
|
||||
"server_details": "Server Details",
|
||||
"server_error": "Server Error",
|
||||
@@ -692,7 +696,6 @@
|
||||
"turn_all_off": "Turn all off",
|
||||
"unsaved_changes": "Unsaved Changes",
|
||||
"unsaved_changes_copy": "Are you sure you want to leave this page?",
|
||||
"update": "Update",
|
||||
"update_confirm": "Are you sure you want to update {count} items?",
|
||||
"update_field": "Update Field",
|
||||
"upload_exceeds_max_size": "{filename} can't be uploaded. Your server is not configured to handle uploads of this size.",
|
||||
@@ -718,7 +721,6 @@
|
||||
"why": "Why?",
|
||||
"wrapping_up": "Wrapping Up",
|
||||
"wrong_super_admin_password": "The super admin password you provided is incorrect.",
|
||||
"writable_fields": "Writable Fields",
|
||||
"writable_fields_copy": "Select the fields that the user can edit",
|
||||
"yes": "Yes"
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<div class="fields">
|
||||
<h2 class="title type-label">
|
||||
{{ $t('fields_and_layout') }}
|
||||
<span class="instant-save">{{ $t('fields_are_saved_instantly') }}</span>
|
||||
<span class="instant-save">{{ $t('saves_automatically') }}</span>
|
||||
</h2>
|
||||
<fields-management :collection="collection" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import PermissionsFields from './permissions-fields.vue';
|
||||
|
||||
export { PermissionsFields };
|
||||
export default PermissionsFields;
|
||||
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<v-modal v-model="modalActive" :title="$t('select_fields')" persistent>
|
||||
<template #activator="{ on }">
|
||||
<span class="activator" @click="on" :class="{ limited: allAllowed === false }">
|
||||
{{ allAllowed ? $t('all') : $t('limited') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div class="fields">
|
||||
<div class="read">
|
||||
<p class="type-label">{{ $t('readable_fields') }}</p>
|
||||
<v-checkbox
|
||||
v-model="readableFields"
|
||||
v-for="field in fields"
|
||||
:value="field.field"
|
||||
:key="field.field"
|
||||
:indeterminate="readIndeterminate.includes(field.field)"
|
||||
@update:indeterminate="
|
||||
readIndeterminate = readIndeterminate.filter((f) => f !== field.field)
|
||||
"
|
||||
:label="field.name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="write">
|
||||
<p class="type-label">{{ $t('writable_fields') }}</p>
|
||||
<v-checkbox
|
||||
v-model="writableFields"
|
||||
v-for="field in fields"
|
||||
:value="field.field"
|
||||
:key="field.field"
|
||||
:label="field.name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer="{ close }">
|
||||
<v-button secondary @click="close" :disabled="saving">{{ $t('cancel') }}</v-button>
|
||||
<v-button @click="save" :loading="saving">{{ $t('save') }}</v-button>
|
||||
</template>
|
||||
</v-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, toRefs, computed, watch, PropType } from '@vue/composition-api';
|
||||
import useCollection from '@/compositions/use-collection';
|
||||
import { Permission } from '../../compositions/use-permissions';
|
||||
import { intersection } from 'lodash';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
permissionId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
role: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
readBlacklist: {
|
||||
type: Array as PropType<string[] | string[][]>,
|
||||
required: true,
|
||||
},
|
||||
writeBlacklist: {
|
||||
type: Array as PropType<string[] | string[][]>,
|
||||
required: true,
|
||||
},
|
||||
savePermission: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
combined: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { collection } = toRefs(props);
|
||||
|
||||
const { fields } = useCollection(collection);
|
||||
|
||||
const fieldKeys = computed(() => fields.value.map((field) => field.field));
|
||||
|
||||
const modalActive = ref(false);
|
||||
const readableFields = ref<string[]>([]);
|
||||
const writableFields = ref<string[]>([]);
|
||||
const readIndeterminate = ref<string[]>([]);
|
||||
const writeIndeterminate = ref<string[]>([]);
|
||||
|
||||
const allAllowed = computed(() => {
|
||||
let blacklist = [...props.readBlacklist, ...props.writeBlacklist];
|
||||
|
||||
if (props.combined === true) {
|
||||
blacklist = blacklist.flat();
|
||||
}
|
||||
|
||||
return blacklist.length === 0;
|
||||
});
|
||||
|
||||
watch(modalActive, (newVal) => {
|
||||
if (newVal !== true) return;
|
||||
|
||||
if (props.combined === true) {
|
||||
readableFields.value = invertBlacklist(
|
||||
intersection(...(props.readBlacklist as string[][]))
|
||||
);
|
||||
|
||||
readIndeterminate.value = [...new Set(props.readBlacklist.flat())].filter((k) =>
|
||||
readableFields.value.includes(k)
|
||||
);
|
||||
} else {
|
||||
readableFields.value = invertBlacklist(props.readBlacklist as string[]);
|
||||
}
|
||||
|
||||
if (props.combined === true) {
|
||||
writableFields.value = invertBlacklist(
|
||||
intersection(...(props.writeBlacklist as string[][]))
|
||||
);
|
||||
|
||||
writeIndeterminate.value = [...new Set(props.writeBlacklist.flat())].filter((k) =>
|
||||
writableFields.value.includes(k)
|
||||
);
|
||||
} else {
|
||||
writableFields.value = invertBlacklist(props.writeBlacklist as string[]);
|
||||
}
|
||||
});
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
return {
|
||||
save,
|
||||
fields,
|
||||
modalActive,
|
||||
readableFields,
|
||||
writableFields,
|
||||
saving,
|
||||
allAllowed,
|
||||
readIndeterminate,
|
||||
writeIndeterminate,
|
||||
};
|
||||
|
||||
async function save() {
|
||||
saving.value = true;
|
||||
|
||||
const values: Partial<Permission> = {
|
||||
collection: props.collection,
|
||||
status: props.status,
|
||||
role: props.role,
|
||||
read_field_blacklist: fieldKeys.value.filter(
|
||||
(key) => readableFields.value.includes(key) === false
|
||||
),
|
||||
write_field_blacklist: fieldKeys.value.filter(
|
||||
(key) => writableFields.value.includes(key) === false
|
||||
),
|
||||
};
|
||||
|
||||
if (props.permissionId) {
|
||||
values.id = props.permissionId;
|
||||
}
|
||||
|
||||
await props.savePermission(values);
|
||||
|
||||
modalActive.value = false;
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
function invertBlacklist(blacklist: string[]) {
|
||||
return fieldKeys.value.filter((key) => blacklist.includes(key) === false);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.read,
|
||||
.write {
|
||||
display: grid;
|
||||
grid-gap: 0 32px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
|
||||
.type-label {
|
||||
grid-column: 1 / -1;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.read {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.limited {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.activator {
|
||||
position: relative;
|
||||
width: max-content;
|
||||
margin: -4px -8px;
|
||||
margin-left: 32px;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-normal);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--background-normal-alt);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import PermissionsHeader from './permissions-header.vue';
|
||||
|
||||
export { PermissionsHeader };
|
||||
export default PermissionsHeader;
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="permissions-header">
|
||||
<div class="name">{{ $tc('collection', 2) }}</div>
|
||||
<v-icon name="add_circle" v-tooltip="$t('create')" />
|
||||
<v-icon name="visibility" v-tooltip="$t('read')" />
|
||||
<v-icon name="edit" v-tooltip="$t('update')" />
|
||||
<v-icon name="delete" v-tooltip="$t('delete')" />
|
||||
<v-icon name="comment" v-tooltip="$t('comment')" />
|
||||
<div class="name fields">{{ $tc('field', 2) }}</div>
|
||||
<div class="name">{{ $t('statuses') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.permissions-header {
|
||||
display: grid;
|
||||
grid-gap: var(--grid-gap);
|
||||
grid-template-columns: var(--grid-template-columns);
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
|
||||
.v-icon {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
.fields {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import PermissionsManagement from './permissions-management.vue';
|
||||
|
||||
export { PermissionsManagement };
|
||||
export default PermissionsManagement;
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="permissions-management">
|
||||
<div
|
||||
class="loading"
|
||||
v-if="loading && permissions === null"
|
||||
:style="{
|
||||
'--rows': collectionKeys.normal.length,
|
||||
}"
|
||||
>
|
||||
<v-progress-circular indeterminate />
|
||||
</div>
|
||||
<template v-else>
|
||||
<permissions-header />
|
||||
|
||||
<permissions-row
|
||||
v-for="key in collectionKeys.normal"
|
||||
:key="key"
|
||||
:collection="key"
|
||||
:role="role"
|
||||
:saved-permissions="getPermissionsForCollection(key)"
|
||||
:save-permission="savePermission"
|
||||
:save-all="saveAll"
|
||||
/>
|
||||
|
||||
<div class="system" v-if="systemActive">
|
||||
<permissions-row
|
||||
v-for="key in collectionKeys.system"
|
||||
system
|
||||
:key="key"
|
||||
:collection="key"
|
||||
:role="role"
|
||||
:saved-permissions="getPermissionsForCollection(key)"
|
||||
:save-permission="savePermission"
|
||||
:save-all="saveAll"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button @click="systemActive = !systemActive" class="system-toggle">
|
||||
{{ systemActive ? $t('hide_system_collections') : $t('show_system_collections') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, toRefs } from '@vue/composition-api';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
import { orderBy } from 'lodash';
|
||||
import PermissionsRow from '../permissions-row';
|
||||
import usePermissions from '../../compositions/use-permissions';
|
||||
import PermissionsHeader from '../permissions-header';
|
||||
|
||||
export default defineComponent({
|
||||
components: { PermissionsRow, PermissionsHeader },
|
||||
props: {
|
||||
role: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const { role } = toRefs(props);
|
||||
const collectionKeys = computed(() => {
|
||||
const keys = orderBy(
|
||||
collectionsStore.state.collections.map((collection) => collection.collection),
|
||||
['collection'],
|
||||
['asc']
|
||||
);
|
||||
|
||||
return {
|
||||
normal: keys.filter((key) => key.startsWith('directus_') === false),
|
||||
system: keys.filter((key) => key.startsWith('directus_') === true),
|
||||
};
|
||||
});
|
||||
|
||||
const systemActive = ref(false);
|
||||
|
||||
const { loading, error, permissions, savePermission, saveAll } = usePermissions(role);
|
||||
|
||||
return {
|
||||
collectionKeys,
|
||||
systemActive,
|
||||
loading,
|
||||
error,
|
||||
permissions,
|
||||
getPermissionsForCollection,
|
||||
savePermission,
|
||||
saveAll,
|
||||
};
|
||||
|
||||
function getPermissionsForCollection(key: string) {
|
||||
return permissions.value?.filter((permission) => permission.collection === key);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.permissions-management {
|
||||
--grid-template-columns: 2fr repeat(5, 24px) repeat(2, 1fr) 24px;
|
||||
--grid-gap: 0 8px;
|
||||
|
||||
max-width: 800px; // same as fields setup
|
||||
border: 2px solid var(--border-normal);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc((var(--rows) * (40px + 2px)) + 38px);
|
||||
}
|
||||
|
||||
.system {
|
||||
border-top: 2px solid var(--border-subdued);
|
||||
}
|
||||
|
||||
.system-toggle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
color: var(--foreground-subdued);
|
||||
background-color: var(--background-subdued);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
transition: color var(--fast) var(--transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import PermissionsRow from './permissions-row.vue';
|
||||
|
||||
export { PermissionsRow };
|
||||
export default PermissionsRow;
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="permissions-row">
|
||||
<div class="row">
|
||||
<div class="name">{{ info.name }}</div>
|
||||
|
||||
<permissions-toggle
|
||||
type="create"
|
||||
:options="['none', 'full']"
|
||||
:value="getCombinedPermission('create')"
|
||||
:save-permission="saveForAllStatuses"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="read"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getCombinedPermission('read')"
|
||||
:save-permission="saveForAllStatuses"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="update"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getCombinedPermission('update')"
|
||||
:save-permission="saveForAllStatuses"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="delete"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getCombinedPermission('delete')"
|
||||
:save-permission="saveForAllStatuses"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="comment"
|
||||
:options="['none', 'read', 'create', 'update', 'full']"
|
||||
:value="getCombinedPermission('comment')"
|
||||
:save-permission="saveForAllStatuses"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
/>
|
||||
|
||||
<permissions-fields
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
:save-permission="saveForAllStatuses"
|
||||
:read-blacklist="getCombinedPermission('read_field_blacklist')"
|
||||
:write-blacklist="getCombinedPermission('write_field_blacklist')"
|
||||
combined
|
||||
/>
|
||||
<permissions-statuses
|
||||
v-if="statuses"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
:save-permission="saveForAllStatuses"
|
||||
:status-blacklist="getCombinedPermission('status_blacklist')"
|
||||
:statuses="statuses"
|
||||
combined
|
||||
/>
|
||||
<div class="spacer" v-else>--</div>
|
||||
|
||||
<v-icon
|
||||
@click="detailsOpen = !detailsOpen"
|
||||
:name="detailsOpen ? 'expand_less' : 'expand_more'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="details" v-if="detailsOpen">
|
||||
<div class="row">
|
||||
<div class="name">
|
||||
<v-icon class="sub-indicator" name="subdirectory_arrow_right" />
|
||||
{{ $t('on_create') }}
|
||||
</div>
|
||||
|
||||
<v-icon v-for="n in 5" :key="n" class="spacer" name="block" />
|
||||
|
||||
<permissions-fields
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
:save-permission="savePermission"
|
||||
status="$create"
|
||||
:permission-id="getPermissionValue('id', '$create')"
|
||||
:read-blacklist="getPermissionValue('read_field_blacklist', '$create')"
|
||||
:write-blacklist="getPermissionValue('write_field_blacklist', '$create')"
|
||||
/>
|
||||
|
||||
<permissions-statuses
|
||||
v-if="statuses"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
:save-permission="savePermission"
|
||||
status="$create"
|
||||
:statuses="statuses"
|
||||
:permission-id="getPermissionValue('id', '$create')"
|
||||
:status-blacklist="getPermissionValue('status_blacklist', '$create')"
|
||||
/>
|
||||
<div class="spacer" v-else>--</div>
|
||||
</div>
|
||||
|
||||
<template v-if="statuses">
|
||||
<div class="row" v-for="status in statuses" :key="status.value">
|
||||
<div class="name">
|
||||
<v-icon class="sub-indicator" name="subdirectory_arrow_right" />
|
||||
{{ status.name }}
|
||||
</div>
|
||||
|
||||
<permissions-toggle
|
||||
type="create"
|
||||
:options="['none', 'full']"
|
||||
:value="getPermissionValue('create', status.value)"
|
||||
:status="status.value"
|
||||
:save-permission="savePermission"
|
||||
:permission-id="getPermissionValue('id', status.value)"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="read"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getPermissionValue('read', status.value)"
|
||||
:status="status.value"
|
||||
:save-permission="savePermission"
|
||||
:permission-id="getPermissionValue('id', status.value)"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="update"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getPermissionValue('update', status.value)"
|
||||
:status="status.value"
|
||||
:save-permission="savePermission"
|
||||
:permission-id="getPermissionValue('id', status.value)"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="delete"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getPermissionValue('delete', status.value)"
|
||||
:status="status.value"
|
||||
:save-permission="savePermission"
|
||||
:permission-id="getPermissionValue('id', status.value)"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="comment"
|
||||
:options="['none', 'read', 'create', 'update', 'full']"
|
||||
:value="getPermissionValue('comment', status.value)"
|
||||
:status="status.value"
|
||||
:save-permission="savePermission"
|
||||
:permission-id="getPermissionValue('id', status.value)"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
/>
|
||||
|
||||
<permissions-fields
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
:save-permission="savePermission"
|
||||
:status="status.value"
|
||||
:permission-id="getPermissionValue('id', status.value)"
|
||||
:read-blacklist="getPermissionValue('read_field_blacklist', status.value)"
|
||||
:write-blacklist="getPermissionValue('write_field_blacklist', status.value)"
|
||||
/>
|
||||
|
||||
<permissions-statuses
|
||||
v-if="statuses"
|
||||
:collection="collection"
|
||||
:role="role"
|
||||
:save-permission="savePermission"
|
||||
:status="status.value"
|
||||
:statuses="statuses"
|
||||
:permission-id="getPermissionValue('id', status.value)"
|
||||
:status-blacklist="getPermissionValue('status_blacklist', status.value)"
|
||||
/>
|
||||
<div class="spacer" v-else>--</div>
|
||||
|
||||
<div class="spacer" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs, ref, computed, PropType } from '@vue/composition-api';
|
||||
import useCollection from '@/compositions/use-collection';
|
||||
import PermissionsToggle from '../permissions-toggle';
|
||||
import PermissionsFields from '../permissions-fields';
|
||||
import PermissionsStatuses from '../permissions-statuses';
|
||||
import { Permission } from '../../compositions/use-permissions';
|
||||
|
||||
function getDefaultPermission(collection: string, role: number, status?: string) {
|
||||
const defaultPermission: Permission = {
|
||||
collection: collection,
|
||||
role: role,
|
||||
create: 'none',
|
||||
read: 'none',
|
||||
update: 'none',
|
||||
delete: 'none',
|
||||
comment: 'none',
|
||||
read_field_blacklist: [],
|
||||
write_field_blacklist: [],
|
||||
status_blacklist: [],
|
||||
status: status || null,
|
||||
};
|
||||
|
||||
return defaultPermission;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: { PermissionsToggle, PermissionsFields, PermissionsStatuses },
|
||||
props: {
|
||||
role: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
system: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
savedPermissions: {
|
||||
type: Array as PropType<Permission[]>,
|
||||
required: true,
|
||||
},
|
||||
savePermission: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
saveAll: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { collection } = toRefs(props);
|
||||
const { fields, info, statusField, ownerField } = useCollection(collection);
|
||||
|
||||
const detailsOpen = ref(false);
|
||||
|
||||
type Status = {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const statuses = computed<Status[] | null>(() => {
|
||||
if (statusField.value && statusField.value.options) {
|
||||
return Object.keys(statusField.value.options.status_mapping).map((key: string) => ({
|
||||
...statusField.value?.options?.status_mapping[key],
|
||||
value: key,
|
||||
}));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const permissions = computed<Permission[]>(() => {
|
||||
const createPermission =
|
||||
props.savedPermissions?.find((permission) => permission.status === '$create') ||
|
||||
getDefaultPermission(props.collection, props.role, '$create');
|
||||
|
||||
if (statusField.value && statuses.value) {
|
||||
const statusPermissions = statuses.value.map((status) => {
|
||||
const existing = props.savedPermissions.find(
|
||||
(permission) => permission.status === status.value
|
||||
);
|
||||
|
||||
return (
|
||||
existing || getDefaultPermission(props.collection, props.role, status.value)
|
||||
);
|
||||
});
|
||||
|
||||
return [...statusPermissions, createPermission];
|
||||
} else {
|
||||
const collectionPermission =
|
||||
props.savedPermissions.find((permission) => permission.status === null) ||
|
||||
getDefaultPermission(props.collection, props.role);
|
||||
|
||||
return [collectionPermission, createPermission];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
info,
|
||||
fields,
|
||||
statusField,
|
||||
statuses,
|
||||
detailsOpen,
|
||||
permissions,
|
||||
ownerField,
|
||||
getPermissionValue,
|
||||
getCombinedPermission,
|
||||
saveForAllStatuses,
|
||||
};
|
||||
|
||||
function getPermissionValue(type: keyof Permission, status: string | null = null) {
|
||||
const permission = permissions.value.find((permission) => permission.status === status);
|
||||
|
||||
return permission?.[type];
|
||||
}
|
||||
|
||||
function getCombinedPermission(type: keyof Permission) {
|
||||
if (type.endsWith('_blacklist')) {
|
||||
return permissions.value.map((permission) => permission[type]);
|
||||
}
|
||||
|
||||
if (statusField.value) {
|
||||
let value = permissions.value[0][type];
|
||||
|
||||
for (const permission of permissions.value.filter(
|
||||
({ status }) => status !== '$create'
|
||||
)) {
|
||||
if (value !== permission[type]) {
|
||||
value = 'indeterminate';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
} else {
|
||||
const permission = permissions.value.find(
|
||||
(permission) => permission.status === null
|
||||
);
|
||||
|
||||
return permission?.[type];
|
||||
}
|
||||
}
|
||||
|
||||
async function saveForAllStatuses(updates: Partial<Permission>) {
|
||||
const create: Partial<Permission>[] = [];
|
||||
const update: Partial<Permission>[] = [];
|
||||
|
||||
permissions.value.forEach((permission) => {
|
||||
if (permission.id) {
|
||||
update.push({
|
||||
...updates,
|
||||
id: permission.id,
|
||||
status: permission.status,
|
||||
});
|
||||
} else {
|
||||
create.push({
|
||||
...updates,
|
||||
status: permission.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await props.saveAll(create, update);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.row {
|
||||
display: grid;
|
||||
grid-gap: var(--grid-gap);
|
||||
grid-template-columns: var(--grid-template-columns);
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.permissions-row:not(:first-child),
|
||||
.details .row:first-child {
|
||||
border-top: 2px solid var(--border-subdued);
|
||||
}
|
||||
|
||||
.details {
|
||||
grid-column: 1 / -1;
|
||||
background-color: var(--background-subdued);
|
||||
}
|
||||
|
||||
.sub-indicator {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import PermissionsStatuses from './permissions-statuses.vue';
|
||||
|
||||
export { PermissionsStatuses };
|
||||
export default PermissionsStatuses;
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<v-modal v-model="modalActive" :title="$t('select_statuses')" persistent>
|
||||
<template #activator="{ on }">
|
||||
<span class="activator" @click="on" :class="{ limited: allAllowed === false }">
|
||||
{{ allAllowed ? $t('all') : $t('limited') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div class="statuses">
|
||||
<v-checkbox
|
||||
v-model="allowedStatuses"
|
||||
v-for="status in statuses"
|
||||
:value="status.value"
|
||||
:key="status.value"
|
||||
:indeterminate="indeterminate.includes(status.value)"
|
||||
@update:indeterminate="
|
||||
indeterminate = indeterminate.filter((s) => s !== status.value)
|
||||
"
|
||||
:label="status.name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer="{ close }">
|
||||
<v-button secondary @click="close" :disabled="saving">{{ $t('cancel') }}</v-button>
|
||||
<v-button @click="save" :loading="saving">{{ $t('save') }}</v-button>
|
||||
</template>
|
||||
</v-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, watch, PropType } from '@vue/composition-api';
|
||||
import { Permission } from '../../compositions/use-permissions';
|
||||
import { intersection } from 'lodash';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
permissionId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
role: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
statuses: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
statusBlacklist: {
|
||||
type: Array as PropType<string[] | string[][]>,
|
||||
required: true,
|
||||
},
|
||||
savePermission: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
combined: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const modalActive = ref(false);
|
||||
const allowedStatuses = ref<string[]>([]);
|
||||
const indeterminate = ref<string[]>([]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const statusKeys = computed(() => props.statuses.map((status: any) => status.value));
|
||||
|
||||
const allAllowed = computed(() => {
|
||||
let blacklist = [...props.statusBlacklist];
|
||||
|
||||
if (props.combined === true) {
|
||||
blacklist = blacklist.flat();
|
||||
}
|
||||
|
||||
return blacklist.length === 0;
|
||||
});
|
||||
|
||||
watch(modalActive, (newVal) => {
|
||||
if (newVal !== true) return;
|
||||
|
||||
if (props.combined === true) {
|
||||
allowedStatuses.value = invertBlacklist(
|
||||
intersection(...(props.statusBlacklist as string[][]))
|
||||
);
|
||||
|
||||
allowedStatuses.value = [...new Set(props.statusBlacklist.flat())].filter((k) =>
|
||||
allowedStatuses.value.includes(k)
|
||||
);
|
||||
} else {
|
||||
allowedStatuses.value = invertBlacklist(props.statusBlacklist as string[]);
|
||||
}
|
||||
});
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
return {
|
||||
save,
|
||||
modalActive,
|
||||
saving,
|
||||
allAllowed,
|
||||
allowedStatuses,
|
||||
indeterminate,
|
||||
};
|
||||
|
||||
async function save() {
|
||||
saving.value = true;
|
||||
|
||||
const values: Partial<Permission> = {
|
||||
collection: props.collection,
|
||||
status: props.status,
|
||||
role: props.role,
|
||||
status_blacklist: statusKeys.value.filter(
|
||||
(key) => allowedStatuses.value.includes(key) === false
|
||||
),
|
||||
};
|
||||
|
||||
if (props.permissionId) {
|
||||
values.id = props.permissionId;
|
||||
}
|
||||
|
||||
await props.savePermission(values);
|
||||
|
||||
modalActive.value = false;
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
function invertBlacklist(blacklist: string[]) {
|
||||
return statusKeys.value.filter((key) => blacklist.includes(key) === false);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.limited {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.activator {
|
||||
position: relative;
|
||||
width: max-content;
|
||||
margin: -4px -8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-normal);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--background-normal-alt);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import PermissionsToggle from './permissions-toggle.vue';
|
||||
|
||||
export { PermissionsToggle };
|
||||
export default PermissionsToggle;
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<v-menu
|
||||
show-arrow
|
||||
placement="left-start"
|
||||
class="permissions-toggle"
|
||||
close-on-content-click
|
||||
:disabled="saving"
|
||||
>
|
||||
<template #activator="{ toggle }">
|
||||
<span>
|
||||
<v-progress-circular class="spinner" indeterminate small v-if="saving" />
|
||||
<div class="box" :class="value" v-else @click="toggle">
|
||||
<v-icon v-if="iconName" :name="iconName" />
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<v-list dense>
|
||||
<v-list-item
|
||||
v-for="option in _options"
|
||||
:key="option.value"
|
||||
:active="value === option.value"
|
||||
@click="save(option.value)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<div class="box" :class="option.value">
|
||||
<v-icon v-if="option.icon" :name="option.icon" />
|
||||
</div>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ option.name }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed, ref } from '@vue/composition-api';
|
||||
import i18n from '@/lang';
|
||||
import { Permission } from '../../compositions/use-permissions';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
savePermission: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
permissionId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
role: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const iconName = computed(() => {
|
||||
return getIconForValue(props.value);
|
||||
});
|
||||
|
||||
const _options = computed(() => {
|
||||
return props.options.map((option) => ({
|
||||
value: option,
|
||||
name: i18n.t(option),
|
||||
icon: getIconForValue(option),
|
||||
}));
|
||||
});
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
return { iconName, _options, save, saving };
|
||||
|
||||
async function save(newValue: string) {
|
||||
saving.value = true;
|
||||
|
||||
const values: Partial<Permission> = {
|
||||
[props.type]: newValue,
|
||||
collection: props.collection,
|
||||
status: props.status,
|
||||
role: props.role,
|
||||
};
|
||||
|
||||
if (props.permissionId) {
|
||||
values.id = props.permissionId;
|
||||
}
|
||||
|
||||
await props.savePermission(values);
|
||||
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
function getIconForValue(value: string) {
|
||||
switch (value) {
|
||||
case 'indeterminate':
|
||||
return 'remove';
|
||||
case 'mine':
|
||||
return 'person';
|
||||
case 'role':
|
||||
return 'group';
|
||||
case 'full':
|
||||
return 'check';
|
||||
case 'read':
|
||||
return 'remove_red_eye';
|
||||
case 'create':
|
||||
return 'add';
|
||||
case 'update':
|
||||
return 'edit';
|
||||
case 'none':
|
||||
return null;
|
||||
default:
|
||||
return 'check_box_outline_blank';
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
--color: var(--foreground-subdued);
|
||||
|
||||
position: relative;
|
||||
left: 3px; // aligns it better with regular material icons
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: var(--color);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&.none {
|
||||
--color: transparent;
|
||||
|
||||
border-color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
&.indeterminate {
|
||||
--color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
&.mine {
|
||||
--color: #ff9800;
|
||||
}
|
||||
|
||||
&.role {
|
||||
--color: #fbc02d;
|
||||
}
|
||||
|
||||
&.full {
|
||||
--color: var(--success);
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
--v-icon-size: 14px;
|
||||
--v-icon-color: var(--foreground-inverted);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
import { ref, Ref, watch } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
|
||||
export type Permission = {
|
||||
id?: number;
|
||||
collection: string;
|
||||
role: number;
|
||||
status: null | string;
|
||||
create: 'none' | 'full';
|
||||
read: 'none' | 'mine' | 'role' | 'full';
|
||||
update: 'none' | 'mine' | 'role' | 'full';
|
||||
delete: 'none' | 'mine' | 'role' | 'full';
|
||||
comment: 'none' | 'read' | 'create' | 'update' | 'full';
|
||||
read_field_blacklist: null | string[];
|
||||
write_field_blacklist: null | string[];
|
||||
status_blacklist: null | string[];
|
||||
};
|
||||
|
||||
export default function usePermissions(role: Ref<number>) {
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const permissions = ref<Permission[]>(null);
|
||||
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
watch(role, (newRole, oldRole) => {
|
||||
if (newRole !== oldRole) {
|
||||
reset();
|
||||
fetchPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
return { loading, error, permissions, fetchPermissions, savePermission, saveAll };
|
||||
|
||||
function reset() {
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
permissions.value = null;
|
||||
}
|
||||
|
||||
async function fetchPermissions() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/${currentProjectKey}/permissions`, {
|
||||
params: {
|
||||
'filter[role][eq]': role.value,
|
||||
},
|
||||
});
|
||||
|
||||
permissions.value = response.data.data;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePermission(updates: Partial<Permission>) {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
try {
|
||||
if (updates.id !== undefined) {
|
||||
await api.patch(`/${currentProjectKey}/permissions/${updates.id}`, {
|
||||
...updates,
|
||||
});
|
||||
} else {
|
||||
await api.post(`/${currentProjectKey}/permissions`, updates);
|
||||
}
|
||||
|
||||
await fetchPermissions();
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAll(create: Partial<Permission>[], update: Partial<Permission>[]) {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
try {
|
||||
if (create.length > 0) {
|
||||
await api.post(`/${currentProjectKey}/permissions`, create);
|
||||
}
|
||||
|
||||
if (update.length > 0) {
|
||||
await api.patch(`/${currentProjectKey}/permissions`, update);
|
||||
}
|
||||
|
||||
await fetchPermissions();
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,13 +62,22 @@
|
||||
<settings-navigation />
|
||||
</template>
|
||||
|
||||
<v-form
|
||||
collection="directus_roles"
|
||||
:loading="loading"
|
||||
:initial-values="item"
|
||||
:batch-mode="isBatch"
|
||||
v-model="edits"
|
||||
/>
|
||||
<div class="roles">
|
||||
<div class="permissions">
|
||||
<h2 class="title type-label">
|
||||
{{ $t('permissions') }}
|
||||
<span class="instant-save">{{ $t('saves_automatically') }}</span>
|
||||
</h2>
|
||||
<permissions-management :role="+primaryKey" />
|
||||
</div>
|
||||
<v-form
|
||||
collection="directus_roles"
|
||||
:loading="loading"
|
||||
:initial-values="item"
|
||||
:batch-mode="isBatch"
|
||||
v-model="edits"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #drawer>
|
||||
<activity-drawer-detail
|
||||
@@ -89,6 +98,7 @@ import router from '@/router';
|
||||
import ActivityDrawerDetail from '@/views/private/components/activity-drawer-detail';
|
||||
import useItem from '@/compositions/use-item';
|
||||
import SaveOptions from '@/views/private/components/save-options';
|
||||
import PermissionsManagement from './components/permissions-management';
|
||||
|
||||
type Values = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -97,7 +107,7 @@ type Values = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'roles-detail',
|
||||
components: { SettingsNavigation, ActivityDrawerDetail, SaveOptions },
|
||||
components: { SettingsNavigation, ActivityDrawerDetail, SaveOptions, PermissionsManagement },
|
||||
props: {
|
||||
primaryKey: {
|
||||
type: String,
|
||||
@@ -191,7 +201,7 @@ export default defineComponent({
|
||||
--v-button-background-color-hover: var(--danger-dark);
|
||||
}
|
||||
|
||||
.v-form {
|
||||
.roles {
|
||||
padding: var(--content-padding);
|
||||
}
|
||||
|
||||
@@ -199,4 +209,17 @@ export default defineComponent({
|
||||
--v-button-color-disabled: var(--warning);
|
||||
--v-button-background-color-disabled: var(--warning-alt);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.instant-save {
|
||||
margin-left: 4px;
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
.permissions {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user