@platforma-sdk/ui-vue 1.3.5
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/.eslintignore +2 -0
- package/.eslintrc.json +56 -0
- package/.prettierrc +6 -0
- package/CHANGELOG.md +9 -0
- package/README.md +5 -0
- package/dist/lib.js +50299 -0
- package/dist/lib.umd.cjs +248 -0
- package/dist/src/components/BlockLayout.vue.d.ts +5 -0
- package/dist/src/components/BlockLayout.vue.d.ts.map +1 -0
- package/dist/src/components/LoaderPage.vue.d.ts +3 -0
- package/dist/src/components/LoaderPage.vue.d.ts.map +1 -0
- package/dist/src/components/NotFound.vue.d.ts +3 -0
- package/dist/src/components/NotFound.vue.d.ts.map +1 -0
- package/dist/src/components/PlAgDataTable/OverlayLoading.vue.d.ts +11 -0
- package/dist/src/components/PlAgDataTable/OverlayLoading.vue.d.ts.map +1 -0
- package/dist/src/components/PlAgDataTable/OverlayNoRows.vue.d.ts +3 -0
- package/dist/src/components/PlAgDataTable/OverlayNoRows.vue.d.ts.map +1 -0
- package/dist/src/components/PlAgDataTable/PlAgDataTable.vue.d.ts +16 -0
- package/dist/src/components/PlAgDataTable/PlAgDataTable.vue.d.ts.map +1 -0
- package/dist/src/components/PlAgDataTable/index.d.ts +3 -0
- package/dist/src/components/PlAgDataTable/index.d.ts.map +1 -0
- package/dist/src/components/PlAgDataTable/sources/file-source.d.ts +7 -0
- package/dist/src/components/PlAgDataTable/sources/file-source.d.ts.map +1 -0
- package/dist/src/components/PlAgDataTable/sources/table-source.d.ts +15 -0
- package/dist/src/components/PlAgDataTable/sources/table-source.d.ts.map +1 -0
- package/dist/src/components/PlAgDataTable/types.d.ts +35 -0
- package/dist/src/components/PlAgDataTable/types.d.ts.map +1 -0
- package/dist/src/components/ValueOrErrorsComponent.vue.d.ts +27 -0
- package/dist/src/components/ValueOrErrorsComponent.vue.d.ts.map +1 -0
- package/dist/src/composition/useWatchResult.d.ts +7 -0
- package/dist/src/composition/useWatchResult.d.ts.map +1 -0
- package/dist/src/computedResult.d.ts +35 -0
- package/dist/src/computedResult.d.ts.map +1 -0
- package/dist/src/createApp.d.ts +27 -0
- package/dist/src/createApp.d.ts.map +1 -0
- package/dist/src/createModel.d.ts +3 -0
- package/dist/src/createModel.d.ts.map +1 -0
- package/dist/src/defineApp.d.ts +14 -0
- package/dist/src/defineApp.d.ts.map +1 -0
- package/dist/src/defineStore.d.ts +2 -0
- package/dist/src/defineStore.d.ts.map +1 -0
- package/dist/src/lib.d.ts +16 -0
- package/dist/src/lib.d.ts.map +1 -0
- package/dist/src/types.d.ts +73 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.static-test.d.ts +7 -0
- package/dist/src/types.static-test.d.ts.map +1 -0
- package/dist/src/urls.d.ts +4 -0
- package/dist/src/urls.d.ts.map +1 -0
- package/dist/src/utils.d.ts +20 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/style.css +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/index.html +13 -0
- package/package.json +64 -0
- package/src/assets/base.scss +9 -0
- package/src/assets/block.scss +71 -0
- package/src/assets/file-dialog.scss +181 -0
- package/src/assets/icons/empty-cat.svg +21 -0
- package/src/assets/icons/no-data-cat.svg +40 -0
- package/src/assets/ui.scss +9 -0
- package/src/components/BlockLayout.vue +40 -0
- package/src/components/LoaderPage.vue +7 -0
- package/src/components/NotFound.vue +21 -0
- package/src/components/PlAgDataTable/OverlayLoading.vue +47 -0
- package/src/components/PlAgDataTable/OverlayNoRows.vue +29 -0
- package/src/components/PlAgDataTable/PlAgDataTable.vue +293 -0
- package/src/components/PlAgDataTable/ag-theme.css +403 -0
- package/src/components/PlAgDataTable/assets/cat-in-bag.png +0 -0
- package/src/components/PlAgDataTable/assets/sad-cat.png +0 -0
- package/src/components/PlAgDataTable/index.ts +3 -0
- package/src/components/PlAgDataTable/sources/file-source.ts +25 -0
- package/src/components/PlAgDataTable/sources/table-source.ts +235 -0
- package/src/components/PlAgDataTable/types.ts +38 -0
- package/src/components/ValueOrErrorsComponent.vue +28 -0
- package/src/composition/useWatchResult.ts +42 -0
- package/src/computedResult.ts +47 -0
- package/src/createApp.ts +213 -0
- package/src/createModel.ts +108 -0
- package/src/defineApp.ts +78 -0
- package/src/defineStore.ts +32 -0
- package/src/lib.ts +26 -0
- package/src/types.static-test.ts +80 -0
- package/src/types.ts +106 -0
- package/src/urls.ts +14 -0
- package/src/utils.ts +75 -0
- package/src/vite-env.d.ts +8 -0
- package/tsconfig.json +11 -0
- package/tsconfig.lib.json +27 -0
- package/tsconfig.node.json +13 -0
- package/vite.config.ts +26 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ColDef, GridApi, IDatasource } from '@ag-grid-community/core';
|
|
2
|
+
import type { BlobDriver, LocalBlobHandleAndSize, RemoteBlobHandleAndSize } from '@platforma-sdk/model';
|
|
3
|
+
|
|
4
|
+
export async function updateXsvGridOptions(
|
|
5
|
+
gridApi: GridApi,
|
|
6
|
+
blobDriver: BlobDriver,
|
|
7
|
+
file: LocalBlobHandleAndSize | RemoteBlobHandleAndSize,
|
|
8
|
+
): Promise<{
|
|
9
|
+
columnDefs: ColDef[];
|
|
10
|
+
datasource: IDatasource;
|
|
11
|
+
}> {
|
|
12
|
+
gridApi;
|
|
13
|
+
blobDriver;
|
|
14
|
+
file;
|
|
15
|
+
//blobDriver.getContent(file.handle!)
|
|
16
|
+
// return {
|
|
17
|
+
// datasource,
|
|
18
|
+
// columnDefs,
|
|
19
|
+
// maxBlocksInCache: 10000,
|
|
20
|
+
// cacheBlockSize: 100,
|
|
21
|
+
// rowModelType: "infinite",
|
|
22
|
+
// };
|
|
23
|
+
|
|
24
|
+
throw Error('not implemented');
|
|
25
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { ColDef, GridApi, IDatasource, IGetRowsParams } from '@ag-grid-community/core';
|
|
2
|
+
import {
|
|
3
|
+
type ValueType,
|
|
4
|
+
type PValueInt,
|
|
5
|
+
PValueIntNA,
|
|
6
|
+
PValueLongNA,
|
|
7
|
+
type PValueFloat,
|
|
8
|
+
PValueFloatNA,
|
|
9
|
+
type PValueDouble,
|
|
10
|
+
PValueDoubleNA,
|
|
11
|
+
type PValueString,
|
|
12
|
+
PValueStringNA,
|
|
13
|
+
type PValueBytes,
|
|
14
|
+
PValueBytesNA,
|
|
15
|
+
type PFrameDriver,
|
|
16
|
+
type PTableColumnId,
|
|
17
|
+
type PTableColumnSpec,
|
|
18
|
+
type PTableHandle,
|
|
19
|
+
type PTableVector,
|
|
20
|
+
} from '@platforma-sdk/model';
|
|
21
|
+
import * as lodash from 'lodash';
|
|
22
|
+
import canonicalize from 'canonicalize';
|
|
23
|
+
import { type PlDataTableSheet } from '../types';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate unique colId based on the column spec.
|
|
27
|
+
*/
|
|
28
|
+
function makeColId(spec: PTableColumnSpec) {
|
|
29
|
+
return canonicalize({
|
|
30
|
+
type: spec.type,
|
|
31
|
+
id: spec.id,
|
|
32
|
+
})!;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract `PTableColumnId` from colId string
|
|
37
|
+
*/
|
|
38
|
+
export function parseColId(str: string) {
|
|
39
|
+
return JSON.parse(str) as PTableColumnId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Calculates column definition for a given p-table column
|
|
44
|
+
*/
|
|
45
|
+
function getColDef(iCol: number, spec: PTableColumnSpec): ColDef {
|
|
46
|
+
return {
|
|
47
|
+
colId: makeColId(spec),
|
|
48
|
+
field: iCol.toString(),
|
|
49
|
+
headerName: spec.spec.annotations?.['pl7.app/label']?.trim() ?? 'Unlabeled ' + spec.type + ' ' + iCol.toString(),
|
|
50
|
+
lockPosition: spec.type === 'axis',
|
|
51
|
+
valueFormatter: (value) => {
|
|
52
|
+
if (!value) {
|
|
53
|
+
return 'ERROR';
|
|
54
|
+
} else if (value.value === undefined) {
|
|
55
|
+
return 'NULL';
|
|
56
|
+
} else if (value.value === null) {
|
|
57
|
+
return 'NA';
|
|
58
|
+
} else {
|
|
59
|
+
return value.value.toString();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
cellDataType: ((valueType: ValueType) => {
|
|
63
|
+
switch (valueType) {
|
|
64
|
+
case 'Int':
|
|
65
|
+
case 'Long':
|
|
66
|
+
case 'Float':
|
|
67
|
+
case 'Double':
|
|
68
|
+
return 'number';
|
|
69
|
+
case 'String':
|
|
70
|
+
case 'Bytes':
|
|
71
|
+
return 'text';
|
|
72
|
+
default:
|
|
73
|
+
throw Error(`unsupported data type: ${valueType satisfies never}`);
|
|
74
|
+
}
|
|
75
|
+
})(spec.type === 'axis' ? spec.spec.type : spec.spec.valueType),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if value is NA
|
|
81
|
+
*/
|
|
82
|
+
function isValueNA(value: unknown, valueType: ValueType): boolean {
|
|
83
|
+
switch (valueType) {
|
|
84
|
+
case 'Int':
|
|
85
|
+
return (value as PValueInt) === PValueIntNA;
|
|
86
|
+
case 'Long':
|
|
87
|
+
return (value as bigint) === PValueLongNA;
|
|
88
|
+
case 'Float':
|
|
89
|
+
return (value as PValueFloat) === PValueFloatNA;
|
|
90
|
+
case 'Double':
|
|
91
|
+
return (value as PValueDouble) === PValueDoubleNA;
|
|
92
|
+
case 'String':
|
|
93
|
+
return (value as PValueString) === PValueStringNA;
|
|
94
|
+
case 'Bytes':
|
|
95
|
+
return (value as PValueBytes) === PValueBytesNA;
|
|
96
|
+
default:
|
|
97
|
+
throw Error(`unsupported data type: ${valueType satisfies never}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if value is absent
|
|
103
|
+
*/
|
|
104
|
+
function isValueAbsent(array: Uint8Array, index: number): boolean {
|
|
105
|
+
const chunkIndex = Math.floor(index / 8);
|
|
106
|
+
const mask = 1 << (7 - (index % 8));
|
|
107
|
+
return (array[chunkIndex] & mask) > 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convert value to displayable form
|
|
112
|
+
*/
|
|
113
|
+
function toDisplayValue(value: string | number | bigint | Uint8Array, valueType: ValueType): string | number {
|
|
114
|
+
switch (valueType) {
|
|
115
|
+
case 'Int':
|
|
116
|
+
return value as number;
|
|
117
|
+
case 'Long':
|
|
118
|
+
return Number(value as bigint);
|
|
119
|
+
case 'Float':
|
|
120
|
+
return value as number;
|
|
121
|
+
case 'Double':
|
|
122
|
+
return value as number;
|
|
123
|
+
case 'String':
|
|
124
|
+
return value as string;
|
|
125
|
+
case 'Bytes':
|
|
126
|
+
return Buffer.from(value as Uint8Array).toString('hex');
|
|
127
|
+
default:
|
|
128
|
+
throw Error(`unsupported data type: ${valueType satisfies never}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Convert columnar data from the driver to rows, used by ag-grid
|
|
134
|
+
* @param specs column specs
|
|
135
|
+
* @param columns columnar data
|
|
136
|
+
* @param nRows number of rows
|
|
137
|
+
* @returns
|
|
138
|
+
*/
|
|
139
|
+
function columns2rows(indices: number[], columns: PTableVector[]): unknown[] {
|
|
140
|
+
const nCols = columns.length;
|
|
141
|
+
const rowData = [];
|
|
142
|
+
for (let iRow = 0; iRow < columns[0].data.length; ++iRow) {
|
|
143
|
+
const row: Record<string, unknown> = {};
|
|
144
|
+
|
|
145
|
+
const index = [];
|
|
146
|
+
for (let iCol = 0; iCol < nCols; ++iCol) {
|
|
147
|
+
const field = indices[iCol].toString();
|
|
148
|
+
const value = columns[iCol].data[iRow];
|
|
149
|
+
const valueType = columns[iCol].type;
|
|
150
|
+
if (isValueAbsent(columns[iCol].absent, iRow)) {
|
|
151
|
+
row[field] = undefined; // NULL
|
|
152
|
+
} else if (isValueNA(value, valueType) || value === null) {
|
|
153
|
+
row[field] = null; // NA
|
|
154
|
+
} else {
|
|
155
|
+
row[field] = toDisplayValue(value, valueType);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
index.push(valueType === 'Long' ? Number(value) : value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// generate ID based on the axes information
|
|
162
|
+
row['id'] = JSON.stringify(index);
|
|
163
|
+
|
|
164
|
+
rowData.push(row);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return rowData;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Calculate GridOptions for p-table data source type
|
|
172
|
+
*/
|
|
173
|
+
export async function updatePFrameGridOptions(
|
|
174
|
+
gridApi: GridApi,
|
|
175
|
+
pfDriver: PFrameDriver,
|
|
176
|
+
pt: PTableHandle,
|
|
177
|
+
sheets: PlDataTableSheet[],
|
|
178
|
+
): Promise<{
|
|
179
|
+
columnDefs: ColDef[];
|
|
180
|
+
datasource: IDatasource;
|
|
181
|
+
}> {
|
|
182
|
+
const specs = await pfDriver.getSpec(pt);
|
|
183
|
+
const indices = specs
|
|
184
|
+
.map((spec, i) => (!lodash.find(sheets, (sheet) => lodash.isEqual(sheet.axis, spec.id)) ? i : null))
|
|
185
|
+
.filter((entry) => entry !== null);
|
|
186
|
+
const columnDefs = indices.map((i) => getColDef(i, specs[i]));
|
|
187
|
+
|
|
188
|
+
const ptShape = await pfDriver.getShape(pt);
|
|
189
|
+
const rowCount = ptShape.rows;
|
|
190
|
+
|
|
191
|
+
let lastParams: IGetRowsParams | undefined = undefined;
|
|
192
|
+
const datasource = {
|
|
193
|
+
rowCount,
|
|
194
|
+
getRows: async (params) => {
|
|
195
|
+
gridApi.setGridOption('loading', true);
|
|
196
|
+
try {
|
|
197
|
+
if (rowCount == 0) {
|
|
198
|
+
params.successCallback([], rowCount);
|
|
199
|
+
gridApi.setGridOption('loading', false);
|
|
200
|
+
gridApi.showNoRowsOverlay();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// this is to avoid double flickering when underlying table is changed
|
|
205
|
+
if (lastParams && !lodash.isEqual(lastParams.sortModel, params.sortModel)) {
|
|
206
|
+
lastParams = undefined;
|
|
207
|
+
params.failCallback();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
lastParams = params;
|
|
211
|
+
|
|
212
|
+
const length = params.endRow - params.startRow;
|
|
213
|
+
let rowData: unknown[] = [];
|
|
214
|
+
if (length > 0) {
|
|
215
|
+
const data = await pfDriver.getData(pt, indices, {
|
|
216
|
+
offset: params.startRow,
|
|
217
|
+
length,
|
|
218
|
+
});
|
|
219
|
+
rowData = columns2rows(indices, data);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
params.successCallback(rowData, ptShape.rows);
|
|
223
|
+
gridApi.setGridOption('loading', false);
|
|
224
|
+
gridApi.autoSizeAllColumns();
|
|
225
|
+
} catch (error: unknown) {
|
|
226
|
+
params.failCallback();
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
} satisfies IDatasource;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
columnDefs,
|
|
233
|
+
datasource,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AxisId, LocalBlobHandleAndSize, PTableHandle, RemoteBlobHandleAndSize, ValueOrErrors } from '@platforma-sdk/model';
|
|
2
|
+
|
|
3
|
+
export type PlDataTableSheet = {
|
|
4
|
+
/** id of the axis to use */
|
|
5
|
+
axis: AxisId;
|
|
6
|
+
/** options to show in the filter tan */
|
|
7
|
+
options: {
|
|
8
|
+
/** value of the option (should be one of the values in the axis) */
|
|
9
|
+
value: string | number;
|
|
10
|
+
/** corresponding label */
|
|
11
|
+
text: string;
|
|
12
|
+
}[];
|
|
13
|
+
/** default (selected) value */
|
|
14
|
+
defaultValue?: string | number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Data table settings
|
|
19
|
+
*/
|
|
20
|
+
export type PlDataTableSettings =
|
|
21
|
+
| {
|
|
22
|
+
/**
|
|
23
|
+
* The type of the source to feed the data into the table.
|
|
24
|
+
*/
|
|
25
|
+
sourceType: 'ptable';
|
|
26
|
+
/**
|
|
27
|
+
* PTable handle output
|
|
28
|
+
*/
|
|
29
|
+
pTable?: ValueOrErrors<PTableHandle | undefined> | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Sheets that we want to show in our table
|
|
32
|
+
*/
|
|
33
|
+
sheets?: PlDataTableSheet[];
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
sourceType: 'xsv';
|
|
37
|
+
xsvFile?: ValueOrErrors<RemoteBlobHandleAndSize | undefined> | ValueOrErrors<LocalBlobHandleAndSize | undefined> | undefined;
|
|
38
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import type { ValueOrErrors } from '@platforma-sdk/model';
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
valueOrError: ValueOrErrors<T> | undefined;
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
const value = computed(() => (props.valueOrError && props.valueOrError.ok ? props.valueOrError.value : undefined));
|
|
10
|
+
|
|
11
|
+
const error = computed(() => (props.valueOrError && !props.valueOrError.ok ? props.valueOrError.errors : undefined));
|
|
12
|
+
|
|
13
|
+
const isUnresolved = computed(() => value.value === undefined && error.value === undefined);
|
|
14
|
+
|
|
15
|
+
defineSlots<{
|
|
16
|
+
default(props: { value: T }): void;
|
|
17
|
+
}>();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div>
|
|
22
|
+
<slot v-if="value !== undefined" name="default" v-bind="{ value }" />
|
|
23
|
+
<div v-if="error" class="alert-error">
|
|
24
|
+
{{ error }}
|
|
25
|
+
</div>
|
|
26
|
+
<div v-if="isUnresolved">Unresolved</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { WatchSource } from 'vue';
|
|
2
|
+
import { reactive, watch } from 'vue';
|
|
3
|
+
import type { OptionalResult } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Use for synchronous/asynchronous converters (wip)
|
|
7
|
+
*/
|
|
8
|
+
export function useWatchResult<S, V>(watchSource: WatchSource<S>, load: (s: S) => Promise<V> | V): OptionalResult<V> {
|
|
9
|
+
const data = reactive({
|
|
10
|
+
value: undefined as unknown,
|
|
11
|
+
errors: undefined as unknown,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const state = {
|
|
15
|
+
version: 0,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const resolve = async (s: S, version: number) => {
|
|
19
|
+
const value = await load(s);
|
|
20
|
+
return { value, version };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
watch(
|
|
24
|
+
watchSource,
|
|
25
|
+
async (s) => {
|
|
26
|
+
data.errors = undefined;
|
|
27
|
+
data.value = undefined;
|
|
28
|
+
try {
|
|
29
|
+
const { value, version } = await resolve(s, ++state.version);
|
|
30
|
+
|
|
31
|
+
if (version === state.version) {
|
|
32
|
+
data.value = value;
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
data.errors = [String(error)];
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{ immediate: true, deep: true },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return data as OptionalResult<V>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue';
|
|
2
|
+
import { computed, type ComputedGetter } from 'vue';
|
|
3
|
+
import type { OptionalResult } from './types';
|
|
4
|
+
import { wrapOptionalResult } from './utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a computed reference that wraps the result of a getter function in an `OptionalResult` object.
|
|
8
|
+
* This wrapper provides error handling, ensuring that if an error occurs during the computation of the result,
|
|
9
|
+
* the error is captured and included in the `OptionalResult` instead of throwing the error.
|
|
10
|
+
*
|
|
11
|
+
* @template V - The type of the value returned by the getter function.
|
|
12
|
+
*
|
|
13
|
+
* @param {ComputedGetter<V>} getter - A function that returns a value of type `V`. This function is executed inside
|
|
14
|
+
* the computed reference and its result is wrapped in an `OptionalResult`.
|
|
15
|
+
*
|
|
16
|
+
* @returns {ComputedRef<OptionalResult<V>>} - A computed reference containing the result of the getter function
|
|
17
|
+
* wrapped in an `OptionalResult`. If the getter function executes successfully, the `value` property of the `OptionalResult`
|
|
18
|
+
* will contain the result. If an error occurs, the `errors` property will contain the error message, and `value` will be `undefined`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const myGetter = () => {
|
|
22
|
+
* if (someCondition) {
|
|
23
|
+
* throw new Error('An error occurred');
|
|
24
|
+
* }
|
|
25
|
+
* return 'Success';
|
|
26
|
+
* };
|
|
27
|
+
*
|
|
28
|
+
* const myComputedResult = computedResult(myGetter);
|
|
29
|
+
*
|
|
30
|
+
* // If someCondition is true:
|
|
31
|
+
* // myComputedResult.value will be { errors: ['An error occurred'], value: undefined }
|
|
32
|
+
*
|
|
33
|
+
* // If someCondition is false:
|
|
34
|
+
* // myComputedResult.value will be { errors: undefined, value: 'Success' }
|
|
35
|
+
*/
|
|
36
|
+
export function computedResult<V>(getter: ComputedGetter<V>): ComputedRef<OptionalResult<V>> {
|
|
37
|
+
return computed(() => {
|
|
38
|
+
try {
|
|
39
|
+
return wrapOptionalResult(getter());
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return {
|
|
42
|
+
errors: [String(err)],
|
|
43
|
+
value: undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
package/src/createApp.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { deepClone } from '@milaboratories/helpers';
|
|
2
|
+
import type { Mutable } from '@milaboratories/helpers';
|
|
3
|
+
import type { NavigationState, BlockOutputsBase, BlockState, Platforma } from '@platforma-sdk/model';
|
|
4
|
+
import { reactive, nextTick, computed, watch } from 'vue';
|
|
5
|
+
import type { UnwrapValueOrErrors, StateModelOptions, UnwrapOutputs, OptionalResult, OutputValues, OutputErrors } from './types';
|
|
6
|
+
import { createModel } from './createModel';
|
|
7
|
+
import { parseQuery } from './urls';
|
|
8
|
+
import { MultiError, unwrapValueOrErrors } from './utils';
|
|
9
|
+
|
|
10
|
+
export function createApp<
|
|
11
|
+
Args = unknown,
|
|
12
|
+
Outputs extends BlockOutputsBase = BlockOutputsBase,
|
|
13
|
+
UiState = unknown,
|
|
14
|
+
Href extends `/${string}` = `/${string}`,
|
|
15
|
+
>(state: BlockState<Args, Outputs, UiState, Href>, platforma: Platforma<Args, Outputs, UiState, Href>) {
|
|
16
|
+
const innerState = reactive({
|
|
17
|
+
args: Object.freeze(state.args),
|
|
18
|
+
outputs: Object.freeze(state.outputs),
|
|
19
|
+
ui: Object.freeze(state.ui),
|
|
20
|
+
navigationState: Object.freeze(state.navigationState) as NavigationState<Href>,
|
|
21
|
+
}) as {
|
|
22
|
+
args: Readonly<Args>;
|
|
23
|
+
outputs: Partial<Readonly<Outputs>>;
|
|
24
|
+
ui: Readonly<UiState>;
|
|
25
|
+
navigationState: Readonly<NavigationState<Href>>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
platforma.onStateUpdates(async (updates) => {
|
|
29
|
+
updates.forEach((patch) => {
|
|
30
|
+
if (patch.key === 'args') {
|
|
31
|
+
innerState.args = Object.freeze(patch.value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (patch.key === 'ui') {
|
|
35
|
+
innerState.ui = Object.freeze(patch.value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (patch.key === 'outputs') {
|
|
39
|
+
innerState.outputs = Object.freeze(patch.value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (patch.key === 'navigationState') {
|
|
43
|
+
innerState.navigationState = Object.freeze(patch.value);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await nextTick(); // @todo remove
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const cloneArgs = () => deepClone(innerState.args) as Args;
|
|
51
|
+
const cloneUiState = () => deepClone(innerState.ui) as UiState;
|
|
52
|
+
const cloneNavigationState = () => deepClone(innerState.navigationState) as Mutable<NavigationState<Href>>;
|
|
53
|
+
|
|
54
|
+
const methods = {
|
|
55
|
+
createArgsModel<T = Args>(options: StateModelOptions<Args, T> = {}) {
|
|
56
|
+
return createModel<T, Args>({
|
|
57
|
+
get() {
|
|
58
|
+
if (options.transform) {
|
|
59
|
+
return options.transform(innerState.args);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return innerState.args as T;
|
|
63
|
+
},
|
|
64
|
+
validate: options.validate,
|
|
65
|
+
autoSave: true,
|
|
66
|
+
onSave(newArgs) {
|
|
67
|
+
platforma.setBlockArgs(newArgs);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* defaultUiState is temporarily here, remove it after implementing initialUiState
|
|
73
|
+
*/
|
|
74
|
+
createUiModel<T = UiState>(options: StateModelOptions<UiState, T> = {}, defaultUiState: () => UiState) {
|
|
75
|
+
return createModel<T, UiState>({
|
|
76
|
+
get() {
|
|
77
|
+
if (options.transform) {
|
|
78
|
+
return options.transform(innerState.ui);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (innerState.ui ?? defaultUiState()) as T;
|
|
82
|
+
},
|
|
83
|
+
validate: options.validate,
|
|
84
|
+
autoSave: true,
|
|
85
|
+
onSave(newData) {
|
|
86
|
+
platforma.setBlockUiState(newData);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
/**
|
|
91
|
+
* Note: Don't forget to list the output names, like: useOutputs('output1', 'output2', ...etc)
|
|
92
|
+
* @param keys - List of output names
|
|
93
|
+
* @returns {OptionalResult<UnwrapOutputs<Outputs, K>>}
|
|
94
|
+
*/
|
|
95
|
+
useOutputs<K extends keyof Outputs>(...keys: K[]): OptionalResult<UnwrapOutputs<Outputs, K>> {
|
|
96
|
+
const data = reactive({
|
|
97
|
+
errors: undefined,
|
|
98
|
+
value: undefined,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
watch(
|
|
102
|
+
() => innerState.outputs,
|
|
103
|
+
() => {
|
|
104
|
+
try {
|
|
105
|
+
Object.assign(data, {
|
|
106
|
+
value: this.unwrapOutputs<K>(...keys),
|
|
107
|
+
errors: undefined,
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
Object.assign(data, {
|
|
111
|
+
value: undefined,
|
|
112
|
+
errors: [String(error)],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{ immediate: true, deep: true },
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return data as OptionalResult<UnwrapOutputs<Outputs, K>>;
|
|
120
|
+
},
|
|
121
|
+
/**
|
|
122
|
+
* @throws Error
|
|
123
|
+
* @param keys
|
|
124
|
+
* @returns
|
|
125
|
+
*/
|
|
126
|
+
unwrapOutputs<K extends keyof Outputs>(...keys: K[]): UnwrapOutputs<Outputs, K> {
|
|
127
|
+
const outputs = innerState.outputs;
|
|
128
|
+
const entries = keys.map((key) => [key, unwrapValueOrErrors(outputs[key])]);
|
|
129
|
+
return Object.fromEntries(entries);
|
|
130
|
+
},
|
|
131
|
+
/**
|
|
132
|
+
* @deprecated use app.outputs.[fieldName] instead
|
|
133
|
+
* @see outputs
|
|
134
|
+
*/
|
|
135
|
+
getOutputField(key: keyof Outputs) {
|
|
136
|
+
return innerState.outputs[key];
|
|
137
|
+
},
|
|
138
|
+
/**
|
|
139
|
+
* @deprecated use outputValues instead
|
|
140
|
+
* @see outputValues
|
|
141
|
+
*/
|
|
142
|
+
getOutputFieldOkOptional<K extends keyof Outputs>(key: K): UnwrapValueOrErrors<Outputs[K]> | undefined {
|
|
143
|
+
const result = this.getOutputField(key);
|
|
144
|
+
|
|
145
|
+
if (result && result.ok) {
|
|
146
|
+
return result.value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return undefined;
|
|
150
|
+
},
|
|
151
|
+
getOutputFieldErrorsOptional<K extends keyof Outputs>(key: K): string[] | undefined {
|
|
152
|
+
const result = this.getOutputField(key);
|
|
153
|
+
|
|
154
|
+
if (result && !result.ok) {
|
|
155
|
+
return result.errors;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return undefined;
|
|
159
|
+
},
|
|
160
|
+
updateArgs(cb: (args: Args) => void) {
|
|
161
|
+
const newArgs = cloneArgs();
|
|
162
|
+
cb(newArgs);
|
|
163
|
+
return platforma.setBlockArgs(newArgs);
|
|
164
|
+
},
|
|
165
|
+
updateUiState(cb: (args: UiState) => UiState) {
|
|
166
|
+
const newUiState = cloneUiState();
|
|
167
|
+
return platforma.setBlockUiState(cb(newUiState));
|
|
168
|
+
},
|
|
169
|
+
updateNavigationState(cb: (args: Mutable<NavigationState<Href>>) => void) {
|
|
170
|
+
const newState = cloneNavigationState();
|
|
171
|
+
cb(newState);
|
|
172
|
+
return platforma.setNavigationState(newState);
|
|
173
|
+
},
|
|
174
|
+
navigateTo(href: Href) {
|
|
175
|
+
const newState = cloneNavigationState();
|
|
176
|
+
newState.href = href;
|
|
177
|
+
return platforma.setNavigationState(newState);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const getters = {
|
|
182
|
+
args: computed(() => innerState.args),
|
|
183
|
+
outputs: computed(() => innerState.outputs),
|
|
184
|
+
ui: computed(() => innerState.ui),
|
|
185
|
+
navigationState: computed(() => innerState.navigationState),
|
|
186
|
+
href: computed(() => innerState.navigationState.href),
|
|
187
|
+
|
|
188
|
+
outputValues: computed<OutputValues<Outputs>>(() => {
|
|
189
|
+
const entries = Object.entries(innerState.outputs).map(([k, vOrErr]) => [
|
|
190
|
+
k,
|
|
191
|
+
vOrErr.ok && vOrErr.value !== undefined ? vOrErr.value : undefined,
|
|
192
|
+
]);
|
|
193
|
+
return Object.fromEntries(entries);
|
|
194
|
+
}),
|
|
195
|
+
|
|
196
|
+
outputErrors: computed<OutputErrors<Outputs>>(() => {
|
|
197
|
+
const entries = Object.entries(innerState.outputs).map(([k, vOrErr]) => [k, vOrErr && !vOrErr.ok ? new MultiError(vOrErr.errors) : undefined]);
|
|
198
|
+
return Object.fromEntries(entries);
|
|
199
|
+
}),
|
|
200
|
+
|
|
201
|
+
queryParams: computed(() => parseQuery<Href>(innerState.navigationState.href)),
|
|
202
|
+
hasErrors: computed(() => Object.values(innerState.outputs).some((v) => !v?.ok)), // @TODO: there is middle-layer error, v sometimes is undefined
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return reactive(Object.assign(methods, getters));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export type BaseApp<
|
|
209
|
+
Args = unknown,
|
|
210
|
+
Outputs extends BlockOutputsBase = BlockOutputsBase,
|
|
211
|
+
UiState = unknown,
|
|
212
|
+
Href extends `/${string}` = `/${string}`,
|
|
213
|
+
> = ReturnType<typeof createApp<Args, Outputs, UiState, Href>>;
|