nexus-shared 2.0.0 → 3.0.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/CHANGELOG.md +262 -0
- package/README.md +122 -25
- package/dist/Client.Index.d.ts +11 -0
- package/dist/Client.Index.js +11 -0
- package/dist/Components/Chats/Chat.d.ts +28 -0
- package/dist/Components/Chats/Chat.js +19 -0
- package/dist/Components/Chats/ChatButton.d.ts +26 -0
- package/dist/Components/Chats/ChatButton.js +41 -0
- package/dist/Components/Chats/ChatComposer.d.ts +41 -0
- package/dist/Components/Chats/ChatComposer.js +177 -0
- package/dist/Components/Chats/ChatConversations.d.ts +35 -0
- package/dist/Components/Chats/ChatConversations.js +38 -0
- package/dist/Components/Chats/ChatPanel.d.ts +66 -0
- package/dist/Components/Chats/ChatPanel.js +94 -0
- package/dist/Components/Chats/ChatParts.d.ts +87 -0
- package/dist/Components/Chats/ChatParts.js +100 -0
- package/dist/Components/Chats/ChatThread.d.ts +22 -0
- package/dist/Components/Chats/ChatThread.js +96 -0
- package/dist/Components/Documents/Menu.js +24 -20
- package/dist/Components/Documents/SplitButton.js +5 -3
- package/dist/Components/Documents/TabButtons.d.ts +24 -6
- package/dist/Components/Documents/TabButtons.js +23 -4
- package/dist/Components/Forms/ApiForm.d.ts +6 -4
- package/dist/Components/Forms/ApiForm.js +15 -14
- package/dist/Components/Forms/Crud.js +202 -50
- package/dist/Components/Forms/ExcelImport.d.ts +42 -0
- package/dist/Components/Forms/ExcelImport.js +190 -0
- package/dist/Components/Forms/Form.js +5 -1
- package/dist/Components/Inputs/DateTimePicker.js +2 -1
- package/dist/Components/Inputs/GroupForm.js +4 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +2 -0
- package/dist/Components/Inputs/InputRenderer.js +1 -1
- package/dist/Components/Inputs/ReadOnlyNotice.js +2 -1
- package/dist/Components/Inputs/RowsInput.d.ts +12 -1
- package/dist/Components/Inputs/RowsInput.js +53 -4
- package/dist/Components/Inputs/TabularForm.d.ts +7 -3
- package/dist/Components/Inputs/TabularForm.js +85 -20
- package/dist/Components/Inputs/TimePicker.js +2 -1
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +2 -2
- package/dist/Components/Layouts/ThemeSwitcher.js +35 -15
- package/dist/Components/Viewers/DataTable.js +88 -34
- package/dist/Components/Viewers/DataTableColumns.d.ts +28 -0
- package/dist/Components/Viewers/DataTableColumns.js +243 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +6 -2
- package/dist/Components/Viewers/DataTableParts.js +21 -9
- package/dist/Helpers/ApiClient.d.ts +22 -0
- package/dist/Helpers/ApiClient.js +4 -1
- package/dist/Helpers/ApiFormHelpers.d.ts +27 -4
- package/dist/Helpers/ApiFormHelpers.js +73 -9
- package/dist/Helpers/ApiModules.d.ts +0 -4
- package/dist/Helpers/ApiModules.js +1 -0
- package/dist/Helpers/ApiResponses.d.ts +14 -4
- package/dist/Helpers/ApiResponses.js +116 -25
- package/dist/Helpers/ApiRoutes.d.ts +0 -10
- package/dist/Helpers/ApiRoutes.js +2 -10
- package/dist/Helpers/ChatBackend.d.ts +134 -0
- package/dist/Helpers/ChatBackend.js +332 -0
- package/dist/Helpers/ChatHelpers.d.ts +189 -0
- package/dist/Helpers/ChatHelpers.js +486 -0
- package/dist/Helpers/ChatHooks.d.ts +81 -0
- package/dist/Helpers/ChatHooks.js +175 -0
- package/dist/Helpers/ChatStore.d.ts +132 -0
- package/dist/Helpers/ChatStore.js +394 -0
- package/dist/Helpers/ChatSummaries.d.ts +39 -0
- package/dist/Helpers/ChatSummaries.js +118 -0
- package/dist/Helpers/CrudBackend.d.ts +47 -5
- package/dist/Helpers/CrudBackend.js +99 -4
- package/dist/Helpers/CrudHelpers.d.ts +39 -4
- package/dist/Helpers/CrudHelpers.js +76 -11
- package/dist/Helpers/ExcelBackend.d.ts +106 -0
- package/dist/Helpers/ExcelBackend.js +290 -0
- package/dist/Helpers/ExcelHelpers.d.ts +98 -0
- package/dist/Helpers/ExcelHelpers.js +302 -0
- package/dist/Helpers/FormStore.d.ts +2 -0
- package/dist/Helpers/FormStore.js +19 -1
- package/dist/Helpers/MessageBuilder.js +0 -2
- package/dist/Helpers/PopoverHelpers.d.ts +31 -6
- package/dist/Helpers/PopoverHelpers.js +116 -12
- package/dist/Helpers/RowsStore.d.ts +7 -1
- package/dist/Helpers/RowsStore.js +22 -0
- package/dist/Helpers/TableColumns.d.ts +7 -0
- package/dist/Helpers/TableColumns.js +41 -13
- package/dist/Helpers/TableHelpers.d.ts +9 -1
- package/dist/Helpers/TableHelpers.js +20 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +29 -4
- package/dist/Interfaces/ApiInterfaces.js +16 -10
- package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
- package/dist/Interfaces/ChatInterfaces.js +1 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +73 -12
- package/dist/Interfaces/ExcelInterfaces.d.ts +215 -0
- package/dist/Interfaces/ExcelInterfaces.js +45 -0
- package/dist/Interfaces/FormInterfaces.d.ts +52 -2
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/TableInterfaces.d.ts +62 -2
- package/dist/Services/BrowserApi.d.ts +18 -1
- package/dist/Services/BrowserApi.js +18 -0
- package/dist/Services/ChatApi.d.ts +37 -0
- package/dist/Services/ChatApi.js +98 -0
- package/dist/Services/ExcelApi.d.ts +46 -0
- package/dist/Services/ExcelApi.js +196 -0
- package/dist/Services/ServerApi.d.ts +6 -1
- package/dist/Services/ServerApi.js +3 -0
- package/dist/Shared.Index.d.ts +8 -0
- package/dist/Shared.Index.js +8 -0
- package/package.json +6 -3
- package/src/Styles/Nexus.Button.css +2 -0
- package/src/Styles/Nexus.Chat.css +1489 -0
- package/src/Styles/Nexus.Crud.css +77 -0
- package/src/Styles/Nexus.Excel.css +370 -0
- package/src/Styles/Nexus.Form.css +23 -0
- package/src/Styles/Nexus.Index.css +2 -0
- package/src/Styles/Nexus.Menu.css +12 -2
- package/src/Styles/Nexus.Rows.css +78 -48
- package/src/Styles/Nexus.Tab.Buttons.css +55 -0
- package/src/Styles/Nexus.Table.css +286 -4
- package/src/Styles/Nexus.Theme.Picker.css +75 -2
|
@@ -2,6 +2,10 @@ import { buildMessage, describeResult } from "./MessageBuilder.js";
|
|
|
2
2
|
/*
|
|
3
3
|
* Reading a server's answer as success or failure, the same way for every call: Nexus API responses, ASP.NET Core problem
|
|
4
4
|
* details, or any JSON by its status. Pure, so it works in tests, in the browser, and on a server.
|
|
5
|
+
*
|
|
6
|
+
* What the server says always wins: a body that explains its refusal is shown in the server's own words, however it
|
|
7
|
+
* carried them (a list, one message object, or the message at the top level), and the messages built from the HTTP
|
|
8
|
+
* status are only the fallback for an answer that explains nothing.
|
|
5
9
|
*/
|
|
6
10
|
/** Shown when nothing reached the server (the default wording; `messages.reason({ network: true })` follows `configureMessages`). */
|
|
7
11
|
export const NETWORK_ERROR_MESSAGE = buildMessage("reason", { network: true });
|
|
@@ -15,6 +19,9 @@ function isRecord(value) {
|
|
|
15
19
|
function text(value) {
|
|
16
20
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
17
21
|
}
|
|
22
|
+
function count(value) {
|
|
23
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
24
|
+
}
|
|
18
25
|
/** Messages by field from `{ errors: { Email: ["…"], "Addresses[0].City": "…" } }`. */
|
|
19
26
|
function readErrorMap(errors) {
|
|
20
27
|
if (!isRecord(errors))
|
|
@@ -27,14 +34,91 @@ function readErrorMap(errors) {
|
|
|
27
34
|
}
|
|
28
35
|
return Object.keys(result).length ? result : undefined;
|
|
29
36
|
}
|
|
37
|
+
/** The backend's defaults say nothing about what went wrong, so they are left out. */
|
|
38
|
+
const EMPTY_CODES = ["SUCCESS", "GENERIC_ERROR"];
|
|
39
|
+
/** A code worth keeping: not one of the backend's defaults. */
|
|
40
|
+
function codeOf(value) {
|
|
41
|
+
const code = text(value);
|
|
42
|
+
return code && !EMPTY_CODES.includes(code) ? code : undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One problem from any shape a backend sends it in: `{ message, messageCode, errorCode, inputName }`, a plain sentence,
|
|
46
|
+
* or a wording of the same fields (`errorMessage`, `code`, `field`, `propertyName`). Nothing without words to show.
|
|
47
|
+
*/
|
|
48
|
+
function readErrorMessage(value) {
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
const sentence = text(value);
|
|
51
|
+
return sentence ? { message: sentence } : undefined;
|
|
52
|
+
}
|
|
53
|
+
if (!isRecord(value))
|
|
54
|
+
return undefined;
|
|
55
|
+
const message = text(value.message) ?? text(value.errorMessage) ?? text(value.detail) ?? text(value.title);
|
|
56
|
+
if (!message)
|
|
57
|
+
return undefined;
|
|
58
|
+
const messageCode = codeOf(value.messageCode) ?? codeOf(value.code);
|
|
59
|
+
const errorCode = count(value.errorCode);
|
|
60
|
+
const inputName = text(value.inputName) ?? text(value.field) ?? text(value.propertyName);
|
|
61
|
+
return {
|
|
62
|
+
message,
|
|
63
|
+
...(messageCode ? { messageCode } : {}),
|
|
64
|
+
...(errorCode ? { errorCode } : {}),
|
|
65
|
+
...(inputName ? { inputName } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Every problem a body lists, in its order, however it listed them: `errorMessages` as a list or as one message object,
|
|
70
|
+
* `errorMessage` either way, or an `errors` list (an `errors` map by field name is read by `readErrorMap` instead).
|
|
71
|
+
* Plain sentences count. Empty for a body that lists nothing; `fallback` is then read as the one problem, which is how
|
|
72
|
+
* the message and codes at the top level of a Nexus answer are read.
|
|
73
|
+
*/
|
|
74
|
+
export function readErrorMessages(body, fallback) {
|
|
75
|
+
const list = (values) => values.map(readErrorMessage).filter((problem) => Boolean(problem));
|
|
76
|
+
if (Array.isArray(body))
|
|
77
|
+
return list(body);
|
|
78
|
+
if (isRecord(body)) {
|
|
79
|
+
for (const key of ["errorMessages", "errorMessage", "errors"]) {
|
|
80
|
+
const value = body[key];
|
|
81
|
+
if (value === undefined || value === null)
|
|
82
|
+
continue;
|
|
83
|
+
// `errors` is a list of problems only when it is a list; as an object it names fields.
|
|
84
|
+
if (key === "errors" && !Array.isArray(value))
|
|
85
|
+
continue;
|
|
86
|
+
const problems = list(Array.isArray(value) ? value : [value]);
|
|
87
|
+
if (problems.length)
|
|
88
|
+
return problems;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (fallback === undefined)
|
|
92
|
+
return [];
|
|
93
|
+
const own = readErrorMessage(fallback);
|
|
94
|
+
return own ? [own] : [];
|
|
95
|
+
}
|
|
96
|
+
/** The problems that name an input, as one message per name: several on one name joined, `"floor|rate"` naming both. */
|
|
97
|
+
function fieldErrors(problems) {
|
|
98
|
+
const errors = {};
|
|
99
|
+
for (const { message, inputName } of problems) {
|
|
100
|
+
if (!inputName)
|
|
101
|
+
continue;
|
|
102
|
+
for (const part of inputName.split("|")) {
|
|
103
|
+
const name = part.trim();
|
|
104
|
+
if (!name)
|
|
105
|
+
continue;
|
|
106
|
+
errors[name] = errors[name] && errors[name] !== message ? `${errors[name]} ${message}` : message;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return Object.keys(errors).length ? errors : undefined;
|
|
110
|
+
}
|
|
30
111
|
/**
|
|
31
112
|
* Reads a server's answer as success or failure:
|
|
32
|
-
* - Nexus API responses, `{ isSuccess, message, messageCode, errorCode, inputName, result }`:
|
|
33
|
-
*
|
|
113
|
+
* - Nexus API responses, `{ isSuccess, code, errorMessages: [{ message, messageCode, errorCode, inputName }], result }`:
|
|
114
|
+
* every problem is kept, in the server's order, and `inputName` names the inputs a message is about (several with "|").
|
|
115
|
+
* `errorMessages` may be one message object instead of a list, and an older answer carries its one message and codes at
|
|
116
|
+
* the top level (`{ isSuccess, message, messageCode, errorCode, inputName }`); all of them are read the same way.
|
|
34
117
|
* - ASP.NET Core problem details and validation errors, `{ title, detail, errors: { Email: ["…"] } }`.
|
|
35
118
|
* - Anything else by its HTTP status; the body is the result.
|
|
36
119
|
*
|
|
37
|
-
*
|
|
120
|
+
* The server's own words are the message whenever it sent any. Only a failure that explains nothing gets a message by
|
|
121
|
+
* status, worded for what the call did (`context`, default: a save).
|
|
38
122
|
*/
|
|
39
123
|
export function readApiResponse(body, status, context) {
|
|
40
124
|
const fallback = () => buildMessage("reason", { action: "save", ...context, status });
|
|
@@ -42,33 +126,40 @@ export function readApiResponse(body, status, context) {
|
|
|
42
126
|
if (isRecord(body)) {
|
|
43
127
|
if (typeof body.isSuccess === "boolean") {
|
|
44
128
|
const ok = body.isSuccess && statusOk;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
for (const name of names.split("|"))
|
|
51
|
-
if (name.trim())
|
|
52
|
-
errors[name.trim()] = message;
|
|
53
|
-
}
|
|
54
|
-
// The backend's defaults ("SUCCESS", "GENERIC_ERROR", error 0) say nothing, so they are left out.
|
|
55
|
-
const code = text(body.messageCode);
|
|
56
|
-
const messageCode = code && code !== (ok ? "SUCCESS" : "GENERIC_ERROR") ? code : undefined;
|
|
57
|
-
const errorCode = typeof body.errorCode === "number" && body.errorCode !== 0 ? body.errorCode : undefined;
|
|
129
|
+
// A failure's problems: its list, else the message and codes at the top level as the one problem.
|
|
130
|
+
const problems = ok ? [] : readErrorMessages(body, body);
|
|
131
|
+
const said = problems.map(problem => problem.message).join(" ") || text(body.message);
|
|
132
|
+
const messageCode = problems[0]?.messageCode ?? codeOf(body.messageCode);
|
|
133
|
+
const errorCode = problems[0]?.errorCode ?? count(body.errorCode);
|
|
58
134
|
return {
|
|
59
135
|
ok,
|
|
60
|
-
status
|
|
136
|
+
// An answer read without its HTTP status (an app's own send function) still carries it as `code`.
|
|
137
|
+
status: status ?? count(body.code),
|
|
61
138
|
result: body.result,
|
|
62
|
-
message:
|
|
63
|
-
errors,
|
|
139
|
+
message: said ?? (ok ? undefined : fallback()),
|
|
140
|
+
errors: fieldErrors(problems),
|
|
141
|
+
...(problems.length ? { errorMessages: problems } : {}),
|
|
64
142
|
...(messageCode ? { messageCode } : {}),
|
|
65
|
-
...(errorCode
|
|
143
|
+
...(errorCode ? { errorCode } : {}),
|
|
66
144
|
};
|
|
67
145
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
146
|
+
// Another backend's envelope: its problems if it lists any, ASP.NET Core's field map, or its status.
|
|
147
|
+
const problems = readErrorMessages(body);
|
|
148
|
+
const errors = readErrorMap(body.errors) ?? fieldErrors(problems);
|
|
149
|
+
if (problems.length || errors || !statusOk) {
|
|
150
|
+
const said = problems.map(problem => problem.message).join(" ") || (text(body.message) ?? (errors ? undefined : (text(body.detail) ?? text(body.title))));
|
|
151
|
+
const messageCode = problems[0]?.messageCode;
|
|
152
|
+
const errorCode = problems[0]?.errorCode;
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
status,
|
|
156
|
+
result: body,
|
|
157
|
+
message: said ?? (errors ? undefined : fallback()),
|
|
158
|
+
errors,
|
|
159
|
+
...(problems.length ? { errorMessages: problems } : {}),
|
|
160
|
+
...(messageCode ? { messageCode } : {}),
|
|
161
|
+
...(errorCode ? { errorCode } : {}),
|
|
162
|
+
};
|
|
72
163
|
}
|
|
73
164
|
return { ok: true, status, result: body, message: text(body.message) };
|
|
74
165
|
}
|
|
@@ -124,7 +215,7 @@ export function toFormData(values) {
|
|
|
124
215
|
}
|
|
125
216
|
return data;
|
|
126
217
|
}
|
|
127
|
-
const RESULT_KEYS = ["result", "message", "errors", "status", "network", "timeout", "aborted", "messageCode", "errorCode"];
|
|
218
|
+
const RESULT_KEYS = ["result", "message", "errors", "errorMessages", "status", "network", "timeout", "aborted", "messageCode", "errorCode"];
|
|
128
219
|
/** Whether a value is already a read answer (`{ ok, … }`), not a server's body. */
|
|
129
220
|
export function isApiResult(value) {
|
|
130
221
|
if (!isRecord(value) || typeof value.ok !== "boolean" || "isSuccess" in value)
|
|
@@ -16,7 +16,6 @@ export declare const API_ACTIONS: {
|
|
|
16
16
|
readonly listings: {
|
|
17
17
|
readonly normal: "NormalListing";
|
|
18
18
|
readonly flag: "FlagListing";
|
|
19
|
-
readonly archive: "ArchiveListing";
|
|
20
19
|
readonly trash: "TrashListing";
|
|
21
20
|
readonly deleted: "DeleteListing";
|
|
22
21
|
};
|
|
@@ -24,7 +23,6 @@ export declare const API_ACTIONS: {
|
|
|
24
23
|
readonly paginations: {
|
|
25
24
|
readonly normal: "NormalPagination";
|
|
26
25
|
readonly flag: "FlagPagination";
|
|
27
|
-
readonly archive: "ArchivePagination";
|
|
28
26
|
readonly trash: "TrashPagination";
|
|
29
27
|
readonly deleted: "DeletePagination";
|
|
30
28
|
};
|
|
@@ -35,21 +33,17 @@ export declare const API_ACTIONS: {
|
|
|
35
33
|
readonly delete: "Delete";
|
|
36
34
|
readonly trash: "Trash";
|
|
37
35
|
readonly trashFromDelete: "TrashFromDelete";
|
|
38
|
-
readonly archive: "Archive";
|
|
39
36
|
readonly recover: "Recover";
|
|
40
|
-
readonly restore: "Restore";
|
|
41
37
|
readonly flag: "Flag";
|
|
42
38
|
readonly pin: "Pin";
|
|
43
39
|
readonly audit: "AuditListing";
|
|
44
40
|
readonly accessActions: "AccessActions";
|
|
45
41
|
readonly massDescription: "MassDescription";
|
|
46
|
-
readonly massArchive: "MassArchive";
|
|
47
42
|
readonly massTrash: "MassTrash";
|
|
48
43
|
readonly massFlag: "MassFlag";
|
|
49
44
|
readonly massPin: "MassPin";
|
|
50
45
|
readonly massDelete: "MassDelete";
|
|
51
46
|
readonly massRecover: "MassRecover";
|
|
52
|
-
readonly massRestore: "MassRestore";
|
|
53
47
|
readonly massTrashFromDelete: "MassTrashFromDelete";
|
|
54
48
|
};
|
|
55
49
|
/** An endpoint's optional parts: `bodyType`, `query`, `action`, `subject`. */
|
|
@@ -100,8 +94,6 @@ export declare function apiController<TModule extends ApiModuleValue>(module: TM
|
|
|
100
94
|
delete: ApiEndpoint<TModule>;
|
|
101
95
|
trash: ApiEndpoint<TModule>;
|
|
102
96
|
trashFromDelete: ApiEndpoint<TModule>;
|
|
103
|
-
archive: ApiEndpoint<TModule>;
|
|
104
|
-
restore: ApiEndpoint<TModule>;
|
|
105
97
|
recover: ApiEndpoint<TModule>;
|
|
106
98
|
flag: ApiEndpoint<TModule>;
|
|
107
99
|
pin: ApiEndpoint<TModule>;
|
|
@@ -112,14 +104,12 @@ export declare function apiController<TModule extends ApiModuleValue>(module: TM
|
|
|
112
104
|
/** Actions on many rows at once: POST `Room/MassDelete` and the rest, with the ids. */
|
|
113
105
|
mass: {
|
|
114
106
|
description: ApiEndpoint<TModule>;
|
|
115
|
-
archive: ApiEndpoint<TModule>;
|
|
116
107
|
trash: ApiEndpoint<TModule>;
|
|
117
108
|
trashFromDelete: ApiEndpoint<TModule>;
|
|
118
109
|
flag: ApiEndpoint<TModule>;
|
|
119
110
|
pin: ApiEndpoint<TModule>;
|
|
120
111
|
delete: ApiEndpoint<TModule>;
|
|
121
112
|
recover: ApiEndpoint<TModule>;
|
|
122
|
-
restore: ApiEndpoint<TModule>;
|
|
123
113
|
};
|
|
124
114
|
/** Any other action of the controller: `ROOMS.action("PUT", "Block/{id}", { action: "update" })`. */
|
|
125
115
|
action: (method: HttpMethod, action: string, extras?: EndpointExtras) => ApiEndpoint<TModule>;
|
|
@@ -16,9 +16,9 @@ export const API_ACTIONS = {
|
|
|
16
16
|
/** One page of the `normal` list. */
|
|
17
17
|
pagination: "NormalPagination",
|
|
18
18
|
/** Every record of each list a CRUD shows. */
|
|
19
|
-
listings: { normal: "NormalListing", flag: "FlagListing",
|
|
19
|
+
listings: { normal: "NormalListing", flag: "FlagListing", trash: "TrashListing", deleted: "DeleteListing" },
|
|
20
20
|
/** One page of each list a CRUD shows. */
|
|
21
|
-
paginations: { normal: "NormalPagination", flag: "FlagPagination",
|
|
21
|
+
paginations: { normal: "NormalPagination", flag: "FlagPagination", trash: "TrashPagination", deleted: "DeletePagination" },
|
|
22
22
|
/** The records the user pinned, which a CRUD shows above its list. */
|
|
23
23
|
pinListing: "PinListing",
|
|
24
24
|
create: "Create",
|
|
@@ -26,21 +26,17 @@ export const API_ACTIONS = {
|
|
|
26
26
|
delete: "Delete",
|
|
27
27
|
trash: "Trash",
|
|
28
28
|
trashFromDelete: "TrashFromDelete",
|
|
29
|
-
archive: "Archive",
|
|
30
29
|
recover: "Recover",
|
|
31
|
-
restore: "Restore",
|
|
32
30
|
flag: "Flag",
|
|
33
31
|
pin: "Pin",
|
|
34
32
|
audit: "AuditListing",
|
|
35
33
|
accessActions: "AccessActions",
|
|
36
34
|
massDescription: "MassDescription",
|
|
37
|
-
massArchive: "MassArchive",
|
|
38
35
|
massTrash: "MassTrash",
|
|
39
36
|
massFlag: "MassFlag",
|
|
40
37
|
massPin: "MassPin",
|
|
41
38
|
massDelete: "MassDelete",
|
|
42
39
|
massRecover: "MassRecover",
|
|
43
|
-
massRestore: "MassRestore",
|
|
44
40
|
massTrashFromDelete: "MassTrashFromDelete",
|
|
45
41
|
};
|
|
46
42
|
const define = (method) => (module, path, extras) => ({ ...extras, module, path, method });
|
|
@@ -95,8 +91,6 @@ export function apiController(module, controller, subject) {
|
|
|
95
91
|
delete: endpoint("DELETE", at(`${A.delete}/{id}`), { action: "delete" }),
|
|
96
92
|
trash: endpoint("DELETE", at(`${A.trash}/{id}`), { action: "trash" }),
|
|
97
93
|
trashFromDelete: endpoint("PUT", at(`${A.trashFromDelete}/{id}`), { action: "trash" }),
|
|
98
|
-
archive: endpoint("PUT", at(`${A.archive}/{id}`), { action: "archive" }),
|
|
99
|
-
restore: endpoint("PUT", at(`${A.restore}/{id}`), { action: "restore" }),
|
|
100
94
|
recover: endpoint("PUT", at(`${A.recover}/{id}`), { action: "recover" }),
|
|
101
95
|
flag: endpoint("PUT", at(`${A.flag}/{id}`), { action: "flag" }),
|
|
102
96
|
pin: endpoint("PUT", at(`${A.pin}/{id}`), { action: "pin" }),
|
|
@@ -107,14 +101,12 @@ export function apiController(module, controller, subject) {
|
|
|
107
101
|
/** Actions on many rows at once: POST `Room/MassDelete` and the rest, with the ids. */
|
|
108
102
|
mass: {
|
|
109
103
|
description: endpoint("POST", at(A.massDescription), { action: "update" }),
|
|
110
|
-
archive: endpoint("POST", at(A.massArchive), { action: "archive" }),
|
|
111
104
|
trash: endpoint("POST", at(A.massTrash), { action: "trash" }),
|
|
112
105
|
trashFromDelete: endpoint("POST", at(A.massTrashFromDelete), { action: "trash" }),
|
|
113
106
|
flag: endpoint("POST", at(A.massFlag), { action: "flag" }),
|
|
114
107
|
pin: endpoint("POST", at(A.massPin), { action: "pin" }),
|
|
115
108
|
delete: endpoint("POST", at(A.massDelete), { action: "delete" }),
|
|
116
109
|
recover: endpoint("POST", at(A.massRecover), { action: "recover" }),
|
|
117
|
-
restore: endpoint("POST", at(A.massRestore), { action: "restore" }),
|
|
118
110
|
},
|
|
119
111
|
/** Any other action of the controller: `ROOMS.action("PUT", "Block/{id}", { action: "update" })`. */
|
|
120
112
|
action: (method, action, extras) => endpoint(method, at(action), extras),
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type ApiEndpoint, type ApiModuleValue } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { AppChatDomain, ChatConversation, ChatDomainInfo, ChatDomainValue, ChatDraft, ChatHistory, ChatHistoryRequest, ChatId, ChatMessage, ChatParticipant, ChatParticipantPage, ChatParticipantQuery, ChatScope, ChatSummary, NewChatRequest } from "../Interfaces/ChatInterfaces.ts";
|
|
3
|
+
/** The actions a chat needs from a backend. A backend that does not serve one takes that feature off the panel. */
|
|
4
|
+
export declare const CHAT_ACTIONS: {
|
|
5
|
+
/** The conversations a record holds, for this reader. */
|
|
6
|
+
readonly conversations: "ConversationListing";
|
|
7
|
+
/** Starts one. */
|
|
8
|
+
readonly createConversation: "CreateConversation";
|
|
9
|
+
/** One page of messages. */
|
|
10
|
+
readonly history: "History";
|
|
11
|
+
/** Writes one message. */
|
|
12
|
+
readonly send: "Create";
|
|
13
|
+
/** The counts of many records at once, for the trigger icons in a list. */
|
|
14
|
+
readonly summaries: "Summaries";
|
|
15
|
+
/** The people in a conversation, and the ones who could join, searched. */
|
|
16
|
+
readonly participants: "ParticipantPagination";
|
|
17
|
+
/** How far this user has read. */
|
|
18
|
+
readonly read: "Read";
|
|
19
|
+
/** Removes one message. */
|
|
20
|
+
readonly remove: "Delete";
|
|
21
|
+
/** Adds people to a conversation. */
|
|
22
|
+
readonly addParticipants: "AddParticipants";
|
|
23
|
+
};
|
|
24
|
+
export type ChatAction = keyof typeof CHAT_ACTIONS;
|
|
25
|
+
type Row = Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* What a chat needs from a backend. Extend it for an app whose chat API differs from the Nexus one, and register it
|
|
28
|
+
* once beside the app's API modules:
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* class HrChatBackend extends NexusChatBackend {
|
|
32
|
+
* override endpoint(action: ChatAction) {
|
|
33
|
+
* return action === "history" ? apiEndpoint.post(HrModule.Staff, "LeaveChat/Page") : super.endpoint(action);
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* defineChatBackend(new HrChatBackend());
|
|
37
|
+
*/
|
|
38
|
+
export declare abstract class ChatBackend<TDomain extends ChatDomainValue = AppChatDomain> {
|
|
39
|
+
/**
|
|
40
|
+
* Which endpoint serves an action. The module is one of the app's own (`AppModule`), as everywhere else in the API
|
|
41
|
+
* layer: an app on another backend registers a backend of its own and names its own modules here.
|
|
42
|
+
*/
|
|
43
|
+
abstract endpoint(action: ChatAction): ApiEndpoint;
|
|
44
|
+
/** Whether the backend serves an action. Default true; `false` takes the feature off the panel. */
|
|
45
|
+
supports(action: ChatAction): boolean;
|
|
46
|
+
/** What asking for a record's conversations sends. */
|
|
47
|
+
abstract conversationsRequest(scope: ChatScope<TDomain>): unknown;
|
|
48
|
+
/** Reads them. */
|
|
49
|
+
abstract readConversations(result: unknown): readonly ChatConversation[];
|
|
50
|
+
/** What starting a conversation sends. */
|
|
51
|
+
abstract createConversationRequest(scope: ChatScope<TDomain>, request: NewChatRequest): unknown;
|
|
52
|
+
/** Reads the conversation that was started, or the one that already existed. */
|
|
53
|
+
abstract readConversation(result: unknown): ChatConversation;
|
|
54
|
+
/** What a history request sends: which conversation, and which page of it. */
|
|
55
|
+
abstract historyRequest(scope: ChatScope<TDomain>, request: ChatHistoryRequest, conversationId?: ChatId): unknown;
|
|
56
|
+
/** Reads a page of history. */
|
|
57
|
+
abstract readHistory(result: unknown): ChatHistory;
|
|
58
|
+
/** What sending a message sends. */
|
|
59
|
+
abstract sendRequest(scope: ChatScope<TDomain>, draft: ChatDraft, conversationId?: ChatId): unknown;
|
|
60
|
+
/** Reads the saved message. The draft is there for a backend that answers with an id alone. */
|
|
61
|
+
abstract readMessage(result: unknown, scope: ChatScope<TDomain>, draft: ChatDraft): ChatMessage;
|
|
62
|
+
/** What asking for many records' counts sends. One call carries one domain and many parent ids. */
|
|
63
|
+
abstract summariesRequest(domain: TDomain, parentIds: readonly ChatId[], userId: ChatId): unknown;
|
|
64
|
+
/** Reads those counts, by parent id as text (`String(parentId)`). Records the answer leaves out have no messages. */
|
|
65
|
+
abstract readSummaries(result: unknown): Map<string, ChatSummary>;
|
|
66
|
+
/** What a participant search sends. Without a conversation it asks who could be put in a new one. */
|
|
67
|
+
abstract participantsRequest(scope: ChatScope<TDomain>, query: ChatParticipantQuery, conversationId?: ChatId): unknown;
|
|
68
|
+
/** Reads a page of participants. */
|
|
69
|
+
abstract readParticipants(result: unknown): ChatParticipantPage;
|
|
70
|
+
/** What the read mark sends. */
|
|
71
|
+
abstract readMarkRequest(scope: ChatScope<TDomain>, upTo: ChatId, conversationId?: ChatId): unknown;
|
|
72
|
+
/** What adding people sends. */
|
|
73
|
+
abstract addParticipantsRequest(scope: ChatScope<TDomain>, userIds: readonly ChatId[], conversationId?: ChatId): unknown;
|
|
74
|
+
/** What removing a message sends, as the call's `params`: the id in the path. */
|
|
75
|
+
removeParams(scope: ChatScope<TDomain>, messageId: ChatId): Record<string, string | number>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The Nexus Chat module: one controller, `Chat`, whose actions take the domain, the parent id and the reader, and
|
|
79
|
+
* answer as the other Nexus endpoints do (`{ results, totalCount, filterCount }` for pages). See the note at the top
|
|
80
|
+
* of this file: these are the shape the module is expected to take.
|
|
81
|
+
*/
|
|
82
|
+
export declare class NexusChatBackend<TDomain extends ChatDomainValue = AppChatDomain> extends ChatBackend<TDomain> {
|
|
83
|
+
/** The module the chat endpoints live in. `NexusModule.Chat` (bit 6). */
|
|
84
|
+
readonly module: ApiModuleValue;
|
|
85
|
+
/** The controller the actions hang off. */
|
|
86
|
+
readonly controller: string;
|
|
87
|
+
endpoint(action: ChatAction): ApiEndpoint;
|
|
88
|
+
conversationsRequest(scope: ChatScope<TDomain>): unknown;
|
|
89
|
+
readConversations(result: unknown): readonly ChatConversation[];
|
|
90
|
+
createConversationRequest(scope: ChatScope<TDomain>, request: NewChatRequest): unknown;
|
|
91
|
+
readConversation(result: unknown): ChatConversation;
|
|
92
|
+
historyRequest(scope: ChatScope<TDomain>, request: ChatHistoryRequest, conversationId?: ChatId): unknown;
|
|
93
|
+
readHistory(result: unknown): ChatHistory;
|
|
94
|
+
sendRequest(scope: ChatScope<TDomain>, draft: ChatDraft, conversationId?: ChatId): unknown;
|
|
95
|
+
readMessage(result: unknown, scope: ChatScope<TDomain>, draft: ChatDraft): ChatMessage;
|
|
96
|
+
summariesRequest(domain: TDomain, parentIds: readonly ChatId[], userId: ChatId): unknown;
|
|
97
|
+
readSummaries(result: unknown): Map<string, ChatSummary>;
|
|
98
|
+
participantsRequest(scope: ChatScope<TDomain>, query: ChatParticipantQuery, conversationId?: ChatId): unknown;
|
|
99
|
+
readParticipants(result: unknown): ChatParticipantPage;
|
|
100
|
+
readMarkRequest(scope: ChatScope<TDomain>, upTo: ChatId, conversationId?: ChatId): unknown;
|
|
101
|
+
addParticipantsRequest(scope: ChatScope<TDomain>, userIds: readonly ChatId[], conversationId?: ChatId): unknown;
|
|
102
|
+
/**
|
|
103
|
+
* One conversation row. The type comes back as the backend's `EChatTypes` number (0 one-to-one, 1 group) or as a
|
|
104
|
+
* name; both are read, because which it will be is not settled.
|
|
105
|
+
*/
|
|
106
|
+
protected toConversation(row: Row): ChatConversation;
|
|
107
|
+
/** One message row. The names are the ones a Nexus contract uses; override this alone for a backend that differs. */
|
|
108
|
+
protected toMessage(row: Row): ChatMessage;
|
|
109
|
+
/** One set of counts. */
|
|
110
|
+
protected toSummary(row: Row): ChatSummary;
|
|
111
|
+
/** One participant row. */
|
|
112
|
+
protected toParticipant(row: Row): ChatParticipant;
|
|
113
|
+
}
|
|
114
|
+
/** Makes an app's chat backend the one every chat uses, and returns it. Call it once, beside `defineApiModules`. */
|
|
115
|
+
export declare function defineChatBackend<T extends ChatBackend<any>>(backend: T): T;
|
|
116
|
+
/** The app's chat backend: the one given to `defineChatBackend`, else the Nexus one. */
|
|
117
|
+
export declare function getChatBackend(): ChatBackend<any>;
|
|
118
|
+
/**
|
|
119
|
+
* Names the app's domains, for the panel's heading and its messages. The values are the app's own enum, so this
|
|
120
|
+
* package never holds a number of its own: the owner's list is Leave, Procurement, Applicants, Students, Companies,
|
|
121
|
+
* Tenders and the account kinds, and which number each one is belongs to the backend's `EDomainTypes`.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* defineChatDomains([
|
|
125
|
+
* { domain: EDomainTypes.Leave, key: "leave", label: "Leave request" },
|
|
126
|
+
* { domain: EDomainTypes.Procurement, key: "procurement", label: "Purchase order" },
|
|
127
|
+
* ]);
|
|
128
|
+
*/
|
|
129
|
+
export declare function defineChatDomains<TDomain extends ChatDomainValue>(list: readonly ChatDomainInfo<TDomain>[]): readonly ChatDomainInfo<TDomain>[];
|
|
130
|
+
/** The registered domains. Empty until an app calls `defineChatDomains`. */
|
|
131
|
+
export declare function getChatDomains(): readonly ChatDomainInfo<any>[];
|
|
132
|
+
/** A domain's name on screen, or `undefined` when the app has not named it. */
|
|
133
|
+
export declare function chatDomainLabel(domain: ChatDomainValue): string | undefined;
|
|
134
|
+
export {};
|