Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1fd1b9e
[Cases] tasks: add feature flag xpack.cases.tasks.enabled
michaelolo24 Mar 15, 2026
770ad8e
[Cases] tasks: add CaseTask and CaseTaskTemplate domain types in common/
michaelolo24 Mar 15, 2026
0b0565d
[Cases] tasks: add task-related user action types
michaelolo24 Mar 15, 2026
7572354
[Cases] tasks: register case-task and case-task-template SavedObject …
michaelolo24 Mar 15, 2026
b8dc288
[Cases] tasks: add task_summary field to case SO
michaelolo24 Mar 15, 2026
4691192
[Cases] PR 2: Add CaseTaskService — core CRUD, cascade delete, sort o…
michaelolo24 Mar 15, 2026
13dfec5
[Cases] PR 3: Add CaseTaskTemplateService — CRUD + applyTemplate
michaelolo24 Mar 16, 2026
2c96d67
[Cases] PR 4: Client layer — TasksSubClient + TaskTemplatesSubClient …
michaelolo24 Mar 16, 2026
6f754c5
[Cases] PR 5: HTTP API routes for tasks and task templates
michaelolo24 Mar 16, 2026
43a62be
[Cases] PR 6: UI layer — Tasks tab in case view
michaelolo24 Mar 16, 2026
2cefb5b
[Cases] PR 7: Task user action builders for activity timeline
michaelolo24 Mar 16, 2026
1a3651d
[Cases] Fix: set importableAndExportable: true on task SO types
michaelolo24 Mar 16, 2026
b90d1f1
[Cases] PR 8: Add task telemetry collection
michaelolo24 Mar 16, 2026
7d015ee
[Cases] Add task creation flyout
michaelolo24 Mar 16, 2026
08c0c1b
[Cases] Fix tasks page load: add missing GET route, disable retries, …
michaelolo24 Mar 16, 2026
1bc358f
[Cases] Fix 'Unsupported saved object type: cases-tasks' across all c…
michaelolo24 Mar 16, 2026
c435986
[Cases] Tasks: assignees, edit flyout, completion notes, sub-tasks
michaelolo24 Mar 16, 2026
dbdc4f7
Fix assignees infinite loading, add action text labels, add task temp…
michaelolo24 Mar 16, 2026
794c506
Match task assignees field exactly to main case assignees flow
michaelolo24 Mar 16, 2026
f06b022
Fix tasks tab hiding case view navigation tabs
michaelolo24 Mar 16, 2026
6163df2
Add task checklist indicator and apply-template from cases UI
michaelolo24 Mar 16, 2026
96a3a9d
[Cases] Add task settings page, board view, and task status improvements
michaelolo24 Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions x-pack/platform/plugins/shared/cases/common/api/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import {
INTERNAL_CASE_SIMILAR_CASES_URL,
INTERNAL_CASE_OBSERVABLES_DELETE_URL,
INTERNAL_BULK_CREATE_CASE_OBSERVABLES_URL,
CASE_TASKS_URL,
CASE_TASK_DETAILS_URL,
CASE_TASKS_REORDER_URL,
CASE_TASKS_APPLY_TEMPLATE_URL,
} from '../constants';

export const getCaseDetailsUrl = (id: string): string => {
Expand Down Expand Up @@ -116,3 +120,19 @@ export const getBulkCreateObservablesUrl = (id: string): string => {
export const getCaseSimilarCasesUrl = (caseId: string) => {
return INTERNAL_CASE_SIMILAR_CASES_URL.replace('{case_id}', caseId);
};

export const getCaseTasksUrl = (caseId: string): string => {
return CASE_TASKS_URL.replace('{case_id}', caseId);
};

export const getCaseTaskDetailsUrl = (caseId: string, taskId: string): string => {
return CASE_TASK_DETAILS_URL.replace('{case_id}', caseId).replace('{task_id}', taskId);
};

export const getCaseTasksReorderUrl = (caseId: string): string => {
return CASE_TASKS_REORDER_URL.replace('{case_id}', caseId);
};

export const getCaseTasksApplyTemplateUrl = (caseId: string): string => {
return CASE_TASKS_APPLY_TEMPLATE_URL.replace('{case_id}', caseId);
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const CASE_VIEW_TAB_PATH = `${CASE_VIEW_PATH}/?tabId=:tabId` as const;
export const CASES_TEMPLATES_PATH = '/templates' as const;
export const CASES_CREATE_TEMPLATE_PATH = `${CASES_TEMPLATES_PATH}/create` as const;
export const CASES_EDIT_TEMPLATE_PATH = `${CASES_TEMPLATES_PATH}/:templateId/edit` as const;
export const CASES_TASK_SETTINGS_PATH = '/task-settings' as const;
/**
* The main Cases application is in the stack management under the
* Alerts and Insights section. To do that, Cases registers to the management
Expand Down
22 changes: 22 additions & 0 deletions x-pack/platform/plugins/shared/cases/common/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure' as const;
export const CASE_RULES_SAVED_OBJECT = 'cases-rules' as const;
export const CASE_ID_INCREMENTER_SAVED_OBJECT = 'cases-incrementing-id' as const;
export const CASE_TEMPLATE_SAVED_OBJECT = 'cases-templates' as const;
export const CASE_TASK_SAVED_OBJECT = 'cases-tasks' as const;
export const CASE_TASK_TEMPLATE_SAVED_OBJECT = 'cases-task-templates' as const;

/**
* If more values are added here please also add them here: x-pack/test/cases_api_integration/common/plugins
Expand Down Expand Up @@ -108,6 +110,26 @@ export const INTERNAL_CASE_GET_CASES_BY_ATTACHMENT_URL =
`${CASES_INTERNAL_URL}/case/attachments/_find_containing_all` as const;
export const INTERNAL_BULK_CREATE_CASE_OBSERVABLES_URL = `${CASES_INTERNAL_URL}/{case_id}/observables/_bulk_create`;

/**
* Task routes
*/
export const CASES_TASKS_URL = `${CASES_URL}/tasks` as const;
export const CASE_TASKS_URL = `${CASE_DETAILS_URL}/tasks` as const;
export const CASE_TASK_DETAILS_URL = `${CASE_TASKS_URL}/{task_id}` as const;
export const CASE_TASKS_BULK_CREATE_URL = `${CASE_TASKS_URL}/_bulk_create` as const;
export const CASE_TASKS_BULK_UPDATE_URL = `${CASE_TASKS_URL}/_bulk_update` as const;
export const CASE_TASKS_BULK_DELETE_URL = `${CASE_TASKS_URL}/_bulk_delete` as const;
export const CASE_TASKS_REORDER_URL = `${CASE_TASKS_URL}/_reorder` as const;
export const CASE_TASKS_APPLY_TEMPLATE_URL = `${CASE_TASKS_URL}/_apply_template` as const;
export const CASES_TASKS_MY_URL = `${CASES_TASKS_URL}/_my` as const;
export const CASES_TASKS_FIND_URL = `${CASES_TASKS_URL}/_find` as const;

/**
* Task template routes
*/
export const CASES_TASK_TEMPLATES_URL = `${CASES_URL}/task_templates` as const;
export const CASE_TASK_TEMPLATE_DETAILS_URL = `${CASES_TASK_TEMPLATES_URL}/{template_id}` as const;

export const INTERNAL_TEMPLATES_URL = `${CASES_INTERNAL_URL}/templates` as const;
export const INTERNAL_TEMPLATE_DETAILS_URL = `${INTERNAL_TEMPLATES_URL}/{template_id}` as const;
export const INTERNAL_BULK_DELETE_TEMPLATES_URL = `${INTERNAL_TEMPLATES_URL}/_bulk_delete` as const;
Expand Down
1 change: 1 addition & 0 deletions x-pack/platform/plugins/shared/cases/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ export enum CASE_VIEW_PAGE_TABS {
OBSERVABLES = 'observables',
SIMILAR_CASES = 'similar_cases',
ATTACHMENTS = 'attachments',
TASKS = 'tasks',
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import {
CustomFieldNumberTypeRt,
} from '../../domain';
import type { Configurations, Configuration } from '../../domain/configure/v1';
import { ConfigurationBasicWithoutOwnerRt, ClosureTypeRt } from '../../domain/configure/v1';
import {
ConfigurationBasicWithoutOwnerRt,
ClosureTypeRt,
TaskStatusesConfigurationRt,
} from '../../domain/configure/v1';
import { CaseConnectorRt } from '../../domain/connector/v1';
import { CaseBaseOptionalFieldsRequestRt } from '../case/v1';
import {
Expand Down Expand Up @@ -189,6 +193,7 @@ export const ConfigurationRequestRt = rt.intersection([
customFields: CustomFieldsConfigurationRt,
templates: TemplatesConfigurationRt,
observableTypes: ObservableTypesConfigurationRt,
taskStatuses: TaskStatusesConfigurationRt,
})
),
]);
Expand All @@ -210,11 +215,12 @@ export const CaseConfigureRequestParamsRt = rt.strict({
export const ConfigurationPatchRequestRt = rt.intersection([
rt.exact(
rt.partial({
closure_type: ConfigurationBasicWithoutOwnerRt.type.props.closure_type,
connector: ConfigurationBasicWithoutOwnerRt.type.props.connector,
closure_type: ConfigurationBasicWithoutOwnerRt.types[0].type.props.closure_type,
connector: ConfigurationBasicWithoutOwnerRt.types[0].type.props.connector,
customFields: CustomFieldsConfigurationRt,
templates: TemplatesConfigurationRt,
observableTypes: ObservableTypesConfigurationRt,
taskStatuses: TaskStatusesConfigurationRt,
})
),
rt.strict({ version: rt.string }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '../custom_field/v1';
import { CaseBaseOptionalFieldsRt } from '../case/v1';
import { CaseObservableTypeRt } from '../observable/v1';
import { TaskStatusDefinitionRt } from '../task/v1';

export const ClosureTypeRt = rt.union([
rt.literal('close-by-user'),
Expand Down Expand Up @@ -107,28 +108,40 @@ export const TemplateConfigurationRt = rt.intersection([

export const TemplatesConfigurationRt = rt.array(TemplateConfigurationRt);

export const ConfigurationBasicWithoutOwnerRt = rt.strict({
/**
* The external connector
*/
connector: CaseConnectorRt,
/**
* Whether to close the case after it has been synced with the external system
*/
closure_type: ClosureTypeRt,
/**
* The custom fields configured for the case
*/
customFields: CustomFieldsConfigurationRt,
/**
* Templates configured for the case
*/
templates: TemplatesConfigurationRt,
/**
* Observable types configured for the case
*/
observableTypes: ObservableTypesConfigurationRt,
});
export const TaskStatusesConfigurationRt = rt.array(TaskStatusDefinitionRt);

export const ConfigurationBasicWithoutOwnerRt = rt.intersection([
rt.strict({
/**
* The external connector
*/
connector: CaseConnectorRt,
/**
* Whether to close the case after it has been synced with the external system
*/
closure_type: ClosureTypeRt,
/**
* The custom fields configured for the case
*/
customFields: CustomFieldsConfigurationRt,
/**
* Templates configured for the case
*/
templates: TemplatesConfigurationRt,
/**
* Observable types configured for the case
*/
observableTypes: ObservableTypesConfigurationRt,
}),
rt.exact(
rt.partial({
/**
* Custom task status definitions for this configuration
*/
taskStatuses: TaskStatusesConfigurationRt,
})
),
]);

export const CasesConfigureBasicRt = rt.intersection([
ConfigurationBasicWithoutOwnerRt,
Expand Down Expand Up @@ -175,3 +188,4 @@ export type Configuration = rt.TypeOf<typeof ConfigurationRt>;
export type Configurations = rt.TypeOf<typeof ConfigurationsRt>;
export type ObservableTypesConfiguration = rt.TypeOf<typeof ObservableTypesConfigurationRt>;
export type ObservableTypeConfiguration = rt.TypeOf<typeof CaseObservableTypeRt>;
export type TaskStatusesConfiguration = rt.TypeOf<typeof TaskStatusesConfigurationRt>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './v1';
126 changes: 126 additions & 0 deletions x-pack/platform/plugins/shared/cases/common/types/domain/task/v1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import * as rt from 'io-ts';
import { UserRt } from '../user/v1';

export const CaseTaskStatusRt = rt.string;

export const TaskStatusDefinitionRt = rt.intersection([
rt.strict({
key: rt.string,
label: rt.string,
color: rt.string,
}),
rt.exact(rt.partial({ disabled: rt.boolean })),
]);

export const TaskStatusesConfigurationRt = rt.array(TaskStatusDefinitionRt);

/** Keys of the four built-in statuses that can be disabled but never deleted. */
export const BUILTIN_STATUS_KEYS: ReadonlySet<string> = new Set([
'open',
'in_progress',
'done',
'cancelled',
]);

export const DEFAULT_TASK_STATUSES: Array<rt.TypeOf<typeof TaskStatusDefinitionRt>> = [
{ key: 'open', label: 'Open', color: 'default' },
{ key: 'in_progress', label: 'In progress', color: 'primary' },
{ key: 'done', label: 'Done', color: 'success' },
{ key: 'cancelled', label: 'Cancelled', color: 'default' },
];

/**
* Merge stored task statuses with the 4 built-in defaults.
* Built-ins always appear first, preserving any stored overrides (label/color/disabled).
* Custom statuses (not in BUILTIN_STATUS_KEYS) are appended after.
*/
export function mergeTaskStatusesWithDefaults(
stored: TaskStatusDefinition[]
): TaskStatusDefinition[] {
const storedByKey = new Map(stored.map((s) => [s.key, s]));
const builtins = DEFAULT_TASK_STATUSES.map((def) => storedByKey.get(def.key) ?? def);
const custom = stored.filter((s) => !BUILTIN_STATUS_KEYS.has(s.key));
return [...builtins, ...custom];
}

export const CaseTaskPriorityRt = rt.keyof({
low: null,
medium: null,
high: null,
critical: null,
});

export const CaseTaskCustomFieldRt = rt.strict({
key: rt.string,
type: rt.keyof({ text: null, toggle: null, number: null, date: null }),
value: rt.union([rt.string, rt.boolean, rt.number, rt.null]),
});

export const CaseTaskAssigneeRt = rt.strict({
uid: rt.string,
});

export const CaseTaskAttributesRt = rt.intersection([
rt.strict({
title: rt.string,
description: rt.string,
case_id: rt.string,
parent_task_id: rt.union([rt.string, rt.null]),
status: CaseTaskStatusRt,
priority: CaseTaskPriorityRt,
assignees: rt.array(CaseTaskAssigneeRt),
due_date: rt.union([rt.string, rt.null]),
started_at: rt.union([rt.string, rt.null]),
completed_at: rt.union([rt.string, rt.null]),
sort_order: rt.number,
template_id: rt.union([rt.string, rt.null]),
custom_fields: rt.array(CaseTaskCustomFieldRt),
required_role: rt.union([rt.string, rt.null]),
owner_team: rt.union([rt.string, rt.null]),
owner: rt.string,
created_at: rt.string,
created_by: UserRt,
updated_at: rt.union([rt.string, rt.null]),
updated_by: rt.union([UserRt, rt.null]),
}),
rt.exact(rt.partial({
completion_notes: rt.union([rt.string, rt.null]),
})),
]);

export const CaseTaskRt = rt.intersection([
CaseTaskAttributesRt,
rt.strict({
id: rt.string,
version: rt.string,
}),
]);

export const CaseTasksRt = rt.array(CaseTaskRt);

export const CaseTaskSummaryRt = rt.strict({
total: rt.number,
open: rt.number,
in_progress: rt.number,
completed: rt.number,
cancelled: rt.number,
next_due_date: rt.union([rt.string, rt.null]),
});

export type CaseTaskStatus = rt.TypeOf<typeof CaseTaskStatusRt>;
export type TaskStatusDefinition = rt.TypeOf<typeof TaskStatusDefinitionRt>;
export type TaskStatusesConfiguration = rt.TypeOf<typeof TaskStatusesConfigurationRt>;
export type CaseTaskPriority = rt.TypeOf<typeof CaseTaskPriorityRt>;
export type CaseTaskAssignee = rt.TypeOf<typeof CaseTaskAssigneeRt>;
export type CaseTaskCustomField = rt.TypeOf<typeof CaseTaskCustomFieldRt>;
export type CaseTaskAttributes = rt.TypeOf<typeof CaseTaskAttributesRt>;
export type CaseTask = rt.TypeOf<typeof CaseTaskRt>;
export type CaseTasks = rt.TypeOf<typeof CaseTasksRt>;
export type CaseTaskSummary = rt.TypeOf<typeof CaseTaskSummaryRt>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import * as rt from 'io-ts';
import { UserRt } from '../user/v1';
import { CaseTaskPriorityRt } from '../task/v1';

export const CaseTaskTemplateSubtaskRt = rt.strict({
title: rt.string,
description: rt.string,
priority: CaseTaskPriorityRt,
relative_due_days: rt.union([rt.number, rt.null]),
sort_order: rt.number,
});

export const CaseTaskTemplateTaskRt = rt.strict({
title: rt.string,
description: rt.string,
priority: CaseTaskPriorityRt,
relative_due_days: rt.union([rt.number, rt.null]),
sort_order: rt.number,
subtasks: rt.array(CaseTaskTemplateSubtaskRt),
});

export const CaseTaskTemplateAttributesRt = rt.strict({
name: rt.string,
description: rt.string,
scope: rt.keyof({ global: null, space: null }),
tags: rt.array(rt.string),
tasks: rt.array(CaseTaskTemplateTaskRt),
owner: rt.string,
created_at: rt.string,
created_by: UserRt,
updated_at: rt.union([rt.string, rt.null]),
updated_by: rt.union([UserRt, rt.null]),
});

export const CaseTaskTemplateRt = rt.intersection([
CaseTaskTemplateAttributesRt,
rt.strict({
id: rt.string,
version: rt.string,
}),
]);

export const CaseTaskTemplatesRt = rt.array(CaseTaskTemplateRt);

export type CaseTaskTemplateSubtask = rt.TypeOf<typeof CaseTaskTemplateSubtaskRt>;
export type CaseTaskTemplateTask = rt.TypeOf<typeof CaseTaskTemplateTaskRt>;
export type CaseTaskTemplateAttributes = rt.TypeOf<typeof CaseTaskTemplateAttributesRt>;
export type CaseTaskTemplate = rt.TypeOf<typeof CaseTaskTemplateRt>;
export type CaseTaskTemplates = rt.TypeOf<typeof CaseTaskTemplatesRt>;
Loading
Loading