Playbook run attributes UI (#9279)
* migrate naming * temp * temp 2 * fixed button when editing * some bugs solved * fix bugs * add localization * some review requests * review comments * upgrade db to 16 * Refactor property fields handling to batch database operations for improved performance * Enhance error handling in PropertyFieldsListComponent by logging fetch errors and improving type definitions for property fields * added tests for value change * fix missalignment * fix indent & typing * fix test version * missing version
This commit is contained in:
parent
83769b9265
commit
6dd4a01145
39 changed files with 3224 additions and 187 deletions
|
|
@ -23,7 +23,7 @@ import AppDataOperator from '@database/operator/app_data_operator';
|
|||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import {schema as appSchema} from '@database/schema/app';
|
||||
import {serverSchema} from '@database/schema/server';
|
||||
import {PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunAttributeModel, PlaybookRunAttributeValueModel} from '@playbooks/database/models';
|
||||
import {PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunPropertyFieldModel, PlaybookRunPropertyValueModel} from '@playbooks/database/models';
|
||||
import {deleteIOSDatabase} from '@utils/mattermost_managed';
|
||||
import {urlSafeBase64Encode} from '@utils/security';
|
||||
import {removeProtocol} from '@utils/url';
|
||||
|
|
@ -55,7 +55,7 @@ class DatabaseManagerSingleton {
|
|||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
ScheduledPostModel, SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, TeamThreadsSyncModel, UserModel,
|
||||
PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunAttributeModel, PlaybookRunAttributeValueModel,
|
||||
PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunPropertyFieldModel, PlaybookRunPropertyValueModel,
|
||||
];
|
||||
this.databaseDirectory = '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import ServerDataOperator from '@database/operator/server_data_operator';
|
|||
import {schema as appSchema} from '@database/schema/app';
|
||||
import {serverSchema} from '@database/schema/server';
|
||||
import {beforeUpgrade} from '@helpers/database/upgrade';
|
||||
import {PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunAttributeModel, PlaybookRunAttributeValueModel} from '@playbooks/database/models';
|
||||
import {PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunPropertyFieldModel, PlaybookRunPropertyValueModel} from '@playbooks/database/models';
|
||||
import {getActiveServer, getServer, getServerByIdentifier} from '@queries/app/servers';
|
||||
import {logDebug, logError} from '@utils/log';
|
||||
import {deleteIOSDatabase, getIOSAppGroupDetails, renameIOSDatabase} from '@utils/mattermost_managed';
|
||||
|
|
@ -51,7 +51,7 @@ class DatabaseManagerSingleton {
|
|||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
ScheduledPostModel, SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, TeamThreadsSyncModel, UserModel,
|
||||
PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunAttributeModel, PlaybookRunAttributeValueModel,
|
||||
PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunPropertyFieldModel, PlaybookRunPropertyValueModel,
|
||||
];
|
||||
|
||||
this.databaseDirectory = Platform.OS === 'ios' ? getIOSAppGroupDetails().appGroupDatabase : `${documentDirectory}/databases/`;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@ const {
|
|||
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
|
||||
|
||||
export default schemaMigrations({migrations: [
|
||||
{
|
||||
toVersion: 17,
|
||||
steps: [
|
||||
addColumns({
|
||||
table: PLAYBOOK_RUN_ATTRIBUTE_VALUE,
|
||||
columns: [
|
||||
{name: 'update_at', type: 'number'},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
toVersion: 16,
|
||||
steps: [
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import {
|
|||
} from './table_schemas';
|
||||
|
||||
export const serverSchema: AppSchema = appSchema({
|
||||
version: 16,
|
||||
version: 17,
|
||||
tables: [
|
||||
CategorySchema,
|
||||
CategoryChannelSchema,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_A
|
|||
describe('*** Test schema for SERVER database ***', () => {
|
||||
it('=> The SERVER SCHEMA should strictly match', () => {
|
||||
expect(serverSchema).toEqual({
|
||||
version: 16,
|
||||
version: 17,
|
||||
unsafeSql: undefined,
|
||||
tables: {
|
||||
[CATEGORY]: {
|
||||
|
|
@ -590,11 +590,13 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||
attribute_id: {name: 'attribute_id', type: 'string', isIndexed: true},
|
||||
run_id: {name: 'run_id', type: 'string', isIndexed: true},
|
||||
value: {name: 'value', type: 'string'},
|
||||
update_at: {name: 'update_at', type: 'number'},
|
||||
},
|
||||
columnArray: [
|
||||
{name: 'attribute_id', type: 'string', isIndexed: true},
|
||||
{name: 'run_id', type: 'string', isIndexed: true},
|
||||
{name: 'value', type: 'string'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
],
|
||||
},
|
||||
[POSTS_IN_THREAD]: {
|
||||
|
|
|
|||
322
app/products/playbooks/actions/local/property_fields.test.ts
Normal file
322
app/products/playbooks/actions/local/property_fields.test.ts
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {handlePlaybookRunPropertyFields} from './property_fields';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type PlaybookRunAttributeModel from '@playbooks/types/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunAttributeValueModel from '@playbooks/types/database/models/playbook_run_attribute_value';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
describe('handlePlaybookRunPropertyFields', () => {
|
||||
let database: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
|
||||
});
|
||||
|
||||
it('should handle not found database', async () => {
|
||||
const propertyFields = [TestHelper.fakePlaybookRunAttribute()];
|
||||
const propertyValues: PlaybookRunPropertyValue[] = [];
|
||||
|
||||
const {error} = await handlePlaybookRunPropertyFields('foo', propertyFields, propertyValues);
|
||||
expect(error).toBeDefined();
|
||||
expect((error as Error).message).toContain('foo database not found');
|
||||
});
|
||||
|
||||
it('should handle property fields successfully', async () => {
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-1', name: 'Priority'}),
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-2', name: 'Status'}),
|
||||
];
|
||||
const propertyValues: PlaybookRunPropertyValue[] = [];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, propertyValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Verify property fields were stored
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
expect(storedFields.length).toBe(2);
|
||||
expect(storedFields[0].id).toBe('field-1');
|
||||
expect(storedFields[0].name).toBe('Priority');
|
||||
expect(storedFields[1].id).toBe('field-2');
|
||||
expect(storedFields[1].name).toBe('Status');
|
||||
});
|
||||
|
||||
it('should handle property values successfully', async () => {
|
||||
const propertyFields: PlaybookRunPropertyField[] = [];
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-1', 'run-1', {id: 'value-1', value: 'high'}),
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-2', 'run-1', {id: 'value-2', value: 'in-progress'}),
|
||||
];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, propertyValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Verify property values were stored
|
||||
const storedValues = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(storedValues.length).toBe(2);
|
||||
expect(storedValues[0].id).toBe('value-1');
|
||||
expect(storedValues[0].value).toBe('high');
|
||||
expect(storedValues[1].id).toBe('value-2');
|
||||
expect(storedValues[1].value).toBe('in-progress');
|
||||
});
|
||||
|
||||
it('should handle both property fields and values together', async () => {
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-1', name: 'Priority'}),
|
||||
];
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-1', 'run-1', {id: 'value-1', value: 'high'}),
|
||||
];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, propertyValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Verify both were stored
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
const storedValues = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(storedFields.length).toBe(1);
|
||||
expect(storedValues.length).toBe(1);
|
||||
expect(storedFields[0].id).toBe('field-1');
|
||||
expect(storedValues[0].id).toBe('value-1');
|
||||
});
|
||||
|
||||
it('should handle empty property fields array', async () => {
|
||||
const propertyFields: PlaybookRunPropertyField[] = [];
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-1', 'run-1', {id: 'value-1', value: 'test'}),
|
||||
];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, propertyValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Only values should be stored
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
const storedValues = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(storedFields.length).toBe(0);
|
||||
expect(storedValues.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle empty property values array', async () => {
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-1', name: 'Priority'}),
|
||||
];
|
||||
const propertyValues: PlaybookRunPropertyValue[] = [];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, propertyValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Only fields should be stored
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
const storedValues = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(storedFields.length).toBe(1);
|
||||
expect(storedValues.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle both empty arrays', async () => {
|
||||
const propertyFields: PlaybookRunPropertyField[] = [];
|
||||
const propertyValues: PlaybookRunPropertyValue[] = [];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, propertyValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Nothing should be stored
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
const storedValues = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(storedFields.length).toBe(0);
|
||||
expect(storedValues.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should update existing property field', async () => {
|
||||
// First insert
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-1', name: 'Priority', update_at: 1000}),
|
||||
];
|
||||
await handlePlaybookRunPropertyFields(serverUrl, propertyFields, []);
|
||||
|
||||
// Update with new name and timestamp
|
||||
const updatedFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-1', name: 'Updated Priority', update_at: 2000}),
|
||||
];
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, updatedFields, []);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Verify update
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
expect(storedFields.length).toBe(1);
|
||||
expect(storedFields[0].id).toBe('field-1');
|
||||
expect(storedFields[0].name).toBe('Updated Priority');
|
||||
expect(storedFields[0].updateAt).toBe(2000);
|
||||
});
|
||||
|
||||
it('should update existing property value', async () => {
|
||||
// First insert
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-1', 'run-1', {id: 'value-1', value: 'high'}),
|
||||
];
|
||||
await handlePlaybookRunPropertyFields(serverUrl, [], propertyValues);
|
||||
|
||||
// Update with new value
|
||||
const updatedValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-1', 'run-1', {id: 'value-1', value: 'low'}),
|
||||
];
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, [], updatedValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
// Verify update
|
||||
const storedValues = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(storedValues.length).toBe(1);
|
||||
expect(storedValues[0].id).toBe('value-1');
|
||||
expect(storedValues[0].value).toBe('low');
|
||||
});
|
||||
|
||||
it('should handle text property field type', async () => {
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-1', name: 'Description', type: 'text'}),
|
||||
];
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-1', 'run-1', {
|
||||
id: 'value-1',
|
||||
value: 'This is a text value',
|
||||
}),
|
||||
];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, propertyValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
const storedValues = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(storedFields[0].type).toBe('text');
|
||||
expect(storedValues[0].value).toBe('This is a text value');
|
||||
});
|
||||
|
||||
it('should handle select property field type', async () => {
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-1', name: 'Priority', type: 'select'}),
|
||||
];
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-1', 'run-1', {
|
||||
id: 'value-1',
|
||||
value: 'option-id-123',
|
||||
}),
|
||||
];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, propertyValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
const storedValues = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(storedFields[0].type).toBe('select');
|
||||
expect(storedValues[0].value).toBe('option-id-123');
|
||||
});
|
||||
|
||||
it('should handle multiselect property field type with JSON array', async () => {
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-1', name: 'Tags', type: 'multiselect'}),
|
||||
];
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-1', 'run-1', {
|
||||
id: 'value-1',
|
||||
value: '["option-id-1","option-id-2","option-id-3"]',
|
||||
}),
|
||||
];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, propertyValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
const storedValues = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(storedFields[0].type).toBe('multiselect');
|
||||
expect(storedValues[0].value).toBe('["option-id-1","option-id-2","option-id-3"]');
|
||||
});
|
||||
|
||||
it('should handle property field with attrs JSON', async () => {
|
||||
const attrs = JSON.stringify({
|
||||
options: [
|
||||
{id: 'opt-1', name: 'High'},
|
||||
{id: 'opt-2', name: 'Medium'},
|
||||
{id: 'opt-3', name: 'Low'},
|
||||
],
|
||||
sort_order: 1,
|
||||
visibility: 'always',
|
||||
value_type: '',
|
||||
});
|
||||
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: 'field-1',
|
||||
name: 'Priority',
|
||||
type: 'select',
|
||||
attrs,
|
||||
}),
|
||||
];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, []);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
expect(storedFields[0].attrs).toBe(attrs);
|
||||
});
|
||||
|
||||
it('should handle multiple property fields for the same run', async () => {
|
||||
const runId = 'run-123';
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-1', name: 'Priority', target_id: runId}),
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-2', name: 'Status', target_id: runId}),
|
||||
TestHelper.fakePlaybookRunAttribute({id: 'field-3', name: 'Tags', target_id: runId}),
|
||||
];
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-1', runId, {id: 'value-1', value: 'high'}),
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-2', runId, {id: 'value-2', value: 'in-progress'}),
|
||||
TestHelper.fakePlaybookRunAttributeValue('field-3', runId, {id: 'value-3', value: '["tag1","tag2"]'}),
|
||||
];
|
||||
|
||||
const {data, error} = await handlePlaybookRunPropertyFields(serverUrl, propertyFields, propertyValues);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const storedFields = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
const storedValues = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(storedFields.length).toBe(3);
|
||||
expect(storedValues.length).toBe(3);
|
||||
});
|
||||
});
|
||||
55
app/products/playbooks/actions/local/property_fields.ts
Normal file
55
app/products/playbooks/actions/local/property_fields.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
/**
|
||||
* Store property fields and values in the local database
|
||||
* @param serverUrl - The server URL
|
||||
* @param propertyFields - Array of property field records to store
|
||||
* @param propertyValues - Array of property value records to store
|
||||
* @returns Promise with data or error
|
||||
*/
|
||||
export async function handlePlaybookRunPropertyFields(
|
||||
serverUrl: string,
|
||||
propertyFields: PlaybookRunPropertyField[],
|
||||
propertyValues: PlaybookRunPropertyValue[],
|
||||
) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
// Prepare records for batching
|
||||
const batch: Model[] = [];
|
||||
|
||||
// Handle property fields
|
||||
if (propertyFields.length) {
|
||||
const propertyFieldRecords = await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
batch.push(...propertyFieldRecords);
|
||||
}
|
||||
|
||||
// Handle property values
|
||||
if (propertyValues.length) {
|
||||
const propertyValueRecords = await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
batch.push(...propertyValueRecords);
|
||||
}
|
||||
|
||||
// Batch all records together in a single database operation
|
||||
if (batch.length) {
|
||||
await operator.batchRecords(batch, 'handlePlaybookRunPropertyFields');
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logError('failed to handle playbook run property fields', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
304
app/products/playbooks/actions/remote/property_fields.test.ts
Normal file
304
app/products/playbooks/actions/remote/property_fields.test.ts
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {handlePlaybookRunPropertyFields} from '@playbooks/actions/local/property_fields';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {fetchPlaybookRunPropertyFields, updatePlaybookRunPropertyValue} from './property_fields';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
const runId = 'run-id-1';
|
||||
|
||||
const mockPropertyField = TestHelper.fakePlaybookRunAttribute({target_id: runId});
|
||||
const mockPropertyField2 = TestHelper.fakePlaybookRunAttribute({target_id: runId});
|
||||
const mockPropertyValue = TestHelper.fakePlaybookRunAttributeValue(mockPropertyField.id, runId);
|
||||
const mockPropertyValue2 = TestHelper.fakePlaybookRunAttributeValue(mockPropertyField2.id, runId);
|
||||
|
||||
const mockClient = {
|
||||
fetchRunPropertyFields: jest.fn(),
|
||||
fetchRunPropertyValues: jest.fn(),
|
||||
setRunPropertyValue: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@playbooks/actions/local/property_fields');
|
||||
|
||||
const throwFunc = () => {
|
||||
throw Error('error');
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
// @ts-ignore
|
||||
NetworkManager.getClient = () => mockClient;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
describe('fetchPlaybookRunPropertyFields', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(handlePlaybookRunPropertyFields).mockResolvedValue({data: true});
|
||||
});
|
||||
|
||||
it('should fetch property fields and values successfully', async () => {
|
||||
mockClient.fetchRunPropertyFields.mockResolvedValueOnce([mockPropertyField, mockPropertyField2]);
|
||||
mockClient.fetchRunPropertyValues.mockResolvedValueOnce([mockPropertyValue, mockPropertyValue2]);
|
||||
|
||||
const result = await fetchPlaybookRunPropertyFields(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(mockClient.fetchRunPropertyFields).toHaveBeenCalledWith(runId);
|
||||
expect(mockClient.fetchRunPropertyValues).toHaveBeenCalledWith(runId);
|
||||
expect(handlePlaybookRunPropertyFields).toHaveBeenCalledWith(
|
||||
serverUrl,
|
||||
[mockPropertyField, mockPropertyField2],
|
||||
[mockPropertyValue, mockPropertyValue2],
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch empty property fields and values', async () => {
|
||||
mockClient.fetchRunPropertyFields.mockResolvedValueOnce([]);
|
||||
mockClient.fetchRunPropertyValues.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await fetchPlaybookRunPropertyFields(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(mockClient.fetchRunPropertyFields).toHaveBeenCalledWith(runId);
|
||||
expect(mockClient.fetchRunPropertyValues).toHaveBeenCalledWith(runId);
|
||||
expect(handlePlaybookRunPropertyFields).toHaveBeenCalledWith(
|
||||
serverUrl,
|
||||
[],
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle fetchOnly mode without storing in DB', async () => {
|
||||
mockClient.fetchRunPropertyFields.mockResolvedValueOnce([mockPropertyField]);
|
||||
mockClient.fetchRunPropertyValues.mockResolvedValueOnce([mockPropertyValue]);
|
||||
|
||||
const result = await fetchPlaybookRunPropertyFields(serverUrl, runId, true);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(mockClient.fetchRunPropertyFields).toHaveBeenCalledWith(runId);
|
||||
expect(mockClient.fetchRunPropertyValues).toHaveBeenCalledWith(runId);
|
||||
expect(handlePlaybookRunPropertyFields).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle client error on fetchRunPropertyFields', async () => {
|
||||
mockClient.fetchRunPropertyFields.mockRejectedValueOnce(new Error('Client error'));
|
||||
|
||||
const result = await fetchPlaybookRunPropertyFields(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(handlePlaybookRunPropertyFields).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle client error on fetchRunPropertyValues', async () => {
|
||||
mockClient.fetchRunPropertyFields.mockResolvedValueOnce([mockPropertyField]);
|
||||
mockClient.fetchRunPropertyValues.mockRejectedValueOnce(new Error('Client error'));
|
||||
|
||||
const result = await fetchPlaybookRunPropertyFields(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(handlePlaybookRunPropertyFields).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle network manager error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await fetchPlaybookRunPropertyFields(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(handlePlaybookRunPropertyFields).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle database storage error', async () => {
|
||||
mockClient.fetchRunPropertyFields.mockResolvedValueOnce([mockPropertyField]);
|
||||
mockClient.fetchRunPropertyValues.mockResolvedValueOnce([mockPropertyValue]);
|
||||
jest.mocked(handlePlaybookRunPropertyFields).mockResolvedValueOnce({error: new Error('DB error')});
|
||||
|
||||
const result = await fetchPlaybookRunPropertyFields(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(mockClient.fetchRunPropertyFields).toHaveBeenCalledWith(runId);
|
||||
expect(mockClient.fetchRunPropertyValues).toHaveBeenCalledWith(runId);
|
||||
expect(handlePlaybookRunPropertyFields).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch property fields and values in parallel', async () => {
|
||||
let propertyFieldsResolve: any;
|
||||
let propertyValuesResolve: any;
|
||||
|
||||
const propertyFieldsPromise = new Promise((resolve) => {
|
||||
propertyFieldsResolve = resolve;
|
||||
});
|
||||
const propertyValuesPromise = new Promise((resolve) => {
|
||||
propertyValuesResolve = resolve;
|
||||
});
|
||||
|
||||
mockClient.fetchRunPropertyFields.mockReturnValueOnce(propertyFieldsPromise);
|
||||
mockClient.fetchRunPropertyValues.mockReturnValueOnce(propertyValuesPromise);
|
||||
|
||||
const resultPromise = fetchPlaybookRunPropertyFields(serverUrl, runId);
|
||||
|
||||
// Resolve in reverse order to ensure parallel execution
|
||||
propertyValuesResolve([mockPropertyValue]);
|
||||
propertyFieldsResolve([mockPropertyField]);
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(handlePlaybookRunPropertyFields).toHaveBeenCalledWith(
|
||||
serverUrl,
|
||||
[mockPropertyField],
|
||||
[mockPropertyValue],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePlaybookRunPropertyValue', () => {
|
||||
const fieldId = 'field-id-1';
|
||||
const value = 'test value';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(handlePlaybookRunPropertyFields).mockResolvedValue({data: true});
|
||||
});
|
||||
|
||||
it('should update property value successfully', async () => {
|
||||
const updatedValue = {...mockPropertyValue, value};
|
||||
mockClient.setRunPropertyValue.mockResolvedValueOnce(updatedValue);
|
||||
|
||||
const result = await updatePlaybookRunPropertyValue(serverUrl, runId, fieldId, value);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.propertyValue).toEqual(updatedValue);
|
||||
expect(mockClient.setRunPropertyValue).toHaveBeenCalledWith(runId, fieldId, value, undefined);
|
||||
expect(handlePlaybookRunPropertyFields).toHaveBeenCalledWith(
|
||||
serverUrl,
|
||||
[],
|
||||
[updatedValue],
|
||||
);
|
||||
});
|
||||
|
||||
it('should update property value for text field', async () => {
|
||||
const textValue = 'Some text content';
|
||||
const fieldType = 'text';
|
||||
const updatedValue = {...mockPropertyValue, value: textValue};
|
||||
mockClient.setRunPropertyValue.mockResolvedValueOnce(updatedValue);
|
||||
|
||||
const result = await updatePlaybookRunPropertyValue(serverUrl, runId, fieldId, textValue, fieldType);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.propertyValue).toEqual(updatedValue);
|
||||
expect(mockClient.setRunPropertyValue).toHaveBeenCalledWith(runId, fieldId, textValue, fieldType);
|
||||
});
|
||||
|
||||
it('should update property value for select field', async () => {
|
||||
const selectValue = 'option-id-123';
|
||||
const fieldType = 'select';
|
||||
const updatedValue = {...mockPropertyValue, value: selectValue};
|
||||
mockClient.setRunPropertyValue.mockResolvedValueOnce(updatedValue);
|
||||
|
||||
const result = await updatePlaybookRunPropertyValue(serverUrl, runId, fieldId, selectValue, fieldType);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.propertyValue).toEqual(updatedValue);
|
||||
expect(mockClient.setRunPropertyValue).toHaveBeenCalledWith(runId, fieldId, selectValue, fieldType);
|
||||
});
|
||||
|
||||
it('should update property value for multiselect field', async () => {
|
||||
const multiselectValue = 'option-id-1,option-id-2';
|
||||
const fieldType = 'multiselect';
|
||||
const updatedValue = {...mockPropertyValue, value: '["option-id-1","option-id-2"]'};
|
||||
mockClient.setRunPropertyValue.mockResolvedValueOnce(updatedValue);
|
||||
|
||||
const result = await updatePlaybookRunPropertyValue(serverUrl, runId, fieldId, multiselectValue, fieldType);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.propertyValue).toEqual(updatedValue);
|
||||
expect(mockClient.setRunPropertyValue).toHaveBeenCalledWith(runId, fieldId, multiselectValue, fieldType);
|
||||
});
|
||||
|
||||
it('should update property value for multiselect field with empty value', async () => {
|
||||
const emptyMultiselectValue = '';
|
||||
const fieldType = 'multiselect';
|
||||
const updatedValue = {...mockPropertyValue, value: '[]'};
|
||||
mockClient.setRunPropertyValue.mockResolvedValueOnce(updatedValue);
|
||||
|
||||
const result = await updatePlaybookRunPropertyValue(serverUrl, runId, fieldId, emptyMultiselectValue, fieldType);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.propertyValue).toEqual(updatedValue);
|
||||
expect(mockClient.setRunPropertyValue).toHaveBeenCalledWith(runId, fieldId, emptyMultiselectValue, fieldType);
|
||||
});
|
||||
|
||||
it('should update property value with empty string', async () => {
|
||||
const emptyValue = '';
|
||||
const updatedValue = {...mockPropertyValue, value: emptyValue};
|
||||
mockClient.setRunPropertyValue.mockResolvedValueOnce(updatedValue);
|
||||
|
||||
const result = await updatePlaybookRunPropertyValue(serverUrl, runId, fieldId, emptyValue);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.propertyValue).toEqual(updatedValue);
|
||||
expect(mockClient.setRunPropertyValue).toHaveBeenCalledWith(runId, fieldId, emptyValue, undefined);
|
||||
});
|
||||
|
||||
it('should handle client error', async () => {
|
||||
mockClient.setRunPropertyValue.mockRejectedValueOnce(new Error('Client error'));
|
||||
|
||||
const result = await updatePlaybookRunPropertyValue(serverUrl, runId, fieldId, value);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.propertyValue).toBeUndefined();
|
||||
expect(mockClient.setRunPropertyValue).toHaveBeenCalledWith(runId, fieldId, value, undefined);
|
||||
expect(handlePlaybookRunPropertyFields).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle network manager error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await updatePlaybookRunPropertyValue(serverUrl, runId, fieldId, value);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.propertyValue).toBeUndefined();
|
||||
expect(handlePlaybookRunPropertyFields).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle database storage error', async () => {
|
||||
const updatedValue = {...mockPropertyValue, value};
|
||||
mockClient.setRunPropertyValue.mockResolvedValueOnce(updatedValue);
|
||||
jest.mocked(handlePlaybookRunPropertyFields).mockResolvedValueOnce({error: new Error('DB error')});
|
||||
|
||||
const result = await updatePlaybookRunPropertyValue(serverUrl, runId, fieldId, value);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.propertyValue).toBeUndefined();
|
||||
expect(mockClient.setRunPropertyValue).toHaveBeenCalledWith(runId, fieldId, value, undefined);
|
||||
expect(handlePlaybookRunPropertyFields).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
92
app/products/playbooks/actions/remote/property_fields.ts
Normal file
92
app/products/playbooks/actions/remote/property_fields.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {handlePlaybookRunPropertyFields} from '@playbooks/actions/local/property_fields';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
/**
|
||||
* Fetch property fields and values for a playbook run from the server and store them in the database
|
||||
* @param serverUrl - The server URL
|
||||
* @param runId - The playbook run ID
|
||||
* @param fetchOnly - If true, only fetch from server without storing in DB
|
||||
* @returns Promise with error if any
|
||||
*/
|
||||
export const fetchPlaybookRunPropertyFields = async (
|
||||
serverUrl: string,
|
||||
runId: string,
|
||||
fetchOnly = false,
|
||||
): Promise<{error?: unknown}> => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
// Fetch property fields and values from the server
|
||||
const [propertyFields, propertyValues] = await Promise.all([
|
||||
client.fetchRunPropertyFields(runId),
|
||||
client.fetchRunPropertyValues(runId),
|
||||
]);
|
||||
|
||||
// Store in database if not fetchOnly
|
||||
if (!fetchOnly) {
|
||||
const result = await handlePlaybookRunPropertyFields(
|
||||
serverUrl,
|
||||
propertyFields,
|
||||
propertyValues,
|
||||
);
|
||||
if (result.error) {
|
||||
logDebug('error on handlePlaybookRunPropertyFields', getFullErrorMessage(result.error));
|
||||
return {error: result.error};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
logDebug('error on fetchPlaybookRunPropertyFields', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a property value for a playbook run on the server and locally
|
||||
* @param serverUrl - The server URL
|
||||
* @param runId - The playbook run ID
|
||||
* @param fieldId - The property field ID
|
||||
* @param value - The new value (string for text/select, comma-separated string for multiselect)
|
||||
* @param fieldType - The type of the field ('text', 'select', 'multiselect')
|
||||
* @returns Promise with updated property value or error
|
||||
*/
|
||||
export const updatePlaybookRunPropertyValue = async (
|
||||
serverUrl: string,
|
||||
runId: string,
|
||||
fieldId: string,
|
||||
value: string,
|
||||
fieldType?: string,
|
||||
): Promise<{error?: unknown; propertyValue?: PlaybookRunPropertyValue}> => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
// Update value on server
|
||||
const propertyValue = await client.setRunPropertyValue(runId, fieldId, value, fieldType);
|
||||
|
||||
// Update local database
|
||||
const result = await handlePlaybookRunPropertyFields(
|
||||
serverUrl,
|
||||
[],
|
||||
[propertyValue],
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
logDebug('error on handlePlaybookRunPropertyFields after update', getFullErrorMessage(result.error));
|
||||
return {error: result.error};
|
||||
}
|
||||
|
||||
return {propertyValue};
|
||||
} catch (error) {
|
||||
logDebug('error on updatePlaybookRunPropertyValue', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
@ -12,6 +12,7 @@ import {getLastPlaybookRunsFetchAt} from '@playbooks/database/queries/run';
|
|||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {fetchPlaybookRunPropertyFields} from './property_fields';
|
||||
import {fetchPlaybookRunsForChannel, fetchFinishedRunsForChannel, fetchPlaybookRunsPageForParticipant, setOwner, finishRun, renamePlaybookRun, createPlaybookRun, fetchPlaybookRun, fetchPlaybookRunMetadata, postStatusUpdate} from './runs';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
|
@ -34,6 +35,7 @@ const mockClient = {
|
|||
jest.mock('@playbooks/database/queries/run');
|
||||
jest.mock('@playbooks/actions/local/run');
|
||||
jest.mock('@playbooks/actions/local/channel');
|
||||
jest.mock('./property_fields');
|
||||
|
||||
const throwFunc = () => {
|
||||
throw Error('error');
|
||||
|
|
@ -239,6 +241,106 @@ describe('fetchFinishedRunsForChannel', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fetchPlaybookRun', () => {
|
||||
const runId = 'run-id-123';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(handlePlaybookRuns).mockResolvedValue({data: []});
|
||||
jest.mocked(fetchPlaybookRunPropertyFields).mockResolvedValue({});
|
||||
});
|
||||
|
||||
it('should fetch playbook run successfully', async () => {
|
||||
const mockRun = TestHelper.fakePlaybookRun({id: runId});
|
||||
mockClient.fetchPlaybookRun.mockResolvedValueOnce(mockRun);
|
||||
|
||||
const result = await fetchPlaybookRun(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.run).toEqual(mockRun);
|
||||
expect(mockClient.fetchPlaybookRun).toHaveBeenCalledWith(runId);
|
||||
expect(handlePlaybookRuns).toHaveBeenCalledWith(serverUrl, [mockRun], false, true);
|
||||
expect(fetchPlaybookRunPropertyFields).toHaveBeenCalledWith(serverUrl, runId, false);
|
||||
});
|
||||
|
||||
it('should handle fetchOnly mode without storing in DB', async () => {
|
||||
const mockRun = TestHelper.fakePlaybookRun({id: runId});
|
||||
mockClient.fetchPlaybookRun.mockResolvedValueOnce(mockRun);
|
||||
|
||||
const result = await fetchPlaybookRun(serverUrl, runId, true);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.run).toEqual(mockRun);
|
||||
expect(mockClient.fetchPlaybookRun).toHaveBeenCalledWith(runId);
|
||||
expect(handlePlaybookRuns).not.toHaveBeenCalled();
|
||||
expect(fetchPlaybookRunPropertyFields).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch property fields after fetching run', async () => {
|
||||
const mockRun = TestHelper.fakePlaybookRun({id: runId});
|
||||
mockClient.fetchPlaybookRun.mockResolvedValueOnce(mockRun);
|
||||
|
||||
await fetchPlaybookRun(serverUrl, runId);
|
||||
|
||||
expect(fetchPlaybookRunPropertyFields).toHaveBeenCalledWith(serverUrl, runId, false);
|
||||
expect(handlePlaybookRuns).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should continue even if property fields fetch fails', async () => {
|
||||
const mockRun = TestHelper.fakePlaybookRun({id: runId});
|
||||
mockClient.fetchPlaybookRun.mockResolvedValueOnce(mockRun);
|
||||
jest.mocked(fetchPlaybookRunPropertyFields).mockResolvedValueOnce({error: new Error('Property fields error')});
|
||||
|
||||
const result = await fetchPlaybookRun(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.run).toEqual(mockRun);
|
||||
expect(fetchPlaybookRunPropertyFields).toHaveBeenCalledWith(serverUrl, runId, false);
|
||||
});
|
||||
|
||||
it('should handle client error on fetchPlaybookRun', async () => {
|
||||
mockClient.fetchPlaybookRun.mockRejectedValueOnce(new Error('Client error'));
|
||||
|
||||
const result = await fetchPlaybookRun(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.run).toBeUndefined();
|
||||
expect(handlePlaybookRuns).not.toHaveBeenCalled();
|
||||
expect(fetchPlaybookRunPropertyFields).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle network manager error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await fetchPlaybookRun(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.run).toBeUndefined();
|
||||
expect(handlePlaybookRuns).not.toHaveBeenCalled();
|
||||
expect(fetchPlaybookRunPropertyFields).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle handlePlaybookRuns error', async () => {
|
||||
const mockRun = TestHelper.fakePlaybookRun({id: runId});
|
||||
mockClient.fetchPlaybookRun.mockResolvedValueOnce(mockRun);
|
||||
jest.mocked(handlePlaybookRuns).mockResolvedValueOnce({error: new Error('DB error')});
|
||||
|
||||
const result = await fetchPlaybookRun(serverUrl, runId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.run).toBeUndefined();
|
||||
expect(mockClient.fetchPlaybookRun).toHaveBeenCalledWith(runId);
|
||||
expect(handlePlaybookRuns).toHaveBeenCalledWith(serverUrl, [mockRun], false, true);
|
||||
expect(fetchPlaybookRunPropertyFields).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOwner', () => {
|
||||
const playbookRunId = 'playbook-run-id-1';
|
||||
const ownerId = 'owner-user-id-1';
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import EphemeralStore from '@store/ephemeral_store';
|
|||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import {fetchPlaybookRunPropertyFields} from './property_fields';
|
||||
|
||||
type PlaybookRunsRequest = {
|
||||
runs?: PlaybookRun[];
|
||||
error?: unknown;
|
||||
|
|
@ -100,6 +102,14 @@ export const fetchPlaybookRun = async (serverUrl: string, runId: string, fetchOn
|
|||
logDebug('error on handlePlaybookRuns', getFullErrorMessage(result.error));
|
||||
return {error: result.error};
|
||||
}
|
||||
|
||||
// Fetch property fields and values for the run
|
||||
const propertyFieldsResult = await fetchPlaybookRunPropertyFields(serverUrl, runId, fetchOnly);
|
||||
if (propertyFieldsResult.error) {
|
||||
logDebug('error on fetchPlaybookRunPropertyFields', getFullErrorMessage(propertyFieldsResult.error));
|
||||
|
||||
// Don't return error - property fields are not critical, just log it
|
||||
}
|
||||
}
|
||||
|
||||
return {run};
|
||||
|
|
|
|||
|
|
@ -712,7 +712,6 @@ describe('fetchPlaybooks', () => {
|
|||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.fetchPlaybooks(params);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
|
@ -814,7 +813,6 @@ describe('createPlaybookRun', () => {
|
|||
name,
|
||||
description,
|
||||
);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
|
@ -892,7 +890,6 @@ describe('createPlaybookRun', () => {
|
|||
name,
|
||||
playbook_id,
|
||||
};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.createPlaybookRun(
|
||||
|
|
@ -935,7 +932,6 @@ describe('createPlaybookRun', () => {
|
|||
};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.createPlaybookRun(
|
||||
playbook_id,
|
||||
owner_user_id,
|
||||
|
|
@ -945,7 +941,6 @@ describe('createPlaybookRun', () => {
|
|||
undefined,
|
||||
create_public_run,
|
||||
);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
|
@ -1083,3 +1078,271 @@ describe('addChecklistItem', () => {
|
|||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchRunPropertyFields', () => {
|
||||
test('should fetch property fields without updatedSince parameter', async () => {
|
||||
const runId = 'run123';
|
||||
const expectedUrl = '/plugins/playbooks/api/v0/runs/run123/property_fields';
|
||||
const expectedOptions = {method: 'get'};
|
||||
const mockResponse: PlaybookRunPropertyField[] = [
|
||||
{
|
||||
id: 'field1',
|
||||
group_id: 'group1',
|
||||
name: 'Priority',
|
||||
type: 'select',
|
||||
target_id: 'run123',
|
||||
target_type: 'run',
|
||||
create_at: 1234567890,
|
||||
update_at: 1234567890,
|
||||
delete_at: 0,
|
||||
attrs: '{"options":[{"id":"opt1","name":"High"}]}',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.fetchRunPropertyFields(runId);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should fetch property fields with updatedSince parameter', async () => {
|
||||
const runId = 'run123';
|
||||
const updatedSince = 1234567890;
|
||||
const queryParams = buildQueryString({updated_since: updatedSince});
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/run123/property_fields${queryParams}`;
|
||||
const expectedOptions = {method: 'get'};
|
||||
const mockResponse: PlaybookRunPropertyField[] = [];
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.fetchRunPropertyFields(runId, updatedSince);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should return empty array when doFetch returns null', async () => {
|
||||
const runId = 'run123';
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(null);
|
||||
|
||||
const result = await client.fetchRunPropertyFields(runId);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle error when fetching property fields', async () => {
|
||||
const runId = 'run123';
|
||||
|
||||
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(client.fetchRunPropertyFields(runId)).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchRunPropertyValues', () => {
|
||||
test('should fetch property values without updatedSince parameter', async () => {
|
||||
const runId = 'run123';
|
||||
const expectedUrl = '/plugins/playbooks/api/v0/runs/run123/property_values';
|
||||
const expectedOptions = {method: 'get'};
|
||||
const mockResponse: PlaybookRunPropertyValue[] = [
|
||||
{
|
||||
id: 'value1',
|
||||
field_id: 'field1',
|
||||
target_id: 'run123',
|
||||
update_at: 1234567890,
|
||||
value: 'opt1',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.fetchRunPropertyValues(runId);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should fetch property values with updatedSince parameter', async () => {
|
||||
const runId = 'run123';
|
||||
const updatedSince = 1234567890;
|
||||
const queryParams = buildQueryString({updated_since: updatedSince});
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/run123/property_values${queryParams}`;
|
||||
const expectedOptions = {method: 'get'};
|
||||
const mockResponse: PlaybookRunPropertyValue[] = [];
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.fetchRunPropertyValues(runId, updatedSince);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should return empty array when doFetch returns null', async () => {
|
||||
const runId = 'run123';
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(null);
|
||||
|
||||
const result = await client.fetchRunPropertyValues(runId);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle error when fetching property values', async () => {
|
||||
const runId = 'run123';
|
||||
|
||||
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(client.fetchRunPropertyValues(runId)).rejects.toThrow('Network error');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('setRunPropertyValue', () => {
|
||||
test('should set property value for text field', async () => {
|
||||
const runId = 'run123';
|
||||
const fieldId = 'field1';
|
||||
const value = 'New text value';
|
||||
const expectedUrl = '/plugins/playbooks/api/v0/runs/run123/property_fields/field1/value';
|
||||
const expectedOptions = {method: 'put', body: {value}};
|
||||
const mockResponse: PlaybookRunPropertyValue = {
|
||||
id: 'value1',
|
||||
field_id: fieldId,
|
||||
target_id: runId,
|
||||
update_at: 1234567890,
|
||||
value,
|
||||
};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setRunPropertyValue(runId, fieldId, value);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should set property value for select field', async () => {
|
||||
const runId = 'run123';
|
||||
const fieldId = 'field2';
|
||||
const value = 'opt1';
|
||||
const expectedUrl = '/plugins/playbooks/api/v0/runs/run123/property_fields/field2/value';
|
||||
const expectedOptions = {method: 'put', body: {value}};
|
||||
const mockResponse: PlaybookRunPropertyValue = {
|
||||
id: 'value2',
|
||||
field_id: fieldId,
|
||||
target_id: runId,
|
||||
update_at: 1234567890,
|
||||
value,
|
||||
};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setRunPropertyValue(runId, fieldId, value);
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should set property value for multiselect field with multiple values', async () => {
|
||||
const runId = 'run123';
|
||||
const fieldId = 'field3';
|
||||
const value = 'opt1,opt2';
|
||||
const fieldType = 'multiselect';
|
||||
const expectedUrl = '/plugins/playbooks/api/v0/runs/run123/property_fields/field3/value';
|
||||
const expectedOptions = {method: 'put', body: {value: ['opt1', 'opt2']}};
|
||||
const mockResponse: PlaybookRunPropertyValue = {
|
||||
id: 'value3',
|
||||
field_id: fieldId,
|
||||
target_id: runId,
|
||||
update_at: 1234567890,
|
||||
value: '["opt1","opt2"]',
|
||||
};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setRunPropertyValue(runId, fieldId, value, fieldType);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should set property value for multiselect field with single value', async () => {
|
||||
const runId = 'run123';
|
||||
const fieldId = 'field3';
|
||||
const value = 'opt1';
|
||||
const fieldType = 'multiselect';
|
||||
const expectedUrl = '/plugins/playbooks/api/v0/runs/run123/property_fields/field3/value';
|
||||
const expectedOptions = {method: 'put', body: {value: ['opt1']}};
|
||||
const mockResponse: PlaybookRunPropertyValue = {
|
||||
id: 'value3',
|
||||
field_id: fieldId,
|
||||
target_id: runId,
|
||||
update_at: 1234567890,
|
||||
value: '["opt1"]',
|
||||
};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setRunPropertyValue(runId, fieldId, value, fieldType);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should set property value for multiselect field with empty value', async () => {
|
||||
const runId = 'run123';
|
||||
const fieldId = 'field3';
|
||||
const value = '';
|
||||
const fieldType = 'multiselect';
|
||||
const expectedUrl = '/plugins/playbooks/api/v0/runs/run123/property_fields/field3/value';
|
||||
const expectedOptions = {method: 'put', body: {value: []}};
|
||||
const mockResponse: PlaybookRunPropertyValue = {
|
||||
id: 'value3',
|
||||
field_id: fieldId,
|
||||
target_id: runId,
|
||||
update_at: 1234567890,
|
||||
value: '[]',
|
||||
};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setRunPropertyValue(runId, fieldId, value, fieldType);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should set property value with empty string', async () => {
|
||||
const runId = 'run123';
|
||||
const fieldId = 'field1';
|
||||
const value = '';
|
||||
const expectedUrl = '/plugins/playbooks/api/v0/runs/run123/property_fields/field1/value';
|
||||
const expectedOptions = {method: 'put', body: {value}};
|
||||
const mockResponse: PlaybookRunPropertyValue = {
|
||||
id: 'value1',
|
||||
field_id: fieldId,
|
||||
target_id: runId,
|
||||
update_at: 1234567890,
|
||||
value,
|
||||
};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setRunPropertyValue(runId, fieldId, value);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should handle error when setting property value', async () => {
|
||||
const runId = 'run123';
|
||||
const fieldId = 'field1';
|
||||
const value = 'test';
|
||||
|
||||
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(client.setRunPropertyValue(runId, fieldId, value)).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ export interface ClientPlaybooksMix {
|
|||
// Slash Commands
|
||||
runChecklistItemSlashCommand: (playbookRunId: string, checklistNumber: number, itemNumber: number) => Promise<{trigger_id: string}>;
|
||||
setChecklistItemCommand: (playbookRunID: string, checklistNum: number, itemNum: number, command: string) => Promise<void>;
|
||||
|
||||
// Property Fields
|
||||
fetchRunPropertyFields: (runId: string, updatedSince?: number) => Promise<PlaybookRunPropertyField[]>;
|
||||
fetchRunPropertyValues: (runId: string, updatedSince?: number) => Promise<PlaybookRunPropertyValue[]>;
|
||||
setRunPropertyValue: (runId: string, fieldId: string, value: string, fieldType?: string) => Promise<PlaybookRunPropertyValue>;
|
||||
}
|
||||
|
||||
const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
|
|
@ -229,6 +234,45 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
|||
|
||||
return data;
|
||||
};
|
||||
|
||||
// Property Fields
|
||||
fetchRunPropertyFields = async (runId: string, updatedSince?: number) => {
|
||||
const queryParams = updatedSince ? buildQueryString({updated_since: updatedSince}) : '';
|
||||
const data = await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(runId)}/property_fields${queryParams}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
return data || [];
|
||||
};
|
||||
|
||||
fetchRunPropertyValues = async (runId: string, updatedSince?: number) => {
|
||||
const queryParams = updatedSince ? buildQueryString({updated_since: updatedSince}) : '';
|
||||
const data = await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(runId)}/property_values${queryParams}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
return data || [];
|
||||
};
|
||||
|
||||
setRunPropertyValue = async (runId: string, fieldId: string, value: string, fieldType?: string) => {
|
||||
// Convert value to appropriate format based on field type
|
||||
let bodyValue: string | string[] = value;
|
||||
|
||||
if (fieldType === 'multiselect') {
|
||||
// For multiselect fields, always convert to array (even if empty)
|
||||
if (value) {
|
||||
bodyValue = value.split(',').filter((id) => id.length > 0);
|
||||
} else {
|
||||
bodyValue = [];
|
||||
}
|
||||
}
|
||||
|
||||
const data = await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(runId)}/property_fields/${fieldId}/value`,
|
||||
{method: 'put', body: {value: bodyValue}},
|
||||
);
|
||||
return data;
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientPlaybooks;
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@
|
|||
export {default as PlaybookRunModel} from './playbook_run';
|
||||
export {default as PlaybookChecklistModel} from './playbook_checklist';
|
||||
export {default as PlaybookChecklistItemModel} from './playbook_checklist_item';
|
||||
export {default as PlaybookRunAttributeModel} from './playbook_run_attribute';
|
||||
export {default as PlaybookRunAttributeValueModel} from './playbook_run_attribute_value';
|
||||
export {default as PlaybookRunPropertyFieldModel} from './playbook_run_attribute';
|
||||
export {default as PlaybookRunPropertyValueModel} from './playbook_run_attribute_value';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import DatabaseManager from '@database/manager';
|
|||
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import PlaybookRunAttributeModel from './playbook_run_attribute';
|
||||
import PlaybookRunPropertyFieldModel from './playbook_run_attribute';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ const {PLAYBOOK_RUN_ATTRIBUTE} = PLAYBOOK_TABLES;
|
|||
|
||||
const SERVER_URL = `playbookRunAttributeModel.test.${Date.now()}.com`;
|
||||
|
||||
const applyMockData = (attribute: PlaybookRunAttributeModel, mockData: PlaybookRunAttribute, includeAttrs = true) => {
|
||||
const applyMockData = (attribute: PlaybookRunPropertyFieldModel, mockData: PlaybookRunPropertyField, includeAttrs = true) => {
|
||||
attribute._raw.id = mockData.id;
|
||||
attribute.groupId = mockData.group_id;
|
||||
attribute.name = mockData.name;
|
||||
|
|
@ -24,13 +24,20 @@ const applyMockData = (attribute: PlaybookRunAttributeModel, mockData: PlaybookR
|
|||
attribute.updateAt = mockData.update_at;
|
||||
attribute.deleteAt = mockData.delete_at;
|
||||
if (includeAttrs) {
|
||||
attribute.attrs = mockData.attrs || '{"placeholder": "Enter value"}';
|
||||
// API can send attrs as string or object, DB expects string
|
||||
if (!mockData.attrs) {
|
||||
attribute.attrs = '{"placeholder": "Enter value"}';
|
||||
} else if (typeof mockData.attrs === 'string') {
|
||||
attribute.attrs = mockData.attrs;
|
||||
} else {
|
||||
attribute.attrs = JSON.stringify(mockData.attrs);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('PlaybookRunAttributeModel', () => {
|
||||
describe('PlaybookRunPropertyFieldModel', () => {
|
||||
let operator: ServerDataOperator;
|
||||
let playbook_run_attribute: PlaybookRunAttributeModel;
|
||||
let playbook_run_attribute: PlaybookRunPropertyFieldModel;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([SERVER_URL]);
|
||||
|
|
@ -38,7 +45,7 @@ describe('PlaybookRunAttributeModel', () => {
|
|||
const {database} = operator;
|
||||
|
||||
await database.write(async () => {
|
||||
playbook_run_attribute = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((attribute: PlaybookRunAttributeModel) => {
|
||||
playbook_run_attribute = await database.get<PlaybookRunPropertyFieldModel>(PLAYBOOK_RUN_ATTRIBUTE).create((attribute: PlaybookRunPropertyFieldModel) => {
|
||||
applyMockData(attribute, TestHelper.createPlaybookRunAttribute('test', 0));
|
||||
});
|
||||
});
|
||||
|
|
@ -63,15 +70,15 @@ describe('PlaybookRunAttributeModel', () => {
|
|||
});
|
||||
|
||||
it('=> should have the correct table name', () => {
|
||||
expect(PlaybookRunAttributeModel.table).toBe(PLAYBOOK_RUN_ATTRIBUTE);
|
||||
expect(PlaybookRunPropertyFieldModel.table).toBe(PLAYBOOK_RUN_ATTRIBUTE);
|
||||
});
|
||||
|
||||
it('=> should handle optional attrs field', async () => {
|
||||
let attributeWithoutAttrs: PlaybookRunAttributeModel;
|
||||
let attributeWithoutAttrs: PlaybookRunPropertyFieldModel;
|
||||
const {database} = operator;
|
||||
|
||||
await database.write(async () => {
|
||||
attributeWithoutAttrs = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((attribute: PlaybookRunAttributeModel) => {
|
||||
attributeWithoutAttrs = await database.get<PlaybookRunPropertyFieldModel>(PLAYBOOK_RUN_ATTRIBUTE).create((attribute: PlaybookRunPropertyFieldModel) => {
|
||||
applyMockData(attribute, TestHelper.createPlaybookRunAttribute('test', 1), false);
|
||||
});
|
||||
});
|
||||
|
|
@ -85,7 +92,7 @@ describe('PlaybookRunAttributeModel', () => {
|
|||
const {database} = operator;
|
||||
|
||||
await database.write(async () => {
|
||||
await playbook_run_attribute.update((attribute: PlaybookRunAttributeModel) => {
|
||||
await playbook_run_attribute.update((attribute: PlaybookRunPropertyFieldModel) => {
|
||||
attribute.name = 'Updated Attribute Name';
|
||||
attribute.type = 'number';
|
||||
attribute.updateAt = 1620000005000;
|
||||
|
|
@ -103,7 +110,7 @@ describe('PlaybookRunAttributeModel', () => {
|
|||
const {database} = operator;
|
||||
|
||||
await database.write(async () => {
|
||||
await playbook_run_attribute.update((attribute: PlaybookRunAttributeModel) => {
|
||||
await playbook_run_attribute.update((attribute: PlaybookRunPropertyFieldModel) => {
|
||||
attribute.deleteAt = 1620000010000;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import type PlaybookRunAttributeModelInterface from '@playbooks/types/database/m
|
|||
const {PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
|
||||
|
||||
/**
|
||||
* The model represents a playbook run attribute definition in the Mattermost app.
|
||||
* The model represents a playbook run property field definition in the Mattermost app.
|
||||
*/
|
||||
export default class PlaybookRunAttributeModel extends Model implements PlaybookRunAttributeModelInterface {
|
||||
export default class PlaybookRunPropertyFieldModel extends Model implements PlaybookRunAttributeModelInterface {
|
||||
/** table (name) : PlaybookRunAttribute */
|
||||
static table = PLAYBOOK_RUN_ATTRIBUTE;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
|||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import PlaybookRunModel from './playbook_run';
|
||||
import PlaybookRunAttributeModel from './playbook_run_attribute';
|
||||
import PlaybookRunAttributeValueModel from './playbook_run_attribute_value';
|
||||
import PlaybookRunPropertyFieldModel from './playbook_run_attribute';
|
||||
import PlaybookRunPropertyValueModel from './playbook_run_attribute_value';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ const applyRunData = (run: PlaybookRunModel, mockData: PlaybookRun) => {
|
|||
run.updateAt = mockData.update_at;
|
||||
};
|
||||
|
||||
const applyAttributeData = (attribute: PlaybookRunAttributeModel, mockData: PlaybookRunAttribute) => {
|
||||
const applyAttributeData = (attribute: PlaybookRunPropertyFieldModel, mockData: PlaybookRunPropertyField) => {
|
||||
attribute._raw.id = mockData.id;
|
||||
attribute.groupId = mockData.group_id;
|
||||
attribute.name = mockData.name;
|
||||
|
|
@ -50,19 +50,24 @@ const applyAttributeData = (attribute: PlaybookRunAttributeModel, mockData: Play
|
|||
attribute.createAt = mockData.create_at;
|
||||
attribute.updateAt = mockData.update_at;
|
||||
attribute.deleteAt = mockData.delete_at;
|
||||
attribute.attrs = mockData.attrs;
|
||||
|
||||
// API can send attrs as string or object, DB expects string
|
||||
if (mockData.attrs) {
|
||||
attribute.attrs = typeof mockData.attrs === 'string' ? mockData.attrs : JSON.stringify(mockData.attrs);
|
||||
}
|
||||
};
|
||||
|
||||
const applyAttributeValueData = (attributeValue: PlaybookRunAttributeValueModel, mockData: PlaybookRunAttributeValue) => {
|
||||
const applyAttributeValueData = (attributeValue: PlaybookRunPropertyValueModel, mockData: PlaybookRunPropertyValue) => {
|
||||
attributeValue._raw.id = mockData.id;
|
||||
attributeValue.attributeId = mockData.attribute_id;
|
||||
attributeValue.runId = mockData.run_id;
|
||||
attributeValue.attributeId = mockData.field_id;
|
||||
attributeValue.runId = mockData.target_id;
|
||||
attributeValue.value = mockData.value;
|
||||
attributeValue.updateAt = mockData.update_at;
|
||||
};
|
||||
|
||||
describe('PlaybookRunAttributeValueModel', () => {
|
||||
describe('PlaybookRunPropertyValueModel', () => {
|
||||
let operator: ServerDataOperator;
|
||||
let playbook_run_attribute_value: PlaybookRunAttributeValueModel;
|
||||
let playbook_run_attribute_value: PlaybookRunPropertyValueModel;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([SERVER_URL]);
|
||||
|
|
@ -75,8 +80,8 @@ describe('PlaybookRunAttributeValueModel', () => {
|
|||
|
||||
await database.write(async () => {
|
||||
await database.get<PlaybookRunModel>(PLAYBOOK_RUN).create((run) => applyRunData(run, mockRun));
|
||||
await database.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((attr) => applyAttributeData(attr, mockAttribute));
|
||||
playbook_run_attribute_value = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((attrVal) => applyAttributeValueData(attrVal, mockAttributeValue));
|
||||
await database.get<PlaybookRunPropertyFieldModel>(PLAYBOOK_RUN_ATTRIBUTE).create((attr) => applyAttributeData(attr, mockAttribute));
|
||||
playbook_run_attribute_value = await database.get<PlaybookRunPropertyValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((attrVal) => applyAttributeValueData(attrVal, mockAttributeValue));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -90,19 +95,20 @@ describe('PlaybookRunAttributeValueModel', () => {
|
|||
expect(playbook_run_attribute_value.attributeId).toBe('test-attribute_0');
|
||||
expect(playbook_run_attribute_value.runId).toBe('playbook_run_0');
|
||||
expect(playbook_run_attribute_value.value).toBe('Value 1');
|
||||
expect(playbook_run_attribute_value.updateAt).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('=> should have the correct table name', () => {
|
||||
expect(PlaybookRunAttributeValueModel.table).toBe(PLAYBOOK_RUN_ATTRIBUTE_VALUE);
|
||||
expect(PlaybookRunPropertyValueModel.table).toBe(PLAYBOOK_RUN_ATTRIBUTE_VALUE);
|
||||
});
|
||||
|
||||
it('=> should have correct associations defined', () => {
|
||||
expect(PlaybookRunAttributeValueModel.associations).toBeDefined();
|
||||
expect(PlaybookRunAttributeValueModel.associations[PLAYBOOK_RUN_ATTRIBUTE]).toEqual({
|
||||
expect(PlaybookRunPropertyValueModel.associations).toBeDefined();
|
||||
expect(PlaybookRunPropertyValueModel.associations[PLAYBOOK_RUN_ATTRIBUTE]).toEqual({
|
||||
type: 'belongs_to',
|
||||
key: 'attribute_id',
|
||||
});
|
||||
expect(PlaybookRunAttributeValueModel.associations[PLAYBOOK_RUN]).toEqual({
|
||||
expect(PlaybookRunPropertyValueModel.associations[PLAYBOOK_RUN]).toEqual({
|
||||
type: 'belongs_to',
|
||||
key: 'run_id',
|
||||
});
|
||||
|
|
@ -132,7 +138,7 @@ describe('PlaybookRunAttributeValueModel', () => {
|
|||
const {database} = operator;
|
||||
|
||||
await database.write(async () => {
|
||||
await playbook_run_attribute_value.update((attributeValue: PlaybookRunAttributeValueModel) => {
|
||||
await playbook_run_attribute_value.update((attributeValue: PlaybookRunPropertyValueModel) => {
|
||||
attributeValue.value = 'Updated Test Value';
|
||||
});
|
||||
});
|
||||
|
|
@ -140,13 +146,26 @@ describe('PlaybookRunAttributeValueModel', () => {
|
|||
expect(playbook_run_attribute_value.value).toBe('Updated Test Value');
|
||||
});
|
||||
|
||||
it('=> should allow updating updateAt timestamp', async () => {
|
||||
const {database} = operator;
|
||||
const newTimestamp = Date.now() + 10000;
|
||||
|
||||
await database.write(async () => {
|
||||
await playbook_run_attribute_value.update((attributeValue: PlaybookRunPropertyValueModel) => {
|
||||
attributeValue.updateAt = newTimestamp;
|
||||
});
|
||||
});
|
||||
|
||||
expect(playbook_run_attribute_value.updateAt).toBe(newTimestamp);
|
||||
});
|
||||
|
||||
it('=> should handle empty value', async () => {
|
||||
let attributeValueWithEmptyValue: PlaybookRunAttributeValueModel;
|
||||
let attributeValueWithEmptyValue: PlaybookRunPropertyValueModel;
|
||||
const {database} = operator;
|
||||
|
||||
await database.write(async () => {
|
||||
const mockData = TestHelper.createPlaybookRunAttributeValue('test-attribute_0', 'playbook_run_0', 1);
|
||||
attributeValueWithEmptyValue = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((attributeValue: PlaybookRunAttributeValueModel) => {
|
||||
attributeValueWithEmptyValue = await database.get<PlaybookRunPropertyValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((attributeValue: PlaybookRunPropertyValueModel) => {
|
||||
applyAttributeValueData(attributeValue, mockData);
|
||||
attributeValue.value = '';
|
||||
});
|
||||
|
|
@ -159,12 +178,12 @@ describe('PlaybookRunAttributeValueModel', () => {
|
|||
it('=> should handle long text values', async () => {
|
||||
const longValue = 'This is a very long text value that might be used to store detailed information about the playbook run attribute. It could contain multiple sentences and various types of content.';
|
||||
|
||||
let attributeValueWithLongText: PlaybookRunAttributeValueModel;
|
||||
let attributeValueWithLongText: PlaybookRunPropertyValueModel;
|
||||
const {database} = operator;
|
||||
|
||||
await database.write(async () => {
|
||||
const mockData = TestHelper.createPlaybookRunAttributeValue('test-attribute_0', 'playbook_run_0', 2);
|
||||
attributeValueWithLongText = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((attributeValue: PlaybookRunAttributeValueModel) => {
|
||||
attributeValueWithLongText = await database.get<PlaybookRunPropertyValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((attributeValue: PlaybookRunPropertyValueModel) => {
|
||||
applyAttributeValueData(attributeValue, mockData);
|
||||
attributeValue.value = longValue;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,16 +7,16 @@ import Model, {type Associations} from '@nozbe/watermelondb/Model';
|
|||
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
||||
|
||||
import type PlaybookRunModel from './playbook_run';
|
||||
import type PlaybookRunAttributeModel from './playbook_run_attribute';
|
||||
import type PlaybookRunPropertyFieldModel from './playbook_run_attribute';
|
||||
import type {Relation} from '@nozbe/watermelondb';
|
||||
import type PlaybookRunAttributeValueModelInterface from '@playbooks/types/database/models/playbook_run_attribute_value';
|
||||
|
||||
const {PLAYBOOK_RUN, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
|
||||
|
||||
/**
|
||||
* The PlaybookRunAttributeValue model represents a playbook run attribute value in the Mattermost app.
|
||||
* The PlaybookRunPropertyValue model represents a playbook run property value in the Mattermost app.
|
||||
*/
|
||||
export default class PlaybookRunAttributeValueModel extends Model implements PlaybookRunAttributeValueModelInterface {
|
||||
export default class PlaybookRunPropertyValueModel extends Model implements PlaybookRunAttributeValueModelInterface {
|
||||
/** table (name) : PlaybookRunAttributeValue */
|
||||
static table = PLAYBOOK_RUN_ATTRIBUTE_VALUE;
|
||||
|
||||
|
|
@ -39,8 +39,11 @@ export default class PlaybookRunAttributeValueModel extends Model implements Pla
|
|||
/** value : The value of the attribute */
|
||||
@field('value') value!: string;
|
||||
|
||||
/** updateAt : The timestamp when this attribute value was last updated */
|
||||
@field('update_at') updateAt!: number;
|
||||
|
||||
/** attribute : The attribute this attribute value belongs to */
|
||||
@immutableRelation(PLAYBOOK_RUN_ATTRIBUTE, 'attribute_id') attribute!: Relation<PlaybookRunAttributeModel>;
|
||||
@immutableRelation(PLAYBOOK_RUN_ATTRIBUTE, 'attribute_id') attribute!: Relation<PlaybookRunPropertyFieldModel>;
|
||||
|
||||
/** run : The playbook run this attribute belongs to */
|
||||
@immutableRelation(PLAYBOOK_RUN, 'run_id') run!: Relation<PlaybookRunModel>;
|
||||
|
|
|
|||
|
|
@ -554,20 +554,20 @@ describe('PlaybookHandler', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('handlePlaybookRunAttribute', () => {
|
||||
it('should return an empty array if attributes is undefined or empty', async () => {
|
||||
describe('handlePlaybookRunPropertyField', () => {
|
||||
it('should return an empty array if propertyFields is undefined or empty', async () => {
|
||||
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
|
||||
|
||||
let result = await operator.handlePlaybookRunAttribute({
|
||||
attributes: undefined,
|
||||
let result = await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields: undefined,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(spyOnPrepareRecords).not.toHaveBeenCalled();
|
||||
|
||||
result = await operator.handlePlaybookRunAttribute({
|
||||
attributes: [],
|
||||
result = await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields: [],
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
|
|
@ -575,54 +575,54 @@ describe('PlaybookHandler', () => {
|
|||
expect(spyOnPrepareRecords).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should process attributes correctly', async () => {
|
||||
const mockAttributes = [
|
||||
it('should process property fields correctly', async () => {
|
||||
const mockPropertyFields = [
|
||||
TestHelper.createPlaybookRunAttribute('attribute_1', 0),
|
||||
TestHelper.createPlaybookRunAttribute('attribute_2', 1),
|
||||
].map<PartialPlaybookRunAttribute>((attribute, index) => ({
|
||||
...attribute,
|
||||
run_id: 'playbook_run_1',
|
||||
].map<PartialPlaybookRunPropertyField>((propertyField, index) => ({
|
||||
...propertyField,
|
||||
target_id: 'playbook_run_1',
|
||||
order: index,
|
||||
}));
|
||||
|
||||
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
|
||||
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
|
||||
|
||||
const result = await operator.handlePlaybookRunAttribute({
|
||||
attributes: mockAttributes,
|
||||
const result = await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields: mockPropertyFields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.length).toBe(mockAttributes.length);
|
||||
expect(result.length).toBe(mockPropertyFields.length);
|
||||
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const attributeRecords = await database.get(PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
|
||||
expect(attributeRecords.length).toBe(mockAttributes.length);
|
||||
expect(attributeRecords.length).toBe(mockPropertyFields.length);
|
||||
});
|
||||
|
||||
it('should only prepare records when prepareRecordsOnly is true', async () => {
|
||||
const mockAttributes = [
|
||||
const mockPropertyFields = [
|
||||
TestHelper.createPlaybookRunAttribute('attribute_3', 2),
|
||||
].map<PartialPlaybookRunAttribute>((attribute, index) => ({
|
||||
...attribute,
|
||||
run_id: 'playbook_run_1',
|
||||
].map<PartialPlaybookRunPropertyField>((propertyField, index) => ({
|
||||
...propertyField,
|
||||
target_id: 'playbook_run_1',
|
||||
order: index,
|
||||
}));
|
||||
|
||||
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
|
||||
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
|
||||
|
||||
const result = await operator.handlePlaybookRunAttribute({
|
||||
attributes: mockAttributes,
|
||||
const result = await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields: mockPropertyFields,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.length).toBe(mockAttributes.length);
|
||||
expect(result.length).toBe(mockPropertyFields.length);
|
||||
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnBatchOperation).not.toHaveBeenCalled();
|
||||
|
||||
|
|
@ -633,20 +633,20 @@ describe('PlaybookHandler', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('handlePlaybookRunAttributeValue', () => {
|
||||
it('should return an empty array if attributeValues is undefined or empty', async () => {
|
||||
describe('handlePlaybookRunPropertyValue', () => {
|
||||
it('should return an empty array if propertyValues is undefined or empty', async () => {
|
||||
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
|
||||
|
||||
let result = await operator.handlePlaybookRunAttributeValue({
|
||||
attributeValues: undefined,
|
||||
let result = await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues: undefined,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(spyOnPrepareRecords).not.toHaveBeenCalled();
|
||||
|
||||
result = await operator.handlePlaybookRunAttributeValue({
|
||||
attributeValues: [],
|
||||
result = await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues: [],
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
|
|
@ -654,8 +654,8 @@ describe('PlaybookHandler', () => {
|
|||
expect(spyOnPrepareRecords).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should process attribute values correctly', async () => {
|
||||
const mockAttributeValues = [
|
||||
it('should process property values correctly', async () => {
|
||||
const mockPropertyValues = [
|
||||
TestHelper.createPlaybookRunAttributeValue('attribute_1', 'playbook_run_1', 0),
|
||||
TestHelper.createPlaybookRunAttributeValue('attribute_2', 'playbook_run_2', 1),
|
||||
];
|
||||
|
|
@ -663,37 +663,37 @@ describe('PlaybookHandler', () => {
|
|||
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
|
||||
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
|
||||
|
||||
const result = await operator.handlePlaybookRunAttributeValue({
|
||||
attributeValues: mockAttributeValues,
|
||||
const result = await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues: mockPropertyValues,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.length).toBe(mockAttributeValues.length);
|
||||
expect(result.length).toBe(mockPropertyValues.length);
|
||||
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const attributeValueRecords = await database.get(PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
|
||||
expect(attributeValueRecords.length).toBe(mockAttributeValues.length);
|
||||
expect(attributeValueRecords.length).toBe(mockPropertyValues.length);
|
||||
});
|
||||
|
||||
it('should only prepare records when prepareRecordsOnly is true', async () => {
|
||||
const mockAttributeValues = [
|
||||
const mockPropertyValues = [
|
||||
TestHelper.createPlaybookRunAttributeValue('attribute_3', 'playbook_run_3', 2),
|
||||
];
|
||||
|
||||
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
|
||||
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
|
||||
|
||||
const result = await operator.handlePlaybookRunAttributeValue({
|
||||
attributeValues: mockAttributeValues,
|
||||
const result = await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues: mockPropertyValues,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.length).toBe(mockAttributeValues.length);
|
||||
expect(result.length).toBe(mockPropertyValues.length);
|
||||
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnBatchOperation).not.toHaveBeenCalled();
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
|||
import {logWarning} from '@utils/log';
|
||||
|
||||
import {shouldHandlePlaybookChecklistItemRecord, shouldHandlePlaybookChecklistRecord, shouldUpdatePlaybookRunRecord} from '../comparators';
|
||||
import {transformPlaybookChecklistItemRecord, transformPlaybookChecklistRecord, transformPlaybookRunRecord, transformPlaybookRunAttributeRecord, transformPlaybookRunAttributeValueRecord} from '../transformers';
|
||||
import {transformPlaybookChecklistItemRecord, transformPlaybookChecklistRecord, transformPlaybookRunRecord, transformPlaybookRunPropertyFieldRecord, transformPlaybookRunPropertyValueRecord} from '../transformers';
|
||||
|
||||
import type ServerDataOperatorBase from '@database/operator/server_data_operator/handlers';
|
||||
import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist';
|
||||
|
|
@ -34,14 +34,14 @@ type HandlePlaybookChecklistItemArgs = {
|
|||
items?: PartialChecklistItem[];
|
||||
}
|
||||
|
||||
type HandlePlaybookRunAttributeArgs = {
|
||||
type HandlePlaybookRunPropertyFieldArgs = {
|
||||
prepareRecordsOnly: boolean;
|
||||
attributes?: PartialPlaybookRunAttribute[];
|
||||
propertyFields?: PartialPlaybookRunPropertyField[];
|
||||
}
|
||||
|
||||
type HandlePlaybookRunAttributeValueArgs = {
|
||||
type HandlePlaybookRunPropertyValueArgs = {
|
||||
prepareRecordsOnly: boolean;
|
||||
attributeValues?: PartialPlaybookRunAttributeValue[];
|
||||
propertyValues?: PartialPlaybookRunPropertyValue[];
|
||||
}
|
||||
|
||||
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
|
||||
|
|
@ -50,8 +50,8 @@ export interface PlaybookHandlerMix {
|
|||
handlePlaybookRun: (args: HandlePlaybookRunArgs) => Promise<Model[]>;
|
||||
handlePlaybookChecklist: (args: HandlePlaybookChecklistArgs) => Promise<Model[]>;
|
||||
handlePlaybookChecklistItem: (args: HandlePlaybookChecklistItemArgs) => Promise<PlaybookChecklistItemModel[]>;
|
||||
handlePlaybookRunAttribute: (args: HandlePlaybookRunAttributeArgs) => Promise<Model[]>;
|
||||
handlePlaybookRunAttributeValue: (args: HandlePlaybookRunAttributeValueArgs) => Promise<Model[]>;
|
||||
handlePlaybookRunPropertyField: (args: HandlePlaybookRunPropertyFieldArgs) => Promise<Model[]>;
|
||||
handlePlaybookRunPropertyValue: (args: HandlePlaybookRunPropertyValueArgs) => Promise<Model[]>;
|
||||
}
|
||||
|
||||
const PlaybookHandler = <TBase extends Constructor<ServerDataOperatorBase>>(superclass: TBase) => class extends superclass {
|
||||
|
|
@ -157,6 +157,37 @@ const PlaybookHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supe
|
|||
|
||||
const childRecords = await this.handlePlaybookChecklist({checklists, prepareRecordsOnly: true, processChildren});
|
||||
batchRecords.push(...childRecords);
|
||||
|
||||
// Process property_fields and property_values
|
||||
const propertyFields = uniqueRaws.reduce<PartialPlaybookRunPropertyField[]>((res, raw) => {
|
||||
if (raw.property_fields?.length) {
|
||||
res.push(...raw.property_fields);
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
const propertyValues = uniqueRaws.reduce<PartialPlaybookRunPropertyValue[]>((res, raw) => {
|
||||
if (raw.property_values?.length) {
|
||||
res.push(...raw.property_values);
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
if (propertyFields.length) {
|
||||
const propertyFieldRecords = await this.handlePlaybookRunPropertyField({
|
||||
propertyFields,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
batchRecords.push(...propertyFieldRecords);
|
||||
}
|
||||
|
||||
if (propertyValues.length) {
|
||||
const propertyValueRecords = await this.handlePlaybookRunPropertyValue({
|
||||
propertyValues,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
batchRecords.push(...propertyValueRecords);
|
||||
}
|
||||
}
|
||||
|
||||
if (batchRecords.length && !prepareRecordsOnly) {
|
||||
|
|
@ -288,55 +319,55 @@ const PlaybookHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supe
|
|||
};
|
||||
|
||||
/**
|
||||
* Handles the playbook run attribute records.
|
||||
* @param {HandlePlaybookRunAttributeArgs} args - The arguments for handling playbook run attribute records.
|
||||
* Handles the playbook run property field records.
|
||||
* @param {HandlePlaybookRunPropertyFieldArgs} args - The arguments for handling playbook run property field records.
|
||||
* @param {boolean} args.prepareRecordsOnly - If true, only prepares the records without saving them.
|
||||
* @param {PlaybookRunAttribute[]} [args.attributes] - The playbook run attribute records to handle.
|
||||
* @returns {Promise<Model[]>} - A promise that resolves to an array of handled playbook run attribute records.
|
||||
* @param {PlaybookRunPropertyField[]} [args.propertyFields] - The playbook run property field records to handle.
|
||||
* @returns {Promise<Model[]>} - A promise that resolves to an array of handled playbook run property field records.
|
||||
*/
|
||||
handlePlaybookRunAttribute = async ({attributes, prepareRecordsOnly = true}: HandlePlaybookRunAttributeArgs): Promise<Model[]> => {
|
||||
if (!attributes?.length) {
|
||||
handlePlaybookRunPropertyField = async ({propertyFields, prepareRecordsOnly = true}: HandlePlaybookRunPropertyFieldArgs): Promise<Model[]> => {
|
||||
if (!propertyFields?.length) {
|
||||
logWarning(
|
||||
'An empty or undefined "attributes" array has been passed to the handlePlaybookRunAttribute method',
|
||||
'An empty or undefined "propertyFields" array has been passed to the handlePlaybookRunPropertyField method',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const createOrUpdateRawValues = getUniqueRawsBy({raws: attributes, key: 'id'});
|
||||
const createOrUpdateRawValues = getUniqueRawsBy({raws: propertyFields, key: 'id'});
|
||||
|
||||
return this.handleRecords({
|
||||
fieldName: 'id',
|
||||
transformer: transformPlaybookRunAttributeRecord,
|
||||
transformer: transformPlaybookRunPropertyFieldRecord,
|
||||
createOrUpdateRawValues,
|
||||
tableName: PLAYBOOK_RUN_ATTRIBUTE,
|
||||
prepareRecordsOnly,
|
||||
}, 'handlePlaybookRunAttribute');
|
||||
}, 'handlePlaybookRunPropertyField');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the playbook run attribute value records.
|
||||
* @param {HandlePlaybookRunAttributeValueArgs} args - The arguments for handling playbook run attribute value records.
|
||||
* Handles the playbook run property value records.
|
||||
* @param {HandlePlaybookRunPropertyValueArgs} args - The arguments for handling playbook run property value records.
|
||||
* @param {boolean} args.prepareRecordsOnly - If true, only prepares the records without saving them.
|
||||
* @param {PlaybookRunAttributeValue[]} [args.attributeValues] - The playbook run attribute value records to handle.
|
||||
* @returns {Promise<Model[]>} - A promise that resolves to an array of handled playbook run attribute value records.
|
||||
* @param {PlaybookRunPropertyValue[]} [args.propertyValues] - The playbook run property value records to handle.
|
||||
* @returns {Promise<Model[]>} - A promise that resolves to an array of handled playbook run property value records.
|
||||
*/
|
||||
handlePlaybookRunAttributeValue = async ({attributeValues, prepareRecordsOnly = true}: HandlePlaybookRunAttributeValueArgs): Promise<Model[]> => {
|
||||
if (!attributeValues?.length) {
|
||||
handlePlaybookRunPropertyValue = async ({propertyValues, prepareRecordsOnly = true}: HandlePlaybookRunPropertyValueArgs): Promise<Model[]> => {
|
||||
if (!propertyValues?.length) {
|
||||
logWarning(
|
||||
'An empty or undefined "attributeValues" array has been passed to the handlePlaybookRunAttributeValue method',
|
||||
'An empty or undefined "propertyValues" array has been passed to the handlePlaybookRunPropertyValue method',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const createOrUpdateRawValues = getUniqueRawsBy({raws: attributeValues, key: 'id'});
|
||||
const createOrUpdateRawValues = getUniqueRawsBy({raws: propertyValues, key: 'id'});
|
||||
|
||||
return this.handleRecords({
|
||||
fieldName: 'id',
|
||||
transformer: transformPlaybookRunAttributeValueRecord,
|
||||
transformer: transformPlaybookRunPropertyValueRecord,
|
||||
createOrUpdateRawValues,
|
||||
tableName: PLAYBOOK_RUN_ATTRIBUTE_VALUE,
|
||||
prepareRecordsOnly,
|
||||
}, 'handlePlaybookRunAttributeValue');
|
||||
}, 'handlePlaybookRunPropertyValue');
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
|||
import {PLAYBOOK_RUN_TYPES} from '@playbooks/constants/playbook_run';
|
||||
import {PlaybookRunModel} from '@playbooks/database/models';
|
||||
|
||||
import {transformPlaybookChecklistItemRecord, transformPlaybookChecklistRecord, transformPlaybookRunRecord, transformPlaybookRunAttributeRecord, transformPlaybookRunAttributeValueRecord} from '.';
|
||||
import {transformPlaybookChecklistItemRecord, transformPlaybookChecklistRecord, transformPlaybookRunRecord, transformPlaybookRunPropertyFieldRecord, transformPlaybookRunPropertyValueRecord} from '.';
|
||||
|
||||
import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist';
|
||||
import type PlaybookChecklistItemModel from '@playbooks/types/database/models/playbook_checklist_item';
|
||||
import type PlaybookRunAttributeModel from '@playbooks/types/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunAttributeValueModel from '@playbooks/types/database/models/playbook_run_attribute_value';
|
||||
import type PlaybookRunPropertyFieldModel from '@playbooks/types/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunPropertyValueModel from '@playbooks/types/database/models/playbook_run_attribute_value';
|
||||
|
||||
const {PLAYBOOK_RUN, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
|
||||
|
||||
|
|
@ -978,13 +978,13 @@ describe('*** PLAYBOOK_CHECKLIST_ITEM Prepare Records Test ***', () => {
|
|||
});
|
||||
|
||||
describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
||||
it('=> transformPlaybookRunAttributeRecord: should return a record of type PlaybookRunAttribute for CREATE action', async () => {
|
||||
it('=> transformPlaybookRunPropertyFieldRecord: should return a record of type PlaybookRunAttribute for CREATE action', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeRecord({
|
||||
const preparedRecord = await transformPlaybookRunPropertyFieldRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
|
|
@ -1008,15 +1008,15 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
|||
expect(preparedRecord!.collection.table).toBe(PLAYBOOK_RUN_ATTRIBUTE);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeRecord: should return a record of type PlaybookRunAttribute for UPDATE action', async () => {
|
||||
it('=> transformPlaybookRunPropertyFieldRecord: should return a record of type PlaybookRunAttribute for UPDATE action', async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
let existingRecord: PlaybookRunAttributeModel | undefined;
|
||||
let existingRecord: PlaybookRunPropertyFieldModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((record) => {
|
||||
existingRecord = await database!.get<PlaybookRunPropertyFieldModel>(PLAYBOOK_RUN_ATTRIBUTE).create((record) => {
|
||||
record._raw.id = 'attribute_2';
|
||||
record.groupId = 'group_2';
|
||||
record.name = 'Existing Attribute';
|
||||
|
|
@ -1030,7 +1030,7 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeRecord({
|
||||
const preparedRecord = await transformPlaybookRunPropertyFieldRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
|
|
@ -1059,14 +1059,14 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
|||
expect(preparedRecord!.collection.table).toBe(PLAYBOOK_RUN_ATTRIBUTE);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeRecord: should throw an error for non-create action without an existing record', async () => {
|
||||
it('=> transformPlaybookRunPropertyFieldRecord: should throw an error for non-create action without an existing record', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
await expect(
|
||||
transformPlaybookRunAttributeRecord({
|
||||
transformPlaybookRunPropertyFieldRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
|
|
@ -1087,13 +1087,13 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
|||
).rejects.toThrow('Record not found for non create action');
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeRecord: should keep most of the data if the partial attribute is empty', async () => {
|
||||
it('=> transformPlaybookRunPropertyFieldRecord: should keep most of the data if the partial attribute is empty', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
let existingRecord: PlaybookRunAttributeModel | undefined;
|
||||
let existingRecord: PlaybookRunPropertyFieldModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((record) => {
|
||||
existingRecord = await database!.get<PlaybookRunPropertyFieldModel>(PLAYBOOK_RUN_ATTRIBUTE).create((record) => {
|
||||
record._raw.id = 'attribute_2';
|
||||
record.groupId = 'group_2';
|
||||
record.name = 'Existing Attribute';
|
||||
|
|
@ -1107,14 +1107,21 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeRecord({
|
||||
const preparedRecord = await transformPlaybookRunPropertyFieldRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: existingRecord,
|
||||
raw: {
|
||||
id: 'attribute_2',
|
||||
group_id: 'group_2',
|
||||
name: 'Existing Attribute',
|
||||
type: 'text',
|
||||
target_id: 'target_2',
|
||||
target_type: 'playbook_run',
|
||||
create_at: 1620000000000,
|
||||
update_at: 1620000004000,
|
||||
delete_at: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1135,11 +1142,50 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
|||
expect(preparedRecord.updateAt).toBe(1620000004000);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunPropertyFieldRecord: should serialize attrs object to string', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const attrsObject = {
|
||||
options: [{id: 'opt1', name: 'Option 1'}, {id: 'opt2', name: 'Option 2'}],
|
||||
parent_id: 'parent123',
|
||||
sort_order: 5,
|
||||
value_type: 'select',
|
||||
visibility: 'always' as const,
|
||||
};
|
||||
|
||||
const preparedRecord = await transformPlaybookRunPropertyFieldRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'attribute_with_object',
|
||||
group_id: 'group_1',
|
||||
name: 'Select Attribute',
|
||||
type: 'select',
|
||||
target_id: 'target_1',
|
||||
target_type: 'playbook_run',
|
||||
create_at: 1620000000000,
|
||||
update_at: 1620000001000,
|
||||
delete_at: 0,
|
||||
attrs: attrsObject, // Pass as object (as API sends it)
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
|
||||
// Verify attrs was serialized to string
|
||||
expect(preparedRecord!.attrs).toBe(JSON.stringify(attrsObject));
|
||||
});
|
||||
it('=> transformPlaybookRunAttributeRecord: should handle CREATE with missing raw.id', async () => {
|
||||
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeRecord({
|
||||
const preparedRecord = await transformPlaybookRunPropertyFieldRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
|
|
@ -1158,9 +1204,9 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
|||
it('=> transformPlaybookRunAttributeRecord: should handle completely empty raw object', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
|
||||
|
||||
let existingRecord: PlaybookRunAttributeModel | undefined;
|
||||
let existingRecord: PlaybookRunPropertyFieldModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((record) => {
|
||||
existingRecord = await database!.get<PlaybookRunPropertyFieldModel>(PLAYBOOK_RUN_ATTRIBUTE).create((record) => {
|
||||
record._raw.id = 'attribute_empty_raw';
|
||||
record.groupId = 'group_1';
|
||||
record.name = 'Existing Attribute';
|
||||
|
|
@ -1174,7 +1220,7 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeRecord({
|
||||
const preparedRecord = await transformPlaybookRunPropertyFieldRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
|
|
@ -1193,21 +1239,22 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
|||
});
|
||||
|
||||
describe('*** PLAYBOOK_RUN_ATTRIBUTE_VALUE Prepare Records Test ***', () => {
|
||||
it('=> transformPlaybookRunAttributeValueRecord: should return a record of type PlaybookRunAttributeValue for CREATE action', async () => {
|
||||
it('=> transformPlaybookRunPropertyValueRecord: should return a record of type PlaybookRunAttributeValue for CREATE action', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeValueRecord({
|
||||
const preparedRecord = await transformPlaybookRunPropertyValueRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'attribute_value_1',
|
||||
attribute_id: 'attribute_1',
|
||||
run_id: 'playbook_run_1',
|
||||
field_id: 'attribute_1', // API field name
|
||||
target_id: 'playbook_run_1', // API field name
|
||||
update_at: 1620000001000,
|
||||
value: 'Test Value',
|
||||
},
|
||||
},
|
||||
|
|
@ -1217,31 +1264,33 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE_VALUE Prepare Records Test ***', () => {
|
|||
expect(preparedRecord!.collection.table).toBe(PLAYBOOK_RUN_ATTRIBUTE_VALUE);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeValueRecord: should return a record of type PlaybookRunAttributeValue for UPDATE action', async () => {
|
||||
it('=> transformPlaybookRunPropertyValueRecord: should return a record of type PlaybookRunAttributeValue for UPDATE action', async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
let existingRecord: PlaybookRunAttributeValueModel | undefined;
|
||||
let existingRecord: PlaybookRunPropertyValueModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
|
||||
existingRecord = await database!.get<PlaybookRunPropertyValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
|
||||
record._raw.id = 'attribute_value_2';
|
||||
record.attributeId = 'attribute_2';
|
||||
record.runId = 'playbook_run_2';
|
||||
record.value = 'Existing Value';
|
||||
record.updateAt = 1620000001000;
|
||||
});
|
||||
});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeValueRecord({
|
||||
const preparedRecord = await transformPlaybookRunPropertyValueRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: existingRecord,
|
||||
raw: {
|
||||
id: 'attribute_value_2',
|
||||
attribute_id: 'attribute_2',
|
||||
run_id: 'playbook_run_2',
|
||||
field_id: 'attribute_2', // API field name
|
||||
target_id: 'playbook_run_2', // API field name
|
||||
update_at: 1620000002000,
|
||||
value: 'Updated Value',
|
||||
},
|
||||
},
|
||||
|
|
@ -1256,22 +1305,23 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE_VALUE Prepare Records Test ***', () => {
|
|||
expect(preparedRecord!.collection.table).toBe(PLAYBOOK_RUN_ATTRIBUTE_VALUE);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeValueRecord: should throw an error for non-create action without an existing record', async () => {
|
||||
it('=> transformPlaybookRunPropertyValueRecord: should throw an error for non-create action without an existing record', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
await expect(
|
||||
transformPlaybookRunAttributeValueRecord({
|
||||
transformPlaybookRunPropertyValueRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'attribute_value_3',
|
||||
attribute_id: 'attribute_3',
|
||||
run_id: 'playbook_run_3',
|
||||
field_id: 'attribute_3', // API field name
|
||||
target_id: 'playbook_run_3', // API field name
|
||||
update_at: 1620000001000,
|
||||
value: 'Invalid Value',
|
||||
},
|
||||
},
|
||||
|
|
@ -1279,29 +1329,32 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE_VALUE Prepare Records Test ***', () => {
|
|||
).rejects.toThrow('Record not found for non create action');
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeValueRecord: should keep most of the data if the partial attribute value is empty', async () => {
|
||||
it('=> transformPlaybookRunPropertyValueRecord: should keep most of the data if the partial attribute value is empty', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
let existingRecord: PlaybookRunAttributeValueModel | undefined;
|
||||
let existingRecord: PlaybookRunPropertyValueModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
|
||||
existingRecord = await database!.get<PlaybookRunPropertyValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
|
||||
record._raw.id = 'attribute_value_2';
|
||||
record.attributeId = 'attribute_2';
|
||||
record.runId = 'playbook_run_2';
|
||||
record.value = 'Existing Value';
|
||||
record.updateAt = 1620000000000;
|
||||
});
|
||||
});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeValueRecord({
|
||||
const preparedRecord = await transformPlaybookRunPropertyValueRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: existingRecord,
|
||||
raw: {
|
||||
id: 'attribute_value_2',
|
||||
run_id: 'playbook_run_2',
|
||||
attribute_id: 'attribute_2',
|
||||
field_id: 'attribute_2', // API field name
|
||||
target_id: 'playbook_run_2', // API field name
|
||||
update_at: 1620000001000,
|
||||
value: 'Existing Value',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1314,12 +1367,95 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE_VALUE Prepare Records Test ***', () => {
|
|||
expect(preparedRecord.attributeId).toBe('attribute_2');
|
||||
expect(preparedRecord.runId).toBe('playbook_run_2');
|
||||
expect(preparedRecord.value).toBe('Existing Value');
|
||||
expect(preparedRecord.updateAt).toBe(1620000001000);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunPropertyValueRecord: should update to empty array when multiselect value is cleared', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_empty_multiselect', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
let existingRecord: PlaybookRunPropertyValueModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunPropertyValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
|
||||
record._raw.id = 'attribute_value_3';
|
||||
record.attributeId = 'attribute_3';
|
||||
record.runId = 'playbook_run_3';
|
||||
record.value = '["option1","option2"]';
|
||||
record.updateAt = 1620000000000;
|
||||
});
|
||||
});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunPropertyValueRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: existingRecord,
|
||||
raw: {
|
||||
id: 'attribute_value_3',
|
||||
field_id: 'attribute_3',
|
||||
target_id: 'playbook_run_3',
|
||||
update_at: 1620000001000,
|
||||
value: [] as any, // Server sends arrays for multiselect fields
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await database?.write(async () => {
|
||||
await database?.batch(preparedRecord);
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.attributeId).toBe('attribute_3');
|
||||
expect(preparedRecord.runId).toBe('playbook_run_3');
|
||||
expect(preparedRecord.value).toBe('[]');
|
||||
expect(preparedRecord.updateAt).toBe(1620000001000);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunPropertyValueRecord: should update to empty string when value is null', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_null', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
let existingRecord: PlaybookRunPropertyValueModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunPropertyValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
|
||||
record._raw.id = 'attribute_value_4';
|
||||
record.attributeId = 'attribute_4';
|
||||
record.runId = 'playbook_run_4';
|
||||
record.value = 'Some Value';
|
||||
record.updateAt = 1620000000000;
|
||||
});
|
||||
});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunPropertyValueRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: existingRecord,
|
||||
raw: {
|
||||
id: 'attribute_value_4',
|
||||
field_id: 'attribute_4',
|
||||
target_id: 'playbook_run_4',
|
||||
update_at: 1620000001000,
|
||||
value: null as any, // Server can send null to clear values
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await database?.write(async () => {
|
||||
await database?.batch(preparedRecord);
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.attributeId).toBe('attribute_4');
|
||||
expect(preparedRecord.runId).toBe('playbook_run_4');
|
||||
expect(preparedRecord.value).toBe('');
|
||||
expect(preparedRecord.updateAt).toBe(1620000001000);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeValueRecord: should handle CREATE with missing raw.id', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeValueRecord({
|
||||
const preparedRecord = await transformPlaybookRunPropertyValueRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
|
|
@ -1340,9 +1476,9 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE_VALUE Prepare Records Test ***', () => {
|
|||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
let existingRecord: PlaybookRunAttributeValueModel | undefined;
|
||||
let existingRecord: PlaybookRunPropertyValueModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
|
||||
existingRecord = await database!.get<PlaybookRunPropertyValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
|
||||
record._raw.id = 'attribute_value_empty_raw';
|
||||
record.attributeId = 'attribute_1';
|
||||
record.runId = 'playbook_run_1';
|
||||
|
|
@ -1350,7 +1486,7 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE_VALUE Prepare Records Test ***', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeValueRecord({
|
||||
const preparedRecord = await transformPlaybookRunPropertyValueRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import {OperationType} from '@constants/database';
|
|||
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
|
||||
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
||||
import {PLAYBOOK_RUN_TYPES} from '@playbooks/constants/playbook_run';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist';
|
||||
import type PlaybookChecklistItemModel from '@playbooks/types/database/models/playbook_checklist_item';
|
||||
import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run';
|
||||
import type PlaybookRunAttributeModel from '@playbooks/types/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunAttributeValueModel from '@playbooks/types/database/models/playbook_run_attribute_value';
|
||||
import type PlaybookRunPropertyFieldModel from '@playbooks/types/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunPropertyValueModel from '@playbooks/types/database/models/playbook_run_attribute_value';
|
||||
import type{TransformerArgs} from '@typings/database/database';
|
||||
|
||||
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
|
||||
|
|
@ -146,13 +147,13 @@ export const transformPlaybookChecklistItemRecord = ({action, database, value}:
|
|||
};
|
||||
|
||||
/**
|
||||
* transformPlaybookRunAttributeRecord: Prepares a record of the SERVER database 'PlaybookRunAttribute' table for update or create actions.
|
||||
* transformPlaybookRunPropertyFieldRecord: Prepares a record of the SERVER database 'PlaybookRunAttribute' table for update or create actions.
|
||||
* @param {TransformerArgs} transformerArgs
|
||||
* @param {Database} transformerArgs.database
|
||||
* @param {RecordPair} transformerArgs.value
|
||||
* @returns {Promise<PlaybookRunAttributeModel>}
|
||||
*/
|
||||
export const transformPlaybookRunAttributeRecord = ({action, database, value}: TransformerArgs<PlaybookRunAttributeModel, PartialPlaybookRunAttribute>): Promise<PlaybookRunAttributeModel> => {
|
||||
export const transformPlaybookRunPropertyFieldRecord = ({action, database, value}: TransformerArgs<PlaybookRunPropertyFieldModel, PartialPlaybookRunPropertyField>): Promise<PlaybookRunPropertyFieldModel> => {
|
||||
const raw = value.raw;
|
||||
const record = value.record;
|
||||
const isCreateAction = action === OperationType.CREATE;
|
||||
|
|
@ -160,7 +161,7 @@ export const transformPlaybookRunAttributeRecord = ({action, database, value}: T
|
|||
return Promise.reject(new Error('Record not found for non create action'));
|
||||
}
|
||||
|
||||
const fieldsMapper = (attribute: PlaybookRunAttributeModel) => {
|
||||
const fieldsMapper = (attribute: PlaybookRunPropertyFieldModel) => {
|
||||
attribute._raw.id = isCreateAction ? (raw?.id ?? attribute.id) : record!.id;
|
||||
attribute.groupId = raw.group_id ?? record?.groupId ?? '';
|
||||
attribute.name = raw.name ?? record?.name ?? '';
|
||||
|
|
@ -170,7 +171,13 @@ export const transformPlaybookRunAttributeRecord = ({action, database, value}: T
|
|||
attribute.createAt = raw.create_at ?? record?.createAt ?? 0;
|
||||
attribute.updateAt = raw.update_at ?? record?.updateAt ?? 0;
|
||||
attribute.deleteAt = raw.delete_at ?? record?.deleteAt ?? 0;
|
||||
attribute.attrs = raw.attrs ?? record?.attrs ?? '';
|
||||
|
||||
// API sends attrs as object, but DB expects string - serialize it
|
||||
if (raw.attrs) {
|
||||
attribute.attrs = typeof raw.attrs === 'string' ? raw.attrs : JSON.stringify(raw.attrs);
|
||||
} else {
|
||||
attribute.attrs = record?.attrs ?? '';
|
||||
}
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
|
|
@ -183,13 +190,13 @@ export const transformPlaybookRunAttributeRecord = ({action, database, value}: T
|
|||
};
|
||||
|
||||
/**
|
||||
* transformPlaybookRunAttributeValueRecord: Prepares a record of the SERVER database 'PlaybookRunAttributeValue' table for update or create actions.
|
||||
* transformPlaybookRunPropertyValueRecord: Prepares a record of the SERVER database 'PlaybookRunAttributeValue' table for update or create actions.
|
||||
* @param {TransformerArgs} transformerArgs
|
||||
* @param {Database} transformerArgs.database
|
||||
* @param {RecordPair} transformerArgs.value
|
||||
* @returns {Promise<PlaybookRunAttributeValueModel>}
|
||||
*/
|
||||
export const transformPlaybookRunAttributeValueRecord = ({action, database, value}: TransformerArgs<PlaybookRunAttributeValueModel, PartialPlaybookRunAttributeValue>): Promise<PlaybookRunAttributeValueModel> => {
|
||||
export const transformPlaybookRunPropertyValueRecord = ({action, database, value}: TransformerArgs<PlaybookRunPropertyValueModel, PartialPlaybookRunPropertyValue>): Promise<PlaybookRunPropertyValueModel> => {
|
||||
const raw = value.raw;
|
||||
const record = value.record;
|
||||
const isCreateAction = action === OperationType.CREATE;
|
||||
|
|
@ -197,11 +204,27 @@ export const transformPlaybookRunAttributeValueRecord = ({action, database, valu
|
|||
return Promise.reject(new Error('Record not found for non create action'));
|
||||
}
|
||||
|
||||
const fieldsMapper = (attribute: PlaybookRunAttributeValueModel) => {
|
||||
const fieldsMapper = (attribute: PlaybookRunPropertyValueModel) => {
|
||||
attribute._raw.id = isCreateAction ? (raw?.id ?? attribute.id) : record!.id;
|
||||
attribute.attributeId = raw.attribute_id ?? record?.attributeId ?? '';
|
||||
attribute.runId = raw.run_id ?? record?.runId ?? '';
|
||||
attribute.value = raw.value ?? record?.value ?? '';
|
||||
attribute.attributeId = raw.field_id ?? record?.attributeId ?? ''; // API field_id → DB attributeId
|
||||
attribute.runId = raw.target_id ?? record?.runId ?? ''; // API target_id → DB runId
|
||||
|
||||
// Handle value: convert array to JSON string if needed (for multiselect fields)
|
||||
// Check if 'value' field is present in raw to distinguish between "not sent" and "sent as empty"
|
||||
const hasValueField = raw && 'value' in raw;
|
||||
let valueToStore = hasValueField ? (raw.value ?? '') : (record?.value ?? '');
|
||||
|
||||
if (Array.isArray(valueToStore)) {
|
||||
try {
|
||||
valueToStore = JSON.stringify(valueToStore);
|
||||
} catch (e) {
|
||||
// If JSON stringify fails, log the error and store empty string
|
||||
logError('Failed to stringify value array in transformPlaybookRunPropertyValueRecord:', valueToStore);
|
||||
valueToStore = '';
|
||||
}
|
||||
}
|
||||
attribute.value = valueToStore;
|
||||
attribute.updateAt = raw.update_at ?? record?.updateAt ?? 0;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
|
|
|
|||
672
app/products/playbooks/database/queries/property_fields.test.ts
Normal file
672
app/products/playbooks/database/queries/property_fields.test.ts
Normal file
|
|
@ -0,0 +1,672 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {
|
||||
observePlaybookRunPropertyFields,
|
||||
getPlaybookRunPropertyFieldById,
|
||||
observePlaybookRunPropertyValues,
|
||||
observePlaybookRunPropertyValue,
|
||||
observePlaybookRunPropertyFieldsWithValues,
|
||||
} from './property_fields';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
||||
describe('Property Fields Queries', () => {
|
||||
let operator: ServerDataOperator;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init(['propertyFields.test.com']);
|
||||
operator = DatabaseManager.serverDatabases['propertyFields.test.com']!.operator;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DatabaseManager.destroyServerDatabase('propertyFields.test.com');
|
||||
});
|
||||
|
||||
describe('observePlaybookRunPropertyFields', () => {
|
||||
it('should observe property fields for a run', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'run-id-1';
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: 'field-1',
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Priority',
|
||||
type: 'select',
|
||||
delete_at: 0,
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: 'field-2',
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Description',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyFields(operator.database, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const fields = subscriptionNext.mock.calls[0][0];
|
||||
expect(fields).toHaveLength(2);
|
||||
expect(fields[0].name).toBe('Priority');
|
||||
expect(fields[1].name).toBe('Description');
|
||||
});
|
||||
|
||||
it('should filter out deleted property fields', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'run-id-2';
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: 'field-1',
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Active Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: 'field-2',
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Deleted Field',
|
||||
type: 'text',
|
||||
delete_at: 1620000000000,
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyFields(operator.database, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const fields = subscriptionNext.mock.calls[0][0];
|
||||
expect(fields).toHaveLength(1);
|
||||
expect(fields[0].name).toBe('Active Field');
|
||||
});
|
||||
|
||||
it('should only return property fields for the specified run', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId1 = 'run-id-1';
|
||||
const runId2 = 'run-id-2';
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: 'field-1',
|
||||
target_id: runId1,
|
||||
target_type: 'run',
|
||||
name: 'Field for Run 1',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: 'field-2',
|
||||
target_id: runId2,
|
||||
target_type: 'run',
|
||||
name: 'Field for Run 2',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyFields(operator.database, runId1);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const fields = subscriptionNext.mock.calls[0][0];
|
||||
expect(fields).toHaveLength(1);
|
||||
expect(fields[0].name).toBe('Field for Run 1');
|
||||
});
|
||||
|
||||
it('should return empty array when no property fields exist', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'nonexistent-run';
|
||||
|
||||
const result = observePlaybookRunPropertyFields(operator.database, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should only return fields with target_type = run', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'run-id-1';
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: 'field-1',
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Run Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: 'field-2',
|
||||
target_id: runId,
|
||||
target_type: 'other',
|
||||
name: 'Other Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyFields(operator.database, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const fields = subscriptionNext.mock.calls[0][0];
|
||||
expect(fields).toHaveLength(1);
|
||||
expect(fields[0].name).toBe('Run Field');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlaybookRunPropertyFieldById', () => {
|
||||
it('should return the property field if found', async () => {
|
||||
const fieldId = 'field-1';
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: fieldId,
|
||||
target_id: 'run-id-1',
|
||||
target_type: 'run',
|
||||
name: 'Priority',
|
||||
type: 'select',
|
||||
delete_at: 0,
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = await getPlaybookRunPropertyFieldById(operator.database, fieldId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.id).toBe(fieldId);
|
||||
expect(result!.name).toBe('Priority');
|
||||
});
|
||||
|
||||
it('should return undefined if the property field is not found', async () => {
|
||||
const result = await getPlaybookRunPropertyFieldById(operator.database, 'nonexistent-field');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('observePlaybookRunPropertyValues', () => {
|
||||
it('should observe property values for a run', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'run-id-1';
|
||||
const fieldId1 = 'field-1';
|
||||
const fieldId2 = 'field-2';
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue(fieldId1, runId, {
|
||||
id: 'value-1',
|
||||
value: 'high',
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttributeValue(fieldId2, runId, {
|
||||
id: 'value-2',
|
||||
value: 'Some text description',
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyValues(operator.database, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const values = subscriptionNext.mock.calls[0][0];
|
||||
expect(values).toHaveLength(2);
|
||||
const valueTexts = values.map((v: any) => v.value).sort();
|
||||
expect(valueTexts).toContain('high');
|
||||
expect(valueTexts).toContain('Some text description');
|
||||
});
|
||||
|
||||
it('should only return values for the specified run', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId1 = 'run-id-1';
|
||||
const runId2 = 'run-id-2';
|
||||
const fieldId = 'field-1';
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue(fieldId, runId1, {
|
||||
id: 'value-1',
|
||||
value: 'Value for Run 1',
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttributeValue(fieldId, runId2, {
|
||||
id: 'value-2',
|
||||
value: 'Value for Run 2',
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyValues(operator.database, runId1);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const values = subscriptionNext.mock.calls[0][0];
|
||||
expect(values).toHaveLength(1);
|
||||
expect(values[0].value).toBe('Value for Run 1');
|
||||
});
|
||||
|
||||
it('should return empty array when no property values exist', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'nonexistent-run';
|
||||
|
||||
const result = observePlaybookRunPropertyValues(operator.database, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should emit when a property value changes', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'run-id-1';
|
||||
const fieldId = 'field-1';
|
||||
const valueId = 'value-1';
|
||||
|
||||
// Create initial value
|
||||
const initialValue = TestHelper.fakePlaybookRunAttributeValue(fieldId, runId, {
|
||||
id: valueId,
|
||||
value: 'initial value',
|
||||
});
|
||||
|
||||
await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues: [initialValue],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
// Subscribe to observable
|
||||
const result = observePlaybookRunPropertyValues(operator.database, runId);
|
||||
const subscription = result.subscribe({next: subscriptionNext});
|
||||
|
||||
// Wait for initial emission
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const initialValues = subscriptionNext.mock.calls[0][0];
|
||||
expect(initialValues).toHaveLength(1);
|
||||
expect(initialValues[0].value).toBe('initial value');
|
||||
|
||||
// Clear previous calls
|
||||
subscriptionNext.mockClear();
|
||||
|
||||
// Update the value
|
||||
const updatedValue = TestHelper.fakePlaybookRunAttributeValue(fieldId, runId, {
|
||||
id: valueId,
|
||||
value: 'updated value',
|
||||
update_at: Date.now(),
|
||||
});
|
||||
|
||||
await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues: [updatedValue],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
// Wait for the observer to emit the change
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Verify observer emitted with updated value
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const updatedValues = subscriptionNext.mock.calls[0][0];
|
||||
expect(updatedValues).toHaveLength(1);
|
||||
expect(updatedValues[0].value).toBe('updated value');
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
describe('observePlaybookRunPropertyValue', () => {
|
||||
it('should observe a specific property value by field ID and run ID', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'run-id-1';
|
||||
const fieldId = 'field-1';
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue(fieldId, runId, {
|
||||
id: 'value-1',
|
||||
value: 'medium',
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyValue(operator.database, fieldId, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const value = subscriptionNext.mock.calls[0][0];
|
||||
expect(value).toBeDefined();
|
||||
expect(value!.value).toBe('medium');
|
||||
expect(value!.attributeId).toBe(fieldId);
|
||||
expect(value!.runId).toBe(runId);
|
||||
});
|
||||
|
||||
it('should return undefined if property value is not found', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const result = observePlaybookRunPropertyValue(operator.database, 'nonexistent-field', 'nonexistent-run');
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined if field ID matches but run ID does not', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId1 = 'run-id-1';
|
||||
const runId2 = 'run-id-2';
|
||||
const fieldId = 'field-1';
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue(fieldId, runId1, {
|
||||
id: 'value-1',
|
||||
value: 'Value for Run 1',
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyValue(operator.database, fieldId, runId2);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('observePlaybookRunPropertyFieldsWithValues', () => {
|
||||
it('should join property fields with their values', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'run-id-1';
|
||||
const fieldId1 = 'field-1';
|
||||
const fieldId2 = 'field-2';
|
||||
const fieldId3 = 'field-3';
|
||||
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: fieldId1,
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Priority',
|
||||
type: 'select',
|
||||
delete_at: 0,
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: fieldId2,
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Description',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: fieldId3,
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Empty Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
];
|
||||
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue(fieldId1, runId, {
|
||||
id: 'value-1',
|
||||
value: 'high',
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttributeValue(fieldId2, runId, {
|
||||
id: 'value-2',
|
||||
value: 'Some description text',
|
||||
}),
|
||||
|
||||
// fieldId3 has no value
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyFieldsWithValues(operator.database, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const fieldsWithValues = subscriptionNext.mock.calls[0][0];
|
||||
expect(fieldsWithValues).toHaveLength(3);
|
||||
|
||||
// Check field 1 with value
|
||||
expect(fieldsWithValues[0].propertyField.id).toBe(fieldId1);
|
||||
expect(fieldsWithValues[0].propertyField.name).toBe('Priority');
|
||||
expect(fieldsWithValues[0].value).toBeDefined();
|
||||
expect(fieldsWithValues[0].value!.value).toBe('high');
|
||||
|
||||
// Check field 2 with value
|
||||
expect(fieldsWithValues[1].propertyField.id).toBe(fieldId2);
|
||||
expect(fieldsWithValues[1].propertyField.name).toBe('Description');
|
||||
expect(fieldsWithValues[1].value).toBeDefined();
|
||||
expect(fieldsWithValues[1].value!.value).toBe('Some description text');
|
||||
|
||||
// Check field 3 without value
|
||||
expect(fieldsWithValues[2].propertyField.id).toBe(fieldId3);
|
||||
expect(fieldsWithValues[2].propertyField.name).toBe('Empty Field');
|
||||
expect(fieldsWithValues[2].value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return empty array when no property fields exist', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'nonexistent-run';
|
||||
|
||||
const result = observePlaybookRunPropertyFieldsWithValues(operator.database, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should handle property fields without any values', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'run-id-1';
|
||||
const fieldId1 = 'field-1';
|
||||
const fieldId2 = 'field-2';
|
||||
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: fieldId1,
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Field 1',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: fieldId2,
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Field 2',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyFieldsWithValues(operator.database, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const fieldsWithValues = subscriptionNext.mock.calls[0][0];
|
||||
expect(fieldsWithValues).toHaveLength(2);
|
||||
expect(fieldsWithValues[0].value).toBeUndefined();
|
||||
expect(fieldsWithValues[1].value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should filter out deleted property fields even with values', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'run-id-1';
|
||||
const fieldId1 = 'field-1';
|
||||
const fieldId2 = 'field-2';
|
||||
|
||||
const propertyFields = [
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: fieldId1,
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Active Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttribute({
|
||||
id: fieldId2,
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Deleted Field',
|
||||
type: 'text',
|
||||
delete_at: 1620000000000,
|
||||
}),
|
||||
];
|
||||
|
||||
const propertyValues = [
|
||||
TestHelper.fakePlaybookRunAttributeValue(fieldId1, runId, {
|
||||
id: 'value-1',
|
||||
value: 'Value 1',
|
||||
}),
|
||||
TestHelper.fakePlaybookRunAttributeValue(fieldId2, runId, {
|
||||
id: 'value-2',
|
||||
value: 'Value 2',
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = observePlaybookRunPropertyFieldsWithValues(operator.database, runId);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const fieldsWithValues = subscriptionNext.mock.calls[0][0];
|
||||
expect(fieldsWithValues).toHaveLength(1);
|
||||
expect(fieldsWithValues[0].propertyField.name).toBe('Active Field');
|
||||
expect(fieldsWithValues[0].value!.value).toBe('Value 1');
|
||||
});
|
||||
|
||||
it('should emit when a property value changes', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const runId = 'run-id-1';
|
||||
const fieldId = 'field-1';
|
||||
const valueId = 'value-1';
|
||||
|
||||
// Create property field
|
||||
const propertyField = TestHelper.fakePlaybookRunAttribute({
|
||||
id: fieldId,
|
||||
target_id: runId,
|
||||
target_type: 'run',
|
||||
name: 'Test Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
});
|
||||
|
||||
// Create initial value
|
||||
const initialValue = TestHelper.fakePlaybookRunAttributeValue(fieldId, runId, {
|
||||
id: valueId,
|
||||
value: 'initial value',
|
||||
});
|
||||
|
||||
await operator.handlePlaybookRunPropertyField({
|
||||
propertyFields: [propertyField],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues: [initialValue],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
// Subscribe to observable
|
||||
const result = observePlaybookRunPropertyFieldsWithValues(operator.database, runId);
|
||||
const subscription = result.subscribe({next: subscriptionNext});
|
||||
|
||||
// Wait for initial emission
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const initialFieldsWithValues = subscriptionNext.mock.calls[0][0];
|
||||
expect(initialFieldsWithValues).toHaveLength(1);
|
||||
expect(initialFieldsWithValues[0].propertyField.id).toBe(fieldId);
|
||||
expect(initialFieldsWithValues[0].value).toBeDefined();
|
||||
expect(initialFieldsWithValues[0].value!.value).toBe('initial value');
|
||||
|
||||
// Clear previous calls
|
||||
subscriptionNext.mockClear();
|
||||
|
||||
// Update the value
|
||||
const updatedValue = TestHelper.fakePlaybookRunAttributeValue(fieldId, runId, {
|
||||
id: valueId,
|
||||
value: 'updated value',
|
||||
update_at: Date.now(),
|
||||
});
|
||||
|
||||
await operator.handlePlaybookRunPropertyValue({
|
||||
propertyValues: [updatedValue],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
// Wait for the observer to emit the change
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Verify observer emitted with updated value
|
||||
expect(subscriptionNext).toHaveBeenCalled();
|
||||
const updatedFieldsWithValues = subscriptionNext.mock.calls[0][0];
|
||||
expect(updatedFieldsWithValues).toHaveLength(1);
|
||||
expect(updatedFieldsWithValues[0].propertyField.id).toBe(fieldId);
|
||||
expect(updatedFieldsWithValues[0].value).toBeDefined();
|
||||
expect(updatedFieldsWithValues[0].value!.value).toBe('updated value');
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
||||
});
|
||||
105
app/products/playbooks/database/queries/property_fields.ts
Normal file
105
app/products/playbooks/database/queries/property_fields.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q, type Database} from '@nozbe/watermelondb';
|
||||
import {of as of$, combineLatest, type Observable} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap, map} from 'rxjs/operators';
|
||||
|
||||
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
||||
|
||||
import type PlaybookRunPropertyFieldModel from '@playbooks/types/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunPropertyValueModel from '@playbooks/types/database/models/playbook_run_attribute_value';
|
||||
|
||||
const {PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
|
||||
|
||||
/**
|
||||
* Get all property fields for a run (by target_id = runId and target_type = 'run')
|
||||
* @param database - The database instance
|
||||
* @param runId - The run ID
|
||||
* @returns Observable of property field models
|
||||
*/
|
||||
export const observePlaybookRunPropertyFields = (database: Database, runId: string): Observable<PlaybookRunPropertyFieldModel[]> => {
|
||||
return database.get<PlaybookRunPropertyFieldModel>(PLAYBOOK_RUN_ATTRIBUTE).query(
|
||||
Q.where('target_id', runId),
|
||||
Q.where('target_type', 'run'),
|
||||
Q.where('delete_at', 0),
|
||||
).observeWithColumns(['update_at', 'delete_at']).pipe(
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a specific property field by ID
|
||||
* @param database - The database instance
|
||||
* @param fieldId - The property field ID
|
||||
* @returns The property field model or undefined
|
||||
*/
|
||||
export const getPlaybookRunPropertyFieldById = async (database: Database, fieldId: string): Promise<PlaybookRunPropertyFieldModel | undefined> => {
|
||||
try {
|
||||
const fieldRecord = await database.get<PlaybookRunPropertyFieldModel>(PLAYBOOK_RUN_ATTRIBUTE).find(fieldId);
|
||||
return fieldRecord;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all property values for a run (by run_id)
|
||||
* @param database - The database instance
|
||||
* @param runId - The run ID
|
||||
* @returns Observable of property value models
|
||||
*/
|
||||
export const observePlaybookRunPropertyValues = (database: Database, runId: string): Observable<PlaybookRunPropertyValueModel[]> => {
|
||||
return database.get<PlaybookRunPropertyValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).query(
|
||||
Q.where('run_id', runId),
|
||||
).observeWithColumns(['value', 'update_at']).pipe(
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get property value by field ID (attributeId in DB) and run_id
|
||||
* @param database - The database instance
|
||||
* @param fieldId - The property field ID (maps to attribute_id in DB)
|
||||
* @param runId - The run ID
|
||||
* @returns Observable of the property value model or undefined
|
||||
*/
|
||||
export const observePlaybookRunPropertyValue = (database: Database, fieldId: string, runId: string): Observable<PlaybookRunPropertyValueModel | undefined> => {
|
||||
return database.get<PlaybookRunPropertyValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).query(
|
||||
Q.where('attribute_id', fieldId),
|
||||
Q.where('run_id', runId),
|
||||
Q.take(1),
|
||||
).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$(undefined))),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get property fields with their values joined
|
||||
* @param database - The database instance
|
||||
* @param runId - The run ID
|
||||
* @returns Observable of array of objects containing propertyField and optional value
|
||||
*/
|
||||
export const observePlaybookRunPropertyFieldsWithValues = (
|
||||
database: Database,
|
||||
runId: string,
|
||||
): Observable<Array<{propertyField: PlaybookRunPropertyFieldModel; value?: PlaybookRunPropertyValueModel}>> => {
|
||||
const propertyFields$ = observePlaybookRunPropertyFields(database, runId);
|
||||
const propertyValues$ = observePlaybookRunPropertyValues(database, runId);
|
||||
|
||||
return combineLatest([propertyFields$, propertyValues$]).pipe(
|
||||
map(([propertyFields, propertyValues]) => {
|
||||
// Create a map of values by attribute_id for quick lookup
|
||||
const valuesMap = new Map<string, PlaybookRunPropertyValueModel>();
|
||||
propertyValues.forEach((value) => {
|
||||
valuesMap.set(value.attributeId, value);
|
||||
});
|
||||
|
||||
// Join property fields with their values
|
||||
return propertyFields.map((propertyField) => ({
|
||||
propertyField,
|
||||
value: valuesMap.get(propertyField.id),
|
||||
}));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -21,5 +21,6 @@ export default tableSchema({
|
|||
{name: 'attribute_id', type: 'string', isIndexed: true},
|
||||
{name: 'run_id', type: 'string', isIndexed: true},
|
||||
{name: 'value', type: 'string'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {default as PropertyField} from './property_field';
|
||||
export {default as PropertyFieldsList} from './property_fields_list';
|
||||
export {default as PropertySelectField} from './property_select_field';
|
||||
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {TouchableOpacity, View} from 'react-native';
|
||||
import {Text} from 'react-native-gesture-handler';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {bottomSheet, dismissBottomSheet} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import PropertyFieldEditor from './property_field_editor';
|
||||
|
||||
import type PlaybookRunPropertyFieldModel from '@playbooks/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunPropertyValueModel from '@playbooks/database/models/playbook_run_attribute_value';
|
||||
|
||||
type PropertyFieldProps = {
|
||||
propertyField: PlaybookRunPropertyFieldModel;
|
||||
value?: PlaybookRunPropertyValueModel;
|
||||
onValueChange: (fieldId: string, newValue: string) => void;
|
||||
isDisabled?: boolean;
|
||||
testID: string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
viewContainer: {
|
||||
marginVertical: 8,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
labelContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
value: {
|
||||
fontSize: 14,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const PropertyField = ({
|
||||
propertyField,
|
||||
value,
|
||||
onValueChange,
|
||||
isDisabled = false,
|
||||
testID,
|
||||
}: PropertyFieldProps) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const subContainer = [style.viewContainer, {paddingHorizontal: isTablet ? 42 : 20}];
|
||||
const currentValue = value?.value || '';
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleSave = (newValue: string) => {
|
||||
onValueChange(propertyField.id, newValue);
|
||||
dismissBottomSheet();
|
||||
};
|
||||
|
||||
const renderContent = () => (
|
||||
<PropertyFieldEditor
|
||||
initialValue={currentValue}
|
||||
label={propertyField.name}
|
||||
onSave={handleSave}
|
||||
theme={theme}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
|
||||
bottomSheet({
|
||||
title: propertyField.name,
|
||||
renderContent,
|
||||
snapPoints: ['50%', '50%'],
|
||||
theme,
|
||||
closeButtonId: 'close-property-field',
|
||||
});
|
||||
}, [isDisabled, currentValue, onValueChange, propertyField.id, propertyField.name, testID, theme]);
|
||||
|
||||
return (
|
||||
<View
|
||||
testID={testID}
|
||||
style={subContainer}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
disabled={isDisabled}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={style.labelContainer}>
|
||||
<Text style={style.label}>{`${propertyField.name}: `}</Text>
|
||||
<Text style={style.value}>{currentValue || intl.formatMessage({id: 'playbooks.property_field.tap_to_edit', defaultMessage: 'Tap to edit'})}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PropertyField);
|
||||
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import Button from '@components/button';
|
||||
import FloatingTextInput, {type FloatingTextInputRef} from '@components/floating_input/floating_text_input_label';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme(() => {
|
||||
return {
|
||||
modalContent: {
|
||||
padding: 20,
|
||||
},
|
||||
buttonContainer: {
|
||||
marginTop: 20,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type PropertyFieldEditorProps = {
|
||||
initialValue: string;
|
||||
label: string;
|
||||
onSave: (value: string) => void;
|
||||
theme: Theme;
|
||||
testID: string;
|
||||
};
|
||||
|
||||
const PropertyFieldEditor: React.FC<PropertyFieldEditorProps> = ({
|
||||
initialValue,
|
||||
label,
|
||||
onSave,
|
||||
theme,
|
||||
testID,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [tempValue, setTempValue] = useState(initialValue);
|
||||
const inputRef = useRef<FloatingTextInputRef>(null);
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSave(tempValue);
|
||||
}, [tempValue, onSave]);
|
||||
|
||||
return (
|
||||
<View style={styles.modalContent}>
|
||||
<FloatingTextInput
|
||||
ref={inputRef}
|
||||
rawInput={true}
|
||||
disableFullscreenUI={true}
|
||||
editable={true}
|
||||
keyboardType='default'
|
||||
label={label}
|
||||
onChangeText={setTempValue}
|
||||
testID={`${testID}.input.modal`}
|
||||
theme={theme}
|
||||
value={tempValue}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
onPress={handleSave}
|
||||
text={intl.formatMessage({id: 'mobile.managed_app.save', defaultMessage: 'Update'})}
|
||||
theme={theme}
|
||||
size='lg'
|
||||
emphasis='primary'
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PropertyFieldEditor;
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {ActivityIndicator, Text, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {fetchPlaybookRunPropertyFields, updatePlaybookRunPropertyValue} from '@playbooks/actions/remote/property_fields';
|
||||
import {observePlaybookRunPropertyFieldsWithValues} from '@playbooks/database/queries/property_fields';
|
||||
import {sortPropertyFieldsByOrder} from '@playbooks/utils/property_fields';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logError} from '@utils/log';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import {PropertyField, PropertySelectField} from './index';
|
||||
|
||||
import type PlaybookRunPropertyFieldModel from '@playbooks/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunPropertyValueModel from '@playbooks/database/models/playbook_run_attribute_value';
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const messages = defineMessages({
|
||||
propertyFields: {
|
||||
id: 'playbooks.playbook_run.property_fields',
|
||||
defaultMessage: 'Property Fields',
|
||||
},
|
||||
});
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
container: {
|
||||
gap: 12,
|
||||
marginTop: 16,
|
||||
},
|
||||
headerContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
header: {
|
||||
...typography('Heading', 200, 'SemiBold'),
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
loadingContainer: {
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
emptyContainer: {
|
||||
paddingVertical: 16,
|
||||
},
|
||||
emptyText: {
|
||||
...typography('Body', 100, 'Regular'),
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
},
|
||||
}));
|
||||
|
||||
type PropertyFieldsListProps = WithDatabaseArgs & {
|
||||
serverUrl: string;
|
||||
runId: string;
|
||||
isReadOnly: boolean;
|
||||
};
|
||||
|
||||
type PropertyFieldsWithValues = {
|
||||
propertyField: PlaybookRunPropertyFieldModel;
|
||||
value?: PlaybookRunPropertyValueModel;
|
||||
};
|
||||
|
||||
const PropertyFieldsListComponent = ({
|
||||
serverUrl,
|
||||
runId,
|
||||
isReadOnly,
|
||||
propertyFieldsWithValues = [],
|
||||
}: PropertyFieldsListProps & {propertyFieldsWithValues?: PropertyFieldsWithValues[]}) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const styles = getStyleSheet(theme);
|
||||
const [updatingFieldId, setUpdatingFieldId] = useState<string | null>(null);
|
||||
|
||||
// Fetch property fields when component mounts to ensure they're loaded
|
||||
useEffect(() => {
|
||||
fetchPlaybookRunPropertyFields(serverUrl, runId, false).catch((error) => {
|
||||
logError('failed to fetch playbook run property fields', getFullErrorMessage(error) ?? 'unknown error');
|
||||
});
|
||||
}, [serverUrl, runId]);
|
||||
|
||||
// Sort property fields by sort_order
|
||||
const sortedPropertyFields = useMemo(() => {
|
||||
if (!propertyFieldsWithValues || propertyFieldsWithValues.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const fields = propertyFieldsWithValues.map((item) => item.propertyField);
|
||||
return sortPropertyFieldsByOrder(fields);
|
||||
}, [propertyFieldsWithValues]);
|
||||
|
||||
// Create a map of values by field ID for quick lookup
|
||||
const valuesMap = useMemo(() => {
|
||||
const map = new Map<string, PlaybookRunPropertyValueModel>();
|
||||
if (propertyFieldsWithValues) {
|
||||
propertyFieldsWithValues.forEach((item) => {
|
||||
if (item.value) {
|
||||
map.set(item.propertyField.id, item.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [propertyFieldsWithValues]);
|
||||
|
||||
const handleValueChange = useCallback(async (fieldId: string, newValue: string, fieldType?: string) => {
|
||||
if (isReadOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdatingFieldId(fieldId);
|
||||
const result = await updatePlaybookRunPropertyValue(serverUrl, runId, fieldId, newValue, fieldType);
|
||||
setUpdatingFieldId(null);
|
||||
|
||||
if (result.error) {
|
||||
showPlaybookErrorSnackbar();
|
||||
}
|
||||
}, [isReadOnly, serverUrl, runId]);
|
||||
|
||||
// Render property fields
|
||||
const renderPropertyFields = () => {
|
||||
if (sortedPropertyFields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sortedPropertyFields.map((propertyField: PlaybookRunPropertyFieldModel) => {
|
||||
const value = valuesMap.get(propertyField.id);
|
||||
const isUpdating = updatingFieldId === propertyField.id;
|
||||
const isDisabled = isReadOnly || isUpdating;
|
||||
const testID = `playbook_run.property_field.${propertyField.id}`;
|
||||
|
||||
if (propertyField.type === 'text') {
|
||||
return (
|
||||
<PropertyField
|
||||
key={propertyField.id}
|
||||
propertyField={propertyField}
|
||||
value={value}
|
||||
onValueChange={handleValueChange}
|
||||
isDisabled={isDisabled}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (propertyField.type === 'select' || propertyField.type === 'multiselect') {
|
||||
// Include value's updateAt in key to force re-render when value changes
|
||||
// Use propertyField.id as fallback to ensure uniqueness when there's no value
|
||||
const valueKey = value?.updateAt ?? propertyField.id;
|
||||
const containerKey = `${propertyField.id}-container-${valueKey}`;
|
||||
const fieldKey = `${propertyField.id}-field-${valueKey}`;
|
||||
return (
|
||||
<View key={containerKey}>
|
||||
<PropertySelectField
|
||||
key={fieldKey}
|
||||
propertyField={propertyField}
|
||||
value={value}
|
||||
onValueChange={handleValueChange}
|
||||
isDisabled={isDisabled}
|
||||
isMultiselect={propertyField.type === 'multiselect'}
|
||||
testID={testID}
|
||||
/>
|
||||
{isUpdating && (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator
|
||||
size='small'
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.container}
|
||||
testID='playbook_run.property_fields_list'
|
||||
>
|
||||
<View style={styles.headerContainer}>
|
||||
<Text style={styles.header}>
|
||||
{intl.formatMessage(messages.propertyFields)}
|
||||
</Text>
|
||||
</View>
|
||||
{renderPropertyFields()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withObservables(['runId'], ({database, runId}: PropertyFieldsListProps) => {
|
||||
return {
|
||||
propertyFieldsWithValues: observePlaybookRunPropertyFieldsWithValues(database, runId),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(PropertyFieldsListComponent));
|
||||
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {TouchableOpacity, View} from 'react-native';
|
||||
import {Text} from 'react-native-gesture-handler';
|
||||
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {formatPropertyFieldOptionsForSelector, getPropertyValueDisplay} from '@playbooks/utils/property_fields';
|
||||
import {goToScreen} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {getSelectedOptionIds} from '@utils/user';
|
||||
|
||||
import type PlaybookRunPropertyFieldModel from '@playbooks/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunPropertyValueModel from '@playbooks/database/models/playbook_run_attribute_value';
|
||||
|
||||
type PropertySelectFieldProps = {
|
||||
propertyField: PlaybookRunPropertyFieldModel;
|
||||
value?: PlaybookRunPropertyValueModel;
|
||||
onValueChange: (fieldId: string, newValue: string, fieldType?: string) => void;
|
||||
isDisabled?: boolean;
|
||||
isMultiselect: boolean;
|
||||
testID: string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
viewContainer: {
|
||||
marginVertical: 8,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
touchableContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
labelContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
value: {
|
||||
fontSize: 14,
|
||||
color: theme.centerChannelColor,
|
||||
flexShrink: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const PropertySelectField = ({
|
||||
propertyField,
|
||||
value,
|
||||
onValueChange,
|
||||
isDisabled = false,
|
||||
isMultiselect = false,
|
||||
testID,
|
||||
}: PropertySelectFieldProps) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
// Convert stored value to selected format for selector screen
|
||||
const selectedValue = useMemo(() => {
|
||||
if (!value?.value) {
|
||||
return isMultiselect ? [] : undefined;
|
||||
}
|
||||
|
||||
const selectedIds = getSelectedOptionIds(value.value, isMultiselect ? 'multiselect' : 'select');
|
||||
|
||||
if (isMultiselect) {
|
||||
return selectedIds;
|
||||
}
|
||||
|
||||
return selectedIds[0] || undefined;
|
||||
}, [value?.value, isMultiselect]);
|
||||
|
||||
// Get options from property field attrs
|
||||
const options = useMemo(() => {
|
||||
return formatPropertyFieldOptionsForSelector(propertyField);
|
||||
}, [propertyField]);
|
||||
|
||||
// Get display text
|
||||
const displayText = useMemo(() => {
|
||||
const text = getPropertyValueDisplay(propertyField, value);
|
||||
if (!text) {
|
||||
return intl.formatMessage({id: 'playbooks.property_field.tap_to_select', defaultMessage: 'Tap to select'});
|
||||
}
|
||||
return text;
|
||||
}, [propertyField, value, intl]);
|
||||
|
||||
const handleSelect = useCallback((newSelection?: SelectedDialogOption) => {
|
||||
if (!newSelection) {
|
||||
onValueChange(propertyField.id, '', propertyField.type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(newSelection)) {
|
||||
// Multiselect: send array of IDs as comma-separated string
|
||||
// The client will convert to array based on field type
|
||||
const selectedIds = newSelection.map((option) => option.value);
|
||||
const commaSeparated = selectedIds.join(',');
|
||||
onValueChange(propertyField.id, commaSeparated, propertyField.type);
|
||||
} else {
|
||||
// Single select: store the option ID directly as string
|
||||
onValueChange(propertyField.id, newSelection.value, propertyField.type);
|
||||
}
|
||||
}, [propertyField.id, propertyField.type, onValueChange]);
|
||||
|
||||
// Open selector screen when tapped
|
||||
const handlePress = useCallback(() => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
goToScreen(Screens.INTEGRATION_SELECTOR, propertyField.name, {
|
||||
dataSource: '',
|
||||
options,
|
||||
handleSelect,
|
||||
selected: selectedValue,
|
||||
isMultiselect,
|
||||
});
|
||||
}, [isDisabled, propertyField.name, options, handleSelect, selectedValue, isMultiselect]);
|
||||
|
||||
const subContainer = [styles.viewContainer, {paddingHorizontal: isTablet ? 42 : 20}];
|
||||
|
||||
// Always render as text that opens selector when tapped
|
||||
return (
|
||||
<View
|
||||
testID={testID}
|
||||
style={subContainer}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
disabled={isDisabled}
|
||||
activeOpacity={0.8}
|
||||
style={styles.touchableContainer}
|
||||
>
|
||||
<View style={styles.labelContainer}>
|
||||
<Text style={styles.label}>{`${propertyField.name}: `}</Text>
|
||||
<Text style={styles.value}>{displayText}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PropertySelectField);
|
||||
|
||||
|
|
@ -26,6 +26,7 @@ import {typography} from '@utils/typography';
|
|||
import {goToRenamePlaybookRun, goToSelectUser} from '../navigation';
|
||||
|
||||
import ChecklistList from './checklist_list';
|
||||
import {PropertyFieldsList} from './components';
|
||||
import ErrorState from './error_state';
|
||||
import OutOfDateHeader from './out_of_date_header';
|
||||
import StatusUpdateIndicator from './status_update_indicator';
|
||||
|
|
@ -393,6 +394,11 @@ export default function PlaybookRun({
|
|||
/>
|
||||
)}
|
||||
</View>
|
||||
<PropertyFieldsList
|
||||
serverUrl={serverUrl}
|
||||
runId={playbookRun.id}
|
||||
isReadOnly={readOnly}
|
||||
/>
|
||||
<View style={styles.tasksContainer}>
|
||||
<View style={styles.tasksHeaderContainer}>
|
||||
<Text style={styles.tasksHeader}>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ describe('StatusUpdateIndicator', () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
const text = getByText(/Update due/);
|
||||
const text = getByText('Update due', {exact: false});
|
||||
expect(text).toHaveStyle({color: changeOpacity(Preferences.THEMES.denim.centerChannelColor, 0.72)});
|
||||
|
||||
const icon = getByTestId('compass-icon');
|
||||
|
|
@ -61,7 +61,7 @@ describe('StatusUpdateIndicator', () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
const text = getByText(/Update overdue/);
|
||||
const text = getByText('Update overdue', {exact: false});
|
||||
expect(text).toHaveStyle({color: Preferences.THEMES.denim.dndIndicator});
|
||||
|
||||
const icon = getByTestId('compass-icon');
|
||||
|
|
|
|||
18
app/products/playbooks/types/api.d.ts
vendored
18
app/products/playbooks/types/api.d.ts
vendored
|
|
@ -72,7 +72,7 @@ type TimelineEvent = {
|
|||
|
||||
type PlaybookRunStatusType = typeof PlaybookRunStatus[keyof typeof PlaybookRunStatus];
|
||||
|
||||
type PlaybookRunAttribute = {
|
||||
type PlaybookRunPropertyField = {
|
||||
id: string;
|
||||
group_id: string;
|
||||
name: string;
|
||||
|
|
@ -82,13 +82,19 @@ type PlaybookRunAttribute = {
|
|||
create_at: number;
|
||||
update_at: number;
|
||||
delete_at: number;
|
||||
attrs?: string;
|
||||
attrs?: string | {
|
||||
options?: Array<{id: string; name: string}>;
|
||||
parent_id?: string;
|
||||
sort_order?: number;
|
||||
value_type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type PlaybookRunAttributeValue = {
|
||||
type PlaybookRunPropertyValue = {
|
||||
id: string;
|
||||
attribute_id: string;
|
||||
run_id: string;
|
||||
field_id: string;
|
||||
target_id: string;
|
||||
update_at: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
|
@ -137,6 +143,8 @@ type PlaybookRun = {
|
|||
status_posts: StatusPost[];
|
||||
checklists: PlaybookChecklist[];
|
||||
metrics_data: RunMetricData[];
|
||||
property_fields?: PlaybookRunPropertyField[];
|
||||
property_values?: PlaybookRunPropertyValue[];
|
||||
update_at: number;
|
||||
items_order: string[];
|
||||
status_update_broadcast_channels_enabled: boolean;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ interface PlaybookRunAttributeValueModelInterface extends Model {
|
|||
/** value : The value of the attribute */
|
||||
value: string;
|
||||
|
||||
/** updateAt : The timestamp when this attribute value was last updated */
|
||||
updateAt: number;
|
||||
|
||||
/** attribute : The attribute this attribute value belongs to */
|
||||
attribute: Relation<PlaybookRunAttributeModel>;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,5 +18,7 @@ type PartialPlaybookRun = PartialWithId<PlaybookRun>;
|
|||
type PartialChecklist = PartialWithId<PlaybookChecklist> & WithRunId;
|
||||
type PartialChecklistItem = PartialWithId<PlaybookChecklistItem> & WithChecklistId;
|
||||
|
||||
type PartialPlaybookRunAttribute = PartialWithId<PlaybookRunAttribute>;
|
||||
type PartialPlaybookRunAttributeValue = PartialWithId<PlaybookRunAttributeValue> & WithRunId & WithAttributeId;
|
||||
// Types matching API naming (property_field/property_value)
|
||||
type PartialPlaybookRunPropertyField = PartialWithId<PlaybookRunPropertyField>;
|
||||
|
||||
type PartialPlaybookRunPropertyValue = PartialWithId<PlaybookRunPropertyValue>;
|
||||
|
|
|
|||
139
app/products/playbooks/utils/property_fields.test.ts
Normal file
139
app/products/playbooks/utils/property_fields.test.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
parsePropertyFieldAttrs,
|
||||
getPropertyFieldSortOrder,
|
||||
sortPropertyFieldsByOrder,
|
||||
formatPropertyFieldOptionsForSelector,
|
||||
getOptionNameById,
|
||||
getPropertyValueDisplay,
|
||||
} from './property_fields';
|
||||
|
||||
import type PlaybookRunPropertyFieldModel from '@playbooks/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunPropertyValueModel from '@playbooks/database/models/playbook_run_attribute_value';
|
||||
|
||||
describe('property_fields utils', () => {
|
||||
const makeField = (overrides: Partial<PlaybookRunPropertyFieldModel> = {}): PlaybookRunPropertyFieldModel => ({
|
||||
id: 'field-id',
|
||||
groupId: 'group-id',
|
||||
name: 'Field Name',
|
||||
type: 'text',
|
||||
targetId: 'run-id',
|
||||
targetType: 'run',
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
deleteAt: 0,
|
||||
attrs: undefined,
|
||||
...overrides,
|
||||
} as PlaybookRunPropertyFieldModel);
|
||||
|
||||
const makeValue = (overrides: Partial<PlaybookRunPropertyValueModel> = {}): PlaybookRunPropertyValueModel => ({
|
||||
id: 'value-id',
|
||||
attributeId: 'field-id',
|
||||
runId: 'run-id',
|
||||
value: '',
|
||||
updateAt: 0,
|
||||
...overrides,
|
||||
} as PlaybookRunPropertyValueModel);
|
||||
|
||||
test('parsePropertyFieldAttrs parses valid JSON', () => {
|
||||
const json = JSON.stringify({
|
||||
options: [{id: '1', name: 'One'}],
|
||||
parent_id: 'p',
|
||||
sort_order: 2,
|
||||
value_type: 'text',
|
||||
visibility: 'when_set',
|
||||
});
|
||||
const parsed = parsePropertyFieldAttrs(json);
|
||||
expect(parsed).toEqual({
|
||||
options: [{id: '1', name: 'One'}],
|
||||
parent_id: 'p',
|
||||
sort_order: 2,
|
||||
value_type: 'text',
|
||||
});
|
||||
});
|
||||
|
||||
test('parsePropertyFieldAttrs handles invalid/missing JSON', () => {
|
||||
expect(parsePropertyFieldAttrs(undefined)).toBeNull();
|
||||
expect(parsePropertyFieldAttrs('not-json')).toBeNull();
|
||||
expect(parsePropertyFieldAttrs(JSON.stringify('string'))).toBeNull();
|
||||
});
|
||||
|
||||
test('getPropertyFieldSortOrder defaults to 999', () => {
|
||||
const field = makeField();
|
||||
expect(getPropertyFieldSortOrder(field)).toBe(999);
|
||||
});
|
||||
|
||||
test('getPropertyFieldSortOrder reads sort_order from attrs', () => {
|
||||
const field = makeField({attrs: JSON.stringify({sort_order: 5})});
|
||||
expect(getPropertyFieldSortOrder(field)).toBe(5);
|
||||
});
|
||||
|
||||
test('sortPropertyFieldsByOrder sorts by sort_order with default last', () => {
|
||||
const a = makeField({attrs: JSON.stringify({sort_order: 10}), name: 'a'} as any);
|
||||
const b = makeField({attrs: JSON.stringify({sort_order: 1}), name: 'b'} as any);
|
||||
const c = makeField({name: 'c'} as any);
|
||||
const sorted = sortPropertyFieldsByOrder([a, b, c]);
|
||||
expect(sorted).toEqual([b, a, c]);
|
||||
});
|
||||
|
||||
test('formatPropertyFieldOptionsForSelector returns formatted options for select', () => {
|
||||
const field = makeField({
|
||||
type: 'select',
|
||||
attrs: JSON.stringify({options: [{id: 'id1', name: 'Name 1'}]}),
|
||||
});
|
||||
expect(formatPropertyFieldOptionsForSelector(field)).toEqual([
|
||||
{text: 'Name 1', value: 'id1'},
|
||||
]);
|
||||
});
|
||||
|
||||
test('formatPropertyFieldOptionsForSelector returns [] for non-select or missing options', () => {
|
||||
const textField = makeField({type: 'text'});
|
||||
const selectNoOptions = makeField({type: 'select', attrs: JSON.stringify({})});
|
||||
expect(formatPropertyFieldOptionsForSelector(textField)).toEqual([]);
|
||||
expect(formatPropertyFieldOptionsForSelector(selectNoOptions)).toEqual([]);
|
||||
});
|
||||
|
||||
test('getOptionNameById returns name or falls back to id', () => {
|
||||
const options = [{id: 'x', name: 'Ex'}];
|
||||
expect(getOptionNameById(options, 'x')).toBe('Ex');
|
||||
expect(getOptionNameById(options, 'y')).toBe('y');
|
||||
});
|
||||
|
||||
test('getPropertyValueDisplay returns text for text fields', () => {
|
||||
const field = makeField({type: 'text'});
|
||||
const value = makeValue({value: 'hello'});
|
||||
expect(getPropertyValueDisplay(field, value)).toBe('hello');
|
||||
});
|
||||
|
||||
test('getPropertyValueDisplay maps select id to name and falls back to id', () => {
|
||||
const field = makeField({
|
||||
type: 'select',
|
||||
attrs: JSON.stringify({options: [{id: 'id1', name: 'Name 1'}]}),
|
||||
});
|
||||
expect(getPropertyValueDisplay(field, makeValue({value: 'id1'}))).toBe('Name 1');
|
||||
expect(getPropertyValueDisplay(field, makeValue({value: 'unknown'}))).toBe('unknown');
|
||||
});
|
||||
|
||||
test('getPropertyValueDisplay formats multiselect as comma-separated names', () => {
|
||||
const field = makeField({
|
||||
type: 'multiselect',
|
||||
attrs: JSON.stringify({options: [
|
||||
{id: 'a', name: 'Alpha'},
|
||||
{id: 'b', name: 'Beta'},
|
||||
]}),
|
||||
});
|
||||
const value = makeValue({value: JSON.stringify(['a', 'b'])});
|
||||
expect(getPropertyValueDisplay(field, value)).toBe('Alpha, Beta');
|
||||
});
|
||||
|
||||
test('getPropertyValueDisplay handles multiselect fallbacks and empty', () => {
|
||||
const field = makeField({type: 'multiselect', attrs: JSON.stringify({options: []})});
|
||||
expect(getPropertyValueDisplay(field, makeValue({value: ''}))).toBe('');
|
||||
|
||||
// Comma string fallback
|
||||
expect(getPropertyValueDisplay(field, makeValue({value: 'x,y'}))).toBe('x, y');
|
||||
});
|
||||
});
|
||||
|
||||
109
app/products/playbooks/utils/property_fields.ts
Normal file
109
app/products/playbooks/utils/property_fields.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {safeParseJSON} from '@utils/helpers';
|
||||
import {logWarning} from '@utils/log';
|
||||
import {getSelectedOptionIds} from '@utils/user';
|
||||
|
||||
import type PlaybookRunPropertyFieldModel from '@playbooks/database/models/playbook_run_attribute';
|
||||
import type PlaybookRunPropertyValueModel from '@playbooks/database/models/playbook_run_attribute_value';
|
||||
|
||||
type PropertyFieldAttrs = {
|
||||
options?: Array<{id: string; name: string}>;
|
||||
parent_id?: string;
|
||||
sort_order: number;
|
||||
value_type: string;
|
||||
} | null;
|
||||
|
||||
export const parsePropertyFieldAttrs = (attrs?: string): PropertyFieldAttrs => {
|
||||
if (!attrs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = safeParseJSON(attrs) as Record<string, unknown> | null | undefined;
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = Array.isArray((parsed as any).options) ? (parsed as any).options as Array<{id: string; name: string}> : undefined;
|
||||
const parent_id = typeof (parsed as any).parent_id === 'string' ? (parsed as any).parent_id : undefined;
|
||||
const sort_order = typeof (parsed as any).sort_order === 'number' ? (parsed as any).sort_order : 999;
|
||||
const value_type = typeof (parsed as any).value_type === 'string' ? (parsed as any).value_type : '';
|
||||
|
||||
return {options, parent_id, sort_order, value_type};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPropertyFieldSortOrder = (propertyField: PlaybookRunPropertyFieldModel): number => {
|
||||
const attrs = parsePropertyFieldAttrs(propertyField.attrs);
|
||||
if (!attrs || typeof attrs.sort_order !== 'number') {
|
||||
return 999;
|
||||
}
|
||||
return attrs.sort_order;
|
||||
};
|
||||
|
||||
export const sortPropertyFieldsByOrder = (propertyFields: PlaybookRunPropertyFieldModel[]): PlaybookRunPropertyFieldModel[] => {
|
||||
return [...propertyFields].sort((a, b) => getPropertyFieldSortOrder(a) - getPropertyFieldSortOrder(b));
|
||||
};
|
||||
|
||||
export const formatPropertyFieldOptionsForSelector = (propertyField: PlaybookRunPropertyFieldModel): DialogOption[] => {
|
||||
if (propertyField.type !== 'select' && propertyField.type !== 'multiselect') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const attrs = parsePropertyFieldAttrs(propertyField.attrs);
|
||||
const options = attrs?.options;
|
||||
if (!options || !Array.isArray(options)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return options.map((o) => ({text: o.name, value: o.id}));
|
||||
};
|
||||
|
||||
export const getOptionNameById = (options: Array<{id: string; name: string}>, optionId: string): string => {
|
||||
const found = options.find((o) => o.id === optionId);
|
||||
return found ? found.name : optionId;
|
||||
};
|
||||
|
||||
export const getPropertyValueDisplay = (
|
||||
propertyField: PlaybookRunPropertyFieldModel,
|
||||
value?: PlaybookRunPropertyValueModel,
|
||||
): string => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const rawValue = value.value || '';
|
||||
|
||||
if (propertyField.type === 'text') {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
const attrs = parsePropertyFieldAttrs(propertyField.attrs);
|
||||
const options = attrs?.options || [];
|
||||
|
||||
if (propertyField.type === 'select') {
|
||||
if (!rawValue) {
|
||||
return '';
|
||||
}
|
||||
return getOptionNameById(options, rawValue);
|
||||
}
|
||||
|
||||
if (propertyField.type === 'multiselect') {
|
||||
const selectedIds = getSelectedOptionIds(rawValue, 'multiselect');
|
||||
if (!selectedIds.length) {
|
||||
return '';
|
||||
}
|
||||
const names = selectedIds.map((id) => getOptionNameById(options, id));
|
||||
return names.join(', ');
|
||||
}
|
||||
|
||||
logWarning('Unknown playbook run propertyField type, will use raw value', propertyField.type);
|
||||
|
||||
// Unknown type fallback
|
||||
return rawValue;
|
||||
};
|
||||
|
||||
|
|
@ -760,6 +760,7 @@
|
|||
"mobile.manage_members.remove_member": "Remove from Channel",
|
||||
"mobile.manage_members.section_title_admins": "CHANNEL ADMINS",
|
||||
"mobile.manage_members.section_title_members": "MEMBERS",
|
||||
"mobile.managed_app.save": "Update",
|
||||
"mobile.managed.biometric_failed": "Biometric or Passcode authentication failed.",
|
||||
"mobile.managed.blocked_by": "Blocked by {vendor}",
|
||||
"mobile.managed.exit": "Exit",
|
||||
|
|
@ -1082,6 +1083,7 @@
|
|||
"playbooks.playbook_run.owner": "Owner",
|
||||
"playbooks.playbook_run.participants": "Participants",
|
||||
"playbooks.playbook_run.participants_title": "Participants",
|
||||
"playbooks.playbook_run.property_fields": "Property Fields",
|
||||
"playbooks.playbook_run.rename.button": "Save",
|
||||
"playbooks.playbook_run.rename.label": "Checklist name",
|
||||
"playbooks.playbook_run.rename.title": "Rename playbook run",
|
||||
|
|
@ -1112,6 +1114,8 @@
|
|||
"playbooks.post_update.placeholder": "Enter your update message",
|
||||
"playbooks.post_update.post.button": "Post",
|
||||
"playbooks.post_update.title": "Post update",
|
||||
"playbooks.property_field.tap_to_edit": "Tap to edit",
|
||||
"playbooks.property_field.tap_to_select": "Tap to select",
|
||||
"playbooks.retrospective_not_available.description": "Only Playbook Checklists are available on mobile. To fill the Run Retrospective, please use the desktop or web app.",
|
||||
"playbooks.retrospective_not_available.ok": "OK",
|
||||
"playbooks.retrospective_not_available.title": "Playbooks Retrospective not available",
|
||||
|
|
|
|||
|
|
@ -1120,7 +1120,7 @@ class TestHelperSingleton {
|
|||
update_at: 0,
|
||||
});
|
||||
|
||||
createPlaybookRunAttribute = (prefix: string, index: number): PlaybookRunAttribute => ({
|
||||
createPlaybookRunAttribute = (prefix: string, index: number): PlaybookRunPropertyField => ({
|
||||
id: `${prefix}-attribute_${index}`,
|
||||
group_id: 'group_1',
|
||||
name: `Attribute ${index + 1}`,
|
||||
|
|
@ -1133,10 +1133,11 @@ class TestHelperSingleton {
|
|||
attrs: '',
|
||||
});
|
||||
|
||||
createPlaybookRunAttributeValue = (attributeId: string, runId: string, index: number): PlaybookRunAttributeValue => ({
|
||||
createPlaybookRunAttributeValue = (attributeId: string, runId: string, index: number): PlaybookRunPropertyValue => ({
|
||||
id: `${runId}-${attributeId}-value_${index}`,
|
||||
attribute_id: attributeId,
|
||||
run_id: runId,
|
||||
field_id: attributeId,
|
||||
target_id: runId,
|
||||
update_at: Date.now(),
|
||||
value: `Value ${index + 1}`,
|
||||
});
|
||||
|
||||
|
|
@ -1360,7 +1361,7 @@ class TestHelperSingleton {
|
|||
};
|
||||
};
|
||||
|
||||
fakePlaybookRunAttribute = (overwrite: Partial<PlaybookRunAttribute> = {}): PlaybookRunAttribute => {
|
||||
fakePlaybookRunAttribute = (overwrite: Partial<PlaybookRunPropertyField> = {}): PlaybookRunPropertyField => {
|
||||
return {
|
||||
id: this.generateId(),
|
||||
group_id: this.generateId(),
|
||||
|
|
@ -1376,11 +1377,12 @@ class TestHelperSingleton {
|
|||
};
|
||||
};
|
||||
|
||||
fakePlaybookRunAttributeValue = (attributeId: string, runId: string, overwrite: Partial<PlaybookRunAttributeValue> = {}): PlaybookRunAttributeValue => {
|
||||
fakePlaybookRunAttributeValue = (attributeId: string, runId: string, overwrite: Partial<PlaybookRunPropertyValue> = {}): PlaybookRunPropertyValue => {
|
||||
return {
|
||||
id: this.generateId(),
|
||||
attribute_id: attributeId,
|
||||
run_id: runId,
|
||||
field_id: attributeId,
|
||||
target_id: runId,
|
||||
update_at: Date.now(),
|
||||
value: 'Test Value',
|
||||
...overwrite,
|
||||
};
|
||||
|
|
@ -1408,6 +1410,7 @@ class TestHelperSingleton {
|
|||
attributeId: this.generateId(),
|
||||
runId: this.generateId(),
|
||||
value: 'Test Value',
|
||||
updateAt: Date.now(),
|
||||
attribute: this.fakeRelation(),
|
||||
run: this.fakeRelation(),
|
||||
...overwrite,
|
||||
|
|
|
|||
Loading…
Reference in a new issue