@knowmax/genericlist-core
Version:
Knowmax Generic list with basic CRUD support without any user interface implementation.
1 lines • 77.5 kB
Source Map (JSON)
{"version":3,"sources":["../src/utils/error.ts","../src/types/constants.ts","../src/types/IFilter.ts","../src/utils/serialization.ts","../src/utils/filterutils.ts","../src/utils/orderutils.ts","../src/utils/params.ts","../src/list/GenericList.ts","../src/list/GenericListSettings.ts","../src/list/GenericCudList.ts","../src/list/GenericListHook.tsx","../src/list/GenericListCacheProvider.tsx","../src/list/GenericListCache.ts","../src/list/GenericListSettingsProvider.tsx","../src/list/SimpleListHook.tsx","../src/index.ts"],"sourcesContent":["/** Returns error message display when calling usePROVIDER at location outside PROVIDER context. */\r\nexport const providerErrorMessage = (provider: string) => `use${provider} must be used within a ${provider}Provider.`","export const PARAM_PAGE = \"page\";\r\nexport const PARAM_ORDER = \"order\";\r\nexport const PARAM_DELETED = \"deleted\";","export enum FilterOperator { eq = 'eq', ne = 'ne', contains = 'contains', gt = 'gt', ge = 'ge', lt = 'lt', le = 'le' }\r\n\r\nexport const FILTEROPERATORSSTRING = [FilterOperator.contains, FilterOperator.eq, FilterOperator.ne];\r\nexport const FILTEROPERATORSNUMBER = [FilterOperator.eq, FilterOperator.ne, FilterOperator.gt, FilterOperator.ge, FilterOperator.lt, FilterOperator.le];\r\nexport const FILTEROPERATORSDATE = [FilterOperator.eq, FilterOperator.ne, FilterOperator.gt, FilterOperator.ge, FilterOperator.lt, FilterOperator.le];\r\n\r\nexport enum FilterValueType { string, number, date, boolean }\r\n\r\n/** Call back method, called from within custom OnRenderCustomType */\r\nexport type OnSelectCustomValue = (value?: string, listId?: string) => void;\r\n\r\n/** Type for rendering custom filter */\r\nexport type OnRenderCustomType = (filter: IFilter, value?: string, onChange?: OnSelectCustomValue, listId?: string) => JSX.Element;\r\n\r\n/** Type for custom handling of deserialization of filter from given params. Should return filter to add to all filter and-wise. */\r\nexport type OnDeserializeCustom = (filter: IFilter, params: URLSearchParams, listId?: string) => string[] | undefined;\r\n\r\nexport interface IFilter {\r\n /** Friendly display name fo filter. */\r\n name: string;\r\n\r\n /** Id used to represent selection in url. */\r\n id: string;\r\n\r\n /** Optional name of field for query in case it differs from @see id. */\r\n field?: string;\r\n\r\n /** Array of options for closed domain filters. If set filter will be presented as a closed domain, otherwise an open input value. */\r\n options?: IFilterOption[];\r\n\r\n /** Text to show as placeholder in input value. */\r\n placeholderText?: string;\r\n\r\n /** Selected operator to use for comparison. Defaults to eq. */\r\n operator?: FilterOperator;\r\n\r\n /** Available operators for comparison. */\r\n operators?: FilterOperator[];\r\n\r\n /** Optional value type. Needed for proper serialization to server and to determine type of input control to render. Defaults to string. */\r\n valueType?: FilterValueType;\r\n\r\n /** Hidden filter will be deserialized from params but are not visible. */\r\n hidden?: boolean;\r\n\r\n /** Set to true to make sure filter is initially collapsed (currently only supported for filters with @see options set and no value selected) */\r\n collapsed?: boolean;\r\n\r\n /** Set to true if this filter is to be used as search field in request to server. Only for fields that render as open input value. */\r\n isSearch?: boolean;\r\n\r\n /** Callback method to determine if filter is available. */\r\n isAvailable?: (filter: IFilter, params: URLSearchParams, listId?: string) => boolean;\r\n\r\n /** Callback method to handle filter in a custom way including rendering. */\r\n renderCustom?: OnRenderCustomType;\r\n\r\n /** Callback method to handle custom filter deserialization. */\r\n deserializeCustom?: OnDeserializeCustom;\r\n}\r\n\r\nexport interface IFilterOption {\r\n /** Unique identification. */\r\n id: string;\r\n\r\n /** Friendly name. */\r\n name?: string;\r\n\r\n /** Value associated with option. Used in combination with @see operator. */\r\n value?: string | number | boolean;\r\n\r\n /** Operator to be used with @see value. Defaults to equal (eq). */\r\n operator?: FilterOperator;\r\n\r\n /** Expression to override @see value and @see operator. */\r\n expression?: string;\r\n}","import { PARAM_PAGE, PARAM_ORDER, PARAM_DELETED, IFilter, FilterOperator, FilterValueType } from \"../types\"\r\nimport { GenericList } from \"../list\"\r\nimport { serializeFilterValue, serializeFilterOperator, parseOrderExpression, fieldIdForOperator, getParamId } from \".\"\r\n\r\n/** Deserialize GenericList settings from given URLSearchParams. \r\n * @returns true if params have been updated. Caller is reposible to update params in state.\r\n */\r\nexport const deserializeListState = (list: GenericList, params: URLSearchParams): boolean => {\r\n const initialstate = listState(list);\r\n let paramsupdated = false;\r\n\r\n deserializeDeleted(list, params);\r\n\r\n if (deserializeFilters(list, params)) {\r\n paramsupdated = true;\r\n }\r\n\r\n if (deserializeOrder(list, params)) {\r\n paramsupdated = true;\r\n }\r\n\r\n if (deserializePage(list, params, initialstate)) {\r\n paramsupdated = true;\r\n }\r\n\r\n return paramsupdated;\r\n}\r\n\r\n/** Current state of list used for state change detection */\r\nconst listState = (list: GenericList) => `f=${list.filter ?? ''}&o=${list.orderExpression}&s=${list.search ?? ''}&d=${list.deleted}`;\r\n\r\nconst deserializeOrder = (list: GenericList<unknown>, params: URLSearchParams) => {\r\n const orderparamid = getParamId(PARAM_ORDER, list.id);\r\n const ordervalue = params.get(orderparamid);\r\n\r\n if (ordervalue) {\r\n const [field, descending] = parseOrderExpression(ordervalue);\r\n\r\n const order = list.orderList.find((order) => order.field === field && (order.descending === descending || (order.descending === undefined && descending === false)));\r\n\r\n if (order && (!order.isAvailable || order.isAvailable(order, params, list.id))) {\r\n list.setOrder(order);\r\n } else {\r\n // reset to default\r\n params.delete(orderparamid);\r\n list.setOrder(list.orderList?.find(o => o.default === true));\r\n return true;\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n\r\nconst deserializeFilters = (list: GenericList<unknown>, params: URLSearchParams) => {\r\n let paramsupdated = false;\r\n let hassearch = false;\r\n\r\n const filters: string[] = [];\r\n\r\n list.filterList.forEach((filter: IFilter) => {\r\n if (filter.deserializeCustom) {\r\n const customfilters = filter.deserializeCustom(filter, params, list.id);\r\n\r\n if (customfilters) {\r\n filters.push(...customfilters);\r\n }\r\n } else {\r\n const paramid = getParamId(filter.id, list.id);\r\n const value = params.get(paramid);\r\n\r\n if (value && value !== '') {\r\n if (filter.isAvailable && !filter.isAvailable(filter, params, list.id)) {\r\n params.delete(paramid);\r\n params.delete(fieldIdForOperator(paramid));\r\n paramsupdated = true;\r\n } else if (filter.options) {\r\n const option = filter.options.find((o) => o.id === value);\r\n\r\n if (option) {\r\n if (option.expression !== undefined && option.expression !== '') {\r\n filters.push(option.expression);\r\n } else if (option.value !== undefined && (typeof option.value === 'number' || typeof option.value === 'boolean' || option.value !== '')) {\r\n filters.push(`${filter.field ?? filter.id} ${serializeFilterOperator(option.operator)} ${serializeFilterValue(option.value, filter.valueType)}`);\r\n }\r\n }\r\n } else if (filter.isSearch === true) {\r\n list.setSearch(value);\r\n hassearch = true;\r\n } else {\r\n filters.push(`${filter.field ?? filter.id} ${serializeFilterOperator(operatorForFilter(filter, params, list.id))} ${serializeFilterValue(value, filter.valueType)}`);\r\n }\r\n }\r\n }\r\n });\r\n list.setFilter(filters.length > 0 ? filters.join(' and ') : undefined/*list.filter*/);\r\n\r\n if (!hassearch) {\r\n list.setSearch(undefined);\r\n }\r\n\r\n return paramsupdated;\r\n}\r\n\r\nconst deserializePage = (list: GenericList, params: URLSearchParams, initialState: string) => {\r\n const pageparamid = getParamId(PARAM_PAGE, list.id);\r\n const page = params.get(pageparamid);\r\n\r\n if (page && initialState === listState(list)) {\r\n const p = parseInt(page, 10);\r\n\r\n if (isNaN(p) || p === 1) {\r\n if (list.page !== 1) {\r\n list.setPage(1);\r\n\r\n params.delete(pageparamid);\r\n return true;\r\n }\r\n } else {\r\n list.setPage(p);\r\n }\r\n } else if (list.page !== 1) {\r\n list.setPage(1);\r\n\r\n params.delete(pageparamid);\r\n return true;\r\n }\r\n\r\n return false;\r\n}\r\n\r\nconst deserializeDeleted = (list: GenericList<unknown>, params: URLSearchParams) => {\r\n const paramid = getParamId(PARAM_DELETED, list.id);\r\n const value = params.get(paramid);\r\n\r\n list.setDeleted(params.has(paramid) && value !== 'false');\r\n}\r\n\r\nexport const operatorForFilter = (filter: IFilter, params: URLSearchParams, listId?: string) => {\r\n if (filter.valueType === FilterValueType.boolean) {\r\n return FilterOperator.eq;\r\n } else {\r\n const operatorvalue = params.get(fieldIdForOperator(getParamId(filter.id, listId)));\r\n\r\n if (operatorvalue) {\r\n const o = FilterOperator[operatorvalue as keyof typeof FilterOperator];\r\n\r\n if (filter.operators && filter.operators.indexOf(o) !== -1) {\r\n return o;\r\n }\r\n }\r\n\r\n return filter.operator;\r\n }\r\n}","import { FilterValueType, FilterOperator } from \"../types\"\r\n\r\n/** Serialize value for filter by adding quotes in case value type is string and value contains spaces. */\r\nexport const serializeFilterValue = (value: string | boolean | number, valueType?: FilterValueType) => {\r\n if (typeof value === 'boolean' || typeof value === 'number') {\r\n return value.toString()\r\n } else if ((valueType === FilterValueType.string || valueType === undefined) && value.indexOf(' ') !== -1) {\r\n return `\"${value}\"`\r\n } else {\r\n return value\r\n }\r\n}\r\n\r\n/** Serialize operator for filter, default to eq. */\r\nexport const serializeFilterOperator = (operator?: FilterOperator) => FilterOperator[operator ?? FilterOperator.eq]\r\n\r\n/** Determine name for field to store operator for field with given id in (url) state. */\r\nexport const fieldIdForOperator = (id: string) => `${id}-o`","/** Parse order expression by splitting order field name and optional sort indicator. */\r\nexport const parseOrderExpression = (expression: string): [string, boolean] => {\r\n const parts = expression.trim().split(/\\s+/);\r\n\r\n if (parts.length > 0) {\r\n return [parts[0].trim(), parts.length === 2 && parts[1].trim() === 'desc'];\r\n } else {\r\n return [expression.trim(), false];\r\n }\r\n}","/** Returns given param id optionally prefixed with id of given list. */\r\nexport const getParamId = (id: string, listId?: string) => {\r\n if (listId !== undefined && listId !== '') {\r\n return `${listId}.${id}`\r\n } else {\r\n return id;\r\n }\r\n}\r\n\r\n/** Get value for param with id optionally prefixed with id of given list. */\r\nexport const getParamValue = (id: string, params: URLSearchParams, listId?: string) => {\r\n return params.get(getParamId(id, listId));\r\n}\r\n\r\n/** Returns true if given id starts with given listId, false otherwise. */\r\nexport const hasListPrefix = (id: string, listId: string) => {\r\n return id.startsWith(`${listId}.`);\r\n}","import { action, computed, makeObservable, observable } from \"mobx\"\r\nimport type { IListResponse, FetchOptions, FetchSearchParams, IOrder, IFilter } from \"../types\"\r\nimport { GenericListSettings } from \"./GenericListSettings\"\r\n\r\nexport interface IGenericListConfiguration {\r\n /** Optional unique id for this list. */\r\n id?: string;\r\n\r\n /** Endpoint to use for loading list of data. */\r\n endpoint: string;\r\n\r\n /** Endpoint to use for loading a single item. */\r\n endpointGetSingle?: string;\r\n\r\n /** Set in case this list required configuration for endpoint postfix. */\r\n requiredEndpointPostfix?: boolean;\r\n\r\n /** List with order options. */\r\n orderList?: IOrder[];\r\n \r\n /** List with filter options. */\r\n filterList?: IFilter[];\r\n\r\n /** Page number for pagination. */\r\n page?: number;\r\n\r\n /** Page size for pagination. */\r\n pageSize?: number;\r\n}\r\n\r\n/** Generic implementation to support lists from different endpoints supporting Knowmax LinqUtility ListRequest.\r\n * Can be inherited or used in combination with GenericListHook, @see useList. */\r\nexport class GenericList<T = unknown> {\r\n\r\n configuration: IGenericListConfiguration;\r\n\r\n private _settingsUpdated: boolean = false;\r\n private _loadOptions?: FetchOptions;\r\n\r\n constructor(configuration: IGenericListConfiguration) {\r\n\r\n makeObservable(this, {\r\n ready: computed,\r\n id: observable,\r\n setId: action,\r\n settings: observable,\r\n setSettings: action,\r\n loadingSingle: observable,\r\n setLoadingSingle: action,\r\n loaded: observable,\r\n setLoaded: action,\r\n loading: observable,\r\n setLoading: action,\r\n error: observable,\r\n setError: action,\r\n orderList: observable,\r\n setOrderList: action,\r\n order: observable,\r\n setOrder: action,\r\n orderExpression: computed,\r\n deleted: observable,\r\n setDeleted: action,\r\n filterList: observable,\r\n setFilterList: action,\r\n filter: observable,\r\n setFilter: action,\r\n search: observable,\r\n setSearch: action,\r\n page: observable,\r\n setPage: action,\r\n pageSize: observable,\r\n setPageSize: action,\r\n list: observable,\r\n setList: action,\r\n totalCount: observable,\r\n setTotalCount: action,\r\n endpointPostFix: observable,\r\n setEndpointPostFix: action,\r\n hash: computed,\r\n currentHash: observable,\r\n setCurrentHash: action,\r\n selectedItem: observable,\r\n selectedItemTag: observable,\r\n setSelectedItem: action,\r\n load: action,\r\n loadSingle: action\r\n });\r\n\r\n this.configuration = configuration;\r\n\r\n this.id = configuration.id;\r\n this.orderList = configuration.orderList ?? [];\r\n this.order = configuration.orderList?.find(o => o.default === true);\r\n this.filterList = configuration.filterList ?? [];\r\n\r\n this.list = [];\r\n this.page = configuration.page ?? 1;\r\n this.pageSize = configuration.pageSize ?? 10;\r\n this.totalCount = 0;\r\n }\r\n\r\n /** Set when this generic list is ready for use. Essentially this means settings configuration is ready. */\r\n get ready() {\r\n return this.settings.ready;\r\n }\r\n\r\n /** Unique id representing this list. Will be provided to methods from settings when called to determine active list. Optional due to backward compatibility. */\r\n id: string | undefined;\r\n setId(value?: string) {\r\n this.id = value;\r\n }\r\n\r\n /** Settings for all generic lists. */\r\n settings: GenericListSettings = new GenericListSettings();\r\n /** Usually set from value provided GenericListSettinsProvider. */\r\n setSettings(value: GenericListSettings) {\r\n this._settingsUpdated = true;\r\n this.settings = value;\r\n }\r\n\r\n /** Set while loading a single item */\r\n loadingSingle: boolean = false;\r\n setLoadingSingle(value: boolean) {\r\n this.loadingSingle = value;\r\n }\r\n\r\n /** Set after initially loading data. */\r\n loaded: boolean = false;\r\n setLoaded(value: boolean) {\r\n this.loaded = value;\r\n }\r\n\r\n /** Set while loading data. */\r\n loading: boolean = false;\r\n setLoading(value: boolean) {\r\n this.loading = value;\r\n }\r\n\r\n error?: Error | string;\r\n setError(value?: Error | string) {\r\n this.error = value;\r\n }\r\n\r\n /** Available options for order. */\r\n orderList: IOrder[];\r\n setOrderList(value: IOrder[]) {\r\n this.orderList = value;\r\n }\r\n\r\n /** Selected order expression. Set to IOrder from orderList or string order expression. */\r\n order?: IOrder | string;\r\n setOrder(value?: IOrder | string) {\r\n this.order = value;\r\n }\r\n\r\n /** Server expression based on current order setting. */\r\n get orderExpression() {\r\n return this.order ? typeof this.order !== 'string' ? `${this.order.field}${this.order.descending === true ? ' desc' : ''}` : this.order : '';\r\n }\r\n\r\n deleted: boolean = false;\r\n setDeleted(value: boolean) {\r\n this.deleted = value;\r\n }\r\n\r\n /** List with all filter definitions available for this list. */\r\n filterList: IFilter[];\r\n setFilterList(value: IFilter[]) {\r\n this.filterList = value;\r\n }\r\n\r\n /** Filter to use while loading data. */\r\n filter?: string;\r\n setFilter(value?: string) {\r\n this.filter = value;\r\n }\r\n\r\n /** Query to be used while loading data. */\r\n search?: string;\r\n setSearch(value?: string) {\r\n this.search = value;\r\n }\r\n\r\n /** Current selected page. 1-based. */\r\n page: number;\r\n setPage(value: number) {\r\n this.page = value;\r\n }\r\n\r\n /** Number of items per page. */\r\n pageSize: number;\r\n setPageSize(value: number) {\r\n this.pageSize = value;\r\n }\r\n\r\n /** List containing actual page of data. */\r\n list: T[];\r\n setList(value: T[]) {\r\n this.list = value;\r\n }\r\n\r\n /** Total number of items available. */\r\n totalCount: number;\r\n setTotalCount(value: number) {\r\n this.totalCount = value;\r\n }\r\n\r\n /** Will be used as post fix for currently configured endpoint. */\r\n endpointPostFix?: string;\r\n /** Set to postfix for current endpoint. For example the value \"abc\" will result in \"/abc\" postfix after currently configured endpoint. */\r\n setEndpointPostFix(value?: string) {\r\n this.endpointPostFix = value;\r\n }\r\n\r\n /** Dynamic hash representing current state of component. Use as dependecy to trigger effects */\r\n get hash() {\r\n return `id=${this.id}&p=${this.page}&ps=${this.pageSize}&o=${this.orderExpression}&f=${this.filter}&s=${this.search}${this.deleted ? '&d=true' : ''}&t=${this.settings.token}&epf=${this.endpointPostFix}`;\r\n }\r\n\r\n /** Hash at the moment of last update of list with server data. */\r\n currentHash?: string;\r\n setCurrentHash(value: string) {\r\n this.currentHash = value;\r\n }\r\n\r\n /** Set when hash was updated. Rather use hash as dependecy to trigger effects. */\r\n get hashUpdated() {\r\n return this.hash !== this.currentHash;\r\n }\r\n\r\n /** Set to selected item for specific purpose. */\r\n selectedItem?: T;\r\n /** Tag to identify purpose of selected item.. */\r\n selectedItemTag?: string;\r\n /** Selects item for specific purpose (not edit or delete). */\r\n setSelectedItem(value?: T, tag?: string) {\r\n this.selectedItem = value;\r\n this.selectedItemTag = value ? tag : undefined;\r\n }\r\n\r\n /** Load current page of items.\r\n * @param reload If true, will reload current page of items using original options used on inital load.\r\n * @param options Options to use while loading data. If not provided, will use options used on inital load only in case of reload.\r\n */\r\n async load(reload: boolean = false, options?: FetchOptions) {\r\n if (!this.settings.token || (this.configuration.requiredEndpointPostfix === true && (!this.endpointPostFix || this.endpointPostFix.trim() === ''))) {\r\n this.setList([]);\r\n this.setTotalCount(0);\r\n this.setLoaded(false);\r\n } else if (this.hashUpdated || reload) {\r\n try {\r\n try {\r\n this.checkSettings();\r\n this.setCurrentHash(this.hash);\r\n this.setError(undefined);\r\n this.setLoading(true);\r\n\r\n // On reload use options from initial load in case no new options are provided.\r\n if (reload && !options && this._loadOptions) {\r\n options = this._loadOptions;\r\n } else if (!reload) {\r\n this._loadOptions = options;\r\n }\r\n\r\n const endpoint = (this.configuration.requiredEndpointPostfix === true && this.endpointPostFix && this.endpointPostFix.trim() !== '') ?\r\n `${this.configuration.endpoint}/${this.endpointPostFix}` :\r\n this.configuration.endpoint; const method = options?.method ?? 'post';\r\n\r\n const searchparams: FetchSearchParams = Array.isArray(options?.searchParams)\r\n ? [...options.searchParams]\r\n : { ...(options?.searchParams as Record<string, string | number | boolean>) };\r\n\r\n // Dynamically add deleted parameter if this.deleted is true\r\n if (this.deleted) {\r\n if (Array.isArray(searchparams)) {\r\n (searchparams as Array<Array<string | number | boolean>>).push(['deleted', 'true']);\r\n } else {\r\n (searchparams as Record<string, string | number | boolean>).deleted = 'true';\r\n }\r\n }\r\n\r\n const response = await this.settings.onFetch<IListResponse<T>>(endpoint, {\r\n searchParams: searchparams,//options?.searchParams,\r\n method: method,\r\n headers: options?.headers ?? this.settings.onGetHeaders(this.settings.token, this.settings.language, undefined, this.id),\r\n // Make sure we never post json when not post was specified as method\r\n json: method === 'post' ? options?.json ?? {\r\n count: true,\r\n take: this.pageSize,\r\n skip: (this.page - 1) * this.pageSize,\r\n orderBy: this.orderExpression,\r\n filter: this.filter,\r\n search: this.search\r\n } : undefined\r\n }, this.id);\r\n\r\n this.setList(response.value ?? []);\r\n this.setTotalCount(response.count ?? 0);\r\n this.setLoaded(true);\r\n } catch (e) {\r\n if (e instanceof Error) {\r\n this.setError(e);\r\n }\r\n this.setLoaded(false);\r\n }\r\n } finally {\r\n this.setLoading(false);\r\n }\r\n }\r\n }\r\n\r\n /** Load single item identified by id. \r\n * @param id Id of item to load.\r\n * @param options Options to use while loading data.\r\n * @param endpoint Endpoint to use while loading data. If not provided, will use endpointGetSingle or endpoint from configuration.\r\n */\r\n async loadSingle(id: string | number, options?: FetchOptions, endpoint?: string) {\r\n this.setError(undefined);\r\n\r\n if (this.settings.token) {\r\n try {\r\n this.checkSettings();\r\n this.setLoadingSingle(true);\r\n\r\n try {\r\n return await this.settings.onFetch<T>(`${endpoint ?? this.configuration.endpointGetSingle ?? this.configuration.endpoint}/${id}`, {\r\n searchParams: options?.searchParams,\r\n method: options?.method ?? 'get',\r\n headers: options?.headers ?? this.settings.onGetHeaders(this.settings.token, this.settings.language, undefined, this.id)\r\n }, this.id);\r\n } catch (e) {\r\n if (e instanceof Error) {\r\n this.setError(e);\r\n }\r\n }\r\n } finally {\r\n this.setLoadingSingle(false);\r\n }\r\n }\r\n }\r\n\r\n private checkSettings() {\r\n if (!this._settingsUpdated) {\r\n console.warn(`Settings not set for generic list with endpoint ${this.configuration.endpoint}`)\r\n }\r\n }\r\n}","import { makeObservable, observable, action } from \"mobx\"\r\nimport type { FetchOptions } from \"../types\"\r\n\r\nexport type TranslationValues = Record<string, string | number>;\r\n\r\nexport type OnGetHeadersType = (token?: string, language?: string, headers?: any, listId?: string) => any;\r\nexport type OnFetchType = <T>(url: string, options?: FetchOptions, listId?: string) => Promise<T>;\r\nexport type OnErrorMessageType = (error: Error) => Promise<string>;\r\nexport type OnSerializeType = (params: URLSearchParams, settings: GenericListSettings, listId?: string) => void;\r\nexport type OnTranslateType = (key: string, values?: TranslationValues, listId?: string) => string;\r\n\r\n/** Responsible for all system settings in use for all generic lists. Allows system specific implementation of generic list components. */\r\nexport class GenericListSettings {\r\n\r\n /** Never use directly. Always use getter. */\r\n private _onFetch: OnFetchType = defaultFetch;\r\n private _onFetchSet = false;\r\n\r\n /** Never use directly. Always use getter. */\r\n private _onTranslate: OnTranslateType = defaultTranslate;\r\n private _onTranslateSet = false;\r\n\r\n constructor() {\r\n makeObservable(this, {\r\n ready: observable,\r\n setReady: action,\r\n token: observable,\r\n setToken: action,\r\n language: observable,\r\n setLanguage: action,\r\n params: observable,\r\n setParams: action\r\n })\r\n }\r\n\r\n /** Set after required configuration of onFetch and onTranslate were made. */\r\n ready: boolean = false;\r\n\r\n /** Internally called after configuration of required settings was done. No need to call it externally. */\r\n setReady(value: boolean) {\r\n this.ready = value;\r\n }\r\n\r\n /** Set to required bearer token for use in authentication of fetch requests. */\r\n token?: string;\r\n setToken(value?: string) {\r\n this.token = value;\r\n }\r\n\r\n /** Set to required language of user for use in fetch requests to get localized data from server. */\r\n language?: string;\r\n setLanguage(value?: string) {\r\n this.language = value;\r\n }\r\n\r\n /** Contains current set of values components user for state. */\r\n params: URLSearchParams = new URLSearchParams();\r\n /** Set to current set of values for components to use for state. */\r\n setParams(value: URLSearchParams) {\r\n this.params = value;\r\n }\r\n\r\n /** Internally used to serialize params. By default set to internally maintained params. Override for custom onSerialize for custom serialization. */\r\n serialize(params: URLSearchParams) {\r\n this.onSerialize(params, this);\r\n }\r\n\r\n /** Used for fetching data from server. */\r\n get onFetch() {\r\n return this._onFetch;\r\n }\r\n set onFetch(value: OnFetchType) {\r\n this._onFetch = value;\r\n this._onFetchSet = true;\r\n this.setReady(this._onFetchSet && this._onTranslateSet);\r\n }\r\n\r\n /** Used for translation of all language sensitive contents. */\r\n get onTranslate() {\r\n return this._onTranslate;\r\n }\r\n set onTranslate(value: OnTranslateType) {\r\n this._onTranslate = value;\r\n this._onTranslateSet = true;\r\n this.setReady(this._onFetchSet && this._onTranslateSet);\r\n }\r\n\r\n /** Override to handle custom serialization of user settings like sorting, filtering and paging. */\r\n onSerialize: OnSerializeType = defaultSerialize;\r\n\r\n /** Override for custom implementation of header generation for fetch requests. */\r\n onGetHeaders: OnGetHeadersType = defaultOnGetHeaders;\r\n\r\n /** Override for custom implementation of error message extraction after for example fetch requests. */\r\n onErrorMessage: OnErrorMessageType = defaultErrorMessage;\r\n}\r\n\r\n/** Default implementation for making fetch requests to server. Always needs to be overrriden with a custom implementation. */\r\nconst defaultFetch: OnFetchType = <T>(url: string, options?: FetchOptions): Promise<T> => {\r\n return new Promise<T>((resolve, reject) => {\r\n console.warn('onFetch not configured');\r\n reject('not implemented');\r\n });\r\n}\r\n\r\n/** Default implementation for handling translations. Always need to be overriden with a custom implementation. */\r\nconst defaultTranslate: OnTranslateType = (key: string, options?: TranslationValues) => {\r\n console.warn('onTranslate not configured');\r\n return key;\r\n}\r\n\r\n/** Default imnplementation of serialization of settings like sorting, filtering and paging made by user */\r\nconst defaultSerialize: OnSerializeType = (params: URLSearchParams, settings: GenericListSettings) => {\r\n settings.setParams(params);\r\n}\r\n\r\n/** Default implementation to get headers for requests to server. */\r\nconst defaultOnGetHeaders: OnGetHeadersType = (token?: string, language?: string, headers?: any) => {\r\n const h: any = headers ?? {};\r\n\r\n if (token) {\r\n h[\"Authorization\"] = \"Bearer \" + token;\r\n }\r\n\r\n if (language) {\r\n h[\"Accept-Language\"] = language;\r\n }\r\n\r\n return h;\r\n}\r\n\r\n/** Default implementation to extract human readible error message from given Error object. */\r\nconst defaultErrorMessage: OnErrorMessageType = (error: Error) => {\r\n return new Promise((resolve) => {\r\n resolve(error.message)\r\n });\r\n}","import { action, computed, observable, makeObservable } from 'mobx'\r\nimport { GenericList } from '.'\r\nimport type { IGenericListConfiguration } from '.'\r\n\r\nexport interface IGenericCudListConfiguration<ET> extends IGenericListConfiguration {\r\n idField?: string;\r\n nameField?: string;\r\n\r\n /** Returns default item to use for new entries. */\r\n defaultItem: () => ET;\r\n\r\n /** Validate given item. Return boolean true for valid item or string error. */\r\n validateItem?: (item: ET) => boolean | string;\r\n\r\n /** Transform given item into item suitable for serialization to server. */\r\n serverItem?: (item: ET) => unknown;\r\n\r\n endpointPost?: string;\r\n endpointPut?: string;\r\n endpointDelete?: string;\r\n endpointRestore?: string;\r\n}\r\n\r\nexport class GenericCudList<T, ET> extends GenericList<T> {\r\n\r\n cudConfiguration: IGenericCudListConfiguration<ET>;\r\n\r\n constructor(cudConfiguration: IGenericCudListConfiguration<ET>) {\r\n super(cudConfiguration);\r\n\r\n makeObservable(this, {\r\n cudError: observable,\r\n setCudError: action,\r\n validationError: observable,\r\n setValidationError: action,\r\n hasCudOrValidationError: computed,\r\n createError: computed,\r\n updateError: computed,\r\n deleteError: computed,\r\n restoreError: computed,\r\n\r\n editableItem: observable,\r\n setEditableItem: action,\r\n editableItemUnmodified: observable,\r\n setEditableItemUnmodified: action,\r\n deleteItem: observable,\r\n setDeleteItem: action,\r\n deletedItem: observable,\r\n setDeletedItem: action,\r\n createdItem: observable,\r\n setCreatedItem: action,\r\n restoreItem: observable,\r\n setRestoreItem: action,\r\n restoredItem: observable,\r\n setRestoredItem: action,\r\n\r\n create: action,\r\n update: action,\r\n delete: action,\r\n restore: action,\r\n cancel: action,\r\n isCreate: computed,\r\n isUpdate: computed,\r\n isDelete: computed,\r\n isRestore: computed,\r\n isModified: computed,\r\n isValidated: computed,\r\n\r\n save: action,\r\n deleteConfirmed: action,\r\n restoreConfirmed: action,\r\n });\r\n\r\n this.cudConfiguration = cudConfiguration;\r\n }\r\n\r\n /** Set in case of Create Update Delete error. Prefer using specialized error fields per mode. */\r\n cudError?: Error | string;\r\n setCudError(value?: Error | string) {\r\n this.cudError = value;\r\n }\r\n\r\n /** Might be set in case custom validation method returned error message. */\r\n validationError?: string;\r\n setValidationError(value?: string) {\r\n this.validationError = value;\r\n }\r\n\r\n /** Set in case of CUD error or validation error (latter only when in update mode). */\r\n get hasCudOrValidationError() {\r\n return ((this.isCreate || this.isUpdate) && (this.cudError !== undefined)) ||\r\n (this.isUpdate && this.validationError !== undefined)\r\n }\r\n\r\n /** Set after error in create mode. */\r\n get createError() {\r\n return (this.isCreate && this.cudError !== undefined) ? this.cudError : undefined;\r\n }\r\n\r\n /** Set after error in update mode. */\r\n get updateError() {\r\n return (this.isUpdate && this.cudError !== undefined) ? this.cudError : undefined;\r\n }\r\n\r\n /** Set after error in delete mode. */\r\n get deleteError() {\r\n return (this.isDelete && this.cudError !== undefined) ? this.cudError : undefined;\r\n }\r\n\r\n /** Set after error in restore mode. */\r\n get restoreError() {\r\n return (this.isRestore && this.cudError !== undefined) ? this.cudError : undefined;\r\n }\r\n\r\n /** Set to editable item when in create or update mode. */\r\n editableItem?: ET;\r\n setEditableItem(value?: ET) {\r\n this.editableItem = value;\r\n\r\n const validationerror = value !== undefined && this.cudConfiguration.validateItem && this.cudConfiguration.validateItem(value);\r\n if (typeof validationerror === 'string') {\r\n this.setValidationError(validationerror);\r\n } else {\r\n this.setValidationError(undefined);\r\n }\r\n }\r\n\r\n /** Set to original editable item when starting update mode. */\r\n editableItemUnmodified?: ET;\r\n setEditableItemUnmodified(value?: ET) {\r\n this.editableItemUnmodified = value;\r\n }\r\n\r\n /** Set to item when in delete mode. */\r\n deleteItem?: T;\r\n setDeleteItem(value?: T) {\r\n this.deleteItem = value;\r\n }\r\n\r\n /** Set with data of just deleted item. */\r\n deletedItem?: T;\r\n setDeletedItem(value?: T) {\r\n this.deletedItem = value;\r\n }\r\n\r\n /** Set with data of newly created item. */\r\n createdItem?: T;\r\n setCreatedItem(value?: T) {\r\n this.createdItem = value;\r\n }\r\n\r\n /** Set to item when in restore mode. */\r\n restoreItem?: T;\r\n setRestoreItem(value?: T) {\r\n this.restoreItem = value;\r\n }\r\n\r\n /** Set with data of just restored item. */\r\n restoredItem?: T;\r\n setRestoredItem(value?: T) {\r\n this.restoredItem = value;\r\n }\r\n /** Start create mode for item. */\r\n create = (template?: ET) => {\r\n this.deleteItem = undefined;\r\n this.restoreItem = undefined;\r\n this.editableItem = template ?? this.cudConfiguration.defaultItem();\r\n this.editableItemUnmodified = undefined;\r\n this.cudError = undefined;\r\n this.validationError = undefined;\r\n }\r\n /** Start update mode for given item. */\r\n update(entity: ET | T) {\r\n this.deleteItem = undefined;\r\n this.restoreItem = undefined;\r\n this.editableItem = { ...entity as ET };\r\n this.editableItemUnmodified = { ...entity as ET };\r\n this.cudError = undefined;\r\n this.validationError = undefined;\r\n }\r\n\r\n /** Start delete mode for given item. */\r\n delete(entity: T) {\r\n this.editableItem = undefined;\r\n this.editableItemUnmodified = undefined;\r\n this.deleteItem = entity;\r\n this.restoreItem = undefined;\r\n this.cudError = undefined;\r\n this.validationError = undefined;\r\n }\r\n\r\n /** Start restore mode for given item. */\r\n restore(entity: T) {\r\n this.editableItem = undefined;\r\n this.editableItemUnmodified = undefined;\r\n this.deleteItem = undefined;\r\n this.restoreItem = entity;\r\n this.cudError = undefined;\r\n this.validationError = undefined;\r\n }\r\n\r\n /** Cancel any mode we are currently in (create, update, delete, restore). Using lambda syntax here so we can directly hook this method to onClick/onChange like events of user interface. */\r\n cancel = () => {\r\n this.editableItem = undefined;\r\n this.editableItemUnmodified = undefined;\r\n this.deleteItem = undefined;\r\n this.restoreItem = undefined;\r\n this.cudError = undefined;\r\n this.validationError = undefined;\r\n }\r\n\r\n /** True if in create mode. */\r\n get isCreate() {\r\n return this.editableItem !== undefined && this.idForItem(this.editableItem) === undefined;\r\n }\r\n\r\n /** True if in update mode. */\r\n get isUpdate() {\r\n return this.editableItem !== undefined && this.idForItem(this.editableItem) !== undefined;\r\n }\r\n\r\n /** True if in delete mode. */\r\n get isDelete() {\r\n return this.deleteItem !== undefined;\r\n }\r\n\r\n /** True if in restore mode. */\r\n get isRestore() {\r\n return this.restoreItem !== undefined;\r\n }\r\n\r\n /** True is editable data was changed while in update mode. */\r\n get isModified() {\r\n return this.editableItemUnmodified === undefined || JSON.stringify(this.editableItemUnmodified) !== JSON.stringify(this.editableItem);\r\n }\r\n\r\n /** True if editable data contains valid data while in create or update mode. */\r\n get isValidated() {\r\n return this.editableItem && (!this.cudConfiguration.validateItem || this.cudConfiguration.validateItem(this.editableItem) === true);\r\n }\r\n\r\n /** Perform actual create or update operation of item set in create or update mode. */\r\n async save() {\r\n this.setCudError(undefined);\r\n this.setCreatedItem(undefined);\r\n\r\n if (this.settings.token && this.editableItem) {\r\n try {\r\n // Construct item specifically for server to prevent overposting of data not used by server\r\n const serveritem = this.cudConfiguration.serverItem ?\r\n this.cudConfiguration.serverItem(this.editableItem) :\r\n { ...this.editableItem };\r\n\r\n if (this.isCreate) {\r\n const created = await this.settings.onFetch<T>(this.cudConfiguration.endpointPost ?? this.configuration.endpoint, {\r\n method: 'post',\r\n headers: this.settings.onGetHeaders(this.settings.token, this.settings.language, undefined, this.id),\r\n json: serveritem\r\n }, this.id);\r\n\r\n this.cancel();\r\n this.setCreatedItem(created);\r\n\r\n return true;\r\n } else if (this.isUpdate && this.idForItem(this.editableItem) !== undefined) {\r\n await this.settings.onFetch(`${this.cudConfiguration.endpointPut ?? this.configuration.endpoint}/${this.idForItem(this.editableItem)}`, {\r\n method: 'put',\r\n headers: this.settings.onGetHeaders(this.settings.token, this.settings.language, undefined, this.id),\r\n json: serveritem\r\n }, this.id);\r\n\r\n this.setEditableItemUnmodified({ ...this.editableItem });\r\n return true;\r\n }\r\n } catch (e) {\r\n if (e instanceof Error) {\r\n this.setCudError(e);\r\n }\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n /** Perform actual delete operation of item previously set in delete mode. */\r\n async deleteConfirmed() {\r\n this.setCudError(undefined);\r\n\r\n if (this.isDelete && this.deleteItem && this.settings.token) {\r\n try {\r\n await this.settings.onFetch(`${this.cudConfiguration.endpointDelete ?? this.configuration.endpoint}/${this.idForItem(this.deleteItem)}`, {\r\n method: 'delete',\r\n headers: this.settings.onGetHeaders(this.settings.token, this.settings.language, undefined, this.id),\r\n // TODO: reintroduce retry\r\n //retry: 0\r\n }, this.id);\r\n\r\n this.setDeletedItem(this.deleteItem);\r\n this.cancel();\r\n return true;\r\n } catch (e) {\r\n if (e instanceof Error) {\r\n this.setCudError(e);\r\n }\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n /** Perform actual restore operation of item previously set in restore mode. */\r\n async restoreConfirmed() {\r\n this.setCudError(undefined);\r\n\r\n if (this.isRestore && this.restoreItem && this.settings.token) {\r\n try {\r\n await this.settings.onFetch(`${this.cudConfiguration.endpointRestore ?? this.configuration.endpoint}/restore/${this.idForItem(this.restoreItem)}`, {\r\n method: 'post',\r\n headers: this.settings.onGetHeaders(this.settings.token, this.settings.language, undefined, this.id),\r\n // TODO: reintroduce retry\r\n //retry: 0\r\n }, this.id);\r\n\r\n this.setRestoredItem(this.restoreItem);\r\n this.cancel();\r\n return true;\r\n } catch (e) {\r\n if (e instanceof Error) {\r\n this.setCudError(e);\r\n }\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n /** Return id of given item using settings provided in provided @see IGenericCudListConfiguration. */\r\n idForItem(item: T | ET): string | number | undefined {\r\n const idField = this.cudConfiguration.idField ?? 'id';\r\n return this.getNestedProperty(item, idField);\r\n }\r\n\r\n /** Return name of given item using settings provided in provided @see IGenericCudListConfiguration. */\r\n nameForItem(item: T | ET): string | number | undefined {\r\n const nameField = this.cudConfiguration.nameField ?? 'name';\r\n return this.getNestedProperty(item, nameField);\r\n }\r\n\r\n /** Helper function to get nested property values using dot notation (e.g., \"user.name.surname\"). */\r\n private getNestedProperty(obj: any, path: string): string | number | undefined {\r\n if (!obj || !path) {\r\n return undefined;\r\n }\r\n\r\n // Split the path by dots and traverse the object\r\n const keys = path.split('.');\r\n let current = obj;\r\n\r\n for (const key of keys) {\r\n if (current === null || current === undefined || typeof current !== 'object') {\r\n return undefined;\r\n }\r\n current = current[key];\r\n }\r\n\r\n // Return the final value if it's a string or number, otherwise undefined\r\n return (typeof current === 'string' || typeof current === 'number') ? current : undefined;\r\n }\r\n}","import { useEffect, useMemo } from \"react\"\r\nimport type { DependencyList } from \"react\"\r\nimport { deserializeListState } from \"../utils\"\r\nimport { useGenericListCache } from \"./GenericListCacheProvider\"\r\nimport { GenericList, useGenericListSettings } from '.'\r\nimport type { IGenericListConfiguration } from '.'\r\n\r\n/** \r\n * Generic hook for specified type and configuration. Attempts to cache lists using endpoint as key. \r\n * Automatically loads data, updates token and selected application.\r\n * \r\n * @param configuration - List configuration including endpoint and other settings\r\n * @param autoLoad - Whether to automatically load data when the list is created or dependencies change\r\n * @param cache - Whether to use caching. When true, lists are cached by endpoint/id key\r\n * @param dependencies - Optional custom dependencies. When any of these change, the cached list (if any) \r\n * will be removed and a fresh list instance will be created\r\n */\r\nexport const useList = <T extends object>(\r\n configuration: IGenericListConfiguration,\r\n autoLoad: boolean = true,\r\n cache: boolean = true,\r\n dependencies?: DependencyList\r\n): GenericList<T> => {\r\n const listsettings = useGenericListSettings()\r\n const listcache = useGenericListCache()\r\n\r\n const list = useMemo(() => {\r\n if (cache) {\r\n const key = configuration.id ? `${configuration.id}:${configuration.endpoint}` : configuration.endpoint;\r\n\r\n // If custom dependencies are provided, remove the existing cached list first\r\n // to ensure we get a fresh instance when dependencies change\r\n if (dependencies) {\r\n listcache.remove(key);\r\n }\r\n\r\n return listcache.getOrCreate<T>(key, () => new GenericList<T>(configuration))\r\n } else {\r\n return new GenericList<T>(configuration)\r\n }\r\n }, [cache, configuration.endpoint, configuration.id, listcache, ...(dependencies || [])])\r\n\r\n useEffect(() => {\r\n list.setSettings(listsettings)\r\n }, [list, listsettings])\r\n\r\n useEffect(() => {\r\n const params = new URLSearchParams(listsettings.params)\r\n if (deserializeListState(list, params)) {\r\n listsettings.serialize(params)\r\n }\r\n }, [list, listsettings.params])\r\n\r\n useEffect(() => {\r\n if (autoLoad) {\r\n list.load()\r\n }\r\n }, [list.hash, list, autoLoad])\r\n\r\n return list\r\n}","import { FunctionComponent, createContext, useContext, useMemo } from 'react'\r\nimport { observer } from 'mobx-react-lite'\r\nimport { providerErrorMessage } from '../utils'\r\nimport { GenericListCache } from './GenericListCache'\r\n\r\nconst GenericListCacheContext = createContext<GenericListCache | null>(null)\r\n\r\nexport const useGenericListCache = (): GenericListCache => {\r\n const store = useContext(GenericListCacheContext)\r\n\r\n if (!store) {\r\n throw new Error(providerErrorMessage('GenericListCache'))\r\n }\r\n\r\n return store\r\n}\r\n\r\nexport const GenericListCacheProvider: FunctionComponent<{ children?: React.ReactNode }> = observer(({ children }) => {\r\n const genericlistcache = useMemo(() => new GenericListCache(10), [])\r\n\r\n return (\r\n <GenericListCacheContext.Provider value={genericlistcache}>\r\n {children}\r\n </GenericListCacheContext.Provider>\r\n )\r\n})","import { GenericList } from '.'\r\n\r\ninterface ICacheItem {\r\n key: string;\r\n value: GenericList\r\n}\r\n\r\nexport class GenericListCache {\r\n list: ICacheItem[] = [];\r\n maxSize: number;\r\n\r\n constructor(maxSize: number) {\r\n this.maxSize = maxSize;\r\n }\r\n\r\n get<T>(key: string) {\r\n const list = this.list.find((item) => item.key === key)?.value;\r\n\r\n return list ? list as GenericList<T> : undefined;\r\n }\r\n\r\n getOrCreate<T>(key: string, creator: () => GenericList<T>) {\r\n return (this.list.find((item) => item.key === key)?.value ?? this.set(key, creator)) as GenericList<T>;\r\n }\r\n\r\n set(key: string, creator: () => GenericList) {\r\n const list = creator();\r\n const index = this.list.findIndex((item) => item.key === key);\r\n\r\n if (index !== -1) {\r\n this.list[index].value = list;\r\n } else {\r\n if (this.list.length === this.maxSize) {\r\n this.list.shift();\r\n }\r\n this.list.push({ key: key, value: list });\r\n }\r\n\r\n return list;\r\n }\r\n\r\n remove(key: string) {\r\n const index = this.list.findIndex((item) => item.key === key);\r\n\r\n if (index !== -1) {\r\n this.list.splice(index, 1);\r\n }\r\n }\r\n}","import { FunctionComponent, createContext, useContext, useMemo } from 'react'\r\nimport { observer } from 'mobx-react-lite'\r\nimport { providerErrorMessage } from '../utils'\r\nimport { GenericListSettings } from './GenericListSettings'\r\nimport { GenericListCacheProvider } from './GenericListCacheProvider'\r\n\r\nconst GenericListSettingsContext = createContext<GenericListSettings | null>(null);\r\n\r\nexport const useGenericListSettings = (): GenericListSettings => {\r\n const genericlistsettings = useContext(GenericListSettingsContext);\r\n\r\n if (!genericlistsettings) {\r\n throw new Error(providerErrorMessage('GenericListSettings'));\r\n }\r\n\r\n return genericlistsettings;\r\n}\r\n\r\n/** Wrap this around your application to be able to configure and use generic lists. \r\n * Also includes GenericListCacheProvider, so no need to wrap that around your application. */\r\nexport const GenericListSettingsProvider: FunctionComponent<{ children?: React.ReactNode }> = observer(({ children }) => {\r\n const genericlistsettings = useMemo(() => new GenericListSettings(), []);\r\n\r\n return (\r\n <GenericListSettingsContext.Provider value={genericlistsettings}>\r\n <GenericListCacheProvider>\r\n {children}\r\n </GenericListCacheProvider>\r\n </GenericListSettingsContext.Provider>\r\n )\r\n})","import { useEffect, useMemo, useState, useCallback } from \"react\"\r\nimport type { DependencyList } from \"react\"\r\nimport { headers, FetchError } from \"@knowmax/http-utils\"\r\nimport type { IListRequest, IListResponse } from \"../types\"\r\n\r\n/**\r\n * Configuration object for the useSimpleList hook.\r\n */\r\ntype TSimpleListConfiguration = {\r\n /** The API endpoint URL to fetch data from */\r\n endpoint: string\r\n /** Bearer token for authentication */\r\n token: string\r\n /** Optional ordering specification (e.g., \"name ASC\", \"date DESC\") */\r\n order?: string\r\n /** Optional filter criteria for the query */\r\n filter?: string\r\n /** Whether to include total count in the response */\r\n count?: boolean\r\n /** Current page number (1-based, defaults to 1) */\r\n page?: number\r\n /** Number of items per page (defaults to 10) */\r\n pageSize?: number\r\n /** Optional search string for full-text search */\r\n search?: string\r\n}\r\n\r\n/**\r\n * Return type for the useSimpleList hook.\r\n */\r\nexport interface ISimpleListHookResult<T> {\r\n /** The fetched list response containing data and metadata */\r\n result?: IListResponse<T>\r\n /** Loading state indicator */\r\n isLoading: boolean\r\n /** Error object if the request failed */\r\n error?: FetchError | Error\r\n /** Http status code */\r\n status?: number\r\n /** Function to manually refetch the data */\r\n refetch: () => Promise<void>\r\n}\r\n\r\n/**\r\n * A simplified React hook for fetching paginated list data from an API endpoint.\r\n * \r\n * This hook is designed as a lightweight alternative to