Identifiers of issues in the configured issue tracker that track the progress of fixing this {target}.
+ When the issues have all been resolved, or the technical debt end date has passed, whichever happens first, the technical debt should be resolved and the technical debt target is no longer evaluated.
+ {issueTrackerInstruction}
+ >
+ )
+ const [suggestions, setSuggestions] = useState([]);
+ const labelId = `issue-identifiers-label-${metric_uuid}`
+ const issue_ids = getMetricIssueIds(metric, entityKey);
+ return (
+ {
+ if (query) {
+ get_report_issue_tracker_suggestions(report_uuid, query).then((suggestionsResponse) => {
+ const suggestionOptions = suggestionsResponse.suggestions.map((s) => ({ key: s.key, text: `${s.key}: ${s.text}`, value: s.key }))
+ setSuggestions(suggestionOptions)
+ })
+ } else {
+ setSuggestions([])
+ }
+ }}
+ requiredPermissions={[EDIT_REPORT_PERMISSION]}
+ label={}
+ options={suggestions}
+ set_value={(value) => set_metric_attribute(metric_uuid, "issue_ids", value, reload)}
+ value={issue_ids}
+ key={issue_ids} // Make sure the multiple choice input is rerendered when the issue ids change
+ />
+ )
+}
+IssueIdentifiers.propTypes = {
+ entityKey: PropTypes.string,
+ issueTrackerInstruction: PropTypes.node,
+ metric: metricPropType,
+ metric_uuid: PropTypes.string,
+ report_uuid: PropTypes.string,
+ target: PropTypes.string,
+ reload: PropTypes.func
+}
+
+export function IssuesRows({ entityKey, metric, metric_uuid, reload, report, target }) {
+ const parameters = report?.issue_tracker?.parameters;
+ const issueTrackerConfigured = Boolean(report?.issue_tracker?.type && parameters?.url && parameters?.project_key && parameters?.issue_type);
+ const issueTrackerInstruction = issueTrackerConfigured ? null : Please configure an issue tracker by expanding the report title, selecting the 'Issue tracker' tab, and configuring an issue tracker.
;
+ const issueIdentifiersProps = {
+ entityKey: entityKey,
+ issueTrackerInstruction: issueTrackerInstruction,
+ metric: metric,
+ metric_uuid: metric_uuid,
+ report_uuid: report.report_uuid,
+ target: target ?? "metric",
+ reload: reload
+ }
+ return (
+ <>
+
+
+
+
+ }
+ editableComponent={
+ <>
+
+
+
+
+
+
+ >
+ }
+ />
+
+ {(getMetricIssueIds(metric, entityKey).length > 0 && !issueTrackerConfigured) &&
+
+
+
+
+
+ }
+ {(metric.issue_status ?? []).filter((issue_status => issue_status.connection_error)).map((issue_status) =>
+
+
+
+
+
+ )}
+ {(metric.issue_status ?? []).filter((issue_status => issue_status.parse_error)).map((issue_status) =>
+
+
+
+
+
+ )}
+ >
+ )
+}
+IssuesRows.propTypes = {
+ entityKey: PropTypes.string,
+ metric: metricPropType,
+ metric_uuid: PropTypes.string,
+ reload: PropTypes.func,
+ report: reportPropType,
+ target: PropTypes.string,
+}
diff --git a/components/frontend/src/issue/IssuesRows.test.js b/components/frontend/src/issue/IssuesRows.test.js
new file mode 100644
index 0000000000..f527ebc357
--- /dev/null
+++ b/components/frontend/src/issue/IssuesRows.test.js
@@ -0,0 +1,112 @@
+import React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { EDIT_REPORT_PERMISSION, Permissions } from '../context/Permissions';
+import { IssuesRows } from './IssuesRows';
+import * as fetch_server_api from '../api/fetch_server_api';
+
+jest.mock("../api/fetch_server_api.js")
+
+const reportWithIssueTracker = {
+ issue_tracker: { type: "Jira", parameters: { url: "https://jira", project_key: "KEY", issue_type: "Bug" } }
+}
+
+function renderIssuesRow(
+ {
+ issue_ids = [],
+ report = { subjects: {} },
+ permissions = [EDIT_REPORT_PERMISSION],
+ issue_status = []
+ } = {}) {
+ render(
+
+
+
+ );
+}
+
+it('does not show an error message if the metric has no issues and no issue tracker is configured', () => {
+ renderIssuesRow()
+ expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(0);
+});
+
+it('does not show an error message if the metric has no issues and an issue tracker is configured', () => {
+ renderIssuesRow({ report: { issue_tracker: { type: "Jira" } } })
+ expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(0);
+});
+
+it('does not show an error message if the metric has issues and an issue tracker is configured', () => {
+ renderIssuesRow({ issue_ids: ["BAR-42"], report: { issue_tracker: { type: "Jira", parameters: { url: "https://jira", project_key: "KEY", issue_type: "Bug" } } } })
+ expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(0);
+});
+
+it('shows an error message if the metric has issues but no issue tracker is configured', () => {
+ renderIssuesRow({ issue_ids: ["FOO-42"] })
+ expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(1);
+});
+
+it('shows a connection error', () => {
+ renderIssuesRow({ issue_status: [{ issue_id: "FOO-43", connection_error: "Oops" }] })
+ expect(screen.queryAllByText(/Connection error/).length).toBe(1);
+ expect(screen.queryAllByText(/Oops/).length).toBe(1);
+});
+
+it('shows a parse error', () => {
+ renderIssuesRow({ issue_status: [{ issue_id: "FOO-43", parse_error: "Oops" }] })
+ expect(screen.queryAllByText(/Parse error/).length).toBe(1);
+ expect(screen.queryAllByText(/Oops/).length).toBe(1);
+});
+
+it('creates an issue', () => {
+ window.open = jest.fn()
+ fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ ok: true, error: "", issue_url: "https://tracker/foo-42" });
+ renderIssuesRow({ report: reportWithIssueTracker })
+ fireEvent.click(screen.getByText(/Create new issue/))
+ expect(fetch_server_api.fetch_server_api).toHaveBeenLastCalledWith("post", "metric/metric_uuid/issue/new", { metric_url: "http://localhost/#metric_uuid" });
+});
+
+it('tries to create an issue', () => {
+ fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ ok: false, error: "Dummy", issue_url: "" });
+ renderIssuesRow({ report: reportWithIssueTracker })
+ fireEvent.click(screen.getByText(/Create new issue/))
+ expect(fetch_server_api.fetch_server_api).toHaveBeenLastCalledWith("post", "metric/metric_uuid/issue/new", { metric_url: "http://localhost/#metric_uuid" });
+});
+
+it('does not show the create issue button if the user has no permissions', () => {
+ renderIssuesRow({ report: reportWithIssueTracker, permissions: [] })
+ expect(screen.queryAllByText(/Create new issue/).length).toBe(0)
+})
+
+it('adds an issue id', async () => {
+ fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ suggestions: [{ key: "FOO-42", text: "Suggestion" }] });
+ renderIssuesRow()
+ await userEvent.type(screen.getByLabelText(/Issue identifiers/), 'FOO-42{Enter}');
+ expect(fetch_server_api.fetch_server_api).toHaveBeenLastCalledWith("post", "metric/metric_uuid/attribute/issue_ids", { issue_ids: ["FOO-42"] });
+});
+
+it('shows issue id suggestions', async () => {
+ fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ suggestions: [{ key: "FOO-42", text: "Suggestion" }] });
+ renderIssuesRow({ report: { issue_tracker: { type: "Jira", parameters: { url: "https://jira" } } } })
+ await userEvent.type(screen.getByLabelText(/Issue identifiers/), 'u');
+ expect(screen.queryAllByText(/FOO-42: Suggestion/).length).toBe(1)
+});
+
+it('shows no issue id suggestions without a query', async () => {
+ fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ suggestions: [{ key: "FOO-42", text: "Suggestion" }] });
+ renderIssuesRow({ report: { issue_tracker: { type: "Jira", parameters: { url: "https://jira" } } } })
+ await userEvent.type(screen.getByLabelText(/Issue identifiers/), 's');
+ expect(screen.queryAllByText(/FOO-42: Suggestion/).length).toBe(1)
+ await userEvent.clear(screen.getByLabelText(/Issue identifiers/).firstChild);
+ expect(screen.queryAllByText(/FOO-42: Suggestion/).length).toBe(0)
+});
diff --git a/components/frontend/src/metric/MetricDebtParameters.js b/components/frontend/src/metric/MetricDebtParameters.js
index 5379b6ab25..e64b2e9896 100644
--- a/components/frontend/src/metric/MetricDebtParameters.js
+++ b/components/frontend/src/metric/MetricDebtParameters.js
@@ -1,18 +1,13 @@
-import React, { useState } from 'react';
+import React from 'react';
import { Grid } from 'semantic-ui-react';
-import { get_report_issue_tracker_suggestions } from '../api/report';
-import { MultipleChoiceInput } from '../fields/MultipleChoiceInput';
import { SingleChoiceInput } from '../fields/SingleChoiceInput';
import { Comment } from '../fields/Comment';
-import { set_metric_attribute, set_metric_debt, add_metric_issue } from '../api/metric';
+import { set_metric_attribute, set_metric_debt } from '../api/metric';
import { DateInput } from '../fields/DateInput';
-import { ActionButton } from '../widgets/Button';
import { LabelWithDate } from '../widgets/LabelWithDate';
-import { LabelWithHelp } from '../widgets/LabelWithHelp';
import { LabelWithHyperLink } from '../widgets/LabelWithHyperLink';
-import { ErrorMessage } from '../errorMessage';
-import { EDIT_REPORT_PERMISSION, ReadOnlyOrEditable } from '../context/Permissions';
-import { getMetricIssueIds } from '../utils';
+import { EDIT_REPORT_PERMISSION } from '../context/Permissions';
+import { IssuesRows } from '../issue/IssuesRows';
import { Target } from './Target';
function AcceptTechnicalDebt({ metric, metric_uuid, reload }) {
@@ -67,46 +62,7 @@ function TechnicalDebtEndDate({ metric, metric_uuid, reload }) {
)
}
-function IssueIdentifiers({ issue_tracker_instruction, metric, metric_uuid, report_uuid, reload }) {
- const issueStatusHelp = (
- <>
- Identifiers of issues in the configured issue tracker that track the progress of fixing this metric.
- When the issues have all been resolved, or the technical debt end date has passed, whichever happens first, the technical debt should be resolved and the technical debt target is no longer evaluated.
- {issue_tracker_instruction}
- >
- )
- const [suggestions, setSuggestions] = useState([]);
- const labelId = `issue-identifiers-label-${metric_uuid}`
- const issue_ids = getMetricIssueIds(metric);
- return (
- {
- if (query) {
- get_report_issue_tracker_suggestions(report_uuid, query).then((suggestionsResponse) => {
- const suggestionOptions = suggestionsResponse.suggestions.map((s) => ({ key: s.key, text: `${s.key}: ${s.text}`, value: s.key }))
- setSuggestions(suggestionOptions)
- })
- } else {
- setSuggestions([])
- }
- }}
- requiredPermissions={[EDIT_REPORT_PERMISSION]}
- label={}
- options={suggestions}
- set_value={(value) => set_metric_attribute(metric_uuid, "issue_ids", value, reload)}
- value={issue_ids}
- key={issue_ids} // Make sure the multiple choice input is rerendered when the issue ids change
- />
- )
-}
-
-
export function MetricDebtParameters({ report, metric, metric_uuid, reload }) {
- const parameters = report?.issue_tracker?.parameters;
- const issueTrackerConfigured = report?.issue_tracker?.type && parameters?.url && parameters?.project_key && parameters?.issue_type;
- const issueTrackerInstruction = issueTrackerConfigured ? null : Please configure an issue tracker by expanding the report title, selecting the 'Issue tracker' tab, and configuring an issue tracker.
;
return (
@@ -120,56 +76,7 @@ export function MetricDebtParameters({ report, metric, metric_uuid, reload }) {
-
-
-
-
- }
- editableComponent={
- <>
- < Grid.Column width={3} verticalAlign="bottom">
- add_metric_issue(metric_uuid, reload)}
- popup={Create a new issue for this metric in the configured issue tracker and add its identifier to the tracked issue identifiers.{issueTrackerInstruction}
}
- position='top center'
- />
-
-
-
-
- >
- }
- />
-
- {(getMetricIssueIds(metric).length > 0 && !issueTrackerConfigured) &&
-
-
-
-
-
- }
- {(metric.issue_status ?? []).filter((issue_status => issue_status.connection_error)).map((issue_status) =>
-
-
-
-
-
- )}
- {(metric.issue_status ?? []).filter((issue_status => issue_status.parse_error)).map((issue_status) =>
-
-
-
-
-
- )}
+
@@ -43,8 +37,6 @@ function renderMetricDebtParameters(
tags: [],
accept_debt: accept_debt,
scale: scale,
- issue_ids: issue_ids,
- issue_status: issue_status
}
}
metric_uuid="metric_uuid"
@@ -84,81 +76,6 @@ it('sets the technical debt end date', async () => {
expect(fetch_server_api.fetch_server_api).toHaveBeenLastCalledWith("post", "metric/metric_uuid/attribute/debt_end_date", { debt_end_date: "2022-12-31" });
});
-it('does not show an error message if the metric has no issues and no issue tracker is configured', async () => {
- renderMetricDebtParameters()
- expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(0);
-});
-
-it('does not show an error message if the metric has no issues and an issue tracker is configured', async () => {
- renderMetricDebtParameters({ report: { issue_tracker: { type: "Jira" } } })
- expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(0);
-});
-
-it('does not show an error message if the metric has issues and an issue tracker is configured', async () => {
- renderMetricDebtParameters({ issue_ids: ["BAR-42"], report: { issue_tracker: { type: "Jira", parameters: { url: "https://jira", project_key: "KEY", issue_type: "Bug" } } } })
- expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(0);
-});
-
-it('shows an error message if the metric has issues but no issue tracker is configured', async () => {
- renderMetricDebtParameters({ issue_ids: ["FOO-42"] })
- expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(1);
-});
-
-it('shows a connection error', async () => {
- renderMetricDebtParameters({ issue_status: [{ issue_id: "FOO-43", connection_error: "Oops" }] })
- expect(screen.queryAllByText(/Connection error/).length).toBe(1);
- expect(screen.queryAllByText(/Oops/).length).toBe(1);
-});
-
-it('shows a parse error', async () => {
- renderMetricDebtParameters({ issue_status: [{ issue_id: "FOO-43", parse_error: "Oops" }] })
- expect(screen.queryAllByText(/Parse error/).length).toBe(1);
- expect(screen.queryAllByText(/Oops/).length).toBe(1);
-});
-
-it('creates an issue', async () => {
- window.open = jest.fn()
- fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ ok: true, error: "", issue_url: "https://tracker/foo-42" });
- renderMetricDebtParameters({ report: reportWithIssueTracker })
- fireEvent.click(screen.getByText(/Create new issue/))
- expect(fetch_server_api.fetch_server_api).toHaveBeenLastCalledWith("post", "metric/metric_uuid/issue/new", { metric_url: "http://localhost/#metric_uuid" });
-});
-
-it('tries to create an issue', async () => {
- fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ ok: false, error: "Dummy", issue_url: "" });
- renderMetricDebtParameters({ report: reportWithIssueTracker })
- fireEvent.click(screen.getByText(/Create new issue/))
- expect(fetch_server_api.fetch_server_api).toHaveBeenLastCalledWith("post", "metric/metric_uuid/issue/new", { metric_url: "http://localhost/#metric_uuid" });
-});
-
-it('does not show the create issue button if the user has no permissions', async () => {
- renderMetricDebtParameters({ report: reportWithIssueTracker, permissions: [] })
- expect(screen.queryAllByText(/Create new issue/).length).toBe(0)
-})
-
-it('adds an issue id', async () => {
- fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ suggestions: [{ key: "FOO-42", text: "Suggestion" }] });
- renderMetricDebtParameters()
- await userEvent.type(screen.getByLabelText(/Issue identifiers/), 'FOO-42{Enter}');
- expect(fetch_server_api.fetch_server_api).toHaveBeenLastCalledWith("post", "metric/metric_uuid/attribute/issue_ids", { issue_ids: ["FOO-42"] });
-});
-
-it('shows issue id suggestions', async () => {
- fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ suggestions: [{ key: "FOO-42", text: "Suggestion" }] });
- renderMetricDebtParameters({ report: { issue_tracker: { type: "Jira", parameters: { url: "https://jira" } } } })
- await userEvent.type(screen.getByLabelText(/Issue identifiers/), 'u');
- expect(screen.queryAllByText(/FOO-42: Suggestion/).length).toBe(1)
-});
-
-it('shows no issue id suggestions without a query', async () => {
- fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ suggestions: [{ key: "FOO-42", text: "Suggestion" }] });
- renderMetricDebtParameters({ report: { issue_tracker: { type: "Jira", parameters: { url: "https://jira" } } } })
- await userEvent.type(screen.getByLabelText(/Issue identifiers/), 's');
- expect(screen.queryAllByText(/FOO-42: Suggestion/).length).toBe(1)
- await userEvent.clear(screen.getByLabelText(/Issue identifiers/).firstChild);
- expect(screen.queryAllByText(/FOO-42: Suggestion/).length).toBe(0)
-});
-
it('adds a comment', async () => {
renderMetricDebtParameters()
await userEvent.type(screen.getByLabelText(/Comment/), 'Keep cool{Tab}');
diff --git a/components/frontend/src/metric/MetricDetails.js b/components/frontend/src/metric/MetricDetails.js
index e54859a807..11fb1eda26 100644
--- a/components/frontend/src/metric/MetricDetails.js
+++ b/components/frontend/src/metric/MetricDetails.js
@@ -17,7 +17,7 @@ import { MetricConfigurationParameters } from './MetricConfigurationParameters';
import { MetricDebtParameters } from './MetricDebtParameters';
import { MetricTypeHeader } from './MetricTypeHeader';
import { TrendGraph } from './TrendGraph';
-import { datePropType, reportPropType, reportsPropType, stringsPropType, stringsURLSearchQueryPropType} from '../sharedPropTypes';
+import { datePropType, reportPropType, reportsPropType, settingsPropType, stringsPropType, stringsURLSearchQueryPropType } from '../sharedPropTypes';
function Buttons({ isFirstMetric, isLastMetric, metric_uuid, reload, stopSorting }) {
return (
@@ -53,7 +53,6 @@ fetchMeasurements.propTypes = {
setMeasurements: PropTypes.func
}
-
export function MetricDetails({
changed_fields,
isFirstMetric,
@@ -63,6 +62,7 @@ export function MetricDetails({
report_date,
reports,
report,
+ settings,
stopSorting,
subject_uuid,
visibleDetailsTabs,
@@ -140,7 +140,7 @@ export function MetricDetails({
const source_name = get_source_name(report_source, dataModel);
panes.push({
menuItem: {source_name},
- render: () =>
+ render: () =>
});
});
}
@@ -172,6 +172,7 @@ MetricDetails.propTypes = {
report_date: datePropType,
reports: reportsPropType,
report: reportPropType,
+ settings: settingsPropType,
stopSorting: PropTypes.func,
subject_uuid: PropTypes.string,
visibleDetailsTabs: stringsURLSearchQueryPropType
diff --git a/components/frontend/src/source/SourceEntities.js b/components/frontend/src/source/SourceEntities.js
index 6597e6ccfc..d621e42d0d 100644
--- a/components/frontend/src/source/SourceEntities.js
+++ b/components/frontend/src/source/SourceEntities.js
@@ -4,7 +4,7 @@ import { Button, Icon, Popup, Table } from '../semantic_ui_react_wrappers';
import { SourceEntity } from './SourceEntity';
import { capitalize } from '../utils';
import { DataModel } from '../context/DataModel';
-import { metricPropType, reportPropType, sourcePropType } from '../sharedPropTypes';
+import { metricPropType, reportPropType, settingsPropType, sourcePropType } from '../sharedPropTypes';
export function alignment(attributeType, attributeAlignment) {
if (attributeAlignment === "left" || attributeAlignment === "right") {
@@ -18,7 +18,7 @@ alignment.propTypes = {
attributeAligment: PropTypes.string
}
-export function SourceEntities({ metric, metric_uuid, reload, report, source }) {
+export function SourceEntities({ metric, metric_uuid, reload, report, settings, source }) {
const dataModel = useContext(DataModel)
const [hideIgnoredEntities, setHideIgnoredEntities] = useState(false);
const [sortColumn, setSortColumn] = useState(null);
@@ -73,6 +73,9 @@ export function SourceEntities({ metric, metric_uuid, reload, report, source })
>
{capitalize(entity_name)} first seen
+
+ Issues
+
{entity_attributes.map((entity_attribute) =>
{status === "unconfirmed" ? "" : status_end_date}
{status === "unconfirmed" ? "" : rationale}
{entity.first_seen ? : ""}
+
+
+
{entity_attributes.map((entity_attribute) =>
diff --git a/components/frontend/src/source/SourceEntityDetails.js b/components/frontend/src/source/SourceEntityDetails.js
index 0a4eee5a47..5c229159eb 100644
--- a/components/frontend/src/source/SourceEntityDetails.js
+++ b/components/frontend/src/source/SourceEntityDetails.js
@@ -9,7 +9,8 @@ import { capitalize } from '../utils';
import { source_entity_status_name as status_name } from './source_entity_status';
import { EDIT_ENTITY_PERMISSION } from '../context/Permissions';
import { LabelWithDate } from '../widgets/LabelWithDate';
-import { entityPropType, entityStatusPropType, reportPropType } from '../sharedPropTypes';
+import { IssuesRows } from '../issue/IssuesRows';
+import { entityPropType, entityStatusPropType, metricPropType, reportPropType } from '../sharedPropTypes';
function entityStatusOption(status, text, content, subheader) {
return {
@@ -46,6 +47,7 @@ entityStatusOptions.propTypes = {
export function SourceEntityDetails(
{
entity,
+ metric,
metric_uuid,
name,
rationale,
@@ -96,11 +98,13 @@ export function SourceEntityDetails(
/>
+
);
}
SourceEntityDetails.propTypes = {
entity: entityPropType,
+ metric: metricPropType,
metric_uuid: PropTypes.string,
name: PropTypes.string,
rationale: PropTypes.string,
diff --git a/components/frontend/src/source/SourceEntityDetails.test.js b/components/frontend/src/source/SourceEntityDetails.test.js
index 20d7558949..9ab8e9bff9 100644
--- a/components/frontend/src/source/SourceEntityDetails.test.js
+++ b/components/frontend/src/source/SourceEntityDetails.test.js
@@ -15,9 +15,11 @@ function renderSourceEntityDetails() {
render(
handleSort(null)}
subject_uuid={subject_uuid}
visibleDetailsTabs={settings.visibleDetailsTabs}
diff --git a/components/frontend/src/utils.js b/components/frontend/src/utils.js
index f12b508698..7fa157e3e3 100644
--- a/components/frontend/src/utils.js
+++ b/components/frontend/src/utils.js
@@ -222,9 +222,16 @@ export function nrMetricsInReports(reports) {
return nrMetrics
}
-export function getMetricIssueIds(metric) {
- let issueIds = metric.issue_ids ?? [];
- issueIds.sort();
+export function getMetricIssueIds(metric, entityKey) {
+ let issueIds = [];
+ if (entityKey) {
+ issueIds = metric.entity_issue_ids?.[entityKey] ?? [];
+ } else {
+ issueIds = metric.issue_ids ?? [];
+ Object.values(metric?.entity_issue_ids ?? {}).forEach((entityIssueIds) => issueIds.push(...entityIssueIds))
+ }
+ issueIds = [...new Set(issueIds)];
+ issueIds.sort((id1, id2) => id1.localeCompare(id2));
return issueIds
}
diff --git a/docs/src/changelog.md b/docs/src/changelog.md
index 8210eab9fc..85958433c6 100644
--- a/docs/src/changelog.md
+++ b/docs/src/changelog.md
@@ -20,6 +20,7 @@ If your currently installed *Quality-time* version is v4.10.0 or older, please r
### Added
+- Support creating issues for specific measurement entities. Closes [#5955](https://github.com/ICTU/quality-time/issues/5955).
- Add an endpoint `api/v3/report//metric_status_summary` that returns a summary of the metric statuses for the specified report in JSON format. Closes [#6146](https://github.com/ICTU/quality-time/issues/6146).
## v5.3.1 - 2023-11-08