@things-factory/inquiry 0.0.0-stage → 10.1.251
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/client/bootstrap.ts +27 -0
- package/client/components/inquiry-thread.ts +277 -0
- package/client/components/inquiry-todo.ts +36 -0
- package/client/components/inquiry-writer.ts +208 -0
- package/client/index.ts +2 -0
- package/client/inquiry-watch.ts +45 -0
- package/client/pages/inquiry-list.ts +218 -0
- package/client/route.ts +8 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/inquiry-api.ts +103 -0
- package/client/utils/inquiry-badge.ts +46 -0
- package/client/utils/inquiry-words.ts +26 -0
- package/dist-client/bootstrap.d.ts +2 -0
- package/dist-client/bootstrap.js +25 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/components/inquiry-thread.d.ts +28 -0
- package/dist-client/components/inquiry-thread.js +303 -0
- package/dist-client/components/inquiry-thread.js.map +1 -0
- package/dist-client/components/inquiry-todo.d.ts +9 -0
- package/dist-client/components/inquiry-todo.js +43 -0
- package/dist-client/components/inquiry-todo.js.map +1 -0
- package/dist-client/components/inquiry-writer.d.ts +23 -0
- package/dist-client/components/inquiry-writer.js +230 -0
- package/dist-client/components/inquiry-writer.js.map +1 -0
- package/dist-client/index.d.ts +2 -0
- package/dist-client/index.js +3 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/inquiry-watch.d.ts +2 -0
- package/dist-client/inquiry-watch.js +41 -0
- package/dist-client/inquiry-watch.js.map +1 -0
- package/dist-client/pages/inquiry-list.d.ts +56 -0
- package/dist-client/pages/inquiry-list.js +241 -0
- package/dist-client/pages/inquiry-list.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +9 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-client/utils/inquiry-api.d.ts +73 -0
- package/dist-client/utils/inquiry-api.js +34 -0
- package/dist-client/utils/inquiry-api.js.map +1 -0
- package/dist-client/utils/inquiry-badge.d.ts +4 -0
- package/dist-client/utils/inquiry-badge.js +47 -0
- package/dist-client/utils/inquiry-badge.js.map +1 -0
- package/dist-client/utils/inquiry-words.d.ts +13 -0
- package/dist-client/utils/inquiry-words.js +22 -0
- package/dist-client/utils/inquiry-words.js.map +1 -0
- package/dist-server/attachment-reader.d.ts +1 -0
- package/dist-server/attachment-reader.js +21 -0
- package/dist-server/attachment-reader.js.map +1 -0
- package/dist-server/index.d.ts +2 -0
- package/dist-server/index.js +22 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/inquiry-activity.d.ts +38 -0
- package/dist-server/inquiry-activity.js +38 -0
- package/dist-server/inquiry-activity.js.map +1 -0
- package/dist-server/role-tasks.d.ts +2 -0
- package/dist-server/role-tasks.js +14 -0
- package/dist-server/role-tasks.js.map +1 -0
- package/dist-server/service/index.d.ts +7 -0
- package/dist-server/service/index.js +24 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/inquiry/index.d.ts +4 -0
- package/dist-server/service/inquiry/index.js +8 -0
- package/dist-server/service/inquiry/index.js.map +1 -0
- package/dist-server/service/inquiry/inquiry-resolver.d.ts +29 -0
- package/dist-server/service/inquiry/inquiry-resolver.js +407 -0
- package/dist-server/service/inquiry/inquiry-resolver.js.map +1 -0
- package/dist-server/service/inquiry/inquiry-rules.d.ts +88 -0
- package/dist-server/service/inquiry/inquiry-rules.js +157 -0
- package/dist-server/service/inquiry/inquiry-rules.js.map +1 -0
- package/dist-server/service/inquiry/inquiry-service.d.ts +35 -0
- package/dist-server/service/inquiry/inquiry-service.js +143 -0
- package/dist-server/service/inquiry/inquiry-service.js.map +1 -0
- package/dist-server/service/inquiry/inquiry-types.d.ts +18 -0
- package/dist-server/service/inquiry/inquiry-types.js +69 -0
- package/dist-server/service/inquiry/inquiry-types.js.map +1 -0
- package/dist-server/service/inquiry/inquiry.d.ts +48 -0
- package/dist-server/service/inquiry/inquiry.js +232 -0
- package/dist-server/service/inquiry/inquiry.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +51 -4
- package/server/attachment-reader.ts +18 -0
- package/server/index.ts +18 -0
- package/server/inquiry-activity.ts +35 -0
- package/server/role-tasks.ts +13 -0
- package/server/service/index.ts +19 -0
- package/server/service/inquiry/index.ts +5 -0
- package/server/service/inquiry/inquiry-resolver.ts +321 -0
- package/server/service/inquiry/inquiry-rules.ts +173 -0
- package/server/service/inquiry/inquiry-service.ts +143 -0
- package/server/service/inquiry/inquiry-types.ts +45 -0
- package/server/service/inquiry/inquiry.ts +180 -0
- package/server/tsconfig.json +11 -0
- package/test/inquiry-persistence.test.ts +218 -0
- package/test/inquiry-rules.test.ts +121 -0
- package/test/open-doors.test.ts +44 -0
- package/test/translations.test.ts +53 -0
- package/things-factory.config.js +8 -0
- package/translations/en.json +93 -0
- package/translations/es.json +93 -0
- package/translations/ja.json +93 -0
- package/translations/ko.json +93 -0
- package/translations/ms.json +93 -0
- package/translations/zh.json +93 -0
- package/README.md +0 -3
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* The rules of an inquiry (ADR-0184 decisions 1-8) - what may be written, who reads and answers it, where it stands
|
|
4
|
+
* after a reply or a move, whose turn it is, and what its notice says.
|
|
5
|
+
*
|
|
6
|
+
* Pure - the rules are tested without a database. The doors (inquiry-resolver.ts) ask them.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.viewAddressOf = exports.handlersTurn = exports.HANDLERS_TURN = exports.MAX_PAGE_URL = exports.MAX_BODY = exports.MAX_TITLE = exports.INQUIRY_VISIBILITIES = exports.INQUIRY_STATUSES = exports.INQUIRY_KINDS = void 0;
|
|
10
|
+
exports.inputRefusal = inputRefusal;
|
|
11
|
+
exports.replyRefusal = replyRefusal;
|
|
12
|
+
exports.mayRead = mayRead;
|
|
13
|
+
exports.replyRefusalFor = replyRefusalFor;
|
|
14
|
+
exports.sideOf = sideOf;
|
|
15
|
+
exports.statusAfterReply = statusAfterReply;
|
|
16
|
+
exports.answerRefusal = answerRefusal;
|
|
17
|
+
exports.moveRefusal = moveRefusal;
|
|
18
|
+
exports.newForAsker = newForAsker;
|
|
19
|
+
exports.badgeOf = badgeOf;
|
|
20
|
+
exports.plainSummary = plainSummary;
|
|
21
|
+
exports.noticeWordsOf = noticeWordsOf;
|
|
22
|
+
/** The kinds of inquiry - a question, a bug report, a feature proposal (`inquiry.kind.<code>`). */
|
|
23
|
+
exports.INQUIRY_KINDS = ['QUESTION', 'BUG', 'PROPOSAL'];
|
|
24
|
+
/** Where an inquiry stands (`inquiry.status.<code>`). PLANNED and DECLINED are a proposal's only. */
|
|
25
|
+
exports.INQUIRY_STATUSES = ['RECEIVED', 'IN_PROGRESS', 'ANSWERED', 'PLANNED', 'DECLINED', 'CLOSED'];
|
|
26
|
+
/** Who reads it: everyone of the domain, or its writer and the handlers (`inquiry.visibility.<code>`). */
|
|
27
|
+
exports.INQUIRY_VISIBILITIES = ['DOMAIN', 'PRIVATE'];
|
|
28
|
+
exports.MAX_TITLE = 200;
|
|
29
|
+
exports.MAX_BODY = 20000;
|
|
30
|
+
exports.MAX_PAGE_URL = 2000;
|
|
31
|
+
/** The states that wait on the handlers - a to-do is open for them, and the handlers' badge counts them. */
|
|
32
|
+
exports.HANDLERS_TURN = ['RECEIVED', 'IN_PROGRESS'];
|
|
33
|
+
/** Why what was written cannot be kept, or nothing. */
|
|
34
|
+
function inputRefusal(input) {
|
|
35
|
+
if (!exports.INQUIRY_KINDS.includes(String(input.kind ?? '')))
|
|
36
|
+
return 'unknown-kind';
|
|
37
|
+
const title = String(input.title ?? '').trim();
|
|
38
|
+
if (!title)
|
|
39
|
+
return 'no-title';
|
|
40
|
+
if (title.length > exports.MAX_TITLE)
|
|
41
|
+
return 'title-too-long';
|
|
42
|
+
if (String(input.body ?? '').length > exports.MAX_BODY)
|
|
43
|
+
return 'body-too-long';
|
|
44
|
+
if (input.visibility && !exports.INQUIRY_VISIBILITIES.includes(input.visibility))
|
|
45
|
+
return 'unknown-visibility';
|
|
46
|
+
if (String(input.pageUrl ?? '').length > exports.MAX_PAGE_URL)
|
|
47
|
+
return 'page-url-too-long';
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Whether a reply says something - words, or files. */
|
|
51
|
+
function replyRefusal(body, files) {
|
|
52
|
+
const text = String(body ?? '').trim();
|
|
53
|
+
if (!text && !files)
|
|
54
|
+
return 'empty-reply';
|
|
55
|
+
if (text.length > exports.MAX_BODY)
|
|
56
|
+
return 'body-too-long';
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const isAuthor = (one, viewer) => !!one.creatorId && one.creatorId === viewer.userId;
|
|
60
|
+
/** An open inquiry is read by everyone of the domain; a private one by its writer and the handlers (decision 5). */
|
|
61
|
+
function mayRead(one, viewer) {
|
|
62
|
+
return one.visibility !== 'PRIVATE' || isAuthor(one, viewer) || viewer.handler;
|
|
63
|
+
}
|
|
64
|
+
/** Replies go back and forth between the writer and the handlers, until it is closed (decision 3). */
|
|
65
|
+
function replyRefusalFor(one, viewer) {
|
|
66
|
+
if (!isAuthor(one, viewer) && !viewer.handler)
|
|
67
|
+
return 'not-a-party';
|
|
68
|
+
if (one.status === 'CLOSED')
|
|
69
|
+
return 'closed';
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
/** Which side a reply is from - the writer's own reply is the asker's, even when the writer is a handler. */
|
|
73
|
+
function sideOf(one, viewer) {
|
|
74
|
+
return isAuthor(one, viewer) ? 'asker' : 'handler';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Where it stands after a reply (decision 2). A handler's reply marked as the answer answers it; any other handler's
|
|
78
|
+
* reply puts a received one in progress. The asker replying to an answer or a decision hands it back to the handlers.
|
|
79
|
+
*/
|
|
80
|
+
function statusAfterReply(one, side, asAnswer) {
|
|
81
|
+
if (side === 'handler') {
|
|
82
|
+
if (asAnswer)
|
|
83
|
+
return 'ANSWERED';
|
|
84
|
+
return one.status === 'RECEIVED' ? 'IN_PROGRESS' : one.status;
|
|
85
|
+
}
|
|
86
|
+
return ['ANSWERED', 'PLANNED', 'DECLINED'].includes(one.status) ? 'IN_PROGRESS' : one.status;
|
|
87
|
+
}
|
|
88
|
+
/** Whether `side` may mark a reply as the answer - the handlers' only. */
|
|
89
|
+
function answerRefusal(side, asAnswer) {
|
|
90
|
+
return asAnswer && side !== 'handler' ? 'handlers-only' : undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Why the request may not move it to `to`, or nothing (decision 2). The writer closes their own; everything else is the
|
|
94
|
+
* handlers'. A closed one stays closed; PLANNED and DECLINED are a proposal's; nothing goes back to RECEIVED.
|
|
95
|
+
*/
|
|
96
|
+
function moveRefusal(one, to, viewer) {
|
|
97
|
+
if (!exports.INQUIRY_STATUSES.includes(to))
|
|
98
|
+
return 'unknown-status';
|
|
99
|
+
if (one.status === 'CLOSED')
|
|
100
|
+
return 'closed';
|
|
101
|
+
if (to === one.status)
|
|
102
|
+
return 'same-status';
|
|
103
|
+
if (to === 'RECEIVED')
|
|
104
|
+
return 'not-a-move';
|
|
105
|
+
if ((to === 'PLANNED' || to === 'DECLINED') && one.kind !== 'PROPOSAL')
|
|
106
|
+
return 'not-a-proposal';
|
|
107
|
+
if (to === 'CLOSED' && isAuthor(one, viewer))
|
|
108
|
+
return undefined;
|
|
109
|
+
if (!viewer.handler)
|
|
110
|
+
return 'handlers-only';
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
/** Whether it waits on the handlers. */
|
|
114
|
+
const handlersTurn = (status) => exports.HANDLERS_TURN.includes(status);
|
|
115
|
+
exports.handlersTurn = handlersTurn;
|
|
116
|
+
/** Whether the writer has a handler's word they have not seen - a reply or a move since they last opened it. */
|
|
117
|
+
function newForAsker(one) {
|
|
118
|
+
const said = one.handlerActivityAt ? new Date(one.handlerActivityAt).getTime() : undefined;
|
|
119
|
+
if (said === undefined)
|
|
120
|
+
return false;
|
|
121
|
+
const seen = one.askerSeenAt ? new Date(one.askerSeenAt).getTime() : undefined;
|
|
122
|
+
return seen === undefined || said > seen;
|
|
123
|
+
}
|
|
124
|
+
/** The count on the menu entry (decision 8): new words on one's own inquiries, and for a handler those waiting on them. */
|
|
125
|
+
function badgeOf(summary) {
|
|
126
|
+
return summary.newAnswers + (summary.handler ? summary.unhandled : 0);
|
|
127
|
+
}
|
|
128
|
+
/** The screen an inquiry opens on. */
|
|
129
|
+
const viewAddressOf = (id) => `inquiry-list?id=${id}`;
|
|
130
|
+
exports.viewAddressOf = viewAddressOf;
|
|
131
|
+
/** The body as one line of plain words - the notice's summary. */
|
|
132
|
+
function plainSummary(body, max = 120) {
|
|
133
|
+
const text = String(body ?? '')
|
|
134
|
+
.replace(/\s+/g, ' ')
|
|
135
|
+
.trim();
|
|
136
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
137
|
+
}
|
|
138
|
+
/** Who is told of what (decision 7): the handlers of a new one, the writer of a handler's word, the handler of the writer's. */
|
|
139
|
+
function noticeWordsOf(one, kind, said, say) {
|
|
140
|
+
const kindWord = say(`inquiry.kind.${one.kind}`, one.kind);
|
|
141
|
+
const title = one.title;
|
|
142
|
+
const subject = {
|
|
143
|
+
received: say('inquiry.notice.received-subject', 'New {kind}: {title}', { kind: kindWord, title }),
|
|
144
|
+
replied: say('inquiry.notice.replied-subject', 'A reply to your {kind}: {title}', { kind: kindWord, title }),
|
|
145
|
+
answered: say('inquiry.notice.answered-subject', 'Answered: {title}', { title }),
|
|
146
|
+
moved: say('inquiry.notice.moved-subject', '{status}: {title}', { status: say(`inquiry.status.${said.status}`, String(said.status ?? '')), title }),
|
|
147
|
+
'asker-replied': say('inquiry.notice.asker-replied-subject', 'The writer replied: {title}', { title })
|
|
148
|
+
}[kind];
|
|
149
|
+
return {
|
|
150
|
+
subject,
|
|
151
|
+
summary: plainSummary(said.body) || subject,
|
|
152
|
+
body: said.body ?? '',
|
|
153
|
+
action: say('inquiry.notice.action', 'Open the inquiry'),
|
|
154
|
+
note: say('inquiry.notice.note', 'Open Inquiries in the application to read it.')
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=inquiry-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inquiry-rules.js","sourceRoot":"","sources":["../../../server/service/inquiry/inquiry-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The rules of an inquiry (ADR-0184 decisions 1-8) - what may be written, who reads and answers it, where it stands\n * after a reply or a move, whose turn it is, and what its notice says.\n *\n * Pure - the rules are tested without a database. The doors (inquiry-resolver.ts) ask them.\n */\n\n/** The kinds of inquiry - a question, a bug report, a feature proposal (`inquiry.kind.<code>`). */\nexport const INQUIRY_KINDS = ['QUESTION', 'BUG', 'PROPOSAL'] as const\n\n/** Where an inquiry stands (`inquiry.status.<code>`). PLANNED and DECLINED are a proposal's only. */\nexport const INQUIRY_STATUSES = ['RECEIVED', 'IN_PROGRESS', 'ANSWERED', 'PLANNED', 'DECLINED', 'CLOSED'] as const\n\n/** Who reads it: everyone of the domain, or its writer and the handlers (`inquiry.visibility.<code>`). */\nexport const INQUIRY_VISIBILITIES = ['DOMAIN', 'PRIVATE'] as const\n\nexport type InquiryKind = (typeof INQUIRY_KINDS)[number]\nexport type InquiryStatus = (typeof INQUIRY_STATUSES)[number]\n\nexport const MAX_TITLE = 200\nexport const MAX_BODY = 20000\nexport const MAX_PAGE_URL = 2000\n\n/** The states that wait on the handlers - a to-do is open for them, and the handlers' badge counts them. */\nexport const HANDLERS_TURN: readonly string[] = ['RECEIVED', 'IN_PROGRESS']\n\nexport interface InquiryFacts {\n kind: string\n status: string\n visibility: string\n creatorId?: string | null\n handlerActivityAt?: Date | string | null\n askerSeenAt?: Date | string | null\n}\n\n/** The request's person as an inquiry sees them. */\nexport interface Viewer {\n userId: string\n /** Holds `inquiry:mutation` (the task `inquiry.answering`), or owns the domain. */\n handler: boolean\n}\n\nexport interface InquiryInputFacts {\n kind?: string | null\n title?: string | null\n body?: string | null\n visibility?: string | null\n pageUrl?: string | null\n}\n\n/** Why what was written cannot be kept, or nothing. */\nexport function inputRefusal(input: InquiryInputFacts): string | undefined {\n if (!(INQUIRY_KINDS as readonly string[]).includes(String(input.kind ?? ''))) return 'unknown-kind'\n const title = String(input.title ?? '').trim()\n if (!title) return 'no-title'\n if (title.length > MAX_TITLE) return 'title-too-long'\n if (String(input.body ?? '').length > MAX_BODY) return 'body-too-long'\n if (input.visibility && !(INQUIRY_VISIBILITIES as readonly string[]).includes(input.visibility)) return 'unknown-visibility'\n if (String(input.pageUrl ?? '').length > MAX_PAGE_URL) return 'page-url-too-long'\n return undefined\n}\n\n/** Whether a reply says something - words, or files. */\nexport function replyRefusal(body: string | null | undefined, files: number): string | undefined {\n const text = String(body ?? '').trim()\n if (!text && !files) return 'empty-reply'\n if (text.length > MAX_BODY) return 'body-too-long'\n return undefined\n}\n\nconst isAuthor = (one: InquiryFacts, viewer: Viewer) => !!one.creatorId && one.creatorId === viewer.userId\n\n/** An open inquiry is read by everyone of the domain; a private one by its writer and the handlers (decision 5). */\nexport function mayRead(one: InquiryFacts, viewer: Viewer): boolean {\n return one.visibility !== 'PRIVATE' || isAuthor(one, viewer) || viewer.handler\n}\n\n/** Replies go back and forth between the writer and the handlers, until it is closed (decision 3). */\nexport function replyRefusalFor(one: InquiryFacts, viewer: Viewer): string | undefined {\n if (!isAuthor(one, viewer) && !viewer.handler) return 'not-a-party'\n if (one.status === 'CLOSED') return 'closed'\n return undefined\n}\n\n/** Which side a reply is from - the writer's own reply is the asker's, even when the writer is a handler. */\nexport function sideOf(one: InquiryFacts, viewer: Viewer): 'asker' | 'handler' {\n return isAuthor(one, viewer) ? 'asker' : 'handler'\n}\n\n/**\n * Where it stands after a reply (decision 2). A handler's reply marked as the answer answers it; any other handler's\n * reply puts a received one in progress. The asker replying to an answer or a decision hands it back to the handlers.\n */\nexport function statusAfterReply(one: InquiryFacts, side: 'asker' | 'handler', asAnswer: boolean): string {\n if (side === 'handler') {\n if (asAnswer) return 'ANSWERED'\n return one.status === 'RECEIVED' ? 'IN_PROGRESS' : one.status\n }\n return ['ANSWERED', 'PLANNED', 'DECLINED'].includes(one.status) ? 'IN_PROGRESS' : one.status\n}\n\n/** Whether `side` may mark a reply as the answer - the handlers' only. */\nexport function answerRefusal(side: 'asker' | 'handler', asAnswer: boolean): string | undefined {\n return asAnswer && side !== 'handler' ? 'handlers-only' : undefined\n}\n\n/**\n * Why the request may not move it to `to`, or nothing (decision 2). The writer closes their own; everything else is the\n * handlers'. A closed one stays closed; PLANNED and DECLINED are a proposal's; nothing goes back to RECEIVED.\n */\nexport function moveRefusal(one: InquiryFacts, to: string, viewer: Viewer): string | undefined {\n if (!(INQUIRY_STATUSES as readonly string[]).includes(to)) return 'unknown-status'\n if (one.status === 'CLOSED') return 'closed'\n if (to === one.status) return 'same-status'\n if (to === 'RECEIVED') return 'not-a-move'\n if ((to === 'PLANNED' || to === 'DECLINED') && one.kind !== 'PROPOSAL') return 'not-a-proposal'\n if (to === 'CLOSED' && isAuthor(one, viewer)) return undefined\n if (!viewer.handler) return 'handlers-only'\n return undefined\n}\n\n/** Whether it waits on the handlers. */\nexport const handlersTurn = (status: string) => HANDLERS_TURN.includes(status)\n\n/** Whether the writer has a handler's word they have not seen - a reply or a move since they last opened it. */\nexport function newForAsker(one: InquiryFacts): boolean {\n const said = one.handlerActivityAt ? new Date(one.handlerActivityAt).getTime() : undefined\n if (said === undefined) return false\n const seen = one.askerSeenAt ? new Date(one.askerSeenAt).getTime() : undefined\n return seen === undefined || said > seen\n}\n\n/** The count on the menu entry (decision 8): new words on one's own inquiries, and for a handler those waiting on them. */\nexport function badgeOf(summary: { newAnswers: number; unhandled: number; handler: boolean }): number {\n return summary.newAnswers + (summary.handler ? summary.unhandled : 0)\n}\n\n/** The screen an inquiry opens on. */\nexport const viewAddressOf = (id: string) => `inquiry-list?id=${id}`\n\n/** Why the people are told. */\nexport type NoticeKind = 'received' | 'replied' | 'answered' | 'moved' | 'asker-replied'\n\n/** A sentence in one language, with the English one when that language has none. */\nexport type Say = (key: string, english: string, params?: Record<string, unknown>) => string\n\n/** The body as one line of plain words - the notice's summary. */\nexport function plainSummary(body: string | null | undefined, max = 120): string {\n const text = String(body ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n return text.length > max ? `${text.slice(0, max - 1)}…` : text\n}\n\n/** Who is told of what (decision 7): the handlers of a new one, the writer of a handler's word, the handler of the writer's. */\nexport function noticeWordsOf(one: { title: string; kind: string }, kind: NoticeKind, said: { body?: string | null; status?: string }, say: Say) {\n const kindWord = say(`inquiry.kind.${one.kind}`, one.kind)\n const title = one.title\n const subject = {\n received: say('inquiry.notice.received-subject', 'New {kind}: {title}', { kind: kindWord, title }),\n replied: say('inquiry.notice.replied-subject', 'A reply to your {kind}: {title}', { kind: kindWord, title }),\n answered: say('inquiry.notice.answered-subject', 'Answered: {title}', { title }),\n moved: say('inquiry.notice.moved-subject', '{status}: {title}', { status: say(`inquiry.status.${said.status}`, String(said.status ?? '')), title }),\n 'asker-replied': say('inquiry.notice.asker-replied-subject', 'The writer replied: {title}', { title })\n }[kind]\n return {\n subject,\n summary: plainSummary(said.body) || subject,\n body: said.body ?? '',\n action: say('inquiry.notice.action', 'Open the inquiry'),\n note: say('inquiry.notice.note', 'Open Inquiries in the application to read it.')\n }\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { EntityManager } from 'typeorm';
|
|
2
|
+
import { Domain } from '@things-factory/shell';
|
|
3
|
+
import { Inquiry } from './inquiry';
|
|
4
|
+
import { type NoticeKind, type Viewer } from './inquiry-rules';
|
|
5
|
+
/** The attachments of an inquiry hang on it under this `refType`, a reply's under the next (attachment-base). */
|
|
6
|
+
export declare const INQUIRY_REF_TYPE = "Inquiry";
|
|
7
|
+
export declare const INQUIRY_REPLY_REF_TYPE = "InquiryReply";
|
|
8
|
+
/** Whether the request handles inquiries - the one privilege of the module (`inquiry.answering`). */
|
|
9
|
+
export declare function isHandler(context: any): Promise<boolean>;
|
|
10
|
+
export declare function viewerOf(context: any): Promise<Viewer>;
|
|
11
|
+
/** The inquiry as the request may read it, or nothing - one answer for "none" and "not yours". */
|
|
12
|
+
export declare function visibleInquiry(id: string, context: any, tx?: EntityManager): Promise<{
|
|
13
|
+
inquiry: Inquiry;
|
|
14
|
+
viewer: Viewer;
|
|
15
|
+
} | undefined>;
|
|
16
|
+
/** The roles of the domain that give `inquiry:mutation` - the handlers' pool. */
|
|
17
|
+
export declare function handlerRoleIds(domain: Domain, tx?: EntityManager): Promise<string[]>;
|
|
18
|
+
/** The handlers to tell - those in a handler role, and the domain's owner, who handles as owners do. */
|
|
19
|
+
export declare function handlerUserIds(domain: Domain, tx?: EntityManager): Promise<string[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Opens the handlers' to-do on it, unless one is open: one case, pooled on the handler roles - whoever of them picks it.
|
|
22
|
+
* With the activity not installed in the domain, or no handler role, there is no to-do: the notice and the badge still
|
|
23
|
+
* reach the handlers.
|
|
24
|
+
*/
|
|
25
|
+
export declare function openTodo(inquiry: Inquiry, context: any): Promise<void>;
|
|
26
|
+
/** Closes the handlers' to-do when it is no longer their turn - left alone when it is closed already. */
|
|
27
|
+
export declare function closeTodo(inquiry: Inquiry, context: any, reason: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Tells `userIds` - never the request's own person - once the request's transaction has committed. A notice that does
|
|
30
|
+
* not arrive undoes nothing: the list and the badge still reach them.
|
|
31
|
+
*/
|
|
32
|
+
export declare function tellWhenCommitted(context: any, one: Inquiry, kind: NoticeKind, userIds: string[], said?: {
|
|
33
|
+
body?: string | null;
|
|
34
|
+
status?: string;
|
|
35
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INQUIRY_REPLY_REF_TYPE = exports.INQUIRY_REF_TYPE = void 0;
|
|
4
|
+
exports.isHandler = isHandler;
|
|
5
|
+
exports.viewerOf = viewerOf;
|
|
6
|
+
exports.visibleInquiry = visibleInquiry;
|
|
7
|
+
exports.handlerRoleIds = handlerRoleIds;
|
|
8
|
+
exports.handlerUserIds = handlerUserIds;
|
|
9
|
+
exports.openTodo = openTodo;
|
|
10
|
+
exports.closeTodo = closeTodo;
|
|
11
|
+
exports.tellWhenCommitted = tellWhenCommitted;
|
|
12
|
+
/*
|
|
13
|
+
* What the doors of inquiries do with the database (ADR-0184 decisions 5-7): who is a handler, who reads one, the
|
|
14
|
+
* handlers' turn as a worklist to-do, and telling people of it through notification.
|
|
15
|
+
*/
|
|
16
|
+
const typeorm_1 = require("typeorm");
|
|
17
|
+
const auth_base_1 = require("@things-factory/auth-base");
|
|
18
|
+
const env_1 = require("@things-factory/env");
|
|
19
|
+
const translator_1 = require("@things-factory/i18n-base/translator");
|
|
20
|
+
const notification_1 = require("@things-factory/notification");
|
|
21
|
+
const shell_1 = require("@things-factory/shell");
|
|
22
|
+
const worklist_1 = require("@things-factory/worklist");
|
|
23
|
+
const inquiry_activity_1 = require("../../inquiry-activity");
|
|
24
|
+
const inquiry_1 = require("./inquiry");
|
|
25
|
+
const inquiry_rules_1 = require("./inquiry-rules");
|
|
26
|
+
/** The attachments of an inquiry hang on it under this `refType`, a reply's under the next (attachment-base). */
|
|
27
|
+
exports.INQUIRY_REF_TYPE = 'Inquiry';
|
|
28
|
+
exports.INQUIRY_REPLY_REF_TYPE = 'InquiryReply';
|
|
29
|
+
/** Whether the request handles inquiries - the one privilege of the module (`inquiry.answering`). */
|
|
30
|
+
async function isHandler(context) {
|
|
31
|
+
const state = context?.state ?? {};
|
|
32
|
+
const { domain, user, unsafeIP, prohibitedPrivileges } = state;
|
|
33
|
+
if (!domain || !user)
|
|
34
|
+
return false;
|
|
35
|
+
/* Asked once per request - a list asks it for each row. */
|
|
36
|
+
state.__inquiryHandler ??= (0, auth_base_1.checkPermission)({ category: 'inquiry', privilege: 'mutation', owner: true }, user, domain, unsafeIP, prohibitedPrivileges).then(Boolean);
|
|
37
|
+
return await state.__inquiryHandler;
|
|
38
|
+
}
|
|
39
|
+
async function viewerOf(context) {
|
|
40
|
+
return { userId: context.state.user.id, handler: await isHandler(context) };
|
|
41
|
+
}
|
|
42
|
+
/** The inquiry as the request may read it, or nothing - one answer for "none" and "not yours". */
|
|
43
|
+
async function visibleInquiry(id, context, tx) {
|
|
44
|
+
const { domain, user } = context?.state ?? {};
|
|
45
|
+
if (!id || !domain || !user || !/^[0-9a-f-]{36}$/i.test(id))
|
|
46
|
+
return undefined;
|
|
47
|
+
const inquiry = await (0, shell_1.getRepository)(inquiry_1.Inquiry, tx).findOne({ where: { id, domain: { id: domain.id } } });
|
|
48
|
+
if (!inquiry)
|
|
49
|
+
return undefined;
|
|
50
|
+
const viewer = await viewerOf(context);
|
|
51
|
+
return (0, inquiry_rules_1.mayRead)(inquiry, viewer) ? { inquiry, viewer } : undefined;
|
|
52
|
+
}
|
|
53
|
+
/** The roles of the domain that give `inquiry:mutation` - the handlers' pool. */
|
|
54
|
+
async function handlerRoleIds(domain, tx) {
|
|
55
|
+
const roles = await (0, shell_1.getRepository)(auth_base_1.Role, tx)
|
|
56
|
+
.createQueryBuilder('R')
|
|
57
|
+
.innerJoin('R.privileges', 'P')
|
|
58
|
+
.where('R.domain = :domain', { domain: domain.id })
|
|
59
|
+
.andWhere('P.category = :category', { category: 'inquiry' })
|
|
60
|
+
/* A privilege row's axis is its name. */
|
|
61
|
+
.andWhere('P.name = :axis', { axis: 'mutation' })
|
|
62
|
+
.getMany();
|
|
63
|
+
return roles.map(one => one.id);
|
|
64
|
+
}
|
|
65
|
+
/** The handlers to tell - those in a handler role, and the domain's owner, who handles as owners do. */
|
|
66
|
+
async function handlerUserIds(domain, tx) {
|
|
67
|
+
const roleIds = await handlerRoleIds(domain, tx);
|
|
68
|
+
const roles = roleIds.length ? await (0, shell_1.getRepository)(auth_base_1.Role, tx).find({ where: { id: (0, typeorm_1.In)(roleIds) }, relations: ['users'] }) : [];
|
|
69
|
+
const ids = new Set(roles.flatMap(one => (one.users ?? []).map(user => user.id)));
|
|
70
|
+
if (domain.owner)
|
|
71
|
+
ids.add(domain.owner);
|
|
72
|
+
return [...ids];
|
|
73
|
+
}
|
|
74
|
+
/*
|
|
75
|
+
* ---------------------------------------------------------------- the handlers' turn, as worklist holds it (decision 7)
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* Opens the handlers' to-do on it, unless one is open: one case, pooled on the handler roles - whoever of them picks it.
|
|
79
|
+
* With the activity not installed in the domain, or no handler role, there is no to-do: the notice and the badge still
|
|
80
|
+
* reach the handlers.
|
|
81
|
+
*/
|
|
82
|
+
async function openTodo(inquiry, context) {
|
|
83
|
+
const { domain, tx } = context.state;
|
|
84
|
+
if (inquiry.todoInstanceId)
|
|
85
|
+
return;
|
|
86
|
+
const activity = await tx.getRepository(worklist_1.Activity).findOne({ where: { domain: { id: domain.id }, name: inquiry_activity_1.INQUIRY_HANDLING } });
|
|
87
|
+
if (!activity) {
|
|
88
|
+
env_1.logger.warn(`[inquiry] ${domain.subdomain}: the activity "${inquiry_activity_1.INQUIRY_HANDLING}" is not installed - no to-do for inquiry ${inquiry.id}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const pool = await handlerRoleIds(domain, tx);
|
|
92
|
+
if (!pool.length) {
|
|
93
|
+
env_1.logger.warn(`[inquiry] ${domain.subdomain}: no role gives inquiry handling - no to-do for inquiry ${inquiry.id}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const name = inquiry.title.slice(0, 200);
|
|
97
|
+
const instance = await (0, worklist_1.issue)({ name, description: name, activityId: activity.id, input: { inquiryId: inquiry.id }, assignees: [], assigneeRoles: pool, threadsMin: 1 }, context);
|
|
98
|
+
await tx.getRepository(inquiry_1.Inquiry).update(inquiry.id, { todoInstanceId: instance.id });
|
|
99
|
+
inquiry.todoInstanceId = instance.id;
|
|
100
|
+
}
|
|
101
|
+
/** Closes the handlers' to-do when it is no longer their turn - left alone when it is closed already. */
|
|
102
|
+
async function closeTodo(inquiry, context, reason) {
|
|
103
|
+
const { tx } = context.state;
|
|
104
|
+
if (!inquiry.todoInstanceId)
|
|
105
|
+
return;
|
|
106
|
+
const open = await tx.getRepository(worklist_1.ActivityThread).count({ where: { activityInstance: { id: inquiry.todoInstanceId }, state: (0, typeorm_1.In)(['assigned', 'started', 'unassigned']) } });
|
|
107
|
+
if (open)
|
|
108
|
+
await (0, worklist_1.abort)({ id: inquiry.todoInstanceId, reason }, context);
|
|
109
|
+
await tx.getRepository(inquiry_1.Inquiry).update(inquiry.id, { todoInstanceId: null });
|
|
110
|
+
inquiry.todoInstanceId = null;
|
|
111
|
+
}
|
|
112
|
+
/*
|
|
113
|
+
* ---------------------------------------------------------------- telling people (decision 7)
|
|
114
|
+
*/
|
|
115
|
+
/** The sentences in `lng` from this module's translations, the English one where `lng` has none. */
|
|
116
|
+
const sayIn = (lng) => (key, english, params = {}) => (0, translator_1.translatorFor)(lng)(key, { ...params, defaultValue: english.replace(/\{(\w+)\}/g, (_, name) => String(params[name] ?? '')) });
|
|
117
|
+
/** The people to tell, as notification takes them - only those who can sign in, each with their mail and language. */
|
|
118
|
+
async function receiversOf(userIds, tx) {
|
|
119
|
+
if (!userIds.length)
|
|
120
|
+
return [];
|
|
121
|
+
const people = await (0, shell_1.getRepository)(auth_base_1.User, tx).find({ where: { id: (0, typeorm_1.In)(userIds) } });
|
|
122
|
+
return people.filter(one => one.status === auth_base_1.UserStatus.ACTIVATED).map(one => ({ id: one.id, email: one.email, locale: one.locale }));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Tells `userIds` - never the request's own person - once the request's transaction has committed. A notice that does
|
|
126
|
+
* not arrive undoes nothing: the list and the badge still reach them.
|
|
127
|
+
*/
|
|
128
|
+
async function tellWhenCommitted(context, one, kind, userIds, said = {}) {
|
|
129
|
+
const receivers = await receiversOf(userIds.filter(id => id !== context.state.user?.id), context.state.tx);
|
|
130
|
+
if (!receivers.length)
|
|
131
|
+
return;
|
|
132
|
+
const notice = {
|
|
133
|
+
type: 'inquiry',
|
|
134
|
+
level: 'info',
|
|
135
|
+
receivers,
|
|
136
|
+
words: lng => (0, inquiry_rules_1.noticeWordsOf)(one, kind, said, sayIn(lng)),
|
|
137
|
+
address: (0, inquiry_rules_1.viewAddressOf)(one.id),
|
|
138
|
+
link: (0, notification_1.noticeLinkOf)(context, (0, inquiry_rules_1.viewAddressOf)(one.id)),
|
|
139
|
+
label: `inquiry ${kind} ${one.id}`
|
|
140
|
+
};
|
|
141
|
+
(0, notification_1.sendNoticeWhenCommitted)(context, notice);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=inquiry-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inquiry-service.js","sourceRoot":"","sources":["../../../server/service/inquiry/inquiry-service.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What the doors of inquiries do with the database (ADR-0184 decisions 5-7): who is a handler, who reads one, the\n * handlers' turn as a worklist to-do, and telling people of it through notification.\n */\nimport { EntityManager, In } from 'typeorm'\n\nimport { checkPermission, Role, User, UserStatus } from '@things-factory/auth-base'\nimport { logger } from '@things-factory/env'\nimport { translatorFor } from '@things-factory/i18n-base/translator'\nimport { noticeLinkOf, sendNoticeWhenCommitted, type Notice } from '@things-factory/notification'\nimport { Domain, getRepository } from '@things-factory/shell'\nimport { abort, Activity, ActivityThread, issue } from '@things-factory/worklist'\n\nimport { INQUIRY_HANDLING } from '../../inquiry-activity'\nimport { Inquiry } from './inquiry'\nimport { mayRead, noticeWordsOf, viewAddressOf, type NoticeKind, type Say, type Viewer } from './inquiry-rules'\n\n/** The attachments of an inquiry hang on it under this `refType`, a reply's under the next (attachment-base). */\nexport const INQUIRY_REF_TYPE = 'Inquiry'\nexport const INQUIRY_REPLY_REF_TYPE = 'InquiryReply'\n\n/** Whether the request handles inquiries - the one privilege of the module (`inquiry.answering`). */\nexport async function isHandler(context: any): Promise<boolean> {\n const state = context?.state ?? {}\n const { domain, user, unsafeIP, prohibitedPrivileges } = state\n if (!domain || !user) return false\n /* Asked once per request - a list asks it for each row. */\n state.__inquiryHandler ??= checkPermission({ category: 'inquiry', privilege: 'mutation', owner: true } as any, user, domain, unsafeIP, prohibitedPrivileges).then(Boolean)\n return await state.__inquiryHandler\n}\n\nexport async function viewerOf(context: any): Promise<Viewer> {\n return { userId: context.state.user.id, handler: await isHandler(context) }\n}\n\n/** The inquiry as the request may read it, or nothing - one answer for \"none\" and \"not yours\". */\nexport async function visibleInquiry(id: string, context: any, tx?: EntityManager): Promise<{ inquiry: Inquiry; viewer: Viewer } | undefined> {\n const { domain, user } = context?.state ?? {}\n if (!id || !domain || !user || !/^[0-9a-f-]{36}$/i.test(id)) return undefined\n const inquiry = await getRepository(Inquiry, tx).findOne({ where: { id, domain: { id: domain.id } } as any })\n if (!inquiry) return undefined\n const viewer = await viewerOf(context)\n return mayRead(inquiry, viewer) ? { inquiry, viewer } : undefined\n}\n\n/** The roles of the domain that give `inquiry:mutation` - the handlers' pool. */\nexport async function handlerRoleIds(domain: Domain, tx?: EntityManager): Promise<string[]> {\n const roles = await getRepository(Role, tx)\n .createQueryBuilder('R')\n .innerJoin('R.privileges', 'P')\n .where('R.domain = :domain', { domain: domain.id })\n .andWhere('P.category = :category', { category: 'inquiry' })\n /* A privilege row's axis is its name. */\n .andWhere('P.name = :axis', { axis: 'mutation' })\n .getMany()\n return roles.map(one => one.id)\n}\n\n/** The handlers to tell - those in a handler role, and the domain's owner, who handles as owners do. */\nexport async function handlerUserIds(domain: Domain, tx?: EntityManager): Promise<string[]> {\n const roleIds = await handlerRoleIds(domain, tx)\n const roles = roleIds.length ? await getRepository(Role, tx).find({ where: { id: In(roleIds) } as any, relations: ['users'] }) : []\n const ids = new Set<string>(roles.flatMap(one => (one.users ?? []).map(user => user.id)))\n if (domain.owner) ids.add(domain.owner)\n return [...ids]\n}\n\n/*\n * ---------------------------------------------------------------- the handlers' turn, as worklist holds it (decision 7)\n */\n\n/**\n * Opens the handlers' to-do on it, unless one is open: one case, pooled on the handler roles - whoever of them picks it.\n * With the activity not installed in the domain, or no handler role, there is no to-do: the notice and the badge still\n * reach the handlers.\n */\nexport async function openTodo(inquiry: Inquiry, context: any): Promise<void> {\n const { domain, tx } = context.state\n if (inquiry.todoInstanceId) return\n const activity = await tx.getRepository(Activity).findOne({ where: { domain: { id: domain.id }, name: INQUIRY_HANDLING } as any })\n if (!activity) {\n logger.warn(`[inquiry] ${domain.subdomain}: the activity \"${INQUIRY_HANDLING}\" is not installed - no to-do for inquiry ${inquiry.id}`)\n return\n }\n const pool = await handlerRoleIds(domain, tx)\n if (!pool.length) {\n logger.warn(`[inquiry] ${domain.subdomain}: no role gives inquiry handling - no to-do for inquiry ${inquiry.id}`)\n return\n }\n const name = inquiry.title.slice(0, 200)\n const instance = await issue({ name, description: name, activityId: activity.id, input: { inquiryId: inquiry.id }, assignees: [], assigneeRoles: pool, threadsMin: 1 } as any, context)\n await tx.getRepository(Inquiry).update(inquiry.id, { todoInstanceId: instance.id })\n inquiry.todoInstanceId = instance.id\n}\n\n/** Closes the handlers' to-do when it is no longer their turn - left alone when it is closed already. */\nexport async function closeTodo(inquiry: Inquiry, context: any, reason: string): Promise<void> {\n const { tx } = context.state\n if (!inquiry.todoInstanceId) return\n const open = await tx.getRepository(ActivityThread).count({ where: { activityInstance: { id: inquiry.todoInstanceId }, state: In(['assigned', 'started', 'unassigned']) } as any })\n if (open) await abort({ id: inquiry.todoInstanceId, reason } as any, context)\n await tx.getRepository(Inquiry).update(inquiry.id, { todoInstanceId: null } as any)\n inquiry.todoInstanceId = null as any\n}\n\n/*\n * ---------------------------------------------------------------- telling people (decision 7)\n */\n\n/** The sentences in `lng` from this module's translations, the English one where `lng` has none. */\nconst sayIn =\n (lng: string): Say =>\n (key, english, params = {}) =>\n translatorFor(lng)(key, { ...params, defaultValue: english.replace(/\\{(\\w+)\\}/g, (_, name) => String(params[name] ?? '')) })\n\n/** The people to tell, as notification takes them - only those who can sign in, each with their mail and language. */\nasync function receiversOf(userIds: string[], tx?: EntityManager) {\n if (!userIds.length) return []\n const people = await getRepository(User, tx).find({ where: { id: In(userIds) } as any })\n return people.filter(one => one.status === UserStatus.ACTIVATED).map(one => ({ id: one.id, email: one.email, locale: (one as any).locale }))\n}\n\n/**\n * Tells `userIds` - never the request's own person - once the request's transaction has committed. A notice that does\n * not arrive undoes nothing: the list and the badge still reach them.\n */\nexport async function tellWhenCommitted(context: any, one: Inquiry, kind: NoticeKind, userIds: string[], said: { body?: string | null; status?: string } = {}): Promise<void> {\n const receivers = await receiversOf(\n userIds.filter(id => id !== context.state.user?.id),\n context.state.tx\n )\n if (!receivers.length) return\n const notice: Notice = {\n type: 'inquiry',\n level: 'info',\n receivers,\n words: lng => noticeWordsOf(one, kind, said, sayIn(lng)),\n address: viewAddressOf(one.id),\n link: noticeLinkOf(context, viewAddressOf(one.id)),\n label: `inquiry ${kind} ${one.id}`\n } as Notice\n sendNoticeWhenCommitted(context, notice)\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Inquiry } from './inquiry';
|
|
2
|
+
export declare class InquiryInput {
|
|
3
|
+
kind: string;
|
|
4
|
+
title: string;
|
|
5
|
+
body?: string;
|
|
6
|
+
visibility?: string;
|
|
7
|
+
pageUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class InquiryList {
|
|
10
|
+
items: Inquiry[];
|
|
11
|
+
total: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class InquirySummary {
|
|
14
|
+
newAnswers: number;
|
|
15
|
+
unhandled: number;
|
|
16
|
+
handler: boolean;
|
|
17
|
+
badge: number;
|
|
18
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InquirySummary = exports.InquiryList = exports.InquiryInput = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const type_graphql_1 = require("type-graphql");
|
|
6
|
+
const inquiry_1 = require("./inquiry");
|
|
7
|
+
let InquiryInput = class InquiryInput {
|
|
8
|
+
};
|
|
9
|
+
exports.InquiryInput = InquiryInput;
|
|
10
|
+
tslib_1.__decorate([
|
|
11
|
+
(0, type_graphql_1.Field)({ description: 'QUESTION, BUG or PROPOSAL.' }),
|
|
12
|
+
tslib_1.__metadata("design:type", String)
|
|
13
|
+
], InquiryInput.prototype, "kind", void 0);
|
|
14
|
+
tslib_1.__decorate([
|
|
15
|
+
(0, type_graphql_1.Field)({ description: 'The title.' }),
|
|
16
|
+
tslib_1.__metadata("design:type", String)
|
|
17
|
+
], InquiryInput.prototype, "title", void 0);
|
|
18
|
+
tslib_1.__decorate([
|
|
19
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'The body, plain text.' }),
|
|
20
|
+
tslib_1.__metadata("design:type", String)
|
|
21
|
+
], InquiryInput.prototype, "body", void 0);
|
|
22
|
+
tslib_1.__decorate([
|
|
23
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'DOMAIN (everyone of the domain reads it, the default) or PRIVATE (its writer and the handlers).' }),
|
|
24
|
+
tslib_1.__metadata("design:type", String)
|
|
25
|
+
], InquiryInput.prototype, "visibility", void 0);
|
|
26
|
+
tslib_1.__decorate([
|
|
27
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'The address of the screen it is written from - the browser sends it.' }),
|
|
28
|
+
tslib_1.__metadata("design:type", String)
|
|
29
|
+
], InquiryInput.prototype, "pageUrl", void 0);
|
|
30
|
+
exports.InquiryInput = InquiryInput = tslib_1.__decorate([
|
|
31
|
+
(0, type_graphql_1.InputType)({ description: 'What an inquiry asks, and who reads it.' })
|
|
32
|
+
], InquiryInput);
|
|
33
|
+
let InquiryList = class InquiryList {
|
|
34
|
+
};
|
|
35
|
+
exports.InquiryList = InquiryList;
|
|
36
|
+
tslib_1.__decorate([
|
|
37
|
+
(0, type_graphql_1.Field)(type => [inquiry_1.Inquiry], { description: 'The inquiries of the page.' }),
|
|
38
|
+
tslib_1.__metadata("design:type", Array)
|
|
39
|
+
], InquiryList.prototype, "items", void 0);
|
|
40
|
+
tslib_1.__decorate([
|
|
41
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'How many there are in all.' }),
|
|
42
|
+
tslib_1.__metadata("design:type", Number)
|
|
43
|
+
], InquiryList.prototype, "total", void 0);
|
|
44
|
+
exports.InquiryList = InquiryList = tslib_1.__decorate([
|
|
45
|
+
(0, type_graphql_1.ObjectType)({ description: 'A page of inquiries.' })
|
|
46
|
+
], InquiryList);
|
|
47
|
+
let InquirySummary = class InquirySummary {
|
|
48
|
+
};
|
|
49
|
+
exports.InquirySummary = InquirySummary;
|
|
50
|
+
tslib_1.__decorate([
|
|
51
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Their own inquiries with a handler\'s word they have not seen.' }),
|
|
52
|
+
tslib_1.__metadata("design:type", Number)
|
|
53
|
+
], InquirySummary.prototype, "newAnswers", void 0);
|
|
54
|
+
tslib_1.__decorate([
|
|
55
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'For a handler, the inquiries waiting on the handlers - received or in progress; 0 otherwise.' }),
|
|
56
|
+
tslib_1.__metadata("design:type", Number)
|
|
57
|
+
], InquirySummary.prototype, "unhandled", void 0);
|
|
58
|
+
tslib_1.__decorate([
|
|
59
|
+
(0, type_graphql_1.Field)({ description: 'Whether the person handles inquiries.' }),
|
|
60
|
+
tslib_1.__metadata("design:type", Boolean)
|
|
61
|
+
], InquirySummary.prototype, "handler", void 0);
|
|
62
|
+
tslib_1.__decorate([
|
|
63
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'The count the menu shows - new answers, and for a handler those waiting too.' }),
|
|
64
|
+
tslib_1.__metadata("design:type", Number)
|
|
65
|
+
], InquirySummary.prototype, "badge", void 0);
|
|
66
|
+
exports.InquirySummary = InquirySummary = tslib_1.__decorate([
|
|
67
|
+
(0, type_graphql_1.ObjectType)({ description: "What the menu counts for the request's person." })
|
|
68
|
+
], InquirySummary);
|
|
69
|
+
//# sourceMappingURL=inquiry-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inquiry-types.js","sourceRoot":"","sources":["../../../server/service/inquiry/inquiry-types.ts"],"names":[],"mappings":";;;;AAAA,+CAAgE;AAEhE,uCAAmC;AAG5B,IAAM,YAAY,GAAlB,MAAM,YAAY;CAexB,CAAA;AAfY,oCAAY;AAEvB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;0CACzC;AAGZ;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;;2CACxB;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC;;0CACnD;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iGAAiG,EAAE,CAAC;;gDACvH;AAGnB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,sEAAsE,EAAE,CAAC;;6CAC/F;uBAdL,YAAY;IADxB,IAAA,wBAAS,EAAC,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;GACzD,YAAY,CAexB;AAGM,IAAM,WAAW,GAAjB,MAAM,WAAW;CAMvB,CAAA;AANY,kCAAW;AAEtB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,iBAAO,CAAC,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;0CACxD;AAGhB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;0CACrD;sBALF,WAAW;IADvB,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,sBAAsB,EAAE,CAAC;GACvC,WAAW,CAMvB;AAGM,IAAM,cAAc,GAApB,MAAM,cAAc;CAY1B,CAAA;AAZY,wCAAc;AAEzB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,gEAAgE,EAAE,CAAC;;kDACpF;AAGlB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,8FAA8F,EAAE,CAAC;;iDACnH;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,uCAAuC,EAAE,CAAC;;+CAChD;AAGhB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,8EAA8E,EAAE,CAAC;;6CACvG;yBAXF,cAAc;IAD1B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC;GACjE,cAAc,CAY1B","sourcesContent":["import { Field, InputType, Int, ObjectType } from 'type-graphql'\n\nimport { Inquiry } from './inquiry'\n\n@InputType({ description: 'What an inquiry asks, and who reads it.' })\nexport class InquiryInput {\n @Field({ description: 'QUESTION, BUG or PROPOSAL.' })\n kind: string\n\n @Field({ description: 'The title.' })\n title: string\n\n @Field({ nullable: true, description: 'The body, plain text.' })\n body?: string\n\n @Field({ nullable: true, description: 'DOMAIN (everyone of the domain reads it, the default) or PRIVATE (its writer and the handlers).' })\n visibility?: string\n\n @Field({ nullable: true, description: 'The address of the screen it is written from - the browser sends it.' })\n pageUrl?: string\n}\n\n@ObjectType({ description: 'A page of inquiries.' })\nexport class InquiryList {\n @Field(type => [Inquiry], { description: 'The inquiries of the page.' })\n items: Inquiry[]\n\n @Field(type => Int, { description: 'How many there are in all.' })\n total: number\n}\n\n@ObjectType({ description: \"What the menu counts for the request's person.\" })\nexport class InquirySummary {\n @Field(type => Int, { description: 'Their own inquiries with a handler\\'s word they have not seen.' })\n newAnswers: number\n\n @Field(type => Int, { description: 'For a handler, the inquiries waiting on the handlers - received or in progress; 0 otherwise.' })\n unhandled: number\n\n @Field({ description: 'Whether the person handles inquiries.' })\n handler: boolean\n\n @Field(type => Int, { description: 'The count the menu shows - new answers, and for a handler those waiting too.' })\n badge: number\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
import { User } from '@things-factory/auth-base';
|
|
3
|
+
export declare class Inquiry {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
domain?: Domain;
|
|
6
|
+
domainId?: string;
|
|
7
|
+
kind: string;
|
|
8
|
+
title: string;
|
|
9
|
+
body?: string;
|
|
10
|
+
status: string;
|
|
11
|
+
visibility: string;
|
|
12
|
+
pageUrl?: string;
|
|
13
|
+
appName?: string;
|
|
14
|
+
appVersion?: string;
|
|
15
|
+
assignee?: User;
|
|
16
|
+
assigneeId?: string;
|
|
17
|
+
replyCount: number;
|
|
18
|
+
handlerActivityAt?: Date;
|
|
19
|
+
askerSeenAt?: Date;
|
|
20
|
+
answeredAt?: Date;
|
|
21
|
+
closedAt?: Date;
|
|
22
|
+
todoInstanceId?: string;
|
|
23
|
+
creator?: User;
|
|
24
|
+
creatorId?: string;
|
|
25
|
+
updater?: User;
|
|
26
|
+
updaterId?: string;
|
|
27
|
+
createdAt?: Date;
|
|
28
|
+
updatedAt?: Date;
|
|
29
|
+
deletedAt?: Date;
|
|
30
|
+
}
|
|
31
|
+
export declare class InquiryReply {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
domain?: Domain;
|
|
34
|
+
domainId?: string;
|
|
35
|
+
inquiry?: Inquiry;
|
|
36
|
+
inquiryId?: string;
|
|
37
|
+
body?: string;
|
|
38
|
+
byHandler: boolean;
|
|
39
|
+
answer: boolean;
|
|
40
|
+
status?: string;
|
|
41
|
+
creator?: User;
|
|
42
|
+
creatorId?: string;
|
|
43
|
+
updater?: User;
|
|
44
|
+
updaterId?: string;
|
|
45
|
+
createdAt?: Date;
|
|
46
|
+
updatedAt?: Date;
|
|
47
|
+
deletedAt?: Date;
|
|
48
|
+
}
|