Initial commit
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { resolveObject, resolveList, getDisplayProperty } from '@/lib/schemaResolver';
|
||||
import { jmapGetBatched, jmapQueryAllAndGet, getAccountId } from '@/services/jmap/client';
|
||||
import { useAccountStore } from '@/stores/accountStore';
|
||||
import { useCacheStore, type ObjectListEntry } from '@/stores/cacheStore';
|
||||
import type { Schema } from '@/types/schema';
|
||||
|
||||
interface PendingBatch {
|
||||
ids: Set<string>;
|
||||
displayProp: string;
|
||||
schema: Schema;
|
||||
}
|
||||
|
||||
const pendingByType = new Map<string, PendingBatch>();
|
||||
|
||||
function requestObjectLabel(parentObjectName: string, viewOrObjectName: string, id: string, schema: Schema): void {
|
||||
const existing = pendingByType.get(parentObjectName);
|
||||
if (existing) {
|
||||
existing.ids.add(id);
|
||||
return;
|
||||
}
|
||||
|
||||
const batch: PendingBatch = {
|
||||
ids: new Set([id]),
|
||||
displayProp: getDisplayProperty(schema, viewOrObjectName),
|
||||
schema,
|
||||
};
|
||||
pendingByType.set(parentObjectName, batch);
|
||||
|
||||
queueMicrotask(() => {
|
||||
const taken = pendingByType.get(parentObjectName);
|
||||
if (taken !== batch) return;
|
||||
pendingByType.delete(parentObjectName);
|
||||
void executeBatch(parentObjectName, taken);
|
||||
});
|
||||
}
|
||||
|
||||
async function executeBatch(parentObjectName: string, batch: PendingBatch): Promise<void> {
|
||||
const ids = Array.from(batch.ids);
|
||||
const setDisplayNames = useCacheStore.getState().setDisplayNames;
|
||||
|
||||
try {
|
||||
const accountId = getAccountId(parentObjectName);
|
||||
const list = await jmapGetBatched(parentObjectName, accountId, ids, ['id', batch.displayProp]);
|
||||
const entries: Record<string, string> = {};
|
||||
for (const item of list) {
|
||||
const itemId = item.id as string;
|
||||
if (!itemId) continue;
|
||||
entries[itemId] = (item[batch.displayProp] as string) ?? itemId;
|
||||
}
|
||||
for (const id of ids) {
|
||||
if (!(id in entries)) entries[id] = id;
|
||||
}
|
||||
setDisplayNames(parentObjectName, entries);
|
||||
} catch (err) {
|
||||
console.error('Failed to batch-fetch labels for', parentObjectName, err);
|
||||
const fallback: Record<string, string> = {};
|
||||
for (const id of ids) fallback[id] = id;
|
||||
setDisplayNames(parentObjectName, fallback);
|
||||
}
|
||||
}
|
||||
|
||||
export type ObjectOption = ObjectListEntry;
|
||||
|
||||
async function fetchObjectList(viewOrObjectName: string, schema: Schema): Promise<ObjectOption[]> {
|
||||
const resolved = resolveObject(schema, viewOrObjectName);
|
||||
const objectName = resolved?.objectName ?? viewOrObjectName;
|
||||
const list = resolveList(schema, viewOrObjectName, objectName);
|
||||
const filtersStatic = list?.filtersStatic;
|
||||
|
||||
const accountId = getAccountId(objectName);
|
||||
const displayProp = getDisplayProperty(schema, viewOrObjectName);
|
||||
|
||||
let items: Array<Record<string, unknown>> = [];
|
||||
try {
|
||||
const result = await jmapQueryAllAndGet(
|
||||
objectName,
|
||||
accountId,
|
||||
{ filter: filtersStatic && Object.keys(filtersStatic).length > 0 ? filtersStatic : undefined },
|
||||
['id', displayProp],
|
||||
);
|
||||
items = result.list;
|
||||
} catch (err) {
|
||||
console.error('JMAP error fetching', viewOrObjectName, err);
|
||||
return [];
|
||||
}
|
||||
|
||||
return items.map((item) => ({
|
||||
id: item.id as string,
|
||||
label: (item[displayProp] as string) ?? (item.id as string),
|
||||
}));
|
||||
}
|
||||
|
||||
export function useObjectList(viewOrObjectName: string, schema: Schema) {
|
||||
const cached = useCacheStore((s) => s.objectLists[viewOrObjectName]);
|
||||
const setObjectList = useCacheStore((s) => s.setObjectList);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const ensureLoaded = useCallback(async () => {
|
||||
if (cached) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const opts = await fetchObjectList(viewOrObjectName, schema);
|
||||
setObjectList(viewOrObjectName, opts);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch objects for', viewOrObjectName, err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [cached, viewOrObjectName, schema, setObjectList]);
|
||||
|
||||
return { options: cached ?? [], loading, hasLoaded: cached != null, ensureLoaded };
|
||||
}
|
||||
|
||||
export function useObjectLabel(
|
||||
viewOrObjectName: string,
|
||||
id: string | null | undefined,
|
||||
schema: Schema,
|
||||
): { label: string | null; loading: boolean } {
|
||||
const resolved = resolveObject(schema, viewOrObjectName);
|
||||
const parentObjectName = resolved?.objectName ?? viewOrObjectName;
|
||||
const cachedLabel = useCacheStore((s) => (id ? s.displayNames[parentObjectName]?.[id] : undefined));
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
if (cachedLabel != null) return;
|
||||
requestObjectLabel(parentObjectName, viewOrObjectName, id, schema);
|
||||
}, [id, cachedLabel, parentObjectName, viewOrObjectName, schema]);
|
||||
|
||||
return {
|
||||
label: cachedLabel ?? null,
|
||||
loading: !!id && cachedLabel == null,
|
||||
};
|
||||
}
|
||||
|
||||
export function objectSupportsSearch(schema: Schema, objectName: string): boolean {
|
||||
const resolved = resolveObject(schema, objectName);
|
||||
if (!resolved) return false;
|
||||
const list = resolveList(schema, objectName, resolved.objectName);
|
||||
return list?.filters?.some((f) => f.type === 'text') ?? false;
|
||||
}
|
||||
|
||||
export function useNoPermissionMessage(schema: Schema, viewOrObjectName: string): string | null {
|
||||
const { t } = useTranslation();
|
||||
const hasObjectPermission = useAccountStore((s) => s.hasObjectPermission);
|
||||
const resolved = resolveObject(schema, viewOrObjectName);
|
||||
if (!resolved) return null;
|
||||
const prefix = resolved.permissionPrefix;
|
||||
if (hasObjectPermission(prefix, 'Get') && hasObjectPermission(prefix, 'Query')) return null;
|
||||
const list = resolveList(schema, viewOrObjectName, resolved.objectName);
|
||||
const name = list?.pluralName ?? resolved.objectName;
|
||||
return t('field.noPermissionToView', 'You do not have permission to view {{name}}', { name });
|
||||
}
|
||||
Reference in New Issue
Block a user