R: Add ReadOnly wrapper for backend data
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
Frontend CI / notify-failure (push) Has been cancelled

This commit is contained in:
Ivan 2025-04-30 12:31:30 +03:00
parent d90cf08b6c
commit b7358d3cc7
33 changed files with 178 additions and 63 deletions

View File

@ -8,6 +8,7 @@ import { type z, ZodError } from 'zod';
import { buildConstants } from '@/utils/build-constants';
import { PARAMETER } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
import { type RO } from '@/utils/meta';
import { extractErrorMessage } from '@/utils/utils';
export { AxiosError } from 'axios';
@ -58,7 +59,7 @@ export function axiosGet<ResponseData>({ endpoint, options, schema }: IAxiosGetR
.get<ResponseData>(endpoint, options)
.then(response => {
schema?.parse(response.data);
return response.data;
return response.data as RO<ResponseData>;
})
.catch((error: Error | AxiosError) => {
// Note: Ignore cancellation errors
@ -81,7 +82,7 @@ export function axiosPost<RequestData, ResponseData = void>({
.then(response => {
schema?.parse(response.data);
notifySuccess(response.data, request?.successMessage);
return response.data;
return response.data as RO<ResponseData>;
})
.catch((error: Error | AxiosError | ZodError) => {
notifyError(error);
@ -100,7 +101,7 @@ export function axiosDelete<RequestData, ResponseData = void>({
.then(response => {
schema?.parse(response.data);
notifySuccess(response.data, request?.successMessage);
return response.data;
return response.data as RO<ResponseData>;
})
.catch((error: Error | AxiosError | ZodError) => {
notifyError(error);
@ -119,7 +120,7 @@ export function axiosPatch<RequestData, ResponseData = void>({
.then(response => {
schema?.parse(response.data);
notifySuccess(response.data, request?.successMessage);
return response.data;
return response.data as RO<ResponseData>;
})
.catch((error: Error | AxiosError | ZodError) => {
notifyError(error);

View File

@ -11,7 +11,7 @@ import { cn } from '../utils';
interface ComboBoxProps<Option> extends Styling {
id?: string;
items?: Option[];
items?: readonly Option[];
value: Option | null;
onChange: (newValue: Option | null) => void;

View File

@ -4,6 +4,7 @@ import { type IOperationSchemaDTO } from '@/features/oss';
import { type IRSFormDTO } from '@/features/rsform';
import { KEYS } from '@/backend/configuration';
import { type RO } from '@/utils/meta';
import { libraryApi } from './api';
import { type AccessPolicy, type ILibraryItem } from './types';
@ -38,7 +39,7 @@ export const useSetAccessPolicy = () => {
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
!prev ? undefined : { ...prev, access_policy: variables.policy }
);
client.setQueryData(libraryKey, (prev: ILibraryItem[] | undefined) =>
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, access_policy: variables.policy } : item))
);
},

View File

@ -4,6 +4,7 @@ import { type IOperationSchemaDTO } from '@/features/oss';
import { type IRSFormDTO } from '@/features/rsform';
import { KEYS } from '@/backend/configuration';
import { type RO } from '@/utils/meta';
import { libraryApi } from './api';
import { type ILibraryItem } from './types';
@ -38,7 +39,7 @@ export const useSetLocation = () => {
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
!prev ? undefined : { ...prev, location: variables.location }
);
client.setQueryData(libraryKey, (prev: ILibraryItem[] | undefined) =>
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item))
);
},

View File

@ -4,6 +4,7 @@ import { type IOperationSchemaDTO } from '@/features/oss';
import { type IRSFormDTO } from '@/features/rsform';
import { KEYS } from '@/backend/configuration';
import { type RO } from '@/utils/meta';
import { libraryApi } from './api';
import { type ILibraryItem } from './types';
@ -38,7 +39,7 @@ export const useSetOwner = () => {
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
!prev ? undefined : { ...prev, owner: variables.owner }
);
client.setQueryData(libraryKey, (prev: ILibraryItem[] | undefined) =>
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, owner: variables.owner } : item))
);
},

View File

@ -4,6 +4,7 @@ import { type IOperationSchemaDTO } from '@/features/oss';
import { type IRSFormDTO } from '@/features/rsform';
import { KEYS } from '@/backend/configuration';
import { type RO } from '@/utils/meta';
import { libraryApi } from './api';
import { type ILibraryItem, type IUpdateLibraryItemDTO, LibraryItemType } from './types';
@ -20,7 +21,7 @@ export const useUpdateItem = () => {
data.item_type === LibraryItemType.RSFORM
? KEYS.composite.rsItem({ itemID: data.id })
: KEYS.composite.ossItem({ itemID: data.id });
client.setQueryData(libraryKey, (prev: ILibraryItem[] | undefined) =>
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
prev?.map(item => (item.id === data.id ? data : item))
);
client.setQueryData(itemKey, (prev: IRSFormDTO | IOperationSchemaDTO | undefined) =>

View File

@ -1,5 +1,7 @@
import { useQueryClient } from '@tanstack/react-query';
import { type RO } from '@/utils/meta';
import { type ILibraryItem } from './types';
import { useLibraryListKey } from './use-library';
@ -10,7 +12,7 @@ export function useUpdateTimestamp() {
updateTimestamp: (target: number) =>
client.setQueryData(
libraryKey, //
(prev: ILibraryItem[] | undefined) =>
(prev: RO<ILibraryItem[]> | undefined) =>
prev?.map(item => (item.id === target ? { ...item, time_update: Date() } : item))
)
};

View File

@ -9,6 +9,7 @@ import { DataTable, type IConditionalStyle, type VisibilityState } from '@/compo
import { useWindowSize } from '@/hooks/use-window-size';
import { useFitHeight } from '@/stores/app-layout';
import { usePreferencesStore } from '@/stores/preferences';
import { type RO } from '@/utils/meta';
import { type ILibraryItem, LibraryItemType } from '../../backend/types';
import { useLibrarySearchStore } from '../../stores/library-search';
@ -16,7 +17,7 @@ import { useLibrarySearchStore } from '../../stores/library-search';
import { useLibraryColumns } from './use-library-columns';
interface TableLibraryItemsProps {
items: ILibraryItem[];
items: RO<ILibraryItem[]>;
}
export function TableLibraryItems({ items }: TableLibraryItemsProps) {
@ -55,7 +56,7 @@ export function TableLibraryItems({ items }: TableLibraryItemsProps) {
<DataTable
id='library_data'
columns={columns}
data={items}
data={items as ILibraryItem[]}
headPosition='0'
className={clsx('cc-scroll-y h-fit text-xs sm:text-sm border-b', folderMode && 'border-l')}
style={{ maxHeight: tableHeight }}

View File

@ -6,12 +6,13 @@ import { MiniButton } from '@/components/control';
import { createColumnHelper } from '@/components/data-table';
import { IconFolderTree } from '@/components/icons';
import { useWindowSize } from '@/hooks/use-window-size';
import { type RO } from '@/utils/meta';
import { type ILibraryItem } from '../../backend/types';
import { BadgeLocation } from '../../components/badge-location';
import { useLibrarySearchStore } from '../../stores/library-search';
const columnHelper = createColumnHelper<ILibraryItem>();
const columnHelper = createColumnHelper<RO<ILibraryItem>>();
export function useLibraryColumns() {
const { isSmall } = useWindowSize();

View File

@ -5,6 +5,7 @@
import { type ILibraryItem } from '@/features/library';
import { Graph } from '@/models/graph';
import { type RO } from '@/utils/meta';
import { type IBlock, type IOperation, type IOperationSchema, type IOperationSchemaStats } from '../models/oss';
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from '../pages/oss-page/editor-oss-graph/graph/block-node';
@ -19,9 +20,9 @@ export class OssLoader {
private operationByID = new Map<number, IOperation>();
private blockByID = new Map<number, IBlock>();
private schemaIDs: number[] = [];
private items: ILibraryItem[];
private items: RO<ILibraryItem[]>;
constructor(input: IOperationSchemaDTO, items: ILibraryItem[]) {
constructor(input: RO<IOperationSchemaDTO>, items: RO<ILibraryItem[]>) {
this.oss = structuredClone(input) as IOperationSchema;
this.items = items;
}

View File

@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration';
import { type RO } from '@/utils/meta';
import { ossApi } from './api';
import { type IOperationSchemaDTO, type IOssLayout } from './types';
@ -17,7 +18,7 @@ export const useUpdateLayout = () => {
updateTimestamp(variables.itemID);
client.setQueryData(
ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey,
(prev: IOperationSchemaDTO | undefined) =>
(prev: RO<IOperationSchemaDTO> | undefined) =>
!prev
? prev
: {

View File

@ -9,9 +9,10 @@ import { ComboBox } from '@/components/input/combo-box';
import { type Styling } from '@/components/props';
import { cn } from '@/components/utils';
import { NoData } from '@/components/view';
import { type RO } from '@/utils/meta';
import { labelOssItem } from '../labels';
import { type IBlock, type IOperation, type IOperationSchema } from '../models/oss';
import { type IOperationSchema, type IOssItem } from '../models/oss';
import { getItemID, isOperation } from '../models/oss-api';
const SELECTION_CLEAR_TIMEOUT = 1000;
@ -25,7 +26,7 @@ interface PickMultiOperationProps extends Styling {
disallowBlocks?: boolean;
}
const columnHelper = createColumnHelper<IOperation | IBlock>();
const columnHelper = createColumnHelper<RO<IOssItem>>();
export function PickContents({
rows,
@ -40,7 +41,7 @@ export function PickContents({
const selectedItems = value
.map(itemID => (itemID > 0 ? schema.operationByID.get(itemID) : schema.blockByID.get(-itemID)))
.filter(item => item !== undefined);
const [lastSelected, setLastSelected] = useState<IOperation | IBlock | null>(null);
const [lastSelected, setLastSelected] = useState<RO<IOssItem> | null>(null);
const items = [
...(disallowBlocks ? [] : schema.blocks.filter(item => !value.includes(-item.id) && !exclude?.includes(-item.id))),
...schema.operations.filter(item => !value.includes(item.id) && !exclude?.includes(item.id))
@ -50,7 +51,7 @@ export function PickContents({
onChange(value.filter(item => item !== target));
}
function handleSelect(target: IOperation | IBlock | null) {
function handleSelect(target: RO<IOssItem> | null) {
if (target) {
setLastSelected(target);
onChange([...value, getItemID(target)]);

View File

@ -1,3 +1,5 @@
import { type RO } from '@/utils/meta';
import { OperationType } from './backend/types';
import {
type IOperation,
@ -26,7 +28,7 @@ export function describeOperationType(itemType: OperationType): string {
}
/** Generates error description for {@link ISubstitutionErrorDescription}. */
export function describeSubstitutionError(error: ISubstitutionErrorDescription): string {
export function describeSubstitutionError(error: RO<ISubstitutionErrorDescription>): string {
switch (error.errorType) {
case SubstitutionErrorType.invalidIDs:
return 'Ошибка в идентификаторах схем';
@ -55,7 +57,7 @@ export function describeSubstitutionError(error: ISubstitutionErrorDescription):
}
/** Retrieves label for {@link IOssItem}. */
export function labelOssItem(item: IOssItem): string {
export function labelOssItem(item: RO<IOssItem>): string {
if (isOperation(item)) {
return `${(item as IOperation).alias}: ${item.title}`;
} else {

View File

@ -20,6 +20,7 @@ import {
} from '@/features/rsform/models/rslang-api';
import { infoMsg } from '@/utils/labels';
import { type RO } from '@/utils/meta';
import { Graph } from '../../../models/graph';
import { describeSubstitutionError } from '../labels';
@ -29,17 +30,17 @@ import { type IOperationSchema, type IOssItem, SubstitutionErrorType } from './o
const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution
/** Checks if element is {@link IOperation} or {@link IBlock}. */
export function isOperation(item: IOssItem | null): boolean {
export function isOperation(item: RO<IOssItem> | null): boolean {
return !!item && 'arguments' in item;
}
/** Extract contiguous ID of {@link IOperation} or {@link IBlock}. */
export function getItemID(item: IOssItem): number {
export function getItemID(item: RO<IOssItem>): number {
return isOperation(item) ? item.id : -item.id;
}
/** Sorts library items relevant for the specified {@link IOperationSchema}. */
export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): ILibraryItem[] {
export function sortItemsForOSS(oss: IOperationSchema, items: readonly ILibraryItem[]): ILibraryItem[] {
const result = items.filter(item => item.location === oss.location);
for (const item of items) {
if (item.visible && item.owner === oss.owner && !result.includes(item)) {

View File

@ -3,6 +3,7 @@
*/
import { Graph } from '@/models/graph';
import { type RO } from '@/utils/meta';
import { type IConstituenta, type IRSForm, type IRSFormStats } from '../models/rsform';
import { inferClass, inferStatus, inferTemplate, isBaseSet, isFunctional } from '../models/rsform-api';
@ -23,7 +24,7 @@ export class RSFormLoader {
private cstByAlias = new Map<string, IConstituenta>();
private cstByID = new Map<number, IConstituenta>();
constructor(input: IRSFormDTO) {
constructor(input: RO<IRSFormDTO>) {
this.schema = structuredClone(input) as IRSForm;
this.schema.version = input.version ?? 'latest';
}

View File

@ -6,6 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels';
import { type RO } from '@/utils/meta';
import {
type IConstituentaBasicsDTO,
@ -21,7 +22,7 @@ import { FormCreateCst } from './form-create-cst';
export interface DlgCreateCstProps {
initial: ICreateConstituentaDTO;
schema: IRSForm;
onCreate: (data: IConstituentaBasicsDTO) => void;
onCreate: (data: RO<IConstituentaBasicsDTO>) => void;
}
export function DlgCreateCst() {

View File

@ -10,6 +10,7 @@ import { Loader } from '@/components/loader';
import { ModalForm } from '@/components/modal';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
import { useDialogsStore } from '@/stores/dialogs';
import { type RO } from '@/utils/meta';
import {
CstType,
@ -28,7 +29,7 @@ import { TemplateState } from './template-state';
export interface DlgCstTemplateProps {
schema: IRSForm;
onCreate: (data: IConstituentaBasicsDTO) => void;
onCreate: (data: RO<IConstituentaBasicsDTO>) => void;
insertAfter?: number;
}

View File

@ -4,6 +4,7 @@ import { useEffect } from 'react';
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState } from 'reactflow';
import { DiagramFlow } from '@/components/flow/diagram-flow';
import { type RO } from '@/utils/meta';
import { type SyntaxTree } from '../../models/rslang';
@ -22,7 +23,7 @@ const flowOptions = {
} as const;
interface ASTFlowProps {
data: SyntaxTree;
data: RO<SyntaxTree>;
onNodeEnter: (node: Node) => void;
onNodeLeave: (node: Node) => void;
onChangeDragging: (value: boolean) => void;

View File

@ -9,6 +9,7 @@ import { HelpTopic } from '@/features/help';
import { ModalView } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs';
import { type RO } from '@/utils/meta';
import { type SyntaxTree } from '../../models/rslang';
@ -17,7 +18,7 @@ import { ASTFlow } from './ast-flow';
const NODE_POPUP_DELAY = 100;
export interface DlgShowASTProps {
syntaxTree: SyntaxTree;
syntaxTree: RO<SyntaxTree>;
expression: string;
}

View File

@ -8,6 +8,7 @@ import { HelpTopic } from '@/features/help';
import { ModalView } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels';
import { type RO } from '@/utils/meta';
import { type ITypeInfo } from '../../models/rslang';
import { TypificationGraph } from '../../models/typification-graph';
@ -15,7 +16,7 @@ import { TypificationGraph } from '../../models/typification-graph';
import { MGraphFlow } from './mgraph-flow';
export interface DlgShowTypeGraphProps {
items: ITypeInfo[];
items: RO<ITypeInfo[]>;
}
export function DlgShowTypeGraph() {

View File

@ -3,6 +3,7 @@
*/
import { PARAMETER } from '@/utils/constants';
import { type RO } from '@/utils/meta';
import { prepareTooltip } from '@/utils/utils';
import { type IVersionInfo } from '../library/backend/types';
@ -18,7 +19,7 @@ import { type GraphColoring } from './stores/term-graph';
/**
* Generates description for {@link IConstituenta}.
*/
export function describeConstituenta(cst: IConstituenta): string {
export function describeConstituenta(cst: RO<IConstituenta>): string {
if (cst.cst_type === CstType.STRUCTURED) {
return (
cst.term_resolved ||
@ -43,7 +44,7 @@ export function describeConstituenta(cst: IConstituenta): string {
/**
* Generates description for term of a given {@link IConstituenta}.
*/
export function describeConstituentaTerm(cst: IConstituenta | null): string {
export function describeConstituentaTerm(cst: RO<IConstituenta> | null): string {
if (!cst) {
return '!Конституента отсутствует!';
}
@ -57,14 +58,14 @@ export function describeConstituentaTerm(cst: IConstituenta | null): string {
/**
* Generates label for {@link IConstituenta}.
*/
export function labelConstituenta(cst: IConstituenta) {
export function labelConstituenta(cst: RO<IConstituenta>) {
return `${cst.alias}: ${describeConstituenta(cst)}`;
}
/**
* Generates label for {@link IVersionInfo} of {@link IRSForm}.
*/
export function labelVersion(value: CurrentVersion, items: IVersionInfo[]) {
export function labelVersion(value: CurrentVersion, items: RO<IVersionInfo[]>) {
const version = items.find(ver => ver.id === value);
return version ? version.version : 'актуальная';
}
@ -345,7 +346,7 @@ export function labelTypification({
}: {
isValid: boolean;
resultType: string;
args: IArgumentInfo[];
args: RO<IArgumentInfo[]>;
}): string {
if (!isValid) {
return 'N/A';
@ -363,7 +364,7 @@ export function labelTypification({
/**
* Generates label for {@link IConstituenta} typification.
*/
export function labelCstTypification(cst: IConstituenta): string {
export function labelCstTypification(cst: RO<IConstituenta>): string {
return labelTypification({
isValid: cst.parse.status === ParsingStatus.VERIFIED,
resultType: cst.parse.typification,
@ -374,7 +375,7 @@ export function labelCstTypification(cst: IConstituenta): string {
/**
* Generates label for {@link ISyntaxTreeNode}.
*/
export function labelSyntaxTree(node: ISyntaxTreeNode): string {
export function labelSyntaxTree(node: RO<ISyntaxTreeNode>): string {
// prettier-ignore
switch (node.typeID) {
case TokenID.ID_LOCAL:
@ -531,7 +532,7 @@ export function labelGrammeme(gram: Grammeme): string {
/**
* Generates error description for {@link IRSErrorDescription}.
*/
export function describeRSError(error: IRSErrorDescription): string {
export function describeRSError(error: RO<IRSErrorDescription>): string {
// prettier-ignore
switch (error.errorType) {
case RSErrorType.unknownSymbol:

View File

@ -4,6 +4,7 @@
import { BASIC_SCHEMAS, type ILibraryItem } from '@/features/library';
import { type RO } from '@/utils/meta';
import { TextMatcher } from '@/utils/utils';
import { CstType, ParsingStatus, ValueClass } from '../backend/types';
@ -18,7 +19,7 @@ import { CATEGORY_CST_TYPE, CstClass, ExpressionStatus, type IConstituenta, type
* @param query - The query string used for matching.
* @param mode - The matching mode to determine which properties to include in the matching process.
*/
export function matchConstituenta(target: IConstituenta, query: string, mode: CstMatchMode): boolean {
export function matchConstituenta(target: RO<IConstituenta>, query: string, mode: CstMatchMode): boolean {
const matcher = new TextMatcher(query);
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.NAME) && matcher.test(target.alias)) {
return true;
@ -214,7 +215,7 @@ export function isFunctional(type: CstType): boolean {
/**
* Evaluate if {@link IConstituenta} can be used produce structure.
*/
export function canProduceStructure(cst: IConstituenta): boolean {
export function canProduceStructure(cst: RO<IConstituenta>): boolean {
return !!cst.parse.typification && cst.cst_type !== CstType.BASE && cst.cst_type !== CstType.CONSTANT;
}
@ -271,7 +272,7 @@ export function generateAlias(type: CstType, schema: IRSForm, takenAliases: stri
/**
* Sorts library items relevant for InlineSynthesis with specified {@link IRSForm}.
*/
export function sortItemsForInlineSynthesis(receiver: IRSForm, items: ILibraryItem[]): ILibraryItem[] {
export function sortItemsForInlineSynthesis(receiver: IRSForm, items: readonly ILibraryItem[]): ILibraryItem[] {
const result = items.filter(item => item.location === receiver.location);
for (const item of items) {
if (item.visible && item.owner === item.owner && !result.includes(item)) {

View File

@ -6,6 +6,7 @@ import { type Tree } from '@lezer/common';
import { cursorNode } from '@/utils/codemirror';
import { PARAMETER } from '@/utils/constants';
import { type RO } from '@/utils/meta';
import { CstType, type IRSErrorDescription, type RSErrorType } from '../backend/types';
import { labelCstTypification } from '../labels';
@ -36,7 +37,7 @@ export function isSetTypification(text: string): boolean {
}
/** Infers type of constituent for a given template and arguments. */
export function inferTemplatedType(templateType: CstType, args: IArgumentValue[]): CstType {
export function inferTemplatedType(templateType: CstType, args: RO<IArgumentValue[]>): CstType {
if (args.length === 0 || args.some(arg => !arg.value)) {
return templateType;
} else if (templateType === CstType.PREDICATE) {
@ -92,7 +93,7 @@ export function splitTemplateDefinition(target: string) {
* It replaces template argument placeholders in the expression with their corresponding values
* from the provided arguments.
*/
export function substituteTemplateArgs(expression: string, args: IArgumentValue[]): string {
export function substituteTemplateArgs(expression: string, args: RO<IArgumentValue[]>): string {
if (args.every(arg => !arg.value)) {
return expression;
}
@ -121,7 +122,7 @@ export function substituteTemplateArgs(expression: string, args: IArgumentValue[
/**
* Generate ErrorID label.
*/
export function getRSErrorPrefix(error: IRSErrorDescription): string {
export function getRSErrorPrefix(error: RO<IRSErrorDescription>): string {
const id = error.errorType.toString(16);
// prettier-ignore
switch(inferErrorClass(error.errorType)) {
@ -133,12 +134,12 @@ export function getRSErrorPrefix(error: IRSErrorDescription): string {
}
/** Apply alias mapping. */
export function applyAliasMapping(target: string, mapping: AliasMapping): string {
export function applyAliasMapping(target: string, mapping: RO<AliasMapping>): string {
return applyPattern(target, mapping, GLOBALS_REGEXP);
}
/** Apply alias typification mapping. */
export function applyTypificationMapping(target: string, mapping: AliasMapping): string {
export function applyTypificationMapping(target: string, mapping: RO<AliasMapping>): string {
const modified = applyAliasMapping(target, mapping);
if (modified === target) {
return target;

View File

@ -3,6 +3,7 @@
*/
import { PARAMETER } from '@/utils/constants';
import { type RO } from '@/utils/meta';
import { type IArgumentInfo } from './rslang';
@ -35,7 +36,7 @@ export class TypificationGraph {
* @param result - typification of the formal definition.
* @param args - arguments for term or predicate function.
*/
addConstituenta(alias: string, result: string, args: IArgumentInfo[]): void {
addConstituenta(alias: string, result: string, args: RO<IArgumentInfo[]>): void {
const argsNode = this.processArguments(args);
const resultNode = this.processResult(result);
const combinedNode = this.combineResults(resultNode, argsNode);
@ -122,7 +123,7 @@ export class TypificationGraph {
this.nodeByAlias.set(alias, nodeToAnnotate);
}
private processArguments(args: IArgumentInfo[]): TypificationGraphNode | null {
private processArguments(args: RO<IArgumentInfo[]>): TypificationGraphNode | null {
if (args.length === 0) {
return null;
}

View File

@ -13,6 +13,7 @@ import { Indicator } from '@/components/view';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { errorMsg, tooltipText } from '@/utils/labels';
import { type RO } from '@/utils/meta';
import { promptUnsaved } from '@/utils/utils';
import {
@ -68,7 +69,7 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
}
});
const [forceComment, setForceComment] = useState(false);
const [localParse, setLocalParse] = useState<IExpressionParseDTO | null>(null);
const [localParse, setLocalParse] = useState<RO<IExpressionParseDTO> | null>(null);
const typification = useMemo(
() =>

View File

@ -8,6 +8,7 @@ import { useResetOnChange } from '@/hooks/use-reset-on-change';
import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences';
import { errorMsg } from '@/utils/labels';
import { type RO } from '@/utils/meta';
import {
type ICheckConstituentaDTO,
@ -42,7 +43,7 @@ interface EditorRSExpressionProps {
disabled?: boolean;
toggleReset?: boolean;
onChangeLocalParse: (typification: IExpressionParseDTO) => void;
onChangeLocalParse: (typification: RO<IExpressionParseDTO>) => void;
onOpenEdit: (cstID: number) => void;
onShowTypeGraph: (event: React.MouseEvent<Element>) => void;
}
@ -62,7 +63,7 @@ export function EditorRSExpression({
const [isModified, setIsModified] = useState(false);
const rsInput = useRef<ReactCodeMirrorRef>(null);
const [parseData, setParseData] = useState<IExpressionParseDTO | null>(null);
const [parseData, setParseData] = useState<RO<IExpressionParseDTO> | null>(null);
const isProcessing = useMutatingRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls);
@ -78,7 +79,7 @@ export function EditorRSExpression({
function checkConstituenta(
expression: string,
activeCst: IConstituenta,
onSuccess?: (data: IExpressionParseDTO) => void
onSuccess?: (data: RO<IExpressionParseDTO>) => void
) {
const data: ICheckConstituentaDTO = {
definition_formal: expression,
@ -96,7 +97,7 @@ export function EditorRSExpression({
setIsModified(newValue !== activeCst.definition_formal);
}
function handleCheckExpression(callback?: (parse: IExpressionParseDTO) => void) {
function handleCheckExpression(callback?: (parse: RO<IExpressionParseDTO>) => void) {
checkConstituenta(value, activeCst, parse => {
onChangeLocalParse(parse);
if (parse.errors.length > 0) {
@ -109,7 +110,7 @@ export function EditorRSExpression({
});
}
function onShowError(error: IRSErrorDescription, prefixLen: number) {
function onShowError(error: RO<IRSErrorDescription>, prefixLen: number) {
if (!rsInput.current) {
return;
}

View File

@ -1,14 +1,16 @@
import clsx from 'clsx';
import { type RO } from '@/utils/meta';
import { type IExpressionParseDTO, type IRSErrorDescription } from '../../../backend/types';
import { describeRSError } from '../../../labels';
import { getRSErrorPrefix } from '../../../models/rslang-api';
interface ParsingResultProps {
data: IExpressionParseDTO | null;
data: RO<IExpressionParseDTO> | null;
disabled?: boolean;
isOpen: boolean;
onShowError: (error: IRSErrorDescription) => void;
onShowError: (error: RO<IRSErrorDescription>) => void;
}
export function ParsingResult({ isOpen, data, disabled, onShowError }: ParsingResultProps) {

View File

@ -8,6 +8,7 @@ import { BadgeHelp } from '@/features/help/components';
import { Loader } from '@/components/loader';
import { APP_COLORS } from '@/styling/colors';
import { globalIDs } from '@/utils/constants';
import { type RO } from '@/utils/meta';
import { prepareTooltip } from '@/utils/utils';
import { type IExpressionParseDTO, ParsingStatus } from '../../../backend/types';
@ -21,7 +22,7 @@ interface StatusBarProps {
className?: string;
processing: boolean;
isModified: boolean;
parseData: IExpressionParseDTO | null;
parseData: RO<IExpressionParseDTO> | null;
activeCst: IConstituenta;
onAnalyze: () => void;
}

View File

@ -81,7 +81,7 @@ export function MenuEditSchema() {
cstID: targetCst.id
}).then(cstList => {
if (cstList.length !== 0) {
setSelected(cstList);
setSelected([...cstList]);
}
});
}

View File

@ -26,6 +26,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { infoMsg, tooltipText } from '@/utils/labels';
import { type RO } from '@/utils/meta';
import { generatePageQR, promptUnsaved, sharePage } from '@/utils/utils';
import { useDownloadRSForm } from '../../backend/use-download-rsform';
@ -78,9 +79,9 @@ export function MenuMain() {
void download({
itemID: schema.id,
version: schema.version === 'latest' ? undefined : schema.version
}).then((data: Blob) => {
}).then((data: RO<Blob>) => {
try {
fileDownload(data, fileName);
fileDownload(data as Blob, fileName);
} catch (error) {
console.error(error);
}

View File

@ -14,6 +14,7 @@ import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences';
import { PARAMETER, prefixes } from '@/utils/constants';
import { promptText } from '@/utils/labels';
import { type RO } from '@/utils/meta';
import { promptUnsaved } from '@/utils/utils';
import { CstType, type IConstituentaBasicsDTO, type ICreateConstituentaDTO } from '../../backend/types';
@ -136,7 +137,7 @@ export const RSEditState = ({
});
}
function onCreateCst(newCst: IConstituentaBasicsDTO) {
function onCreateCst(newCst: RO<IConstituentaBasicsDTO>) {
setSelected([newCst.id]);
navigateRSForm({ tab: activeTab, activeID: newCst.id });
if (activeTab === RSTabID.CST_LIST) {

View File

@ -0,0 +1,83 @@
/**
* Module: Generic high order utility functions.
*/
// List of disallowed object types (non-plain)
// ALLOW: Date | RegExp | ArrayBuffer | DataView
type DisallowedObjects =
| Map<unknown, unknown>
| Set<unknown>
| WeakMap<object, unknown>
| WeakSet<object>
| Promise<unknown>;
// Detects any disallowed object type directly
type IsDisallowedObject<T> = T extends DisallowedObjects ? true : false;
// Detects if any property in T is a function
type HasFunctionProps<T> = {
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? true : false;
}[keyof T] extends true
? true
: false;
// Detects if any property in T is a disallowed object
type HasDisallowedProps<T> = {
[K in keyof T]: IsDisallowedObject<T[K]>;
}[keyof T] extends true
? true
: false;
// Detects if T is a class instance (has constructor)
type IsClassInstance<T> = T extends object
? T extends { constructor: new (...args: unknown[]) => unknown }
? true
: false
: false;
// The final check — should the object be rejected?
type IsInvalid<T> = T extends object
? HasFunctionProps<T> extends true
? true
: HasDisallowedProps<T> extends true
? true
: IsClassInstance<T> extends true
? true
: false
: false;
/**
* Apply readonly modifier to all properties of a SIMPLE object recursively.
* only works for arrays, dictionaries and primitives.
*/
export type RO<T> = IsInvalid<T> extends true
? never
: T extends readonly unknown[]
? readonly RO<T[number]>[]
: T extends object
? { readonly [K in keyof T]: RO<T[K]> }
: T;
/**
* Freeze an object.
*/
export function deepFreeze<T>(obj: T): RO<T> {
// Ensure the input object is not null or undefined
if (obj === null || obj === undefined) {
throw new Error('Cannot freeze null or undefined');
}
// Freeze the current object
Object.freeze(obj);
// Freeze properties recursively
Object.getOwnPropertyNames(obj).forEach(prop => {
const value = (obj as Record<string, unknown>)[prop];
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value);
}
});
// Return the frozen object
return obj as RO<T>;
}

View File

@ -128,7 +128,7 @@ export function extractErrorMessage(error: Error | AxiosError): string {
/**
* Convert array of objects to CSV Blob.
*/
export function convertToCSV(targetObj: object[]): Blob {
export function convertToCSV(targetObj: readonly object[]): Blob {
if (!targetObj || targetObj.length === 0) {
return new Blob([], { type: 'text/csv;charset=utf-8;' });
}