From 40b9fb0fe61e5cf63f18252bbcd499ba1f7ac1bb Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Fri, 16 Apr 2021 16:26:18 -0400 Subject: [PATCH] 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 --- .../20210415A-make-filesize-nullable.ts | 2 +- ...0210416A-add-collections-accountability.ts | 15 +++++ .../system-data/collections/collections.yaml | 7 ++- .../system-data/fields/collections.yaml | 22 +++++++ api/src/services/items.ts | 58 ++++++++++--------- api/src/types/collection.ts | 1 + api/src/types/schema.ts | 1 + api/src/utils/get-schema.ts | 5 +- .../use-collection/use-collection.ts | 8 ++- app/src/lang/translations/en-US.yaml | 6 ++ app/src/modules/collections/routes/item.vue | 10 +++- app/src/types/collections.ts | 1 + .../revisions-drawer-detail.vue | 2 +- 13 files changed, 101 insertions(+), 37 deletions(-) create mode 100644 api/src/database/migrations/20210416A-add-collections-accountability.ts diff --git a/api/src/database/migrations/20210415A-make-filesize-nullable.ts b/api/src/database/migrations/20210415A-make-filesize-nullable.ts index 7180b82813..7809a87e89 100644 --- a/api/src/database/migrations/20210415A-make-filesize-nullable.ts +++ b/api/src/database/migrations/20210415A-make-filesize-nullable.ts @@ -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(); }); } diff --git a/api/src/database/migrations/20210416A-add-collections-accountability.ts b/api/src/database/migrations/20210416A-add-collections-accountability.ts new file mode 100644 index 0000000000..e22bea9251 --- /dev/null +++ b/api/src/database/migrations/20210416A-add-collections-accountability.ts @@ -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'); + }); +} diff --git a/api/src/database/system-data/collections/collections.yaml b/api/src/database/system-data/collections/collections.yaml index a1971dcbbe..b79bb5f7e2 100644 --- a/api/src/database/system-data/collections/collections.yaml +++ b/api/src/database/system-data/collections/collections.yaml @@ -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 diff --git a/api/src/database/system-data/fields/collections.yaml b/api/src/database/system-data/fields/collections.yaml index 957e387316..5ff74b85a5 100644 --- a/api/src/database/system-data/fields/collections.yaml +++ b/api/src/database/system-data/fields/collections.yaml @@ -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 diff --git a/api/src/services/items.ts b/api/src/services/items.ts index eed2018e3c..846d7e0a8e 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -129,7 +129,7 @@ export class ItemsService 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 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 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 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 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, diff --git a/api/src/types/collection.ts b/api/src/types/collection.ts index 41fbb32460..3a2506aae1 100644 --- a/api/src/types/collection.ts +++ b/api/src/types/collection.ts @@ -8,6 +8,7 @@ export type CollectionMeta = { singleton: boolean; icon: string | null; translations: Record; + accountability: 'all' | 'accountability' | null; }; export type Collection = { diff --git a/api/src/types/schema.ts b/api/src/types/schema.ts index d83e457540..6acf5613c3 100644 --- a/api/src/types/schema.ts +++ b/api/src/types/schema.ts @@ -9,6 +9,7 @@ type CollectionsOverview = { singleton: boolean; sortField: string | null; note: string | null; + accountability: 'all' | 'activity' | null; fields: { [name: string]: { field: string; diff --git a/api/src/utils/get-schema.ts b/api/src/utils/get-schema.ts index 4ef2e0c70a..6a4f912f01 100644 --- a/api/src/utils/get-schema.ts +++ b/api/src/utils/get-schema.ts @@ -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, diff --git a/app/src/composables/use-collection/use-collection.ts b/app/src/composables/use-collection/use-collection.ts index ece8090456..67e39e0021 100644 --- a/app/src/composables/use-collection/use-collection.ts +++ b/app/src/composables/use-collection/use-collection.ts @@ -50,5 +50,11 @@ export function useCollection(collectionKey: string | Ref) { 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 }; } diff --git a/app/src/lang/translations/en-US.yaml b/app/src/lang/translations/en-US.yaml index 71758f0697..6a02d4ee92 100644 --- a/app/src/lang/translations/en-US.yaml +++ b/app/src/lang/translations/en-US.yaml @@ -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 diff --git a/app/src/modules/collections/routes/item.vue b/app/src/modules/collections/routes/item.vue index 2e313ff635..b398c3ad69 100644 --- a/app/src/modules/collections/routes/item.vue +++ b/app/src/modules/collections/routes/item.vue @@ -175,9 +175,10 @@
@@ -191,7 +192,7 @@