@coworkee/airworks 0.0.1033 → 0.0.1035
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { errorText, esc } from "../dom.js";
|
|
2
2
|
import { formDialog } from "../form.js";
|
|
3
3
|
import { icons } from "../icons.js";
|
|
4
|
-
import { createWidget, hasData, isNumber, place, presets, reportIcons } from "./report-model.js";
|
|
4
|
+
import { createWidget, fits, hasData, isNumber, place, presets, reportIcons } from "./report-model.js";
|
|
5
5
|
import { humanize, rangeSelect, renderGrid, renderWidget, reportAppId, reportDesignerAppId } from "./reports.js";
|
|
6
6
|
const reports = '/api/v1/reports';
|
|
7
7
|
const json = (method, body) => ({ method, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
@@ -32,7 +32,8 @@ function panelHtml(coworkee, widget, sources) {
|
|
|
32
32
|
widget.type === 'Chart' ? select('options.chart', 'Chart', presets.filter((preset) => preset.type === 'Chart').map((preset) => [preset.chart, t(preset.name)]), widget.options.chart) : '',
|
|
33
33
|
];
|
|
34
34
|
if (q && hasData(widget.type)) {
|
|
35
|
-
parts.push(`<h4>${esc(t('Data'))}</h4>`, select('query.entitySet', 'Data source', sources.
|
|
35
|
+
parts.push(`<h4>${esc(t('Data'))}</h4>`, select('query.entitySet', 'Data source', sources.filter((source) => fits(widget.type, source))
|
|
36
|
+
.map((source) => [source.entitySet, `${source.computed ? '∑ ' : ''}${t(source.label)}`]), q.entitySet));
|
|
36
37
|
if (widget.type === 'Chart' || widget.type === 'Pivot')
|
|
37
38
|
parts.push(groupBy('groupBy', widget.type === 'Pivot' ? 'Rows' : 'Group by', false), groupBy('splitBy', widget.type === 'Pivot' ? 'Columns' : 'Series', true));
|
|
38
39
|
if (widget.type === 'Kpi' || widget.type === 'Chart' || widget.type === 'Pivot') {
|
|
@@ -113,7 +114,7 @@ export const reportDesignerApp = (coworkee) => ({
|
|
|
113
114
|
const panel = host.querySelector('.cw-designer-panel');
|
|
114
115
|
const title = () => setTitle(report.name || t('New report'));
|
|
115
116
|
const draw = () => {
|
|
116
|
-
renderGrid(coworkee, grid, widgets(), onClose, report.definition.dateRange, selected);
|
|
117
|
+
renderGrid(coworkee, grid, widgets(), onClose, report.definition.dateRange, selected, sources);
|
|
117
118
|
grid.querySelectorAll('[data-resize]').forEach((handle) => { handle.hidden = false; });
|
|
118
119
|
if (widgets().length === 0)
|
|
119
120
|
grid.innerHTML = `<p class="cw-muted">${esc(t('This report has no widgets yet.'))}</p>`;
|
|
@@ -143,7 +144,7 @@ export const reportDesignerApp = (coworkee) => ({
|
|
|
143
144
|
return draw();
|
|
144
145
|
element.querySelector('header').firstChild.textContent = widget.title ?? '';
|
|
145
146
|
const body = element.querySelector('.cw-widget-body');
|
|
146
|
-
renderWidget(coworkee, widget, body, onClose, report.definition.dateRange).catch((error) => { body.innerHTML = `<p class="cw-error">${esc(errorText(error))}</p>`; });
|
|
147
|
+
renderWidget(coworkee, widget, body, onClose, report.definition.dateRange, sources).catch((error) => { body.innerHTML = `<p class="cw-error">${esc(errorText(error))}</p>`; });
|
|
147
148
|
};
|
|
148
149
|
const settings = async () => formDialog(coworkee, {
|
|
149
150
|
title: t('Report settings'),
|
|
@@ -269,7 +270,8 @@ export const reportDesignerApp = (coworkee) => ({
|
|
|
269
270
|
return;
|
|
270
271
|
}
|
|
271
272
|
const structural = input.tagName === 'SELECT' || input.type === 'checkbox';
|
|
272
|
-
|
|
273
|
+
const unfit = sources.find((source) => source.entitySet === widget.query?.entitySet && !fits(widget.type, source));
|
|
274
|
+
if (input.dataset.path === 'type' && hasData(widget.type) && (!widget.query || unfit))
|
|
273
275
|
widget.query = createWidget(presets.find((preset) => preset.type === widget.type), sources, []).query;
|
|
274
276
|
if (input.dataset.path === 'query.entitySet')
|
|
275
277
|
Object.assign(widget.query, { groupBy: null, splitBy: null, dateField: null, columns: [], orderBy: null, filter: null, rules: null, facets: [], aggregates: [{ function: 'Count', field: null, label: null }] });
|
|
@@ -17,6 +17,8 @@ export declare const reportIcons: Record<string, string>;
|
|
|
17
17
|
export declare const icon: (svg?: string | null) => string;
|
|
18
18
|
export declare const hasData: (type: Widget["type"]) => type is "Kpi" | "Chart" | "Table" | "DataTable" | "Pivot";
|
|
19
19
|
export declare const isNumber: (field: Field) => boolean;
|
|
20
|
+
/** The data table pages through OData, which computed sources (the app's own, `computed`) do not have. */
|
|
21
|
+
export declare const fits: (type: Widget["type"], source: Source) => boolean;
|
|
20
22
|
/** A new widget below the others with a first query that already shows something, like the web app's palette. */
|
|
21
23
|
export declare function createWidget(preset: Preset, sources: readonly Source[], existing: readonly Widget[], entitySet?: string): Widget;
|
|
22
24
|
/** A widget moved or resized on the 12 column grid stays inside it and at least one cell big. */
|
|
@@ -36,5 +38,11 @@ export declare function resolveRange(range: DateRange, today?: Date): {
|
|
|
36
38
|
from: string;
|
|
37
39
|
to: string;
|
|
38
40
|
};
|
|
39
|
-
/** The widget filter with the report's date range on the widget's date field. */
|
|
40
|
-
export declare function widgetFilter(query: NonNullable<Widget['query']>, range?: DateRange | null, today?: Date): string | null;
|
|
41
|
+
/** The widget filter with the report's date range on the widget's date field (plain days for date-only fields). */
|
|
42
|
+
export declare function widgetFilter(query: NonNullable<Widget['query']>, range?: DateRange | null, today?: Date, dateOnly?: boolean): string | null;
|
|
43
|
+
/** The filter of a widget on its source: the date range as plain days when the source's date field is date-only. */
|
|
44
|
+
export declare const sourceFilter: (query: NonNullable<Widget["query"]>, source: Source | undefined, range?: DateRange | null, today?: Date) => string | null;
|
|
45
|
+
/** The columns of a table: the chosen ones, otherwise the first six plain fields of the source. */
|
|
46
|
+
export declare const tableColumns: (query: NonNullable<Widget["query"]>, source?: Source) => string[];
|
|
47
|
+
/** The first page of a table on a computed source, for POST /api/v1/reports/rows. */
|
|
48
|
+
export declare const rowsRequest: (query: NonNullable<Widget["query"]>, source: Source, range?: DateRange | null, today?: Date, top?: number) => Schemas["ReportRowsRequest"];
|
|
@@ -34,6 +34,8 @@ export const reportIcons = {
|
|
|
34
34
|
export const icon = (svg) => svg ? `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="cw-report-icon">${svg}</svg>` : '';
|
|
35
35
|
export const hasData = (type) => type === 'Kpi' || type === 'Chart' || type === 'Table' || type === 'DataTable' || type === 'Pivot';
|
|
36
36
|
export const isNumber = (field) => field.kind === 'Integer' || field.kind === 'Number';
|
|
37
|
+
/** The data table pages through OData, which computed sources (the app's own, `computed`) do not have. */
|
|
38
|
+
export const fits = (type, source) => type !== 'DataTable' || !source.computed;
|
|
37
39
|
let counter = 0;
|
|
38
40
|
const id = () => (globalThis.crypto?.randomUUID?.() ?? `w${Date.now()}${counter++}`).replace(/-/g, '').slice(0, 12);
|
|
39
41
|
/** A new widget below the others with a first query that already shows something, like the web app's palette. */
|
|
@@ -43,7 +45,7 @@ export function createWidget(preset, sources, existing, entitySet) {
|
|
|
43
45
|
w: preset.w, h: preset.h, query: null,
|
|
44
46
|
options: { chart: preset.chart, format: 'Number', currency: 'EUR', showLegend: true, showDataLabels: false, palette: [], compareToPrevious: false, higherIsBetter: true, sparkline: 'None', target: null, view: 'Table', crossFilter: false, text: preset.type === 'Markdown' ? '## Notes\nWrite **markdown** here.' : null, url: null },
|
|
45
47
|
};
|
|
46
|
-
const source = sources.find((s) => s.entitySet === entitySet) ?? sources
|
|
48
|
+
const source = sources.find((s) => s.entitySet === entitySet && fits(preset.type, s)) ?? sources.find((s) => fits(preset.type, s));
|
|
47
49
|
if (!hasData(preset.type) || !source)
|
|
48
50
|
return widget;
|
|
49
51
|
const date = source.fields.find((f) => f.kind === 'Date');
|
|
@@ -97,12 +99,24 @@ export function resolveRange(range, today = new Date()) {
|
|
|
97
99
|
})();
|
|
98
100
|
return { from: iso(from), to: iso(to) };
|
|
99
101
|
}
|
|
100
|
-
/** The widget filter with the report's date range on the widget's date field. */
|
|
101
|
-
export function widgetFilter(query, range, today) {
|
|
102
|
+
/** The widget filter with the report's date range on the widget's date field (plain days for date-only fields). */
|
|
103
|
+
export function widgetFilter(query, range, today, dateOnly = false) {
|
|
102
104
|
const parts = [query.filter, ...(query.facets ?? []).map((facet) => facet.oData)].filter((part) => !!part);
|
|
103
105
|
if (range && query.dateField) {
|
|
104
106
|
const { from, to } = resolveRange(range, today);
|
|
105
|
-
|
|
107
|
+
const time = dateOnly ? '' : 'T00:00:00Z';
|
|
108
|
+
parts.push(`${query.dateField} ge ${from}${time} and ${query.dateField} lt ${to}${time}`);
|
|
106
109
|
}
|
|
107
110
|
return parts.length === 0 ? null : parts.map((part) => `(${part})`).join(' and ');
|
|
108
111
|
}
|
|
112
|
+
/** The filter of a widget on its source: the date range as plain days when the source's date field is date-only. */
|
|
113
|
+
export const sourceFilter = (query, source, range, today) => widgetFilter(query, range, today, !!source?.fields.find((field) => field.path === query.dateField)?.dateOnly);
|
|
114
|
+
/** The columns of a table: the chosen ones, otherwise the first six plain fields of the source. */
|
|
115
|
+
export const tableColumns = (query, source) => query.columns?.length
|
|
116
|
+
? query.columns.map(String)
|
|
117
|
+
: (source?.fields ?? []).filter((field) => field.kind !== 'Guid' && !field.path.includes('/')).slice(0, 6).map((field) => field.path);
|
|
118
|
+
/** The first page of a table on a computed source, for POST /api/v1/reports/rows. */
|
|
119
|
+
export const rowsRequest = (query, source, range, today, top = 50) => ({
|
|
120
|
+
entitySet: String(query.entitySet), columns: tableColumns(query, source), filter: sourceFilter(query, source, range, today),
|
|
121
|
+
orderBy: query.orderBy ?? null, descending: !!query.descending, skip: 0, top,
|
|
122
|
+
});
|
package/dist/apps/reports.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AppContext } from '@fgilde/airworks';
|
|
2
2
|
import type { Coworkee, CoworkeeApp } from '../coworkee.ts';
|
|
3
3
|
import { humanize } from '../dom.ts';
|
|
4
|
-
import { type DateRange, type Widget } from './report-model.ts';
|
|
4
|
+
import { type DateRange, type Source, type Widget } from './report-model.ts';
|
|
5
5
|
export declare const reportAppId = "coworkee.report";
|
|
6
6
|
export declare const reportDesignerAppId = "coworkee.report-designer";
|
|
7
7
|
export declare const datePresets: string[];
|
|
@@ -9,9 +9,9 @@ export { humanize };
|
|
|
9
9
|
/** A value in the widget's number format. */
|
|
10
10
|
export declare function formatNumber(value: unknown, format: string | undefined, culture: string, currency?: string): string;
|
|
11
11
|
/** Renders one widget into body: KPI, chart (bars, lines, areas, pies), table, pivot, data table, text, image or web page. */
|
|
12
|
-
export declare function renderWidget(coworkee: Coworkee, widget: Widget, body: HTMLElement, onClose: AppContext['onClose'], range?: DateRange | null): Promise<void>;
|
|
12
|
+
export declare function renderWidget(coworkee: Coworkee, widget: Widget, body: HTMLElement, onClose: AppContext['onClose'], range?: DateRange | null, sources?: readonly Source[]): Promise<void>;
|
|
13
13
|
/** Widgets on the 12 column grid, each loading its data. */
|
|
14
|
-
export declare function renderGrid(coworkee: Coworkee, grid: HTMLElement, widgets: readonly Widget[], onClose: AppContext['onClose'], range?: DateRange | null, selected?: string): void;
|
|
14
|
+
export declare function renderGrid(coworkee: Coworkee, grid: HTMLElement, widgets: readonly Widget[], onClose: AppContext['onClose'], range?: DateRange | null, selected?: string, sources?: readonly Source[]): void;
|
|
15
15
|
export declare const rangeSelect: (coworkee: Coworkee, range?: DateRange | null) => string;
|
|
16
16
|
/** One report: its widgets in their places on the grid, the date range to look at, and the designer for those who may change it. */
|
|
17
17
|
export declare const reportApp: (coworkee: Coworkee) => CoworkeeApp;
|
package/dist/apps/reports.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { crudApp } from "../crud.js";
|
|
2
|
-
import { errorText, esc, humanize } from "../dom.js";
|
|
2
|
+
import { display, errorText, esc, humanize } from "../dom.js";
|
|
3
3
|
import { renderEntityTable } from "../table.js";
|
|
4
|
-
import { icon,
|
|
4
|
+
import { icon, rowsRequest, sourceFilter } from "./report-model.js";
|
|
5
5
|
const reports = '/api/v1/reports';
|
|
6
6
|
const json = (method, body) => ({ method, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
7
7
|
export const reportAppId = 'coworkee.report';
|
|
@@ -21,10 +21,10 @@ export function formatNumber(value, format, culture, currency) {
|
|
|
21
21
|
default: return number.toLocaleString(culture, { maximumFractionDigits: 2 });
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
const query = (coworkee, widget, range) => {
|
|
24
|
+
const query = (coworkee, widget, range, set) => {
|
|
25
25
|
const source = widget.query;
|
|
26
26
|
return coworkee.request(`${reports}/query`, json('POST', {
|
|
27
|
-
entitySet: source.entitySet, filter:
|
|
27
|
+
entitySet: source.entitySet, filter: sourceFilter(source, set, range), groupBy: source.groupBy ?? null, splitBy: source.splitBy ?? null,
|
|
28
28
|
aggregates: source.aggregates, sortBy: source.sortBy, descending: source.descending, top: source.top ?? null,
|
|
29
29
|
}));
|
|
30
30
|
};
|
|
@@ -67,8 +67,19 @@ function table(coworkee, widget, result) {
|
|
|
67
67
|
return `<table class="cw-table"><thead><tr><th></th>${aggregates.map((aggregate) => `<th>${esc(coworkee.t(aggregate.label ?? `${humanize(aggregate.function)}${aggregate.field ? ` ${aggregate.field}` : ''}`))}</th>`).join('')}</tr></thead>
|
|
68
68
|
<tbody>${result.rows.map((row) => `<tr><td>${esc(label(row))}</td>${row.values.map((value) => `<td class="cw-number">${esc(formatNumber(value, widget.options.format, coworkee.culture, widget.options.currency))}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
|
|
69
69
|
}
|
|
70
|
+
/** The rows of a table on a computed source, read from the report API (there is no OData set). */
|
|
71
|
+
async function rows(coworkee, widget, source, range) {
|
|
72
|
+
const request = rowsRequest(widget.query, source, range);
|
|
73
|
+
if (request.columns.length === 0)
|
|
74
|
+
return '';
|
|
75
|
+
const result = await coworkee.request(`${reports}/rows`, json('POST', request));
|
|
76
|
+
const label = (path) => source.fields.find((field) => field.path === path)?.label ?? humanize(path);
|
|
77
|
+
return `<table class="cw-table"><thead><tr>${request.columns.map((path) => `<th>${esc(coworkee.t(label(path)))}</th>`).join('')}</tr></thead>
|
|
78
|
+
<tbody>${result.rows.map((row) => `<tr>${request.columns.map((path) => `<td>${esc(display(row[path], coworkee.culture))}</td>`).join('')}</tr>`).join('')
|
|
79
|
+
|| `<tr><td colspan="${request.columns.length}" class="cw-muted">${esc(coworkee.t('Nothing found.'))}</td></tr>`}</tbody></table>`;
|
|
80
|
+
}
|
|
70
81
|
/** Renders one widget into body: KPI, chart (bars, lines, areas, pies), table, pivot, data table, text, image or web page. */
|
|
71
|
-
export async function renderWidget(coworkee, widget, body, onClose, range) {
|
|
82
|
+
export async function renderWidget(coworkee, widget, body, onClose, range, sources = []) {
|
|
72
83
|
const options = widget.options;
|
|
73
84
|
switch (widget.type) {
|
|
74
85
|
case 'Markdown':
|
|
@@ -85,12 +96,17 @@ export async function renderWidget(coworkee, widget, body, onClose, range) {
|
|
|
85
96
|
body.innerHTML = '';
|
|
86
97
|
return;
|
|
87
98
|
}
|
|
99
|
+
const source = sources.find((item) => item.entitySet === widget.query.entitySet);
|
|
100
|
+
if (source?.computed && (widget.type === 'Table' || widget.type === 'DataTable')) {
|
|
101
|
+
body.innerHTML = await rows(coworkee, widget, source, range);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
88
104
|
if (widget.type === 'DataTable') {
|
|
89
105
|
const columns = widget.query.columns?.length ? widget.query.columns.map((field) => ({ field: String(field) })) : undefined;
|
|
90
|
-
renderEntityTable(coworkee, body, { entitySet: String(widget.query.entitySet), columns, pageSize: 10, filter:
|
|
106
|
+
renderEntityTable(coworkee, body, { entitySet: String(widget.query.entitySet), columns, pageSize: 10, filter: sourceFilter(widget.query, source, range) ?? undefined, orderBy: widget.query.orderBy ? `${widget.query.orderBy}${widget.query.descending ? ' desc' : ''}` : undefined }, onClose);
|
|
91
107
|
return;
|
|
92
108
|
}
|
|
93
|
-
const result = await query(coworkee, widget, range);
|
|
109
|
+
const result = await query(coworkee, widget, range, source);
|
|
94
110
|
if (widget.type === 'Kpi') {
|
|
95
111
|
const value = result.rows[0]?.values[0];
|
|
96
112
|
const target = options.target === null || options.target === undefined ? '' : ` <small class="cw-muted">/ ${esc(formatNumber(options.target, options.format, coworkee.culture, options.currency))}</small>`;
|
|
@@ -106,14 +122,14 @@ export async function renderWidget(coworkee, widget, body, onClose, range) {
|
|
|
106
122
|
}
|
|
107
123
|
}
|
|
108
124
|
/** Widgets on the 12 column grid, each loading its data. */
|
|
109
|
-
export function renderGrid(coworkee, grid, widgets, onClose, range, selected) {
|
|
125
|
+
export function renderGrid(coworkee, grid, widgets, onClose, range, selected, sources = []) {
|
|
110
126
|
grid.innerHTML = widgets.map((widget) => `<article class="cw-widget${widget.id === selected ? ' cw-selected' : ''}" data-widget="${esc(widget.id)}"
|
|
111
127
|
style="grid-column:${Number(widget.x) + 1} / span ${widget.w};grid-row:${Number(widget.y) + 1} / span ${widget.h}">
|
|
112
128
|
<header data-drag>${esc(widget.title ?? '')}${widget.subtitle ? `<small class="cw-muted"> ${esc(widget.subtitle)}</small>` : ''}</header><div class="cw-widget-body"></div><span class="cw-resize" data-resize hidden></span></article>`).join('')
|
|
113
129
|
|| `<p class="cw-muted">${esc(coworkee.t('Nothing found.'))}</p>`;
|
|
114
130
|
for (const widget of widgets) {
|
|
115
131
|
const body = grid.querySelector(`[data-widget="${CSS.escape(widget.id)}"] .cw-widget-body`);
|
|
116
|
-
renderWidget(coworkee, widget, body, onClose, range).catch((error) => { body.innerHTML = `<p class="cw-error">${esc(errorText(error))}</p>`; });
|
|
132
|
+
renderWidget(coworkee, widget, body, onClose, range, sources).catch((error) => { body.innerHTML = `<p class="cw-error">${esc(errorText(error))}</p>`; });
|
|
117
133
|
}
|
|
118
134
|
}
|
|
119
135
|
export const rangeSelect = (coworkee, range) => `<select data-range aria-label="${esc(coworkee.t('Date range'))}">
|
|
@@ -125,7 +141,8 @@ export const reportApp = (coworkee) => ({
|
|
|
125
141
|
render: async ({ host, intent, setTitle, onClose }) => {
|
|
126
142
|
const { t } = coworkee;
|
|
127
143
|
host.classList.add('cw-host');
|
|
128
|
-
|
|
144
|
+
// the sources say which sets the app computes: their tables read the report API, not OData
|
|
145
|
+
const [report, sources] = await Promise.all([coworkee.request(`${reports}/${intent?.id}`), coworkee.request(`${reports}/sources`).catch(() => [])]);
|
|
129
146
|
let range = report.definition.dateRange ?? null;
|
|
130
147
|
setTitle(report.name);
|
|
131
148
|
host.innerHTML = `<div class="cw-app"><div class="cw-toolbar">${icon(report.icon)}<strong>${esc(report.name)}</strong><span class="cw-muted">${esc(report.description ?? '')}</span>
|
|
@@ -134,7 +151,7 @@ export const reportApp = (coworkee) => ({
|
|
|
134
151
|
<button data-action="refresh" title="${esc(t('Refresh'))}" aria-label="${esc(t('Refresh'))}">↻</button></div><div class="cw-report"></div></div>`;
|
|
135
152
|
const grid = host.querySelector('.cw-report');
|
|
136
153
|
const widgets = report.definition.widgets ?? [];
|
|
137
|
-
const draw = () => renderGrid(coworkee, grid, widgets, onClose, range);
|
|
154
|
+
const draw = () => renderGrid(coworkee, grid, widgets, onClose, range, undefined, sources);
|
|
138
155
|
host.querySelector('[data-action=refresh]').addEventListener('click', draw);
|
|
139
156
|
host.querySelector('[data-action=edit]')?.addEventListener('click', () => coworkee.airWorks.openApp(reportDesignerAppId, { id: report.id }));
|
|
140
157
|
host.querySelector('[data-range]').addEventListener('change', (event) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coworkee/airworks",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1035",
|
|
4
4
|
"description": "AirWorks web desktop for Coworkee apps: sign-in through the app's BFF, theme, texts, realtime and apps for settings, files, data, chat, reports, the assistant and the administration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://fgilde.github.io/CoworkeeLib/web/airworks/",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"test": "node --test \"test/**/*.test.ts\""
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@coworkee/client": "0.0.
|
|
43
|
+
"@coworkee/client": "0.0.1035"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@fgilde/airworks": ">=0.4.0"
|