mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Improve Permissions in relational Interfaces (#16373)
* improve permissions check in m2m relations * improve m2o relations * More improvements * finish relational permission rework * fix field level permissions * add tests to useRelationPermissions * remove unused dependency * fix rename * fix permissions for translations * fix disabled check * ran linter and disabled "vue/one-component-per-file" for use-relation-permissions test --------- Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch> Co-authored-by: rijkvanzanten <rijkvanzanten@me.com> Co-authored-by: Brainslug <br41nslug@users.noreply.github.com> Co-authored-by: Brainslug <tim@brainslug.nl>
This commit is contained in:
@@ -60,14 +60,14 @@
|
||||
<table-row
|
||||
:headers="internalHeaders"
|
||||
:item="element"
|
||||
:show-select="!disabled && showSelect"
|
||||
:show-select="disabled ? 'none' : showSelect"
|
||||
:show-manual-sort="!disabled && showManualSort"
|
||||
:is-selected="getSelectedState(element)"
|
||||
:subdued="loading || reordering"
|
||||
:sorted-manually="internalSort.by === manualSortKey"
|
||||
:has-click-listener="!disabled && clickable"
|
||||
:height="rowHeight"
|
||||
@click="clickable ? $emit('click:row', { item: element, event: $event }) : null"
|
||||
@click="!disabled && clickable ? $emit('click:row', { item: element, event: $event }) : null"
|
||||
@item-selected="
|
||||
onItemSelected({
|
||||
item: element,
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<div
|
||||
data-dropzone
|
||||
class="v-upload"
|
||||
:class="{ dragging, uploading }"
|
||||
:class="{ dragging: dragging && fromUser, uploading }"
|
||||
@dragenter.prevent="onDragEnter"
|
||||
@dragover.prevent
|
||||
@dragleave.prevent="onDragLeave"
|
||||
@drop.stop.prevent="onDrop"
|
||||
>
|
||||
<template v-if="dragging">
|
||||
<template v-if="dragging && fromUser">
|
||||
<v-icon class="upload-icon" x-large name="file_upload" />
|
||||
<p class="type-label">{{ t('drop_to_upload') }}</p>
|
||||
</template>
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<template v-else>
|
||||
<div class="actions">
|
||||
<v-button v-tooltip="t('click_to_browse')" icon rounded secondary @click="openFileBrowser">
|
||||
<v-button v-if="fromUser" v-tooltip="t('click_to_browse')" icon rounded secondary @click="openFileBrowser">
|
||||
<input ref="input" class="browse" type="file" :multiple="multiple" @input="onBrowseSelect" />
|
||||
<v-icon name="file_upload" />
|
||||
</v-button>
|
||||
@@ -41,12 +41,19 @@
|
||||
>
|
||||
<v-icon name="folder_open" />
|
||||
</v-button>
|
||||
<v-button v-if="fromUrl" v-tooltip="t('import_from_url')" icon rounded secondary @click="activeDialog = 'url'">
|
||||
<v-button
|
||||
v-if="fromUrl && fromUser"
|
||||
v-tooltip="t('import_from_url')"
|
||||
icon
|
||||
rounded
|
||||
secondary
|
||||
@click="activeDialog = 'url'"
|
||||
>
|
||||
<v-icon name="link" />
|
||||
</v-button>
|
||||
</div>
|
||||
|
||||
<p class="type-label">{{ t('drag_file_here') }}</p>
|
||||
<p class="type-label">{{ t(fromUser ? 'drag_file_here' : 'choose_from_library') }}</p>
|
||||
|
||||
<template v-if="fromUrl !== false || fromLibrary !== false">
|
||||
<drawer-collection
|
||||
@@ -99,6 +106,8 @@ interface Props {
|
||||
multiple?: boolean;
|
||||
preset?: Record<string, any>;
|
||||
fileId?: string;
|
||||
/** In case that the user isn't allowed to upload files */
|
||||
fromUser?: boolean;
|
||||
fromUrl?: boolean;
|
||||
fromLibrary?: boolean;
|
||||
folder?: string;
|
||||
@@ -108,6 +117,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
multiple: false,
|
||||
preset: () => ({}),
|
||||
fileId: undefined,
|
||||
fromUser: true,
|
||||
fromUrl: false,
|
||||
fromLibrary: false,
|
||||
folder: undefined,
|
||||
@@ -239,7 +249,7 @@ function useDragging() {
|
||||
|
||||
const files = event.dataTransfer?.files;
|
||||
|
||||
if (files) {
|
||||
if (files && props.fromUser) {
|
||||
upload(files);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function useRelationMultiple(
|
||||
const fetchedItems = ref<Record<string, any>[]>([]);
|
||||
const existingItemCount = ref(0);
|
||||
|
||||
const { cleanItem, getPage, localDelete, getItemEdits, isEmpty } = useUtil();
|
||||
const { cleanItem, getPage, isLocalItem, getItemEdits, isEmpty } = useUtil();
|
||||
|
||||
const _value = computed<ChangesItem>({
|
||||
get() {
|
||||
@@ -672,7 +672,7 @@ export function useRelationMultiple(
|
||||
return false;
|
||||
}
|
||||
|
||||
function localDelete(item: DisplayItem) {
|
||||
function isLocalItem(item: DisplayItem) {
|
||||
return item.$type !== undefined && (item.$type !== 'updated' || isItemSelected(item));
|
||||
}
|
||||
|
||||
@@ -709,7 +709,7 @@ export function useRelationMultiple(
|
||||
return {};
|
||||
}
|
||||
|
||||
return { cleanItem, getPage, localDelete, getItemEdits, isEmpty };
|
||||
return { cleanItem, getPage, isLocalItem, getItemEdits, isEmpty };
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -726,7 +726,7 @@ export function useRelationMultiple(
|
||||
useActions,
|
||||
cleanItem,
|
||||
isItemSelected,
|
||||
localDelete,
|
||||
isLocalItem,
|
||||
getItemEdits,
|
||||
};
|
||||
}
|
||||
|
||||
435
app/src/composables/use-relation-permissions.test.ts
Normal file
435
app/src/composables/use-relation-permissions.test.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
import { test, expect, vi } from 'vitest';
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import {
|
||||
useRelationPermissionsM2A,
|
||||
useRelationPermissionsM2M,
|
||||
useRelationPermissionsM2O,
|
||||
useRelationPermissionsO2M,
|
||||
} from '@/composables/use-relation-permissions';
|
||||
import { RelationO2M } from './use-relation-o2m';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { merge } from 'lodash';
|
||||
import { RelationM2O } from './use-relation-m2o';
|
||||
import { RelationM2M } from './use-relation-m2m';
|
||||
import { RelationM2A } from './use-relation-m2a';
|
||||
|
||||
const currentUser = {
|
||||
id: '9ff55949-9f97-4fa7-a09e-db81585d6c52',
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
email: 'admin@example.com',
|
||||
role: {
|
||||
admin_access: true,
|
||||
app_access: true,
|
||||
id: 'd5c4be68-627f-4082-a8c2-5d1f3ebce2f2',
|
||||
},
|
||||
};
|
||||
|
||||
const permissions = [
|
||||
{
|
||||
role: 'd5c4be68-627f-4082-a8c2-5d1f3ebce2f2',
|
||||
permissions: {},
|
||||
validation: null,
|
||||
presets: null,
|
||||
fields: ['*'],
|
||||
system: false,
|
||||
collection: 'a_b',
|
||||
action: 'update',
|
||||
},
|
||||
{
|
||||
role: 'd5c4be68-627f-4082-a8c2-5d1f3ebce2f2',
|
||||
permissions: {},
|
||||
validation: null,
|
||||
presets: null,
|
||||
fields: ['*'],
|
||||
system: false,
|
||||
collection: 'b',
|
||||
action: 'update',
|
||||
},
|
||||
];
|
||||
|
||||
const relationO2M = ref({
|
||||
relation: {
|
||||
meta: {
|
||||
one_deselect_action: 'nullify',
|
||||
},
|
||||
},
|
||||
relatedCollection: {
|
||||
collection: 'b',
|
||||
},
|
||||
type: 'o2m',
|
||||
} as RelationO2M);
|
||||
|
||||
test('useRelationPermissionsO2M as admin', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsO2M(relationO2M);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser },
|
||||
permissionsStore: { permissions },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toBeTruthy();
|
||||
expect(wrapper.vm.updateAllowed).toBeTruthy();
|
||||
expect(wrapper.vm.deleteAllowed).toBeTruthy();
|
||||
});
|
||||
|
||||
test('useRelationPermissionsO2M with no permissions', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsO2M(relationO2M);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser: merge({}, currentUser, { role: { admin_access: false } }) },
|
||||
permissionsStore: { permissions: [] },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toBeFalsy();
|
||||
expect(wrapper.vm.updateAllowed).toBeFalsy();
|
||||
expect(wrapper.vm.deleteAllowed).toBeFalsy();
|
||||
});
|
||||
|
||||
test('useRelationPermissionsO2M with update permissions', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsO2M(relationO2M);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser: merge({}, currentUser, { role: { admin_access: false } }) },
|
||||
permissionsStore: { permissions },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toBeFalsy();
|
||||
expect(wrapper.vm.updateAllowed).toBeTruthy();
|
||||
expect(wrapper.vm.deleteAllowed).toBeTruthy();
|
||||
});
|
||||
|
||||
const relationM2O = ref({
|
||||
relatedCollection: {
|
||||
collection: 'b',
|
||||
},
|
||||
type: 'm2o',
|
||||
} as RelationM2O);
|
||||
|
||||
test('useRelationPermissionsM2O as admin', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsM2O(relationM2O);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser },
|
||||
permissionsStore: { permissions },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toBeTruthy();
|
||||
expect(wrapper.vm.updateAllowed).toBeTruthy();
|
||||
});
|
||||
|
||||
test('useRelationPermissionsM2O with no permissions', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsM2O(relationM2O);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser: merge({}, currentUser, { role: { admin_access: false } }) },
|
||||
permissionsStore: { permissions: [] },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toBeFalsy();
|
||||
expect(wrapper.vm.updateAllowed).toBeFalsy();
|
||||
});
|
||||
|
||||
test('useRelationPermissionsM2O with update permissions', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsM2O(relationM2O);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser: merge({}, currentUser, { role: { admin_access: false } }) },
|
||||
permissionsStore: { permissions },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toBeFalsy();
|
||||
expect(wrapper.vm.updateAllowed).toBeTruthy();
|
||||
});
|
||||
|
||||
const relationM2M = ref({
|
||||
junctionCollection: {
|
||||
collection: 'a_b',
|
||||
},
|
||||
relatedCollection: {
|
||||
collection: 'b',
|
||||
},
|
||||
junction: {
|
||||
meta: {
|
||||
one_deselect_action: 'nullify',
|
||||
},
|
||||
},
|
||||
} as RelationM2M);
|
||||
|
||||
test('useRelationPermissionsM2M as admin', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsM2M(relationM2M);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser },
|
||||
permissionsStore: { permissions },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toBeTruthy();
|
||||
expect(wrapper.vm.updateAllowed).toBeTruthy();
|
||||
expect(wrapper.vm.deleteAllowed).toBeTruthy();
|
||||
expect(wrapper.vm.selectAllowed).toBeTruthy();
|
||||
});
|
||||
|
||||
test('useRelationPermissionsM2M with no permissions', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsM2M(relationM2M);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser: merge({}, currentUser, { role: { admin_access: false } }) },
|
||||
permissionsStore: { permissions: [] },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toBeFalsy();
|
||||
expect(wrapper.vm.updateAllowed).toBeFalsy();
|
||||
expect(wrapper.vm.deleteAllowed).toBeFalsy();
|
||||
expect(wrapper.vm.selectAllowed).toBeFalsy();
|
||||
});
|
||||
|
||||
test('useRelationPermissionsM2M with update permissions', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsM2M(relationM2M);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser: merge({}, currentUser, { role: { admin_access: false } }) },
|
||||
permissionsStore: { permissions },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toBeFalsy();
|
||||
expect(wrapper.vm.updateAllowed).toBeTruthy();
|
||||
expect(wrapper.vm.deleteAllowed).toBeTruthy();
|
||||
expect(wrapper.vm.selectAllowed).toBeFalsy();
|
||||
});
|
||||
|
||||
const relationM2A = ref({
|
||||
junctionCollection: {
|
||||
collection: 'a_b',
|
||||
},
|
||||
allowedCollections: [{ collection: 'a' }, { collection: 'b' }],
|
||||
junction: {
|
||||
meta: {
|
||||
one_deselect_action: 'nullify',
|
||||
},
|
||||
},
|
||||
} as RelationM2A);
|
||||
|
||||
test('useRelationPermissionsM2A as admin', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsM2A(relationM2A);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser },
|
||||
permissionsStore: { permissions },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toEqual({ a: true, b: true });
|
||||
expect(wrapper.vm.updateAllowed).toEqual({ a: true, b: true });
|
||||
expect(wrapper.vm.deleteAllowed).toEqual({ a: true, b: true });
|
||||
expect(wrapper.vm.selectAllowed).toBeTruthy();
|
||||
});
|
||||
|
||||
test('useRelationPermissionsM2A with no permissions', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsM2A(relationM2A);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser: merge({}, currentUser, { role: { admin_access: false } }) },
|
||||
permissionsStore: { permissions: [] },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toEqual({ a: false, b: false });
|
||||
expect(wrapper.vm.updateAllowed).toEqual({ a: false, b: false });
|
||||
expect(wrapper.vm.deleteAllowed).toEqual({ a: false, b: false });
|
||||
expect(wrapper.vm.selectAllowed).toBeFalsy();
|
||||
});
|
||||
|
||||
test('useRelationPermissionsM2A with update permissions', () => {
|
||||
// eslint-disable-next-line vue/one-component-per-file
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
return useRelationPermissionsM2A(relationM2A);
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
userStore: { currentUser: merge({}, currentUser, { role: { admin_access: false } }) },
|
||||
permissionsStore: { permissions },
|
||||
},
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.createAllowed).toEqual({ a: false, b: false });
|
||||
expect(wrapper.vm.updateAllowed).toEqual({ a: false, b: true });
|
||||
expect(wrapper.vm.deleteAllowed).toEqual({ a: false, b: true });
|
||||
expect(wrapper.vm.selectAllowed).toBeFalsy();
|
||||
});
|
||||
151
app/src/composables/use-relation-permissions.ts
Normal file
151
app/src/composables/use-relation-permissions.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { usePermissionsStore } from '@/stores/permissions';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { PermissionsAction } from '@directus/shared/types';
|
||||
import { computed, Ref } from 'vue';
|
||||
import { RelationM2A } from './use-relation-m2a';
|
||||
import { RelationM2M } from './use-relation-m2m';
|
||||
import { RelationM2O } from './use-relation-m2o';
|
||||
import { RelationO2M } from './use-relation-o2m';
|
||||
|
||||
type Permissions = Record<PermissionsAction, boolean>;
|
||||
|
||||
export function useRelationPermissionsM2O(info: Ref<RelationM2O | undefined>) {
|
||||
const relatedPerms = computed(() => getPermsForCollection(info.value?.relatedCollection.collection));
|
||||
const createAllowed = computed(() => relatedPerms.value?.create);
|
||||
const updateAllowed = computed(() => relatedPerms.value?.update);
|
||||
|
||||
return {
|
||||
relatedPerms,
|
||||
createAllowed,
|
||||
updateAllowed,
|
||||
};
|
||||
}
|
||||
|
||||
export function useRelationPermissionsO2M(info: Ref<RelationO2M | undefined>) {
|
||||
const relatedPerms = computed(() => getPermsForCollection(info.value?.relatedCollection.collection));
|
||||
const createAllowed = computed(() => relatedPerms.value?.create);
|
||||
const updateAllowed = computed(() => relatedPerms.value?.update);
|
||||
|
||||
const deleteAllowed = computed(() => {
|
||||
if (info.value?.relation.meta?.one_deselect_action === 'delete') {
|
||||
return relatedPerms.value?.delete;
|
||||
}
|
||||
|
||||
return relatedPerms.value?.update;
|
||||
});
|
||||
|
||||
return {
|
||||
relatedPerms,
|
||||
createAllowed,
|
||||
updateAllowed,
|
||||
deleteAllowed,
|
||||
};
|
||||
}
|
||||
|
||||
export function useRelationPermissionsM2M(info: Ref<RelationM2M | undefined>) {
|
||||
const relatedPerms = computed(() => getPermsForCollection(info.value?.relatedCollection.collection));
|
||||
const junctionPerms = computed(() => getPermsForCollection(info.value?.junctionCollection.collection));
|
||||
|
||||
const createAllowed = computed(() => junctionPerms.value.create && relatedPerms.value.create);
|
||||
const selectAllowed = computed(() => junctionPerms.value.create);
|
||||
const updateAllowed = computed(() => junctionPerms.value.update && relatedPerms.value.update);
|
||||
|
||||
const deleteAllowed = computed(() => {
|
||||
if (info.value?.junction.meta?.one_deselect_action === 'delete') {
|
||||
return junctionPerms.value.delete;
|
||||
}
|
||||
|
||||
return junctionPerms.value.update;
|
||||
});
|
||||
|
||||
return {
|
||||
createAllowed,
|
||||
selectAllowed,
|
||||
updateAllowed,
|
||||
deleteAllowed,
|
||||
relatedPerms,
|
||||
junctionPerms,
|
||||
};
|
||||
}
|
||||
|
||||
export function useRelationPermissionsM2A(info: Ref<RelationM2A | undefined>) {
|
||||
const relatedPerms = computed(() => {
|
||||
const perms: Record<string, Permissions> = {};
|
||||
|
||||
for (const collection of info.value?.allowedCollections ?? []) {
|
||||
perms[collection.collection] = getPermsForCollection(collection.collection);
|
||||
}
|
||||
|
||||
return perms;
|
||||
});
|
||||
|
||||
const junctionPerms = computed(() => getPermsForCollection(info.value?.junctionCollection.collection));
|
||||
|
||||
const createAllowed = computed(() => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(relatedPerms.value).map(([key, value]) => [key, value.create && junctionPerms.value.create])
|
||||
);
|
||||
});
|
||||
|
||||
const selectAllowed = computed(() => junctionPerms.value.create);
|
||||
|
||||
const updateAllowed = computed(() => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(relatedPerms.value).map(([key, value]) => [key, value.update && junctionPerms.value.update])
|
||||
);
|
||||
});
|
||||
|
||||
const deleteAllowed = computed(() => {
|
||||
if (info.value?.junction.meta?.one_deselect_action === 'delete') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(relatedPerms.value).map(([key, value]) => [key, value.delete && junctionPerms.value.delete])
|
||||
);
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(relatedPerms.value).map(([key, value]) => [key, value.update && junctionPerms.value.update])
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
createAllowed,
|
||||
selectAllowed,
|
||||
relatedPerms,
|
||||
junctionPerms,
|
||||
deleteAllowed,
|
||||
updateAllowed,
|
||||
};
|
||||
}
|
||||
|
||||
function getPermsForCollection(collection: string | undefined) {
|
||||
const userStore = useUserStore();
|
||||
const isAdmin = userStore.currentUser?.role?.admin_access;
|
||||
|
||||
if (isAdmin) return getFullPerms(true);
|
||||
|
||||
const role = userStore.currentUser?.role?.id;
|
||||
const permissions = getFullPerms(false);
|
||||
const permissionsStore = usePermissionsStore();
|
||||
|
||||
if (collection === undefined) return permissions;
|
||||
|
||||
for (const permission of permissionsStore.permissions) {
|
||||
if (permission.collection !== collection || permission.role !== role) continue;
|
||||
|
||||
permissions[permission.action] = true;
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
function getFullPerms(bool: boolean): Permissions {
|
||||
return {
|
||||
read: bool,
|
||||
create: bool,
|
||||
update: bool,
|
||||
delete: bool,
|
||||
comment: bool,
|
||||
explain: bool,
|
||||
share: bool,
|
||||
};
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function useRelationSingle(
|
||||
const displayItem = ref<Record<string, any> | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
watch([value, previewQuery, relation], getDisplayItems, { immediate: true });
|
||||
watch([value, previewQuery, relation], getDisplayItem, { immediate: true });
|
||||
|
||||
return { update, remove, refresh, displayItem, loading };
|
||||
|
||||
@@ -41,10 +41,10 @@ export function useRelationSingle(
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await getDisplayItems();
|
||||
await getDisplayItem();
|
||||
}
|
||||
|
||||
async function getDisplayItems() {
|
||||
async function getDisplayItem() {
|
||||
const val = value.value;
|
||||
|
||||
if (!val) {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<v-button v-tooltip="t('edit')" icon rounded @click="editImageDetails = true">
|
||||
<v-icon name="open_in_new" />
|
||||
</v-button>
|
||||
<v-button v-tooltip="t('edit_image')" icon rounded @click="editImageEditor = true">
|
||||
<v-button v-if="updateAllowed" v-tooltip="t('edit_image')" icon rounded @click="editImageEditor = true">
|
||||
<v-icon name="tune" />
|
||||
</v-button>
|
||||
<v-button v-tooltip="t('deselect')" icon rounded @click="deselect">
|
||||
@@ -61,8 +61,9 @@
|
||||
</div>
|
||||
|
||||
<drawer-item
|
||||
v-if="!disabled && image"
|
||||
v-if="image"
|
||||
v-model:active="editImageDetails"
|
||||
:disabled="disabled || !updateAllowed"
|
||||
collection="directus_files"
|
||||
:primary-key="image.id"
|
||||
:edits="edits"
|
||||
@@ -79,13 +80,14 @@
|
||||
|
||||
<file-lightbox :id="image.id" v-model="lightboxActive" />
|
||||
</div>
|
||||
<v-upload v-else from-library from-url :folder="folder" @input="update($event.id)" />
|
||||
<v-upload v-else from-library from-url :from-user="createAllowed" :folder="folder" @input="update($event.id)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api, { addTokenToURL } from '@/api';
|
||||
import { useRelationM2O } from '@/composables/use-relation-m2o';
|
||||
import { useRelationPermissionsM2O } from '@/composables/use-relation-permissions';
|
||||
import { RelationQuerySingle, useRelationSingle } from '@/composables/use-relation-single';
|
||||
import { formatFilesize } from '@/utils/format-filesize';
|
||||
import { getAssetUrl } from '@/utils/get-asset-url';
|
||||
@@ -198,6 +200,8 @@ const edits = computed(() => {
|
||||
|
||||
return props.value;
|
||||
});
|
||||
|
||||
const { createAllowed, updateAllowed } = useRelationPermissionsM2O(relationInfo);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<v-divider v-if="!disabled" />
|
||||
</template>
|
||||
<template v-if="!disabled">
|
||||
<v-list-item clickable @click="activeDialog = 'upload'">
|
||||
<v-list-item v-if="createAllowed" clickable @click="activeDialog = 'upload'">
|
||||
<v-list-item-icon><v-icon name="phonelink" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{ t(file ? 'replace_from_device' : 'upload_from_device') }}
|
||||
@@ -69,7 +69,7 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item clickable @click="activeDialog = 'url'">
|
||||
<v-list-item v-if="createAllowed" clickable @click="activeDialog = 'url'">
|
||||
<v-list-item-icon><v-icon name="link" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{ t(file ? 'replace_from_url' : 'import_from_url') }}
|
||||
@@ -85,7 +85,7 @@
|
||||
collection="directus_files"
|
||||
:primary-key="file.id"
|
||||
:edits="edits"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || !updateAllowed"
|
||||
@input="update"
|
||||
>
|
||||
<template #actions>
|
||||
@@ -145,18 +145,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref, computed, toRefs } from 'vue';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection.vue';
|
||||
import api from '@/api';
|
||||
import { useRelationM2O } from '@/composables/use-relation-m2o';
|
||||
import { useRelationPermissionsM2O } from '@/composables/use-relation-permissions';
|
||||
import { RelationQuerySingle, useRelationSingle } from '@/composables/use-relation-single';
|
||||
import { addQueryToPath } from '@/utils/add-query-to-path';
|
||||
import { getAssetUrl } from '@/utils/get-asset-url';
|
||||
import { readableMimeType } from '@/utils/readable-mime-type';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection.vue';
|
||||
import DrawerItem from '@/views/private/components/drawer-item.vue';
|
||||
import { addQueryToPath } from '@/utils/add-query-to-path';
|
||||
import { useRelationM2O } from '@/composables/use-relation-m2o';
|
||||
import { useRelationSingle, RelationQuerySingle } from '@/composables/use-relation-single';
|
||||
import { Filter } from '@directus/types';
|
||||
import { computed, ref, toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
type FileInfo = {
|
||||
id: string;
|
||||
@@ -195,6 +196,7 @@ const query = ref<RelationQuerySingle>({
|
||||
const { collection, field } = toRefs(props);
|
||||
const { relationInfo } = useRelationM2O(collection, field);
|
||||
const { displayItem: file, loading, update, remove } = useRelationSingle(value, query, relationInfo);
|
||||
const { createAllowed, updateAllowed } = useRelationPermissionsM2O(relationInfo);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<v-list-item
|
||||
:class="{ deleted: element.$type === 'deleted' }"
|
||||
:dense="totalItemCount > 4"
|
||||
:disabled="disabled || !updateAllowed"
|
||||
block
|
||||
clickable
|
||||
@click="editItem(element)"
|
||||
@@ -36,7 +37,7 @@
|
||||
/>
|
||||
<div class="spacer" />
|
||||
<v-icon
|
||||
v-if="!disabled"
|
||||
v-if="!disabled && (deleteAllowed || isLocalItem(element))"
|
||||
:name="getDeselectIcon(element)"
|
||||
class="deselect"
|
||||
@click.stop="deleteItem(element)"
|
||||
@@ -78,7 +79,7 @@
|
||||
|
||||
<drawer-item
|
||||
v-model:active="editModalActive"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || (!updateAllowed && currentlyEditing !== null)"
|
||||
:collection="relationInfo.junctionCollection.collection"
|
||||
:primary-key="currentlyEditing || '+'"
|
||||
:related-primary-key="relatedPrimaryKey || '+'"
|
||||
@@ -127,19 +128,18 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRelationM2M } from '@/composables/use-relation-m2m';
|
||||
import { useRelationMultiple, RelationQueryMultiple, DisplayItem } from '@/composables/use-relation-multiple';
|
||||
import { DisplayItem, RelationQueryMultiple, useRelationMultiple } from '@/composables/use-relation-multiple';
|
||||
import { useRelationPermissionsM2M } from '@/composables/use-relation-permissions';
|
||||
import { adjustFieldsForDisplays } from '@/utils/adjust-fields-for-displays';
|
||||
import { getAssetUrl } from '@/utils/get-asset-url';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection.vue';
|
||||
import DrawerItem from '@/views/private/components/drawer-item.vue';
|
||||
import { Filter } from '@directus/types';
|
||||
import { getFieldsFromTemplate } from '@directus/utils';
|
||||
import { clamp, get, isEmpty, isNil, set } from 'lodash';
|
||||
import { computed, ref, toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import DrawerItem from '@/views/private/components/drawer-item.vue';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection.vue';
|
||||
import Draggable from 'vuedraggable';
|
||||
import { getAssetUrl } from '@/utils/get-asset-url';
|
||||
import { adjustFieldsForDisplays } from '@/utils/adjust-fields-for-displays';
|
||||
import { get, clamp, isNil, set, isEmpty } from 'lodash';
|
||||
import { usePermissionsStore } from '@/stores/permissions';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { getFieldsFromTemplate } from '@directus/utils';
|
||||
import { Filter } from '@directus/types';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -228,19 +228,17 @@ const {
|
||||
loading,
|
||||
selected,
|
||||
isItemSelected,
|
||||
localDelete,
|
||||
isLocalItem,
|
||||
getItemEdits,
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey);
|
||||
|
||||
const pageCount = computed(() => Math.ceil(totalItemCount.value / limit.value));
|
||||
const { createAllowed, updateAllowed, selectAllowed, deleteAllowed } = useRelationPermissionsM2M(relationInfo);
|
||||
|
||||
const allowDrag = computed(
|
||||
() => totalItemCount.value <= limit.value && relationInfo.value?.sortField !== undefined && !props.disabled
|
||||
);
|
||||
const pageCount = computed(() => Math.ceil(totalItemCount.value / limit.value));
|
||||
|
||||
function getDeselectIcon(item: DisplayItem) {
|
||||
if (item.$type === 'deleted') return 'settings_backup_restore';
|
||||
if (localDelete(item)) return 'delete';
|
||||
if (isLocalItem(item)) return 'delete';
|
||||
return 'close';
|
||||
}
|
||||
|
||||
@@ -404,37 +402,13 @@ const customFilter = computed(() => {
|
||||
return filter;
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
const permissionsStore = usePermissionsStore();
|
||||
|
||||
const createAllowed = computed(() => {
|
||||
const admin = userStore.currentUser?.role.admin_access === true;
|
||||
if (admin) return true;
|
||||
|
||||
const hasJunctionPermissions = !!permissionsStore.permissions.find(
|
||||
(permission) =>
|
||||
permission.action === 'create' && permission.collection === relationInfo.value?.junctionCollection.collection
|
||||
);
|
||||
|
||||
const hasRelatedPermissions = !!permissionsStore.permissions.find(
|
||||
(permission) =>
|
||||
permission.action === 'create' && permission.collection === relationInfo.value?.relatedCollection.collection
|
||||
);
|
||||
|
||||
return hasJunctionPermissions && hasRelatedPermissions;
|
||||
});
|
||||
|
||||
const selectAllowed = computed(() => {
|
||||
const admin = userStore.currentUser?.role.admin_access === true;
|
||||
if (admin) return true;
|
||||
|
||||
const hasJunctionPermissions = !!permissionsStore.permissions.find(
|
||||
(permission) =>
|
||||
permission.action === 'create' && permission.collection === relationInfo.value?.junctionCollection.collection
|
||||
);
|
||||
|
||||
return hasJunctionPermissions;
|
||||
});
|
||||
const allowDrag = computed(
|
||||
() =>
|
||||
totalItemCount.value <= limit.value &&
|
||||
relationInfo.value?.sortField !== undefined &&
|
||||
!props.disabled &&
|
||||
updateAllowed.value
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -471,6 +445,7 @@ const selectAllowed = computed(() => {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
margin-right: 4px;
|
||||
transition: color var(--fast) var(--transition);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
--v-icon-color: var(--danger);
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
/>
|
||||
<div class="spacer" />
|
||||
<v-icon
|
||||
v-if="!disabled"
|
||||
v-if="!disabled && (deleteAllowed[element[relationInfo.collectionField.field]] || isLocalItem(element))"
|
||||
class="clear-icon"
|
||||
:name="getDeselectIcon(element)"
|
||||
@click.stop="deleteItem(element)"
|
||||
@@ -62,7 +62,7 @@
|
||||
</v-list>
|
||||
|
||||
<div class="actions">
|
||||
<v-menu v-if="enableCreate" :disabled="disabled" show-arrow>
|
||||
<v-menu v-if="enableCreate && createCollections.length > 0" :disabled="disabled" show-arrow>
|
||||
<template #activator="{ toggle }">
|
||||
<v-button :disabled="disabled" @click="toggle">
|
||||
{{ t('create_new') }}
|
||||
@@ -85,7 +85,7 @@
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-menu v-if="enableSelect" :disabled="disabled" show-arrow>
|
||||
<v-menu v-if="enableSelect && selectAllowed" :disabled="disabled" show-arrow>
|
||||
<template #activator="{ toggle }">
|
||||
<v-button class="existing" :disabled="disabled" @click="toggle">
|
||||
{{ t('add_existing') }}
|
||||
@@ -123,7 +123,9 @@
|
||||
|
||||
<drawer-item
|
||||
v-model:active="editModalActive"
|
||||
:disabled="disabled"
|
||||
:disabled="
|
||||
disabled || (editingCollection !== null && !updateAllowed[editingCollection] && currentlyEditing !== null)
|
||||
"
|
||||
:collection="relationInfo.junctionCollection.collection"
|
||||
:primary-key="currentlyEditing || '+'"
|
||||
:related-primary-key="relatedPrimaryKey || '+'"
|
||||
@@ -138,6 +140,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useRelationM2A } from '@/composables/use-relation-m2a';
|
||||
import { DisplayItem, RelationQueryMultiple, useRelationMultiple } from '@/composables/use-relation-multiple';
|
||||
import { useRelationPermissionsM2A } from '@/composables/use-relation-permissions';
|
||||
import { addRelatedPrimaryKeyToFields } from '@/utils/add-related-primary-key-to-fields';
|
||||
import { adjustFieldsForDisplays } from '@/utils/adjust-fields-for-displays';
|
||||
import { hideDragImage } from '@/utils/hide-drag-image';
|
||||
@@ -239,19 +242,15 @@ const {
|
||||
loading,
|
||||
selected,
|
||||
isItemSelected,
|
||||
localDelete,
|
||||
isLocalItem,
|
||||
getItemEdits,
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey);
|
||||
|
||||
const pageCount = computed(() => Math.ceil(totalItemCount.value / limit.value));
|
||||
|
||||
const allowDrag = computed(
|
||||
() => totalItemCount.value <= limit.value && relationInfo.value?.sortField !== undefined && !props.disabled
|
||||
);
|
||||
|
||||
function getDeselectIcon(item: DisplayItem) {
|
||||
if (item.$type === 'deleted') return 'settings_backup_restore';
|
||||
if (localDelete(item)) return 'delete';
|
||||
if (isLocalItem(item)) return 'delete';
|
||||
return 'close';
|
||||
}
|
||||
|
||||
@@ -295,6 +294,7 @@ function sortItems(items: DisplayItem[]) {
|
||||
const editModalActive = ref(false);
|
||||
const currentlyEditing = ref<string | number | null>(null);
|
||||
const relatedPrimaryKey = ref<string | number | null>(null);
|
||||
const editingCollection = ref<string | null>(null);
|
||||
const selectingFrom = ref<string | null>(null);
|
||||
const editsAtStart = ref<Record<string, any>>({});
|
||||
let newItem = false;
|
||||
@@ -331,10 +331,12 @@ function editItem(item: DisplayItem) {
|
||||
};
|
||||
|
||||
editModalActive.value = true;
|
||||
editingCollection.value = item[relationInfo.value.collectionField.field];
|
||||
|
||||
if (item?.$type === 'created' && !isItemSelected(item)) {
|
||||
currentlyEditing.value = null;
|
||||
relatedPrimaryKey.value = null;
|
||||
editingCollection.value = null;
|
||||
} else {
|
||||
if (!relationPkField) return;
|
||||
currentlyEditing.value = get(item, [junctionPkField], null);
|
||||
@@ -434,6 +436,25 @@ const customFilter = computed(() => {
|
||||
|
||||
return filter;
|
||||
});
|
||||
|
||||
const { createAllowed, deleteAllowed, selectAllowed, updateAllowed } = useRelationPermissionsM2A(relationInfo);
|
||||
|
||||
const createCollections = computed(() => {
|
||||
const info = relationInfo.value;
|
||||
if (!info) return [];
|
||||
|
||||
return info.allowedCollections.filter((collection) => {
|
||||
return createAllowed.value[collection.collection];
|
||||
});
|
||||
});
|
||||
|
||||
const allowDrag = computed(
|
||||
() =>
|
||||
totalItemCount.value <= limit.value &&
|
||||
relationInfo.value?.sortField !== undefined &&
|
||||
!props.disabled &&
|
||||
updateAllowed.value
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
:loading="loading"
|
||||
:items="displayItems"
|
||||
:row-height="tableRowHeight"
|
||||
:disabled="!updateAllowed"
|
||||
:show-manual-sort="allowDrag"
|
||||
:manual-sort-key="relationInfo?.sortField"
|
||||
show-resize
|
||||
@@ -79,7 +80,7 @@
|
||||
</router-link>
|
||||
|
||||
<v-icon
|
||||
v-if="!disabled && selectAllowed"
|
||||
v-if="!disabled && (deleteAllowed || isLocalItem(item))"
|
||||
v-tooltip="t(getDeselectTooltip(item))"
|
||||
class="deselect"
|
||||
:class="{ deleted: item.$type === 'deleted' }"
|
||||
@@ -114,6 +115,7 @@
|
||||
<v-list-item
|
||||
block
|
||||
clickable
|
||||
:disabled="disabled"
|
||||
:dense="totalItemCount > 4"
|
||||
:class="{ deleted: element.$type === 'deleted' }"
|
||||
@click="editItem(element)"
|
||||
@@ -137,7 +139,7 @@
|
||||
<v-icon name="launch" />
|
||||
</router-link>
|
||||
<v-icon
|
||||
v-if="!disabled && selectAllowed"
|
||||
v-if="!disabled && (deleteAllowed || isLocalItem(element))"
|
||||
v-tooltip="t(getDeselectTooltip(element))"
|
||||
class="deselect"
|
||||
:name="getDeselectIcon(element)"
|
||||
@@ -176,7 +178,7 @@
|
||||
|
||||
<drawer-item
|
||||
v-model:active="editModalActive"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || (!updateAllowed && currentlyEditing !== null)"
|
||||
:collection="relationInfo.junctionCollection.collection"
|
||||
:primary-key="currentlyEditing || '+'"
|
||||
:related-primary-key="relatedPrimaryKey || '+'"
|
||||
@@ -214,12 +216,11 @@ import { Sort } from '@/components/v-table/types';
|
||||
import Draggable from 'vuedraggable';
|
||||
import { adjustFieldsForDisplays } from '@/utils/adjust-fields-for-displays';
|
||||
import { isEmpty, get, clamp, isNil, set } from 'lodash';
|
||||
import { usePermissionsStore } from '@/stores/permissions';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useFieldsStore } from '@/stores/fields';
|
||||
import { LAYOUTS } from '@/types/interfaces';
|
||||
import { formatCollectionItemsCount } from '@/utils/format-collection-items-count';
|
||||
import { addRelatedPrimaryKeyToFields } from '@/utils/add-related-primary-key-to-fields';
|
||||
import { useRelationPermissionsM2M } from '@/composables/use-relation-permissions';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -362,7 +363,7 @@ const {
|
||||
loading,
|
||||
selected,
|
||||
isItemSelected,
|
||||
localDelete,
|
||||
isLocalItem,
|
||||
getItemEdits,
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey);
|
||||
|
||||
@@ -436,13 +437,13 @@ const allowDrag = computed(
|
||||
|
||||
function getDeselectIcon(item: DisplayItem) {
|
||||
if (item.$type === 'deleted') return 'settings_backup_restore';
|
||||
if (localDelete(item)) return 'delete';
|
||||
if (isLocalItem(item)) return 'delete';
|
||||
return 'close';
|
||||
}
|
||||
|
||||
function getDeselectTooltip(item: DisplayItem) {
|
||||
if (item.$type === 'deleted') return 'undo_removed_item';
|
||||
if (localDelete(item)) return 'delete_item';
|
||||
if (isLocalItem(item)) return 'delete_item';
|
||||
return 'remove_item';
|
||||
}
|
||||
|
||||
@@ -605,37 +606,7 @@ function getLinkForItem(item: DisplayItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
const permissionsStore = usePermissionsStore();
|
||||
|
||||
const createAllowed = computed(() => {
|
||||
const admin = userStore.currentUser?.role.admin_access === true;
|
||||
if (admin) return true;
|
||||
|
||||
const hasJunctionPermissions = !!permissionsStore.permissions.find(
|
||||
(permission) =>
|
||||
permission.action === 'create' && permission.collection === relationInfo.value?.junctionCollection.collection
|
||||
);
|
||||
|
||||
const hasRelatedPermissions = !!permissionsStore.permissions.find(
|
||||
(permission) =>
|
||||
permission.action === 'create' && permission.collection === relationInfo.value?.relatedCollection.collection
|
||||
);
|
||||
|
||||
return hasJunctionPermissions && hasRelatedPermissions;
|
||||
});
|
||||
|
||||
const selectAllowed = computed(() => {
|
||||
const admin = userStore.currentUser?.role.admin_access === true;
|
||||
if (admin) return true;
|
||||
|
||||
const hasJunctionPermissions = !!permissionsStore.permissions.find(
|
||||
(permission) =>
|
||||
permission.action === 'create' && permission.collection === relationInfo.value?.junctionCollection.collection
|
||||
);
|
||||
|
||||
return hasJunctionPermissions;
|
||||
});
|
||||
const { createAllowed, updateAllowed, deleteAllowed, selectAllowed } = useRelationPermissionsM2M(relationInfo);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -763,6 +734,7 @@ const selectAllowed = computed(() => {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
transition: color var(--fast) var(--transition);
|
||||
margin: 0 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
--v-icon-color: var(--danger);
|
||||
|
||||
@@ -178,7 +178,7 @@ const query = computed<RelationQueryMultiple>(() => ({
|
||||
page: page.value,
|
||||
}));
|
||||
|
||||
const { displayItems, create, update, remove, select, cleanItem, localDelete, getItemEdits } = useRelationMultiple(
|
||||
const { displayItems, create, update, remove, select, cleanItem, isLocalItem, getItemEdits } = useRelationMultiple(
|
||||
value,
|
||||
query,
|
||||
relationInfo,
|
||||
@@ -187,7 +187,7 @@ const { displayItems, create, update, remove, select, cleanItem, localDelete, ge
|
||||
|
||||
function getDeselectIcon(item: DisplayItem) {
|
||||
if (item.$type === 'deleted') return 'settings_backup_restore';
|
||||
if (localDelete(item)) return 'delete';
|
||||
if (isLocalItem(item)) return 'delete';
|
||||
return 'close';
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</v-button>
|
||||
|
||||
<v-button
|
||||
v-if="!disabled && enableCreate && createAllowed && updateAllowed"
|
||||
v-if="!disabled && enableCreate && createAllowed"
|
||||
v-tooltip.bottom="createAllowed ? t('create_item') : t('not_allowed')"
|
||||
rounded
|
||||
icon
|
||||
@@ -79,7 +79,7 @@
|
||||
</router-link>
|
||||
|
||||
<v-icon
|
||||
v-if="!disabled && updateAllowed"
|
||||
v-if="!disabled && (deleteAllowed || isLocalItem(item))"
|
||||
v-tooltip="t(getDeselectTooltip(item))"
|
||||
class="deselect"
|
||||
:name="getDeselectIcon(item)"
|
||||
@@ -113,6 +113,7 @@
|
||||
<v-list-item
|
||||
block
|
||||
clickable
|
||||
:disabled="disabled"
|
||||
:dense="totalItemCount > 4"
|
||||
:class="{ deleted: element.$type === 'deleted' }"
|
||||
@click="editItem(element)"
|
||||
@@ -136,7 +137,7 @@
|
||||
<v-icon name="launch" />
|
||||
</router-link>
|
||||
<v-icon
|
||||
v-if="!disabled && updateAllowed"
|
||||
v-if="!disabled && (deleteAllowed || isLocalItem(element))"
|
||||
v-tooltip="t(getDeselectTooltip(element))"
|
||||
class="deselect"
|
||||
:name="getDeselectIcon(element)"
|
||||
@@ -161,7 +162,7 @@
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-button v-if="enableCreate && createAllowed && updateAllowed" :disabled="disabled" @click="createItem">
|
||||
<v-button v-if="enableCreate && createAllowed" :disabled="disabled" @click="createItem">
|
||||
{{ t('create_new') }}
|
||||
</v-button>
|
||||
<v-button v-if="enableSelect && updateAllowed" :disabled="disabled" @click="selectModalActive = true">
|
||||
@@ -174,7 +175,7 @@
|
||||
</div>
|
||||
|
||||
<drawer-item
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || (!updateAllowed && currentlyEditing !== '+')"
|
||||
:active="currentlyEditing !== null"
|
||||
:collection="relationInfo.relatedCollection.collection"
|
||||
:primary-key="currentlyEditing || '+'"
|
||||
@@ -211,12 +212,11 @@ import { Sort } from '@/components/v-table/types';
|
||||
import Draggable from 'vuedraggable';
|
||||
import { adjustFieldsForDisplays } from '@/utils/adjust-fields-for-displays';
|
||||
import { isEmpty, clamp, get, isNil } from 'lodash';
|
||||
import { usePermissionsStore } from '@/stores/permissions';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useFieldsStore } from '@/stores/fields';
|
||||
import { LAYOUTS } from '@/types/interfaces';
|
||||
import { formatCollectionItemsCount } from '@/utils/format-collection-items-count';
|
||||
import { addRelatedPrimaryKeyToFields } from '@/utils/add-related-primary-key-to-fields';
|
||||
import { useRelationPermissionsO2M } from '@/composables/use-relation-permissions';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -337,10 +337,12 @@ const {
|
||||
loading,
|
||||
selected,
|
||||
isItemSelected,
|
||||
localDelete,
|
||||
isLocalItem,
|
||||
getItemEdits,
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey);
|
||||
|
||||
const { createAllowed, deleteAllowed, updateAllowed } = useRelationPermissionsO2M(relationInfo);
|
||||
|
||||
const pageCount = computed(() => Math.ceil(totalItemCount.value / limit.value));
|
||||
|
||||
const showingCount = computed(() => {
|
||||
@@ -411,13 +413,13 @@ const allowDrag = computed(
|
||||
|
||||
function getDeselectIcon(item: DisplayItem) {
|
||||
if (item.$type === 'deleted') return 'settings_backup_restore';
|
||||
if (localDelete(item)) return 'delete';
|
||||
if (isLocalItem(item)) return 'delete';
|
||||
return 'close';
|
||||
}
|
||||
|
||||
function getDeselectTooltip(item: DisplayItem) {
|
||||
if (item.$type === 'deleted') return 'undo_removed_item';
|
||||
if (localDelete(item)) return 'delete_item';
|
||||
if (isLocalItem(item)) return 'delete_item';
|
||||
return 'remove_item';
|
||||
}
|
||||
|
||||
@@ -565,29 +567,6 @@ function getLinkForItem(item: DisplayItem) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
const permissionsStore = usePermissionsStore();
|
||||
|
||||
const createAllowed = computed(() => {
|
||||
const admin = userStore.currentUser?.role.admin_access === true;
|
||||
if (admin) return true;
|
||||
|
||||
return !!permissionsStore.permissions.find(
|
||||
(permission) =>
|
||||
permission.action === 'create' && permission.collection === relationInfo.value?.relatedCollection.collection
|
||||
);
|
||||
});
|
||||
|
||||
const updateAllowed = computed(() => {
|
||||
const admin = userStore.currentUser?.role.admin_access === true;
|
||||
if (admin) return true;
|
||||
|
||||
return !!permissionsStore.permissions.find(
|
||||
(permission) =>
|
||||
permission.action === 'update' && permission.collection === relationInfo.value?.relatedCollection.collection
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
<script setup lang="ts">
|
||||
import { RelationQuerySingle, useRelationSingle } from '@/composables/use-relation-single';
|
||||
import { useRelationM2O } from '@/composables/use-relation-m2o';
|
||||
import { usePermissionsStore } from '@/stores/permissions';
|
||||
import { useCollectionsStore } from '@/stores/collections';
|
||||
import { adjustFieldsForDisplays } from '@/utils/adjust-fields-for-displays';
|
||||
import { parseFilter } from '@/utils/parse-filter';
|
||||
@@ -90,6 +89,7 @@ import { get } from 'lodash';
|
||||
import { render } from 'micromustache';
|
||||
import { computed, inject, ref, toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRelationPermissionsM2O } from '@/composables/use-relation-permissions';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -171,6 +171,7 @@ const query = computed<RelationQuerySingle>(() => ({
|
||||
}));
|
||||
|
||||
const { update, remove, displayItem, loading } = useRelationSingle(value, query, relationInfo);
|
||||
const { createAllowed, updateAllowed } = useRelationPermissionsM2O(relationInfo);
|
||||
|
||||
const currentPrimaryKey = computed<string | number>(() => {
|
||||
if (!displayItem.value || !props.value || !relationInfo.value) return '+';
|
||||
@@ -228,18 +229,6 @@ function onSelection(selection: (number | string)[]) {
|
||||
|
||||
selectModalActive.value = false;
|
||||
}
|
||||
|
||||
const { hasPermission } = usePermissionsStore();
|
||||
|
||||
const createAllowed = computed(() => {
|
||||
if (!relationInfo.value) return false;
|
||||
return hasPermission(relationInfo.value.relatedCollection.collection, 'create');
|
||||
});
|
||||
|
||||
const updateAllowed = computed(() => {
|
||||
if (!relationInfo.value) return false;
|
||||
return hasPermission(relationInfo.value.relatedCollection.collection, 'update');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
? firstItemInitial?.[relationInfo?.junctionPrimaryKeyField.field]
|
||||
: null
|
||||
"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || !firstChangesAllowed"
|
||||
:loading="loading"
|
||||
:fields="fields"
|
||||
:fields="firstFields"
|
||||
:model-value="firstItem"
|
||||
:initial-values="firstItemInitial"
|
||||
:badge="languageOptions.find((lang) => lang.value === firstLang)?.text"
|
||||
@@ -50,10 +50,10 @@
|
||||
? secondItemInitial?.[relationInfo?.junctionPrimaryKeyField.field]
|
||||
: null
|
||||
"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || !secondChangesAllowed"
|
||||
:loading="loading"
|
||||
:initial-values="secondItemInitial"
|
||||
:fields="fields"
|
||||
:fields="secondFields"
|
||||
:badge="languageOptions.find((lang) => lang.value === secondLang)?.text"
|
||||
:direction="languageOptions.find((lang) => lang.value === secondLang)?.direction"
|
||||
:model-value="secondItem"
|
||||
@@ -72,12 +72,14 @@ import VForm from '@/components/v-form/v-form.vue';
|
||||
import VIcon from '@/components/v-icon/v-icon.vue';
|
||||
import { useRelationM2M } from '@/composables/use-relation-m2m';
|
||||
import { DisplayItem, RelationQueryMultiple, useRelationMultiple } from '@/composables/use-relation-multiple';
|
||||
import { useRelationPermissionsM2M } from '@/composables/use-relation-permissions';
|
||||
import { useWindowSize } from '@/composables/use-window-size';
|
||||
import vTooltip from '@/directives/tooltip';
|
||||
import { useFieldsStore } from '@/stores/fields';
|
||||
import { usePermissionsStore } from '@/stores/permissions';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import { toArray } from '@directus/utils';
|
||||
import { isNil } from 'lodash';
|
||||
import { cloneDeep, isNil } from 'lodash';
|
||||
import { computed, ref, toRefs, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import LanguageSelect from './language-select.vue';
|
||||
@@ -120,6 +122,7 @@ const { relationInfo } = useRelationM2M(collection, field);
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const fieldsStore = useFieldsStore();
|
||||
const permissionsStore = usePermissionsStore();
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
@@ -314,6 +317,89 @@ function useLanguages() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { junctionPerms } = useRelationPermissionsM2M(relationInfo);
|
||||
|
||||
const createAllowed = computed(() => junctionPerms.value.create);
|
||||
const updateAllowed = computed(() => junctionPerms.value.update);
|
||||
|
||||
const firstItemNew = computed(
|
||||
() => relationInfo.value && firstItemInitial.value?.[relationInfo.value.junctionPrimaryKeyField.field] === undefined
|
||||
);
|
||||
|
||||
const secondItemNew = computed(
|
||||
() => relationInfo.value && secondItemInitial.value?.[relationInfo.value.junctionPrimaryKeyField.field] === undefined
|
||||
);
|
||||
|
||||
const firstChangesAllowed = computed(() => {
|
||||
if (firstItemNew.value) {
|
||||
return updateAllowed.value;
|
||||
}
|
||||
|
||||
return createAllowed.value;
|
||||
});
|
||||
|
||||
const secondChangesAllowed = computed(() => {
|
||||
if (secondItemNew.value) {
|
||||
return updateAllowed.value;
|
||||
}
|
||||
|
||||
return createAllowed.value;
|
||||
});
|
||||
|
||||
const firstFields = computed(() => {
|
||||
let fieldsWithPerms = cloneDeep(fields.value);
|
||||
if (!relationInfo.value) return fieldsWithPerms;
|
||||
|
||||
const permissions = permissionsStore.getPermissionsForUser(
|
||||
relationInfo.value.junctionCollection.collection,
|
||||
firstItemNew.value ? 'create' : 'update'
|
||||
);
|
||||
|
||||
if (!permissions) return fieldsWithPerms;
|
||||
|
||||
if (permissions.fields?.includes('*') === false) {
|
||||
fieldsWithPerms = fieldsWithPerms.map((field) => {
|
||||
if (permissions.fields?.includes(field.field) === false) {
|
||||
field.meta = {
|
||||
...(field.meta || {}),
|
||||
readonly: true,
|
||||
} as any;
|
||||
}
|
||||
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
return fieldsWithPerms;
|
||||
});
|
||||
|
||||
const secondFields = computed(() => {
|
||||
let fieldsWithPerms = cloneDeep(fields.value);
|
||||
if (!relationInfo.value) return fieldsWithPerms;
|
||||
|
||||
const permissions = permissionsStore.getPermissionsForUser(
|
||||
relationInfo.value.junctionCollection.collection,
|
||||
secondItemNew.value ? 'create' : 'update'
|
||||
);
|
||||
|
||||
if (!permissions) return fieldsWithPerms;
|
||||
|
||||
if (permissions.fields?.includes('*') === false) {
|
||||
fieldsWithPerms = fieldsWithPerms.map((field) => {
|
||||
if (permissions.fields?.includes(field.field) === false) {
|
||||
field.meta = {
|
||||
...(field.meta || {}),
|
||||
readonly: true,
|
||||
} as any;
|
||||
}
|
||||
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
return fieldsWithPerms;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user