Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change colors for task status badge #2996

Merged
merged 4 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/components/CriteriaForm/CriteriaForm.i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"Passed weight is not in range": "Weight must be between 1 and {upTo}",
"Simple": "Todo",
"Goal": "",
"Task": "External Task",
"Jira task": "Jira task",
"Weight": "Weight (%)",
"ex: NN%": "ex: {val}",
"Add weight": "",
Expand All @@ -18,6 +18,6 @@
"Suggestions": "",
"This binding is already exist": "",
"Criteria title": "",
"Place link here": "Insert task link or type title",
"Place link here": "Insert task link or type title or jira task key",
"Reset": "Reset"
}
4 changes: 2 additions & 2 deletions src/components/CriteriaForm/CriteriaForm.i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"Passed weight is not in range": "Вес критерия должен быть от 1 до {upTo}",
"Simple": "Критерий",
"Goal": "Цель",
"Task": "Внешняя задача",
"Jira task": "Задача из Jira",
"Weight": "Вес (%)",
"Add weight": "Добавить вес",
"ex: NN%": "ex: {val}",
Expand All @@ -18,6 +18,6 @@
"Suggestions": "Предложения",
"This binding is already exist": "Такая связка уже существует",
"Criteria title": "Заголовок критерия",
"Place link here": "Ссылка на задачу или ее название",
"Place link here": "Ссылка на задачу или ее название или ключ",
"Reset": "Сброс"
}
2 changes: 1 addition & 1 deletion src/components/CriteriaForm/CriteriaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export const CriteriaForm = ({
];

if (externalAllowed) {
base.push({ title: tr('Task'), value: 'task', iconRight: <BetaLiteral size="s" /> });
base.push({ title: tr('Jira task'), value: 'task', iconRight: <BetaLiteral size="s" /> });
}

return base;
Expand Down
2 changes: 1 addition & 1 deletion src/components/HistoryRecord/HistoryRecord.i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"dependencies": "зависимости",
"criteria": "критерий",
"goal as criteria": "цель",
"task as criteria": "задача",
"task as criteria": "задачу",
"marked criteria": "отметил(а) критерий",
"priority": "приоритет",
"from": "с",
Expand Down
4 changes: 3 additions & 1 deletion src/components/HistoryRecord/HistoryRecord.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,9 @@ const HistoryRecordCriteria: React.FC<
to={nullable(to, (val) => (
<>
<HistoryRecordCriteriaItem {...val} strike={action === 'remove'} />
{val?.criteriaGoal && <HistoryRecordText>{tr('as criteria')}</HistoryRecordText>}
{nullable(val.criteriaGoal || val.externalTask, () => (
<HistoryRecordText>{tr('as criteria')}</HistoryRecordText>
))}
{nullable(isChangeAction, () => (
<HistoryRecordText>
{' '}
Expand Down
65 changes: 65 additions & 0 deletions src/utils/db/calculatedGoalsFields.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Role } from '@prisma/client';

import { calcAchievedWeight } from '../recalculateCriteriaScore';
import { getShortId } from '../getShortId';
import { jiraService } from '../integration/jira';

export const addCommonCalculatedGoalFields = (goal: any) => {
const _shortId = `${goal.projectId}-${goal.scopeId}`;
Expand Down Expand Up @@ -63,3 +65,66 @@ export const addCalculatedGoalsFields = <
...addCommonCalculatedGoalFields(goal),
};
};

export const calcStatusColorForExternalTask = <
T extends { stateCategoryId: number; state: string; stateColor: string | null },
>(
externalTask: T,
): T => {
const taskStatusColorMap = jiraService.config?.mapStatusIdToColor;
let color = taskStatusColorMap?.default;

const isFinishedStatus = jiraService.checkStatusIsFinished(externalTask.stateCategoryId);
if (isFinishedStatus) {
const isPositiveStatus = jiraService.positiveStatuses?.includes(externalTask.state) || false;

color = isPositiveStatus ? taskStatusColorMap?.complete : taskStatusColorMap?.failed;
} else if (jiraService.config?.mapStatusKey != null) {
const colorKey = jiraService.config.mapStatusKey[
externalTask.stateCategoryId
] as keyof typeof taskStatusColorMap;
color = taskStatusColorMap?.[colorKey];
}

return {
...externalTask,
stateColor: color,
};
};

// in this case type of Prisma & Kysely model of GoalAchieveCriteria is different and cannot matched between both
// TODO: fix any type annotation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const calculateGoalCriteria = (list: Array<any>) => {
return list.map(({ criteriaGoal, externalTask, ...criteria }) => {
const baseCriteria = {
...criteria,
criteriaGoal: null,
externalTask: null,
};

if (criteriaGoal != null) {
return {
...baseCriteria,
criteriaGoal: {
...criteriaGoal,
_shortId: getShortId(criteriaGoal),
},
};
}

if (externalTask) {
const isFinishedStatus = jiraService.checkStatusIsFinished(externalTask.stateCategoryId);

return {
...baseCriteria,
// mark undone criteria as done if task in finished status
isDone: baseCriteria.isDone || isFinishedStatus,
externalTask: calcStatusColorForExternalTask(externalTask),
criteriaGoal: null,
};
}

return baseCriteria;
});
};
150 changes: 78 additions & 72 deletions src/utils/integration/jira.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'assert';
import JiraApi from 'jira-client';

import { safelyParseJson } from '../safelyParseJson';

export enum AvatarSize {
x1 = 16,
x2 = 24,
Expand Down Expand Up @@ -79,92 +80,97 @@ export interface JiraIssue {
issuetype: JiraIssueType;
}

const isDebugEnabled = process.env.NODE_ENV === 'development' && process.env.DEBUG?.includes('service:jira');
interface JiraServiceConfig {
url: string;
user: string;
password: string;
apiVersion: string;
positiveStatusNames: string;
finishedCategory: JiraIssueStatus['statusCategory'] | null;
mapStatusKey: Record<string, string>;
mapStatusIdToColor: Record<string, string>;
}

// @ts-ignore
class JiraService extends JiraApi {
private positiveFinishedStatuses = '';
const toCamelCase = (key: string): string => {
// drop 'JIRA' prefix
const [_namespace, noChanges, ...toChanges] = key.toLowerCase().split('_');

private finishedStatusCategory?: JiraIssueStatus['statusCategory'];
if (toChanges.length) {
return `${noChanges}${toChanges.map((v) => v[0].toUpperCase() + v.slice(1)).join('')}`;
}

public isEnable = false;
return noChanges;
};

constructor() {
super({
protocol: 'https',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
host: process.env.JIRA_URL!,
username: process.env.JIRA_USER,
password: process.env.JIRA_PASSWORD,
apiVersion: process.env.JIRA_API_VERSION,
strictSSL: process.env.NODE_ENV === 'production',
});
const requiredConfigFields: Array<keyof JiraServiceConfig> = ['url', 'user', 'password', 'apiVersion'];

try {
// check env variables which contains `JIRA` prefix
Object.keys(process.env)
.filter((k) => k.includes('JIRA'))
.forEach((jiraEnvKey) => {
const val = process.env[jiraEnvKey];
const readJiraEnv = () => {
const config = Object.keys(process.env)
.filter((k) => k.startsWith('JIRA'))
.reduce<JiraServiceConfig>((acc, jiraEnvKey) => {
const configKey = toCamelCase(jiraEnvKey) as keyof JiraServiceConfig;
const existingValue = process.env[jiraEnvKey];
const val = existingValue ? safelyParseJson(existingValue) ?? existingValue : null;

assert(val, `Env variable \`${jiraEnvKey}\` must be string, but get ${typeof val}`);
});
acc[configKey] = val;

// here suppoed that env variable is available
this.positiveFinishedStatuses = process.env.JIRA_POSITIVE_STATUS_NAMES as string;
this.finishedStatusCategory = JSON.parse(process.env.JIRA_FINISHED_CATEGORY as string);
return acc;
}, {} as JiraServiceConfig);

assert(
typeof this.finishedStatusCategory === 'object' && this.finishedStatusCategory != null,
"Env variable 'JIRA_FINISHED_CATEGORY' must be JSON string value",
);
this.isEnable = true;
} catch (error) {
console.error(error);
this.isEnable = false;
}
}

/** start overriding private instance methods */
// @ts-ignore
private async doRequest<T extends JiraApi.JsonResponse>(options: any): Promise<T> {
if (isDebugEnabled) {
console.log(options);
}
return config;
};

// @ts-ignore
const res = await super.doRequest(options);
if (isDebugEnabled) {
console.table(res);
}
return res as unknown as T;
}
// TODO: come up with logging for jira queries
const _isDebugEnabled = process.env.NODE_ENV === 'development' && process.env.DEBUG?.includes('service:jira');

const initJiraClient = () => {
const config = readJiraEnv();
const isValidConfig = requiredConfigFields.every((k) => k in config && config[k] != null);

const instance = new JiraApi({
protocol: 'https',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
host: config.url,
username: config.user,
password: config.password,
apiVersion: config.apiVersion,
strictSSL: process.env.NODE_ENV === 'production',
});

protected getUri(options: JiraApi.UriOptions) {
// @ts-ignore
return this.makeUri(options);
}
function checkStatusIsFinished(status: JiraIssueStatus | JiraIssueStatus['statusCategory']['id']) {
if (config.finishedCategory != null) {
if (typeof status === 'number') {
return status === config.finishedCategory.id;
}

protected getRequestHeaders(url: string, options?: JiraApi.UriOptions) {
// @ts-ignore
return this.makeRequestHeader(url, options);
}
/** end overriding private instance methods */

public checkStatusIsFinished(status: JiraIssueStatus) {
if (this.isEnable && this.finishedStatusCategory) {
return (
(status.statusCategory.key === this.finishedStatusCategory.key ||
status.statusCategory.id === this.finishedStatusCategory.id) &&
this.positiveFinishedStatuses.includes(status.name)
return Boolean(
(status.statusCategory.key === config.finishedCategory.key ||
status.statusCategory.id === config.finishedCategory.id) &&
config.positiveStatusNames?.includes(status.name),
);
}

return false;
}
}

export const jiraService = new JiraService();
return {
instance,
checkStatusIsFinished,
get positiveStatuses() {
return config.positiveStatusNames;
},

get config() {
return config;
},

get isEnable() {
return isValidConfig;
},
};
};

export const jiraService = initJiraClient();

const re = '(\\w+)-(\\d+)';

Expand Down Expand Up @@ -207,7 +213,7 @@ export const searchIssue = async (params: { value: string; limit: number }): Pro
const issueKey = extractIssueKey(params.value);

if (issueKey) {
const res = await jiraService.findIssue(issueKey);
const res = await jiraService.instance.findIssue(issueKey);
return [
{
...res,
Expand All @@ -217,7 +223,7 @@ export const searchIssue = async (params: { value: string; limit: number }): Pro
}
}

const searchResults = await jiraService.searchJira(`summary ~ "${escapeSearchString(params.value)}"`, {
const searchResults = await jiraService.instance.searchJira(`summary ~ "${escapeSearchString(params.value)}"`, {
maxResults: params.limit,
});

Expand Down
21 changes: 19 additions & 2 deletions trpc/queries/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Tag,
ExternalTask,
} from '../../generated/kysely/types';
import { calcStatusColorForExternalTask } from '../../src/utils/db/calculatedGoalsFields';

import { Activity, getUserActivity } from './activity';
import { tagQuery } from './tag';
Expand Down Expand Up @@ -59,8 +60,10 @@ export interface HistoryRecordMeta {
owner: Activity;
participants: Activity;
state: ExtractTypeFromGenerated<State>;
criteria: ExtractTypeFromGenerated<GoalAchieveCriteria> &
({ criteriaGoal: ExtendedGoal } | { externalTask: ExtractTypeFromGenerated<ExternalTask> });
criteria: ExtractTypeFromGenerated<GoalAchieveCriteria> & {
criteriaGoal: ExtendedGoal | null;
externalTask: ExtractTypeFromGenerated<ExternalTask> | null;
};
partnerProject: ExtractTypeFromGenerated<Project>;
priority: ExtractTypeFromGenerated<Priority>;
title: string;
Expand Down Expand Up @@ -262,5 +265,19 @@ export const extraDataForEachRecord = async <T extends HisrotyRecord>(
});
}

// update state colors of jira tasks in criteria changes records

historyWithMeta.forEach((record) => {
if (record.subject === 'criteria') {
if (record.nextValue?.externalTask != null) {
record.nextValue.externalTask = calcStatusColorForExternalTask(record.nextValue.externalTask);
}

if (record.previousValue?.externalTask != null) {
record.previousValue.externalTask = calcStatusColorForExternalTask(record.previousValue.externalTask);
}
}
});

return historyWithMeta;
};
Loading
Loading