akanjs 3.0.0-alpha.110 → 3.0.0-alpha.112
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/dictionary/base.dictionary.ts +1 -0
- package/document/documentSchema.ts +20 -0
- package/document/filterMeta.ts +21 -0
- package/document/into.ts +0 -8
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/resolver/database.resolver.ts +70 -72
- package/server/resolver/service.resolver.ts +4 -14
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/documentSchema.d.ts +2 -0
- package/types/document/filterMeta.d.ts +8 -0
- package/types/document/into.d.ts +0 -8
- package/types/ui/Select.d.ts +1 -1
- package/ui/Field/Relation.tsx +4 -12
- package/ui/Select.tsx +204 -121
|
@@ -146,6 +146,7 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
|
|
|
146
146
|
password: ["Password", "비밀번호"],
|
|
147
147
|
passwordConfirm: ["Password Confirm", "비밀번호 확인"],
|
|
148
148
|
noOptions: ["Can't find any datas", "데이터를 찾을 수 없습니다."],
|
|
149
|
+
noSelection: ["No selection", "선택 안 함"],
|
|
149
150
|
addModel: ["Add {model}", "{model} 추가"],
|
|
150
151
|
createModel: ["Create {model}", "{model} 생성하기"],
|
|
151
152
|
createSuccess: ["{model} create success", "{model} 생성 완료"],
|
|
@@ -53,6 +53,26 @@ export class DocumentSchema<Doc = unknown> {
|
|
|
53
53
|
return this;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
removePre<HookDoc = Doc>(type: SaveEventType, hook: DocumentSaveHook<HookDoc>) {
|
|
57
|
+
const hooks = this.preHooks.get(type);
|
|
58
|
+
if (hooks)
|
|
59
|
+
this.preHooks.set(
|
|
60
|
+
type,
|
|
61
|
+
hooks.filter((registered) => registered !== (hook as unknown)),
|
|
62
|
+
);
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
removePost<HookDoc = Doc>(type: SaveEventType, hook: DocumentSaveHook<HookDoc>) {
|
|
67
|
+
const hooks = this.postHooks.get(type);
|
|
68
|
+
if (hooks)
|
|
69
|
+
this.postHooks.set(
|
|
70
|
+
type,
|
|
71
|
+
hooks.filter((registered) => registered !== (hook as unknown)),
|
|
72
|
+
);
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
hook(type: DocumentHookName, hook: DocumentSaveHook<Doc>) {
|
|
57
77
|
const [, phase, event] = /^(before|after)(.+)$/.exec(type) ?? [];
|
|
58
78
|
if (!phase || !event) throw new Error(`Invalid document hook: ${type}`);
|
package/document/filterMeta.ts
CHANGED
|
@@ -100,6 +100,27 @@ export const fillMissingFilterArgs = (filterInfo: FilterInfo, args: unknown[]) =
|
|
|
100
100
|
return [...args, ...Array(filterInfo.args.length - args.length).fill(undefined)];
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
+
const queryOptionKeys = new Set(["select", "skip", "limit", "sort", "sample"]);
|
|
104
|
+
|
|
105
|
+
const isQueryOptionArg = (value: unknown) => {
|
|
106
|
+
if (!value || typeof value !== "object") return false;
|
|
107
|
+
const proto = Object.getPrototypeOf(value);
|
|
108
|
+
if (proto !== Object.prototype && proto !== null) return false;
|
|
109
|
+
return Object.keys(value).every((key) => queryOptionKeys.has(key));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Splits a generated filter method's arguments into the filter's own args and the trailing query option.
|
|
114
|
+
* Shared by the database and service resolvers so the two cannot drift into disagreeing about what an option is.
|
|
115
|
+
*/
|
|
116
|
+
export const splitFilterArgs = (filterInfo: FilterInfo, args: unknown[]) => {
|
|
117
|
+
const hasQueryOption = args.length > filterInfo.args.length || isQueryOptionArg(args.at(-1));
|
|
118
|
+
return {
|
|
119
|
+
queryArgs: fillMissingFilterArgs(filterInfo, hasQueryOption ? args.slice(0, -1) : args),
|
|
120
|
+
queryOption: (hasQueryOption ? args.at(-1) : {}) as Record<string, unknown>,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
103
124
|
export interface FilterArgInfo {
|
|
104
125
|
name: string;
|
|
105
126
|
argRef: ConstantFieldType;
|
package/document/into.ts
CHANGED
|
@@ -24,14 +24,6 @@ interface DefaultMdlStats<
|
|
|
24
24
|
pickById: (docId: string | undefined, projection?: _Projection) => Promise<TDocument>;
|
|
25
25
|
sample: (query: _FilterQuery, size?: number) => Promise<TDocument[]>;
|
|
26
26
|
sampleOne: (query: _FilterQuery) => Promise<TDocument | null>;
|
|
27
|
-
preSaveListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
28
|
-
postSaveListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
29
|
-
preCreateListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
30
|
-
postCreateListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
31
|
-
preUpdateListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
32
|
-
postUpdateListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
33
|
-
preRemoveListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
34
|
-
postRemoveListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
35
27
|
listenPre: (
|
|
36
28
|
eventType: SaveEventType,
|
|
37
29
|
listener: (doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>,
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
type DocumentUpdateInput,
|
|
14
14
|
documentQueryHelper,
|
|
15
15
|
type FindQueryOption,
|
|
16
|
-
fillMissingFilterArgs,
|
|
17
16
|
getFilterInfoByKey,
|
|
18
17
|
getFilterMeta,
|
|
19
18
|
getFilterSortByKey,
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
type Mdl,
|
|
23
22
|
NoDocumentError,
|
|
24
23
|
type SaveEventType,
|
|
24
|
+
splitFilterArgs,
|
|
25
25
|
type UpdateChain,
|
|
26
26
|
} from "akanjs/document";
|
|
27
27
|
import {
|
|
@@ -107,6 +107,25 @@ export class DatabaseResolver {
|
|
|
107
107
|
schema.index(fields as { [key: string]: 1 | -1 });
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
const listen = (phase: "pre" | "post") => {
|
|
111
|
+
return (
|
|
112
|
+
type: SaveEventType,
|
|
113
|
+
listener: (doc: any, type: CRUDEventType, previous?: any) => PromiseOrObject<void>,
|
|
114
|
+
) => {
|
|
115
|
+
const hook = function (this: any, _next?: () => void, crudType?: CRUDEventType, previous?: any) {
|
|
116
|
+
return listener(this, crudType ?? "update", previous);
|
|
117
|
+
};
|
|
118
|
+
if (phase === "pre") schema.pre(type, hook);
|
|
119
|
+
else schema.post(type, hook);
|
|
120
|
+
return () => {
|
|
121
|
+
if (phase === "pre") schema.removePre(type, hook);
|
|
122
|
+
else schema.removePost(type, hook);
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const listenPreHook = listen("pre");
|
|
127
|
+
const listenPostHook = listen("post");
|
|
128
|
+
|
|
110
129
|
class DatabaseModelInstance extends adapt(`${modelName}Model`, ({ plug }) => ({
|
|
111
130
|
__database: plug(DatabaseAdaptorRole, (database) => database),
|
|
112
131
|
__cache: plug(CacheAdaptorRole, (cache) => new CacheDatabase(modelName, cache)),
|
|
@@ -224,50 +243,45 @@ export class DatabaseResolver {
|
|
|
224
243
|
};
|
|
225
244
|
return chain;
|
|
226
245
|
};
|
|
246
|
+
const pickById = async (id: string | undefined, projection?: any) => {
|
|
247
|
+
if (!id) throw new NoDocumentError("No Document ID");
|
|
248
|
+
const doc = await timedQuery(() => store.findOne({ id }, { select: projection }));
|
|
249
|
+
if (!doc) throw new NoDocumentError(`No Document (${modelName}): ${id}`);
|
|
250
|
+
return doc;
|
|
251
|
+
};
|
|
227
252
|
return Object.assign(Model, {
|
|
228
253
|
refName: modelName,
|
|
229
|
-
pickOne: (query: QueryOf<any>, projection?: any) =>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
count: (query: QueryOf<any>) => store.count(query),
|
|
254
|
+
pickOne: (query: QueryOf<any>, projection?: any) =>
|
|
255
|
+
timedQuery(() => store.pickOne(query, { select: projection })),
|
|
256
|
+
pickById,
|
|
257
|
+
|
|
258
|
+
pickAndWrite: async (id: string, rawData: any) => await (await pickById(id)).set(rawData).save(),
|
|
259
|
+
pickOneAndWrite: async (query: QueryOf<any>, rawData: any) =>
|
|
260
|
+
await (await timedQuery(() => store.pickOne(query))).set(rawData).save(),
|
|
261
|
+
exists: async (query: QueryOf<any>) => await timedQuery(() => store.exists(query)),
|
|
262
|
+
sample: (query: QueryOf<any>, size = 1) => timedQuery(() => store.find(query, { sample: size, limit: size })),
|
|
263
|
+
sampleOne: (query: QueryOf<any>) => timedQuery(() => store.findOne(query, { sample: true })),
|
|
264
|
+
find: (query: QueryOf<any>, projection?: any) => createFindManyChain(query, { select: projection }),
|
|
265
|
+
findOne: (query: QueryOf<any>, projection?: any) => createFindOneChain(query, { select: projection }),
|
|
266
|
+
findById: (id: string | undefined, projection?: any) =>
|
|
267
|
+
id ? timedQuery(() => store.findOne({ id }, { select: projection })) : Promise.resolve(null),
|
|
268
|
+
count: (query: QueryOf<any>) => timedQuery(() => store.count(query)),
|
|
244
269
|
updateOne: (query: QueryOf<any>, update: DocumentUpdateInput, options?: { upsert?: boolean }) =>
|
|
245
|
-
store.updateOneByQuery(query, update, options),
|
|
246
|
-
updateMany: (query: QueryOf<any>, update: DocumentUpdateInput) =>
|
|
247
|
-
|
|
248
|
-
|
|
270
|
+
timedQuery(() => store.updateOneByQuery(query, update, options)),
|
|
271
|
+
updateMany: (query: QueryOf<any>, update: DocumentUpdateInput) =>
|
|
272
|
+
timedQuery(() => store.updateManyByQuery(query, update)),
|
|
273
|
+
removeOne: (query: QueryOf<any>) => timedQuery(() => store.removeOneByQuery(query)),
|
|
274
|
+
removeMany: (query: QueryOf<any>) => timedQuery(() => store.removeManyByQuery(query)),
|
|
249
275
|
updateById: (id: string, update: DocumentUpdateInput, options?: { upsert?: boolean }) =>
|
|
250
|
-
store.updateOneByQuery({ id }, update, options),
|
|
251
|
-
removeById: (id: string) => store.removeOneByQuery({ id }),
|
|
276
|
+
timedQuery(() => store.updateOneByQuery({ id }, update, options)),
|
|
277
|
+
removeById: (id: string) => timedQuery(() => store.removeOneByQuery({ id })),
|
|
252
278
|
|
|
253
|
-
countDocuments: (query: QueryOf<any>) => store.count(query),
|
|
279
|
+
countDocuments: (query: QueryOf<any>) => timedQuery(() => store.count(query)),
|
|
254
280
|
bulkWrite: (
|
|
255
281
|
operations: { updateOne: { filter: QueryOf<any>; update: DocumentUpdateInput; upsert?: boolean } }[],
|
|
256
|
-
) => store.bulkWrite(operations),
|
|
257
|
-
listenPre:
|
|
258
|
-
|
|
259
|
-
listener: (doc: any, type: CRUDEventType, previous?: any) => PromiseOrObject<void>,
|
|
260
|
-
) =>
|
|
261
|
-
schema.pre(type, function (this: any, _next, crudType, previous) {
|
|
262
|
-
return listener(this, crudType ?? "update", previous);
|
|
263
|
-
}),
|
|
264
|
-
listenPost: (
|
|
265
|
-
type: SaveEventType,
|
|
266
|
-
listener: (doc: any, type: CRUDEventType, previous?: any) => PromiseOrObject<void>,
|
|
267
|
-
) =>
|
|
268
|
-
schema.post(type, function (this: any, _next, crudType, previous) {
|
|
269
|
-
return listener(this, crudType ?? "update", previous);
|
|
270
|
-
}),
|
|
282
|
+
) => timedQuery(() => store.bulkWrite(operations)),
|
|
283
|
+
listenPre: listenPreHook,
|
|
284
|
+
listenPost: listenPostHook,
|
|
271
285
|
});
|
|
272
286
|
}
|
|
273
287
|
|
|
@@ -289,40 +303,34 @@ export class DatabaseResolver {
|
|
|
289
303
|
}
|
|
290
304
|
async __pick(query?: QueryOf<any>, queryOption?: FindQueryOption): Promise<any> {
|
|
291
305
|
const { find, sort, skip, sample, select } = getFindQuery(query, queryOption);
|
|
292
|
-
return await this.__store.pickOne(find, { sort, skip, sample, select });
|
|
306
|
+
return await timedQuery(() => this.__store.pickOne(find, { sort, skip, sample, select }));
|
|
293
307
|
}
|
|
294
308
|
async __pickId(query?: QueryOf<any>, queryOption?: FindQueryOption): Promise<string> {
|
|
295
309
|
const { find, sort, skip, sample } = getFindQuery(query, queryOption);
|
|
296
|
-
const id = await this.__store.findId(find, { sort, skip, sample });
|
|
310
|
+
const id = await timedQuery(() => this.__store.findId(find, { sort, skip, sample }));
|
|
297
311
|
if (!id) throw new NoDocumentError(`No Document (${database.refName}): ${JSON.stringify(query)}`);
|
|
298
312
|
return id;
|
|
299
313
|
}
|
|
300
314
|
async __exists(query?: QueryOf<any>): Promise<string | null> {
|
|
301
|
-
return await this.__store.exists(query);
|
|
315
|
+
return await timedQuery(() => this.__store.exists(query));
|
|
302
316
|
}
|
|
303
317
|
async __count(query?: QueryOf<any>): Promise<number> {
|
|
304
318
|
return await timedQuery(() => this.__store.count(query));
|
|
305
319
|
}
|
|
306
320
|
async __insight(query?: QueryOf<any>): Promise<any> {
|
|
307
|
-
return await this.__store.insight(query);
|
|
321
|
+
return await timedQuery(() => this.__store.insight(query));
|
|
308
322
|
}
|
|
309
323
|
listenPre(
|
|
310
324
|
type: SaveEventType,
|
|
311
325
|
listener: (doc: any, type: CRUDEventType, previous?: any) => PromiseOrObject<void>,
|
|
312
326
|
) {
|
|
313
|
-
|
|
314
|
-
return listener(this, crudType ?? "update", previous);
|
|
315
|
-
});
|
|
316
|
-
return () => undefined;
|
|
327
|
+
return listenPreHook(type, listener);
|
|
317
328
|
}
|
|
318
329
|
listenPost(
|
|
319
330
|
type: SaveEventType,
|
|
320
331
|
listener: (doc: any, type: CRUDEventType, previous?: any) => PromiseOrObject<void>,
|
|
321
332
|
) {
|
|
322
|
-
|
|
323
|
-
return listener(this, crudType ?? "update", previous);
|
|
324
|
-
});
|
|
325
|
-
return () => undefined;
|
|
333
|
+
return listenPostHook(type, listener);
|
|
326
334
|
}
|
|
327
335
|
async __get(id: string) {
|
|
328
336
|
const doc = await this.__loader.load(id);
|
|
@@ -345,7 +353,7 @@ export class DatabaseResolver {
|
|
|
345
353
|
return this.__loadMany(ids);
|
|
346
354
|
}
|
|
347
355
|
async clone(data: DataInputOf<any, any> & { id: string }) {
|
|
348
|
-
return await this.__store.clone(data);
|
|
356
|
+
return await timedQuery(() => this.__store.clone(data));
|
|
349
357
|
}
|
|
350
358
|
async __create(data: DataInputOf<any, any>) {
|
|
351
359
|
return await timedQuery(() => this.__store.create(data));
|
|
@@ -360,7 +368,7 @@ export class DatabaseResolver {
|
|
|
360
368
|
return this.__update(id, data);
|
|
361
369
|
}
|
|
362
370
|
async __remove(id: string) {
|
|
363
|
-
return await this.__store.remove(id);
|
|
371
|
+
return await timedQuery(() => this.__store.remove(id));
|
|
364
372
|
}
|
|
365
373
|
async __removeMany(query: QueryOf<any>) {
|
|
366
374
|
return await timedQuery(() => this.__store.removeManyByQuery(query));
|
|
@@ -380,21 +388,11 @@ export class DatabaseResolver {
|
|
|
380
388
|
}
|
|
381
389
|
|
|
382
390
|
const getQueryDataFromKey = (queryKey: string, args: any): { query: any; queryOption: any } => {
|
|
383
|
-
const lastArg = args.at(-1);
|
|
384
|
-
const hasQueryOption =
|
|
385
|
-
lastArg &&
|
|
386
|
-
typeof lastArg === "object" &&
|
|
387
|
-
(typeof lastArg.select === "object" ||
|
|
388
|
-
typeof lastArg.skip === "number" ||
|
|
389
|
-
typeof lastArg.limit === "number" ||
|
|
390
|
-
typeof lastArg.sort === "string");
|
|
391
391
|
const filterInfo = getFilterInfoByKey(database.filter, queryKey);
|
|
392
392
|
const queryFn = filterInfo.queryFn;
|
|
393
393
|
if (!queryFn) throw new Error(`No query function for key: ${queryKey}`);
|
|
394
|
-
const queryArgs =
|
|
395
|
-
|
|
396
|
-
const queryOption = hasQueryOption ? lastArg : {};
|
|
397
|
-
return { query, queryOption };
|
|
394
|
+
const { queryArgs, queryOption } = splitFilterArgs(filterInfo, args);
|
|
395
|
+
return { query: queryFn(...queryArgs, documentQueryHelper), queryOption };
|
|
398
396
|
};
|
|
399
397
|
Object.entries(filterMeta.query).forEach(([queryKey, filterInfo]) => {
|
|
400
398
|
const queryFn = filterInfo.queryFn;
|
|
@@ -426,35 +424,35 @@ export class DatabaseResolver {
|
|
|
426
424
|
return (this as unknown as DatabaseInstance).__pickId(query, queryOption);
|
|
427
425
|
},
|
|
428
426
|
[`exists${capitalize(queryKey)}`]: async function (...args: any) {
|
|
429
|
-
const query = queryFn(...
|
|
427
|
+
const query = queryFn(...splitFilterArgs(filterInfo, args).queryArgs, documentQueryHelper);
|
|
430
428
|
return (this as unknown as DatabaseInstance).__exists(query);
|
|
431
429
|
},
|
|
432
430
|
[`count${capitalize(queryKey)}`]: async function (...args: any) {
|
|
433
|
-
const query = queryFn(...
|
|
431
|
+
const query = queryFn(...splitFilterArgs(filterInfo, args).queryArgs, documentQueryHelper);
|
|
434
432
|
return (this as unknown as DatabaseInstance).__count(query);
|
|
435
433
|
},
|
|
436
434
|
[`insight${capitalize(queryKey)}`]: async function (...args: any) {
|
|
437
|
-
const query = queryFn(...
|
|
435
|
+
const query = queryFn(...splitFilterArgs(filterInfo, args).queryArgs, documentQueryHelper);
|
|
438
436
|
return (this as unknown as DatabaseInstance).__insight(query);
|
|
439
437
|
},
|
|
440
438
|
[`query${capitalize(queryKey)}`]: (...args: any) =>
|
|
441
|
-
queryFn(...
|
|
439
|
+
queryFn(...splitFilterArgs(filterInfo, args).queryArgs, documentQueryHelper),
|
|
442
440
|
[`remove${capitalize(queryKey)}`]: async function (...args: any) {
|
|
443
|
-
const query = queryFn(...
|
|
441
|
+
const query = queryFn(...splitFilterArgs(filterInfo, args).queryArgs, documentQueryHelper);
|
|
444
442
|
return (this as unknown as DatabaseInstance).__removeMany(query);
|
|
445
443
|
},
|
|
446
444
|
[`removeOne${capitalize(queryKey)}`]: async function (...args: any) {
|
|
447
|
-
const query = queryFn(...
|
|
445
|
+
const query = queryFn(...splitFilterArgs(filterInfo, args).queryArgs, documentQueryHelper);
|
|
448
446
|
return (this as unknown as DatabaseInstance).__removeOne(query);
|
|
449
447
|
},
|
|
450
448
|
[`update${capitalize(queryKey)}`]: function (...args: any): UpdateChain {
|
|
451
449
|
const instance = this as unknown as DatabaseInstance;
|
|
452
|
-
const query = queryFn(...
|
|
450
|
+
const query = queryFn(...splitFilterArgs(filterInfo, args).queryArgs, documentQueryHelper);
|
|
453
451
|
return { set: (update) => instance.__updateMany(query, update) };
|
|
454
452
|
},
|
|
455
453
|
[`updateOne${capitalize(queryKey)}`]: function (...args: any): UpdateChain {
|
|
456
454
|
const instance = this as unknown as DatabaseInstance;
|
|
457
|
-
const query = queryFn(...
|
|
455
|
+
const query = queryFn(...splitFilterArgs(filterInfo, args).queryArgs, documentQueryHelper);
|
|
458
456
|
return { set: (update) => instance.__updateOne(query, update) };
|
|
459
457
|
},
|
|
460
458
|
});
|
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
type DocumentUpdateInput,
|
|
11
11
|
documentQueryHelper,
|
|
12
12
|
type FindQueryOption,
|
|
13
|
-
fillMissingFilterArgs,
|
|
14
13
|
getFilterInfoByKey,
|
|
15
14
|
getFilterMeta,
|
|
16
15
|
type ListQueryOption,
|
|
17
16
|
type SaveEventType,
|
|
17
|
+
splitFilterArgs,
|
|
18
18
|
type UpdateChain,
|
|
19
19
|
} from "akanjs/document";
|
|
20
20
|
import type { DatabaseService, ServiceCls } from "akanjs/service";
|
|
@@ -127,21 +127,11 @@ export class ServiceResolver {
|
|
|
127
127
|
const className = capitalize(database.refName);
|
|
128
128
|
Object.assign(srvRef.prototype, ServiceResolver.#getDefaultDbServiceMethods(database.refName, className, cascade));
|
|
129
129
|
const getQueryDataFromKey = (queryKey: string, args: any): { query: any; queryOption: any } => {
|
|
130
|
-
const lastArg = args.at(-1);
|
|
131
|
-
const hasQueryOption =
|
|
132
|
-
lastArg &&
|
|
133
|
-
typeof lastArg === "object" &&
|
|
134
|
-
(typeof lastArg.select === "object" ||
|
|
135
|
-
typeof lastArg.skip === "number" ||
|
|
136
|
-
typeof lastArg.limit === "number" ||
|
|
137
|
-
typeof lastArg.sort === "string");
|
|
138
130
|
const filterInfo = getFilterInfoByKey(database.filter, queryKey);
|
|
139
131
|
const queryFn = filterInfo.queryFn;
|
|
140
132
|
if (!queryFn) throw new Error(`No query function for key: ${queryKey}`);
|
|
141
|
-
const queryArgs =
|
|
142
|
-
|
|
143
|
-
const queryOption = hasQueryOption ? lastArg : {};
|
|
144
|
-
return { query, queryOption };
|
|
133
|
+
const { queryArgs, queryOption } = splitFilterArgs(filterInfo, args);
|
|
134
|
+
return { query: queryFn(...queryArgs, documentQueryHelper), queryOption };
|
|
145
135
|
};
|
|
146
136
|
const filterMeta = getFilterMeta(database.filter);
|
|
147
137
|
const queryKeys = Object.keys(filterMeta.query);
|
|
@@ -189,7 +179,7 @@ export class ServiceResolver {
|
|
|
189
179
|
return this.__insight(query);
|
|
190
180
|
},
|
|
191
181
|
[`query${capitalize(queryKey)}`]: function (this: DatabaseService, ...args: any) {
|
|
192
|
-
return queryFn(...
|
|
182
|
+
return queryFn(...splitFilterArgs(filterInfo, args).queryArgs, documentQueryHelper);
|
|
193
183
|
},
|
|
194
184
|
[`remove${capitalizedQueryKey}`]: async function (this: DatabaseService, ...args: any) {
|
|
195
185
|
const { query } = getQueryDataFromKey(queryKey, args);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], "ping" | "pingBody" | "pingParam" | "pingQuery" | "wsPing" | "pubsubPing", "serverUnreachable" | "serverUnavailable" | "gatewayTimeout" | "unexpectedResponse", "error" | "remove" | "save" | "refresh" | "password" | "
|
|
1
|
+
export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], "ping" | "pingBody" | "pingParam" | "pingQuery" | "wsPing" | "pubsubPing", "serverUnreachable" | "serverUnavailable" | "gatewayTimeout" | "unexpectedResponse", "error" | "remove" | "save" | "refresh" | "password" | "skip" | "stop" | "send" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "edit" | "view">;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { AgentEndpoint, AgentTurn, BaseEndpoint } from "akanjs/signal";
|
|
2
2
|
export declare const dictionary: {
|
|
3
|
-
base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "
|
|
3
|
+
base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "skip" | "stop" | "send" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "edit" | "view">, "base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse">;
|
|
4
4
|
agentTurn: import("./locale.d.ts").DictModule<import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc`, never>;
|
|
5
5
|
agent: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">;
|
|
6
6
|
};
|
|
7
|
-
export declare const Err: import("./trans.d.ts").ErrConstructor<"base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse" | "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">, translate: (lang: "en" | "ko" | (string & {}) | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "
|
|
8
|
-
info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "
|
|
9
|
-
success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "
|
|
10
|
-
error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "
|
|
11
|
-
warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "
|
|
12
|
-
loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "
|
|
13
|
-
}, getDictionary: (lang: "en" | "ko" | (string & {}) | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "
|
|
7
|
+
export declare const Err: import("./trans.d.ts").ErrConstructor<"base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse" | "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">, translate: (lang: "en" | "ko" | (string & {}) | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "skip" | "stop" | "send" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, data?: import("./trans.d.ts").TranslationData) => string, msg: {
|
|
8
|
+
info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "skip" | "stop" | "send" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
9
|
+
success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "skip" | "stop" | "send" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
10
|
+
error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "skip" | "stop" | "send" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
11
|
+
warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "skip" | "stop" | "send" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
12
|
+
loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "skip" | "stop" | "send" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
13
|
+
}, getDictionary: (lang: "en" | "ko" | (string & {}) | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "skip" | "stop" | "send" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentQueue" | "agentQueuePlaceholder" | "agentQueued" | "agentQueueEdit" | "agentQueueCancel" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "noSelection" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "draftConflict" | "draftApplied" | "draftRestore" | "draftDiscard" | "draftStartOver" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, __Error_Key__: "base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse" | "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed";
|
|
@@ -28,6 +28,8 @@ export declare class DocumentSchema<Doc = unknown> {
|
|
|
28
28
|
readonly indexes: DocumentIndexDescriptor[];
|
|
29
29
|
pre<HookDoc = Doc>(type: SaveEventType, hook: DocumentSaveHook<HookDoc>): this;
|
|
30
30
|
post<HookDoc = Doc>(type: SaveEventType, hook: DocumentSaveHook<HookDoc>): this;
|
|
31
|
+
removePre<HookDoc = Doc>(type: SaveEventType, hook: DocumentSaveHook<HookDoc>): this;
|
|
32
|
+
removePost<HookDoc = Doc>(type: SaveEventType, hook: DocumentSaveHook<HookDoc>): this;
|
|
31
33
|
hook(type: DocumentHookName, hook: DocumentSaveHook<Doc>): this;
|
|
32
34
|
index(fields: DocumentIndexDescriptor["fields"], options?: Omit<DocumentIndexDescriptor, "fields">): this;
|
|
33
35
|
createIndex(name: string): DocumentIndexBuilder<this>;
|
|
@@ -19,6 +19,14 @@ export declare const getFilterInfoByKey: <ArgNames extends string[] = [], Args e
|
|
|
19
19
|
export declare const setFilterInfoByKey: <ArgNames extends string[] = [], Args extends any[] = any[], Model = any>(modelRef: Cls<Model>, key: string, filterInfo: FilterInfo<ArgNames, Args, Model>) => void;
|
|
20
20
|
export declare const getFilterSortByKey: (modelRef: FilterCls, key: string) => unknown;
|
|
21
21
|
export declare const fillMissingFilterArgs: (filterInfo: FilterInfo, args: unknown[]) => any[];
|
|
22
|
+
/**
|
|
23
|
+
* Splits a generated filter method's arguments into the filter's own args and the trailing query option.
|
|
24
|
+
* Shared by the database and service resolvers so the two cannot drift into disagreeing about what an option is.
|
|
25
|
+
*/
|
|
26
|
+
export declare const splitFilterArgs: (filterInfo: FilterInfo, args: unknown[]) => {
|
|
27
|
+
queryArgs: any[];
|
|
28
|
+
queryOption: Record<string, unknown>;
|
|
29
|
+
};
|
|
22
30
|
export interface FilterArgInfo {
|
|
23
31
|
name: string;
|
|
24
32
|
argRef: ConstantFieldType;
|
package/types/document/into.d.ts
CHANGED
|
@@ -15,14 +15,6 @@ interface DefaultMdlStats<TDocument, TSchema, _Partial extends Partial<TSchema>
|
|
|
15
15
|
pickById: (docId: string | undefined, projection?: _Projection) => Promise<TDocument>;
|
|
16
16
|
sample: (query: _FilterQuery, size?: number) => Promise<TDocument[]>;
|
|
17
17
|
sampleOne: (query: _FilterQuery) => Promise<TDocument | null>;
|
|
18
|
-
preSaveListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
19
|
-
postSaveListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
20
|
-
preCreateListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
21
|
-
postCreateListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
22
|
-
preUpdateListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
23
|
-
postUpdateListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
24
|
-
preRemoveListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
25
|
-
postRemoveListenerSet: Set<(doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>>;
|
|
26
18
|
listenPre: (eventType: SaveEventType, listener: (doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>) => () => void;
|
|
27
19
|
listenPost: (eventType: SaveEventType, listener: (doc: TDocument, type: CRUDEventType, previous?: TDocument) => PromiseOrObject<void>) => () => void;
|
|
28
20
|
}
|
package/types/ui/Select.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface SelectProps<T extends string | number | boolean | null | undefi
|
|
|
24
24
|
selectClassName?: string;
|
|
25
25
|
selectorClassName?: string;
|
|
26
26
|
selectedClassName?: string;
|
|
27
|
-
/** Allow no value to be selected. */
|
|
27
|
+
/** Allow no value to be selected: offers a clear row and the clear button. */
|
|
28
28
|
nullable?: boolean;
|
|
29
29
|
/** Disable open and selection behavior. */
|
|
30
30
|
disabled?: boolean;
|
package/ui/Field/Relation.tsx
CHANGED
|
@@ -80,9 +80,7 @@ export const Parent = <Light extends { id: string }>({
|
|
|
80
80
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
81
81
|
{label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
|
|
82
82
|
<Select<string | null, false, true>
|
|
83
|
-
|
|
84
|
-
desc={desc}
|
|
85
|
-
labelClassName={labelClassName}
|
|
83
|
+
nullable={nullable}
|
|
86
84
|
selectClassName={selectClassName}
|
|
87
85
|
value={value?.id ?? null}
|
|
88
86
|
searchable
|
|
@@ -173,10 +171,8 @@ export const ParentId = <Light extends { id: string }>({
|
|
|
173
171
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
174
172
|
{label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
|
|
175
173
|
<Select<string | null, false, true>
|
|
174
|
+
nullable={nullable}
|
|
176
175
|
searchable
|
|
177
|
-
desc={desc}
|
|
178
|
-
label={label}
|
|
179
|
-
labelClassName={labelClassName}
|
|
180
176
|
selectClassName={selectClassName}
|
|
181
177
|
value={value}
|
|
182
178
|
options={modelList.map((model) => model.id)}
|
|
@@ -268,10 +264,8 @@ export const Children = <Light extends { id: string }>({
|
|
|
268
264
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
269
265
|
{label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
|
|
270
266
|
<Select
|
|
267
|
+
nullable={nullable}
|
|
271
268
|
searchable
|
|
272
|
-
desc={desc}
|
|
273
|
-
label={label}
|
|
274
|
-
labelClassName={labelClassName}
|
|
275
269
|
selectClassName={selectClassName}
|
|
276
270
|
multiple
|
|
277
271
|
value={(value ?? []).map((model) => model.id)}
|
|
@@ -357,10 +351,8 @@ export const ChildrenId = <Light extends { id: string }>({
|
|
|
357
351
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
358
352
|
{label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
|
|
359
353
|
<Select
|
|
354
|
+
nullable={nullable}
|
|
360
355
|
searchable
|
|
361
|
-
desc={desc}
|
|
362
|
-
label={label}
|
|
363
|
-
labelClassName={labelClassName}
|
|
364
356
|
multiple
|
|
365
357
|
|
|
366
358
|
value={value ?? []}
|
package/ui/Select.tsx
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { type Cls, type EnumInstance, isEnum } from "akanjs/base";
|
|
3
3
|
import { cn, usePage } from "akanjs/client";
|
|
4
|
+
import { clamp } from "akanjs/common";
|
|
4
5
|
import { useFieldTool } from "akanjs/store";
|
|
5
6
|
import { useDebounce } from "akanjs/webkit";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type ComponentType,
|
|
9
|
+
createElement,
|
|
10
|
+
type KeyboardEvent,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
useEffect,
|
|
13
|
+
useId,
|
|
14
|
+
useMemo,
|
|
15
|
+
useRef,
|
|
16
|
+
useState,
|
|
17
|
+
} from "react";
|
|
7
18
|
import { createPortal } from "react-dom";
|
|
8
19
|
import { BiCheck, BiChevronDown, BiX } from "react-icons/bi";
|
|
9
20
|
import { BsQuestionCircleFill } from "react-icons/bs";
|
|
10
21
|
import { TiDelete } from "react-icons/ti";
|
|
11
22
|
import { agentAttrs } from "./agentAttrs";
|
|
23
|
+
import { Label } from "./Field/Label";
|
|
12
24
|
import { overlayZ, useOverlayLayerProps } from "./overlayLayer";
|
|
13
25
|
import { useOverlayPosition } from "./overlayPosition";
|
|
14
26
|
import { useUiOverride } from "./UiOverride";
|
|
@@ -43,7 +55,7 @@ export interface SelectProps<
|
|
|
43
55
|
selectClassName?: string;
|
|
44
56
|
selectorClassName?: string;
|
|
45
57
|
selectedClassName?: string;
|
|
46
|
-
/** Allow no value to be selected. */
|
|
58
|
+
/** Allow no value to be selected: offers a clear row and the clear button. */
|
|
47
59
|
nullable?: boolean;
|
|
48
60
|
/** Disable open and selection behavior. */
|
|
49
61
|
disabled?: boolean;
|
|
@@ -91,69 +103,120 @@ const DefaultSelect = <
|
|
|
91
103
|
useFieldTool(onChange, { disabled });
|
|
92
104
|
const { l } = usePage();
|
|
93
105
|
const [isOpen, setIsOpen] = useState(false);
|
|
94
|
-
const labeledOptions:
|
|
95
|
-
()
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
[options],
|
|
109
|
-
);
|
|
106
|
+
const labeledOptions: LabelOption<T>[] = useMemo(() => {
|
|
107
|
+
if (isEnum(options as Cls))
|
|
108
|
+
return (options as EnumInstance<string, T>).values.map((v) => ({
|
|
109
|
+
label: typeof v === "string" ? v : typeof v === "object" ? JSON.stringify(v) : String(v),
|
|
110
|
+
value: v,
|
|
111
|
+
}));
|
|
112
|
+
const list = options as (T | LabelOption<T>)[];
|
|
113
|
+
const first = list[0];
|
|
114
|
+
|
|
115
|
+
const isLabeled = typeof first === "object" && first !== null && "label" in first && "value" in first;
|
|
116
|
+
return isLabeled
|
|
117
|
+
? (list as LabelOption<T>[])
|
|
118
|
+
: (list as T[]).map((v) => ({ label: v as string | boolean | number, value: v }));
|
|
119
|
+
}, [options]);
|
|
110
120
|
|
|
111
|
-
const [selectedValues, setSelectedValues] = useState<T[]>(multiple ? (value as T[]) : [value as T]);
|
|
112
121
|
const [searchText, setSearchText] = useState("");
|
|
113
122
|
|
|
114
123
|
const [portal, setPortal] = useState<HTMLElement | null>(null);
|
|
115
|
-
const [searchOptions, setSearchOptions] = useState<
|
|
124
|
+
const [searchOptions, setSearchOptions] = useState<LabelOption<T>[]>(labeledOptions);
|
|
125
|
+
const [activeIdx, setActiveIdx] = useState(-1);
|
|
126
|
+
const listId = useId();
|
|
116
127
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
128
|
+
|
|
129
|
+
const fieldRef = useRef<HTMLDivElement>(null);
|
|
117
130
|
const optionsRef = useRef<HTMLDivElement>(null);
|
|
118
131
|
|
|
119
132
|
const overlayLayerProps = useOverlayLayerProps();
|
|
120
133
|
const position = useOverlayPosition({
|
|
121
134
|
opened: isOpen,
|
|
122
|
-
triggerRef:
|
|
135
|
+
triggerRef: fieldRef,
|
|
123
136
|
panelRef: optionsRef,
|
|
124
137
|
align: "start",
|
|
125
138
|
});
|
|
126
139
|
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
const selectedValues: T[] = multiple ? ((value as T[]) ?? []) : [value as T];
|
|
141
|
+
const hasSelection = multiple ? selectedValues.length > 0 : value !== null && value !== undefined;
|
|
142
|
+
|
|
143
|
+
const rows: (LabelOption<T> | null)[] = useMemo(
|
|
144
|
+
() => (nullable ? [null, ...searchOptions] : searchOptions),
|
|
145
|
+
[nullable, searchOptions],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const selected = (v: T) => (multiple ? selectedValues.includes(v) : v === selectedValues[0]);
|
|
149
|
+
|
|
150
|
+
const resetSearch = () => {
|
|
151
|
+
setSearchText("");
|
|
152
|
+
setSearchOptions(labeledOptions);
|
|
153
|
+
};
|
|
154
|
+
const close = () => {
|
|
155
|
+
setIsOpen(false);
|
|
156
|
+
setActiveIdx(-1);
|
|
157
|
+
resetSearch();
|
|
130
158
|
};
|
|
131
159
|
|
|
132
160
|
const handleClickOutside = (event: Event) => {
|
|
133
161
|
const target = event.target as Node;
|
|
134
162
|
|
|
135
163
|
if (optionsRef.current?.contains(target)) return;
|
|
136
|
-
if (dropdownRef.current && !dropdownRef.current.contains(target))
|
|
137
|
-
setIsOpen(false);
|
|
138
|
-
}
|
|
164
|
+
if (dropdownRef.current && !dropdownRef.current.contains(target)) close();
|
|
139
165
|
};
|
|
140
|
-
const onSelect = (v: T
|
|
166
|
+
const onSelect = (v: T) => {
|
|
141
167
|
if (multiple) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
selectedValues.filter((rev) => rev !== v),
|
|
146
|
-
selectedValues,
|
|
147
|
-
);
|
|
148
|
-
} else {
|
|
149
|
-
setSelectedValues([...selectedValues, v]);
|
|
150
|
-
(onChange as (value: T[], prev: T[]) => void)([...selectedValues, v], selectedValues);
|
|
151
|
-
}
|
|
168
|
+
const next = selectedValues.includes(v) ? selectedValues.filter((rev) => rev !== v) : [...selectedValues, v];
|
|
169
|
+
(onChange as (value: T[], prev: T[]) => void)(next, selectedValues);
|
|
170
|
+
resetSearch();
|
|
152
171
|
} else {
|
|
153
|
-
setSelectedValues([v]);
|
|
154
172
|
(onChange as (value: T, prev: T) => void)(v, selectedValues[0]);
|
|
155
|
-
|
|
173
|
+
close();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const onClear = () => {
|
|
177
|
+
if (multiple) (onChange as (value: T[], prev: T[]) => void)([], selectedValues);
|
|
178
|
+
else (onChange as (value: T | null, prev: T | null) => void)(null, selectedValues[0]);
|
|
179
|
+
close();
|
|
180
|
+
};
|
|
181
|
+
const commit = (idx: number) => {
|
|
182
|
+
const row = rows[idx];
|
|
183
|
+
if (row === undefined) close();
|
|
184
|
+
else if (row === null) onClear();
|
|
185
|
+
else onSelect(row.value);
|
|
186
|
+
};
|
|
187
|
+
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
188
|
+
if (disabled) return;
|
|
189
|
+
if (e.key === "Tab") {
|
|
190
|
+
if (isOpen) close();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (e.key === "Escape") {
|
|
194
|
+
if (!isOpen) return;
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
close();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
if (!isOpen) {
|
|
202
|
+
setIsOpen(true);
|
|
203
|
+
setActiveIdx(e.key === "ArrowUp" ? rows.length - 1 : 0);
|
|
204
|
+
} else if (rows.length) setActiveIdx(clamp(activeIdx + (e.key === "ArrowDown" ? 1 : -1), 0, rows.length - 1));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (e.key === "Home" || e.key === "End") {
|
|
208
|
+
if (!isOpen || !rows.length) return;
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
setActiveIdx(e.key === "Home" ? 0 : rows.length - 1);
|
|
211
|
+
return;
|
|
156
212
|
}
|
|
213
|
+
|
|
214
|
+
if (e.key !== "Enter" && !(e.key === " " && !searchable)) return;
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
if (!isOpen) {
|
|
217
|
+
setIsOpen(true);
|
|
218
|
+
setActiveIdx(0);
|
|
219
|
+
} else commit(activeIdx);
|
|
157
220
|
};
|
|
158
221
|
const debouncedOnSearch = useDebounce(
|
|
159
222
|
(text: string) => {
|
|
@@ -185,66 +248,86 @@ const DefaultSelect = <
|
|
|
185
248
|
}, [isOpen]);
|
|
186
249
|
|
|
187
250
|
useEffect(() => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
251
|
+
setActiveIdx(-1);
|
|
252
|
+
}, [rows]);
|
|
253
|
+
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
if (!isOpen || activeIdx < 0) return;
|
|
256
|
+
optionsRef.current?.querySelector<HTMLElement>(`[data-idx="${activeIdx}"]`)?.scrollIntoView({ block: "nearest" });
|
|
257
|
+
}, [activeIdx, isOpen]);
|
|
191
258
|
|
|
192
259
|
return (
|
|
193
260
|
<div {...agentAttrs(onChange)} className={cn("relative min-w-[150px]", className)} ref={dropdownRef}>
|
|
261
|
+
{label ? <Label className={labelClassName} label={label} desc={desc} nullable={nullable} /> : null}
|
|
194
262
|
<div
|
|
263
|
+
ref={fieldRef}
|
|
264
|
+
role="combobox"
|
|
265
|
+
aria-haspopup="listbox"
|
|
266
|
+
aria-expanded={isOpen}
|
|
267
|
+
aria-controls={listId}
|
|
268
|
+
aria-disabled={disabled}
|
|
269
|
+
aria-activedescendant={isOpen && activeIdx >= 0 ? `${listId}-${activeIdx}` : undefined}
|
|
270
|
+
tabIndex={disabled || searchable ? -1 : 0}
|
|
195
271
|
data-open={isOpen}
|
|
196
272
|
className={cn(
|
|
197
273
|
"flex h-auto min-h-[40px] w-full cursor-pointer items-center rounded-field border border-input px-0 py-1 pr-5 pl-1 focus:outline-hidden",
|
|
198
|
-
"bg-background data-[open=true]:border-primary",
|
|
274
|
+
"bg-background focus-visible:border-primary data-[open=true]:border-primary",
|
|
199
275
|
disabled && "pointer-events-none opacity-50",
|
|
200
276
|
selectClassName,
|
|
201
277
|
isOpen && "border-border",
|
|
202
278
|
)}
|
|
279
|
+
onKeyDown={onKeyDown}
|
|
203
280
|
onClick={() => {
|
|
204
281
|
if (disabled) return;
|
|
205
|
-
|
|
282
|
+
if (isOpen) close();
|
|
283
|
+
else setIsOpen(true);
|
|
206
284
|
}}
|
|
207
285
|
>
|
|
208
286
|
<span className="flex w-full flex-wrap items-center gap-1">
|
|
209
|
-
{multiple
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
>
|
|
218
|
-
{renderSelected ? renderSelected(optionValue.value) : optionValue.label}
|
|
219
|
-
<button
|
|
220
|
-
onClick={(e) => {
|
|
221
|
-
e.stopPropagation();
|
|
222
|
-
e.preventDefault();
|
|
223
|
-
onSelect(optionValue.value, index);
|
|
224
|
-
}}
|
|
225
|
-
className="opacity-50 duration-300 hover:opacity-100"
|
|
287
|
+
{multiple
|
|
288
|
+
? selectedValues.map((v, index) => {
|
|
289
|
+
const optionValue = labeledOptions.find((option) => option.value === v);
|
|
290
|
+
if (!optionValue) return null;
|
|
291
|
+
return (
|
|
292
|
+
<div
|
|
293
|
+
key={index}
|
|
294
|
+
className="flex items-center gap-2 rounded-field bg-primary px-2 py-1 text-primary-foreground text-xs"
|
|
226
295
|
>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
296
|
+
{renderSelected ? renderSelected(optionValue.value) : optionValue.label}
|
|
297
|
+
<button
|
|
298
|
+
type="button"
|
|
299
|
+
onClick={(e) => {
|
|
300
|
+
e.stopPropagation();
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
onSelect(optionValue.value);
|
|
303
|
+
}}
|
|
304
|
+
className="opacity-50 duration-300 hover:opacity-100"
|
|
305
|
+
>
|
|
306
|
+
<BiX className="text-base" />
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
})
|
|
311
|
+
: null}
|
|
312
|
+
{!multiple && hasSelection ? (
|
|
233
313
|
<div className="px-2 py-1">
|
|
234
|
-
{(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
: ""}
|
|
314
|
+
{(() => {
|
|
315
|
+
|
|
316
|
+
const optionValue = labeledOptions.find((option) => selected(option.value));
|
|
317
|
+
if (!optionValue) return null;
|
|
318
|
+
return renderSelected ? renderSelected(optionValue.value) : optionValue.label;
|
|
319
|
+
})()}
|
|
241
320
|
</div>
|
|
242
|
-
)}
|
|
321
|
+
) : null}
|
|
322
|
+
{!hasSelection && !searchable && placeholder ? (
|
|
323
|
+
<span className="px-2 py-1 text-muted-foreground">{placeholder}</span>
|
|
324
|
+
) : null}
|
|
243
325
|
{searchable ? (
|
|
244
326
|
<input
|
|
245
327
|
type="text"
|
|
328
|
+
aria-autocomplete="list"
|
|
246
329
|
className="w-full flex-1 border-none bg-transparent shadow-none outline-none focus:border-none focus:shadow-none focus:outline-none"
|
|
247
|
-
placeholder={
|
|
330
|
+
placeholder={hasSelection ? "" : placeholder}
|
|
248
331
|
value={searchText}
|
|
249
332
|
onChange={(e) => {
|
|
250
333
|
if (!isOpen) setIsOpen(true);
|
|
@@ -266,18 +349,13 @@ const DefaultSelect = <
|
|
|
266
349
|
/>
|
|
267
350
|
) : null}
|
|
268
351
|
</span>
|
|
269
|
-
{
|
|
270
|
-
(!multiple && selectedValues[0] !== undefined && (selectedValues[0] as T) !== null) ? (
|
|
352
|
+
{nullable && hasSelection ? (
|
|
271
353
|
<TiDelete
|
|
272
354
|
className="absolute right-8 z-2 h-full text-lg duration-300 hover:cursor-pointer hover:opacity-50"
|
|
273
355
|
onClick={(e) => {
|
|
274
356
|
e.stopPropagation();
|
|
275
357
|
e.preventDefault();
|
|
276
|
-
|
|
277
|
-
setSelectedValues([]);
|
|
278
|
-
if (multiple) (onChange as (value: T[], prev: T[]) => void)([], selectedValues);
|
|
279
|
-
else (onChange as (value: T | null, prev: T | null) => void)(null, selectedValues[0]);
|
|
280
|
-
setIsOpen(false);
|
|
358
|
+
onClear();
|
|
281
359
|
}}
|
|
282
360
|
/>
|
|
283
361
|
) : null}
|
|
@@ -289,6 +367,10 @@ const DefaultSelect = <
|
|
|
289
367
|
? createPortal(
|
|
290
368
|
<div
|
|
291
369
|
ref={optionsRef}
|
|
370
|
+
id={listId}
|
|
371
|
+
role="listbox"
|
|
372
|
+
aria-multiselectable={multiple}
|
|
373
|
+
aria-hidden={!isOpen}
|
|
292
374
|
{...overlayLayerProps}
|
|
293
375
|
data-open={isOpen}
|
|
294
376
|
|
|
@@ -307,49 +389,50 @@ const DefaultSelect = <
|
|
|
307
389
|
selectorClassName,
|
|
308
390
|
)}
|
|
309
391
|
>
|
|
310
|
-
{
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
392
|
+
{rows.map((row, idx) => {
|
|
393
|
+
const isSelected = row ? selected(row.value) : !hasSelection;
|
|
394
|
+
return (
|
|
395
|
+
<div key={idx} className="group">
|
|
396
|
+
<div
|
|
397
|
+
id={`${listId}-${idx}`}
|
|
398
|
+
role="option"
|
|
399
|
+
aria-selected={isSelected}
|
|
400
|
+
data-idx={idx}
|
|
401
|
+
data-active={idx === activeIdx}
|
|
402
|
+
className={cn(
|
|
403
|
+
"relative mx-1 my-0.5 cursor-pointer rounded-field p-2 transition-colors last:border-b-0 hover:bg-muted data-[active=true]:bg-muted",
|
|
404
|
+
!row && "text-foreground/50",
|
|
405
|
+
!multiple && isSelected && selectedClassName,
|
|
406
|
+
isSelected && "bg-success/70 text-success-foreground",
|
|
407
|
+
)}
|
|
408
|
+
onClick={() => {
|
|
409
|
+
commit(idx);
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
{row
|
|
413
|
+
? renderOption
|
|
414
|
+
? renderOption(row.value)
|
|
415
|
+
: row.label
|
|
416
|
+
: (placeholder ?? l("base.noSelection"))}
|
|
417
|
+
<div className="absolute top-1/2 right-2 -translate-y-1/2 flex-wrap duration-200">
|
|
323
418
|
<div
|
|
324
419
|
className={cn(
|
|
325
|
-
"
|
|
326
|
-
|
|
327
|
-
isSelected && "bg-success/70 text-success-foreground",
|
|
420
|
+
"duration-200",
|
|
421
|
+
isSelected ? "translate-y-0 opacity-100" : "-translate-y-full opacity-0",
|
|
328
422
|
)}
|
|
329
|
-
onClick={() => {
|
|
330
|
-
onSelect(option.value, index);
|
|
331
|
-
setSearchText("");
|
|
332
|
-
}}
|
|
333
423
|
>
|
|
334
|
-
|
|
335
|
-
{renderOption ? renderOption(option.value) : option.label}
|
|
336
|
-
<div className="absolute top-1/2 right-2 -translate-y-1/2 flex-wrap duration-200">
|
|
337
|
-
<div
|
|
338
|
-
className={cn(
|
|
339
|
-
"duration-200",
|
|
340
|
-
isSelected ? "translate-y-0 opacity-100" : "-translate-y-full opacity-0",
|
|
341
|
-
)}
|
|
342
|
-
>
|
|
343
|
-
<BiCheck />
|
|
344
|
-
</div>
|
|
345
|
-
</div>
|
|
346
|
-
</div>
|
|
347
|
-
<div className="h-px w-full px-2 group-last:hidden">
|
|
348
|
-
<div className="size-full bg-foreground/10" />
|
|
424
|
+
<BiCheck />
|
|
349
425
|
</div>
|
|
350
426
|
</div>
|
|
351
|
-
|
|
352
|
-
|
|
427
|
+
</div>
|
|
428
|
+
<div className="h-px w-full px-2 group-last:hidden">
|
|
429
|
+
<div className="size-full bg-foreground/10" />
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
})}
|
|
434
|
+
{searchOptions.length
|
|
435
|
+
? null
|
|
353
436
|
: (empty ?? (
|
|
354
437
|
<div className="flex size-full flex-col items-center justify-center gap-2 p-2 text-center text-5xl text-foreground/50">
|
|
355
438
|
<BsQuestionCircleFill />
|