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 = () => {
const active = useDialogsStore(state => state.active);
if (active === undefined) {
if (active === null) {
return null;
}
switch (active) {

View File

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

View File

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

View File

@ -141,7 +141,7 @@ function DataTable<TData extends RowData>({
...restProps
}: DataTableProps<TData>) {
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>({
pageIndex: 0,
@ -198,7 +198,7 @@ function DataTable<TData extends RowData>({
enableRowSelection={enableRowSelection}
enableSorting={enableSorting}
headPosition={headPosition}
resetLastSelected={() => setLastSelected(undefined)}
resetLastSelected={() => setLastSelected(null)}
/>
) : null}

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ export const schemaCloneLibraryItem = schemaLibraryItem
alias: z.string().nonempty(errorMsg.requiredField),
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
@ -138,5 +138,5 @@ export const schemaVersionUpdate = z.object({
export const schemaVersionCreate = z.object({
version: 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.subfolders) {
result = result.filter(
item => item.location == filter.location || item.location.startsWith(filter.location! + '/')
item => item.location == filter.location || item.location.startsWith(filter.location + '/')
);
} else {
result = result.filter(item => item.location == filter.location);
}
}
if (filter.type) {
result = result.filter(item => item.item_type === filter.type);
}
if (filter.isVisible !== undefined) {
if (filter.isVisible !== null) {
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));
}
if (filter.isEditor !== undefined) {
if (filter.isEditor !== null) {
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);
}
if (!filter.folderMode && filter.path) {
result = result.filter(item => matchLibraryItemLocation(item, filter.path!));
if (!filter.folderMode && !!filter.path) {
result = result.filter(item => matchLibraryItemLocation(item, filter.path));
}
if (filter.query) {
result = result.filter(item => matchLibraryItem(item, filter.query!));
result = result.filter(item => matchLibraryItem(item, filter.query));
}
return { filtered: result };

View File

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

View File

@ -9,10 +9,10 @@ import { ILibraryItem } from '../backend/types';
import { matchLibraryItem } from '../models/libraryAPI';
interface SelectLibraryItemProps extends CProps.Styling {
items?: ILibraryItem[];
value?: ILibraryItem;
onChange: (newValue?: ILibraryItem) => void;
value: ILibraryItem | null;
onChange: (newValue: ILibraryItem | null) => void;
items?: ILibraryItem[];
placeholder?: string;
noBorder?: boolean;
}
@ -41,7 +41,7 @@ export function SelectLibraryItem({
className={clsx('text-ellipsis', className)}
options={options}
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}
placeholder={placeholder}
{...restProps}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import { NoData } from '@/components/View';
import { IOperation } from '../models/oss';
import SelectOperation from './SelectOperation';
import { SelectOperation } from './SelectOperation';
interface PickMultiOperationProps extends CProps.Styling {
value: number[];
@ -25,17 +25,17 @@ const columnHelper = createColumnHelper<IOperation>();
export function PickMultiOperation({ rows, items, value, onChange, className, ...restProps }: PickMultiOperationProps) {
const selectedItems = value.map(itemID => items.find(item => item.id === itemID)!);
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) {
onChange(value.filter(item => item !== operation));
}
function handleSelect(operation?: IOperation) {
function handleSelect(operation: IOperation | null) {
if (operation) {
setLastSelected(operation);
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';
interface SelectOperationProps extends CProps.Styling {
items?: IOperation[];
value?: IOperation;
onChange: (newValue?: IOperation) => void;
value: IOperation | null;
onChange: (newValue: IOperation | null) => void;
items?: IOperation[];
placeholder?: string;
noBorder?: boolean;
}
function SelectOperation({
export function SelectOperation({
className,
items,
value,
@ -41,12 +41,10 @@ function SelectOperation({
className={clsx('text-ellipsis', className)}
options={options}
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}
placeholder={placeholder}
{...restProps}
/>
);
}
export default SelectOperation;

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ interface BadgeConstituentaProps extends CProps.Styling {
/**
* 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);
return (
@ -46,5 +46,3 @@ function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstitue
</div>
);
}
export default BadgeConstituenta;

View File

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

View File

@ -14,7 +14,7 @@ import { IConstituenta, IRSForm } from '../models/rsform';
import { isBasicConcept, matchConstituenta } from '../models/rsformAPI';
import { CstMatchMode } from '../stores/cstSearch';
import BadgeConstituenta from './BadgeConstituenta';
import { BadgeConstituenta } from './BadgeConstituenta';
import ToolbarGraphSelection from './ToolbarGraphSelection';
interface PickMultiConstituentaProps extends CProps.Styling {
@ -112,7 +112,10 @@ export function PickMultiConstituenta({
/>
<ToolbarGraphSelection
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}
value={value}
onChange={onChange}

View File

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

View File

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

View File

@ -11,15 +11,15 @@ import { matchConstituenta } from '../models/rsformAPI';
import { CstMatchMode } from '../stores/cstSearch';
interface SelectConstituentaProps extends CProps.Styling {
value?: IConstituenta;
onChange: (newValue?: IConstituenta) => void;
value: IConstituenta | null;
onChange: (newValue: IConstituenta | null) => void;
items?: IConstituenta[];
placeholder?: string;
noBorder?: boolean;
}
function SelectConstituenta({
export function SelectConstituenta({
className,
items,
value,
@ -43,12 +43,10 @@ function SelectConstituenta({
className={clsx('text-ellipsis', className)}
options={options}
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}
placeholder={placeholder}
{...restProps}
/>
);
}
export default SelectConstituenta;

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ export function TabEntityReference() {
const { setValue, control, register } = useFormContext<IEditReferenceState>();
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 ?? '';
function handleSelectConstituenta(cst: IConstituenta) {

View File

@ -20,7 +20,7 @@ export interface DlgShowASTProps {
function DlgShowAST() {
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 [isDragging, setIsDragging] = useState(false);
@ -47,7 +47,7 @@ function DlgShowAST() {
<ASTFlow
data={syntaxTree}
onNodeEnter={node => setHoverID(Number(node.id))}
onNodeLeave={() => setHoverID(undefined)}
onNodeLeave={() => setHoverID(null)}
onChangeDragging={setIsDragging}
/>
</ReactFlowProvider>

View File

@ -11,8 +11,7 @@ export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
rankdir: 'TB',
ranksep: 40,
nodesep: 40,
ranker: 'network-simplex',
align: undefined
ranker: 'network-simplex'
});
nodes.forEach(node => {
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',
ranksep: VERT_SEPARATION,
nodesep: HOR_SEPARATION,
ranker: 'network-simplex',
align: undefined
ranker: 'network-simplex'
});
nodes.forEach(node => {
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 { upload } = useUploadTRS();
const [loadMetadata, setLoadMetadata] = useState(false);
const [file, setFile] = useState<File | undefined>();
const [file, setFile] = useState<File | null>(null);
const handleSubmit = () => {
if (file) {
@ -34,7 +34,7 @@ function DlgUploadRSForm() {
if (event.target.files && event.target.files.length > 0) {
setFile(event.target.files[0]);
} 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}.
*/
export function describeConstituentaTerm(cst?: IConstituenta): string {
export function describeConstituentaTerm(cst: IConstituenta | null): string {
if (!cst) {
return '!Конституента отсутствует!';
}
@ -61,7 +61,7 @@ export function labelConstituenta(cst: IConstituenta) {
/**
* 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);
return version ? version.version : 'актуальная';
}

View File

@ -122,9 +122,9 @@ export class TMGraph {
this.nodeByAlias.set(alias, nodeToAnnotate);
}
private processArguments(args: IArgumentInfo[]): TMGraphNode | undefined {
private processArguments(args: IArgumentInfo[]): TMGraphNode | null {
if (args.length === 0) {
return undefined;
return null;
}
const argsNodes = args.map(argument => this.parseToNode(argument.typification));
if (args.length === 1) {
@ -133,16 +133,16 @@ export class TMGraph {
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) {
return undefined;
return null;
}
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) {
return undefined;
return null;
}
if (!result) {
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.
*/
export function isBasicConcept(type?: CstType): boolean {
export function isBasicConcept(type: CstType): boolean {
// prettier-ignore
switch (type) {
case CstType.BASE: return true;
@ -175,7 +175,6 @@ export function isBasicConcept(type?: CstType): boolean {
case CstType.FUNCTION: return false;
case CstType.PREDICATE: 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 EditorControls from './EditorControls';
import FormConstituenta from './FormConstituenta';
import { FormConstituenta } from './FormConstituenta';
import ToolbarConstituenta from './ToolbarConstituenta';
// Threshold window width to switch layout.

View File

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

View File

@ -32,7 +32,7 @@ import { IConstituenta } from '../../../models/rsform';
import { RSTabID, useRSEdit } from '../RSEditContext';
interface ToolbarConstituentaProps {
activeCst?: IConstituenta;
activeCst: IConstituenta | null;
disabled: boolean;
onSubmit: () => void;
@ -103,7 +103,9 @@ function ToolbarConstituenta({
title='Создать конституенту после данной'
icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={!controller.isContentEditable || isProcessing}
onClick={() => controller.createCst(activeCst?.cst_type, false)}
onClick={() =>
activeCst ? controller.createCst(activeCst.cst_type, false) : controller.createCstDefault()
}
/>
<MiniButton
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 { DataCallback } from '@/backend/apiTransport';
import { Overlay } from '@/components/Container';
import { CProps } from '@/components/props';
import { useDialogsStore } from '@/stores/dialogs';
@ -24,10 +23,10 @@ import { getDefinitionPrefix } from '../../../models/rsformAPI';
import { transformAST } from '../../../models/rslangAPI';
import { useRSEdit } from '../RSEditContext';
import ParsingResult from './ParsingResult';
import RSEditorControls from './RSEditControls';
import StatusBar from './StatusBar';
import ToolbarRSExpression from './ToolbarRSExpression';
import { ParsingResult } from './ParsingResult';
import { RSEditorControls } from './RSEditControls';
import { StatusBar } from './StatusBar';
import { ToolbarRSExpression } from './ToolbarRSExpression';
interface EditorRSExpressionProps {
id?: string;
@ -41,12 +40,12 @@ interface EditorRSExpressionProps {
disabled?: boolean;
toggleReset?: boolean;
onChangeLocalParse: (typification: IExpressionParseDTO | undefined) => void;
onOpenEdit?: (cstID: number) => void;
onChangeLocalParse: (typification: IExpressionParseDTO) => void;
onOpenEdit: (cstID: number) => void;
onShowTypeGraph: (event: CProps.EventMouse) => void;
}
function EditorRSExpression({
export function EditorRSExpression({
activeCst,
disabled,
value,
@ -61,7 +60,7 @@ function EditorRSExpression({
const [isModified, setIsModified] = useState(false);
const rsInput = useRef<ReactCodeMirrorRef>(null);
const [parseData, setParseData] = useState<IExpressionParseDTO | undefined>(undefined);
const [parseData, setParseData] = useState<IExpressionParseDTO | null>(null);
const isProcessing = useMutatingRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls);
@ -72,7 +71,7 @@ function EditorRSExpression({
function checkConstituenta(
expression: string,
activeCst: IConstituenta,
onSuccess?: DataCallback<IExpressionParseDTO>
onSuccess?: (data: IExpressionParseDTO) => void
) {
const data: ICheckConstituentaDTO = {
definition_formal: expression,
@ -87,7 +86,7 @@ function EditorRSExpression({
useEffect(() => {
setIsModified(false);
setParseData(undefined);
setParseData(null);
}, [activeCst, toggleReset]);
function handleChange(newValue: string) {
@ -200,5 +199,3 @@ function EditorRSExpression({
</div>
);
}
export default EditorRSExpression;

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ interface ToolbarRSExpressionProps {
showTypeGraph: (event: CProps.EventMouse) => void;
}
function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpressionProps) {
export function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpressionProps) {
const isProcessing = useMutatingRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls);
const toggleControls = usePreferencesStore(state => state.toggleShowExpressionControls);
@ -39,5 +39,3 @@ function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpr
</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)) {
return;
}
void versionRestore({ versionID: controller.schema.version }).then(() => controller.navigateVersion(undefined));
void versionRestore({ versionID: controller.schema.version }).then(() => controller.navigateVersion());
}
function handleCreateVersion() {
@ -50,7 +50,7 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
showEditVersions({
itemID: controller.schema.id,
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
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 'Digit2': controller.createCst(CstType.STRUCTURED, true); return true;
@ -150,7 +150,7 @@ function EditorRSList() {
selected={rowSelection}
setSelected={handleRowSelection}
onEdit={controller.navigateCst}
onCreateNew={() => controller.createCst(undefined, false)}
onCreateNew={controller.createCstDefault}
/>
</div>
</>

View File

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

View File

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

View File

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

View File

@ -11,8 +11,7 @@ export function applyLayout(nodes: Node<TGNodeData>[], edges: Edge[], subLabels?
rankdir: 'TB',
ranksep: subLabels ? 60 : 40,
nodesep: subLabels ? 100 : 20,
ranker: 'network-simplex',
align: undefined
ranker: 'network-simplex'
});
nodes.forEach(node => {
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 {
schema: IRSForm;
selected: number[];
activeCst?: IConstituenta;
activeCst: IConstituenta | null;
activeVersion?: number;
isOwned: boolean;
@ -41,7 +41,7 @@ export interface IRSEditContext extends ILibraryItemEditor {
isAttachedToOSS: boolean;
canDeleteSelected: boolean;
navigateVersion: (versionID: number | undefined) => void;
navigateVersion: (versionID?: number) => void;
navigateRSForm: ({ tab, activeID }: { tab: RSTabID; activeID?: number }) => void;
navigateCst: (cstID: number) => void;
navigateOss: (ossID: number, newTab?: boolean) => void;
@ -56,7 +56,8 @@ export interface IRSEditContext extends ILibraryItemEditor {
moveUp: () => void;
moveDown: () => void;
createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void;
createCst: (type: CstType, skipDialog: boolean, definition?: string) => void;
createCstDefault: () => void;
cloneCst: () => void;
promptDeleteCst: () => void;
promptTemplate: () => void;
@ -103,7 +104,7 @@ export const RSEditState = ({
const [selected, setSelected] = useState<number[]>([]);
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 { cstMove } = useCstMove();
@ -124,7 +125,7 @@ export const RSEditState = ({
[schema, adjustRole, isOwned, user, adminMode]
);
function navigateVersion(versionID: number | undefined) {
function navigateVersion(versionID?: number) {
router.push(urls.schema(schema.id, versionID));
}
@ -164,7 +165,7 @@ export const RSEditState = ({
if (!window.confirm(promptText.deleteLibraryItem)) {
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(() => {
if (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 data: ICstCreateDTO = {
insert_after: activeCst?.id ?? null,
@ -286,7 +287,7 @@ export const RSEditState = ({
selected: selected,
afterDelete: (schema, deleted) => {
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] : []);
if (!nextActive) {
navigateRSForm({ tab: RSTabID.CST_LIST });
@ -338,6 +339,7 @@ export const RSEditState = ({
moveUp,
moveDown,
createCst,
createCstDefault: () => createCst(null, false),
cloneCst,
promptDeleteCst,
@ -350,18 +352,14 @@ export const RSEditState = ({
};
// ====== Internals =========
function getNextActiveOnDelete(
activeID: number | undefined,
items: IConstituenta[],
deleted: number[]
): number | undefined {
function getNextActiveOnDelete(activeID: number | null, items: IConstituenta[], deleted: number[]): number | null {
if (items.length === deleted.length) {
return undefined;
return null;
}
let activeIndex = items.findIndex(cst => cst.id === activeID);
if (activeIndex === -1) {
return undefined;
return null;
}
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) {
return target.items;
}
const ids: number[] | undefined = (() => {
const ids = (() => {
switch (mode) {
case DependencyMode.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 undefined;
})();
if (ids) {
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}
className='h-full pr-2'
icon={<DependencyIcon value={value} size='1rem' />}
text={dense || size.isSmall ? undefined : labelCstSource(value)}
text={!dense && !size.isSmall ? labelCstSource(value) : undefined}
onClick={menu.toggle}
/>
<Dropdown stretchLeft isOpen={menu.isOpen}>

View File

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

View File

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

View File

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

View File

@ -57,7 +57,7 @@ export interface GenericDialogProps {
}
interface DialogsStore {
active: DialogType | undefined;
active: DialogType | null;
props: unknown;
hideDialog: () => void;
@ -87,12 +87,12 @@ interface DialogsStore {
}
export const useDialogsStore = create<DialogsStore>()(set => ({
active: undefined,
props: undefined,
active: null,
props: null,
hideDialog: () => {
set(state => {
(state.props as GenericDialogProps | undefined)?.onHide?.();
return { active: undefined, props: undefined };
(state.props as GenericDialogProps | null)?.onHide?.();
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 }),
showCreateVersion: props => set({ active: DialogType.CREATE_VERSION, 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 }),
showRenameCst: props => set({ active: DialogType.RENAME_CONSTITUENTA, 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';
interface TooltipsStore {
activeCst: IConstituenta | undefined;
setActiveCst: (value: IConstituenta | undefined) => void;
activeCst: IConstituenta | null;
setActiveCst: (value: IConstituenta | null) => void;
}
export const useTooltipsStore = create<TooltipsStore>()(set => ({
activeCst: undefined,
activeCst: null,
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 {
if (prev === undefined) {
export function toggleTristateFlag(prev: boolean | null): boolean | null {
if (prev === null) {
return true;
}
return prev ? false : undefined;
return prev ? false : null;
}
/**
* Toggle tristate color: gray - green - red .
*/
export function tripleToggleColor(value: boolean | undefined): string {
if (value === undefined) {
export function tripleToggleColor(value: boolean | null): string {
if (value === null) {
return 'clr-text-controls';
}
return value ? 'text-ok-600' : 'text-warn-600';