redmine-devrelay-client 0.7.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/dist/attachments.d.ts +26 -1
- package/dist/attachments.js +110 -3
- package/dist/attachments.js.map +1 -1
- package/dist/client.d.ts +15 -1
- package/dist/client.js +32 -2
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts +15 -0
- package/dist/http.js +70 -0
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/issues.d.ts +2 -0
- package/dist/issues.js +34 -4
- package/dist/issues.js.map +1 -1
- package/dist/metadata.d.ts +11 -1
- package/dist/metadata.js +54 -0
- package/dist/metadata.js.map +1 -1
- package/dist/search.d.ts +11 -0
- package/dist/search.js +89 -0
- package/dist/search.js.map +1 -0
- package/dist/timeEntries.d.ts +33 -0
- package/dist/timeEntries.js +124 -0
- package/dist/timeEntries.js.map +1 -0
- package/dist/types.d.ts +177 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/dist/writes.js +6 -0
- package/dist/writes.js.map +1 -1
- package/package.json +1 -1
package/dist/metadata.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RedmineHttp } from "./http.js";
|
|
2
|
-
import type { IssueCategoryOption, IssuePriorityOption, IssueStatusOption, ProjectVersionOption, RedmineNamed } from "./types.js";
|
|
2
|
+
import type { IssueCategoryOption, ListIssueCustomFieldsResult, IssuePriorityOption, IssueStatusOption, ProjectVersionOption, RedmineNamed } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Match `name` against a list of id/name options: exact (case- and
|
|
5
5
|
* space-insensitive) first, then substring. Same rules as matchMemberByName.
|
|
@@ -15,3 +15,13 @@ export declare function listIssuePriorities(http: RedmineHttp): Promise<IssuePri
|
|
|
15
15
|
export declare function listProjectVersions(http: RedmineHttp, projectId: number): Promise<ProjectVersionOption[]>;
|
|
16
16
|
/** 범주 (GET /projects/:id/issue_categories.json) */
|
|
17
17
|
export declare function listIssueCategories(http: RedmineHttp, projectId: number): Promise<IssueCategoryOption[]>;
|
|
18
|
+
/**
|
|
19
|
+
* 프로젝트에서 쓸 수 있는 일감 사용자 정의 필드 (id + 이름).
|
|
20
|
+
*
|
|
21
|
+
* 1. GET /projects/:id.json?include=issue_custom_fields — Redmine 4.2+.
|
|
22
|
+
* 키가 오면 그것이 답이다 (빈 배열이면 정말 없는 것).
|
|
23
|
+
* 2. GET /custom_fields.json — 관리자 키에서만. 401/403이면 건너뛴다.
|
|
24
|
+
* 3. 프로젝트 최근 이슈 100건의 custom_fields를 합친다. 어떤 버전·권한에서도
|
|
25
|
+
* 되지만, 값이 한 번도 안 붙은 필드는 빠질 수 있다 (source: "issues").
|
|
26
|
+
*/
|
|
27
|
+
export declare function listProjectIssueCustomFields(http: RedmineHttp, projectId: number): Promise<ListIssueCustomFieldsResult>;
|
package/dist/metadata.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RedmineError } from "./errors.js";
|
|
1
2
|
function compact(s) {
|
|
2
3
|
return s.toLowerCase().replace(/\s+/g, "");
|
|
3
4
|
}
|
|
@@ -54,4 +55,57 @@ export async function listIssueCategories(http, projectId) {
|
|
|
54
55
|
assignedTo: c.assigned_to ?? null,
|
|
55
56
|
}));
|
|
56
57
|
}
|
|
58
|
+
function named(f) {
|
|
59
|
+
return { id: f.id, name: f.name };
|
|
60
|
+
}
|
|
61
|
+
function isAuthOrPermissionError(err) {
|
|
62
|
+
return (err instanceof RedmineError &&
|
|
63
|
+
(err.code === "REDMINE_PERMISSION_DENIED" ||
|
|
64
|
+
err.code === "REDMINE_AUTHENTICATION_ERROR"));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 프로젝트에서 쓸 수 있는 일감 사용자 정의 필드 (id + 이름).
|
|
68
|
+
*
|
|
69
|
+
* 1. GET /projects/:id.json?include=issue_custom_fields — Redmine 4.2+.
|
|
70
|
+
* 키가 오면 그것이 답이다 (빈 배열이면 정말 없는 것).
|
|
71
|
+
* 2. GET /custom_fields.json — 관리자 키에서만. 401/403이면 건너뛴다.
|
|
72
|
+
* 3. 프로젝트 최근 이슈 100건의 custom_fields를 합친다. 어떤 버전·권한에서도
|
|
73
|
+
* 되지만, 값이 한 번도 안 붙은 필드는 빠질 수 있다 (source: "issues").
|
|
74
|
+
*/
|
|
75
|
+
export async function listProjectIssueCustomFields(http, projectId) {
|
|
76
|
+
const project = await http.getJson(`/projects/${projectId}.json`, { include: "issue_custom_fields" });
|
|
77
|
+
const fromProject = project?.project?.issue_custom_fields;
|
|
78
|
+
if (Array.isArray(fromProject)) {
|
|
79
|
+
return { fields: fromProject.map(named), source: "project" };
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const admin = await http.getJson("/custom_fields.json");
|
|
83
|
+
const fields = (admin?.custom_fields ?? [])
|
|
84
|
+
.filter((f) => f.customized_type === "issue")
|
|
85
|
+
// projects가 없으면 판단할 수 없으니 넣어 둔다 (빠지는 것보다 낫다)
|
|
86
|
+
.filter((f) => f.is_for_all ||
|
|
87
|
+
!f.projects ||
|
|
88
|
+
f.projects.some((p) => p.id === projectId))
|
|
89
|
+
.map(named);
|
|
90
|
+
return { fields, source: "custom_fields" };
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (!isAuthOrPermissionError(err))
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
const sample = await http.getJson("/issues.json", {
|
|
97
|
+
project_id: projectId,
|
|
98
|
+
status_id: "*",
|
|
99
|
+
limit: 100,
|
|
100
|
+
sort: "updated_on:desc",
|
|
101
|
+
});
|
|
102
|
+
const seen = new Map();
|
|
103
|
+
for (const issue of sample?.issues ?? []) {
|
|
104
|
+
for (const f of issue.custom_fields ?? []) {
|
|
105
|
+
if (!seen.has(f.id))
|
|
106
|
+
seen.set(f.id, named(f));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { fields: [...seen.values()], source: "issues" };
|
|
110
|
+
}
|
|
57
111
|
//# sourceMappingURL=metadata.js.map
|
package/dist/metadata.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAc3C,SAAS,OAAO,CAAC,CAAS;IACxB,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAU,EACV,IAAY;IAEZ,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACzD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,8BAA8B;AAC9B,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAC7B,gBAAgB,CACjB,CAAC;IACF,OAAO,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,oCAAoC;AACpC,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAE5B,sBAAsB,CAAC,CAAC;IAC3B,OAAO,CAAC,IAAI,EAAE,cAAc,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9C,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;KAC/B,CAAC,CAAC,CAAC;AACN,CAAC;AAED,qDAAqD;AACrD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAE5B,qCAAqC,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,EAAE,gBAAgB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChD,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC;KACjC,CAAC,CAAC,CAAC;AACN,CAAC;AAED,8CAA8C;AAC9C,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAiB,EACjB,SAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAI5B,aAAa,SAAS,gBAAgB,CAAC,CAAC;IAC3C,OAAO,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACxC,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,MAAM;QAC1B,OAAO,EAAE,CAAC,CAAC,QAAQ,IAAI,IAAI;KAC5B,CAAC,CAAC,CAAC;AACN,CAAC;AAED,mDAAmD;AACnD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAiB,EACjB,SAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAE5B,aAAa,SAAS,wBAAwB,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,EAAE,gBAAgB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChD,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,UAAU,EAAE,CAAC,CAAC,WAAW,IAAI,IAAI;KAClC,CAAC,CAAC,CAAC;AACN,CAAC;AAED,SAAS,KAAK,CAAC,CAAW;IACxB,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AACpC,CAAC;AAED,SAAS,uBAAuB,CAAC,GAAY;IAC3C,OAAO,CACL,GAAG,YAAY,YAAY;QAC3B,CAAC,GAAG,CAAC,IAAI,KAAK,2BAA2B;YACvC,GAAG,CAAC,IAAI,KAAK,8BAA8B,CAAC,CAC/C,CAAC;AACJ,CAAC;AAQD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,IAAiB,EACjB,SAAiB;IAEjB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAE/B,aAAa,SAAS,OAAO,EAAE,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC;IACtE,MAAM,WAAW,GAAG,OAAO,EAAE,OAAO,EAAE,mBAAmB,CAAC;IAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC/D,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAC9B,qBAAqB,CACtB,CAAC;QACF,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,aAAa,IAAI,EAAE,CAAC;aACxC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe,KAAK,OAAO,CAAC;YAC7C,6CAA6C;aAC5C,MAAM,CACL,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,CAAC,QAAQ;YACX,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,CAAC,CAC7C;aACA,GAAG,CAAC,KAAK,CAAC,CAAC;QACd,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC7C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC;YAAE,MAAM,GAAG,CAAC;IAC/C,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAE9B,cAAc,EAAE;QACjB,UAAU,EAAE,SAAS;QACrB,SAAS,EAAE,GAAG;QACd,KAAK,EAAE,GAAG;QACV,IAAI,EAAE,iBAAiB;KACxB,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkC,CAAC;IACvD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,MAAM,IAAI,EAAE,EAAE,CAAC;QACzC,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC;YAC1C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC1D,CAAC"}
|
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RedmineConfig } from "./config.js";
|
|
2
|
+
import type { RedmineHttp } from "./http.js";
|
|
3
|
+
import type { SearchTextInput, SearchTextResult } from "./types.js";
|
|
4
|
+
export declare const SEARCH_TEXT_TYPES: readonly ["issues", "news", "documents", "changesets", "wiki_pages", "messages", "projects"];
|
|
5
|
+
export declare function buildSearchTextQuery(input: SearchTextInput): Record<string, string | number>;
|
|
6
|
+
/**
|
|
7
|
+
* 전문 검색 (GET /search.json, Redmine 3.3+). 프로젝트를 주면
|
|
8
|
+
* /projects/:id/search.json 으로 범위를 좁힌다. 구버전 Redmine은 이 경로가 없어
|
|
9
|
+
* 로그인 페이지로 튕기며 401/404가 오는데, 그건 키 문제가 아니므로 안내를 바꿔 준다.
|
|
10
|
+
*/
|
|
11
|
+
export declare function searchText(http: RedmineHttp, config: RedmineConfig, input: SearchTextInput): Promise<SearchTextResult>;
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { RedmineError } from "./errors.js";
|
|
2
|
+
export const SEARCH_TEXT_TYPES = [
|
|
3
|
+
"issues",
|
|
4
|
+
"news",
|
|
5
|
+
"documents",
|
|
6
|
+
"changesets",
|
|
7
|
+
"wiki_pages",
|
|
8
|
+
"messages",
|
|
9
|
+
"projects",
|
|
10
|
+
];
|
|
11
|
+
export function buildSearchTextQuery(input) {
|
|
12
|
+
const query = { q: input.query.trim() };
|
|
13
|
+
const types = input.types?.length ? input.types : ["issues"];
|
|
14
|
+
for (const t of types)
|
|
15
|
+
query[t] = 1;
|
|
16
|
+
if (input.titlesOnly)
|
|
17
|
+
query.titles_only = 1;
|
|
18
|
+
if (input.openIssuesOnly)
|
|
19
|
+
query.open_issues = 1;
|
|
20
|
+
if (input.allWords !== false)
|
|
21
|
+
query.all_words = 1;
|
|
22
|
+
if (input.includeAttachments)
|
|
23
|
+
query.attachments = 1;
|
|
24
|
+
return query;
|
|
25
|
+
}
|
|
26
|
+
function normalizeRow(raw) {
|
|
27
|
+
return {
|
|
28
|
+
id: raw.id,
|
|
29
|
+
title: raw.title ?? "",
|
|
30
|
+
type: raw.type ?? "",
|
|
31
|
+
url: raw.url ?? "",
|
|
32
|
+
description: raw.description ?? "",
|
|
33
|
+
datetime: raw.datetime ?? null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 전문 검색 (GET /search.json, Redmine 3.3+). 프로젝트를 주면
|
|
38
|
+
* /projects/:id/search.json 으로 범위를 좁힌다. 구버전 Redmine은 이 경로가 없어
|
|
39
|
+
* 로그인 페이지로 튕기며 401/404가 오는데, 그건 키 문제가 아니므로 안내를 바꿔 준다.
|
|
40
|
+
*/
|
|
41
|
+
export async function searchText(http, config, input) {
|
|
42
|
+
const q = input.query.trim();
|
|
43
|
+
if (!q) {
|
|
44
|
+
throw new RedmineError({
|
|
45
|
+
code: "REDMINE_VALIDATION_ERROR",
|
|
46
|
+
message: "query must be non-empty",
|
|
47
|
+
check: ["Pass the words to search for"],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const limit = Math.min(input.limit ?? 25, config.maxResultCount, 100);
|
|
51
|
+
const offset = input.offset ?? 0;
|
|
52
|
+
const path = input.projectId !== undefined
|
|
53
|
+
? `/projects/${input.projectId}/search.json`
|
|
54
|
+
: "/search.json";
|
|
55
|
+
let data;
|
|
56
|
+
try {
|
|
57
|
+
data = await http.getJson(path, {
|
|
58
|
+
...buildSearchTextQuery(input),
|
|
59
|
+
limit,
|
|
60
|
+
offset,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err instanceof RedmineError &&
|
|
65
|
+
(err.httpStatus === 401 || err.httpStatus === 404)) {
|
|
66
|
+
throw new RedmineError({
|
|
67
|
+
code: "REDMINE_UNKNOWN_ERROR",
|
|
68
|
+
message: "Full-text search is not available on this Redmine (the /search.json API needs Redmine 3.3+ and the search module)",
|
|
69
|
+
httpStatus: err.httpStatus,
|
|
70
|
+
retrySafe: false,
|
|
71
|
+
check: [
|
|
72
|
+
"Use redmine_search_issues with subjectContains instead",
|
|
73
|
+
"If other calls also fail, check REDMINE_API_KEY",
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
const rows = (data?.results ?? []).map(normalizeRow);
|
|
80
|
+
const totalCount = data?.total_count ?? rows.length;
|
|
81
|
+
return {
|
|
82
|
+
query: q,
|
|
83
|
+
results: rows,
|
|
84
|
+
totalCount,
|
|
85
|
+
returnedCount: rows.length,
|
|
86
|
+
hasMore: offset + rows.length < totalCount,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RedmineConfig } from "./config.js";
|
|
2
|
+
import type { RedmineHttp } from "./http.js";
|
|
3
|
+
import type { CreateTimeEntryInput, CreateTimeEntryResult, ListTimeEntriesInput, ListTimeEntriesResult, TimeEntry, TimeEntryActivityOption } from "./types.js";
|
|
4
|
+
type RawNamed = {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
type RawTimeEntry = {
|
|
9
|
+
id: number;
|
|
10
|
+
project?: RawNamed | null;
|
|
11
|
+
issue?: {
|
|
12
|
+
id: number;
|
|
13
|
+
} | null;
|
|
14
|
+
user?: RawNamed | null;
|
|
15
|
+
activity?: RawNamed | null;
|
|
16
|
+
hours?: number | string | null;
|
|
17
|
+
comments?: string | null;
|
|
18
|
+
spent_on?: string | null;
|
|
19
|
+
created_on?: string | null;
|
|
20
|
+
updated_on?: string | null;
|
|
21
|
+
};
|
|
22
|
+
export declare function normalizeTimeEntry(raw: RawTimeEntry): TimeEntry;
|
|
23
|
+
/**
|
|
24
|
+
* Redmine의 spent_on 필터 문법: 둘 다 있으면 `><from|to`, 하나면 `>=from` / `<=to`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildTimeEntryQuery(input: ListTimeEntriesInput): Record<string, string | number>;
|
|
27
|
+
/** 작업시간 목록 (GET /time_entries.json) — 최근 날짜부터, 페이지를 이어 받는다 */
|
|
28
|
+
export declare function listTimeEntries(http: RedmineHttp, config: RedmineConfig, input?: ListTimeEntriesInput): Promise<ListTimeEntriesResult>;
|
|
29
|
+
/** 작업시간 기록 (POST /time_entries.json) */
|
|
30
|
+
export declare function createTimeEntry(http: RedmineHttp, input: CreateTimeEntryInput): Promise<CreateTimeEntryResult>;
|
|
31
|
+
/** 작업 분류(활동) 목록 (GET /enumerations/time_entry_activities.json) */
|
|
32
|
+
export declare function listTimeEntryActivities(http: RedmineHttp): Promise<TimeEntryActivityOption[]>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { RedmineError } from "./errors.js";
|
|
2
|
+
function toHours(value) {
|
|
3
|
+
const n = typeof value === "string" ? Number(value) : value ?? 0;
|
|
4
|
+
return Number.isFinite(n) ? n : 0;
|
|
5
|
+
}
|
|
6
|
+
export function normalizeTimeEntry(raw) {
|
|
7
|
+
return {
|
|
8
|
+
id: raw.id,
|
|
9
|
+
project: raw.project ?? null,
|
|
10
|
+
issue: raw.issue ?? null,
|
|
11
|
+
user: raw.user ?? null,
|
|
12
|
+
activity: raw.activity ?? null,
|
|
13
|
+
hours: toHours(raw.hours),
|
|
14
|
+
comments: raw.comments ?? "",
|
|
15
|
+
spentOn: raw.spent_on ?? "",
|
|
16
|
+
createdOn: raw.created_on ?? null,
|
|
17
|
+
updatedOn: raw.updated_on ?? null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Redmine의 spent_on 필터 문법: 둘 다 있으면 `><from|to`, 하나면 `>=from` / `<=to`.
|
|
22
|
+
*/
|
|
23
|
+
export function buildTimeEntryQuery(input) {
|
|
24
|
+
const query = {};
|
|
25
|
+
if (input.issueId !== undefined)
|
|
26
|
+
query.issue_id = input.issueId;
|
|
27
|
+
if (input.projectId !== undefined)
|
|
28
|
+
query.project_id = input.projectId;
|
|
29
|
+
if (input.userId !== undefined)
|
|
30
|
+
query.user_id = String(input.userId);
|
|
31
|
+
if (input.activityId !== undefined)
|
|
32
|
+
query.activity_id = input.activityId;
|
|
33
|
+
if (input.spentFrom && input.spentTo) {
|
|
34
|
+
query.spent_on = `><${input.spentFrom}|${input.spentTo}`;
|
|
35
|
+
}
|
|
36
|
+
else if (input.spentFrom) {
|
|
37
|
+
query.spent_on = `>=${input.spentFrom}`;
|
|
38
|
+
}
|
|
39
|
+
else if (input.spentTo) {
|
|
40
|
+
query.spent_on = `<=${input.spentTo}`;
|
|
41
|
+
}
|
|
42
|
+
query.sort = "spent_on:desc";
|
|
43
|
+
return query;
|
|
44
|
+
}
|
|
45
|
+
/** 작업시간 목록 (GET /time_entries.json) — 최근 날짜부터, 페이지를 이어 받는다 */
|
|
46
|
+
export async function listTimeEntries(http, config, input = {}) {
|
|
47
|
+
const wanted = Math.min(input.limit ?? config.maxResultCount, config.maxResultCount);
|
|
48
|
+
const baseQuery = buildTimeEntryQuery(input);
|
|
49
|
+
const startOffset = input.offset ?? 0;
|
|
50
|
+
const collected = [];
|
|
51
|
+
let offset = startOffset;
|
|
52
|
+
let totalCount = 0;
|
|
53
|
+
while (collected.length < wanted) {
|
|
54
|
+
const pageLimit = Math.min(100, wanted - collected.length);
|
|
55
|
+
const page = await http.getJson("/time_entries.json", { ...baseQuery, limit: pageLimit, offset });
|
|
56
|
+
totalCount = page.total_count;
|
|
57
|
+
const rows = page.time_entries ?? [];
|
|
58
|
+
collected.push(...rows.map(normalizeTimeEntry));
|
|
59
|
+
offset += rows.length;
|
|
60
|
+
if (rows.length === 0 || offset >= totalCount)
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const totalHours = collected.reduce((sum, e) => sum + e.hours, 0);
|
|
64
|
+
return {
|
|
65
|
+
entries: collected,
|
|
66
|
+
totalCount,
|
|
67
|
+
returnedCount: collected.length,
|
|
68
|
+
hasMore: startOffset + collected.length < totalCount,
|
|
69
|
+
// 부동소수 합계를 사람이 읽을 수 있게 소수 둘째 자리까지
|
|
70
|
+
totalHours: Math.round(totalHours * 100) / 100,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** 작업시간 기록 (POST /time_entries.json) */
|
|
74
|
+
export async function createTimeEntry(http, input) {
|
|
75
|
+
if (input.issueId === undefined && input.projectId === undefined) {
|
|
76
|
+
throw new RedmineError({
|
|
77
|
+
code: "REDMINE_VALIDATION_ERROR",
|
|
78
|
+
message: "A time entry needs issueId or projectId",
|
|
79
|
+
check: ["Pass the issue id (preferred) or a project id"],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!(input.hours > 0)) {
|
|
83
|
+
throw new RedmineError({
|
|
84
|
+
code: "REDMINE_VALIDATION_ERROR",
|
|
85
|
+
message: `hours must be positive (got ${input.hours})`,
|
|
86
|
+
check: ["Pass hours as a positive number, e.g. 1.5"],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const timeEntry = { hours: input.hours };
|
|
90
|
+
if (input.issueId !== undefined)
|
|
91
|
+
timeEntry.issue_id = input.issueId;
|
|
92
|
+
else
|
|
93
|
+
timeEntry.project_id = input.projectId;
|
|
94
|
+
if (input.spentOn !== undefined)
|
|
95
|
+
timeEntry.spent_on = input.spentOn;
|
|
96
|
+
if (input.activityId !== undefined)
|
|
97
|
+
timeEntry.activity_id = input.activityId;
|
|
98
|
+
if (input.comments !== undefined)
|
|
99
|
+
timeEntry.comments = input.comments;
|
|
100
|
+
const data = await http.postJson("/time_entries.json", { time_entry: timeEntry });
|
|
101
|
+
if (!data?.time_entry) {
|
|
102
|
+
throw new Error("Redmine createTimeEntry returned empty body");
|
|
103
|
+
}
|
|
104
|
+
const entry = normalizeTimeEntry(data.time_entry);
|
|
105
|
+
return {
|
|
106
|
+
id: entry.id,
|
|
107
|
+
hours: entry.hours,
|
|
108
|
+
spentOn: entry.spentOn,
|
|
109
|
+
issueId: entry.issue?.id ?? input.issueId ?? null,
|
|
110
|
+
projectId: entry.project?.id ?? input.projectId ?? null,
|
|
111
|
+
activity: entry.activity,
|
|
112
|
+
comments: entry.comments,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** 작업 분류(활동) 목록 (GET /enumerations/time_entry_activities.json) */
|
|
116
|
+
export async function listTimeEntryActivities(http) {
|
|
117
|
+
const data = await http.getJson("/enumerations/time_entry_activities.json");
|
|
118
|
+
return (data?.time_entry_activities ?? []).map((a) => ({
|
|
119
|
+
id: a.id,
|
|
120
|
+
name: a.name,
|
|
121
|
+
isDefault: Boolean(a.is_default),
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=timeEntries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeEntries.js","sourceRoot":"","sources":["../src/timeEntries.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -40,6 +40,87 @@ export type ProjectVersionOption = RedmineNamed & {
|
|
|
40
40
|
export type IssueCategoryOption = RedmineNamed & {
|
|
41
41
|
assignedTo: RedmineNamed | null;
|
|
42
42
|
};
|
|
43
|
+
/** 프로젝트에 켜진 일감 사용자 정의 필드 (id + 이름) */
|
|
44
|
+
export type IssueCustomFieldOption = RedmineNamed;
|
|
45
|
+
export type ListIssueCustomFieldsResult = {
|
|
46
|
+
fields: IssueCustomFieldOption[];
|
|
47
|
+
/**
|
|
48
|
+
* project — GET /projects/:id.json?include=issue_custom_fields (Redmine 4.2+)
|
|
49
|
+
* custom_fields — GET /custom_fields.json (관리자 키)
|
|
50
|
+
* issues — 프로젝트 최근 이슈에서 합친 것. 값이 한 번도 안 붙은 필드는 빠질 수 있음
|
|
51
|
+
*/
|
|
52
|
+
source: "project" | "custom_fields" | "issues";
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* 쓰기용 사용자 정의 필드 값. 문자열, 다중 선택이면 문자열 배열,
|
|
56
|
+
* 빈 문자열이면 비움 (Redmine은 ""를 비우기로 해석).
|
|
57
|
+
*/
|
|
58
|
+
export type IssueCustomFieldValue = string | string[];
|
|
59
|
+
export type IssueCustomFieldWrite = {
|
|
60
|
+
id: number;
|
|
61
|
+
value: IssueCustomFieldValue;
|
|
62
|
+
};
|
|
63
|
+
/** 작업 분류(활동) — time_entry_activities */
|
|
64
|
+
export type TimeEntryActivityOption = RedmineNamed & {
|
|
65
|
+
isDefault: boolean;
|
|
66
|
+
};
|
|
67
|
+
/** 작업시간 한 건 (GET /time_entries.json 행) */
|
|
68
|
+
export type TimeEntry = {
|
|
69
|
+
id: number;
|
|
70
|
+
project: RedmineNamed | null;
|
|
71
|
+
issue: {
|
|
72
|
+
id: number;
|
|
73
|
+
} | null;
|
|
74
|
+
user: RedmineNamed | null;
|
|
75
|
+
activity: RedmineNamed | null;
|
|
76
|
+
hours: number;
|
|
77
|
+
comments: string;
|
|
78
|
+
/** YYYY-MM-DD */
|
|
79
|
+
spentOn: string;
|
|
80
|
+
createdOn: string | null;
|
|
81
|
+
updatedOn: string | null;
|
|
82
|
+
};
|
|
83
|
+
export type ListTimeEntriesInput = {
|
|
84
|
+
issueId?: number;
|
|
85
|
+
projectId?: number;
|
|
86
|
+
/** "me" 또는 user id */
|
|
87
|
+
userId?: "me" | number;
|
|
88
|
+
/** YYYY-MM-DD (포함) */
|
|
89
|
+
spentFrom?: string;
|
|
90
|
+
/** YYYY-MM-DD (포함) */
|
|
91
|
+
spentTo?: string;
|
|
92
|
+
activityId?: number;
|
|
93
|
+
limit?: number;
|
|
94
|
+
offset?: number;
|
|
95
|
+
};
|
|
96
|
+
export type ListTimeEntriesResult = {
|
|
97
|
+
entries: TimeEntry[];
|
|
98
|
+
totalCount: number;
|
|
99
|
+
returnedCount: number;
|
|
100
|
+
hasMore: boolean;
|
|
101
|
+
/** 돌려준 행들의 hours 합 (전체 합이 아님 — hasMore면 더 있다) */
|
|
102
|
+
totalHours: number;
|
|
103
|
+
};
|
|
104
|
+
export type CreateTimeEntryInput = {
|
|
105
|
+
/** 둘 중 하나는 필수. 일감이 있으면 일감을 우선 */
|
|
106
|
+
issueId?: number;
|
|
107
|
+
projectId?: number;
|
|
108
|
+
hours: number;
|
|
109
|
+
/** YYYY-MM-DD, 생략하면 Redmine이 오늘로 */
|
|
110
|
+
spentOn?: string;
|
|
111
|
+
/** 활동 id. Redmine에 기본 활동이 없으면 필수 */
|
|
112
|
+
activityId?: number;
|
|
113
|
+
comments?: string;
|
|
114
|
+
};
|
|
115
|
+
export type CreateTimeEntryResult = {
|
|
116
|
+
id: number;
|
|
117
|
+
hours: number;
|
|
118
|
+
spentOn: string;
|
|
119
|
+
issueId: number | null;
|
|
120
|
+
projectId: number | null;
|
|
121
|
+
activity: RedmineNamed | null;
|
|
122
|
+
comments: string;
|
|
123
|
+
};
|
|
43
124
|
/** 이름 또는 id로 지정할 수 있는 필드 값 */
|
|
44
125
|
export type NamedRef = number | string;
|
|
45
126
|
export type IssueInclude = "journals" | "attachments" | "relations" | "children" | "allowed_statuses";
|
|
@@ -73,6 +154,13 @@ export type NormalizedIssueSummary = {
|
|
|
73
154
|
updatedOn: string | null;
|
|
74
155
|
createdOn: string | null;
|
|
75
156
|
};
|
|
157
|
+
/** journal 한 건의 필드 변경: property attr → name은 필드 (status_id, done_ratio, assigned_to_id …) */
|
|
158
|
+
export type JournalDetail = {
|
|
159
|
+
property: string;
|
|
160
|
+
name: string;
|
|
161
|
+
oldValue: string | null;
|
|
162
|
+
newValue: string | null;
|
|
163
|
+
};
|
|
76
164
|
export type NormalizedIssueDetail = NormalizedIssueSummary & {
|
|
77
165
|
description: string;
|
|
78
166
|
author: {
|
|
@@ -102,6 +190,8 @@ export type NormalizedIssueDetail = NormalizedIssueSummary & {
|
|
|
102
190
|
notes: string;
|
|
103
191
|
createdOn: string;
|
|
104
192
|
privateNotes: boolean;
|
|
193
|
+
/** 필드 변경 내역 (상태·진척도·담당자 …). Redmine의 journal details */
|
|
194
|
+
details: JournalDetail[];
|
|
105
195
|
}>;
|
|
106
196
|
attachments?: Array<{
|
|
107
197
|
id: number;
|
|
@@ -120,18 +210,32 @@ export type NormalizedIssueDetail = NormalizedIssueSummary & {
|
|
|
120
210
|
export type SearchIssuesInput = {
|
|
121
211
|
projectId?: number;
|
|
122
212
|
issueId?: number;
|
|
213
|
+
/** "me", user id — names are resolved by the MCP layer before reaching here */
|
|
123
214
|
assignedTo?: string | number;
|
|
124
215
|
status?: "open" | "closed" | "all" | number;
|
|
125
216
|
trackerId?: number;
|
|
126
217
|
priorityId?: number;
|
|
218
|
+
/** 대상 버전 id */
|
|
219
|
+
fixedVersionId?: number;
|
|
220
|
+
/** 범주 id */
|
|
221
|
+
categoryId?: number;
|
|
222
|
+
/** 작성자: "me" 또는 user id */
|
|
223
|
+
authorId?: "me" | number;
|
|
224
|
+
/** 일감관리자(watcher): "me" 또는 user id */
|
|
225
|
+
watcherId?: "me" | number;
|
|
127
226
|
subjectContains?: string;
|
|
128
227
|
createdAfter?: string;
|
|
228
|
+
createdBefore?: string;
|
|
129
229
|
updatedAfter?: string;
|
|
130
|
-
|
|
230
|
+
updatedBefore?: string;
|
|
231
|
+
/** 완료기한 범위 (포함) */
|
|
232
|
+
dueAfter?: string;
|
|
233
|
+
dueBefore?: string;
|
|
131
234
|
customFields?: Array<{
|
|
132
235
|
id: number;
|
|
133
236
|
value: string;
|
|
134
237
|
}>;
|
|
238
|
+
parentIssueId?: number;
|
|
135
239
|
sort?: Array<{
|
|
136
240
|
field: string;
|
|
137
241
|
direction: "asc" | "desc";
|
|
@@ -139,6 +243,38 @@ export type SearchIssuesInput = {
|
|
|
139
243
|
limit?: number;
|
|
140
244
|
offset?: number;
|
|
141
245
|
};
|
|
246
|
+
export type SearchTextType = "issues" | "news" | "documents" | "changesets" | "wiki_pages" | "messages" | "projects";
|
|
247
|
+
/** 전문 검색 (GET /search.json) */
|
|
248
|
+
export type SearchTextInput = {
|
|
249
|
+
query: string;
|
|
250
|
+
/** 있으면 /projects/:id/search.json */
|
|
251
|
+
projectId?: number;
|
|
252
|
+
/** 기본 ["issues"] */
|
|
253
|
+
types?: SearchTextType[];
|
|
254
|
+
titlesOnly?: boolean;
|
|
255
|
+
openIssuesOnly?: boolean;
|
|
256
|
+
/** 기본 true — 모든 단어가 들어간 결과만 */
|
|
257
|
+
allWords?: boolean;
|
|
258
|
+
includeAttachments?: boolean;
|
|
259
|
+
limit?: number;
|
|
260
|
+
offset?: number;
|
|
261
|
+
};
|
|
262
|
+
export type SearchTextRow = {
|
|
263
|
+
id: number;
|
|
264
|
+
title: string;
|
|
265
|
+
/** "issue", "issue closed", "wiki-page", "news" … Redmine이 주는 그대로 */
|
|
266
|
+
type: string;
|
|
267
|
+
url: string;
|
|
268
|
+
description: string;
|
|
269
|
+
datetime: string | null;
|
|
270
|
+
};
|
|
271
|
+
export type SearchTextResult = {
|
|
272
|
+
query: string;
|
|
273
|
+
results: SearchTextRow[];
|
|
274
|
+
totalCount: number;
|
|
275
|
+
returnedCount: number;
|
|
276
|
+
hasMore: boolean;
|
|
277
|
+
};
|
|
142
278
|
export type AttachmentInput = {
|
|
143
279
|
path: string;
|
|
144
280
|
filename?: string;
|
|
@@ -163,6 +299,42 @@ export type IssueUploadToken = {
|
|
|
163
299
|
};
|
|
164
300
|
export declare const ATTACHMENT_MAX_FILES = 5;
|
|
165
301
|
export declare const ATTACHMENT_MAX_BYTES: number;
|
|
302
|
+
/** 내려받기 기본 상한 (요청으로 올릴 수 있는 최대는 HARD) */
|
|
303
|
+
export declare const ATTACHMENT_DOWNLOAD_MAX_BYTES: number;
|
|
304
|
+
export declare const ATTACHMENT_DOWNLOAD_HARD_MAX_BYTES: number;
|
|
305
|
+
/** 텍스트 파일을 결과에 바로 실어 주는 상한 */
|
|
306
|
+
export declare const ATTACHMENT_TEXT_INLINE_MAX_BYTES: number;
|
|
307
|
+
/** GET /attachments/:id.json */
|
|
308
|
+
export type AttachmentInfo = {
|
|
309
|
+
id: number;
|
|
310
|
+
filename: string;
|
|
311
|
+
filesize: number;
|
|
312
|
+
/** 구버전 Redmine은 안 준다 → 내려받을 때 응답 헤더로 채움 */
|
|
313
|
+
contentType: string | null;
|
|
314
|
+
description: string;
|
|
315
|
+
contentUrl: string;
|
|
316
|
+
author: RedmineNamed | null;
|
|
317
|
+
createdOn: string | null;
|
|
318
|
+
};
|
|
319
|
+
export type DownloadAttachmentInput = {
|
|
320
|
+
attachmentId: number;
|
|
321
|
+
/** 저장 폴더. 생략하면 OS 임시 폴더 아래 redmine-devrelay/attachments/<id>/ */
|
|
322
|
+
destDir?: string;
|
|
323
|
+
/** 기본 ATTACHMENT_DOWNLOAD_MAX_BYTES, 최대 ATTACHMENT_DOWNLOAD_HARD_MAX_BYTES */
|
|
324
|
+
maxBytes?: number;
|
|
325
|
+
/** 텍스트 파일이면 내용을 함께 돌려줄지 (기본 true) */
|
|
326
|
+
inlineText?: boolean;
|
|
327
|
+
};
|
|
328
|
+
export type DownloadAttachmentResult = {
|
|
329
|
+
attachment: AttachmentInfo;
|
|
330
|
+
/** 저장된 로컬 경로 */
|
|
331
|
+
path: string;
|
|
332
|
+
sizeBytes: number;
|
|
333
|
+
contentType: string | null;
|
|
334
|
+
/** 텍스트로 판단되면 UTF-8 본문 (ATTACHMENT_TEXT_INLINE_MAX_BYTES까지) */
|
|
335
|
+
text?: string;
|
|
336
|
+
textTruncated?: boolean;
|
|
337
|
+
};
|
|
166
338
|
/** Redmine relation_type values (연결된 일감 관계 종류) */
|
|
167
339
|
export declare const ISSUE_RELATION_TYPES: readonly ["relates", "duplicates", "duplicated", "blocks", "blocked", "precedes", "follows", "copied_to", "copied_from"];
|
|
168
340
|
export type IssueRelationType = (typeof ISSUE_RELATION_TYPES)[number];
|
|
@@ -222,6 +394,8 @@ export type CreateIssueInput = {
|
|
|
222
394
|
fixedVersionId?: number;
|
|
223
395
|
/** 범주 (category_id) */
|
|
224
396
|
categoryId?: number;
|
|
397
|
+
/** 사용자 정의 필드 (custom_fields) — 프로젝트에 켜진 필드만 */
|
|
398
|
+
customFields?: IssueCustomFieldWrite[];
|
|
225
399
|
/** "me", numeric user id (담당자) */
|
|
226
400
|
assignedTo?: "me" | number;
|
|
227
401
|
/** 일감관리자 — Redmine watcher_user_ids */
|
|
@@ -254,6 +428,8 @@ export type UpdateIssueInput = {
|
|
|
254
428
|
fixedVersionId?: number | null;
|
|
255
429
|
/** 범주 — number sets it, null clears it */
|
|
256
430
|
categoryId?: number | null;
|
|
431
|
+
/** 사용자 정의 필드 — 넘긴 필드만 바뀜, value ""면 비움 */
|
|
432
|
+
customFields?: IssueCustomFieldWrite[];
|
|
257
433
|
assignedTo?: "me" | number;
|
|
258
434
|
/** replace-all when provided (including empty) */
|
|
259
435
|
watcherUserIds?: number[];
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export const ATTACHMENT_MAX_FILES = 5;
|
|
2
2
|
export const ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
3
|
+
/** 내려받기 기본 상한 (요청으로 올릴 수 있는 최대는 HARD) */
|
|
4
|
+
export const ATTACHMENT_DOWNLOAD_MAX_BYTES = 10 * 1024 * 1024;
|
|
5
|
+
export const ATTACHMENT_DOWNLOAD_HARD_MAX_BYTES = 50 * 1024 * 1024;
|
|
6
|
+
/** 텍스트 파일을 결과에 바로 실어 주는 상한 */
|
|
7
|
+
export const ATTACHMENT_TEXT_INLINE_MAX_BYTES = 200 * 1024;
|
|
3
8
|
/** Redmine relation_type values (연결된 일감 관계 종류) */
|
|
4
9
|
export const ISSUE_RELATION_TYPES = [
|
|
5
10
|
"relates",
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAwSA,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AACtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAErD,yCAAyC;AACzC,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAC9D,MAAM,CAAC,MAAM,kCAAkC,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AACnE,8BAA8B;AAC9B,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,GAAG,IAAI,CAAC;AAoC3D,kDAAkD;AAClD,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,SAAS;IACT,YAAY;IACZ,YAAY;IACZ,QAAQ;IACR,SAAS;IACT,UAAU;IACV,SAAS;IACT,WAAW;IACX,aAAa;CACL,CAAC;AAIX,mDAAmD;AACnD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,UAAU,EAAE,SAAS,CAAU,CAAC"}
|
package/dist/writes.js
CHANGED
|
@@ -29,6 +29,12 @@ function applyOptionalIssueFields(issue, input, opts = {}) {
|
|
|
29
29
|
if (input.categoryId !== undefined) {
|
|
30
30
|
issue.category_id = input.categoryId === null ? "" : input.categoryId;
|
|
31
31
|
}
|
|
32
|
+
if (input.customFields !== undefined && input.customFields.length > 0) {
|
|
33
|
+
issue.custom_fields = input.customFields.map((f) => ({
|
|
34
|
+
id: f.id,
|
|
35
|
+
value: f.value,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
32
38
|
if (input.assignedTo !== undefined)
|
|
33
39
|
issue.assigned_to_id = input.assignedTo;
|
|
34
40
|
if (input.watcherUserIds !== undefined) {
|