@coworkee/airworks 0.0.1029 → 0.0.1031
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/dist/apps/admin/system.d.ts +2 -0
- package/dist/apps/admin/system.js +20 -2
- package/dist/apps/data.d.ts +3 -1
- package/dist/apps/data.js +55 -19
- package/dist/apps/files.d.ts +1 -0
- package/dist/apps/files.js +15 -11
- package/dist/apps/inbox.d.ts +4 -0
- package/dist/apps/inbox.js +114 -0
- package/dist/apps/profile.d.ts +3 -3
- package/dist/apps/profile.js +28 -38
- package/dist/apps/record.d.ts +8 -0
- package/dist/apps/record.js +183 -0
- package/dist/apps/reports.d.ts +2 -1
- package/dist/apps/reports.js +2 -2
- package/dist/apps/settings.d.ts +2 -2
- package/dist/apps/settings.js +4 -3
- package/dist/apps/web.d.ts +23 -0
- package/dist/apps/web.js +50 -0
- package/dist/coworkee.d.ts +2 -0
- package/dist/coworkee.js +22 -0
- package/dist/dom.d.ts +8 -0
- package/dist/dom.js +13 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +2 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +11 -8
- package/dist/search.d.ts +1 -1
- package/dist/search.js +4 -1
- package/dist/style.css +47 -1
- package/dist/table.d.ts +2 -0
- package/dist/table.js +15 -4
- package/package.json +2 -2
- package/dist/apps/notifications.d.ts +0 -5
- package/dist/apps/notifications.js +0 -73
|
@@ -9,4 +9,6 @@ export declare const editionsApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
|
9
9
|
export declare const tenantsApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
10
10
|
export declare const auditApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
11
11
|
export declare const aiToolCallsApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
12
|
+
/** The API documentation (Swagger UI) of the app in a window. */
|
|
13
|
+
export declare const apiDocsApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
12
14
|
export declare const systemApps: ((coworkee: Coworkee) => CoworkeeApp)[];
|
|
@@ -3,6 +3,7 @@ import { choices, crudApp, renderCrud } from "../../crud.js";
|
|
|
3
3
|
import { esc } from "../../dom.js";
|
|
4
4
|
import { confirmDialog, formDialog, infoDialog } from "../../form.js";
|
|
5
5
|
import { icons } from "../../icons.js";
|
|
6
|
+
import { recordAppId } from "../record.js";
|
|
6
7
|
const json = (method, body) => ({ method, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
7
8
|
const check = (value) => value ? '✓' : '';
|
|
8
9
|
const paged = (page, pageSize, extra = {}) => new URLSearchParams({ Page: String(page), PageSize: String(pageSize), ...Object.fromEntries(Object.entries(extra).filter(([, value]) => value)) });
|
|
@@ -211,12 +212,19 @@ export const tenantsApp = crudApp({
|
|
|
211
212
|
});
|
|
212
213
|
const changesTable = (coworkee, changes) => `<table class="cw-table"><thead><tr><th>${esc(coworkee.t('Field'))}</th>
|
|
213
214
|
<th>${esc(coworkee.t('Changes'))}</th></tr></thead><tbody>${changes.map((change) => `<tr><td>${esc(change.property)}</td><td>${esc(change.oldValue)} → ${esc(change.newValue)}</td></tr>`).join('')}</tbody></table>`;
|
|
215
|
+
/** The entity sets with a history; the audit log names their entity types. */
|
|
216
|
+
let historySets;
|
|
217
|
+
const setOf = (entityType) => historySets?.find((set) => set.entityType === entityType)?.entitySet;
|
|
214
218
|
export const auditApp = crudApp({
|
|
215
219
|
id: 'coworkee.admin.audit', group: 'Monitoring', title: 'Audit log', icon: icons.history, accent: '#5d4037', width: 960,
|
|
216
220
|
permissions: ['Audit.View'],
|
|
217
221
|
search: true, serverSearch: true,
|
|
218
222
|
load: async ({ request }, { page, pageSize, search }) => {
|
|
219
|
-
const result = await
|
|
223
|
+
const [result, sets] = await Promise.all([
|
|
224
|
+
request(`/api/v1/audit?${paged(page, pageSize, { EntityType: search })}`),
|
|
225
|
+
historySets ?? request('/api/v1/history').catch(() => []),
|
|
226
|
+
]);
|
|
227
|
+
historySets = sets;
|
|
220
228
|
return { items: result.items, total: Number(result.totalCount) };
|
|
221
229
|
},
|
|
222
230
|
columns: [
|
|
@@ -226,6 +234,11 @@ export const auditApp = crudApp({
|
|
|
226
234
|
actions: [{
|
|
227
235
|
title: 'Changes', icon: 'ℹ',
|
|
228
236
|
run: async (entry, coworkee) => { await infoDialog(coworkee, `${entry.entityType} ${entry.entityId}`, changesTable(coworkee, entry.changes), true); return false; },
|
|
237
|
+
}, {
|
|
238
|
+
// undo, restore a state or a deleted record: the record window has its history
|
|
239
|
+
title: 'Open record history', icon: '↶',
|
|
240
|
+
visible: (entry) => !!setOf(entry.entityType),
|
|
241
|
+
run: async (entry, coworkee) => { coworkee.airWorks.openApp(recordAppId, { set: setOf(entry.entityType), id: entry.entityId }); return false; },
|
|
229
242
|
}],
|
|
230
243
|
});
|
|
231
244
|
export const aiToolCallsApp = crudApp({
|
|
@@ -249,4 +262,9 @@ export const aiToolCallsApp = crudApp({
|
|
|
249
262
|
},
|
|
250
263
|
}],
|
|
251
264
|
});
|
|
252
|
-
|
|
265
|
+
/** The API documentation (Swagger UI) of the app in a window. */
|
|
266
|
+
export const apiDocsApp = (coworkee) => ({
|
|
267
|
+
id: 'coworkee.api-docs', group: 'Monitoring', title: coworkee.t('API documentation'), icon: icons.dataObject, accent: '#447d1c', width: 1000, height: 680,
|
|
268
|
+
requiredPermissions: ['ApiDocs.View'], url: '/swagger/index.html',
|
|
269
|
+
});
|
|
270
|
+
export const systemApps = [languagesApp, translationsApp, themesApp, backupsApp, servicesApp, editionsApp, tenantsApp, auditApp, aiToolCallsApp, apiDocsApp];
|
package/dist/apps/data.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Coworkee, CoworkeeApp } from '../coworkee.ts';
|
|
2
2
|
import { type EntityTableOptions } from '../table.ts';
|
|
3
|
-
/**
|
|
3
|
+
/** Opens a row of an entity set in the record window. */
|
|
4
|
+
export declare const openRecord: (coworkee: Coworkee, entitySet: string, row: Record<string, unknown>) => void;
|
|
5
|
+
/** Every OData entity set the user may read: a searchable table per set, a click opens the record with its history and workflow, deleted records come back from the bin. */
|
|
4
6
|
export declare const dataApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
5
7
|
export type EntityAppOptions = EntityTableOptions & {
|
|
6
8
|
title?: string;
|
package/dist/apps/data.js
CHANGED
|
@@ -1,36 +1,72 @@
|
|
|
1
1
|
import { entitySets } from '@coworkee/client';
|
|
2
|
-
import { errorText, esc } from "../dom.js";
|
|
2
|
+
import { display, errorText, esc, label, recordTitle } from "../dom.js";
|
|
3
|
+
import { recordAppId } from "./record.js";
|
|
3
4
|
import { icons } from "../icons.js";
|
|
4
5
|
import { renderEntityTable } from "../table.js";
|
|
5
|
-
/**
|
|
6
|
+
/** Opens a row of an entity set in the record window. */
|
|
7
|
+
export const openRecord = (coworkee, entitySet, row) => {
|
|
8
|
+
const id = row.Id ?? row.id;
|
|
9
|
+
if (id !== undefined && id !== null)
|
|
10
|
+
coworkee.airWorks.openApp(recordAppId, { set: entitySet, id: String(id), title: recordTitle(row) });
|
|
11
|
+
};
|
|
12
|
+
function renderDeleted(coworkee, panel, set) {
|
|
13
|
+
const { t, culture } = coworkee;
|
|
14
|
+
panel.innerHTML = `<p class="cw-muted">${esc(t('MudDataGrid_Loading'))}</p>`;
|
|
15
|
+
coworkee.request(`/api/v1/history/${encodeURIComponent(set)}/deleted`).then((items) => {
|
|
16
|
+
panel.innerHTML = `<table class="cw-table"><thead><tr><th>${esc(t('Name'))}</th><th>${esc(t('Deleted'))}</th><th>${esc(t('Who'))}</th></tr></thead><tbody>${items.map((item) => `<tr class="cw-clickable" tabindex="0" data-deleted="${esc(item.entityId)}" data-title="${esc(item.title ?? '')}"><td>${esc(item.title ?? item.entityId)}</td><td>${esc(display(item.deletedAt, culture))}</td><td>${esc(item.deletedByName ?? t('system'))}</td></tr>`).join('')
|
|
17
|
+
|| `<tr><td colspan="3" class="cw-empty">${esc(t('Nothing was deleted.'))}</td></tr>`}</tbody></table>`;
|
|
18
|
+
}).catch((error) => { panel.innerHTML = `<p class="cw-error">${esc(errorText(error))}</p>`; });
|
|
19
|
+
}
|
|
20
|
+
/** Every OData entity set the user may read: a searchable table per set, a click opens the record with its history and workflow, deleted records come back from the bin. */
|
|
6
21
|
export const dataApp = (coworkee) => ({
|
|
7
22
|
id: 'coworkee.data',
|
|
8
|
-
title: coworkee.t('
|
|
23
|
+
title: coworkee.t('Records'),
|
|
9
24
|
icon: 'fn-reporttable.png',
|
|
10
25
|
accent: '#167d79',
|
|
11
|
-
width:
|
|
12
|
-
height:
|
|
26
|
+
width: 1040,
|
|
27
|
+
height: 640,
|
|
13
28
|
probe: '/odata',
|
|
14
|
-
render: async ({ host, intent, setState }) => {
|
|
29
|
+
render: async ({ host, intent, setState, onClose }) => {
|
|
15
30
|
const { t } = coworkee;
|
|
16
31
|
host.classList.add('cw-host');
|
|
17
|
-
const sets =
|
|
18
|
-
|
|
32
|
+
const [sets, histories] = await Promise.all([entitySets(), coworkee.request('/api/v1/history').catch(() => [])]);
|
|
33
|
+
sets.sort((a, b) => label(t, a.name).localeCompare(label(t, b.name)));
|
|
34
|
+
host.innerHTML = `<div class="cw-app cw-data"><nav class="cw-list" aria-label="${esc(t('Records'))}">${sets.map((set) => `<button data-set="${esc(set.name)}">${esc(label(t, set.name))}</button>`).join('') || `<p class="cw-muted">${esc(t('Nothing found.'))}</p>`}</nav>
|
|
35
|
+
<section class="cw-stack"><div class="cw-views" hidden><button data-view="all" aria-pressed="true">${esc(t('All'))}</button><button data-view="deleted" aria-pressed="false">${esc(t('Deleted'))}</button></div>
|
|
36
|
+
<div class="cw-panel cw-grow"></div></section></div>`;
|
|
19
37
|
const panel = host.querySelector('.cw-panel');
|
|
20
|
-
const
|
|
38
|
+
const views = host.querySelector('.cw-views');
|
|
39
|
+
let current = '';
|
|
40
|
+
let stop;
|
|
41
|
+
onClose(() => stop?.());
|
|
42
|
+
const open = (name, view = 'all') => {
|
|
43
|
+
current = name;
|
|
21
44
|
host.querySelectorAll('[data-set]').forEach((button) => button.setAttribute('aria-current', String(button.dataset.set === name)));
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
host.querySelectorAll('[data-view]').forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.view === view)));
|
|
46
|
+
views.hidden = !histories.some((history) => history.entitySet === name && history.softDelete);
|
|
47
|
+
setState({ set: name }, label(t, name));
|
|
48
|
+
stop?.();
|
|
49
|
+
stop = undefined;
|
|
50
|
+
// a fresh element per set: the old table takes its listeners along
|
|
51
|
+
const content = document.createElement('div');
|
|
52
|
+
content.className = 'cw-fill';
|
|
53
|
+
panel.replaceChildren(content);
|
|
54
|
+
if (view === 'deleted')
|
|
55
|
+
return renderDeleted(coworkee, content, name);
|
|
56
|
+
const entityType = histories.find((history) => history.entitySet === name)?.entityType;
|
|
57
|
+
renderEntityTable(coworkee, content, { entitySet: name, liveType: entityType, onOpen: (row) => openRecord(coworkee, name, row) }, (callback) => { stop = callback; });
|
|
29
58
|
};
|
|
30
|
-
host.
|
|
31
|
-
const
|
|
59
|
+
host.addEventListener('click', (event) => {
|
|
60
|
+
const target = event.target;
|
|
61
|
+
const set = target.closest('[data-set]')?.dataset.set;
|
|
62
|
+
const view = target.closest('[data-view]')?.dataset.view;
|
|
63
|
+
const deleted = target.closest('[data-deleted]');
|
|
32
64
|
if (set)
|
|
33
65
|
open(set);
|
|
66
|
+
else if (view)
|
|
67
|
+
open(current, view);
|
|
68
|
+
else if (deleted)
|
|
69
|
+
coworkee.airWorks.openApp(recordAppId, { set: current, id: deleted.dataset.deleted, title: deleted.dataset.title ?? '' });
|
|
34
70
|
});
|
|
35
71
|
const first = intent?.set && sets.some((set) => set.name === intent.set) ? intent.set : sets[0]?.name;
|
|
36
72
|
if (first)
|
|
@@ -49,6 +85,6 @@ export const entityApp = (options) => (coworkee) => ({
|
|
|
49
85
|
render: ({ host, onClose }) => {
|
|
50
86
|
host.classList.add('cw-host');
|
|
51
87
|
host.innerHTML = '<div class="cw-app"></div>';
|
|
52
|
-
renderEntityTable(coworkee, host.firstElementChild, options, onClose);
|
|
88
|
+
renderEntityTable(coworkee, host.firstElementChild, { onOpen: (row) => openRecord(coworkee, options.entitySet, row), ...options }, onClose);
|
|
53
89
|
},
|
|
54
90
|
});
|
package/dist/apps/files.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare const uploadFile: (file: File, folderId?: string) => Promise<{
|
|
|
9
9
|
size: number | string;
|
|
10
10
|
createdAt: string;
|
|
11
11
|
createdBy: null | string;
|
|
12
|
+
isPublic: boolean;
|
|
12
13
|
}>;
|
|
13
14
|
/** The folders and files of the organisation: browse, upload (also by dropping files), rename, delete, download and preview. */
|
|
14
15
|
export declare const filesApp: (coworkee: Coworkee) => CoworkeeApp;
|
package/dist/apps/files.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { apiRequest, unwrap } from '@coworkee/client';
|
|
2
2
|
import { bytes, display, errorText, esc } from "../dom.js";
|
|
3
|
+
import { confirmDialog, promptDialog } from "../form.js";
|
|
3
4
|
import { filePreviewAppId } from "./file-preview.js";
|
|
4
5
|
export const fileContentUrl = (id, download = false) => `/api/v1/files/${encodeURIComponent(id)}/content${download ? '?download=true' : ''}`;
|
|
5
6
|
/** Uploads one file into a folder (the API takes the raw body). */
|
|
@@ -53,7 +54,7 @@ export const filesApp = (coworkee) => ({
|
|
|
53
54
|
...content.folders.map((folder) => `<tr data-folder="${esc(folder.id)}" data-name="${esc(folder.name)}"><td><span class="cw-icon">📁</span>${esc(folder.name)}</td><td></td><td>${esc(display(folder.createdAt, culture))}</td>${actions('folder', folder.id)}</tr>`),
|
|
54
55
|
...content.files.map((file) => `<tr data-file="${esc(file.id)}" data-name="${esc(file.name)}" data-type="${esc(file.contentType)}"><td><span class="cw-icon">📄</span>${esc(file.name)}</td><td>${esc(bytes(file.size, culture))}</td><td>${esc(display(file.createdAt, culture))}</td>${actions('file', file.id)}</tr>`),
|
|
55
56
|
];
|
|
56
|
-
body.innerHTML = rows.join('') || `<tr><td colspan="4" class="cw-
|
|
57
|
+
body.innerHTML = rows.join('') || `<tr><td colspan="4" class="cw-empty">${esc(content.canUpload ? t('Drop files here') : t('(empty)'))}</td></tr>`;
|
|
57
58
|
};
|
|
58
59
|
const run = async (work) => {
|
|
59
60
|
try {
|
|
@@ -66,7 +67,6 @@ export const filesApp = (coworkee) => ({
|
|
|
66
67
|
};
|
|
67
68
|
const upload = (files) => run(async () => { for (const file of [...files])
|
|
68
69
|
await uploadFile(file, folderId()); });
|
|
69
|
-
// shortcut: names and confirmations through the browser's prompt/confirm, an AirWorks dialog once one is wanted
|
|
70
70
|
host.addEventListener('click', (event) => {
|
|
71
71
|
const target = event.target;
|
|
72
72
|
const action = target.closest('[data-action]')?.dataset.action;
|
|
@@ -80,9 +80,10 @@ export const filesApp = (coworkee) => ({
|
|
|
80
80
|
else if (action === 'refresh')
|
|
81
81
|
void load();
|
|
82
82
|
else if (action === 'folder') {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
void promptDialog(coworkee, t('New folder'), 'Name').then((name) => {
|
|
84
|
+
if (name?.trim())
|
|
85
|
+
void run(() => api.POST('/api/v1/files/folders', { body: { parentId: folderId() ?? null, name: name.trim() } }).then(unwrap));
|
|
86
|
+
});
|
|
86
87
|
}
|
|
87
88
|
else if (crumb !== undefined) {
|
|
88
89
|
path.splice(Number(crumb) + 1);
|
|
@@ -91,19 +92,22 @@ export const filesApp = (coworkee) => ({
|
|
|
91
92
|
else if (rename) {
|
|
92
93
|
const [kind, id] = rename.split(':');
|
|
93
94
|
const row = target.closest('tr');
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
void promptDialog(coworkee, t('Rename'), 'Name', row.dataset.name).then((value) => {
|
|
96
|
+
const name = value?.trim();
|
|
97
|
+
if (!name || name === row.dataset.name)
|
|
98
|
+
return;
|
|
96
99
|
void run(() => (kind === 'folder'
|
|
97
100
|
? api.PUT('/api/v1/files/folders/{id}/name', { params: { path: { id } }, body: { name } })
|
|
98
101
|
: api.PUT('/api/v1/files/{id}/name', { params: { path: { id } }, body: { name } })).then(unwrap));
|
|
99
|
-
}
|
|
102
|
+
});
|
|
100
103
|
}
|
|
101
104
|
else if (remove) {
|
|
102
105
|
const [kind, id] = remove.split(':');
|
|
103
106
|
const name = target.closest('tr').dataset.name;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
void confirmDialog(coworkee, kind === 'folder' ? t("Delete the folder '{0}' and everything in it?", name) : t("Delete the file '{0}'?", name), t('Delete')).then((ok) => {
|
|
108
|
+
if (ok)
|
|
109
|
+
void run(() => api.POST('/api/v1/files/delete', { body: kind === 'folder' ? { fileIds: [], folderIds: [id] } : { fileIds: [id], folderIds: [] } }).then(unwrap));
|
|
110
|
+
});
|
|
107
111
|
}
|
|
108
112
|
else if (!target.closest('a, button')) {
|
|
109
113
|
const row = target.closest('tr[data-folder], tr[data-file]');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Coworkee, CoworkeeApp } from '../coworkee.ts';
|
|
2
|
+
export declare const inboxAppId = "coworkee.inbox";
|
|
3
|
+
/** What waits for the user: approvals to decide and notifications, live; the count shows on the tile. */
|
|
4
|
+
export declare const inboxApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { realtimeEvents, realtimeTopics, unwrap } from '@coworkee/client';
|
|
2
|
+
import { display, errorText, esc, label } from "../dom.js";
|
|
3
|
+
import { icons } from "../icons.js";
|
|
4
|
+
import { decide, recordAppId } from "./record.js";
|
|
5
|
+
import { openWebPage } from "./web.js";
|
|
6
|
+
export const inboxAppId = 'coworkee.inbox';
|
|
7
|
+
function approvalsHtml(coworkee, approvals) {
|
|
8
|
+
const { t, culture } = coworkee;
|
|
9
|
+
return approvals.map((approval) => `<li data-approval="${esc(approval.id)}" data-set="${esc(approval.entityType)}" data-entity="${esc(approval.entityId)}">
|
|
10
|
+
<div class="cw-grow"><button class="cw-link" data-open>${esc(approval.title)}</button><br/>
|
|
11
|
+
<small class="cw-muted">${esc(label(t, approval.entityType))} · ${esc(t(approval.state))} · ${esc(t('Requested {0}', display(approval.createdAt, culture)))}${approval.requiresAll
|
|
12
|
+
? ` · ${esc(t('{0} of {1} approved', approval.approvers.filter((a) => a.approved === true).length, approval.approvers.filter((a) => !a.escalation).length))}` : ''}</small>
|
|
13
|
+
${approval.dueAt ? `<br/><small class="${new Date(approval.dueAt) < new Date() ? 'cw-error' : 'cw-muted'}">${esc(t('Due {0}', display(approval.dueAt, culture)))}</small>` : ''}</div>
|
|
14
|
+
${approval.canDecide ? `<span class="cw-decide"><button class="cw-primary" data-approve="true">${esc(t('Approve'))}</button><button data-approve="false">${esc(t('Reject'))}</button></span>` : ''}</li>`).join('')
|
|
15
|
+
|| `<li class="cw-empty">${esc(t('Nothing waits for your decision.'))}</li>`;
|
|
16
|
+
}
|
|
17
|
+
function notificationsHtml(coworkee, items) {
|
|
18
|
+
const { t, culture } = coworkee;
|
|
19
|
+
const linked = (item) => !!item.link && !!coworkee.user.appUrl;
|
|
20
|
+
return items.map((item) => {
|
|
21
|
+
const title = esc(t(item.title, ...(item.arguments ?? [])));
|
|
22
|
+
return `<li class="${item.readAt ? '' : 'unread'}" data-id="${esc(item.id)}" ${linked(item) ? `data-link="${esc(item.link)}"` : ''}>
|
|
23
|
+
<div class="cw-grow">${linked(item) ? `<button class="cw-link" data-follow>${title}</button>` : `<strong>${title}</strong>`}
|
|
24
|
+
${item.body ? `<p>${esc(t(item.body, ...(item.arguments ?? [])))}</p>` : '<br/>'}<small class="cw-muted">${esc(display(item.createdAt, culture))}</small></div>
|
|
25
|
+
<span class="cw-row-actions">${item.readAt ? '' : `<button data-read title="${esc(t('Mark as read'))}" aria-label="${esc(t('Mark as read'))}">✓</button>`}
|
|
26
|
+
<button data-delete title="${esc(t('Delete'))}" aria-label="${esc(t('Delete'))}">✕</button></span></li>`;
|
|
27
|
+
}).join('') || `<li class="cw-empty">${esc(t('No notifications yet.'))}</li>`;
|
|
28
|
+
}
|
|
29
|
+
/** What waits for the user: approvals to decide and notifications, live; the count shows on the tile. */
|
|
30
|
+
export const inboxApp = (coworkee) => ({
|
|
31
|
+
id: inboxAppId,
|
|
32
|
+
title: coworkee.t('Inbox'),
|
|
33
|
+
icon: icons.inbox,
|
|
34
|
+
accent: '#a2273b',
|
|
35
|
+
width: 640,
|
|
36
|
+
height: 600,
|
|
37
|
+
singleInstance: true,
|
|
38
|
+
probe: '/api/v1/notifications/unread-count',
|
|
39
|
+
render: ({ host, intent, setState, onClose }) => {
|
|
40
|
+
const { t, api } = coworkee;
|
|
41
|
+
const withApprovals = coworkee.can('Workflows.View');
|
|
42
|
+
let tab = intent?.tab === 'notifications' || !withApprovals ? 'notifications' : 'approvals';
|
|
43
|
+
host.classList.add('cw-host');
|
|
44
|
+
host.innerHTML = `<div class="cw-app cw-inbox"><div class="cw-toolbar"><nav class="cw-tabs" role="tablist"></nav><span class="cw-spacer"></span>
|
|
45
|
+
<button data-action="all" hidden>${esc(t('Mark all as read'))}</button><button data-action="refresh" title="${esc(t('Refresh'))}" aria-label="${esc(t('Refresh'))}">↻</button></div>
|
|
46
|
+
<ul class="cw-feed"></ul></div>`;
|
|
47
|
+
const list = host.querySelector('ul');
|
|
48
|
+
const tabs = host.querySelector('nav');
|
|
49
|
+
const load = async () => {
|
|
50
|
+
try {
|
|
51
|
+
const [approvals, notifications] = await Promise.all([
|
|
52
|
+
withApprovals ? coworkee.request('/api/v1/workflows/approvals').catch(() => []) : Promise.resolve([]),
|
|
53
|
+
api.GET('/api/v1/notifications', { params: { query: { Page: 1, PageSize: 50 } } }).then(unwrap),
|
|
54
|
+
]);
|
|
55
|
+
const open = approvals.filter((approval) => approval.canDecide).length;
|
|
56
|
+
const unread = notifications.items.filter((item) => !item.readAt).length;
|
|
57
|
+
const sections = [...(withApprovals ? [['approvals', t('Approvals'), open]] : []), ['notifications', t('Notifications'), unread]];
|
|
58
|
+
tabs.innerHTML = sections.map(([id, title, count]) => `<button role="tab" data-tab="${id}" aria-selected="${id === tab}">${esc(title)}${count ? ` <span class="cw-badge">${count}</span>` : ''}</button>`).join('');
|
|
59
|
+
host.querySelector('[data-action="all"]').hidden = tab !== 'notifications' || unread === 0;
|
|
60
|
+
list.innerHTML = tab === 'approvals' ? approvalsHtml(coworkee, approvals) : notificationsHtml(coworkee, notifications.items);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
list.innerHTML = `<li class="cw-error">${esc(errorText(error))}</li>`;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const run = async (work) => {
|
|
67
|
+
try {
|
|
68
|
+
await work();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
coworkee.notify(errorText(error), 'error');
|
|
72
|
+
}
|
|
73
|
+
await load();
|
|
74
|
+
void coworkee.refreshBadges();
|
|
75
|
+
};
|
|
76
|
+
host.addEventListener('click', (event) => {
|
|
77
|
+
const target = event.target;
|
|
78
|
+
const item = target.closest('li');
|
|
79
|
+
const id = item?.dataset.id;
|
|
80
|
+
const action = target.closest('[data-action]')?.dataset.action;
|
|
81
|
+
const nextTab = target.closest('[data-tab]')?.dataset.tab;
|
|
82
|
+
const approve = target.closest('[data-approve]')?.dataset.approve;
|
|
83
|
+
if (nextTab) {
|
|
84
|
+
tab = nextTab;
|
|
85
|
+
setState({ tab });
|
|
86
|
+
void load();
|
|
87
|
+
}
|
|
88
|
+
else if (action === 'refresh')
|
|
89
|
+
void load();
|
|
90
|
+
else if (action === 'all')
|
|
91
|
+
void run(() => api.POST('/api/v1/notifications/read-all'));
|
|
92
|
+
else if (item?.dataset.approval && approve)
|
|
93
|
+
void run(() => decide(coworkee, item.dataset.approval, approve === 'true'));
|
|
94
|
+
else if (item?.dataset.approval && target.closest('[data-open]'))
|
|
95
|
+
coworkee.airWorks.openApp(recordAppId, { set: item.dataset.set, id: item.dataset.entity });
|
|
96
|
+
else if (id && target.closest('[data-read]'))
|
|
97
|
+
void run(() => api.POST('/api/v1/notifications/{id}/read', { params: { path: { id } } }));
|
|
98
|
+
else if (id && target.closest('[data-delete]'))
|
|
99
|
+
void run(() => api.DELETE('/api/v1/notifications/{id}', { params: { path: { id } } }));
|
|
100
|
+
else if (id && target.closest('[data-follow]')) {
|
|
101
|
+
openWebPage(coworkee, item.dataset.link, target.textContent ?? undefined);
|
|
102
|
+
if (item.classList.contains('unread'))
|
|
103
|
+
void run(() => api.POST('/api/v1/notifications/{id}/read', { params: { path: { id } } }));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// new notifications and approval requests arrive on the user's topic
|
|
107
|
+
const stop = coworkee.user.userId ? coworkee.realtime.subscribe(realtimeTopics.user(coworkee.user.userId), ({ type }) => {
|
|
108
|
+
if (type === realtimeEvents.entityChanged)
|
|
109
|
+
void load();
|
|
110
|
+
}) : undefined;
|
|
111
|
+
onClose(() => stop?.());
|
|
112
|
+
void load();
|
|
113
|
+
},
|
|
114
|
+
});
|
package/dist/apps/profile.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { Coworkee
|
|
2
|
-
/** Name and phone of the signed-in user, the account pages of the auth server and sign out. */
|
|
3
|
-
export declare
|
|
1
|
+
import type { Coworkee } from '../coworkee.ts';
|
|
2
|
+
/** Name and phone of the signed-in user, the account pages of the auth server and sign out; the first tab of the settings. */
|
|
3
|
+
export declare function renderProfile(coworkee: Coworkee, host: HTMLElement): Promise<void>;
|
package/dist/apps/profile.js
CHANGED
|
@@ -1,40 +1,30 @@
|
|
|
1
1
|
import { unwrap } from '@coworkee/client';
|
|
2
2
|
import { errorText, esc } from "../dom.js";
|
|
3
|
-
/** Name and phone of the signed-in user, the account pages of the auth server and sign out. */
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
unwrap(await api.PUT('/api/v1/identity/me', { body: { firstName: values.firstName || null, lastName: values.lastName || null, phoneNumber: values.phoneNumber || null, address: profile.address ?? null } }));
|
|
32
|
-
coworkee.notify(t('Saved'));
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
coworkee.notify(errorText(error), 'error');
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
host.querySelector('[data-action="logout"]').addEventListener('click', () => void coworkee.airWorks.logout());
|
|
39
|
-
},
|
|
40
|
-
});
|
|
3
|
+
/** Name and phone of the signed-in user, the account pages of the auth server and sign out; the first tab of the settings. */
|
|
4
|
+
export async function renderProfile(coworkee, host) {
|
|
5
|
+
const { t, api } = coworkee;
|
|
6
|
+
const profile = unwrap(await api.GET('/api/v1/identity/me'));
|
|
7
|
+
const input = (name, label, value) => `<div class="cw-field"><label for="cw-profile-${name}">${esc(t(label))}</label><input id="cw-profile-${name}" name="${name}" value="${esc(value ?? '')}"/></div>`;
|
|
8
|
+
host.innerHTML = `<div class="cw-profile"><header class="cw-profile-head">
|
|
9
|
+
${profile.avatarUrl ? `<img class="cw-avatar" src="${esc(profile.avatarUrl)}" alt=""/>` : `<span class="cw-avatar">${esc((coworkee.user.name ?? profile.email).charAt(0).toUpperCase())}</span>`}
|
|
10
|
+
<div><strong>${esc(coworkee.user.name ?? '')}</strong><br/><span class="cw-muted">${esc(profile.email)}</span></div></header>
|
|
11
|
+
<form>${input('firstName', 'First name', profile.firstName)}${input('lastName', 'Last name', profile.lastName)}${input('phoneNumber', 'Phone', profile.phoneNumber)}
|
|
12
|
+
<div class="cw-actions">
|
|
13
|
+
${coworkee.user.manageUrl ? `<a class="cw-button" href="${esc(coworkee.user.manageUrl)}" target="_blank" rel="noopener">${esc(t('Account'))}</a>` : ''}
|
|
14
|
+
<button type="button" data-action="logout">${esc(t('Sign out'))}</button>
|
|
15
|
+
<button type="submit" class="cw-primary">${esc(t('Save'))}</button>
|
|
16
|
+
</div></form></div>`;
|
|
17
|
+
const form = host.querySelector('form');
|
|
18
|
+
form.addEventListener('submit', async (event) => {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
const values = Object.fromEntries(new FormData(form));
|
|
21
|
+
try {
|
|
22
|
+
unwrap(await api.PUT('/api/v1/identity/me', { body: { firstName: values.firstName || null, lastName: values.lastName || null, phoneNumber: values.phoneNumber || null, address: profile.address ?? null } }));
|
|
23
|
+
coworkee.notify(t('Saved'));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
coworkee.notify(errorText(error), 'error');
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
host.querySelector('[data-action="logout"]').addEventListener('click', () => void coworkee.airWorks.logout());
|
|
30
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Coworkee, CoworkeeApp } from '../coworkee.ts';
|
|
2
|
+
export declare const recordAppId = "coworkee.record";
|
|
3
|
+
/** Approves or rejects an approval with an optional comment; false when the user cancelled. */
|
|
4
|
+
export declare function decide(coworkee: Coworkee, id: string, approve: boolean): Promise<boolean>;
|
|
5
|
+
/** Renders one record of an OData entity set: its fields, its history with undo and restore, its workflow with steps and approvals. */
|
|
6
|
+
export declare function renderRecord(coworkee: Coworkee, host: HTMLElement, set: string, id: string, onTitle?: (title: string) => void): () => void;
|
|
7
|
+
/** One record in a window, opened from the records, the search, the inbox and the audit log with the intent { set, id }. */
|
|
8
|
+
export declare const recordApp: (coworkee: Coworkee) => CoworkeeApp;
|