refactor: improve code formatting and consistency across multiple files
- Standardized import statements and removed unnecessary line breaks for better readability in various components. - Enhanced error handling and logging in the useElectronPrinter hook. - Updated sample data formatting in AgGridShowcase for improved clarity. - Refactored JSX elements for consistent indentation and structure in LandingSample, AuthPage, and EventsPage components. - Consolidated and simplified conditional rendering logic in several components. These changes aim to enhance code maintainability and readability throughout the project.
This commit is contained in:
@@ -420,7 +420,7 @@ describe('BaseRemoteDataServices — Data Transformer Integration', () => {
|
||||
id: '42',
|
||||
bookingCode: 'BK042',
|
||||
customerName: 'Alice',
|
||||
}
|
||||
},
|
||||
});
|
||||
expect(result.status).toBe(200);
|
||||
});
|
||||
@@ -432,7 +432,7 @@ describe('BaseRemoteDataServices — Data Transformer Integration', () => {
|
||||
{ id: '1', booking_code: 'BK001', customer_name: 'Alice' },
|
||||
{ id: '2', booking_code: 'BK002', customer_name: 'Bob' },
|
||||
],
|
||||
meta: { currentPage: 1, itemsPerPage: 15, totalItems: 2, totalPages: 1 }
|
||||
meta: { currentPage: 1, itemsPerPage: 15, totalItems: 2, totalPages: 1 },
|
||||
},
|
||||
status: 200,
|
||||
});
|
||||
@@ -444,7 +444,7 @@ describe('BaseRemoteDataServices — Data Transformer Integration', () => {
|
||||
{ id: '1', bookingCode: 'BK001', customerName: 'Alice' },
|
||||
{ id: '2', bookingCode: 'BK002', customerName: 'Bob' },
|
||||
],
|
||||
meta: { page: 1, limit: 15, total: 2, totalPages: 1 }
|
||||
meta: { page: 1, limit: 15, total: 2, totalPages: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -458,7 +458,7 @@ describe('BaseRemoteDataServices — Data Transformer Integration', () => {
|
||||
|
||||
expect(result.data).toEqual({
|
||||
data: [],
|
||||
meta: { page: 1, limit: 10, total: 0, totalPages: 0 }
|
||||
meta: { page: 1, limit: 10, total: 0, totalPages: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -541,7 +541,7 @@ describe('BaseRemoteDataServices — Data Transformer Integration', () => {
|
||||
id: '42',
|
||||
bookingCode: 'BK042',
|
||||
customerName: 'ALICE', // uppercased by custom hook
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -558,7 +558,7 @@ describe('BaseRemoteDataServices — Data Transformer Integration', () => {
|
||||
|
||||
expect(result.data).toEqual({
|
||||
data: [{ id: '1', bookingCode: 'LIST-BK001', customerName: 'Alice' }],
|
||||
meta: { page: 1, limit: 10, total: 1, totalPages: 1 }
|
||||
meta: { page: 1, limit: 10, total: 1, totalPages: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -280,7 +280,11 @@ export abstract class BaseRemoteDataServices<E extends BaseEntity = BaseEntity,
|
||||
}
|
||||
|
||||
/** Delete multiple entities by IDs. Optionally sends form data as `meta` in the request body. */
|
||||
batchDelete(ids: EntityId[], meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
batchDelete(
|
||||
ids: EntityId[],
|
||||
meta?: Record<string, unknown>,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchDelete, {
|
||||
config: { ...config, data: { ids, ...(meta ? { meta } : {}) } },
|
||||
});
|
||||
@@ -297,7 +301,11 @@ export abstract class BaseRemoteDataServices<E extends BaseEntity = BaseEntity,
|
||||
}
|
||||
|
||||
/** Activate multiple entities. Optionally sends form data as `meta` in the request body. */
|
||||
batchActivate(ids: EntityId[], meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
batchActivate(
|
||||
ids: EntityId[],
|
||||
meta?: Record<string, unknown>,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchActivate, {
|
||||
config: { ...config, data: { ids, ...(meta ? { meta } : {}) } },
|
||||
});
|
||||
@@ -312,7 +320,11 @@ export abstract class BaseRemoteDataServices<E extends BaseEntity = BaseEntity,
|
||||
}
|
||||
|
||||
/** Deactivate multiple entities. Optionally sends form data as `meta` in the request body. */
|
||||
batchDeactivate(ids: EntityId[], meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
batchDeactivate(
|
||||
ids: EntityId[],
|
||||
meta?: Record<string, unknown>,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchDeactivate, {
|
||||
config: { ...config, data: { ids, ...(meta ? { meta } : {}) } },
|
||||
});
|
||||
@@ -329,7 +341,11 @@ export abstract class BaseRemoteDataServices<E extends BaseEntity = BaseEntity,
|
||||
}
|
||||
|
||||
/** Confirm processing of multiple data records. Optionally sends form data as `meta` in the request body. */
|
||||
batchConfirmData(ids: EntityId[], meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
batchConfirmData(
|
||||
ids: EntityId[],
|
||||
meta?: Record<string, unknown>,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchConfirmData, {
|
||||
config: { ...config, data: { ids, ...(meta ? { meta } : {}) } },
|
||||
});
|
||||
@@ -344,7 +360,11 @@ export abstract class BaseRemoteDataServices<E extends BaseEntity = BaseEntity,
|
||||
}
|
||||
|
||||
/** Cancel processing of multiple data records. Optionally sends form data as `meta` in the request body. */
|
||||
batchCancelData(ids: EntityId[], meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
batchCancelData(
|
||||
ids: EntityId[],
|
||||
meta?: Record<string, unknown>,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchCancelData, {
|
||||
config: { ...config, data: { ids, ...(meta ? { meta } : {}) } },
|
||||
});
|
||||
@@ -361,7 +381,11 @@ export abstract class BaseRemoteDataServices<E extends BaseEntity = BaseEntity,
|
||||
}
|
||||
|
||||
/** Rollback multiple transactions. Optionally sends form data as `meta` in the request body. */
|
||||
batchRollbackData(ids: EntityId[], meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
batchRollbackData(
|
||||
ids: EntityId[],
|
||||
meta?: Record<string, unknown>,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchRollbackData, {
|
||||
config: { ...config, data: { ids, ...(meta ? { meta } : {}) } },
|
||||
});
|
||||
@@ -376,7 +400,11 @@ export abstract class BaseRemoteDataServices<E extends BaseEntity = BaseEntity,
|
||||
}
|
||||
|
||||
/** Hold multiple transactions. Optionally sends form data as `meta` in the request body. */
|
||||
batchHoldData(ids: EntityId[], meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
batchHoldData(
|
||||
ids: EntityId[],
|
||||
meta?: Record<string, unknown>,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchHoldData, {
|
||||
config: { ...config, data: { ids, ...(meta ? { meta } : {}) } },
|
||||
});
|
||||
|
||||
@@ -52,8 +52,7 @@ import { BaseRemoteDataServices } from './base-remote.data-services';
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class CommonRemoteDataServices<
|
||||
E extends BaseEntity = BaseEntity,
|
||||
TDTO = E,
|
||||
> extends BaseRemoteDataServices<E, TDTO> {}
|
||||
|
||||
export class CommonRemoteDataServices<E extends BaseEntity = BaseEntity, TDTO = E> extends BaseRemoteDataServices<
|
||||
E,
|
||||
TDTO
|
||||
> {}
|
||||
|
||||
@@ -33,13 +33,7 @@ export class ApiError extends Error {
|
||||
/** The original Axios error, preserved for debugging. */
|
||||
readonly cause: AxiosError | undefined;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
code: ApiErrorCode,
|
||||
status: number,
|
||||
data?: unknown,
|
||||
cause?: AxiosError,
|
||||
) {
|
||||
constructor(message: string, code: ApiErrorCode, status: number, data?: unknown, cause?: AxiosError) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.code = code;
|
||||
@@ -59,30 +53,12 @@ export class ApiError extends Error {
|
||||
// Network error (no response received)
|
||||
if (!error.response) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return new ApiError(
|
||||
'Request timed out',
|
||||
ApiErrorCode.TIMEOUT,
|
||||
0,
|
||||
undefined,
|
||||
error,
|
||||
);
|
||||
return new ApiError('Request timed out', ApiErrorCode.TIMEOUT, 0, undefined, error);
|
||||
}
|
||||
if (error.code === 'ERR_CANCELED') {
|
||||
return new ApiError(
|
||||
'Request was cancelled',
|
||||
ApiErrorCode.CANCELLED,
|
||||
0,
|
||||
undefined,
|
||||
error,
|
||||
);
|
||||
return new ApiError('Request was cancelled', ApiErrorCode.CANCELLED, 0, undefined, error);
|
||||
}
|
||||
return new ApiError(
|
||||
error.message || 'Network error',
|
||||
ApiErrorCode.NETWORK_ERROR,
|
||||
0,
|
||||
undefined,
|
||||
error,
|
||||
);
|
||||
return new ApiError(error.message || 'Network error', ApiErrorCode.NETWORK_ERROR, 0, undefined, error);
|
||||
}
|
||||
|
||||
// Server responded with an error status
|
||||
@@ -93,7 +69,7 @@ export class ApiError extends Error {
|
||||
|
||||
// Extract message from common server response formats
|
||||
const serverMessage =
|
||||
(data && typeof data === 'object' && 'message' in data)
|
||||
data && typeof data === 'object' && 'message' in data
|
||||
? String((data as Record<string, unknown>).message)
|
||||
: `Request failed with status ${status}`;
|
||||
|
||||
|
||||
@@ -28,15 +28,25 @@ export enum ApiErrorCode {
|
||||
*/
|
||||
export function httpStatusToErrorCode(status: number): ApiErrorCode {
|
||||
switch (status) {
|
||||
case 400: return ApiErrorCode.BAD_REQUEST;
|
||||
case 401: return ApiErrorCode.UNAUTHORIZED;
|
||||
case 403: return ApiErrorCode.FORBIDDEN;
|
||||
case 404: return ApiErrorCode.NOT_FOUND;
|
||||
case 409: return ApiErrorCode.CONFLICT;
|
||||
case 422: return ApiErrorCode.UNPROCESSABLE_ENTITY;
|
||||
case 429: return ApiErrorCode.TOO_MANY_REQUESTS;
|
||||
case 500: return ApiErrorCode.INTERNAL_SERVER_ERROR;
|
||||
case 503: return ApiErrorCode.SERVICE_UNAVAILABLE;
|
||||
default: return ApiErrorCode.UNKNOWN;
|
||||
case 400:
|
||||
return ApiErrorCode.BAD_REQUEST;
|
||||
case 401:
|
||||
return ApiErrorCode.UNAUTHORIZED;
|
||||
case 403:
|
||||
return ApiErrorCode.FORBIDDEN;
|
||||
case 404:
|
||||
return ApiErrorCode.NOT_FOUND;
|
||||
case 409:
|
||||
return ApiErrorCode.CONFLICT;
|
||||
case 422:
|
||||
return ApiErrorCode.UNPROCESSABLE_ENTITY;
|
||||
case 429:
|
||||
return ApiErrorCode.TOO_MANY_REQUESTS;
|
||||
case 500:
|
||||
return ApiErrorCode.INTERNAL_SERVER_ERROR;
|
||||
case 503:
|
||||
return ApiErrorCode.SERVICE_UNAVAILABLE;
|
||||
default:
|
||||
return ApiErrorCode.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,7 @@ import { ApiError } from '../errors/api-error';
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createHttpClient(
|
||||
config: HttpClientConfig,
|
||||
hooks?: InterceptorHooks,
|
||||
): AxiosInstance {
|
||||
export function createHttpClient(config: HttpClientConfig, hooks?: InterceptorHooks): AxiosInstance {
|
||||
const observability = config.observability ?? noopObservabilityAdapter;
|
||||
|
||||
// ── Create isolated instance ──────────────────────────────────
|
||||
@@ -49,7 +46,7 @@ export function createHttpClient(
|
||||
timeout: config.timeout ?? 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...(config.defaultHeaders ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
AxiosError,
|
||||
AxiosResponse,
|
||||
InternalAxiosRequestConfig,
|
||||
} from 'axios';
|
||||
import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import type { IObservabilityAdapter } from '../observability/types';
|
||||
|
||||
// ─── Factory Configuration ──────────────────────────────────────
|
||||
@@ -41,9 +37,7 @@ export interface InterceptorHooks {
|
||||
* Called before every request is dispatched.
|
||||
* Use this to inject authentication tokens, tenant headers, etc.
|
||||
*/
|
||||
onRequest?: (
|
||||
config: InternalAxiosRequestConfig,
|
||||
) => Promise<InternalAxiosRequestConfig> | InternalAxiosRequestConfig;
|
||||
onRequest?: (config: InternalAxiosRequestConfig) => Promise<InternalAxiosRequestConfig> | InternalAxiosRequestConfig;
|
||||
|
||||
/**
|
||||
* Called on every successful response (2xx status).
|
||||
@@ -105,13 +99,7 @@ export interface TelemetryContext {
|
||||
|
||||
// ─── Re-export Axios types consumers frequently need ────────────
|
||||
|
||||
export type {
|
||||
AxiosInstance,
|
||||
AxiosError,
|
||||
AxiosResponse,
|
||||
AxiosRequestConfig,
|
||||
InternalAxiosRequestConfig,
|
||||
} from 'axios';
|
||||
export type { AxiosInstance, AxiosError, AxiosResponse, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
// ─── Augment Axios to carry TelemetryContext ────────────────────
|
||||
|
||||
@@ -119,5 +107,7 @@ declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
/** Per-request telemetry context for custom spans, tags, events. */
|
||||
telemetryContext?: TelemetryContext;
|
||||
/** Skip the 401 refresh-and-retry interceptor for this request. */
|
||||
skipAuthRefresh?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +70,7 @@ function makeRequestConfig(
|
||||
} as InternalAxiosRequestConfig;
|
||||
}
|
||||
|
||||
function makeAxiosResponse(
|
||||
config: InternalAxiosRequestConfig,
|
||||
overrides: Partial<AxiosResponse> = {},
|
||||
): AxiosResponse {
|
||||
function makeAxiosResponse(config: InternalAxiosRequestConfig, overrides: Partial<AxiosResponse> = {}): AxiosResponse {
|
||||
return {
|
||||
data: {},
|
||||
status: 200,
|
||||
|
||||
@@ -43,9 +43,7 @@ function getTelemetryContext(config: unknown): TelemetryContext | undefined {
|
||||
/** Convert TelemetryContext tags to a string record for Faro context. */
|
||||
function tagsToFaroContext(tags?: Record<string, string | number | boolean>): Record<string, string> {
|
||||
if (!tags) return {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(tags).map(([k, v]) => [k, String(v)]),
|
||||
);
|
||||
return Object.fromEntries(Object.entries(tags).map(([k, v]) => [k, String(v)]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,10 +133,7 @@ export const faroAdapter: IObservabilityAdapter = {
|
||||
const faro = getFaro();
|
||||
if (faro) {
|
||||
const baseContext = buildBaseContext(method, url, moduleKey, action, ctx?.tags);
|
||||
faro.api.pushLog(
|
||||
[`[core-api] ${method} ${url}`],
|
||||
{ level: LogLevel.DEBUG, context: baseContext },
|
||||
);
|
||||
faro.api.pushLog([`[core-api] ${method} ${url}`], { level: LogLevel.DEBUG, context: baseContext });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -203,10 +198,10 @@ export const faroAdapter: IObservabilityAdapter = {
|
||||
context: errorContext,
|
||||
});
|
||||
|
||||
faro.api.pushLog(
|
||||
[`[core-api] ERROR ${method} ${url} → ${status}`],
|
||||
{ level: LogLevel.ERROR, context: errorContext },
|
||||
);
|
||||
faro.api.pushLog([`[core-api] ERROR ${method} ${url} → ${status}`], {
|
||||
level: LogLevel.ERROR,
|
||||
context: errorContext,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,11 +17,7 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import {
|
||||
getWebInstrumentations,
|
||||
initializeFaro,
|
||||
type Faro,
|
||||
} from '@grafana/faro-react';
|
||||
import { getWebInstrumentations, initializeFaro, type Faro } from '@grafana/faro-react';
|
||||
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
||||
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-web';
|
||||
@@ -125,8 +121,7 @@ export function initTelemetry(config: TelemetryConfig): Faro {
|
||||
new TracingInstrumentation({
|
||||
...tracingOptions,
|
||||
instrumentationOptions: {
|
||||
propagateTraceHeaderCorsUrls:
|
||||
config.propagateTraceHeaderCorsUrls ?? [/.*/],
|
||||
propagateTraceHeaderCorsUrls: config.propagateTraceHeaderCorsUrls ?? [/.*/],
|
||||
fetchInstrumentationOptions: {
|
||||
applyCustomAttributesOnSpan(span) {
|
||||
span.setAttribute('app.synthetic_request', 'false');
|
||||
|
||||
@@ -242,8 +242,7 @@ describe('useAppEvent (React Hook)', () => {
|
||||
const offSpy = vi.spyOn(eventBus, 'off');
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ handler }: { handler: () => void }) =>
|
||||
useAppEvent('TEST:INITIALIZED', handler),
|
||||
({ handler }: { handler: () => void }) => useAppEvent('TEST:INITIALIZED', handler),
|
||||
{ initialProps: { handler: vi.fn() } },
|
||||
);
|
||||
|
||||
|
||||
@@ -31,10 +31,7 @@ export const eventBus = mitt<AppEvents>();
|
||||
* publish('AUTH:PROFILE_UPDATED', { id: '1', name: 'Firman', ... });
|
||||
* ```
|
||||
*/
|
||||
export function publish<K extends keyof AppEvents>(
|
||||
type: K,
|
||||
event: AppEvents[K],
|
||||
): void {
|
||||
export function publish<K extends keyof AppEvents>(type: K, event: AppEvents[K]): void {
|
||||
eventBus.emit(type, event);
|
||||
}
|
||||
|
||||
@@ -58,10 +55,7 @@ export function publish<K extends keyof AppEvents>(
|
||||
* unsub();
|
||||
* ```
|
||||
*/
|
||||
export function subscribe<K extends keyof AppEvents>(
|
||||
type: K,
|
||||
handler: (event: AppEvents[K]) => void,
|
||||
): () => void {
|
||||
export function subscribe<K extends keyof AppEvents>(type: K, handler: (event: AppEvents[K]) => void): () => void {
|
||||
eventBus.on(type, handler);
|
||||
return () => eventBus.off(type, handler);
|
||||
}
|
||||
|
||||
@@ -22,10 +22,7 @@ import { eventBus, publish as busPublish } from './event-bus';
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useAppEvent<K extends keyof AppEvents>(
|
||||
type: K,
|
||||
handler: (event: AppEvents[K]) => void,
|
||||
): void {
|
||||
export function useAppEvent<K extends keyof AppEvents>(type: K, handler: (event: AppEvents[K]) => void): void {
|
||||
// Always keep the latest handler in a ref to avoid stale closures
|
||||
// and prevent re-subscription on every render.
|
||||
const handlerRef = useRef(handler);
|
||||
|
||||
+1
-1
@@ -4,6 +4,6 @@ import type { resources } from './setup';
|
||||
declare module 'react-i18next' {
|
||||
interface CustomTypeOptions {
|
||||
defaultNS: 'common';
|
||||
resources: typeof resources['en'];
|
||||
resources: (typeof resources)['en'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ export class IndexedDBService<TKey extends string> implements IStorageService<TK
|
||||
|
||||
for (const rawKey of rawKeys) {
|
||||
const isGlobalKey = this.encryptedKeys.has(rawKey as TKey) || this.plainTextKeys.has(rawKey as TKey);
|
||||
|
||||
|
||||
if (isGlobalKey && !this.personalizedKeys?.has(rawKey as TKey)) {
|
||||
resultSet.add(rawKey as TKey);
|
||||
}
|
||||
|
||||
@@ -35,9 +35,7 @@ function createTestDB(name: string) {
|
||||
const results: (PouchDB.Core.Response | PouchDB.Core.Error)[] = [];
|
||||
for (let i = 0; i < dataList.length; i += batchSize) {
|
||||
const batch = dataList.slice(i, i + batchSize);
|
||||
const response = await raw.bulkDocs(
|
||||
batch as PouchDB.Core.Document<T>[],
|
||||
);
|
||||
const response = await raw.bulkDocs(batch as PouchDB.Core.Document<T>[]);
|
||||
results.push(...response);
|
||||
}
|
||||
return results;
|
||||
@@ -65,16 +63,12 @@ function createTestDB(name: string) {
|
||||
// ─── Reads ────────────────────────────────────────────────
|
||||
|
||||
async getOne<T>(id: string) {
|
||||
return raw.get<T>(id) as Promise<
|
||||
T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta
|
||||
>;
|
||||
return raw.get<T>(id) as Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta>;
|
||||
},
|
||||
|
||||
async getAll<T>() {
|
||||
const result = await raw.allDocs({ include_docs: true });
|
||||
return result.rows
|
||||
.filter((row) => !row.id.startsWith('_design/'))
|
||||
.map((row) => row.doc as unknown as T);
|
||||
return result.rows.filter((row) => !row.id.startsWith('_design/')).map((row) => row.doc as unknown as T);
|
||||
},
|
||||
|
||||
async getSome<T>(ids: string[]) {
|
||||
@@ -85,9 +79,7 @@ function createTestDB(name: string) {
|
||||
},
|
||||
|
||||
async find<T extends object>(options: PouchDB.Find.FindRequest<T>) {
|
||||
const result = await raw.find(
|
||||
options as PouchDB.Find.FindRequest<object>,
|
||||
);
|
||||
const result = await raw.find(options as PouchDB.Find.FindRequest<object>);
|
||||
return result.docs as unknown as T[];
|
||||
},
|
||||
|
||||
@@ -146,9 +138,7 @@ describe('PouchBase — Core CRUD Operations', () => {
|
||||
let db: ReturnType<typeof createTestDB>;
|
||||
|
||||
beforeEach(() => {
|
||||
db = createTestDB(
|
||||
`test_base_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
||||
);
|
||||
db = createTestDB(`test_base_${Date.now()}_${Math.random().toString(36).slice(2)}`);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -170,9 +160,7 @@ describe('PouchBase — Core CRUD Operations', () => {
|
||||
|
||||
it('should throw a conflict if creating with a duplicate _id', async () => {
|
||||
await db.create({ _id: 'dup-001', name: 'First' });
|
||||
await expect(
|
||||
db.create({ _id: 'dup-001', name: 'Second' }),
|
||||
).rejects.toThrow();
|
||||
await expect(db.create({ _id: 'dup-001', name: 'Second' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,9 +200,7 @@ describe('PouchBase — Core CRUD Operations', () => {
|
||||
it('should retrieve a document by id', async () => {
|
||||
await db.create({ _id: 'fetch-001', product: 'Widget', price: 9.99 });
|
||||
|
||||
const doc = await db.getOne<{ product: string; price: number }>(
|
||||
'fetch-001',
|
||||
);
|
||||
const doc = await db.getOne<{ product: string; price: number }>('fetch-001');
|
||||
expect(doc._id).toBe('fetch-001');
|
||||
expect(doc.product).toBe('Widget');
|
||||
expect(doc.price).toBe(9.99);
|
||||
|
||||
@@ -87,7 +87,11 @@ describe('FieldAsyncSelect', () => {
|
||||
const { control, handleSubmit } = useForm({ defaultValues: { vendor: null } });
|
||||
return (
|
||||
<MantineProvider>
|
||||
<form onSubmit={handleSubmit((data) => { capturedData = data; })}>
|
||||
<form
|
||||
onSubmit={handleSubmit((data) => {
|
||||
capturedData = data;
|
||||
})}
|
||||
>
|
||||
<FieldAsyncSelect
|
||||
name="vendor"
|
||||
control={control}
|
||||
@@ -103,7 +107,7 @@ describe('FieldAsyncSelect', () => {
|
||||
}
|
||||
|
||||
render(<TestForm />);
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockLoadOptions).toHaveBeenCalled();
|
||||
});
|
||||
@@ -157,7 +161,7 @@ describe('FieldAsyncSelect', () => {
|
||||
it('debounces search input and refetches', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockLoadOptions = createMockLoadOptions();
|
||||
|
||||
|
||||
function TestForm() {
|
||||
const { control } = useForm({ defaultValues: { vendor: null } });
|
||||
return (
|
||||
@@ -232,7 +236,7 @@ describe('FieldAsyncSelect', () => {
|
||||
// Should only render "Vendor 1" once, ignoring the duplicate with id=1
|
||||
const vendor1Options = screen.getAllByText('Vendor 1');
|
||||
expect(vendor1Options.length).toBe(1);
|
||||
|
||||
|
||||
// The duplicate name should NOT be rendered
|
||||
expect(screen.queryByText('Vendor 1 (Duplicate)')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -49,11 +49,7 @@ describe('FieldCheckbox', () => {
|
||||
const { control } = useForm({ defaultValues: { acceptTerms: false } });
|
||||
return (
|
||||
<MantineProvider>
|
||||
<FieldCheckbox
|
||||
name="acceptTerms"
|
||||
control={control}
|
||||
label="I accept the terms and conditions"
|
||||
/>
|
||||
<FieldCheckbox name="acceptTerms" control={control} label="I accept the terms and conditions" />
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
@@ -69,11 +65,7 @@ describe('FieldCheckbox', () => {
|
||||
const { control } = useForm({ defaultValues: { acceptTerms: false } });
|
||||
return (
|
||||
<MantineProvider>
|
||||
<FieldCheckbox
|
||||
name="acceptTerms"
|
||||
control={control}
|
||||
label="Accept Terms"
|
||||
/>
|
||||
<FieldCheckbox name="acceptTerms" control={control} label="Accept Terms" />
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
@@ -114,10 +106,7 @@ describe('FieldCheckbox', () => {
|
||||
await user.click(screen.getByText('Submit'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
{ acceptTerms: true },
|
||||
expect.anything(),
|
||||
);
|
||||
expect(onSubmit).toHaveBeenCalledWith({ acceptTerms: true }, expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -70,7 +70,11 @@ describe('FieldLocalSelect', () => {
|
||||
const { control, handleSubmit } = useForm({ defaultValues: { vendor: null } });
|
||||
return (
|
||||
<MantineProvider>
|
||||
<form onSubmit={handleSubmit((data) => { capturedData = data; })}>
|
||||
<form
|
||||
onSubmit={handleSubmit((data) => {
|
||||
capturedData = data;
|
||||
})}
|
||||
>
|
||||
<FieldLocalSelect
|
||||
name="vendor"
|
||||
control={control}
|
||||
@@ -86,13 +90,13 @@ describe('FieldLocalSelect', () => {
|
||||
}
|
||||
|
||||
render(<TestForm />);
|
||||
|
||||
|
||||
// Open dropdown
|
||||
await user.click(screen.getByPlaceholderText('Select vendor'));
|
||||
|
||||
|
||||
// Click Vendor 2
|
||||
await user.click(screen.getByText('Vendor 2'));
|
||||
|
||||
|
||||
// Submit
|
||||
await user.click(screen.getByText('Submit'));
|
||||
|
||||
@@ -102,7 +106,7 @@ describe('FieldLocalSelect', () => {
|
||||
|
||||
it('renders with renderLabel for compound labels', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
function TestForm() {
|
||||
const { control } = useForm({ defaultValues: { vendor: null } });
|
||||
return (
|
||||
@@ -121,16 +125,14 @@ describe('FieldLocalSelect', () => {
|
||||
|
||||
render(<TestForm />);
|
||||
await user.click(screen.getByPlaceholderText('Select vendor'));
|
||||
|
||||
|
||||
// Compound label should be visible
|
||||
expect(screen.getByText('V1 - Vendor 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('filterOption excludes items from dropdown', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
function TestForm() {
|
||||
const { control } = useForm({ defaultValues: { vendor: null } });
|
||||
return (
|
||||
@@ -150,7 +152,7 @@ describe('FieldLocalSelect', () => {
|
||||
|
||||
render(<TestForm />);
|
||||
await user.click(screen.getByPlaceholderText('Select vendor'));
|
||||
|
||||
|
||||
// Active vendors should be present
|
||||
expect(screen.getByText('Vendor 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Vendor 3')).toBeInTheDocument();
|
||||
@@ -166,7 +168,11 @@ describe('FieldLocalSelect', () => {
|
||||
const { control, handleSubmit } = useForm({ defaultValues: { vendors: [] } });
|
||||
return (
|
||||
<MantineProvider>
|
||||
<form onSubmit={handleSubmit((data) => { capturedData = data; })}>
|
||||
<form
|
||||
onSubmit={handleSubmit((data) => {
|
||||
capturedData = data;
|
||||
})}
|
||||
>
|
||||
<FieldLocalSelect
|
||||
multiple
|
||||
name="vendors"
|
||||
@@ -186,7 +192,7 @@ describe('FieldLocalSelect', () => {
|
||||
await user.click(screen.getByPlaceholderText('Select vendors'));
|
||||
await user.click(screen.getByText('Vendor 1'));
|
||||
await user.click(screen.getByText('Vendor 3'));
|
||||
|
||||
|
||||
await user.click(screen.getByText('Submit'));
|
||||
expect(capturedData).toEqual({ vendors: [VENDORS[0], VENDORS[2]] });
|
||||
});
|
||||
@@ -199,7 +205,11 @@ describe('FieldLocalSelect', () => {
|
||||
const { control, handleSubmit } = useForm({ defaultValues: { vendors: [VENDORS[0]] } });
|
||||
return (
|
||||
<MantineProvider>
|
||||
<form onSubmit={handleSubmit((data) => { capturedData = data; })}>
|
||||
<form
|
||||
onSubmit={handleSubmit((data) => {
|
||||
capturedData = data;
|
||||
})}
|
||||
>
|
||||
<FieldLocalSelect
|
||||
multiple
|
||||
name="vendors"
|
||||
@@ -216,8 +226,10 @@ describe('FieldLocalSelect', () => {
|
||||
}
|
||||
|
||||
const { container } = render(<TestForm />);
|
||||
|
||||
const clearButton = container.querySelector('.mantine-CloseButton-root') || container.querySelector('button[aria-label="Clear value"]');
|
||||
|
||||
const clearButton =
|
||||
container.querySelector('.mantine-CloseButton-root') ||
|
||||
container.querySelector('button[aria-label="Clear value"]');
|
||||
expect(clearButton).not.toBeNull();
|
||||
await user.click(clearButton!);
|
||||
|
||||
|
||||
@@ -32,10 +32,7 @@ vi.mock('@repo/core-i18n', () => ({
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(1, 'Username cannot be empty')
|
||||
.min(3, 'Username must be at least 3 characters'),
|
||||
username: z.string().min(1, 'Username cannot be empty').min(3, 'Username must be at least 3 characters'),
|
||||
email: z.string().email('Please enter a valid email address'),
|
||||
});
|
||||
|
||||
@@ -57,12 +54,7 @@ describe('FieldTextInput', () => {
|
||||
});
|
||||
return (
|
||||
<MantineProvider>
|
||||
<FieldTextInput
|
||||
name="username"
|
||||
control={control}
|
||||
label="Username"
|
||||
placeholder="Enter username"
|
||||
/>
|
||||
<FieldTextInput name="username" control={control} label="Username" placeholder="Enter username" />
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
@@ -143,10 +135,7 @@ describe('FieldTextInput', () => {
|
||||
await user.click(screen.getByText('Login'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
{ username: 'john', email: 'john@example.com' },
|
||||
expect.anything(),
|
||||
);
|
||||
expect(onSubmit).toHaveBeenCalledWith({ username: 'john', email: 'john@example.com' }, expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -177,10 +166,7 @@ describe('FieldTextInput', () => {
|
||||
await user.click(screen.getByText('Login'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
{ username: 'johndoe', email: 'john@example.com' },
|
||||
expect.anything(),
|
||||
);
|
||||
expect(onSubmit).toHaveBeenCalledWith({ username: 'johndoe', email: 'john@example.com' }, expect.anything());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,11 +41,7 @@ interface FormTestWrapperProps {
|
||||
onSubmit?: (data: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
function FormTestWrapper({
|
||||
children,
|
||||
defaultValues = {},
|
||||
onSubmit = () => {},
|
||||
}: FormTestWrapperProps) {
|
||||
function FormTestWrapper({ children, defaultValues = {}, onSubmit = () => {} }: FormTestWrapperProps) {
|
||||
const methods = useForm({ defaultValues });
|
||||
|
||||
return (
|
||||
@@ -64,10 +60,7 @@ function FormTestWrapper({
|
||||
// Create a test field component using the HOC
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TestFieldTextInput = withRHF<React.ComponentProps<typeof TextInput>>(
|
||||
'TestFieldTextInput',
|
||||
TextInput,
|
||||
);
|
||||
const TestFieldTextInput = withRHF<React.ComponentProps<typeof TextInput>>('TestFieldTextInput', TextInput);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
@@ -114,7 +107,11 @@ describe('withRHF HOC', () => {
|
||||
const { control, handleSubmit } = useForm({ defaultValues: { email: '' } });
|
||||
return (
|
||||
<MantineProvider>
|
||||
<form onSubmit={handleSubmit((data) => { capturedData = data; })}>
|
||||
<form
|
||||
onSubmit={handleSubmit((data) => {
|
||||
capturedData = data;
|
||||
})}
|
||||
>
|
||||
<TestFieldTextInput name="email" control={control} label="Email" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
@@ -215,16 +212,12 @@ describe('withRHF HOC', () => {
|
||||
|
||||
// The fallback should be the raw JSON string since neither namespace has the key.
|
||||
// Our mock t() returns defaultValue when key is unknown, which is the raw JSON.
|
||||
const errorElements = screen.getAllByText((content) =>
|
||||
content.includes('validation.unknown_key'),
|
||||
);
|
||||
const errorElements = screen.getAllByText((content) => content.includes('validation.unknown_key'));
|
||||
expect(errorElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('has the correct displayName for React DevTools', () => {
|
||||
expect(
|
||||
(TestFieldTextInput as unknown as { displayName: string }).displayName,
|
||||
).toBe('TestFieldTextInput');
|
||||
expect((TestFieldTextInput as unknown as { displayName: string }).displayName).toBe('TestFieldTextInput');
|
||||
});
|
||||
|
||||
it('forwards additional Mantine props (placeholder, etc.)', () => {
|
||||
@@ -232,12 +225,7 @@ describe('withRHF HOC', () => {
|
||||
const { control } = useForm({ defaultValues: { search: '' } });
|
||||
return (
|
||||
<MantineProvider>
|
||||
<TestFieldTextInput
|
||||
name="search"
|
||||
control={control}
|
||||
label="Search"
|
||||
placeholder="Type to search..."
|
||||
/>
|
||||
<TestFieldTextInput name="search" control={control} label="Search" placeholder="Type to search..." />
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,9 +210,7 @@ function AsyncSelectInner<T extends Record<string, any>>(props: AsyncSelectProps
|
||||
|
||||
// Disable Mantine's internal frontend filtering.
|
||||
// The backend handles the search query, so we always display what the backend returns.
|
||||
const mantineFilter = filterOption
|
||||
? ({ options: opts }: any) => opts
|
||||
: undefined;
|
||||
const mantineFilter = filterOption ? ({ options: opts }: any) => opts : undefined;
|
||||
|
||||
// ----- Multi-select mode -----
|
||||
if (multiple) {
|
||||
|
||||
@@ -25,8 +25,8 @@ type ManagedSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'filt
|
||||
type ManagedMultiSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'filter' | 'name' | 'onSelect';
|
||||
|
||||
/** Props for single-select mode */
|
||||
export type LocalSelectSingleProps<T extends Record<string, any>> =
|
||||
LocalSelectBaseProps<T> & Omit<SelectProps, ManagedSelectProps> & {
|
||||
export type LocalSelectSingleProps<T extends Record<string, any>> = LocalSelectBaseProps<T> &
|
||||
Omit<SelectProps, ManagedSelectProps> & {
|
||||
multiple?: false;
|
||||
/** Controlled value — the full object or null */
|
||||
value?: T | null;
|
||||
@@ -35,8 +35,8 @@ export type LocalSelectSingleProps<T extends Record<string, any>> =
|
||||
};
|
||||
|
||||
/** Props for multi-select mode */
|
||||
export type LocalSelectMultiProps<T extends Record<string, any>> =
|
||||
LocalSelectBaseProps<T> & Omit<MultiSelectProps, ManagedMultiSelectProps> & {
|
||||
export type LocalSelectMultiProps<T extends Record<string, any>> = LocalSelectBaseProps<T> &
|
||||
Omit<MultiSelectProps, ManagedMultiSelectProps> & {
|
||||
multiple: true;
|
||||
/** Controlled value — array of full objects */
|
||||
value?: T[];
|
||||
@@ -45,9 +45,7 @@ export type LocalSelectMultiProps<T extends Record<string, any>> =
|
||||
};
|
||||
|
||||
/** Discriminated union — the component narrows based on `multiple` */
|
||||
export type LocalSelectProps<T extends Record<string, any>> =
|
||||
| LocalSelectSingleProps<T>
|
||||
| LocalSelectMultiProps<T>;
|
||||
export type LocalSelectProps<T extends Record<string, any>> = LocalSelectSingleProps<T> | LocalSelectMultiProps<T>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: Resolve label for a data item
|
||||
@@ -70,9 +68,7 @@ function resolveLabel<T extends Record<string, any>>(
|
||||
// Component Implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function LocalSelectInner<T extends Record<string, any>>(
|
||||
props: LocalSelectProps<T>,
|
||||
) {
|
||||
function LocalSelectInner<T extends Record<string, any>>(props: LocalSelectProps<T>) {
|
||||
const {
|
||||
options,
|
||||
valueKey,
|
||||
@@ -132,10 +128,7 @@ function LocalSelectInner<T extends Record<string, any>>(
|
||||
|
||||
// Passthrough filter — we handle filtering ourselves via filterOption in useMemo.
|
||||
// This prevents Mantine from double-filtering.
|
||||
const mantineFilter = filterOption
|
||||
? ({ options: opts }: { options: ComboboxItem[] }) => opts
|
||||
: undefined;
|
||||
|
||||
const mantineFilter = filterOption ? ({ options: opts }: { options: ComboboxItem[] }) => opts : undefined;
|
||||
|
||||
// ----- Multi-select mode -----
|
||||
if (multiple) {
|
||||
@@ -166,7 +159,7 @@ function LocalSelectInner<T extends Record<string, any>>(
|
||||
const currentValue = value ? String((value as T)[valueKey]) : null;
|
||||
|
||||
const handleSingleChange = (val: string | null) => {
|
||||
const obj = val ? lookupMap.get(val) ?? null : null;
|
||||
const obj = val ? (lookupMap.get(val) ?? null) : null;
|
||||
(onChange as ((v: T | null) => void) | undefined)?.(obj);
|
||||
onSelectCallback?.(obj);
|
||||
};
|
||||
|
||||
@@ -117,11 +117,7 @@ export interface LoadOptionsResponse<T> {
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export type LoadOptionsFn<T> = (
|
||||
search: string,
|
||||
page: number,
|
||||
prevOptions: T[],
|
||||
) => Promise<LoadOptionsResponse<T>>;
|
||||
export type LoadOptionsFn<T> = (search: string, page: number, prevOptions: T[]) => Promise<LoadOptionsResponse<T>>;
|
||||
|
||||
/**
|
||||
* Internal cache entry for the search-keyed options cache.
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
useController,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
type UseControllerProps,
|
||||
} from 'react-hook-form';
|
||||
import { useController, type FieldPath, type FieldValues, type UseControllerProps } from 'react-hook-form';
|
||||
import type { SelectProps, MultiSelectProps } from '@mantine/core';
|
||||
import { AsyncSelect } from '../custom/selects/AsyncSelect';
|
||||
import type { AsyncSelectBaseProps, LoadOptionsFn } from '../custom/selects/types';
|
||||
@@ -26,8 +21,26 @@ import { useTranslatedError } from '../useTranslatedError';
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Mantine props we manage ourselves */
|
||||
type ManagedSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'onBlur' | 'error' | 'filter' | 'name' | 'onSelect';
|
||||
type ManagedMultiSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'onBlur' | 'error' | 'filter' | 'name' | 'onSelect';
|
||||
type ManagedSelectProps =
|
||||
| 'data'
|
||||
| 'value'
|
||||
| 'defaultValue'
|
||||
| 'onChange'
|
||||
| 'onBlur'
|
||||
| 'error'
|
||||
| 'filter'
|
||||
| 'name'
|
||||
| 'onSelect';
|
||||
type ManagedMultiSelectProps =
|
||||
| 'data'
|
||||
| 'value'
|
||||
| 'defaultValue'
|
||||
| 'onChange'
|
||||
| 'onBlur'
|
||||
| 'error'
|
||||
| 'filter'
|
||||
| 'name'
|
||||
| 'onSelect';
|
||||
|
||||
/** Async-specific props (IoC pattern) */
|
||||
interface AsyncExtraProps<T> {
|
||||
@@ -69,9 +82,7 @@ export type FieldAsyncSelectProps<
|
||||
T extends Record<string, any>,
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> =
|
||||
| FieldAsyncSelectSingleProps<T, TFieldValues, TName>
|
||||
| FieldAsyncSelectMultiProps<T, TFieldValues, TName>;
|
||||
> = FieldAsyncSelectSingleProps<T, TFieldValues, TName> | FieldAsyncSelectMultiProps<T, TFieldValues, TName>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component Implementation
|
||||
@@ -149,12 +160,7 @@ function FieldAsyncSelectInner<
|
||||
}
|
||||
|
||||
return (
|
||||
<AsyncSelect<T>
|
||||
{...engineProps}
|
||||
value={field.value ?? null}
|
||||
onChange={handleChange}
|
||||
{...(mantineProps as any)}
|
||||
/>
|
||||
<AsyncSelect<T> {...engineProps} value={field.value ?? null} onChange={handleChange} {...(mantineProps as any)} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
useController,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
type UseControllerProps,
|
||||
} from 'react-hook-form';
|
||||
import { useController, type FieldPath, type FieldValues, type UseControllerProps } from 'react-hook-form';
|
||||
import type { SelectProps, MultiSelectProps } from '@mantine/core';
|
||||
import { LocalSelect } from '../custom/selects/LocalSelect';
|
||||
import type { LocalSelectBaseProps } from '../custom/selects/types';
|
||||
@@ -28,8 +23,26 @@ import { useTranslatedError } from '../useTranslatedError';
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Mantine props we manage ourselves */
|
||||
type ManagedSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'onBlur' | 'error' | 'filter' | 'name' | 'onSelect';
|
||||
type ManagedMultiSelectProps = 'data' | 'value' | 'defaultValue' | 'onChange' | 'onBlur' | 'error' | 'filter' | 'name' | 'onSelect';
|
||||
type ManagedSelectProps =
|
||||
| 'data'
|
||||
| 'value'
|
||||
| 'defaultValue'
|
||||
| 'onChange'
|
||||
| 'onBlur'
|
||||
| 'error'
|
||||
| 'filter'
|
||||
| 'name'
|
||||
| 'onSelect';
|
||||
type ManagedMultiSelectProps =
|
||||
| 'data'
|
||||
| 'value'
|
||||
| 'defaultValue'
|
||||
| 'onChange'
|
||||
| 'onBlur'
|
||||
| 'error'
|
||||
| 'filter'
|
||||
| 'name'
|
||||
| 'onSelect';
|
||||
|
||||
/** Single-select RHF props — stores T | null */
|
||||
export type FieldLocalSelectSingleProps<
|
||||
@@ -58,9 +71,7 @@ export type FieldLocalSelectProps<
|
||||
T extends Record<string, any>,
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> =
|
||||
| FieldLocalSelectSingleProps<T, TFieldValues, TName>
|
||||
| FieldLocalSelectMultiProps<T, TFieldValues, TName>;
|
||||
> = FieldLocalSelectSingleProps<T, TFieldValues, TName> | FieldLocalSelectMultiProps<T, TFieldValues, TName>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component Implementation
|
||||
|
||||
@@ -22,17 +22,7 @@ function FieldRichTextEditorComponent<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>(props: FieldRichTextEditorProps<TFieldValues, TName>) {
|
||||
const {
|
||||
name,
|
||||
control,
|
||||
rules,
|
||||
shouldUnregister,
|
||||
defaultValue,
|
||||
disabled,
|
||||
label,
|
||||
description,
|
||||
withAsterisk,
|
||||
} = props;
|
||||
const { name, control, rules, shouldUnregister, defaultValue, disabled, label, description, withAsterisk } = props;
|
||||
|
||||
const {
|
||||
field,
|
||||
@@ -71,12 +61,7 @@ function FieldRichTextEditorComponent<
|
||||
}, [field.value, editor]);
|
||||
|
||||
return (
|
||||
<Input.Wrapper
|
||||
label={label}
|
||||
description={description}
|
||||
withAsterisk={withAsterisk}
|
||||
error={translatedError}
|
||||
>
|
||||
<Input.Wrapper label={label} description={description} withAsterisk={withAsterisk} error={translatedError}>
|
||||
<RichTextEditor editor={editor}>
|
||||
<RichTextEditor.Toolbar sticky stickyOffset={60}>
|
||||
<RichTextEditor.ControlsGroup>
|
||||
|
||||
@@ -9,8 +9,6 @@ export interface FieldSegmentedControlProps extends SegmentedControlProps {
|
||||
|
||||
// SegmentedControl does NOT have a native `error` prop.
|
||||
// The HOC wraps it in Input.Wrapper to display validation errors.
|
||||
export const FieldSegmentedControl = withRHF<FieldSegmentedControlProps>(
|
||||
'FieldSegmentedControl',
|
||||
SegmentedControl,
|
||||
{ requiresWrapper: true },
|
||||
);
|
||||
export const FieldSegmentedControl = withRHF<FieldSegmentedControlProps>('FieldSegmentedControl', SegmentedControl, {
|
||||
requiresWrapper: true,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type {
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
UseControllerProps,
|
||||
} from 'react-hook-form';
|
||||
import type { FieldPath, FieldValues, UseControllerProps } from 'react-hook-form';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zod i18n JSON payload shape
|
||||
@@ -42,8 +38,7 @@ export type WithRHFProps<
|
||||
TComponentProps,
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = Omit<TComponentProps, ManagedProps> &
|
||||
UseControllerProps<TFieldValues, TName>;
|
||||
> = Omit<TComponentProps, ManagedProps> & UseControllerProps<TFieldValues, TName>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Value transform — for components with non-standard value semantics
|
||||
@@ -84,8 +79,4 @@ export interface WithRHFOptions {
|
||||
// Utility: Extract the component's ref type for forwardRef
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type ExtractRef<T> = T extends ComponentType<infer P>
|
||||
? P extends { ref?: infer R }
|
||||
? R
|
||||
: never
|
||||
: never;
|
||||
export type ExtractRef<T> = T extends ComponentType<infer P> ? (P extends { ref?: infer R } ? R : never) : never;
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import React, { type ComponentType, type Ref } from 'react';
|
||||
import {
|
||||
useController,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
type UseControllerProps,
|
||||
} from 'react-hook-form';
|
||||
import { useController, type FieldPath, type FieldValues, type UseControllerProps } from 'react-hook-form';
|
||||
import { Input } from '@mantine/core';
|
||||
import type { WithRHFOptions } from './types';
|
||||
import { useTranslatedError } from './useTranslatedError';
|
||||
@@ -73,16 +68,7 @@ export function withRHF<TComponentProps extends Record<string, any>>(
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>(props: Props<TFieldValues, TName>) {
|
||||
const {
|
||||
name,
|
||||
control,
|
||||
rules,
|
||||
shouldUnregister,
|
||||
defaultValue,
|
||||
disabled,
|
||||
ref,
|
||||
...mantineProps
|
||||
} = props;
|
||||
const { name, control, rules, shouldUnregister, defaultValue, disabled, ref, ...mantineProps } = props;
|
||||
|
||||
const {
|
||||
field,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './page-actions';
|
||||
export * from './row-actions';
|
||||
export * from './row-actions';
|
||||
|
||||
@@ -6,16 +6,16 @@ export interface StatusPageProps {
|
||||
heading?: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
|
||||
|
||||
showActionsBack?: boolean;
|
||||
backButtonLabel?: string;
|
||||
onClickGoBack?(): void;
|
||||
|
||||
|
||||
showActionsHome?: boolean;
|
||||
homeButtonLabel?: string;
|
||||
onClickBackToHome?(): void;
|
||||
homeUrl?: string;
|
||||
|
||||
|
||||
height?: StyleProp<React.CSSProperties['height']>;
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -94,10 +94,13 @@ export function TableFilterDrawer({
|
||||
setPreviousValues(form.getValues());
|
||||
|
||||
// Ensure all fields are explicitly cleared
|
||||
const cleared = Object.keys(form.getValues()).reduce((acc, key) => {
|
||||
acc[key] = '';
|
||||
return acc;
|
||||
}, {} as Record<string, unknown>);
|
||||
const cleared = Object.keys(form.getValues()).reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = '';
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, unknown>,
|
||||
);
|
||||
|
||||
form.reset({ ...cleared, ...(config?.defaultValues || {}) });
|
||||
setHasReset(true);
|
||||
|
||||
@@ -12,28 +12,20 @@ export interface UseConditionalFieldOptions<TFieldValues extends FieldValues> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically cleans up a conditionally rendered React Hook Form field
|
||||
* Automatically cleans up a conditionally rendered React Hook Form field
|
||||
* when its parent condition becomes false.
|
||||
*
|
||||
*
|
||||
* @param options Configuration object for the conditional field behavior
|
||||
*/
|
||||
export function useConditionalField<TFieldValues extends FieldValues>(
|
||||
options: UseConditionalFieldOptions<TFieldValues>
|
||||
options: UseConditionalFieldOptions<TFieldValues>,
|
||||
) {
|
||||
// Destructure with default values
|
||||
const {
|
||||
condition,
|
||||
name,
|
||||
setValue,
|
||||
unregister,
|
||||
clearErrors,
|
||||
defaultValue,
|
||||
mode = 'unregister'
|
||||
} = options;
|
||||
const { condition, name, setValue, unregister, clearErrors, defaultValue, mode = 'unregister' } = options;
|
||||
|
||||
const config = options;
|
||||
|
||||
// Stabilize defaultValue using useRef to prevent infinite render loops
|
||||
// Stabilize defaultValue using useRef to prevent infinite render loops
|
||||
// if developers pass inline arrays/objects (e.g. defaultValue: [])
|
||||
const defaultValueRef = useRef(defaultValue);
|
||||
useEffect(() => {
|
||||
@@ -49,7 +41,7 @@ export function useConditionalField<TFieldValues extends FieldValues>(
|
||||
config.setValue(config.name, targetValue, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
// 2. Execute the appropriate side-effect strategy based on the active mode
|
||||
@@ -59,17 +51,10 @@ export function useConditionalField<TFieldValues extends FieldValues>(
|
||||
config.unregister(config.name);
|
||||
} else if (mode === 'reset' && config.clearErrors) {
|
||||
// Reset Mode: Keeps the field active in the DOM (e.g. cascading or disabled dependencies).
|
||||
// Wipes the value and clears active validation errors so the user can interact
|
||||
// Wipes the value and clears active validation errors so the user can interact
|
||||
// with a fresh state, but keeps the property in the payload.
|
||||
config.clearErrors(config.name);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
condition,
|
||||
name,
|
||||
setValue,
|
||||
unregister,
|
||||
clearErrors,
|
||||
mode
|
||||
]);
|
||||
}
|
||||
}, [condition, name, setValue, unregister, clearErrors, mode]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const radius = {
|
||||
xs: '0.125rem', // 2px
|
||||
sm: '0.25rem', // 4px
|
||||
sm: '0.25rem', // 4px
|
||||
md: '0.375rem', // 6px (Input radius)
|
||||
lg: '0.5rem', // 8px
|
||||
xl: '0.75rem', // 12px
|
||||
lg: '0.5rem', // 8px
|
||||
xl: '0.75rem', // 12px
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('Validator Registry', () => {
|
||||
const schema = compose(z.string(), required('Password'), complexPassword(8));
|
||||
const res = schema.safeParse('Weak');
|
||||
expect(res.success).toBe(false);
|
||||
|
||||
|
||||
const res2 = schema.safeParse('StrongPass1!');
|
||||
expect(res2.success).toBe(true);
|
||||
});
|
||||
@@ -34,7 +34,7 @@ describe('Validator Registry', () => {
|
||||
const res = schema.safeParse('');
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:required', values: { field: 'TestField' } })
|
||||
JSON.stringify({ key: 'validation:required', values: { field: 'TestField' } }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -42,9 +42,7 @@ describe('Validator Registry', () => {
|
||||
const schema = compose(z.string(), emailValidator());
|
||||
const res = schema.safeParse('invalid-email');
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:invalid_email' })
|
||||
);
|
||||
expect(res.error?.issues[0].message).toBe(JSON.stringify({ key: 'validation:invalid_email' }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,7 +52,7 @@ describe('Validator Registry', () => {
|
||||
const res = schema.safeParse(5);
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:min_val', values: { min: 10, field: 'Age' } })
|
||||
JSON.stringify({ key: 'validation:min_val', values: { min: 10, field: 'Age' } }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -63,7 +61,7 @@ describe('Validator Registry', () => {
|
||||
const res = schema.safeParse(105);
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:max_val', values: { max: 100, field: 'Percentage' } })
|
||||
JSON.stringify({ key: 'validation:max_val', values: { max: 100, field: 'Percentage' } }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -72,7 +70,7 @@ describe('Validator Registry', () => {
|
||||
const res = schema.safeParse(5);
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:range_val', values: { min: 10, max: 20, field: 'Range' } })
|
||||
JSON.stringify({ key: 'validation:range_val', values: { min: 10, max: 20, field: 'Range' } }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -81,7 +79,7 @@ describe('Validator Registry', () => {
|
||||
const res = schema.safeParse(-5);
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:must_be_positive', values: { field: 'Amount' } })
|
||||
JSON.stringify({ key: 'validation:must_be_positive', values: { field: 'Amount' } }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -92,7 +90,7 @@ describe('Validator Registry', () => {
|
||||
const res = schema.safeParse('abc');
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:min_len', values: { min: 5, field: 'Username' } })
|
||||
JSON.stringify({ key: 'validation:min_len', values: { min: 5, field: 'Username' } }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -101,7 +99,7 @@ describe('Validator Registry', () => {
|
||||
const res = schema.safeParse('thisisaverylongusername');
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:max_len', values: { max: 10, field: 'Username' } })
|
||||
JSON.stringify({ key: 'validation:max_len', values: { max: 10, field: 'Username' } }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -110,7 +108,7 @@ describe('Validator Registry', () => {
|
||||
const res = schema.safeParse('ab');
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:range_len', values: { min: 3, max: 5, field: 'Code' } })
|
||||
JSON.stringify({ key: 'validation:range_len', values: { min: 3, max: 5, field: 'Code' } }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -121,7 +119,7 @@ describe('Validator Registry', () => {
|
||||
const res = schema.safeParse('short');
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:invalid_password_simple', values: { min: 6 } })
|
||||
JSON.stringify({ key: 'validation:invalid_password_simple', values: { min: 6 } }),
|
||||
);
|
||||
expect(schema.safeParse('longenough').success).toBe(true);
|
||||
});
|
||||
@@ -131,7 +129,7 @@ describe('Validator Registry', () => {
|
||||
expect(schema.safeParse('weakpassword').success).toBe(false);
|
||||
expect(schema.safeParse('NoSpecial1').success).toBe(false);
|
||||
expect(schema.safeParse('ValidPass1!').success).toBe(true);
|
||||
|
||||
|
||||
const res = schema.safeParse('short');
|
||||
expect(res.success).toBe(false);
|
||||
});
|
||||
@@ -142,12 +140,10 @@ describe('Validator Registry', () => {
|
||||
const schema = compose(z.string(), phoneValidator());
|
||||
expect(schema.safeParse('08123456789').success).toBe(false);
|
||||
expect(schema.safeParse('+628123456789').success).toBe(true);
|
||||
|
||||
|
||||
const res = schema.safeParse('invalid');
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error?.issues[0].message).toBe(
|
||||
JSON.stringify({ key: 'validation:invalid_phone' })
|
||||
);
|
||||
expect(res.error?.issues[0].message).toBe(JSON.stringify({ key: 'validation:invalid_phone' }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { ZodString, ZodNumber, ZodTypeAny } from 'zod';
|
||||
|
||||
// ─── UTILITIES ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const compose = <T extends ZodTypeAny>(
|
||||
base: T,
|
||||
...modifiers: ((schema: any) => any)[]
|
||||
): any => {
|
||||
export const compose = <T extends ZodTypeAny>(base: T, ...modifiers: ((schema: any) => any)[]): any => {
|
||||
return modifiers.reduce((acc, curr) => curr(acc), base);
|
||||
};
|
||||
|
||||
@@ -71,20 +68,24 @@ export const rangeLength = (min: number, max: number, field?: string) => (schema
|
||||
|
||||
// ─── SECURITY ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const simplePassword = (min: number = 8) => (schema: ZodString) => {
|
||||
return schema.min(min, {
|
||||
message: JSON.stringify({ key: 'validation:invalid_password_simple', values: { min } }),
|
||||
});
|
||||
};
|
||||
export const simplePassword =
|
||||
(min: number = 8) =>
|
||||
(schema: ZodString) => {
|
||||
return schema.min(min, {
|
||||
message: JSON.stringify({ key: 'validation:invalid_password_simple', values: { min } }),
|
||||
});
|
||||
};
|
||||
|
||||
export const complexPassword = (min: number = 8) => (schema: ZodString) => {
|
||||
return schema
|
||||
.min(min, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
|
||||
.regex(/[A-Z]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
|
||||
.regex(/[a-z]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
|
||||
.regex(/[0-9]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
|
||||
.regex(/[^A-Za-z0-9]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) });
|
||||
};
|
||||
export const complexPassword =
|
||||
(min: number = 8) =>
|
||||
(schema: ZodString) => {
|
||||
return schema
|
||||
.min(min, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
|
||||
.regex(/[A-Z]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
|
||||
.regex(/[a-z]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
|
||||
.regex(/[0-9]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) })
|
||||
.regex(/[^A-Za-z0-9]/, { message: JSON.stringify({ key: 'validation:invalid_password_complex' }) });
|
||||
};
|
||||
|
||||
// ─── TECHNICAL ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user