R: Distinguish undefined semantics vs null

This commit is contained in:
Ivan 2025-02-19 22:33:09 +03:00
parent 9ef4d32376
commit 53b746f3c4
66 changed files with 237 additions and 269 deletions

View File

@ -31,7 +31,7 @@ const DlgUploadRSForm = React.lazy(() => import('@/features/rsform/dialogs/DlgUp
export const GlobalDialogs = () => { export const GlobalDialogs = () => {
const active = useDialogsStore(state => state.active); const active = useDialogsStore(state => state.active);
if (active === undefined) { if (active === null) {
return null; return null;
} }
switch (active) { switch (active) {

View File

@ -32,8 +32,6 @@ axiosInstance.interceptors.request.use(config => {
}); });
// ================ Data transfer types ================ // ================ Data transfer types ================
export type DataCallback<ResponseData = undefined> = (data: ResponseData) => void;
export interface IFrontRequest<RequestData, ResponseData> { export interface IFrontRequest<RequestData, ResponseData> {
data?: RequestData; data?: RequestData;
successMessage?: string | ((data: ResponseData) => string); successMessage?: string | ((data: ResponseData) => string);

View File

@ -5,10 +5,10 @@ import { KEYS } from './configuration';
export const useMutationErrors = () => { export const useMutationErrors = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ignored, setIgnored] = useState<(Error | null)[]>([]); const [ignored, setIgnored] = useState<Error[]>([]);
const mutationErrors = useMutationState({ const mutationErrors = useMutationState({
filters: { mutationKey: [KEYS.global_mutation], status: 'error' }, filters: { mutationKey: [KEYS.global_mutation], status: 'error' },
select: mutation => mutation.state.error select: mutation => mutation.state.error!
}); });
console.log(queryClient.getMutationCache().getAll()); console.log(queryClient.getMutationCache().getAll());

View File

@ -141,7 +141,7 @@ function DataTable<TData extends RowData>({
...restProps ...restProps
}: DataTableProps<TData>) { }: DataTableProps<TData>) {
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []); const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
const [lastSelected, setLastSelected] = useState<string | undefined>(undefined); const [lastSelected, setLastSelected] = useState<string | null>(null);
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
@ -198,7 +198,7 @@ function DataTable<TData extends RowData>({
enableRowSelection={enableRowSelection} enableRowSelection={enableRowSelection}
enableSorting={enableSorting} enableSorting={enableSorting}
headPosition={headPosition} headPosition={headPosition}
resetLastSelected={() => setLastSelected(undefined)} resetLastSelected={() => setLastSelected(null)}
/> />
) : null} ) : null}

View File

@ -6,7 +6,7 @@ import { Checkbox } from '../Input';
interface SelectRowProps<TData> { interface SelectRowProps<TData> {
row: Row<TData>; row: Row<TData>;
onChangeLastSelected: (newValue: string | undefined) => void; onChangeLastSelected: (newValue: string) => void;
} }
function SelectRow<TData>({ row, onChangeLastSelected }: SelectRowProps<TData>) { function SelectRow<TData>({ row, onChangeLastSelected }: SelectRowProps<TData>) {

View File

@ -15,8 +15,8 @@ interface TableBodyProps<TData> {
enableRowSelection?: boolean; enableRowSelection?: boolean;
conditionalRowStyles?: IConditionalStyle<TData>[]; conditionalRowStyles?: IConditionalStyle<TData>[];
lastSelected: string | undefined; lastSelected: string | null;
onChangeLastSelected: (newValue: string | undefined) => void; onChangeLastSelected: (newValue: string | null) => void;
onRowClicked?: (rowData: TData, event: CProps.EventMouse) => void; onRowClicked?: (rowData: TData, event: CProps.EventMouse) => void;
onRowDoubleClicked?: (rowData: TData, event: CProps.EventMouse) => void; onRowDoubleClicked?: (rowData: TData, event: CProps.EventMouse) => void;
@ -49,7 +49,7 @@ function TableBody<TData>({
newSelection[row.id] = !target.getIsSelected(); newSelection[row.id] = !target.getIsSelected();
}); });
table.setRowSelection(prev => ({ ...prev, ...newSelection })); table.setRowSelection(prev => ({ ...prev, ...newSelection }));
onChangeLastSelected(undefined); onChangeLastSelected(null);
} else { } else {
onChangeLastSelected(target.id); onChangeLastSelected(target.id);
target.toggleSelected(!target.getIsSelected()); target.toggleSelected(!target.getIsSelected());
@ -94,7 +94,7 @@ function TableBody<TData>({
width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : 'auto' width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : 'auto'
}} }}
onClick={event => handleRowClicked(row, event)} onClick={event => handleRowClicked(row, event)}
onDoubleClick={event => (onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined)} onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</td> </td>

View File

@ -6,7 +6,7 @@ import { isResponseHtml } from '@/utils/utils';
import { PrettyJson } from './View'; import { PrettyJson } from './View';
export type ErrorData = string | Error | AxiosError | ZodError | undefined | null; export type ErrorData = string | Error | AxiosError | ZodError;
interface InfoErrorProps { interface InfoErrorProps {
error: ErrorData; error: ErrorData;

View File

@ -53,7 +53,7 @@ export function SearchBar({
className={clsx('outline-none bg-transparent', !noIcon && 'pl-10')} className={clsx('outline-none bg-transparent', !noIcon && 'pl-10')}
noBorder={noBorder} noBorder={noBorder}
value={query} value={query}
onChange={event => (onChangeQuery ? onChangeQuery(event.target.value) : undefined)} onChange={event => onChangeQuery?.(event.target.value)}
/> />
</div> </div>
); );

View File

@ -86,7 +86,7 @@ export const schemaCloneLibraryItem = schemaLibraryItem
alias: z.string().nonempty(errorMsg.requiredField), alias: z.string().nonempty(errorMsg.requiredField),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }), location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
items: z.array(z.number()).optional() items: z.array(z.number())
}); });
export const schemaCreateLibraryItem = z export const schemaCreateLibraryItem = z
@ -138,5 +138,5 @@ export const schemaVersionUpdate = z.object({
export const schemaVersionCreate = z.object({ export const schemaVersionCreate = z.object({
version: z.string(), version: z.string(),
description: z.string(), description: z.string(),
items: z.array(z.number()).optional() items: z.array(z.number())
}); });

View File

@ -16,32 +16,29 @@ export function useApplyLibraryFilter(filter: ILibraryFilter) {
if (filter.folderMode && filter.location) { if (filter.folderMode && filter.location) {
if (filter.subfolders) { if (filter.subfolders) {
result = result.filter( result = result.filter(
item => item.location == filter.location || item.location.startsWith(filter.location! + '/') item => item.location == filter.location || item.location.startsWith(filter.location + '/')
); );
} else { } else {
result = result.filter(item => item.location == filter.location); result = result.filter(item => item.location == filter.location);
} }
} }
if (filter.type) { if (filter.isVisible !== null) {
result = result.filter(item => item.item_type === filter.type);
}
if (filter.isVisible !== undefined) {
result = result.filter(item => filter.isVisible === item.visible); result = result.filter(item => filter.isVisible === item.visible);
} }
if (filter.isOwned !== undefined) { if (filter.isOwned !== null) {
result = result.filter(item => filter.isOwned === (item.owner === user.id)); result = result.filter(item => filter.isOwned === (item.owner === user.id));
} }
if (filter.isEditor !== undefined) { if (filter.isEditor !== null) {
result = result.filter(item => filter.isEditor == user.editor.includes(item.id)); result = result.filter(item => filter.isEditor == user.editor.includes(item.id));
} }
if (filter.filterUser !== undefined) { if (filter.filterUser !== null) {
result = result.filter(item => filter.filterUser === item.owner); result = result.filter(item => filter.filterUser === item.owner);
} }
if (!filter.folderMode && filter.path) { if (!filter.folderMode && !!filter.path) {
result = result.filter(item => matchLibraryItemLocation(item, filter.path!)); result = result.filter(item => matchLibraryItemLocation(item, filter.path));
} }
if (filter.query) { if (filter.query) {
result = result.filter(item => matchLibraryItem(item, filter.query!)); result = result.filter(item => matchLibraryItem(item, filter.query));
} }
return { filtered: result }; return { filtered: result };

View File

@ -116,7 +116,7 @@ export function EditorLibraryItem({ controller }: EditorLibraryItemProps) {
{ownerSelector.isOpen ? ( {ownerSelector.isOpen ? (
<SelectUser <SelectUser
className='w-[25rem] sm:w-[26rem] text-sm' className='w-[25rem] sm:w-[26rem] text-sm'
value={controller.schema.owner ?? undefined} value={controller.schema.owner}
onChange={onSelectUser} onChange={onSelectUser}
/> />
) : null} ) : null}

View File

@ -9,10 +9,10 @@ import { ILibraryItem } from '../backend/types';
import { matchLibraryItem } from '../models/libraryAPI'; import { matchLibraryItem } from '../models/libraryAPI';
interface SelectLibraryItemProps extends CProps.Styling { interface SelectLibraryItemProps extends CProps.Styling {
items?: ILibraryItem[]; value: ILibraryItem | null;
value?: ILibraryItem; onChange: (newValue: ILibraryItem | null) => void;
onChange: (newValue?: ILibraryItem) => void;
items?: ILibraryItem[];
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
} }
@ -41,7 +41,7 @@ export function SelectLibraryItem({
className={clsx('text-ellipsis', className)} className={clsx('text-ellipsis', className)}
options={options} options={options}
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null} value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null}
onChange={data => onChange(items?.find(cst => cst.id === data?.value))} onChange={data => onChange(items?.find(cst => cst.id === data?.value) ?? null)}
filterOption={filter} filterOption={filter}
placeholder={placeholder} placeholder={placeholder}
{...restProps} {...restProps}

View File

@ -10,10 +10,10 @@ import { IVersionInfo } from '../backend/types';
interface SelectVersionProps extends CProps.Styling { interface SelectVersionProps extends CProps.Styling {
id?: string; id?: string;
items?: IVersionInfo[]; value: number | undefined;
value?: number; onChange: (newValue: number | undefined) => void;
onChange: (newValue?: number) => void;
items?: IVersionInfo[];
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
} }

View File

@ -53,7 +53,7 @@ function DlgCloneLibraryItem() {
read_only: false, read_only: false,
access_policy: AccessPolicy.PUBLIC, access_policy: AccessPolicy.PUBLIC,
location: initialLocation, location: initialLocation,
items: undefined items: []
}, },
mode: 'onChange', mode: 'onChange',
reValidateMode: 'onChange' reValidateMode: 'onChange'
@ -165,8 +165,8 @@ function DlgCloneLibraryItem() {
<Checkbox <Checkbox
id='dlg_only_selected' id='dlg_only_selected'
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`} label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
value={field.value !== undefined} value={field.value.length > 0}
onChange={value => field.onChange(value ? selected : undefined)} onChange={value => field.onChange(value ? selected : [])}
/> />
)} )}
/> />

View File

@ -32,7 +32,7 @@ function DlgCreateVersion() {
defaultValues: { defaultValues: {
version: versions.length > 0 ? nextVersion(versions[versions.length - 1].version) : '1.0.0', version: versions.length > 0 ? nextVersion(versions[versions.length - 1].version) : '1.0.0',
description: '', description: '',
items: undefined items: []
} }
}); });
const version = useWatch({ control, name: 'version' }); const version = useWatch({ control, name: 'version' });
@ -61,8 +61,8 @@ function DlgCreateVersion() {
<Checkbox <Checkbox
id='dlg_only_selected' id='dlg_only_selected'
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`} label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
value={field.value !== undefined} value={field.value.length > 0}
onChange={value => field.onChange(value ? selected : undefined)} onChange={value => field.onChange(value ? selected : [])}
/> />
)} )}
/> />

View File

@ -61,8 +61,8 @@ function DlgEditEditors() {
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<Label text='Добавить' /> <Label text='Добавить' />
<SelectUser <SelectUser
filter={id => !selected.includes(id)} filter={id => !selected.includes(id)} //
value={undefined} value={null}
onChange={onAddEditor} onChange={onAddEditor}
className='w-[25rem]' className='w-[25rem]'
/> />

View File

@ -9,12 +9,12 @@ export class FolderNode {
rank: number = 0; rank: number = 0;
text: string; text: string;
children: Map<string, FolderNode>; children: Map<string, FolderNode>;
parent: FolderNode | undefined; parent: FolderNode | null;
filesInside: number = 0; filesInside: number = 0;
filesTotal: number = 0; filesTotal: number = 0;
constructor(text: string, parent?: FolderNode) { constructor(text: string, parent: FolderNode | null = null) {
this.text = text; this.text = text;
this.parent = parent; this.parent = parent;
this.children = new Map(); this.children = new Map();
@ -126,7 +126,7 @@ export class FolderTree {
} }
private addNode(text: string, parent?: FolderNode): FolderNode { private addNode(text: string, parent?: FolderNode): FolderNode {
if (parent === undefined) { if (!parent) {
const newNode = new FolderNode(text); const newNode = new FolderNode(text);
this.roots.set(text, newNode); this.roots.set(text, newNode);
return newNode; return newNode;

View File

@ -2,8 +2,6 @@
* Module: Models for LibraryItem. * Module: Models for LibraryItem.
*/ */
import { LibraryItemType } from '../backend/types';
/** /**
* Represents valid location headers. * Represents valid location headers.
*/ */
@ -28,17 +26,16 @@ export interface ILibraryItemReference {
* Represents Library filter parameters. * Represents Library filter parameters.
*/ */
export interface ILibraryFilter { export interface ILibraryFilter {
type?: LibraryItemType; query: string;
query?: string;
folderMode?: boolean; folderMode: boolean;
subfolders?: boolean; subfolders: boolean;
path?: string; path: string;
head?: LocationHead; head: LocationHead | null;
location?: string; location: string;
isVisible?: boolean; isVisible: boolean | null;
isOwned?: boolean; isOwned: boolean | null;
isEditor?: boolean; isEditor: boolean | null;
filterUser?: number; filterUser: number | null;
} }

View File

@ -54,9 +54,9 @@ function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
const resetFilter = useLibrarySearchStore(state => state.resetFilter); const resetFilter = useLibrarySearchStore(state => state.resetFilter);
const hasCustomFilter = useHasCustomFilter(); const hasCustomFilter = useHasCustomFilter();
const userActive = isOwned !== undefined || isEditor !== undefined || filterUser !== undefined; const userActive = isOwned !== null || isEditor !== null || filterUser !== null;
function handleChange(newValue: LocationHead | undefined) { function handleChange(newValue: LocationHead | null) {
headMenu.hide(); headMenu.hide();
setHead(newValue); setHead(newValue);
} }
@ -173,7 +173,7 @@ function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
<span>проводник...</span> <span>проводник...</span>
</div> </div>
</DropdownButton> </DropdownButton>
<DropdownButton className='w-[10rem]' onClick={() => handleChange(undefined)}> <DropdownButton className='w-[10rem]' onClick={() => handleChange(null)}>
<div className='inline-flex items-center gap-3'> <div className='inline-flex items-center gap-3'>
<IconFolder size='1rem' className='clr-text-controls' /> <IconFolder size='1rem' className='clr-text-controls' />
<span>отображать все</span> <span>отображать все</span>

View File

@ -21,20 +21,20 @@ interface LibrarySearchStore {
location: string; location: string;
setLocation: (value: string) => void; setLocation: (value: string) => void;
head: LocationHead | undefined; head: LocationHead | null;
setHead: (value: LocationHead | undefined) => void; setHead: (value: LocationHead | null) => void;
isVisible: boolean | undefined; isVisible: boolean | null;
toggleVisible: () => void; toggleVisible: () => void;
isOwned: boolean | undefined; isOwned: boolean | null;
toggleOwned: () => void; toggleOwned: () => void;
isEditor: boolean | undefined; isEditor: boolean | null;
toggleEditor: () => void; toggleEditor: () => void;
filterUser: number | undefined; filterUser: number | null;
setFilterUser: (value: number | undefined) => void; setFilterUser: (value: number | null) => void;
resetFilter: () => void; resetFilter: () => void;
} }
@ -57,19 +57,19 @@ export const useLibrarySearchStore = create<LibrarySearchStore>()(
location: '', location: '',
setLocation: value => set(!!value ? { location: value, folderMode: true } : { location: '' }), setLocation: value => set(!!value ? { location: value, folderMode: true } : { location: '' }),
head: undefined, head: null,
setHead: value => set({ head: value }), setHead: value => set({ head: value }),
isVisible: true, isVisible: true,
toggleVisible: () => set(state => ({ isVisible: toggleTristateFlag(state.isVisible) })), toggleVisible: () => set(state => ({ isVisible: toggleTristateFlag(state.isVisible) })),
isOwned: undefined, isOwned: null,
toggleOwned: () => set(state => ({ isOwned: toggleTristateFlag(state.isOwned) })), toggleOwned: () => set(state => ({ isOwned: toggleTristateFlag(state.isOwned) })),
isEditor: undefined, isEditor: null,
toggleEditor: () => set(state => ({ isEditor: toggleTristateFlag(state.isEditor) })), toggleEditor: () => set(state => ({ isEditor: toggleTristateFlag(state.isEditor) })),
filterUser: undefined, filterUser: null,
setFilterUser: value => set({ filterUser: value }), setFilterUser: value => set({ filterUser: value }),
resetFilter: () => resetFilter: () =>
@ -77,11 +77,11 @@ export const useLibrarySearchStore = create<LibrarySearchStore>()(
query: '', query: '',
path: '', path: '',
location: '', location: '',
head: undefined, head: null,
isVisible: true, isVisible: true,
isOwned: undefined, isOwned: null,
isEditor: undefined, isEditor: null,
filterUser: undefined filterUser: null
})) }))
}), }),
{ {
@ -116,11 +116,11 @@ export function useHasCustomFilter(): boolean {
!!path || !!path ||
!!query || !!query ||
!!location || !!location ||
head !== undefined || head !== null ||
isEditor !== undefined || isEditor !== null ||
isOwned !== undefined || isOwned !== null ||
isVisible !== true || isVisible !== true ||
filterUser !== undefined filterUser !== null
); );
} }

View File

@ -11,7 +11,7 @@ import { NoData } from '@/components/View';
import { IOperation } from '../models/oss'; import { IOperation } from '../models/oss';
import SelectOperation from './SelectOperation'; import { SelectOperation } from './SelectOperation';
interface PickMultiOperationProps extends CProps.Styling { interface PickMultiOperationProps extends CProps.Styling {
value: number[]; value: number[];
@ -25,17 +25,17 @@ const columnHelper = createColumnHelper<IOperation>();
export function PickMultiOperation({ rows, items, value, onChange, className, ...restProps }: PickMultiOperationProps) { export function PickMultiOperation({ rows, items, value, onChange, className, ...restProps }: PickMultiOperationProps) {
const selectedItems = value.map(itemID => items.find(item => item.id === itemID)!); const selectedItems = value.map(itemID => items.find(item => item.id === itemID)!);
const nonSelectedItems = items.filter(item => !value.includes(item.id)); const nonSelectedItems = items.filter(item => !value.includes(item.id));
const [lastSelected, setLastSelected] = useState<IOperation | undefined>(undefined); const [lastSelected, setLastSelected] = useState<IOperation | null>(null);
function handleDelete(operation: number) { function handleDelete(operation: number) {
onChange(value.filter(item => item !== operation)); onChange(value.filter(item => item !== operation));
} }
function handleSelect(operation?: IOperation) { function handleSelect(operation: IOperation | null) {
if (operation) { if (operation) {
setLastSelected(operation); setLastSelected(operation);
onChange([...value, operation.id]); onChange([...value, operation.id]);
setTimeout(() => setLastSelected(undefined), 1000); setTimeout(() => setLastSelected(null), 1000);
} }
} }

View File

@ -9,15 +9,15 @@ import { IOperation } from '../models/oss';
import { matchOperation } from '../models/ossAPI'; import { matchOperation } from '../models/ossAPI';
interface SelectOperationProps extends CProps.Styling { interface SelectOperationProps extends CProps.Styling {
items?: IOperation[]; value: IOperation | null;
value?: IOperation; onChange: (newValue: IOperation | null) => void;
onChange: (newValue?: IOperation) => void;
items?: IOperation[];
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
} }
function SelectOperation({ export function SelectOperation({
className, className,
items, items,
value, value,
@ -41,12 +41,10 @@ function SelectOperation({
className={clsx('text-ellipsis', className)} className={clsx('text-ellipsis', className)}
options={options} options={options}
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null} value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null}
onChange={data => onChange(items?.find(cst => cst.id === data?.value))} onChange={data => onChange(items?.find(cst => cst.id === data?.value) ?? null)}
filterOption={filter} filterOption={filter}
placeholder={placeholder} placeholder={placeholder}
{...restProps} {...restProps}
/> />
); );
} }
export default SelectOperation;

View File

@ -69,7 +69,7 @@ function DlgDeleteOperation() {
<Checkbox <Checkbox
label='Удалить схему' label='Удалить схему'
titleHtml={ titleHtml={
!target.is_owned || target.result === undefined !target.is_owned || target.result === null
? 'Привязанную схему нельзя удалить' ? 'Привязанную схему нельзя удалить'
: 'Удалить схему вместе с операцией' : 'Удалить схему вместе с операцией'
} }

View File

@ -54,7 +54,6 @@ function DlgEditOperation() {
}, },
mode: 'onChange' mode: 'onChange'
}); });
const [activeTab, setActiveTab] = useState(TabID.CARD); const [activeTab, setActiveTab] = useState(TabID.CARD);
function onSubmit(data: IOperationUpdateDTO) { function onSubmit(data: IOperationUpdateDTO) {

View File

@ -46,11 +46,11 @@ function DlgRelocateConstituents() {
mode: 'onChange' mode: 'onChange'
}); });
const destination = useWatch({ control, name: 'destination' }); const destination = useWatch({ control, name: 'destination' });
const destinationItem = destination ? libraryItems.find(item => item.id === destination) : undefined; const destinationItem = destination ? libraryItems.find(item => item.id === destination) ?? null : null;
const [directionUp, setDirectionUp] = useState(true); const [directionUp, setDirectionUp] = useState(true);
const [source, setSource] = useState<ILibraryItem | undefined>( const [source, setSource] = useState<ILibraryItem | null>(
libraryItems.find(item => item.id === initialTarget?.result) libraryItems.find(item => item.id === initialTarget?.result) ?? null
); );
const operation = oss.items.find(item => item.result === source?.id); const operation = oss.items.find(item => item.result === source?.id);
@ -80,13 +80,13 @@ function DlgRelocateConstituents() {
setValue('destination', null); setValue('destination', null);
} }
function handleSelectSource(newValue: ILibraryItem | undefined) { function handleSelectSource(newValue: ILibraryItem | null) {
setSource(newValue); setSource(newValue);
setValue('destination', null); setValue('destination', null);
setValue('items', []); setValue('items', []);
} }
function handleSelectDestination(newValue: ILibraryItem | undefined) { function handleSelectDestination(newValue: ILibraryItem | null) {
if (newValue) { if (newValue) {
setValue('destination', newValue.id); setValue('destination', newValue.id);
} else { } else {

View File

@ -19,7 +19,7 @@ interface BadgeConstituentaProps extends CProps.Styling {
/** /**
* Displays a badge with a constituenta alias and information tooltip. * Displays a badge with a constituenta alias and information tooltip.
*/ */
function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstituentaProps) { export function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstituentaProps) {
const setActiveCst = useTooltipsStore(state => state.setActiveCst); const setActiveCst = useTooltipsStore(state => state.setActiveCst);
return ( return (
@ -46,5 +46,3 @@ function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstitue
</div> </div>
); );
} }
export default BadgeConstituenta;

View File

@ -14,12 +14,12 @@ import { IConstituenta } from '../models/rsform';
import { matchConstituenta } from '../models/rsformAPI'; import { matchConstituenta } from '../models/rsformAPI';
import { CstMatchMode } from '../stores/cstSearch'; import { CstMatchMode } from '../stores/cstSearch';
import BadgeConstituenta from './BadgeConstituenta'; import { BadgeConstituenta } from './BadgeConstituenta';
interface PickConstituentaProps extends CProps.Styling { interface PickConstituentaProps extends CProps.Styling {
id?: string; id?: string;
items: IConstituenta[]; items: IConstituenta[];
value?: IConstituenta; value: IConstituenta | null;
onChange: (newValue: IConstituenta) => void; onChange: (newValue: IConstituenta) => void;
rows?: number; rows?: number;

View File

@ -14,7 +14,7 @@ import { IConstituenta, IRSForm } from '../models/rsform';
import { isBasicConcept, matchConstituenta } from '../models/rsformAPI'; import { isBasicConcept, matchConstituenta } from '../models/rsformAPI';
import { CstMatchMode } from '../stores/cstSearch'; import { CstMatchMode } from '../stores/cstSearch';
import BadgeConstituenta from './BadgeConstituenta'; import { BadgeConstituenta } from './BadgeConstituenta';
import ToolbarGraphSelection from './ToolbarGraphSelection'; import ToolbarGraphSelection from './ToolbarGraphSelection';
interface PickMultiConstituentaProps extends CProps.Styling { interface PickMultiConstituentaProps extends CProps.Styling {
@ -112,7 +112,10 @@ export function PickMultiConstituenta({
/> />
<ToolbarGraphSelection <ToolbarGraphSelection
graph={foldedGraph} graph={foldedGraph}
isCore={cstID => isBasicConcept(schema.cstByID.get(cstID)?.cst_type)} isCore={cstID => {
const cst = schema.cstByID.get(cstID);
return !!cst && isBasicConcept(cst.cst_type);
}}
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited} isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
value={value} value={value}
onChange={onChange} onChange={onChange}

View File

@ -17,8 +17,8 @@ import { errorMsg } from '@/utils/labels';
import { ICstSubstitute } from '../backend/types'; import { ICstSubstitute } from '../backend/types';
import { IConstituenta, IRSForm } from '../models/rsform'; import { IConstituenta, IRSForm } from '../models/rsform';
import BadgeConstituenta from './BadgeConstituenta'; import { BadgeConstituenta } from './BadgeConstituenta';
import SelectConstituenta from './SelectConstituenta'; import { SelectConstituenta } from './SelectConstituenta';
interface IMultiSubstitution { interface IMultiSubstitution {
original_source: ILibraryItem; original_source: ILibraryItem;
@ -54,15 +54,13 @@ export function PickSubstitutions({
className, className,
...restProps ...restProps
}: PickSubstitutionsProps) { }: PickSubstitutionsProps) {
const [leftArgument, setLeftArgument] = useState<ILibraryItem | undefined>( const [leftArgument, setLeftArgument] = useState<ILibraryItem | null>(schemas.length === 1 ? schemas[0] : null);
schemas.length === 1 ? schemas[0] : undefined const [rightArgument, setRightArgument] = useState<ILibraryItem | null>(
); schemas.length === 1 && allowSelfSubstitution ? schemas[0] : null
const [rightArgument, setRightArgument] = useState<ILibraryItem | undefined>(
schemas.length === 1 && allowSelfSubstitution ? schemas[0] : undefined
); );
const [leftCst, setLeftCst] = useState<IConstituenta | undefined>(undefined); const [leftCst, setLeftCst] = useState<IConstituenta | null>(null);
const [rightCst, setRightCst] = useState<IConstituenta | undefined>(undefined); const [rightCst, setRightCst] = useState<IConstituenta | null>(null);
const [deleteRight, setDeleteRight] = useState(true); const [deleteRight, setDeleteRight] = useState(true);
const toggleDelete = () => setDeleteRight(prev => !prev); const toggleDelete = () => setDeleteRight(prev => !prev);
@ -135,8 +133,8 @@ export function PickSubstitutions({
} }
} }
onChange([...value, newSubstitution]); onChange([...value, newSubstitution]);
setLeftCst(undefined); setLeftCst(null);
setRightCst(undefined); setRightCst(null);
} }
function handleDeclineSuggestion(item: IMultiSubstitution) { function handleDeclineSuggestion(item: IMultiSubstitution) {

View File

@ -23,7 +23,7 @@ export const tooltipProducer = (schema: IRSForm, canClick?: boolean) => {
} }
if ('entity' in parse.ref) { if ('entity' in parse.ref) {
const cst = schema.cstByAlias.get(parse.ref.entity); const cst = schema.cstByAlias.get(parse.ref.entity) ?? null;
return { return {
pos: parse.start, pos: parse.start,
end: parse.end, end: parse.end,
@ -31,7 +31,7 @@ export const tooltipProducer = (schema: IRSForm, canClick?: boolean) => {
create: () => domTooltipEntityReference(parse.ref as IEntityReference, cst, canClick) create: () => domTooltipEntityReference(parse.ref as IEntityReference, cst, canClick)
}; };
} else { } else {
let masterText: string | undefined = undefined; let masterText: string | null = null;
if (parse.ref.offset > 0) { if (parse.ref.offset > 0) {
const entities = findContainedNodes(parse.end, view.state.doc.length, syntaxTree(view.state), [RefEntity]); const entities = findContainedNodes(parse.end, view.state.doc.length, syntaxTree(view.state), [RefEntity]);
if (parse.ref.offset <= entities.length) { if (parse.ref.offset <= entities.length) {
@ -62,11 +62,7 @@ export function refsHoverTooltip(schema: IRSForm, canClick?: boolean): Extension
/** /**
* Create DOM tooltip for {@link IEntityReference}. * Create DOM tooltip for {@link IEntityReference}.
*/ */
function domTooltipEntityReference( function domTooltipEntityReference(ref: IEntityReference, cst: IConstituenta | null, canClick?: boolean): TooltipView {
ref: IEntityReference,
cst: IConstituenta | undefined,
canClick?: boolean
): TooltipView {
const dom = document.createElement('div'); const dom = document.createElement('div');
dom.className = clsx( dom.className = clsx(
'max-h-[25rem] max-w-[25rem] min-w-[10rem]', 'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
@ -122,7 +118,7 @@ function domTooltipEntityReference(
*/ */
function domTooltipSyntacticReference( function domTooltipSyntacticReference(
ref: ISyntacticReference, ref: ISyntacticReference,
masterRef: string | undefined, masterRef: string | null,
canClick?: boolean canClick?: boolean
): TooltipView { ): TooltipView {
const dom = document.createElement('div'); const dom = document.createElement('div');

View File

@ -11,15 +11,15 @@ import { matchConstituenta } from '../models/rsformAPI';
import { CstMatchMode } from '../stores/cstSearch'; import { CstMatchMode } from '../stores/cstSearch';
interface SelectConstituentaProps extends CProps.Styling { interface SelectConstituentaProps extends CProps.Styling {
value?: IConstituenta; value: IConstituenta | null;
onChange: (newValue?: IConstituenta) => void; onChange: (newValue: IConstituenta | null) => void;
items?: IConstituenta[]; items?: IConstituenta[];
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
} }
function SelectConstituenta({ export function SelectConstituenta({
className, className,
items, items,
value, value,
@ -43,12 +43,10 @@ function SelectConstituenta({
className={clsx('text-ellipsis', className)} className={clsx('text-ellipsis', className)}
options={options} options={options}
value={value ? { value: value.id, label: `${value.alias}: ${describeConstituentaTerm(value)}` } : null} value={value ? { value: value.id, label: `${value.alias}: ${describeConstituentaTerm(value)}` } : null}
onChange={data => onChange(items?.find(cst => cst.id === data?.value))} onChange={data => onChange(items?.find(cst => cst.id === data?.value) ?? null)}
filterOption={filter} filterOption={filter}
placeholder={placeholder} placeholder={placeholder}
{...restProps} {...restProps}
/> />
); );
} }
export default SelectConstituenta;

View File

@ -29,10 +29,8 @@ function TabArguments() {
const { args, onChangeArguments } = useTemplateContext(); const { args, onChangeArguments } = useTemplateContext();
const definition = useWatch({ control, name: 'definition_formal' }); const definition = useWatch({ control, name: 'definition_formal' });
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined); const [selectedCst, setSelectedCst] = useState<IConstituenta | null>(null);
const [selectedArgument, setSelectedArgument] = useState<IArgumentValue | undefined>( const [selectedArgument, setSelectedArgument] = useState<IArgumentValue | null>(args.length > 0 ? args[0] : null);
args.length > 0 ? args[0] : undefined
);
const [argumentValue, setArgumentValue] = useState(''); const [argumentValue, setArgumentValue] = useState('');

View File

@ -23,7 +23,7 @@ function TabTemplate() {
} = useTemplateContext(); } = useTemplateContext();
const { templates } = useTemplatesSuspense(); const { templates } = useTemplatesSuspense();
const { schema: templateSchema } = useRSForm({ itemID: templateID }); const { schema: templateSchema } = useRSForm({ itemID: templateID ?? undefined });
if (!templateID) { if (!templateID) {
onChangeTemplateID(templates[0].id); onChangeTemplateID(templates[0].id);
@ -63,7 +63,7 @@ function TabTemplate() {
className='w-[12rem]' className='w-[12rem]'
options={templateSelector} options={templateSelector}
value={templateID ? { value: templateID, label: templates.find(item => item.id == templateID)!.title } : null} value={templateID ? { value: templateID, label: templates.find(item => item.id == templateID)!.title } : null}
onChange={data => onChangeTemplateID(data ? data.value : undefined)} onChange={data => onChangeTemplateID(data ? data.value : null)}
/> />
<SelectSingle <SelectSingle
noBorder noBorder
@ -79,7 +79,7 @@ function TabTemplate() {
} }
: null : null
} }
onChange={data => onChangeFilterCategory(data ? templateSchema?.cstByID.get(data?.value) : undefined)} onChange={data => onChangeFilterCategory(data ? templateSchema?.cstByID.get(data?.value) ?? null : null)}
isClearable isClearable
/> />
</div> </div>

View File

@ -15,14 +15,14 @@ import { DlgCstTemplateProps } from './DlgCstTemplate';
export interface ITemplateContext { export interface ITemplateContext {
args: IArgumentValue[]; args: IArgumentValue[];
prototype?: IConstituenta; prototype: IConstituenta | null;
templateID?: number; templateID: number | null;
filterCategory?: IConstituenta; filterCategory: IConstituenta | null;
onChangeArguments: (newArgs: IArgumentValue[]) => void; onChangeArguments: (newArgs: IArgumentValue[]) => void;
onChangePrototype: (newPrototype: IConstituenta) => void; onChangePrototype: (newPrototype: IConstituenta) => void;
onChangeTemplateID: (newTemplateID: number | undefined) => void; onChangeTemplateID: (newTemplateID: number | null) => void;
onChangeFilterCategory: (newFilterCategory: IConstituenta | undefined) => void; onChangeFilterCategory: (newFilterCategory: IConstituenta | null) => void;
} }
const TemplateContext = createContext<ITemplateContext | null>(null); const TemplateContext = createContext<ITemplateContext | null>(null);
@ -37,10 +37,10 @@ export const useTemplateContext = () => {
export const TemplateState = ({ children }: React.PropsWithChildren) => { export const TemplateState = ({ children }: React.PropsWithChildren) => {
const { schema } = useDialogsStore(state => state.props as DlgCstTemplateProps); const { schema } = useDialogsStore(state => state.props as DlgCstTemplateProps);
const { setValue } = useFormContext<ICstCreateDTO>(); const { setValue } = useFormContext<ICstCreateDTO>();
const [templateID, setTemplateID] = useState<number | undefined>(undefined); const [templateID, setTemplateID] = useState<number | null>(null);
const [args, setArguments] = useState<IArgumentValue[]>([]); const [args, setArguments] = useState<IArgumentValue[]>([]);
const [prototype, setPrototype] = useState<IConstituenta | undefined>(undefined); const [prototype, setPrototype] = useState<IConstituenta | null>(null);
const [filterCategory, setFilterCategory] = useState<IConstituenta | undefined>(undefined); const [filterCategory, setFilterCategory] = useState<IConstituenta | null>(null);
function onChangeArguments(newArgs: IArgumentValue[]) { function onChangeArguments(newArgs: IArgumentValue[]) {
setArguments(newArgs); setArguments(newArgs);
@ -70,9 +70,9 @@ export const TemplateState = ({ children }: React.PropsWithChildren) => {
setValue('definition_raw', newPrototype.definition_raw); setValue('definition_raw', newPrototype.definition_raw);
} }
function onChangeTemplateID(newTemplateID: number | undefined) { function onChangeTemplateID(newTemplateID: number | null) {
setTemplateID(newTemplateID); setTemplateID(newTemplateID);
setPrototype(undefined); setPrototype(null);
setArguments([]); setArguments([]);
} }

View File

@ -19,7 +19,7 @@ export function TabEntityReference() {
const { setValue, control, register } = useFormContext<IEditReferenceState>(); const { setValue, control, register } = useFormContext<IEditReferenceState>();
const alias = useWatch({ control, name: 'entity.entity' }); const alias = useWatch({ control, name: 'entity.entity' });
const selectedCst = schema.cstByAlias.get(alias); const selectedCst = schema.cstByAlias.get(alias) ?? null;
const term = selectedCst?.term_resolved ?? ''; const term = selectedCst?.term_resolved ?? '';
function handleSelectConstituenta(cst: IConstituenta) { function handleSelectConstituenta(cst: IConstituenta) {

View File

@ -20,7 +20,7 @@ export interface DlgShowASTProps {
function DlgShowAST() { function DlgShowAST() {
const { syntaxTree, expression } = useDialogsStore(state => state.props as DlgShowASTProps); const { syntaxTree, expression } = useDialogsStore(state => state.props as DlgShowASTProps);
const [hoverID, setHoverID] = useState<number | undefined>(undefined); const [hoverID, setHoverID] = useState<number | null>(null);
const hoverNode = syntaxTree.find(node => node.uid === hoverID); const hoverNode = syntaxTree.find(node => node.uid === hoverID);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
@ -47,7 +47,7 @@ function DlgShowAST() {
<ASTFlow <ASTFlow
data={syntaxTree} data={syntaxTree}
onNodeEnter={node => setHoverID(Number(node.id))} onNodeEnter={node => setHoverID(Number(node.id))}
onNodeLeave={() => setHoverID(undefined)} onNodeLeave={() => setHoverID(null)}
onChangeDragging={setIsDragging} onChangeDragging={setIsDragging}
/> />
</ReactFlowProvider> </ReactFlowProvider>

View File

@ -11,8 +11,7 @@ export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
rankdir: 'TB', rankdir: 'TB',
ranksep: 40, ranksep: 40,
nodesep: 40, nodesep: 40,
ranker: 'network-simplex', ranker: 'network-simplex'
align: undefined
}); });
nodes.forEach(node => { nodes.forEach(node => {
dagreGraph.setNode(node.id, { width: 2 * PARAMETER.graphNodeRadius, height: 2 * PARAMETER.graphNodeRadius }); dagreGraph.setNode(node.id, { width: 2 * PARAMETER.graphNodeRadius, height: 2 * PARAMETER.graphNodeRadius });

View File

@ -17,8 +17,7 @@ export function applyLayout(nodes: Node<TMGraphNode>[], edges: Edge[]) {
rankdir: 'BT', rankdir: 'BT',
ranksep: VERT_SEPARATION, ranksep: VERT_SEPARATION,
nodesep: HOR_SEPARATION, nodesep: HOR_SEPARATION,
ranker: 'network-simplex', ranker: 'network-simplex'
align: undefined
}); });
nodes.forEach(node => { nodes.forEach(node => {
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT }); dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });

View File

@ -17,7 +17,7 @@ function DlgUploadRSForm() {
const { itemID } = useDialogsStore(state => state.props as DlgUploadRSFormProps); const { itemID } = useDialogsStore(state => state.props as DlgUploadRSFormProps);
const { upload } = useUploadTRS(); const { upload } = useUploadTRS();
const [loadMetadata, setLoadMetadata] = useState(false); const [loadMetadata, setLoadMetadata] = useState(false);
const [file, setFile] = useState<File | undefined>(); const [file, setFile] = useState<File | null>(null);
const handleSubmit = () => { const handleSubmit = () => {
if (file) { if (file) {
@ -34,7 +34,7 @@ function DlgUploadRSForm() {
if (event.target.files && event.target.files.length > 0) { if (event.target.files && event.target.files.length > 0) {
setFile(event.target.files[0]); setFile(event.target.files[0]);
} else { } else {
setFile(undefined); setFile(null);
} }
}; };

View File

@ -40,7 +40,7 @@ export function describeConstituenta(cst: IConstituenta): string {
/** /**
* Generates description for term of a given {@link IConstituenta}. * Generates description for term of a given {@link IConstituenta}.
*/ */
export function describeConstituentaTerm(cst?: IConstituenta): string { export function describeConstituentaTerm(cst: IConstituenta | null): string {
if (!cst) { if (!cst) {
return '!Конституента отсутствует!'; return '!Конституента отсутствует!';
} }
@ -61,7 +61,7 @@ export function labelConstituenta(cst: IConstituenta) {
/** /**
* Generates label for {@link IVersionInfo} of {@link IRSForm}. * Generates label for {@link IVersionInfo} of {@link IRSForm}.
*/ */
export function labelVersion(schema?: IRSForm) { export function labelVersion(schema: IRSForm | undefined) {
const version = schema?.versions.find(ver => ver.id === schema?.version); const version = schema?.versions.find(ver => ver.id === schema?.version);
return version ? version.version : 'актуальная'; return version ? version.version : 'актуальная';
} }

View File

@ -122,9 +122,9 @@ export class TMGraph {
this.nodeByAlias.set(alias, nodeToAnnotate); this.nodeByAlias.set(alias, nodeToAnnotate);
} }
private processArguments(args: IArgumentInfo[]): TMGraphNode | undefined { private processArguments(args: IArgumentInfo[]): TMGraphNode | null {
if (args.length === 0) { if (args.length === 0) {
return undefined; return null;
} }
const argsNodes = args.map(argument => this.parseToNode(argument.typification)); const argsNodes = args.map(argument => this.parseToNode(argument.typification));
if (args.length === 1) { if (args.length === 1) {
@ -133,16 +133,16 @@ export class TMGraph {
return this.addCartesianNode(argsNodes.map(node => node.id)); return this.addCartesianNode(argsNodes.map(node => node.id));
} }
private processResult(result: string): TMGraphNode | undefined { private processResult(result: string): TMGraphNode | null {
if (!result || result === PARAMETER.logicLabel) { if (!result || result === PARAMETER.logicLabel) {
return undefined; return null;
} }
return this.parseToNode(result); return this.parseToNode(result);
} }
private combineResults(result: TMGraphNode | undefined, args: TMGraphNode | undefined): TMGraphNode | undefined { private combineResults(result: TMGraphNode | null, args: TMGraphNode | null): TMGraphNode | null {
if (!result && !args) { if (!result && !args) {
return undefined; return null;
} }
if (!result) { if (!result) {
return this.addBooleanNode(args!.id); return this.addBooleanNode(args!.id);

View File

@ -164,7 +164,7 @@ export function guessCstType(hint: string, defaultType: CstType = CstType.TERM):
/** /**
* Evaluate if {@link CstType} is basic concept. * Evaluate if {@link CstType} is basic concept.
*/ */
export function isBasicConcept(type?: CstType): boolean { export function isBasicConcept(type: CstType): boolean {
// prettier-ignore // prettier-ignore
switch (type) { switch (type) {
case CstType.BASE: return true; case CstType.BASE: return true;
@ -175,7 +175,6 @@ export function isBasicConcept(type?: CstType): boolean {
case CstType.FUNCTION: return false; case CstType.FUNCTION: return false;
case CstType.PREDICATE: return false; case CstType.PREDICATE: return false;
case CstType.THEOREM: return false; case CstType.THEOREM: return false;
case undefined: return false;
} }
} }

View File

@ -16,7 +16,7 @@ import { useRSEdit } from '../RSEditContext';
import { ViewConstituents } from '../ViewConstituents'; import { ViewConstituents } from '../ViewConstituents';
import EditorControls from './EditorControls'; import EditorControls from './EditorControls';
import FormConstituenta from './FormConstituenta'; import { FormConstituenta } from './FormConstituenta';
import ToolbarConstituenta from './ToolbarConstituenta'; import ToolbarConstituenta from './ToolbarConstituenta';
// Threshold window width to switch layout. // Threshold window width to switch layout.

View File

@ -23,7 +23,7 @@ import { RefsInput } from '../../../components/RefsInput';
import { labelCstTypification, labelTypification } from '../../../labels'; import { labelCstTypification, labelTypification } from '../../../labels';
import { IConstituenta, IRSForm } from '../../../models/rsform'; import { IConstituenta, IRSForm } from '../../../models/rsform';
import { isBaseSet, isBasicConcept, isFunctional } from '../../../models/rsformAPI'; import { isBaseSet, isBasicConcept, isFunctional } from '../../../models/rsformAPI';
import EditorRSExpression from '../EditorRSExpression'; import { EditorRSExpression } from '../EditorRSExpression';
interface FormConstituentaProps { interface FormConstituentaProps {
id?: string; id?: string;
@ -32,10 +32,10 @@ interface FormConstituentaProps {
activeCst: IConstituenta; activeCst: IConstituenta;
schema: IRSForm; schema: IRSForm;
onOpenEdit?: (cstID: number) => void; onOpenEdit: (cstID: number) => void;
} }
function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpenEdit }: FormConstituentaProps) { export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpenEdit }: FormConstituentaProps) {
const { cstUpdate } = useCstUpdate(); const { cstUpdate } = useCstUpdate();
const showTypification = useDialogsStore(state => state.showShowTypeGraph); const showTypification = useDialogsStore(state => state.showShowTypeGraph);
const { isModified, setIsModified } = useModificationStore(); const { isModified, setIsModified } = useModificationStore();
@ -49,7 +49,7 @@ function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpen
formState: { isDirty } formState: { isDirty }
} = useForm<ICstUpdateDTO>({ resolver: zodResolver(schemaCstUpdate) }); } = useForm<ICstUpdateDTO>({ resolver: zodResolver(schemaCstUpdate) });
const [localParse, setLocalParse] = useState<IExpressionParseDTO | undefined>(undefined); const [localParse, setLocalParse] = useState<IExpressionParseDTO | null>(null);
const typification = useMemo( const typification = useMemo(
() => () =>
@ -88,7 +88,7 @@ function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpen
} }
}); });
setForceComment(false); setForceComment(false);
setLocalParse(undefined); setLocalParse(null);
}, [activeCst, schema, toggleReset, reset]); }, [activeCst, schema, toggleReset, reset]);
useLayoutEffect(() => { useLayoutEffect(() => {
@ -252,5 +252,3 @@ function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpen
</form> </form>
); );
} }
export default FormConstituenta;

View File

@ -32,7 +32,7 @@ import { IConstituenta } from '../../../models/rsform';
import { RSTabID, useRSEdit } from '../RSEditContext'; import { RSTabID, useRSEdit } from '../RSEditContext';
interface ToolbarConstituentaProps { interface ToolbarConstituentaProps {
activeCst?: IConstituenta; activeCst: IConstituenta | null;
disabled: boolean; disabled: boolean;
onSubmit: () => void; onSubmit: () => void;
@ -103,7 +103,9 @@ function ToolbarConstituenta({
title='Создать конституенту после данной' title='Создать конституенту после данной'
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={!controller.isContentEditable || isProcessing} disabled={!controller.isContentEditable || isProcessing}
onClick={() => controller.createCst(activeCst?.cst_type, false)} onClick={() =>
activeCst ? controller.createCst(activeCst.cst_type, false) : controller.createCstDefault()
}
/> />
<MiniButton <MiniButton
titleHtml={isModified ? tooltipText.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')} titleHtml={isModified ? tooltipText.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')}

View File

@ -6,7 +6,6 @@ import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { BadgeHelp, HelpTopic } from '@/features/help'; import { BadgeHelp, HelpTopic } from '@/features/help';
import { DataCallback } from '@/backend/apiTransport';
import { Overlay } from '@/components/Container'; import { Overlay } from '@/components/Container';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
@ -24,10 +23,10 @@ import { getDefinitionPrefix } from '../../../models/rsformAPI';
import { transformAST } from '../../../models/rslangAPI'; import { transformAST } from '../../../models/rslangAPI';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
import ParsingResult from './ParsingResult'; import { ParsingResult } from './ParsingResult';
import RSEditorControls from './RSEditControls'; import { RSEditorControls } from './RSEditControls';
import StatusBar from './StatusBar'; import { StatusBar } from './StatusBar';
import ToolbarRSExpression from './ToolbarRSExpression'; import { ToolbarRSExpression } from './ToolbarRSExpression';
interface EditorRSExpressionProps { interface EditorRSExpressionProps {
id?: string; id?: string;
@ -41,12 +40,12 @@ interface EditorRSExpressionProps {
disabled?: boolean; disabled?: boolean;
toggleReset?: boolean; toggleReset?: boolean;
onChangeLocalParse: (typification: IExpressionParseDTO | undefined) => void; onChangeLocalParse: (typification: IExpressionParseDTO) => void;
onOpenEdit?: (cstID: number) => void; onOpenEdit: (cstID: number) => void;
onShowTypeGraph: (event: CProps.EventMouse) => void; onShowTypeGraph: (event: CProps.EventMouse) => void;
} }
function EditorRSExpression({ export function EditorRSExpression({
activeCst, activeCst,
disabled, disabled,
value, value,
@ -61,7 +60,7 @@ function EditorRSExpression({
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
const rsInput = useRef<ReactCodeMirrorRef>(null); const rsInput = useRef<ReactCodeMirrorRef>(null);
const [parseData, setParseData] = useState<IExpressionParseDTO | undefined>(undefined); const [parseData, setParseData] = useState<IExpressionParseDTO | null>(null);
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls); const showControls = usePreferencesStore(state => state.showExpressionControls);
@ -72,7 +71,7 @@ function EditorRSExpression({
function checkConstituenta( function checkConstituenta(
expression: string, expression: string,
activeCst: IConstituenta, activeCst: IConstituenta,
onSuccess?: DataCallback<IExpressionParseDTO> onSuccess?: (data: IExpressionParseDTO) => void
) { ) {
const data: ICheckConstituentaDTO = { const data: ICheckConstituentaDTO = {
definition_formal: expression, definition_formal: expression,
@ -87,7 +86,7 @@ function EditorRSExpression({
useEffect(() => { useEffect(() => {
setIsModified(false); setIsModified(false);
setParseData(undefined); setParseData(null);
}, [activeCst, toggleReset]); }, [activeCst, toggleReset]);
function handleChange(newValue: string) { function handleChange(newValue: string) {
@ -200,5 +199,3 @@ function EditorRSExpression({
</div> </div>
); );
} }
export default EditorRSExpression;

View File

@ -3,13 +3,13 @@ import { describeRSError } from '../../../labels';
import { getRSErrorPrefix } from '../../../models/rslangAPI'; import { getRSErrorPrefix } from '../../../models/rslangAPI';
interface ParsingResultProps { interface ParsingResultProps {
data: IExpressionParseDTO | undefined; data: IExpressionParseDTO | null;
disabled?: boolean; disabled?: boolean;
isOpen: boolean; isOpen: boolean;
onShowError: (error: IRSErrorDescription) => void; onShowError: (error: IRSErrorDescription) => void;
} }
function ParsingResult({ isOpen, data, disabled, onShowError }: ParsingResultProps) { export function ParsingResult({ isOpen, data, disabled, onShowError }: ParsingResultProps) {
const errorCount = data ? data.errors.reduce((total, error) => (error.isCritical ? total + 1 : total), 0) : 0; const errorCount = data ? data.errors.reduce((total, error) => (error.isCritical ? total + 1 : total), 0) : 0;
const warningsCount = data ? data.errors.length - errorCount : 0; const warningsCount = data ? data.errors.length - errorCount : 0;
@ -46,5 +46,3 @@ function ParsingResult({ isOpen, data, disabled, onShowError }: ParsingResultPro
</div> </div>
); );
} }
export default ParsingResult;

View File

@ -87,7 +87,7 @@ interface RSEditorControlsProps {
onEdit: (id: TokenID, key?: string) => void; onEdit: (id: TokenID, key?: string) => void;
} }
function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) { export function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) {
return ( return (
<div <div
className={clsx( className={clsx(
@ -149,5 +149,3 @@ function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) {
</div> </div>
); );
} }
export default RSEditorControls;

View File

@ -15,14 +15,14 @@ import { ExpressionStatus, IConstituenta } from '../../../models/rsform';
import { inferStatus } from '../../../models/rsformAPI'; import { inferStatus } from '../../../models/rsformAPI';
interface StatusBarProps { interface StatusBarProps {
processing?: boolean; processing: boolean;
isModified?: boolean; isModified: boolean;
parseData?: IExpressionParseDTO; parseData: IExpressionParseDTO | null;
activeCst: IConstituenta; activeCst: IConstituenta;
onAnalyze: () => void; onAnalyze: () => void;
} }
function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }: StatusBarProps) { export function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }: StatusBarProps) {
const status = (() => { const status = (() => {
if (isModified) { if (isModified) {
return ExpressionStatus.UNKNOWN; return ExpressionStatus.UNKNOWN;
@ -66,5 +66,3 @@ function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }:
</div> </div>
); );
} }
export default StatusBar;

View File

@ -12,7 +12,7 @@ interface ToolbarRSExpressionProps {
showTypeGraph: (event: CProps.EventMouse) => void; showTypeGraph: (event: CProps.EventMouse) => void;
} }
function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpressionProps) { export function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpressionProps) {
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls); const showControls = usePreferencesStore(state => state.showExpressionControls);
const toggleControls = usePreferencesStore(state => state.toggleShowExpressionControls); const toggleControls = usePreferencesStore(state => state.toggleShowExpressionControls);
@ -39,5 +39,3 @@ function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpr
</Overlay> </Overlay>
); );
} }
export default ToolbarRSExpression;

View File

@ -1 +1 @@
export { default } from './EditorRSExpression'; export { EditorRSExpression } from './EditorRSExpression';

View File

@ -30,7 +30,7 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
if (!controller.schema.version || !window.confirm(promptText.restoreArchive)) { if (!controller.schema.version || !window.confirm(promptText.restoreArchive)) {
return; return;
} }
void versionRestore({ versionID: controller.schema.version }).then(() => controller.navigateVersion(undefined)); void versionRestore({ versionID: controller.schema.version }).then(() => controller.navigateVersion());
} }
function handleCreateVersion() { function handleCreateVersion() {
@ -50,7 +50,7 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
showEditVersions({ showEditVersions({
itemID: controller.schema.id, itemID: controller.schema.id,
afterDelete: targetVersion => { afterDelete: targetVersion => {
if (targetVersion === controller.activeVersion) controller.navigateVersion(undefined); if (targetVersion === controller.activeVersion) controller.navigateVersion();
} }
}); });
} }

View File

@ -100,7 +100,7 @@ function EditorRSList() {
} }
// prettier-ignore // prettier-ignore
switch (code) { switch (code) {
case 'Backquote': controller.createCst(undefined, false); return true; case 'Backquote': controller.createCstDefault(); return true;
case 'Digit1': controller.createCst(CstType.BASE, true); return true; case 'Digit1': controller.createCst(CstType.BASE, true); return true;
case 'Digit2': controller.createCst(CstType.STRUCTURED, true); return true; case 'Digit2': controller.createCst(CstType.STRUCTURED, true); return true;
@ -150,7 +150,7 @@ function EditorRSList() {
selected={rowSelection} selected={rowSelection}
setSelected={handleRowSelection} setSelected={handleRowSelection}
onEdit={controller.navigateCst} onEdit={controller.navigateCst}
onCreateNew={() => controller.createCst(undefined, false)} onCreateNew={controller.createCstDefault}
/> />
</div> </div>
</> </>

View File

@ -11,7 +11,7 @@ import useWindowSize from '@/hooks/useWindowSize';
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import { truncateToSymbol } from '@/utils/utils'; import { truncateToSymbol } from '@/utils/utils';
import BadgeConstituenta from '../../../components/BadgeConstituenta'; import { BadgeConstituenta } from '../../../components/BadgeConstituenta';
import { labelCstTypification } from '../../../labels'; import { labelCstTypification } from '../../../labels';
import { IConstituenta } from '../../../models/rsform'; import { IConstituenta } from '../../../models/rsform';

View File

@ -96,7 +96,7 @@ export function ToolbarRSList() {
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')} titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={isProcessing} disabled={isProcessing}
onClick={() => controller.createCst(undefined, false)} onClick={controller.createCstDefault}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')} titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}

View File

@ -68,12 +68,12 @@ function TGFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges] = useEdgesState([]); const [edges, setEdges] = useEdgesState([]);
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined); const [focusCst, setFocusCst] = useState<IConstituenta | null>(null);
const filteredGraph = produceFilteredGraph(controller.schema, filter, focusCst); const filteredGraph = produceFilteredGraph(controller.schema, filter, focusCst);
const [hidden, setHidden] = useState<number[]>([]); const [hidden, setHidden] = useState<number[]>([]);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [hoverID, setHoverID] = useState<number | undefined>(undefined); const [hoverID, setHoverID] = useState<number | null>(null);
const hoverCst = hoverID && controller.schema.cstByID.get(hoverID); const hoverCst = hoverID && controller.schema.cstByID.get(hoverID);
const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay); const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay);
const [hoverLeft, setHoverLeft] = useState(true); const [hoverLeft, setHoverLeft] = useState(true);
@ -102,7 +102,7 @@ function TGFlow() {
} }
}); });
setHidden(newDismissed); setHidden(newDismissed);
setHoverID(undefined); setHoverID(null);
}, [controller.schema, filteredGraph]); }, [controller.schema, filteredGraph]);
const resetNodes = useCallback(() => { const resetNodes = useCallback(() => {
@ -227,7 +227,7 @@ function TGFlow() {
if (event.key === 'Escape') { if (event.key === 'Escape') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setFocusCst(undefined); setFocusCst(null);
handleSetSelected([]); handleSetSelected([]);
return; return;
} }
@ -252,13 +252,17 @@ function TGFlow() {
}, PARAMETER.graphRefreshDelay); }, PARAMETER.graphRefreshDelay);
} }
function handleSetFocus(cstID: number | undefined) { function handleSetFocus(cstID: number | null) {
const target = cstID !== undefined ? controller.schema.cstByID.get(cstID) : cstID; if (cstID === null) {
setFocusCst(prev => (prev === target ? undefined : target)); setFocusCst(null);
} else {
const target = controller.schema.cstByID.get(cstID) ?? null;
setFocusCst(prev => (prev === target ? null : target));
if (target) { if (target) {
controller.setSelected([]); controller.setSelected([]);
} }
} }
}
function handleNodeClick(event: CProps.EventMouse, cstID: number) { function handleNodeClick(event: CProps.EventMouse, cstID: number) {
if (event.altKey) { if (event.altKey) {
@ -304,7 +308,10 @@ function TGFlow() {
{!focusCst ? ( {!focusCst ? (
<ToolbarGraphSelection <ToolbarGraphSelection
graph={controller.schema.graph} graph={controller.schema.graph}
isCore={cstID => isBasicConcept(controller.schema.cstByID.get(cstID)?.cst_type)} isCore={cstID => {
const cst = controller.schema.cstByID.get(cstID);
return !!cst && isBasicConcept(cst.cst_type);
}}
isOwned={ isOwned={
controller.schema.inheritance.length > 0 controller.schema.inheritance.length > 0
? cstID => !controller.schema.cstByID.get(cstID)?.is_inherited ? cstID => !controller.schema.cstByID.get(cstID)?.is_inherited
@ -318,7 +325,7 @@ function TGFlow() {
{focusCst ? ( {focusCst ? (
<ToolbarFocusedCst <ToolbarFocusedCst
center={focusCst} center={focusCst}
reset={() => handleSetFocus(undefined)} reset={() => handleSetFocus(null)}
showInputs={filter.focusShowInputs} showInputs={filter.focusShowInputs}
showOutputs={filter.focusShowOutputs} showOutputs={filter.focusShowOutputs}
toggleShowInputs={() => toggleShowInputs={() =>
@ -395,7 +402,7 @@ function TGFlow() {
onNodeDragStart={() => setIsDragging(true)} onNodeDragStart={() => setIsDragging(true)}
onNodeDragStop={() => setIsDragging(false)} onNodeDragStop={() => setIsDragging(false)}
onNodeMouseEnter={(event, node) => handleNodeEnter(event, Number(node.id))} onNodeMouseEnter={(event, node) => handleNodeEnter(event, Number(node.id))}
onNodeMouseLeave={() => setHoverID(undefined)} onNodeMouseLeave={() => setHoverID(null)}
onNodeClick={(event, node) => handleNodeClick(event, Number(node.id))} onNodeClick={(event, node) => handleNodeClick(event, Number(node.id))}
onNodeDoubleClick={(event, node) => handleNodeDoubleClick(event, Number(node.id))} onNodeDoubleClick={(event, node) => handleNodeDoubleClick(event, Number(node.id))}
/> />
@ -408,7 +415,7 @@ function TGFlow() {
export default TGFlow; export default TGFlow;
// ====== Internals ========= // ====== Internals =========
function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | undefined) { function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) {
const filtered = schema.graph.clone(); const filtered = schema.graph.clone();
const allowedTypes: CstType[] = (() => { const allowedTypes: CstType[] = (() => {
const result: CstType[] = []; const result: CstType[] = [];

View File

@ -11,8 +11,7 @@ export function applyLayout(nodes: Node<TGNodeData>[], edges: Edge[], subLabels?
rankdir: 'TB', rankdir: 'TB',
ranksep: subLabels ? 60 : 40, ranksep: subLabels ? 60 : 40,
nodesep: subLabels ? 100 : 20, nodesep: subLabels ? 100 : 20,
ranker: 'network-simplex', ranker: 'network-simplex'
align: undefined
}); });
nodes.forEach(node => { nodes.forEach(node => {
dagreGraph.setNode(node.id, { width: 2 * PARAMETER.graphNodeRadius, height: 2 * PARAMETER.graphNodeRadius }); dagreGraph.setNode(node.id, { width: 2 * PARAMETER.graphNodeRadius, height: 2 * PARAMETER.graphNodeRadius });

View File

@ -31,7 +31,7 @@ export enum RSTabID {
export interface IRSEditContext extends ILibraryItemEditor { export interface IRSEditContext extends ILibraryItemEditor {
schema: IRSForm; schema: IRSForm;
selected: number[]; selected: number[];
activeCst?: IConstituenta; activeCst: IConstituenta | null;
activeVersion?: number; activeVersion?: number;
isOwned: boolean; isOwned: boolean;
@ -41,7 +41,7 @@ export interface IRSEditContext extends ILibraryItemEditor {
isAttachedToOSS: boolean; isAttachedToOSS: boolean;
canDeleteSelected: boolean; canDeleteSelected: boolean;
navigateVersion: (versionID: number | undefined) => void; navigateVersion: (versionID?: number) => void;
navigateRSForm: ({ tab, activeID }: { tab: RSTabID; activeID?: number }) => void; navigateRSForm: ({ tab, activeID }: { tab: RSTabID; activeID?: number }) => void;
navigateCst: (cstID: number) => void; navigateCst: (cstID: number) => void;
navigateOss: (ossID: number, newTab?: boolean) => void; navigateOss: (ossID: number, newTab?: boolean) => void;
@ -56,7 +56,8 @@ export interface IRSEditContext extends ILibraryItemEditor {
moveUp: () => void; moveUp: () => void;
moveDown: () => void; moveDown: () => void;
createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void; createCst: (type: CstType, skipDialog: boolean, definition?: string) => void;
createCstDefault: () => void;
cloneCst: () => void; cloneCst: () => void;
promptDeleteCst: () => void; promptDeleteCst: () => void;
promptTemplate: () => void; promptTemplate: () => void;
@ -103,7 +104,7 @@ export const RSEditState = ({
const [selected, setSelected] = useState<number[]>([]); const [selected, setSelected] = useState<number[]>([]);
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited); const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited);
const activeCst = selected.length === 0 ? undefined : schema.cstByID.get(selected[selected.length - 1]); const activeCst = selected.length === 0 ? null : schema.cstByID.get(selected[selected.length - 1])!;
const { cstCreate } = useCstCreate(); const { cstCreate } = useCstCreate();
const { cstMove } = useCstMove(); const { cstMove } = useCstMove();
@ -124,7 +125,7 @@ export const RSEditState = ({
[schema, adjustRole, isOwned, user, adminMode] [schema, adjustRole, isOwned, user, adminMode]
); );
function navigateVersion(versionID: number | undefined) { function navigateVersion(versionID?: number) {
router.push(urls.schema(schema.id, versionID)); router.push(urls.schema(schema.id, versionID));
} }
@ -164,7 +165,7 @@ export const RSEditState = ({
if (!window.confirm(promptText.deleteLibraryItem)) { if (!window.confirm(promptText.deleteLibraryItem)) {
return; return;
} }
const ossID = schema.oss.length > 0 ? schema.oss[0].id : undefined; const ossID = schema.oss.length > 0 ? schema.oss[0].id : null;
void deleteItem(schema.id).then(() => { void deleteItem(schema.id).then(() => {
if (ossID) { if (ossID) {
router.push(urls.oss(ossID)); router.push(urls.oss(ossID));
@ -242,7 +243,7 @@ export const RSEditState = ({
}); });
} }
function createCst(type: CstType | undefined, skipDialog: boolean, definition?: string) { function createCst(type: CstType | null, skipDialog: boolean, definition?: string) {
const targetType = type ?? activeCst?.cst_type ?? CstType.BASE; const targetType = type ?? activeCst?.cst_type ?? CstType.BASE;
const data: ICstCreateDTO = { const data: ICstCreateDTO = {
insert_after: activeCst?.id ?? null, insert_after: activeCst?.id ?? null,
@ -286,7 +287,7 @@ export const RSEditState = ({
selected: selected, selected: selected,
afterDelete: (schema, deleted) => { afterDelete: (schema, deleted) => {
const isEmpty = deleted.length === schema.items.length; const isEmpty = deleted.length === schema.items.length;
const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeCst?.id, schema.items, deleted); const nextActive = isEmpty ? null : getNextActiveOnDelete(activeCst?.id ?? null, schema.items, deleted);
setSelected(nextActive ? [nextActive] : []); setSelected(nextActive ? [nextActive] : []);
if (!nextActive) { if (!nextActive) {
navigateRSForm({ tab: RSTabID.CST_LIST }); navigateRSForm({ tab: RSTabID.CST_LIST });
@ -338,6 +339,7 @@ export const RSEditState = ({
moveUp, moveUp,
moveDown, moveDown,
createCst, createCst,
createCstDefault: () => createCst(null, false),
cloneCst, cloneCst,
promptDeleteCst, promptDeleteCst,
@ -350,18 +352,14 @@ export const RSEditState = ({
}; };
// ====== Internals ========= // ====== Internals =========
function getNextActiveOnDelete( function getNextActiveOnDelete(activeID: number | null, items: IConstituenta[], deleted: number[]): number | null {
activeID: number | undefined,
items: IConstituenta[],
deleted: number[]
): number | undefined {
if (items.length === deleted.length) { if (items.length === deleted.length) {
return undefined; return null;
} }
let activeIndex = items.findIndex(cst => cst.id === activeID); let activeIndex = items.findIndex(cst => cst.id === activeID);
if (activeIndex === -1) { if (activeIndex === -1) {
return undefined; return null;
} }
while (activeIndex < items.length && deleted.find(id => id === items[activeIndex].id)) { while (activeIndex < items.length && deleted.find(id => id === items[activeIndex].id)) {

View File

@ -71,7 +71,7 @@ function applyGraphQuery(target: IRSForm, pivot: number, mode: DependencyMode):
if (mode === DependencyMode.ALL) { if (mode === DependencyMode.ALL) {
return target.items; return target.items;
} }
const ids: number[] | undefined = (() => { const ids = (() => {
switch (mode) { switch (mode) {
case DependencyMode.OUTPUTS: { case DependencyMode.OUTPUTS: {
return target.graph.nodes.get(pivot)?.outputs; return target.graph.nodes.get(pivot)?.outputs;
@ -86,7 +86,6 @@ function applyGraphQuery(target: IRSForm, pivot: number, mode: DependencyMode):
return target.graph.expandAllInputs([pivot]); return target.graph.expandAllInputs([pivot]);
} }
} }
return undefined;
})(); })();
if (ids) { if (ids) {
return target.items.filter(cst => ids.find(id => id === cst.id)); return target.items.filter(cst => ids.find(id => id === cst.id));

View File

@ -34,7 +34,7 @@ function SelectGraphFilter({ value, dense, onChange, ...restProps }: SelectGraph
hideTitle={menu.isOpen} hideTitle={menu.isOpen}
className='h-full pr-2' className='h-full pr-2'
icon={<DependencyIcon value={value} size='1rem' />} icon={<DependencyIcon value={value} size='1rem' />}
text={dense || size.isSmall ? undefined : labelCstSource(value)} text={!dense && !size.isSmall ? labelCstSource(value) : undefined}
onClick={menu.toggle} onClick={menu.toggle}
/> />
<Dropdown stretchLeft isOpen={menu.isOpen}> <Dropdown stretchLeft isOpen={menu.isOpen}>

View File

@ -7,7 +7,7 @@ import { NoData, TextContent } from '@/components/View';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import BadgeConstituenta from '../../../components/BadgeConstituenta'; import { BadgeConstituenta } from '../../../components/BadgeConstituenta';
import { describeConstituenta } from '../../../labels'; import { describeConstituenta } from '../../../labels';
import { IConstituenta } from '../../../models/rsform'; import { IConstituenta } from '../../../models/rsform';
@ -15,7 +15,7 @@ const DESCRIPTION_MAX_SYMBOLS = 280;
interface TableSideConstituentsProps { interface TableSideConstituentsProps {
items: IConstituenta[]; items: IConstituenta[];
activeCst?: IConstituenta; activeCst: IConstituenta | null;
onOpenEdit: (cstID: number) => void; onOpenEdit: (cstID: number) => void;
autoScroll?: boolean; autoScroll?: boolean;
maxHeight: string; maxHeight: string;
@ -54,7 +54,6 @@ function TableSideConstituents({
header: () => <span className='pl-3'>Имя</span>, header: () => <span className='pl-3'>Имя</span>,
size: 65, size: 65,
minSize: 65, minSize: 65,
footer: undefined,
cell: props => ( cell: props => (
<BadgeConstituenta className='mr-[-0.5rem]' value={props.row.original} prefixID={prefixes.cst_side_table} /> <BadgeConstituenta className='mr-[-0.5rem]' value={props.row.original} prefixID={prefixes.cst_side_table} />
) )
@ -81,19 +80,19 @@ function TableSideConstituents({
const conditionalRowStyles: IConditionalStyle<IConstituenta>[] = [ const conditionalRowStyles: IConditionalStyle<IConstituenta>[] = [
{ {
when: (cst: IConstituenta) => !!activeCst && cst.id === activeCst?.id, when: (cst: IConstituenta) => !!activeCst && cst.id === activeCst.id,
style: { style: {
backgroundColor: APP_COLORS.bgSelected backgroundColor: APP_COLORS.bgSelected
} }
}, },
{ {
when: (cst: IConstituenta) => !!activeCst && cst.spawner === activeCst?.id && cst.id !== activeCst?.id, when: (cst: IConstituenta) => !!activeCst && cst.spawner === activeCst.id && cst.id !== activeCst.id,
style: { style: {
backgroundColor: APP_COLORS.bgOrange50 backgroundColor: APP_COLORS.bgOrange50
} }
}, },
{ {
when: (cst: IConstituenta) => activeCst?.id !== undefined && cst.spawn.includes(activeCst.id), when: (cst: IConstituenta) => !!activeCst && cst.spawn.includes(activeCst.id),
style: { style: {
backgroundColor: APP_COLORS.bgGreen50 backgroundColor: APP_COLORS.bgGreen50
} }

View File

@ -49,7 +49,7 @@ export function ViewConstituents({ isBottom, isMounted }: ViewConstituentsProps)
}} }}
> >
<ConstituentsSearch <ConstituentsSearch
dense={windowSize.width && windowSize.width < COLUMN_DENSE_SEARCH_THRESHOLD ? true : undefined} dense={!!windowSize.width && windowSize.width < COLUMN_DENSE_SEARCH_THRESHOLD}
schema={schema} schema={schema}
activeID={activeCst?.id} activeID={activeCst?.id}
onChange={setFilteredData} onChange={setFilteredData}

View File

@ -10,7 +10,7 @@ import { useUsers } from '../backend/useUsers';
import { matchUser } from '../models/userAPI'; import { matchUser } from '../models/userAPI';
interface SelectUserProps extends CProps.Styling { interface SelectUserProps extends CProps.Styling {
value?: number; value: number | null;
onChange: (newValue: number) => void; onChange: (newValue: number) => void;
filter?: (userID: number) => boolean; filter?: (userID: number) => boolean;

View File

@ -57,7 +57,7 @@ export interface GenericDialogProps {
} }
interface DialogsStore { interface DialogsStore {
active: DialogType | undefined; active: DialogType | null;
props: unknown; props: unknown;
hideDialog: () => void; hideDialog: () => void;
@ -87,12 +87,12 @@ interface DialogsStore {
} }
export const useDialogsStore = create<DialogsStore>()(set => ({ export const useDialogsStore = create<DialogsStore>()(set => ({
active: undefined, active: null,
props: undefined, props: null,
hideDialog: () => { hideDialog: () => {
set(state => { set(state => {
(state.props as GenericDialogProps | undefined)?.onHide?.(); (state.props as GenericDialogProps | null)?.onHide?.();
return { active: undefined, props: undefined }; return { active: null, props: null };
}); });
}, },
@ -113,7 +113,7 @@ export const useDialogsStore = create<DialogsStore>()(set => ({
showCloneLibraryItem: props => set({ active: DialogType.CLONE_LIBRARY_ITEM, props: props }), showCloneLibraryItem: props => set({ active: DialogType.CLONE_LIBRARY_ITEM, props: props }),
showCreateVersion: props => set({ active: DialogType.CREATE_VERSION, props: props }), showCreateVersion: props => set({ active: DialogType.CREATE_VERSION, props: props }),
showDeleteOperation: props => set({ active: DialogType.DELETE_OPERATION, props: props }), showDeleteOperation: props => set({ active: DialogType.DELETE_OPERATION, props: props }),
showGraphParams: () => set({ active: DialogType.GRAPH_PARAMETERS, props: undefined }), showGraphParams: () => set({ active: DialogType.GRAPH_PARAMETERS, props: null }),
showRelocateConstituents: props => set({ active: DialogType.RELOCATE_CONSTITUENTS, props: props }), showRelocateConstituents: props => set({ active: DialogType.RELOCATE_CONSTITUENTS, props: props }),
showRenameCst: props => set({ active: DialogType.RENAME_CONSTITUENTA, props: props }), showRenameCst: props => set({ active: DialogType.RENAME_CONSTITUENTA, props: props }),
showQR: props => set({ active: DialogType.SHOW_QR_CODE, props: props }), showQR: props => set({ active: DialogType.SHOW_QR_CODE, props: props }),

View File

@ -3,11 +3,11 @@ import { create } from 'zustand';
import { IConstituenta } from '@/features/rsform/models/rsform'; import { IConstituenta } from '@/features/rsform/models/rsform';
interface TooltipsStore { interface TooltipsStore {
activeCst: IConstituenta | undefined; activeCst: IConstituenta | null;
setActiveCst: (value: IConstituenta | undefined) => void; setActiveCst: (value: IConstituenta | null) => void;
} }
export const useTooltipsStore = create<TooltipsStore>()(set => ({ export const useTooltipsStore = create<TooltipsStore>()(set => ({
activeCst: undefined, activeCst: null,
setActiveCst: value => set({ activeCst: value }) setActiveCst: value => set({ activeCst: value })
})); }));

View File

@ -110,20 +110,20 @@ export function promptUnsaved(): boolean {
} }
/** /**
* Toggle tristate flag: undefined - true - false. * Toggle tristate flag: null - true - false.
*/ */
export function toggleTristateFlag(prev: boolean | undefined): boolean | undefined { export function toggleTristateFlag(prev: boolean | null): boolean | null {
if (prev === undefined) { if (prev === null) {
return true; return true;
} }
return prev ? false : undefined; return prev ? false : null;
} }
/** /**
* Toggle tristate color: gray - green - red . * Toggle tristate color: gray - green - red .
*/ */
export function tripleToggleColor(value: boolean | undefined): string { export function tripleToggleColor(value: boolean | null): string {
if (value === undefined) { if (value === null) {
return 'clr-text-controls'; return 'clr-text-controls';
} }
return value ? 'text-ok-600' : 'text-warn-600'; return value ? 'text-ok-600' : 'text-warn-600';