Allow disabling activity/revisions (#5112)

* Add accountability column

* Add field info for accountability

* Add accountability to collection type

* Fetch accountability info from collection meta

* Add field name translation for accountability field

* Hide revisions drawer detail if revisions aren't available

* Only save activity where accountability flag matches

* Disable revisions for directus_presets

Fixes #3767

* Tweak field option naming
This commit is contained in:
Rijk van Zanten
2021-04-16 16:26:18 -04:00
committed by GitHub
parent 88bf146fbb
commit 40b9fb0fe6
13 changed files with 101 additions and 37 deletions

View File

@@ -7,7 +7,7 @@ export async function up(knex: Knex) {
}
export async function down(knex: Knex) {
await knex.schema.alterTable('directus_presets', (table) => {
await knex.schema.alterTable('directus_files', (table) => {
table.integer('filesize').notNullable().defaultTo(0).alter();
});
}

View File

@@ -0,0 +1,15 @@
import { Knex } from 'knex';
export async function up(knex: Knex) {
await knex.schema.alterTable('directus_collections', (table) => {
table.string('accountability').defaultTo('all');
});
await knex('directus_collections').update({ accountability: 'all' });
}
export async function down(knex: Knex) {
await knex.schema.alterTable('directus_collections', (table) => {
table.dropColumn('accountability');
});
}

View File

@@ -21,10 +21,10 @@ data:
- collection: directus_files
icon: folder
note: Metadata for all managed file assets
display_template: "{{ title }}"
display_template: '{{ title }}'
- collection: directus_folders
note: Provides virtual directories for files
display_template: "{{ name }}"
display_template: '{{ name }}'
- collection: directus_migrations
note: What version of the database you're using
- collection: directus_permissions
@@ -33,6 +33,7 @@ data:
- collection: directus_presets
icon: bookmark_border
note: Presets for collection defaults and bookmarks
accountability: null
- collection: directus_relations
icon: merge_type
note: Relationship configuration and metadata
@@ -52,6 +53,6 @@ data:
unarchive_value: draft
icon: people_alt
note: System users for the platform
display_template: "{{ first_name }} {{ last_name }}"
display_template: '{{ first_name }} {{ last_name }}'
- collection: directus_webhooks
note: Configuration for event-based HTTP requests

View File

@@ -135,3 +135,25 @@ fields:
- integer
allowNone: true
width: half
- field: accountability_divider
special:
- alias
- no-data
interface: divider
options:
icon: admin_panel_settings
title: Accountability
width: full
- field: accountability
interface: dropdown
options:
choices:
- text: '$t:field_options.directus_collections.track_activity_revisions'
value: all
- text: '$t:field_options.directus_collections.only_track_activity'
value: activity
- text: '$t:field_options.directus_collections.do_not_track_anything'
value: null
width: half

View File

@@ -129,7 +129,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
await payloadService.processO2M(payloads, key);
}
if (this.accountability) {
if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
const activityRecords = primaryKeys.map((key) => ({
action: Action.CREATE,
user: this.accountability!.user,
@@ -153,16 +153,18 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
activityPrimaryKeys.push(primaryKey);
}
const revisionRecords = activityPrimaryKeys.map((key, index) => ({
activity: key,
collection: this.collection,
item: primaryKeys[index],
data: JSON.stringify(payloads[index]),
delta: JSON.stringify(payloads[index]),
}));
if (this.schema.collections[this.collection].accountability === 'all') {
const revisionRecords = activityPrimaryKeys.map((key, index) => ({
activity: key,
collection: this.collection,
item: primaryKeys[index],
data: JSON.stringify(payloads[index]),
delta: JSON.stringify(payloads[index]),
}));
if (revisionRecords.length > 0) {
await trx.insert(revisionRecords).into('directus_revisions');
if (revisionRecords.length > 0) {
await trx.insert(revisionRecords).into('directus_revisions');
}
}
}
@@ -335,7 +337,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
await payloadService.processO2M(payload, key);
}
if (this.accountability) {
if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
const activityRecords = keys.map((key) => ({
action: Action.UPDATE,
user: this.accountability!.user,
@@ -357,24 +359,26 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
activityPrimaryKeys.push(primaryKey);
}
const itemsService = new ItemsService(this.collection, {
knex: trx,
schema: this.schema,
});
if (this.schema.collections[this.collection].accountability === 'all') {
const itemsService = new ItemsService(this.collection, {
knex: trx,
schema: this.schema,
});
const snapshots = await itemsService.readByKey(keys);
const snapshots = await itemsService.readByKey(keys);
const revisionRecords = activityPrimaryKeys.map((key, index) => ({
activity: key,
collection: this.collection,
item: keys[index],
data:
snapshots && Array.isArray(snapshots) ? JSON.stringify(snapshots?.[index]) : JSON.stringify(snapshots),
delta: JSON.stringify(payloadWithoutAliasAndPK),
}));
const revisionRecords = activityPrimaryKeys.map((key, index) => ({
activity: key,
collection: this.collection,
item: keys[index],
data:
snapshots && Array.isArray(snapshots) ? JSON.stringify(snapshots?.[index]) : JSON.stringify(snapshots),
delta: JSON.stringify(payloadWithoutAliasAndPK),
}));
if (revisionRecords.length > 0) {
await trx.insert(revisionRecords).into('directus_revisions');
if (revisionRecords.length > 0) {
await trx.insert(revisionRecords).into('directus_revisions');
}
}
}
});
@@ -502,7 +506,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
await this.knex.transaction(async (trx) => {
await trx(this.collection).whereIn(primaryKeyField, keys).delete();
if (this.accountability) {
if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
const activityRecords = keys.map((key) => ({
action: Action.DELETE,
user: this.accountability!.user,

View File

@@ -8,6 +8,7 @@ export type CollectionMeta = {
singleton: boolean;
icon: string | null;
translations: Record<string, string>;
accountability: 'all' | 'accountability' | null;
};
export type Collection = {

View File

@@ -9,6 +9,7 @@ type CollectionsOverview = {
singleton: boolean;
sortField: string | null;
note: string | null;
accountability: 'all' | 'activity' | null;
fields: {
[name: string]: {
field: string;

View File

@@ -68,7 +68,9 @@ export async function getSchema(options?: {
const schemaOverview = await schemaInspector.overview();
const collections = [
...(await database.select('collection', 'singleton', 'note', 'sort_field').from('directus_collections')),
...(await database
.select('collection', 'singleton', 'note', 'sort_field', 'accountability')
.from('directus_collections')),
...systemCollectionRows,
];
@@ -87,6 +89,7 @@ export async function getSchema(options?: {
collectionMeta?.singleton === true || collectionMeta?.singleton === 'true' || collectionMeta?.singleton === 1,
note: collectionMeta?.note || null,
sortField: collectionMeta?.sort_field || null,
accountability: collectionMeta ? collectionMeta.accountability : 'all',
fields: mapValues(schemaOverview[collection].columns, (column) => ({
field: column.column_name,
defaultValue: getDefaultValue(column) ?? null,

View File

@@ -50,5 +50,11 @@ export function useCollection(collectionKey: string | Ref<string>) {
return info.value?.meta?.singleton === true;
});
return { info, fields, defaults, primaryKeyField, userCreatedField, sortField, isSingleton };
const accountabilityScope = computed(() => {
if (!info.value) return null;
if (!info.value.meta) return null;
return info.value.meta.accountability;
});
return { info, fields, defaults, primaryKeyField, userCreatedField, sortField, isSingleton, accountabilityScope };
}

View File

@@ -689,6 +689,7 @@ fields:
archive_value: Archive Value
unarchive_value: Unarchive Value
sort_field: Sort Field
accountability: Activity & Revision Tracking
directus_files:
title: Title
description: Description
@@ -762,6 +763,11 @@ fields:
users: Users in Role
module_list: Module Navigation
collection_list: Collection Navigation
field_options:
directus_collections:
track_activity_revisions: Track Activity & Revisions
only_track_activity: Only Track Activity
do_not_track_anything: Do Not Track Anything
no_fields_in_collection: 'There are no fields in "{collection}" yet'
do_nothing: Do Nothing
generate_and_save_uuid: Generate and Save UUID

View File

@@ -175,9 +175,10 @@
<div class="page-description" v-html="marked($t('page_help_collections_item'))" />
</sidebar-detail>
<revisions-drawer-detail
v-if="isNew === false && _primaryKey && revisionsAllowed"
v-if="isNew === false && _primaryKey && revisionsAllowed && accountabilityScope === 'all'"
:collection="collection"
:primary-key="_primaryKey"
:scope="accountabilityScope"
ref="revisionsDrawerDetail"
@revert="revert"
/>
@@ -191,7 +192,7 @@
</template>
<script lang="ts">
import { defineComponent, computed, toRefs, ref, watch } from '@vue/composition-api';
import { defineComponent, computed, toRefs, ref } from '@vue/composition-api';
import Vue from 'vue';
import CollectionsNavigationSearch from '../components/navigation-search.vue';
@@ -245,7 +246,9 @@ export default defineComponent({
const revisionsDrawerDetail = ref<Vue | null>(null);
const { info: collectionInfo, defaults, primaryKeyField, isSingleton } = useCollection(collection);
const { info: collectionInfo, defaults, primaryKeyField, isSingleton, accountabilityScope } = useCollection(
collection
);
const {
isNew,
@@ -396,6 +399,7 @@ export default defineComponent({
_primaryKey,
revisionsAllowed,
revert,
accountabilityScope,
};
function useBreadcrumb() {

View File

@@ -19,6 +19,7 @@ export interface CollectionRaw {
archive_value: string | null;
unarchive_value: string | null;
archive_app_filter: boolean;
accountability: 'all' | 'activity' | null;
} | null;
schema: Record<string, any>;
}

View File

@@ -74,7 +74,7 @@ export default defineComponent({
required: true,
},
},
setup(props, { emit }) {
setup(props) {
const { revisions, revisionsByDate, loading, refresh, revisionsCount, created } = useRevisions(
props.collection,
props.primaryKey