Compare commits
8 Commits
2b29f0b7bf
...
b03e2033eb
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b03e2033eb | ||
![]() |
71659b8c15 | ||
![]() |
4172e387c2 | ||
![]() |
c7df031041 | ||
![]() |
e8c3106563 | ||
![]() |
a1bfa8a119 | ||
![]() |
3f398bd700 | ||
![]() |
03e6bc55d9 |
3
.github/workflows/frontend.yml
vendored
3
.github/workflows/frontend.yml
vendored
|
@ -38,6 +38,7 @@ jobs:
|
||||||
npm install -g typescript vite jest
|
npm install -g typescript vite jest
|
||||||
npm ci
|
npm ci
|
||||||
npm run build --if-present
|
npm run build --if-present
|
||||||
- name: Test
|
- name: Run CI
|
||||||
run: |
|
run: |
|
||||||
|
npm run lint
|
||||||
npm test
|
npm test
|
||||||
|
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -77,6 +77,8 @@
|
||||||
"csrftoken",
|
"csrftoken",
|
||||||
"cstlist",
|
"cstlist",
|
||||||
"csttype",
|
"csttype",
|
||||||
|
"dagre",
|
||||||
|
"dagrejs",
|
||||||
"datv",
|
"datv",
|
||||||
"Debool",
|
"Debool",
|
||||||
"Decart",
|
"Decart",
|
||||||
|
@ -91,6 +93,7 @@
|
||||||
"Geologica",
|
"Geologica",
|
||||||
"Grammeme",
|
"Grammeme",
|
||||||
"Grammemes",
|
"Grammemes",
|
||||||
|
"graphlib",
|
||||||
"GRND",
|
"GRND",
|
||||||
"IDEF",
|
"IDEF",
|
||||||
"impr",
|
"impr",
|
||||||
|
@ -108,6 +111,7 @@
|
||||||
"multiword",
|
"multiword",
|
||||||
"mypy",
|
"mypy",
|
||||||
"nocheck",
|
"nocheck",
|
||||||
|
"nodesep",
|
||||||
"nomn",
|
"nomn",
|
||||||
"nooverlap",
|
"nooverlap",
|
||||||
"NPRO",
|
"NPRO",
|
||||||
|
@ -127,6 +131,8 @@
|
||||||
"pylint",
|
"pylint",
|
||||||
"pymorphy",
|
"pymorphy",
|
||||||
"Quantor",
|
"Quantor",
|
||||||
|
"rankdir",
|
||||||
|
"ranksep",
|
||||||
"razdel",
|
"razdel",
|
||||||
"reactflow",
|
"reactflow",
|
||||||
"reagraph",
|
"reagraph",
|
||||||
|
|
|
@ -50,6 +50,7 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- @uiw/react-codemirror
|
- @uiw/react-codemirror
|
||||||
- @uiw/codemirror-themes
|
- @uiw/codemirror-themes
|
||||||
- @lezer/lr
|
- @lezer/lr
|
||||||
|
- @dagrejs/dagre
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
|
|
540
rsconcept/frontend/package-lock.json
generated
540
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -12,6 +12,7 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@uiw/codemirror-themes": "^4.23.6",
|
"@uiw/codemirror-themes": "^4.23.6",
|
||||||
|
@ -26,7 +27,7 @@
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-error-boundary": "^4.1.2",
|
"react-error-boundary": "^4.1.2",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"react-intl": "^6.8.7",
|
"react-intl": "^6.8.9",
|
||||||
"react-loader-spinner": "^6.1.6",
|
"react-loader-spinner": "^6.1.6",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"react-select": "^5.8.3",
|
"react-select": "^5.8.3",
|
||||||
|
@ -41,14 +42,14 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.7.1",
|
"@lezer/generator": "^1.7.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.9.1",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^15.12.0",
|
"globals": "^15.12.0",
|
||||||
|
@ -57,7 +58,7 @@
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"typescript-eslint": "^8.14.0",
|
"typescript-eslint": "^8.15.0",
|
||||||
"vite": "^5.4.11"
|
"vite": "^5.4.11"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|
|
@ -24,7 +24,7 @@ function UserMenu() {
|
||||||
<AnimatePresence mode='wait'>
|
<AnimatePresence mode='wait'>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<AnimateFade key='nav_user_badge_loader'>
|
<AnimateFade key='nav_user_badge_loader'>
|
||||||
<Loader circular size={3} />
|
<Loader circular scale={1.5} />
|
||||||
</AnimateFade>
|
</AnimateFade>
|
||||||
) : null}
|
) : null}
|
||||||
{!user && !loading ? (
|
{!user && !loading ? (
|
||||||
|
|
|
@ -45,6 +45,7 @@ export interface DomIconProps<RequestData> extends IconProps {
|
||||||
value: RequestData;
|
value: RequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Icon for library item type. */
|
||||||
export function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProps<LibraryItemType>) {
|
export function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProps<LibraryItemType>) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case LibraryItemType.RSFORM:
|
case LibraryItemType.RSFORM:
|
||||||
|
@ -54,6 +55,7 @@ export function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Icon for access policy. */
|
||||||
export function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<AccessPolicy>) {
|
export function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<AccessPolicy>) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case AccessPolicy.PRIVATE:
|
case AccessPolicy.PRIVATE:
|
||||||
|
@ -65,6 +67,7 @@ export function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Icon for visibility. */
|
||||||
export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||||
if (value) {
|
if (value) {
|
||||||
return <IconShow size={size} className={className ?? 'clr-text-green'} />;
|
return <IconShow size={size} className={className ?? 'clr-text-green'} />;
|
||||||
|
@ -73,6 +76,7 @@ export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconPr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Icon for subfolders. */
|
||||||
export function SubfoldersIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
export function SubfoldersIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||||
if (value) {
|
if (value) {
|
||||||
return <IconSubfolders size={size} className={className ?? 'clr-text-green'} />;
|
return <IconSubfolders size={size} className={className ?? 'clr-text-green'} />;
|
||||||
|
@ -81,6 +85,7 @@ export function SubfoldersIcon({ value, size = '1.25rem', className }: DomIconPr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Icon for location. */
|
||||||
export function LocationIcon({ value, size = '1.25rem', className }: DomIconProps<string>) {
|
export function LocationIcon({ value, size = '1.25rem', className }: DomIconProps<string>) {
|
||||||
switch (value.substring(0, 2) as LocationHead) {
|
switch (value.substring(0, 2) as LocationHead) {
|
||||||
case LocationHead.COMMON:
|
case LocationHead.COMMON:
|
||||||
|
@ -94,6 +99,7 @@ export function LocationIcon({ value, size = '1.25rem', className }: DomIconProp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Icon for term graph dependency mode. */
|
||||||
export function DependencyIcon({ value, size = '1.25rem', className }: DomIconProps<DependencyMode>) {
|
export function DependencyIcon({ value, size = '1.25rem', className }: DomIconProps<DependencyMode>) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case DependencyMode.ALL:
|
case DependencyMode.ALL:
|
||||||
|
@ -109,6 +115,7 @@ export function DependencyIcon({ value, size = '1.25rem', className }: DomIconPr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Icon for constituenta match mode. */
|
||||||
export function MatchModeIcon({ value, size = '1.25rem', className }: DomIconProps<CstMatchMode>) {
|
export function MatchModeIcon({ value, size = '1.25rem', className }: DomIconProps<CstMatchMode>) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case CstMatchMode.ALL:
|
case CstMatchMode.ALL:
|
||||||
|
@ -124,6 +131,7 @@ export function MatchModeIcon({ value, size = '1.25rem', className }: DomIconPro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Icon for expression status. */
|
||||||
export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<ExpressionStatus>) {
|
export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<ExpressionStatus>) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case ExpressionStatus.VERIFIED:
|
case ExpressionStatus.VERIFIED:
|
||||||
|
@ -141,6 +149,7 @@ export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Icon for constituenta type. */
|
||||||
export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps<CstType>) {
|
export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps<CstType>) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case CstType.BASE:
|
case CstType.BASE:
|
||||||
|
@ -162,6 +171,7 @@ export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Icon for relocation direction. */
|
||||||
export function RelocateUpIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
export function RelocateUpIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||||
if (value) {
|
if (value) {
|
||||||
return <IconMoveUp size={size} className={className ?? 'clr-text-primary'} />;
|
return <IconMoveUp size={size} className={className ?? 'clr-text-primary'} />;
|
||||||
|
|
|
@ -7,11 +7,19 @@ import { CProps } from '../props';
|
||||||
import TooltipConstituenta from './TooltipConstituenta';
|
import TooltipConstituenta from './TooltipConstituenta';
|
||||||
|
|
||||||
interface BadgeConstituentaProps extends CProps.Styling {
|
interface BadgeConstituentaProps extends CProps.Styling {
|
||||||
|
/** Prefix for tooltip ID. */
|
||||||
prefixID?: string;
|
prefixID?: string;
|
||||||
|
|
||||||
|
/** Constituenta to display. */
|
||||||
value: IConstituenta;
|
value: IConstituenta;
|
||||||
|
|
||||||
|
/** Color theme to use. */
|
||||||
theme: IColorTheme;
|
theme: IColorTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a badge with a constituenta alias and information tooltip.
|
||||||
|
*/
|
||||||
function BadgeConstituenta({ value, prefixID, className, style, theme }: BadgeConstituentaProps) {
|
function BadgeConstituenta({ value, prefixID, className, style, theme }: BadgeConstituentaProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -6,9 +6,13 @@ import { colorFgGrammeme } from '@/styling/color';
|
||||||
import { labelGrammeme } from '@/utils/labels';
|
import { labelGrammeme } from '@/utils/labels';
|
||||||
|
|
||||||
interface BadgeGrammemeProps {
|
interface BadgeGrammemeProps {
|
||||||
|
/** Grammeme to display. */
|
||||||
grammeme: GramData;
|
grammeme: GramData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a badge with a grammeme tag.
|
||||||
|
*/
|
||||||
function BadgeGrammeme({ grammeme }: BadgeGrammemeProps) {
|
function BadgeGrammeme({ grammeme }: BadgeGrammemeProps) {
|
||||||
const { colors } = useConceptOptions();
|
const { colors } = useConceptOptions();
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,12 +8,22 @@ import { IconHelp } from '../Icons';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface BadgeHelpProps extends CProps.Styling {
|
interface BadgeHelpProps extends CProps.Styling {
|
||||||
|
/** Topic to display in a tooltip. */
|
||||||
topic: HelpTopic;
|
topic: HelpTopic;
|
||||||
|
|
||||||
|
/** Offset from the cursor to the tooltip. */
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
|
||||||
|
/** Classname for padding. */
|
||||||
padding?: string;
|
padding?: string;
|
||||||
|
|
||||||
|
/** Place of the tooltip in relation to the cursor. */
|
||||||
place?: PlacesType;
|
place?: PlacesType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display help icon with a manual page tooltip.
|
||||||
|
*/
|
||||||
function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpProps) {
|
function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpProps) {
|
||||||
const { showHelp } = useConceptOptions();
|
const { showHelp } = useConceptOptions();
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,13 @@ import { globals } from '@/utils/constants';
|
||||||
import { LocationIcon } from '../DomainIcons';
|
import { LocationIcon } from '../DomainIcons';
|
||||||
|
|
||||||
interface BadgeLocationProps {
|
interface BadgeLocationProps {
|
||||||
|
/** Location to display. */
|
||||||
location: string;
|
location: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays location icon with a full text tooltip.
|
||||||
|
*/
|
||||||
function BadgeLocation({ location }: BadgeLocationProps) {
|
function BadgeLocation({ location }: BadgeLocationProps) {
|
||||||
return (
|
return (
|
||||||
<div className='pl-2' data-tooltip-id={globals.tooltip} data-tooltip-content={location}>
|
<div className='pl-2' data-tooltip-id={globals.tooltip} data-tooltip-content={location}>
|
||||||
|
|
|
@ -3,10 +3,16 @@ import { IWordForm } from '@/models/language';
|
||||||
import BadgeGrammeme from './BadgeGrammeme';
|
import BadgeGrammeme from './BadgeGrammeme';
|
||||||
|
|
||||||
interface BadgeWordFormProps {
|
interface BadgeWordFormProps {
|
||||||
keyPrefix?: string;
|
/** Word form to display. */
|
||||||
form: IWordForm;
|
form: IWordForm;
|
||||||
|
|
||||||
|
/** Prefix for grammemes keys. */
|
||||||
|
keyPrefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a badge with grammemes of a word form.
|
||||||
|
*/
|
||||||
function BadgeWordForm({ keyPrefix, form }: BadgeWordFormProps) {
|
function BadgeWordForm({ keyPrefix, form }: BadgeWordFormProps) {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-wrap justify-start gap-1 select-none w-fit'>
|
<div className='flex flex-wrap justify-start gap-1 select-none w-fit'>
|
||||||
|
|
|
@ -98,8 +98,8 @@ function PickConstituenta({
|
||||||
id={id ? `${id}__search` : undefined}
|
id={id ? `${id}__search` : undefined}
|
||||||
className='clr-input rounded-t-md'
|
className='clr-input rounded-t-md'
|
||||||
noBorder
|
noBorder
|
||||||
value={filterText}
|
query={filterText}
|
||||||
onChange={newValue => setFilterText(newValue)}
|
onChangeQuery={newValue => setFilterText(newValue)}
|
||||||
/>
|
/>
|
||||||
<DataTable
|
<DataTable
|
||||||
id={id}
|
id={id}
|
||||||
|
|
|
@ -132,8 +132,8 @@ function PickMultiConstituenta({
|
||||||
id='dlg_constituents_search'
|
id='dlg_constituents_search'
|
||||||
noBorder
|
noBorder
|
||||||
className='min-w-[6rem] pr-2 flex-grow'
|
className='min-w-[6rem] pr-2 flex-grow'
|
||||||
value={filterText}
|
query={filterText}
|
||||||
onChange={setFilterText}
|
onChangeQuery={setFilterText}
|
||||||
/>
|
/>
|
||||||
<ToolbarGraphSelection
|
<ToolbarGraphSelection
|
||||||
graph={foldedGraph}
|
graph={foldedGraph}
|
||||||
|
|
|
@ -129,8 +129,8 @@ function PickSchema({
|
||||||
id={id ? `${id}__search` : undefined}
|
id={id ? `${id}__search` : undefined}
|
||||||
className='clr-input flex-grow rounded-t-md'
|
className='clr-input flex-grow rounded-t-md'
|
||||||
noBorder
|
noBorder
|
||||||
value={filterText}
|
query={filterText}
|
||||||
onChange={newValue => setFilterText(newValue)}
|
onChangeQuery={newValue => setFilterText(newValue)}
|
||||||
/>
|
/>
|
||||||
<div ref={locationMenu.ref}>
|
<div ref={locationMenu.ref}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
|
|
@ -11,11 +11,11 @@ interface SelectMultiGrammemeProps
|
||||||
extends Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'>,
|
extends Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'>,
|
||||||
CProps.Styling {
|
CProps.Styling {
|
||||||
value: IGrammemeOption[];
|
value: IGrammemeOption[];
|
||||||
setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
|
onChangeValue: (newValue: IGrammemeOption[]) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectMultiGrammeme({ value, setValue, ...restProps }: SelectMultiGrammemeProps) {
|
function SelectMultiGrammeme({ value, onChangeValue, ...restProps }: SelectMultiGrammemeProps) {
|
||||||
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -29,7 +29,7 @@ function SelectMultiGrammeme({ value, setValue, ...restProps }: SelectMultiGramm
|
||||||
<SelectMulti
|
<SelectMulti
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))}
|
onChange={newValue => onChangeValue([...newValue].sort(compareGrammemeOptions))}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,45 +25,84 @@ import TableHeader from './TableHeader';
|
||||||
|
|
||||||
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
||||||
|
|
||||||
|
/** Style to conditionally apply to rows. */
|
||||||
export interface IConditionalStyle<TData> {
|
export interface IConditionalStyle<TData> {
|
||||||
|
/** Callback to determine if the style should be applied. */
|
||||||
when: (rowData: TData) => boolean;
|
when: (rowData: TData) => boolean;
|
||||||
|
|
||||||
|
/** Style to apply. */
|
||||||
style: React.CSSProperties;
|
style: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataTableProps<TData extends RowData>
|
export interface DataTableProps<TData extends RowData>
|
||||||
extends CProps.Styling,
|
extends CProps.Styling,
|
||||||
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
|
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
|
||||||
|
/** Id of the component. */
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
||||||
|
/** Indicates that padding should be minimal. */
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
|
|
||||||
|
/** Number of rows to display. */
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
|
||||||
|
/** Height of the content. */
|
||||||
contentHeight?: string;
|
contentHeight?: string;
|
||||||
|
|
||||||
|
/** Top position of sticky header (0 if no other sticky elements are present). */
|
||||||
headPosition?: string;
|
headPosition?: string;
|
||||||
|
|
||||||
|
/** Disable header. */
|
||||||
noHeader?: boolean;
|
noHeader?: boolean;
|
||||||
|
|
||||||
|
/** Disable footer. */
|
||||||
noFooter?: boolean;
|
noFooter?: boolean;
|
||||||
|
|
||||||
|
/** List of styles to conditionally apply to rows. */
|
||||||
conditionalRowStyles?: IConditionalStyle<TData>[];
|
conditionalRowStyles?: IConditionalStyle<TData>[];
|
||||||
|
|
||||||
|
/** Component to display when there is no data. */
|
||||||
noDataComponent?: React.ReactNode;
|
noDataComponent?: React.ReactNode;
|
||||||
|
|
||||||
|
/** Callback to be called when a row is clicked. */
|
||||||
onRowClicked?: (rowData: TData, event: CProps.EventMouse) => void;
|
onRowClicked?: (rowData: TData, event: CProps.EventMouse) => void;
|
||||||
|
|
||||||
|
/** Callback to be called when a row is double clicked. */
|
||||||
onRowDoubleClicked?: (rowData: TData, event: CProps.EventMouse) => void;
|
onRowDoubleClicked?: (rowData: TData, event: CProps.EventMouse) => void;
|
||||||
|
|
||||||
|
/** Enable row selection. */
|
||||||
enableRowSelection?: boolean;
|
enableRowSelection?: boolean;
|
||||||
|
|
||||||
|
/** Current row selection. */
|
||||||
rowSelection?: RowSelectionState;
|
rowSelection?: RowSelectionState;
|
||||||
|
|
||||||
|
/** Enable hiding of columns. */
|
||||||
enableHiding?: boolean;
|
enableHiding?: boolean;
|
||||||
|
|
||||||
|
/** Current column visibility. */
|
||||||
columnVisibility?: VisibilityState;
|
columnVisibility?: VisibilityState;
|
||||||
|
|
||||||
|
/** Enable pagination. */
|
||||||
enablePagination?: boolean;
|
enablePagination?: boolean;
|
||||||
|
|
||||||
|
/** Number of rows per page. */
|
||||||
paginationPerPage?: number;
|
paginationPerPage?: number;
|
||||||
|
|
||||||
|
/** List of options to choose from for pagination. */
|
||||||
paginationOptions?: number[];
|
paginationOptions?: number[];
|
||||||
|
|
||||||
|
/** Callback to be called when the pagination option is changed. */
|
||||||
onChangePaginationOption?: (newValue: number) => void;
|
onChangePaginationOption?: (newValue: number) => void;
|
||||||
|
|
||||||
|
/** Enable sorting. */
|
||||||
enableSorting?: boolean;
|
enableSorting?: boolean;
|
||||||
|
|
||||||
|
/** Initial sorting. */
|
||||||
initialSorting?: ColumnSort;
|
initialSorting?: ColumnSort;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI element: data representation as a table.
|
* Dta representation as a table.
|
||||||
*
|
*
|
||||||
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
|
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
|
||||||
* No sticky header if omitted
|
* No sticky header if omitted
|
||||||
|
@ -157,7 +196,7 @@ function DataTable<TData extends RowData>({
|
||||||
enableRowSelection={enableRowSelection}
|
enableRowSelection={enableRowSelection}
|
||||||
enableSorting={enableSorting}
|
enableSorting={enableSorting}
|
||||||
headPosition={headPosition}
|
headPosition={headPosition}
|
||||||
setLastSelected={setLastSelected}
|
resetLastSelected={() => setLastSelected(undefined)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
@ -168,7 +207,7 @@ function DataTable<TData extends RowData>({
|
||||||
conditionalRowStyles={conditionalRowStyles}
|
conditionalRowStyles={conditionalRowStyles}
|
||||||
enableRowSelection={enableRowSelection}
|
enableRowSelection={enableRowSelection}
|
||||||
lastSelected={lastSelected}
|
lastSelected={lastSelected}
|
||||||
setLastSelected={setLastSelected}
|
onChangeLastSelected={setLastSelected}
|
||||||
onRowClicked={onRowClicked}
|
onRowClicked={onRowClicked}
|
||||||
onRowDoubleClicked={onRowDoubleClicked}
|
onRowDoubleClicked={onRowDoubleClicked}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,12 +4,12 @@ import CheckboxTristate from '@/components/ui/CheckboxTristate';
|
||||||
|
|
||||||
interface SelectAllProps<TData> {
|
interface SelectAllProps<TData> {
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
setLastSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
resetLastSelected: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectAll<TData>({ table, setLastSelected }: SelectAllProps<TData>) {
|
function SelectAll<TData>({ table, resetLastSelected }: SelectAllProps<TData>) {
|
||||||
function handleChange(value: boolean | null) {
|
function handleChange(value: boolean | null) {
|
||||||
setLastSelected(undefined);
|
resetLastSelected();
|
||||||
table.toggleAllPageRowsSelected(value !== false);
|
table.toggleAllPageRowsSelected(value !== false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@ import Checkbox from '@/components/ui/Checkbox';
|
||||||
|
|
||||||
interface SelectRowProps<TData> {
|
interface SelectRowProps<TData> {
|
||||||
row: Row<TData>;
|
row: Row<TData>;
|
||||||
setLastSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
onChangeLastSelected: (newValue: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectRow<TData>({ row, setLastSelected }: SelectRowProps<TData>) {
|
function SelectRow<TData>({ row, onChangeLastSelected }: SelectRowProps<TData>) {
|
||||||
function handleChange(value: boolean) {
|
function handleChange(value: boolean) {
|
||||||
setLastSelected(row.id);
|
onChangeLastSelected(row.id);
|
||||||
row.toggleSelected(value);
|
row.toggleSelected(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ interface TableBodyProps<TData> {
|
||||||
conditionalRowStyles?: IConditionalStyle<TData>[];
|
conditionalRowStyles?: IConditionalStyle<TData>[];
|
||||||
|
|
||||||
lastSelected: string | undefined;
|
lastSelected: string | undefined;
|
||||||
setLastSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
onChangeLastSelected: (newValue: string | undefined) => 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;
|
||||||
|
@ -27,7 +27,7 @@ function TableBody<TData>({
|
||||||
enableRowSelection,
|
enableRowSelection,
|
||||||
conditionalRowStyles,
|
conditionalRowStyles,
|
||||||
lastSelected,
|
lastSelected,
|
||||||
setLastSelected,
|
onChangeLastSelected,
|
||||||
onRowClicked,
|
onRowClicked,
|
||||||
onRowDoubleClicked
|
onRowDoubleClicked
|
||||||
}: TableBodyProps<TData>) {
|
}: TableBodyProps<TData>) {
|
||||||
|
@ -49,9 +49,9 @@ function TableBody<TData>({
|
||||||
newSelection[row.id] = !target.getIsSelected();
|
newSelection[row.id] = !target.getIsSelected();
|
||||||
});
|
});
|
||||||
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
|
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
|
||||||
setLastSelected(undefined);
|
onChangeLastSelected(undefined);
|
||||||
} else {
|
} else {
|
||||||
setLastSelected(target.id);
|
onChangeLastSelected(target.id);
|
||||||
target.toggleSelected(!target.getIsSelected());
|
target.toggleSelected(!target.getIsSelected());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ function TableBody<TData>({
|
||||||
>
|
>
|
||||||
{enableRowSelection ? (
|
{enableRowSelection ? (
|
||||||
<td key={`select-${row.id}`} className='pl-3 pr-1 align-middle border-y'>
|
<td key={`select-${row.id}`} className='pl-3 pr-1 align-middle border-y'>
|
||||||
<SelectRow row={row} setLastSelected={setLastSelected} />
|
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
|
||||||
</td>
|
</td>
|
||||||
) : null}
|
) : null}
|
||||||
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
|
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
|
||||||
|
|
|
@ -8,7 +8,7 @@ interface TableHeaderProps<TData> {
|
||||||
headPosition?: string;
|
headPosition?: string;
|
||||||
enableRowSelection?: boolean;
|
enableRowSelection?: boolean;
|
||||||
enableSorting?: boolean;
|
enableSorting?: boolean;
|
||||||
setLastSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
resetLastSelected: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableHeader<TData>({
|
function TableHeader<TData>({
|
||||||
|
@ -16,7 +16,7 @@ function TableHeader<TData>({
|
||||||
headPosition,
|
headPosition,
|
||||||
enableRowSelection,
|
enableRowSelection,
|
||||||
enableSorting,
|
enableSorting,
|
||||||
setLastSelected
|
resetLastSelected
|
||||||
}: TableHeaderProps<TData>) {
|
}: TableHeaderProps<TData>) {
|
||||||
return (
|
return (
|
||||||
<thead
|
<thead
|
||||||
|
@ -30,7 +30,7 @@ function TableHeader<TData>({
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{enableRowSelection ? (
|
{enableRowSelection ? (
|
||||||
<th className='pl-3 pr-1 align-middle'>
|
<th className='pl-3 pr-1 align-middle'>
|
||||||
<SelectAll table={table} setLastSelected={setLastSelected} />
|
<SelectAll table={table} resetLastSelected={resetLastSelected} />
|
||||||
</th>
|
</th>
|
||||||
) : null}
|
) : null}
|
||||||
{headerGroup.headers.map((header: Header<TData, unknown>) => (
|
{headerGroup.headers.map((header: Header<TData, unknown>) => (
|
||||||
|
|
|
@ -7,18 +7,24 @@ import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
import AnimateFade from '../wrap/AnimateFade';
|
import AnimateFade from '../wrap/AnimateFade';
|
||||||
|
|
||||||
interface LoaderProps {
|
interface LoaderProps {
|
||||||
size?: number;
|
/** Scale of the loader from 1 to 10. */
|
||||||
|
scale?: number;
|
||||||
|
|
||||||
|
/** Show a circular loader. */
|
||||||
circular?: boolean;
|
circular?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Loader({ size = 10, circular }: LoaderProps) {
|
/**
|
||||||
|
* Displays animated loader.
|
||||||
|
*/
|
||||||
|
function Loader({ scale = 5, circular }: LoaderProps) {
|
||||||
const { colors } = useConceptOptions();
|
const { colors } = useConceptOptions();
|
||||||
return (
|
return (
|
||||||
<AnimateFade noFadeIn className='flex justify-center'>
|
<AnimateFade noFadeIn className='flex justify-center'>
|
||||||
{circular ? (
|
{circular ? (
|
||||||
<ThreeCircles color={colors.bgPrimary} height={size * 10} width={size * 10} />
|
<ThreeCircles color={colors.bgPrimary} height={scale * 20} width={scale * 20} />
|
||||||
) : (
|
) : (
|
||||||
<ThreeDots color={colors.bgPrimary} height={size * 10} width={size * 10} radius={size} />
|
<ThreeDots color={colors.bgPrimary} height={scale * 20} width={scale * 20} radius={scale * 2} />
|
||||||
)}
|
)}
|
||||||
</AnimateFade>
|
</AnimateFade>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,11 +5,19 @@ import { globals } from '@/utils/constants';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface MiniButtonProps extends CProps.Button {
|
interface MiniButtonProps extends CProps.Button {
|
||||||
|
/** Icon to display in the button. */
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
|
|
||||||
|
/** Disable hover effect. */
|
||||||
noHover?: boolean;
|
noHover?: boolean;
|
||||||
|
|
||||||
|
/** Disable padding. */
|
||||||
noPadding?: boolean;
|
noPadding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays small transparent button with an icon.
|
||||||
|
*/
|
||||||
function MiniButton({
|
function MiniButton({
|
||||||
icon,
|
icon,
|
||||||
noHover,
|
noHover,
|
||||||
|
|
|
@ -18,23 +18,46 @@ import MiniButton from './MiniButton';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
|
|
||||||
export interface ModalProps extends CProps.Styling {
|
export interface ModalProps extends CProps.Styling {
|
||||||
|
/** Title of the modal window. */
|
||||||
header?: string;
|
header?: string;
|
||||||
|
|
||||||
|
/** Text of the submit button. */
|
||||||
submitText?: string;
|
submitText?: string;
|
||||||
|
|
||||||
|
/** Tooltip for the submit button when the form is invalid. */
|
||||||
submitInvalidTooltip?: string;
|
submitInvalidTooltip?: string;
|
||||||
|
|
||||||
|
/** Indicates that form is readonly. */
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
|
||||||
|
/** Indicates that submit button is enabled. */
|
||||||
canSubmit?: boolean;
|
canSubmit?: boolean;
|
||||||
|
|
||||||
|
/** Indicates that the modal window should be scrollable. */
|
||||||
overflowVisible?: boolean;
|
overflowVisible?: boolean;
|
||||||
|
|
||||||
|
/** Callback to be called when the modal window is closed. */
|
||||||
hideWindow: () => void;
|
hideWindow: () => void;
|
||||||
|
|
||||||
|
/** Callback to be called before submit. */
|
||||||
beforeSubmit?: () => boolean;
|
beforeSubmit?: () => boolean;
|
||||||
|
|
||||||
|
/** Callback to be called after submit. */
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
|
|
||||||
|
/** Callback to be called after cancel. */
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
|
||||||
|
/** Help topic to be displayed in the modal window. */
|
||||||
helpTopic?: HelpTopic;
|
helpTopic?: HelpTopic;
|
||||||
|
|
||||||
|
/** Callback to determine if help should be displayed. */
|
||||||
hideHelpWhen?: () => boolean;
|
hideHelpWhen?: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a customizable modal window.
|
||||||
|
*/
|
||||||
function Modal({
|
function Modal({
|
||||||
children,
|
children,
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,9 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps content in a div with a centered text.
|
||||||
|
*/
|
||||||
function NoData({ className, children, ...restProps }: CProps.Div) {
|
function NoData({ className, children, ...restProps }: CProps.Div) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('p-3 flex flex-col items-center text-center select-none w-full', className)} {...restProps}>
|
<div className={clsx('p-3 flex flex-col items-center text-center select-none w-full', className)} {...restProps}>
|
||||||
|
|
|
@ -3,11 +3,19 @@ import clsx from 'clsx';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface OverlayProps extends CProps.Styling {
|
interface OverlayProps extends CProps.Styling {
|
||||||
|
/** Id of the overlay. */
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
||||||
|
/** Classnames for position of the overlay. */
|
||||||
position?: string;
|
position?: string;
|
||||||
|
|
||||||
|
/** Classname for z-index of the overlay. */
|
||||||
layer?: string;
|
layer?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a transparent overlay over the main content.
|
||||||
|
*/
|
||||||
function Overlay({
|
function Overlay({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
|
|
@ -5,15 +5,26 @@ import { useMemo } from 'react';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
|
|
||||||
|
/** Maximum width of the viewer. */
|
||||||
const MAXIMUM_WIDTH = 1600;
|
const MAXIMUM_WIDTH = 1600;
|
||||||
|
|
||||||
|
/** Minimum width of the viewer. */
|
||||||
const MINIMUM_WIDTH = 300;
|
const MINIMUM_WIDTH = 300;
|
||||||
|
|
||||||
interface PDFViewerProps {
|
interface PDFViewerProps {
|
||||||
|
/** PDF file to display. */
|
||||||
file?: string;
|
file?: string;
|
||||||
|
|
||||||
|
/** Offset from the left side of the window. */
|
||||||
offsetXpx?: number;
|
offsetXpx?: number;
|
||||||
|
|
||||||
|
/** Minimum width of the viewer. */
|
||||||
minWidth?: number;
|
minWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a PDF file using an embedded viewer.
|
||||||
|
*/
|
||||||
function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps) {
|
function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps) {
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const { calculateHeight } = useConceptOptions();
|
const { calculateHeight } = useConceptOptions();
|
||||||
|
|
|
@ -2,6 +2,9 @@ interface PrettyJsonProps {
|
||||||
data: unknown;
|
data: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays JSON data in a formatted string.
|
||||||
|
*/
|
||||||
function PrettyJson({ data }: PrettyJsonProps) {
|
function PrettyJson({ data }: PrettyJsonProps) {
|
||||||
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,37 @@ import Overlay from './Overlay';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
|
|
||||||
interface SearchBarProps extends CProps.Styling {
|
interface SearchBarProps extends CProps.Styling {
|
||||||
value: string;
|
/** Id of the search bar. */
|
||||||
noIcon?: boolean;
|
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
||||||
|
/** Search query. */
|
||||||
|
query: string;
|
||||||
|
|
||||||
|
/** Placeholder text. */
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onChange?: (newValue: string) => void;
|
|
||||||
|
/** Callback to be called when the search query changes. */
|
||||||
|
onChangeQuery?: (newValue: string) => void;
|
||||||
|
|
||||||
|
/** Disable search icon. */
|
||||||
|
noIcon?: boolean;
|
||||||
|
|
||||||
|
/** Disable border. */
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchBar({ id, value, noIcon, onChange, noBorder, placeholder = 'Поиск', ...restProps }: SearchBarProps) {
|
/**
|
||||||
|
* Displays a search bar with a search icon and text input.
|
||||||
|
*/
|
||||||
|
function SearchBar({
|
||||||
|
id,
|
||||||
|
query,
|
||||||
|
noIcon,
|
||||||
|
onChangeQuery,
|
||||||
|
noBorder,
|
||||||
|
placeholder = 'Поиск',
|
||||||
|
...restProps
|
||||||
|
}: SearchBarProps) {
|
||||||
return (
|
return (
|
||||||
<div {...restProps}>
|
<div {...restProps}>
|
||||||
{!noIcon ? (
|
{!noIcon ? (
|
||||||
|
@ -29,8 +51,8 @@ function SearchBar({ id, value, noIcon, onChange, noBorder, placeholder = 'По
|
||||||
type='search'
|
type='search'
|
||||||
className={clsx('outline-none bg-transparent', !noIcon && 'pl-10')}
|
className={clsx('outline-none bg-transparent', !noIcon && 'pl-10')}
|
||||||
noBorder={noBorder}
|
noBorder={noBorder}
|
||||||
value={value}
|
value={query}
|
||||||
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
onChange={event => (onChangeQuery ? onChangeQuery(event.target.value) : undefined)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,6 +45,9 @@ export interface SelectMultiProps<Option, Group extends GroupBase<Option> = Grou
|
||||||
noPortal?: boolean;
|
noPortal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a multi-select component.
|
||||||
|
*/
|
||||||
function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
|
function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
|
||||||
noPortal,
|
noPortal,
|
||||||
...restProps
|
...restProps
|
||||||
|
|
|
@ -46,6 +46,9 @@ export interface SelectSingleProps<Option, Group extends GroupBase<Option> = Gro
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a single-select component.
|
||||||
|
*/
|
||||||
function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
|
function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
|
||||||
noPortal,
|
noPortal,
|
||||||
noBorder,
|
noBorder,
|
||||||
|
|
|
@ -11,22 +11,38 @@ import MiniButton from './MiniButton';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
|
|
||||||
interface SelectTreeProps<ItemType> extends CProps.Styling {
|
interface SelectTreeProps<ItemType> extends CProps.Styling {
|
||||||
items: ItemType[];
|
/** Current value. */
|
||||||
value: ItemType;
|
value: ItemType;
|
||||||
setValue: (newItem: ItemType) => void;
|
|
||||||
getParent: (item: ItemType) => ItemType;
|
/** List of available items. */
|
||||||
getLabel: (item: ItemType) => string;
|
items: ItemType[];
|
||||||
getDescription: (item: ItemType) => string;
|
|
||||||
|
/** Prefix for the ids of the elements. */
|
||||||
prefix: string;
|
prefix: string;
|
||||||
|
|
||||||
|
/** Callback to be called when the value changes. */
|
||||||
|
onChangeValue: (newItem: ItemType) => void;
|
||||||
|
|
||||||
|
/** Callback providing the parent of the item. */
|
||||||
|
getParent: (item: ItemType) => ItemType;
|
||||||
|
|
||||||
|
/** Callback providing the label of the item. */
|
||||||
|
getLabel: (item: ItemType) => string;
|
||||||
|
|
||||||
|
/** Callback providing the description of the item. */
|
||||||
|
getDescription: (item: ItemType) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a tree of items and allows user to select one.
|
||||||
|
*/
|
||||||
function SelectTree<ItemType>({
|
function SelectTree<ItemType>({
|
||||||
items,
|
items,
|
||||||
value,
|
value,
|
||||||
getParent,
|
getParent,
|
||||||
getLabel,
|
getLabel,
|
||||||
getDescription,
|
getDescription,
|
||||||
setValue,
|
onChangeValue,
|
||||||
prefix,
|
prefix,
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectTreeProps<ItemType>) {
|
}: SelectTreeProps<ItemType>) {
|
||||||
|
@ -71,9 +87,9 @@ function SelectTree<ItemType>({
|
||||||
(event: CProps.EventMouse, target: ItemType) => {
|
(event: CProps.EventMouse, target: ItemType) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setValue(target);
|
onChangeValue(target);
|
||||||
},
|
},
|
||||||
[setValue]
|
[onChangeValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,13 +5,22 @@ import { globals } from '@/utils/constants';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface SelectorButtonProps extends CProps.Button {
|
interface SelectorButtonProps extends CProps.Button {
|
||||||
|
/** Text to display in the button. */
|
||||||
text?: string;
|
text?: string;
|
||||||
|
|
||||||
|
/** Icon to display in the button. */
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
|
|
||||||
|
/** Classnames for the colors of the button. */
|
||||||
colors?: string;
|
colors?: string;
|
||||||
|
|
||||||
|
/** Indicates if button background should be transparent. */
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a button with an icon and text that opens a dropdown menu.
|
||||||
|
*/
|
||||||
function SelectorButton({
|
function SelectorButton({
|
||||||
text,
|
text,
|
||||||
icon,
|
icon,
|
||||||
|
|
|
@ -3,11 +3,19 @@ import clsx from 'clsx';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface SubmitButtonProps extends CProps.Button {
|
interface SubmitButtonProps extends CProps.Button {
|
||||||
|
/** Text to display in the button. */
|
||||||
text?: string;
|
text?: string;
|
||||||
loading?: boolean;
|
|
||||||
|
/** Icon to display in the button. */
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
|
|
||||||
|
/** Indicates that loading is in progress. */
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays submit type button with icon and text.
|
||||||
|
*/
|
||||||
function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...restProps }: SubmitButtonProps) {
|
function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...restProps }: SubmitButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -7,9 +7,13 @@ import { globals } from '@/utils/constants';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface TabLabelProps extends Omit<TabPropsImpl, 'children'>, CProps.Titled {
|
interface TabLabelProps extends Omit<TabPropsImpl, 'children'>, CProps.Titled {
|
||||||
|
/** Label to display in the tab. */
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a tab header with a label.
|
||||||
|
*/
|
||||||
function TabLabel({ label, title, titleHtml, hideTitle, className, ...otherProps }: TabLabelProps) {
|
function TabLabel({ label, title, titleHtml, hideTitle, className, ...otherProps }: TabLabelProps) {
|
||||||
return (
|
return (
|
||||||
<TabImpl
|
<TabImpl
|
||||||
|
|
|
@ -4,11 +4,19 @@ import { CProps } from '../props';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea {
|
export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea {
|
||||||
|
/** Indicates that padding should be minimal. */
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
|
|
||||||
|
/** Disable resize when content overflows. */
|
||||||
noResize?: boolean;
|
noResize?: boolean;
|
||||||
|
|
||||||
|
/** Disable resize to fit content. */
|
||||||
fitContent?: boolean;
|
fitContent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a customizable textarea with a label.
|
||||||
|
*/
|
||||||
function TextArea({
|
function TextArea({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
|
|
|
@ -6,11 +6,19 @@ import { truncateToLastWord } from '@/utils/utils';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
export interface TextContentProps extends CProps.Styling {
|
export interface TextContentProps extends CProps.Styling {
|
||||||
|
/** Text to display. */
|
||||||
text: string;
|
text: string;
|
||||||
|
|
||||||
|
/** Maximum number of symbols to display. */
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
|
|
||||||
|
/** Disable full text in a tooltip. */
|
||||||
noTooltip?: boolean;
|
noTooltip?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays text limited to a certain number of symbols.
|
||||||
|
*/
|
||||||
function TextContent({ className, text, maxLength, noTooltip, ...restProps }: TextContentProps) {
|
function TextContent({ className, text, maxLength, noTooltip, ...restProps }: TextContentProps) {
|
||||||
const truncated = maxLength ? truncateToLastWord(text, maxLength) : text;
|
const truncated = maxLength ? truncateToLastWord(text, maxLength) : text;
|
||||||
const isTruncated = maxLength && text.length > maxLength;
|
const isTruncated = maxLength && text.length > maxLength;
|
||||||
|
|
|
@ -4,7 +4,10 @@ import { CProps } from '../props';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input {
|
interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input {
|
||||||
|
/** Indicates that padding should be minimal. */
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
|
|
||||||
|
/** Capture enter key. */
|
||||||
allowEnter?: boolean;
|
allowEnter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +17,9 @@ function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a customizable input with a label.
|
||||||
|
*/
|
||||||
function TextInput({
|
function TextInput({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
|
|
|
@ -1,13 +1,25 @@
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
interface TextURLProps {
|
interface TextURLProps {
|
||||||
|
/** Text to display. */
|
||||||
text: string;
|
text: string;
|
||||||
|
|
||||||
|
/** Tooltip for the link. */
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
|
/** URL to link to. */
|
||||||
href?: string;
|
href?: string;
|
||||||
|
|
||||||
|
/** Color of the link. */
|
||||||
color?: string;
|
color?: string;
|
||||||
|
|
||||||
|
/** Callback to be called when the link is clicked. */
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a text with a clickable link.
|
||||||
|
*/
|
||||||
function TextURL({ text, href, title, color = 'clr-text-url', onClick }: TextURLProps) {
|
function TextURL({ text, href, title, color = 'clr-text-url', onClick }: TextURLProps) {
|
||||||
const design = `cursor-pointer hover:underline ${color}`;
|
const design = `cursor-pointer hover:underline ${color}`;
|
||||||
if (href) {
|
if (href) {
|
||||||
|
|
|
@ -10,10 +10,16 @@ import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
export type { PlacesType } from 'react-tooltip';
|
export type { PlacesType } from 'react-tooltip';
|
||||||
|
|
||||||
interface TooltipProps extends Omit<ITooltip, 'variant'> {
|
interface TooltipProps extends Omit<ITooltip, 'variant'> {
|
||||||
layer?: string;
|
/** Text to display in the tooltip. */
|
||||||
text?: string;
|
text?: string;
|
||||||
|
|
||||||
|
/** Classname for z-index */
|
||||||
|
layer?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays content in a tooltip container.
|
||||||
|
*/
|
||||||
function Tooltip({
|
function Tooltip({
|
||||||
text,
|
text,
|
||||||
children,
|
children,
|
||||||
|
|
|
@ -7,16 +7,34 @@ import { CProps } from '../props';
|
||||||
import MiniButton from './MiniButton';
|
import MiniButton from './MiniButton';
|
||||||
|
|
||||||
interface ValueIconProps extends CProps.Styling, CProps.Titled {
|
interface ValueIconProps extends CProps.Styling, CProps.Titled {
|
||||||
|
/** Id of the component. */
|
||||||
id?: string;
|
id?: string;
|
||||||
icon: React.ReactNode;
|
|
||||||
|
/** Value to display. */
|
||||||
value: string | number;
|
value: string | number;
|
||||||
|
|
||||||
|
/** Icon to display. */
|
||||||
|
icon: React.ReactNode;
|
||||||
|
|
||||||
|
/** Classname for the text. */
|
||||||
textClassName?: string;
|
textClassName?: string;
|
||||||
|
|
||||||
|
/** Callback to be called when the component is clicked. */
|
||||||
onClick?: (event: CProps.EventMouse) => void;
|
onClick?: (event: CProps.EventMouse) => void;
|
||||||
|
|
||||||
|
/** Number of symbols to display in a small size. */
|
||||||
smallThreshold?: number;
|
smallThreshold?: number;
|
||||||
|
|
||||||
|
/** Indicates that padding should be minimal. */
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
|
|
||||||
|
/** Disable interaction. */
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a value with an icon that can be clicked.
|
||||||
|
*/
|
||||||
function ValueIcon({
|
function ValueIcon({
|
||||||
id,
|
id,
|
||||||
dense,
|
dense,
|
||||||
|
|
|
@ -3,12 +3,22 @@ import clsx from 'clsx';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface ValueLabeledProps extends CProps.Styling {
|
interface ValueLabeledProps extends CProps.Styling {
|
||||||
|
/** Id of the component. */
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
||||||
|
/** Label to display. */
|
||||||
label: string;
|
label: string;
|
||||||
|
|
||||||
|
/** Value to display. */
|
||||||
text: string | number;
|
text: string | number;
|
||||||
|
|
||||||
|
/** Tooltip for the component. */
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a labeled value.
|
||||||
|
*/
|
||||||
function ValueLabeled({ id, label, text, title, className, ...restProps }: ValueLabeledProps) {
|
function ValueLabeled({ id, label, text, title, className, ...restProps }: ValueLabeledProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('flex justify-between gap-6', className)} {...restProps}>
|
<div className={clsx('flex justify-between gap-6', className)} {...restProps}>
|
||||||
|
|
|
@ -4,11 +4,19 @@ import { CProps } from '../props';
|
||||||
import ValueIcon from './ValueIcon';
|
import ValueIcon from './ValueIcon';
|
||||||
|
|
||||||
interface ValueStatsProps extends CProps.Styling, CProps.Titled {
|
interface ValueStatsProps extends CProps.Styling, CProps.Titled {
|
||||||
|
/** Id of the component. */
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
/** Icon to display. */
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
|
|
||||||
|
/** Value to display. */
|
||||||
value: string | number;
|
value: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays statistics value with an icon.
|
||||||
|
*/
|
||||||
function ValueStats(props: ValueStatsProps) {
|
function ValueStats(props: ValueStatsProps) {
|
||||||
return <ValueIcon dense smallThreshold={PARAMETER.statSmallThreshold} textClassName='min-w-[1.4rem]' {...props} />;
|
return <ValueIcon dense smallThreshold={PARAMETER.statSmallThreshold} textClassName='min-w-[1.4rem]' {...props} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,7 +159,7 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
|
||||||
id={`${globals.tooltip}`}
|
id={`${globals.tooltip}`}
|
||||||
layer='z-topmost'
|
layer='z-topmost'
|
||||||
place='right-start'
|
place='right-start'
|
||||||
className='mt-8 max-w-[20rem]'
|
className='mt-8 max-w-[20rem] break-words'
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
float
|
float
|
||||||
|
|
|
@ -103,15 +103,15 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
|
||||||
<TabInputOperation
|
<TabInputOperation
|
||||||
oss={oss}
|
oss={oss}
|
||||||
alias={alias}
|
alias={alias}
|
||||||
setAlias={setAlias}
|
onChangeAlias={setAlias}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
setComment={setComment}
|
onChangeComment={setComment}
|
||||||
title={title}
|
title={title}
|
||||||
setTitle={setTitle}
|
onChangeTitle={setTitle}
|
||||||
attachedID={attachedID}
|
attachedID={attachedID}
|
||||||
setAttachedID={setAttachedID}
|
onChangeAttachedID={setAttachedID}
|
||||||
createSchema={createSchema}
|
createSchema={createSchema}
|
||||||
setCreateSchema={setCreateSchema}
|
onChangeCreateSchema={setCreateSchema}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
),
|
),
|
||||||
|
@ -124,11 +124,11 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
|
||||||
<TabSynthesisOperation
|
<TabSynthesisOperation
|
||||||
oss={oss}
|
oss={oss}
|
||||||
alias={alias}
|
alias={alias}
|
||||||
setAlias={setAlias}
|
onChangeAlias={setAlias}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
setComment={setComment}
|
onChangeComment={setComment}
|
||||||
title={title}
|
title={title}
|
||||||
setTitle={setTitle}
|
onChangeTitle={setTitle}
|
||||||
inputs={inputs}
|
inputs={inputs}
|
||||||
setInputs={setInputs}
|
setInputs={setInputs}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -18,29 +18,29 @@ import { sortItemsForOSS } from '@/models/ossAPI';
|
||||||
interface TabInputOperationProps {
|
interface TabInputOperationProps {
|
||||||
oss: IOperationSchema;
|
oss: IOperationSchema;
|
||||||
alias: string;
|
alias: string;
|
||||||
setAlias: React.Dispatch<React.SetStateAction<string>>;
|
onChangeAlias: (newValue: string) => void;
|
||||||
title: string;
|
title: string;
|
||||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
onChangeTitle: (newValue: string) => void;
|
||||||
comment: string;
|
comment: string;
|
||||||
setComment: React.Dispatch<React.SetStateAction<string>>;
|
onChangeComment: (newValue: string) => void;
|
||||||
attachedID: LibraryItemID | undefined;
|
attachedID: LibraryItemID | undefined;
|
||||||
setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>;
|
onChangeAttachedID: (newValue: LibraryItemID | undefined) => void;
|
||||||
createSchema: boolean;
|
createSchema: boolean;
|
||||||
setCreateSchema: React.Dispatch<React.SetStateAction<boolean>>;
|
onChangeCreateSchema: (newValue: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabInputOperation({
|
function TabInputOperation({
|
||||||
oss,
|
oss,
|
||||||
alias,
|
alias,
|
||||||
setAlias,
|
onChangeAlias,
|
||||||
title,
|
title,
|
||||||
setTitle,
|
onChangeTitle,
|
||||||
comment,
|
comment,
|
||||||
setComment,
|
onChangeComment,
|
||||||
attachedID,
|
attachedID,
|
||||||
setAttachedID,
|
onChangeAttachedID,
|
||||||
createSchema,
|
createSchema,
|
||||||
setCreateSchema
|
onChangeCreateSchema
|
||||||
}: TabInputOperationProps) {
|
}: TabInputOperationProps) {
|
||||||
const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]);
|
const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]);
|
||||||
const library = useLibrary();
|
const library = useLibrary();
|
||||||
|
@ -48,9 +48,9 @@ function TabInputOperation({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (createSchema) {
|
if (createSchema) {
|
||||||
setAttachedID(undefined);
|
onChangeAttachedID(undefined);
|
||||||
}
|
}
|
||||||
}, [createSchema, setAttachedID]);
|
}, [createSchema, onChangeAttachedID]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimateFade className='cc-column'>
|
<AnimateFade className='cc-column'>
|
||||||
|
@ -58,7 +58,7 @@ function TabInputOperation({
|
||||||
id='operation_title'
|
id='operation_title'
|
||||||
label='Полное название'
|
label='Полное название'
|
||||||
value={title}
|
value={title}
|
||||||
onChange={event => setTitle(event.target.value)}
|
onChange={event => onChangeTitle(event.target.value)}
|
||||||
disabled={attachedID !== undefined}
|
disabled={attachedID !== undefined}
|
||||||
/>
|
/>
|
||||||
<div className='flex gap-6'>
|
<div className='flex gap-6'>
|
||||||
|
@ -67,7 +67,7 @@ function TabInputOperation({
|
||||||
label='Сокращение'
|
label='Сокращение'
|
||||||
className='w-[16rem]'
|
className='w-[16rem]'
|
||||||
value={alias}
|
value={alias}
|
||||||
onChange={event => setAlias(event.target.value)}
|
onChange={event => onChangeAlias(event.target.value)}
|
||||||
disabled={attachedID !== undefined}
|
disabled={attachedID !== undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ function TabInputOperation({
|
||||||
noResize
|
noResize
|
||||||
rows={3}
|
rows={3}
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={event => setComment(event.target.value)}
|
onChange={event => onChangeComment(event.target.value)}
|
||||||
disabled={attachedID !== undefined}
|
disabled={attachedID !== undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,13 +90,13 @@ function TabInputOperation({
|
||||||
noHover
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setAttachedID(undefined)}
|
onClick={() => onChangeAttachedID(undefined)}
|
||||||
disabled={attachedID == undefined}
|
disabled={attachedID == undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={createSchema}
|
value={createSchema}
|
||||||
setValue={setCreateSchema}
|
setValue={onChangeCreateSchema}
|
||||||
label='Создать новую схему'
|
label='Создать новую схему'
|
||||||
titleHtml='Создать пустую схему для загрузки'
|
titleHtml='Создать пустую схему для загрузки'
|
||||||
/>
|
/>
|
||||||
|
@ -106,7 +106,7 @@ function TabInputOperation({
|
||||||
items={sortedItems}
|
items={sortedItems}
|
||||||
value={attachedID}
|
value={attachedID}
|
||||||
itemType={LibraryItemType.RSFORM}
|
itemType={LibraryItemType.RSFORM}
|
||||||
onSelectValue={setAttachedID}
|
onSelectValue={onChangeAttachedID}
|
||||||
rows={8}
|
rows={8}
|
||||||
baseFilter={baseFilter}
|
baseFilter={baseFilter}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,11 +10,11 @@ import PickMultiOperation from '../../components/select/PickMultiOperation';
|
||||||
interface TabSynthesisOperationProps {
|
interface TabSynthesisOperationProps {
|
||||||
oss: IOperationSchema;
|
oss: IOperationSchema;
|
||||||
alias: string;
|
alias: string;
|
||||||
setAlias: React.Dispatch<React.SetStateAction<string>>;
|
onChangeAlias: (newValue: string) => void;
|
||||||
title: string;
|
title: string;
|
||||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
onChangeTitle: (newValue: string) => void;
|
||||||
comment: string;
|
comment: string;
|
||||||
setComment: React.Dispatch<React.SetStateAction<string>>;
|
onChangeComment: (newValue: string) => void;
|
||||||
inputs: OperationID[];
|
inputs: OperationID[];
|
||||||
setInputs: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
setInputs: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,11 @@ interface TabSynthesisOperationProps {
|
||||||
function TabSynthesisOperation({
|
function TabSynthesisOperation({
|
||||||
oss,
|
oss,
|
||||||
alias,
|
alias,
|
||||||
setAlias,
|
onChangeAlias,
|
||||||
title,
|
title,
|
||||||
setTitle,
|
onChangeTitle,
|
||||||
comment,
|
comment,
|
||||||
setComment,
|
onChangeComment,
|
||||||
inputs,
|
inputs,
|
||||||
setInputs
|
setInputs
|
||||||
}: TabSynthesisOperationProps) {
|
}: TabSynthesisOperationProps) {
|
||||||
|
@ -36,7 +36,7 @@ function TabSynthesisOperation({
|
||||||
id='operation_title'
|
id='operation_title'
|
||||||
label='Полное название'
|
label='Полное название'
|
||||||
value={title}
|
value={title}
|
||||||
onChange={event => setTitle(event.target.value)}
|
onChange={event => onChangeTitle(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className='flex gap-6'>
|
<div className='flex gap-6'>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -44,7 +44,7 @@ function TabSynthesisOperation({
|
||||||
label='Сокращение'
|
label='Сокращение'
|
||||||
className='w-[16rem]'
|
className='w-[16rem]'
|
||||||
value={alias}
|
value={alias}
|
||||||
onChange={event => setAlias(event.target.value)}
|
onChange={event => onChangeAlias(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextArea
|
<TextArea
|
||||||
|
@ -53,7 +53,7 @@ function TabSynthesisOperation({
|
||||||
noResize
|
noResize
|
||||||
rows={3}
|
rows={3}
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={event => setComment(event.target.value)}
|
onChange={event => onChangeComment(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -141,11 +141,11 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<TabOperation
|
<TabOperation
|
||||||
alias={alias}
|
alias={alias}
|
||||||
setAlias={setAlias}
|
onChangeAlias={setAlias}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
setComment={setComment}
|
onChangeComment={setComment}
|
||||||
title={title}
|
title={title}
|
||||||
setTitle={setTitle}
|
onChangeTitle={setTitle}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,21 +4,21 @@ import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
|
|
||||||
interface TabOperationProps {
|
interface TabOperationProps {
|
||||||
alias: string;
|
alias: string;
|
||||||
setAlias: React.Dispatch<React.SetStateAction<string>>;
|
onChangeAlias: (newValue: string) => void;
|
||||||
title: string;
|
title: string;
|
||||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
onChangeTitle: (newValue: string) => void;
|
||||||
comment: string;
|
comment: string;
|
||||||
setComment: React.Dispatch<React.SetStateAction<string>>;
|
onChangeComment: (newValue: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabOperation({ alias, setAlias, title, setTitle, comment, setComment }: TabOperationProps) {
|
function TabOperation({ alias, onChangeAlias, title, onChangeTitle, comment, onChangeComment }: TabOperationProps) {
|
||||||
return (
|
return (
|
||||||
<AnimateFade className='cc-column'>
|
<AnimateFade className='cc-column'>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='operation_title'
|
id='operation_title'
|
||||||
label='Полное название'
|
label='Полное название'
|
||||||
value={title}
|
value={title}
|
||||||
onChange={event => setTitle(event.target.value)}
|
onChange={event => onChangeTitle(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className='flex gap-6'>
|
<div className='flex gap-6'>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -26,7 +26,7 @@ function TabOperation({ alias, setAlias, title, setTitle, comment, setComment }:
|
||||||
label='Сокращение'
|
label='Сокращение'
|
||||||
className='w-[16rem]'
|
className='w-[16rem]'
|
||||||
value={alias}
|
value={alias}
|
||||||
onChange={event => setAlias(event.target.value)}
|
onChange={event => onChangeAlias(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextArea
|
<TextArea
|
||||||
|
@ -35,7 +35,7 @@ function TabOperation({ alias, setAlias, title, setTitle, comment, setComment }:
|
||||||
noResize
|
noResize
|
||||||
rows={3}
|
rows={3}
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={event => setComment(event.target.value)}
|
onChange={event => onChangeComment(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AnimateFade>
|
</AnimateFade>
|
||||||
|
|
|
@ -45,7 +45,12 @@ function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditRefere
|
||||||
const entityPanel = useMemo(
|
const entityPanel = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<TabEntityReference initial={initial} schema={schema} setReference={setReference} setIsValid={setIsValid} />
|
<TabEntityReference
|
||||||
|
initial={initial}
|
||||||
|
schema={schema}
|
||||||
|
onChangeReference={setReference}
|
||||||
|
onChangeValid={setIsValid}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
),
|
),
|
||||||
[initial, schema]
|
[initial, schema]
|
||||||
|
@ -54,7 +59,7 @@ function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditRefere
|
||||||
const syntacticPanel = useMemo(
|
const syntacticPanel = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<TabSyntacticReference initial={initial} setReference={setReference} setIsValid={setIsValid} />
|
<TabSyntacticReference initial={initial} onChangeReference={setReference} onChangeValid={setIsValid} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
),
|
),
|
||||||
[initial]
|
[initial]
|
||||||
|
|
|
@ -21,11 +21,11 @@ import { IReferenceInputState } from './DlgEditReference';
|
||||||
interface TabEntityReferenceProps {
|
interface TabEntityReferenceProps {
|
||||||
initial: IReferenceInputState;
|
initial: IReferenceInputState;
|
||||||
schema: IRSForm;
|
schema: IRSForm;
|
||||||
setIsValid: React.Dispatch<React.SetStateAction<boolean>>;
|
onChangeValid: (newValue: boolean) => void;
|
||||||
setReference: React.Dispatch<React.SetStateAction<string>>;
|
onChangeReference: (newValue: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabEntityReference({ initial, schema, setIsValid, setReference }: TabEntityReferenceProps) {
|
function TabEntityReference({ initial, schema, onChangeValid, onChangeReference }: TabEntityReferenceProps) {
|
||||||
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined);
|
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined);
|
||||||
const [alias, setAlias] = useState('');
|
const [alias, setAlias] = useState('');
|
||||||
const [term, setTerm] = useState('');
|
const [term, setTerm] = useState('');
|
||||||
|
@ -43,9 +43,9 @@ function TabEntityReference({ initial, schema, setIsValid, setReference }: TabEn
|
||||||
|
|
||||||
// Produce result
|
// Produce result
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsValid(alias !== '' && selectedGrams.length > 0);
|
onChangeValid(alias !== '' && selectedGrams.length > 0);
|
||||||
setReference(`@{${alias}|${selectedGrams.map(gram => gram.value).join(',')}}`);
|
onChangeReference(`@{${alias}|${selectedGrams.map(gram => gram.value).join(',')}}`);
|
||||||
}, [alias, selectedGrams, setIsValid, setReference]);
|
}, [alias, selectedGrams, onChangeValid, onChangeReference]);
|
||||||
|
|
||||||
// Update term when alias changes
|
// Update term when alias changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -105,7 +105,7 @@ function TabEntityReference({ initial, schema, setIsValid, setReference }: TabEn
|
||||||
className='flex-grow'
|
className='flex-grow'
|
||||||
menuPlacement='top'
|
menuPlacement='top'
|
||||||
value={selectedGrams}
|
value={selectedGrams}
|
||||||
setValue={setSelectedGrams}
|
onChangeValue={setSelectedGrams}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AnimateFade>
|
</AnimateFade>
|
||||||
|
|
|
@ -11,11 +11,11 @@ import { IReferenceInputState } from './DlgEditReference';
|
||||||
|
|
||||||
interface TabSyntacticReferenceProps {
|
interface TabSyntacticReferenceProps {
|
||||||
initial: IReferenceInputState;
|
initial: IReferenceInputState;
|
||||||
setIsValid: React.Dispatch<React.SetStateAction<boolean>>;
|
onChangeValid: (newValue: boolean) => void;
|
||||||
setReference: React.Dispatch<React.SetStateAction<string>>;
|
onChangeReference: (newValue: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabSyntacticReference({ initial, setIsValid, setReference }: TabSyntacticReferenceProps) {
|
function TabSyntacticReference({ initial, onChangeValid, onChangeReference }: TabSyntacticReferenceProps) {
|
||||||
const [nominal, setNominal] = useState('');
|
const [nominal, setNominal] = useState('');
|
||||||
const [offset, setOffset] = useState(1);
|
const [offset, setOffset] = useState(1);
|
||||||
|
|
||||||
|
@ -39,9 +39,9 @@ function TabSyntacticReference({ initial, setIsValid, setReference }: TabSyntact
|
||||||
}, [initial]);
|
}, [initial]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsValid(nominal !== '' && offset !== 0);
|
onChangeValid(nominal !== '' && offset !== 0);
|
||||||
setReference(`@{${offset}|${nominal}}`);
|
onChangeReference(`@{${offset}|${nominal}}`);
|
||||||
}, [nominal, offset, setIsValid, setReference]);
|
}, [nominal, offset, onChangeValid, onChangeReference]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimateFade className='flex flex-col gap-2'>
|
<AnimateFade className='flex flex-col gap-2'>
|
||||||
|
|
|
@ -170,7 +170,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
|
||||||
placeholder='Выберите граммемы'
|
placeholder='Выберите граммемы'
|
||||||
className='min-w-[15rem] h-fit'
|
className='min-w-[15rem] h-fit'
|
||||||
value={inputGrams}
|
value={inputGrams}
|
||||||
setValue={setInputGrams}
|
onChangeValue={setInputGrams}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
74
rsconcept/frontend/src/dialogs/DlgShowAST/ASTFlow.tsx
Normal file
74
rsconcept/frontend/src/dialogs/DlgShowAST/ASTFlow.tsx
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useLayoutEffect } from 'react';
|
||||||
|
import { Edge, MarkerType, Node, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
|
import { SyntaxTree } from '@/models/rslang';
|
||||||
|
|
||||||
|
import { ASTEdgeTypes } from './graph/ASTEdgeTypes';
|
||||||
|
import { applyLayout } from './graph/ASTLayout';
|
||||||
|
import { ASTNodeTypes } from './graph/ASTNodeTypes';
|
||||||
|
|
||||||
|
interface ASTFlowProps {
|
||||||
|
data: SyntaxTree;
|
||||||
|
onNodeEnter: (node: Node) => void;
|
||||||
|
onNodeLeave: (node: Node) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ASTFlow({ data, onNodeEnter, onNodeLeave }: ASTFlowProps) {
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
|
const [edges, setEdges] = useEdgesState([]);
|
||||||
|
const flow = useReactFlow();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const newNodes = data.map(node => ({
|
||||||
|
id: String(node.uid),
|
||||||
|
data: node,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
type: 'token'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const newEdges: Edge[] = [];
|
||||||
|
data.forEach(node => {
|
||||||
|
if (node.parent !== node.uid) {
|
||||||
|
newEdges.push({
|
||||||
|
id: String(node.uid),
|
||||||
|
source: String(node.parent),
|
||||||
|
target: String(node.uid),
|
||||||
|
type: 'dynamic',
|
||||||
|
focusable: false,
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
width: 20,
|
||||||
|
height: 20
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
applyLayout(newNodes, newEdges);
|
||||||
|
|
||||||
|
setNodes(newNodes);
|
||||||
|
setEdges(newEdges);
|
||||||
|
}, [data, setNodes, setEdges, flow]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
edgesFocusable={false}
|
||||||
|
nodesFocusable={false}
|
||||||
|
onNodeMouseEnter={(_, node) => onNodeEnter(node)}
|
||||||
|
onNodeMouseLeave={(_, node) => onNodeLeave(node)}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
nodeTypes={ASTNodeTypes}
|
||||||
|
edgeTypes={ASTEdgeTypes}
|
||||||
|
fitView
|
||||||
|
maxZoom={2}
|
||||||
|
minZoom={0.5}
|
||||||
|
nodesConnectable={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ASTFlow;
|
|
@ -1,17 +1,16 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
import { Node } from 'reactflow';
|
||||||
|
|
||||||
import GraphUI, { GraphEdge, GraphNode } from '@/components/ui/GraphUI';
|
|
||||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { SyntaxTree } from '@/models/rslang';
|
import { SyntaxTree } from '@/models/rslang';
|
||||||
import { graphDarkT, graphLightT } from '@/styling/color';
|
|
||||||
import { colorBgSyntaxTree } from '@/styling/color';
|
import ASTFlow from './ASTFlow';
|
||||||
import { resources } from '@/utils/constants';
|
|
||||||
import { labelSyntaxTree } from '@/utils/labels';
|
|
||||||
|
|
||||||
interface DlgShowASTProps extends Pick<ModalProps, 'hideWindow'> {
|
interface DlgShowASTProps extends Pick<ModalProps, 'hideWindow'> {
|
||||||
syntaxTree: SyntaxTree;
|
syntaxTree: SyntaxTree;
|
||||||
|
@ -19,36 +18,11 @@ interface DlgShowASTProps extends Pick<ModalProps, 'hideWindow'> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
||||||
const { darkMode, colors } = useConceptOptions();
|
const { colors } = useConceptOptions();
|
||||||
const [hoverID, setHoverID] = useState<number | undefined>(undefined);
|
const [hoverID, setHoverID] = useState<number | undefined>(undefined);
|
||||||
const hoverNode = useMemo(() => syntaxTree.find(node => node.uid === hoverID), [hoverID, syntaxTree]);
|
const hoverNode = useMemo(() => syntaxTree.find(node => node.uid === hoverID), [hoverID, syntaxTree]);
|
||||||
|
|
||||||
const nodes: GraphNode[] = useMemo(
|
const handleHoverIn = useCallback((node: Node) => setHoverID(Number(node.id)), []);
|
||||||
() =>
|
|
||||||
syntaxTree.map(node => ({
|
|
||||||
id: String(syntaxTree.length - node.uid), // invert order of IDs to force correct ordering in graph layout
|
|
||||||
label: labelSyntaxTree(node),
|
|
||||||
fill: colorBgSyntaxTree(node, colors)
|
|
||||||
})),
|
|
||||||
[syntaxTree, colors]
|
|
||||||
);
|
|
||||||
|
|
||||||
const edges: GraphEdge[] = useMemo(() => {
|
|
||||||
const result: GraphEdge[] = [];
|
|
||||||
syntaxTree.forEach(node => {
|
|
||||||
if (node.parent !== node.uid) {
|
|
||||||
result.push({
|
|
||||||
id: String(node.uid),
|
|
||||||
source: String(syntaxTree.length - node.parent),
|
|
||||||
target: String(syntaxTree.length - node.uid)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}, [syntaxTree]);
|
|
||||||
|
|
||||||
const handleHoverIn = useCallback((node: GraphNode) => setHoverID(syntaxTree.length - Number(node.id)), [syntaxTree]);
|
|
||||||
|
|
||||||
const handleHoverOut = useCallback(() => setHoverID(undefined), []);
|
const handleHoverOut = useCallback(() => setHoverID(undefined), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -72,18 +46,9 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Overlay>
|
</Overlay>
|
||||||
<div className='flex-grow relative'>
|
<ReactFlowProvider>
|
||||||
<GraphUI
|
<ASTFlow data={syntaxTree} onNodeEnter={handleHoverIn} onNodeLeave={handleHoverOut} />
|
||||||
animated={false}
|
</ReactFlowProvider>
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
layoutType='hierarchicalTd'
|
|
||||||
labelFontUrl={resources.graph_font}
|
|
||||||
theme={darkMode ? graphDarkT : graphLightT}
|
|
||||||
onNodePointerOver={handleHoverIn}
|
|
||||||
onNodePointerOut={handleHoverOut}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
28
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTEdge.tsx
Normal file
28
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTEdge.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { EdgeProps, getStraightPath } from 'reactflow';
|
||||||
|
|
||||||
|
const NODE_RADIUS = 20;
|
||||||
|
const EDGE_RADIUS = 25;
|
||||||
|
|
||||||
|
function ASTEdge({ id, markerEnd, style, ...props }: EdgeProps) {
|
||||||
|
const scale =
|
||||||
|
EDGE_RADIUS /
|
||||||
|
Math.sqrt(
|
||||||
|
Math.pow(props.sourceX - props.targetX, 2) +
|
||||||
|
Math.pow(Math.abs(props.sourceY - props.targetY) + 2 * NODE_RADIUS, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [path] = getStraightPath({
|
||||||
|
sourceX: props.sourceX - (props.sourceX - props.targetX) * scale,
|
||||||
|
sourceY: props.sourceY - (props.sourceY - props.targetY - 2 * NODE_RADIUS) * scale - NODE_RADIUS,
|
||||||
|
targetX: props.targetX + (props.sourceX - props.targetX) * scale,
|
||||||
|
targetY: props.targetY + (props.sourceY - props.targetY - 2 * NODE_RADIUS) * scale + NODE_RADIUS
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<path id={id} className='react-flow__edge-path' d={path} markerEnd={markerEnd} style={style} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ASTEdge;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { EdgeTypes } from 'reactflow';
|
||||||
|
|
||||||
|
import ASTEdge from './ASTEdge';
|
||||||
|
|
||||||
|
export const ASTEdgeTypes: EdgeTypes = {
|
||||||
|
dynamic: ASTEdge
|
||||||
|
};
|
35
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTLayout.ts
Normal file
35
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTLayout.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import dagre from '@dagrejs/dagre';
|
||||||
|
import { Edge, Node } from 'reactflow';
|
||||||
|
|
||||||
|
import { ISyntaxTreeNode } from '@/models/rslang';
|
||||||
|
|
||||||
|
const NODE_WIDTH = 44;
|
||||||
|
const NODE_HEIGHT = 44;
|
||||||
|
const HOR_SEPARATION = 40;
|
||||||
|
const VERT_SEPARATION = 40;
|
||||||
|
|
||||||
|
export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
|
||||||
|
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||||
|
dagreGraph.setGraph({
|
||||||
|
rankdir: 'TB',
|
||||||
|
ranksep: VERT_SEPARATION,
|
||||||
|
nodesep: HOR_SEPARATION,
|
||||||
|
ranker: 'network-simplex',
|
||||||
|
align: undefined
|
||||||
|
});
|
||||||
|
nodes.forEach(node => {
|
||||||
|
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||||
|
});
|
||||||
|
|
||||||
|
edges.forEach(edge => {
|
||||||
|
dagreGraph.setEdge(edge.source, edge.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
dagre.layout(dagreGraph);
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const nodeWithPosition = dagreGraph.node(node.id);
|
||||||
|
node.position.x = nodeWithPosition.x - NODE_WIDTH / 2;
|
||||||
|
node.position.y = nodeWithPosition.y - NODE_HEIGHT / 2;
|
||||||
|
});
|
||||||
|
}
|
44
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTNode.tsx
Normal file
44
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTNode.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
|
||||||
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
|
import { ISyntaxTreeNode } from '@/models/rslang';
|
||||||
|
import { colorBgSyntaxTree } from '@/styling/color';
|
||||||
|
import { labelSyntaxTree } from '@/utils/labels';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents graph AST node internal data.
|
||||||
|
*/
|
||||||
|
interface ASTNodeInternal {
|
||||||
|
id: string;
|
||||||
|
data: ISyntaxTreeNode;
|
||||||
|
dragging: boolean;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ASTNode(node: ASTNodeInternal) {
|
||||||
|
const { colors } = useConceptOptions();
|
||||||
|
const label = useMemo(() => labelSyntaxTree(node.data), [node.data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Handle type='target' position={Position.Top} style={{ opacity: 0 }} />
|
||||||
|
<div
|
||||||
|
className='w-full h-full cursor-default flex items-center justify-center rounded-full'
|
||||||
|
style={{ backgroundColor: colorBgSyntaxTree(node.data, colors) }}
|
||||||
|
/>
|
||||||
|
<Handle type='source' position={Position.Bottom} style={{ opacity: 0 }} />
|
||||||
|
<div
|
||||||
|
className='font-math mt-1 w-fit px-1 text-center translate-x-[calc(-50%+20px)]'
|
||||||
|
style={{ backgroundColor: colors.bgDefault, fontSize: label.length > 3 ? 12 : 14 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ASTNode;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { NodeTypes } from 'reactflow';
|
||||||
|
|
||||||
|
import ASTNode from './ASTNode';
|
||||||
|
|
||||||
|
export const ASTNodeTypes: NodeTypes = {
|
||||||
|
token: ASTNode
|
||||||
|
};
|
1
rsconcept/frontend/src/dialogs/DlgShowAST/index.tsx
Normal file
1
rsconcept/frontend/src/dialogs/DlgShowAST/index.tsx
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './DlgShowAST';
|
|
@ -4,7 +4,6 @@ import { useLayoutEffect } from 'react';
|
||||||
import { Edge, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow';
|
import { Edge, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
import { TMGraph } from '@/models/TMGraph';
|
import { TMGraph } from '@/models/TMGraph';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
|
|
||||||
import { TMGraphEdgeTypes } from './graph/MGraphEdgeTypes';
|
import { TMGraphEdgeTypes } from './graph/MGraphEdgeTypes';
|
||||||
import { applyLayout } from './graph/MGraphLayout';
|
import { applyLayout } from './graph/MGraphLayout';
|
||||||
|
@ -15,7 +14,7 @@ interface MGraphFlowProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function MGraphFlow({ data }: MGraphFlowProps) {
|
function MGraphFlow({ data }: MGraphFlowProps) {
|
||||||
const [nodes, setNodes] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges] = useEdgesState([]);
|
const [edges, setEdges] = useEdgesState([]);
|
||||||
const flow = useReactFlow();
|
const flow = useReactFlow();
|
||||||
|
|
||||||
|
@ -49,11 +48,10 @@ function MGraphFlow({ data }: MGraphFlowProps) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
applyLayout(newNodes);
|
applyLayout(newNodes, newEdges);
|
||||||
|
|
||||||
setNodes(newNodes);
|
setNodes(newNodes);
|
||||||
setEdges(newEdges);
|
setEdges(newEdges);
|
||||||
flow.fitView({ duration: PARAMETER.zoomDuration });
|
|
||||||
}, [data, setNodes, setEdges, flow]);
|
}, [data, setNodes, setEdges, flow]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -62,14 +60,13 @@ function MGraphFlow({ data }: MGraphFlowProps) {
|
||||||
edges={edges}
|
edges={edges}
|
||||||
edgesFocusable={false}
|
edgesFocusable={false}
|
||||||
nodesFocusable={false}
|
nodesFocusable={false}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
nodeTypes={TMGraphNodeTypes}
|
nodeTypes={TMGraphNodeTypes}
|
||||||
edgeTypes={TMGraphEdgeTypes}
|
edgeTypes={TMGraphEdgeTypes}
|
||||||
fitView
|
fitView
|
||||||
maxZoom={2}
|
maxZoom={2}
|
||||||
minZoom={0.5}
|
minZoom={0.5}
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
snapToGrid={true}
|
|
||||||
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,171 +1,38 @@
|
||||||
import { Node } from 'reactflow';
|
import dagre from '@dagrejs/dagre';
|
||||||
|
import { Edge, Node } from 'reactflow';
|
||||||
|
|
||||||
import { Graph } from '@/models/Graph';
|
|
||||||
import { TMGraphNode } from '@/models/TMGraph';
|
import { TMGraphNode } from '@/models/TMGraph';
|
||||||
|
|
||||||
export function applyLayout(nodes: Node<TMGraphNode>[]) {
|
const NODE_WIDTH = 44;
|
||||||
new LayoutManager(nodes).execute();
|
const NODE_HEIGHT = 44;
|
||||||
}
|
const HOR_SEPARATION = 40;
|
||||||
|
const VERT_SEPARATION = 40;
|
||||||
|
|
||||||
const UNIT_HEIGHT = 100;
|
const BOOLEAN_WEIGHT = 2;
|
||||||
const UNIT_WIDTH = 100;
|
const CARTESIAN_WEIGHT = 1;
|
||||||
const MIN_NODE_DISTANCE = 80;
|
|
||||||
const LAYOUT_ITERATIONS = 8;
|
|
||||||
|
|
||||||
class LayoutManager {
|
export function applyLayout(nodes: Node<TMGraphNode>[], edges: Edge[]) {
|
||||||
nodes: Node<TMGraphNode>[];
|
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||||
|
dagreGraph.setGraph({
|
||||||
graph = new Graph();
|
rankdir: 'BT',
|
||||||
ranks = new Map<number, number>();
|
ranksep: VERT_SEPARATION,
|
||||||
posX = new Map<number, number>();
|
nodesep: HOR_SEPARATION,
|
||||||
posY = new Map<number, number>();
|
ranker: 'network-simplex',
|
||||||
|
align: undefined
|
||||||
maxRank = 0;
|
|
||||||
virtualCount = 0;
|
|
||||||
layers: number[][] = [];
|
|
||||||
|
|
||||||
constructor(nodes: Node<TMGraphNode>[]) {
|
|
||||||
this.nodes = nodes;
|
|
||||||
this.prepareGraph();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Prepares graph for layout calculations.
|
|
||||||
*
|
|
||||||
* Assumes that nodes are already topologically sorted.
|
|
||||||
* 1. Adds nodes to graph.
|
|
||||||
* 2. Adds elementary edges to graph.
|
|
||||||
* 3. Splits non-elementary edges via virtual nodes.
|
|
||||||
*/
|
|
||||||
private prepareGraph(): void {
|
|
||||||
this.nodes.forEach(node => {
|
|
||||||
if (this.maxRank < node.data.rank) {
|
|
||||||
this.maxRank = node.data.rank;
|
|
||||||
}
|
|
||||||
const nodeID = node.data.id;
|
|
||||||
this.ranks.set(nodeID, node.data.rank);
|
|
||||||
this.graph.addNode(nodeID);
|
|
||||||
if (node.data.parents.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visited = new Set<number>();
|
|
||||||
node.data.parents.forEach(parent => {
|
|
||||||
if (!visited.has(parent)) {
|
|
||||||
visited.add(parent);
|
|
||||||
let target = nodeID;
|
|
||||||
let currentRank = node.data.rank;
|
|
||||||
const parentRank = this.ranks.get(parent)!;
|
|
||||||
while (currentRank - 1 > parentRank) {
|
|
||||||
currentRank = currentRank - 1;
|
|
||||||
|
|
||||||
this.virtualCount = this.virtualCount + 1;
|
|
||||||
this.ranks.set(-this.virtualCount, currentRank);
|
|
||||||
this.graph.addEdge(-this.virtualCount, target);
|
|
||||||
target = -this.virtualCount;
|
|
||||||
}
|
|
||||||
this.graph.addEdge(parent, target);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
nodes.forEach(node => {
|
||||||
|
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||||
|
});
|
||||||
|
|
||||||
|
edges.forEach(edge => {
|
||||||
|
dagreGraph.setEdge(edge.source, edge.target, { weight: edge.data ? CARTESIAN_WEIGHT : BOOLEAN_WEIGHT });
|
||||||
|
});
|
||||||
|
|
||||||
|
dagre.layout(dagreGraph);
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const nodeWithPosition = dagreGraph.node(node.id);
|
||||||
|
node.position.x = nodeWithPosition.x - NODE_WIDTH / 2;
|
||||||
|
node.position.y = nodeWithPosition.y - NODE_HEIGHT / 2;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
this.calculateLayers();
|
|
||||||
this.calculatePositions();
|
|
||||||
this.savePositions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateLayers(): void {
|
|
||||||
this.initLayers();
|
|
||||||
// TODO: implement ordering algorithm iterations
|
|
||||||
}
|
|
||||||
|
|
||||||
private initLayers(): void {
|
|
||||||
this.layers = Array.from({ length: this.maxRank + 1 }, () => []);
|
|
||||||
|
|
||||||
const visited = new Set<number>();
|
|
||||||
const dfs = (nodeID: number) => {
|
|
||||||
if (visited.has(nodeID)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
visited.add(nodeID);
|
|
||||||
this.layers[this.ranks.get(nodeID)!].push(nodeID);
|
|
||||||
this.graph.at(nodeID)!.outputs.forEach(dfs);
|
|
||||||
};
|
|
||||||
|
|
||||||
const simpleNodes = this.nodes
|
|
||||||
.filter(node => node.data.rank === 0)
|
|
||||||
.sort((a, b) => a.data.text.localeCompare(b.data.text))
|
|
||||||
.map(node => node.data.id);
|
|
||||||
|
|
||||||
simpleNodes.forEach(dfs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculatePositions(): void {
|
|
||||||
this.initPositions();
|
|
||||||
|
|
||||||
for (let i = 0; i < LAYOUT_ITERATIONS; i++) {
|
|
||||||
this.fixLayersPositions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fixLayersPositions(): void {
|
|
||||||
for (let rank = 1; rank <= this.maxRank; rank++) {
|
|
||||||
this.layers[rank].reverse().forEach(nodeID => {
|
|
||||||
const inputs = this.graph.at(nodeID)!.inputs;
|
|
||||||
const currentPos = this.posX.get(nodeID)!;
|
|
||||||
if (inputs.length === 1) {
|
|
||||||
const parent = inputs[0];
|
|
||||||
const parentPos = this.posX.get(parent)!;
|
|
||||||
if (currentPos === parentPos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentPos > parentPos) {
|
|
||||||
this.tryMoveNodeX(parent, currentPos);
|
|
||||||
} else {
|
|
||||||
this.tryMoveNodeX(nodeID, parentPos);
|
|
||||||
}
|
|
||||||
} else if (inputs.length % 2 === 1) {
|
|
||||||
const median = inputs[Math.floor(inputs.length / 2)];
|
|
||||||
const medianPos = this.posX.get(median)!;
|
|
||||||
if (currentPos === medianPos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.tryMoveNodeX(nodeID, medianPos);
|
|
||||||
} else {
|
|
||||||
const median1 = inputs[Math.floor(inputs.length / 2)];
|
|
||||||
const median2 = inputs[Math.floor(inputs.length / 2) - 1];
|
|
||||||
const medianPos = (this.posX.get(median1)! + this.posX.get(median2)!) / 2;
|
|
||||||
this.tryMoveNodeX(nodeID, medianPos);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private tryMoveNodeX(nodeID: number, targetX: number) {
|
|
||||||
const rank = this.ranks.get(nodeID)!;
|
|
||||||
if (this.layers[rank].some(id => id !== nodeID && Math.abs(targetX - this.posX.get(id)!) < MIN_NODE_DISTANCE)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.posX.set(nodeID, targetX);
|
|
||||||
}
|
|
||||||
|
|
||||||
private initPositions(): void {
|
|
||||||
this.layers.forEach((layer, rank) => {
|
|
||||||
layer.forEach((nodeID, index) => {
|
|
||||||
this.posX.set(nodeID, index * UNIT_WIDTH);
|
|
||||||
this.posY.set(nodeID, -rank * UNIT_HEIGHT);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private savePositions(): void {
|
|
||||||
this.nodes.forEach(node => {
|
|
||||||
const nodeID = node.data.id;
|
|
||||||
node.position = {
|
|
||||||
x: this.posX.get(nodeID)!,
|
|
||||||
y: this.posY.get(nodeID)!
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,10 +4,21 @@ import { useMemo } from 'react';
|
||||||
import { Handle, Position } from 'reactflow';
|
import { Handle, Position } from 'reactflow';
|
||||||
|
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
import { MGraphNodeInternal } from '@/models/miscellaneous';
|
import { TMGraphNode } from '@/models/TMGraph';
|
||||||
import { colorBgTMGraphNode } from '@/styling/color';
|
import { colorBgTMGraphNode } from '@/styling/color';
|
||||||
import { globals } from '@/utils/constants';
|
import { globals } from '@/utils/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents graph TMGraph node internal data.
|
||||||
|
*/
|
||||||
|
interface MGraphNodeInternal {
|
||||||
|
id: string;
|
||||||
|
data: TMGraphNode;
|
||||||
|
dragging: boolean;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
}
|
||||||
|
|
||||||
function MGraphNode(node: MGraphNodeInternal) {
|
function MGraphNode(node: MGraphNodeInternal) {
|
||||||
const { colors } = useConceptOptions();
|
const { colors } = useConceptOptions();
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { EdgeProps, Node } from 'reactflow';
|
||||||
|
|
||||||
import { LibraryItemType, LocationHead } from './library';
|
import { LibraryItemType, LocationHead } from './library';
|
||||||
import { IOperation } from './oss';
|
import { IOperation } from './oss';
|
||||||
import { TMGraphNode } from './TMGraph';
|
|
||||||
import { UserID } from './user';
|
import { UserID } from './user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,17 +45,6 @@ export interface OssNodeInternal {
|
||||||
yPos: number;
|
yPos: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents graph TMGraph node internal data.
|
|
||||||
*/
|
|
||||||
export interface MGraphNodeInternal {
|
|
||||||
id: string;
|
|
||||||
data: TMGraphNode;
|
|
||||||
dragging: boolean;
|
|
||||||
xPos: number;
|
|
||||||
yPos: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents graph TMGraph edge internal data.
|
* Represents graph TMGraph edge internal data.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -150,8 +150,8 @@ function LibraryPage() {
|
||||||
const viewLocations = useMemo(
|
const viewLocations = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<ViewSideLocation
|
<ViewSideLocation
|
||||||
active={options.location}
|
activeLocation={options.location}
|
||||||
setActive={options.setLocation}
|
onChangeActiveLocation={options.setLocation}
|
||||||
subfolders={subfolders}
|
subfolders={subfolders}
|
||||||
folderTree={library.folders}
|
folderTree={library.folders}
|
||||||
toggleFolderMode={toggleFolderMode}
|
toggleFolderMode={toggleFolderMode}
|
||||||
|
@ -200,11 +200,11 @@ function LibraryPage() {
|
||||||
filtered={items.length}
|
filtered={items.length}
|
||||||
hasCustomFilter={hasCustomFilter}
|
hasCustomFilter={hasCustomFilter}
|
||||||
query={query}
|
query={query}
|
||||||
setQuery={setQuery}
|
onChangeQuery={setQuery}
|
||||||
path={path}
|
path={path}
|
||||||
setPath={setPath}
|
onChangePath={setPath}
|
||||||
head={head}
|
head={head}
|
||||||
setHead={setHead}
|
onChangeHead={setHead}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
isOwned={isOwned}
|
isOwned={isOwned}
|
||||||
toggleOwned={toggleOwned}
|
toggleOwned={toggleOwned}
|
||||||
|
@ -212,7 +212,7 @@ function LibraryPage() {
|
||||||
isEditor={isEditor}
|
isEditor={isEditor}
|
||||||
toggleEditor={toggleEditor}
|
toggleEditor={toggleEditor}
|
||||||
filterUser={filterUser}
|
filterUser={filterUser}
|
||||||
setFilterUser={setFilterUser}
|
onChangeFilterUser={setFilterUser}
|
||||||
resetFilter={resetFilter}
|
resetFilter={resetFilter}
|
||||||
folderMode={options.folderMode}
|
folderMode={options.folderMode}
|
||||||
toggleFolderMode={toggleFolderMode}
|
toggleFolderMode={toggleFolderMode}
|
||||||
|
|
|
@ -36,11 +36,11 @@ interface ToolbarSearchProps {
|
||||||
hasCustomFilter: boolean;
|
hasCustomFilter: boolean;
|
||||||
|
|
||||||
query: string;
|
query: string;
|
||||||
setQuery: React.Dispatch<React.SetStateAction<string>>;
|
onChangeQuery: (newValue: string) => void;
|
||||||
path: string;
|
path: string;
|
||||||
setPath: React.Dispatch<React.SetStateAction<string>>;
|
onChangePath: (newValue: string) => void;
|
||||||
head: LocationHead | undefined;
|
head: LocationHead | undefined;
|
||||||
setHead: React.Dispatch<React.SetStateAction<LocationHead | undefined>>;
|
onChangeHead: (newValue: LocationHead | undefined) => void;
|
||||||
|
|
||||||
folderMode: boolean;
|
folderMode: boolean;
|
||||||
toggleFolderMode: () => void;
|
toggleFolderMode: () => void;
|
||||||
|
@ -52,7 +52,7 @@ interface ToolbarSearchProps {
|
||||||
isEditor: boolean | undefined;
|
isEditor: boolean | undefined;
|
||||||
toggleEditor: () => void;
|
toggleEditor: () => void;
|
||||||
filterUser: UserID | undefined;
|
filterUser: UserID | undefined;
|
||||||
setFilterUser: React.Dispatch<React.SetStateAction<UserID | undefined>>;
|
onChangeFilterUser: (newValue: UserID | undefined) => void;
|
||||||
|
|
||||||
resetFilter: () => void;
|
resetFilter: () => void;
|
||||||
}
|
}
|
||||||
|
@ -63,11 +63,11 @@ function ToolbarSearch({
|
||||||
hasCustomFilter,
|
hasCustomFilter,
|
||||||
|
|
||||||
query,
|
query,
|
||||||
setQuery,
|
onChangeQuery,
|
||||||
path,
|
path,
|
||||||
setPath,
|
onChangePath,
|
||||||
head,
|
head,
|
||||||
setHead,
|
onChangeHead,
|
||||||
|
|
||||||
folderMode,
|
folderMode,
|
||||||
toggleFolderMode,
|
toggleFolderMode,
|
||||||
|
@ -79,7 +79,7 @@ function ToolbarSearch({
|
||||||
isEditor,
|
isEditor,
|
||||||
toggleEditor,
|
toggleEditor,
|
||||||
filterUser,
|
filterUser,
|
||||||
setFilterUser,
|
onChangeFilterUser,
|
||||||
|
|
||||||
resetFilter
|
resetFilter
|
||||||
}: ToolbarSearchProps) {
|
}: ToolbarSearchProps) {
|
||||||
|
@ -95,9 +95,9 @@ function ToolbarSearch({
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(newValue: LocationHead | undefined) => {
|
(newValue: LocationHead | undefined) => {
|
||||||
headMenu.hide();
|
headMenu.hide();
|
||||||
setHead(newValue);
|
onChangeHead(newValue);
|
||||||
},
|
},
|
||||||
[headMenu, setHead]
|
[headMenu, onChangeHead]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleFolder = useCallback(() => {
|
const handleToggleFolder = useCallback(() => {
|
||||||
|
@ -170,7 +170,7 @@ function ToolbarSearch({
|
||||||
className='min-w-[15rem] text-sm'
|
className='min-w-[15rem] text-sm'
|
||||||
items={users}
|
items={users}
|
||||||
value={filterUser}
|
value={filterUser}
|
||||||
onSelectValue={setFilterUser}
|
onSelectValue={onChangeFilterUser}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -190,8 +190,8 @@ function ToolbarSearch({
|
||||||
placeholder='Поиск'
|
placeholder='Поиск'
|
||||||
noBorder
|
noBorder
|
||||||
className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'flex-grow')}
|
className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'flex-grow')}
|
||||||
value={query}
|
query={query}
|
||||||
onChange={setQuery}
|
onChangeQuery={onChangeQuery}
|
||||||
/>
|
/>
|
||||||
{!folderMode ? (
|
{!folderMode ? (
|
||||||
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
|
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
|
||||||
|
@ -249,8 +249,8 @@ function ToolbarSearch({
|
||||||
noIcon
|
noIcon
|
||||||
noBorder
|
noBorder
|
||||||
className='w-[4.5rem] sm:w-[5rem] flex-grow'
|
className='w-[4.5rem] sm:w-[5rem] flex-grow'
|
||||||
value={path}
|
query={path}
|
||||||
onChange={setPath}
|
onChangeQuery={onChangePath}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,8 +22,8 @@ import { information } from '@/utils/labels';
|
||||||
interface ViewSideLocationProps {
|
interface ViewSideLocationProps {
|
||||||
folderTree: FolderTree;
|
folderTree: FolderTree;
|
||||||
subfolders: boolean;
|
subfolders: boolean;
|
||||||
active: string;
|
activeLocation: string;
|
||||||
setActive: React.Dispatch<React.SetStateAction<string>>;
|
onChangeActiveLocation: (newValue: string) => void;
|
||||||
toggleFolderMode: () => void;
|
toggleFolderMode: () => void;
|
||||||
toggleSubfolders: () => void;
|
toggleSubfolders: () => void;
|
||||||
onRenameLocation: () => void;
|
onRenameLocation: () => void;
|
||||||
|
@ -31,9 +31,9 @@ interface ViewSideLocationProps {
|
||||||
|
|
||||||
function ViewSideLocation({
|
function ViewSideLocation({
|
||||||
folderTree,
|
folderTree,
|
||||||
active,
|
activeLocation,
|
||||||
subfolders,
|
subfolders,
|
||||||
setActive: setActive,
|
onChangeActiveLocation,
|
||||||
toggleFolderMode,
|
toggleFolderMode,
|
||||||
toggleSubfolders,
|
toggleSubfolders,
|
||||||
onRenameLocation
|
onRenameLocation
|
||||||
|
@ -44,16 +44,18 @@ function ViewSideLocation({
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
|
|
||||||
const canRename = useMemo(() => {
|
const canRename = useMemo(() => {
|
||||||
if (active.length <= 3 || !user) {
|
if (activeLocation.length <= 3 || !user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (user.is_staff) {
|
if (user.is_staff) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const owned = items.filter(item => item.owner == user.id);
|
const owned = items.filter(item => item.owner == user.id);
|
||||||
const located = owned.filter(item => item.location == active || item.location.startsWith(`${active}/`));
|
const located = owned.filter(
|
||||||
|
item => item.location == activeLocation || item.location.startsWith(`${activeLocation}/`)
|
||||||
|
);
|
||||||
return located.length !== 0;
|
return located.length !== 0;
|
||||||
}, [active, user, items]);
|
}, [activeLocation, user, items]);
|
||||||
|
|
||||||
const animations = useMemo(() => animateSideMinWidth(windowSize.isSmall ? '10rem' : '15rem'), [windowSize]);
|
const animations = useMemo(() => animateSideMinWidth(windowSize.isSmall ? '10rem' : '15rem'), [windowSize]);
|
||||||
const maxHeight = useMemo(() => calculateHeight('4.5rem'), [calculateHeight]);
|
const maxHeight = useMemo(() => calculateHeight('4.5rem'), [calculateHeight]);
|
||||||
|
@ -68,10 +70,10 @@ function ViewSideLocation({
|
||||||
.then(() => toast.success(information.pathReady))
|
.then(() => toast.success(information.pathReady))
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
} else {
|
} else {
|
||||||
setActive(target.getPath());
|
onChangeActiveLocation(target.getPath());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setActive]
|
[onChangeActiveLocation]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -109,7 +111,7 @@ function ViewSideLocation({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SelectLocation
|
<SelectLocation
|
||||||
value={active}
|
value={activeLocation}
|
||||||
folderTree={folderTree}
|
folderTree={folderTree}
|
||||||
prefix={prefixes.folders_list}
|
prefix={prefixes.folders_list}
|
||||||
onClick={handleClickFolder}
|
onClick={handleClickFolder}
|
||||||
|
|
|
@ -69,7 +69,7 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
|
||||||
<SelectTree
|
<SelectTree
|
||||||
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
|
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
|
||||||
value={activeTopic}
|
value={activeTopic}
|
||||||
setValue={handleSelectTopic}
|
onChangeValue={handleSelectTopic}
|
||||||
prefix={prefixes.topic_list}
|
prefix={prefixes.topic_list}
|
||||||
getParent={item => topicParent.get(item) ?? item}
|
getParent={item => topicParent.get(item) ?? item}
|
||||||
getLabel={labelHelpTopic}
|
getLabel={labelHelpTopic}
|
||||||
|
|
|
@ -17,7 +17,7 @@ function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
|
||||||
<SelectTree
|
<SelectTree
|
||||||
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
|
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
|
||||||
value={activeTopic}
|
value={activeTopic}
|
||||||
setValue={onChangeTopic}
|
onChangeValue={onChangeTopic}
|
||||||
prefix={prefixes.topic_list}
|
prefix={prefixes.topic_list}
|
||||||
getParent={item => topicParent.get(item) ?? item}
|
getParent={item => topicParent.get(item) ?? item}
|
||||||
getLabel={labelHelpTopic}
|
getLabel={labelHelpTopic}
|
||||||
|
|
|
@ -28,7 +28,7 @@ function HelpFormulaTree() {
|
||||||
<span style={{ backgroundColor: colors.bgRed }}>присвоение и итерация</span>
|
<span style={{ backgroundColor: colors.bgRed }}>присвоение и итерация</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span style={{ backgroundColor: '#7ca0ab' }}>составные выражения</span>
|
<span style={{ backgroundColor: colors.bgDisabled }}>составные выражения</span>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,7 +15,8 @@ import {
|
||||||
IconSave,
|
IconSave,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconStatusOK,
|
IconStatusOK,
|
||||||
IconTree
|
IconTree,
|
||||||
|
IconTypeGraph
|
||||||
} from '@/components/Icons';
|
} from '@/components/Icons';
|
||||||
import LinkTopic from '@/components/ui/LinkTopic';
|
import LinkTopic from '@/components/ui/LinkTopic';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
|
@ -90,6 +91,10 @@ function HelpRSEditor() {
|
||||||
<li>
|
<li>
|
||||||
<IconControls className='inline-icon' /> специальная клавиатура и горячие клавиши
|
<IconControls className='inline-icon' /> специальная клавиатура и горячие клавиши
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconTypeGraph className='inline-icon' /> отображение{' '}
|
||||||
|
<LinkTopic text='графа ступеней типизации' topic={HelpTopic.UI_TYPE_GRAPH} />
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconTree className='inline-icon' /> отображение{' '}
|
<IconTree className='inline-icon' /> отображение{' '}
|
||||||
<LinkTopic text='дерева разбора' topic={HelpTopic.UI_FORMULA_TREE} />
|
<LinkTopic text='дерева разбора' topic={HelpTopic.UI_FORMULA_TREE} />
|
||||||
|
|
|
@ -15,7 +15,7 @@ import OssStats from './OssStats';
|
||||||
|
|
||||||
interface EditorOssCardProps {
|
interface EditorOssCardProps {
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModified: (newValue: boolean) => void;
|
||||||
onDestroy: () => void;
|
onDestroy: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { useOssEdit } from '../OssEditContext';
|
||||||
interface FormOSSProps {
|
interface FormOSSProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModified: (newValue: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
|
function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import OssFlow from './OssFlow';
|
||||||
|
|
||||||
interface EditorOssGraphProps {
|
interface EditorOssGraphProps {
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModified: (newValue: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
|
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
|
||||||
|
|
|
@ -34,7 +34,7 @@ import ToolbarOssGraph from './ToolbarOssGraph';
|
||||||
|
|
||||||
interface OssFlowProps {
|
interface OssFlowProps {
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModified: (newValue: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
|
|
|
@ -55,7 +55,7 @@ export interface IOssEditContext extends ILibraryItemEditor {
|
||||||
isAttachedToOSS: boolean;
|
isAttachedToOSS: boolean;
|
||||||
|
|
||||||
showTooltip: boolean;
|
showTooltip: boolean;
|
||||||
setShowTooltip: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowTooltip: (newValue: boolean) => void;
|
||||||
|
|
||||||
setOwner: (newOwner: UserID) => void;
|
setOwner: (newOwner: UserID) => void;
|
||||||
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
||||||
|
|
|
@ -21,7 +21,7 @@ const SIDELIST_LAYOUT_THRESHOLD = 1000; // px
|
||||||
interface EditorConstituentaProps {
|
interface EditorConstituentaProps {
|
||||||
activeCst?: IConstituenta;
|
activeCst?: IConstituenta;
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModified: (newValue: boolean) => void;
|
||||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ interface FormConstituentaProps {
|
||||||
|
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
toggleReset: boolean;
|
toggleReset: boolean;
|
||||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModified: (newValue: boolean) => void;
|
||||||
|
|
||||||
onRename: () => void;
|
onRename: () => void;
|
||||||
onEditTerm: () => void;
|
onEditTerm: () => void;
|
||||||
|
@ -144,7 +144,7 @@ function FormConstituenta({
|
||||||
cstUpdate(data, () => toast.success(information.changesSaved));
|
cstUpdate(data, () => toast.success(information.changesSaved));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTypificationClick(event: CProps.EventMouse) {
|
function handleTypeGraph(event: CProps.EventMouse) {
|
||||||
if (!state || (localParse && !localParse.parseResult) || state.parse.status !== ParsingStatus.VERIFIED) {
|
if (!state || (localParse && !localParse.parseResult) || state.parse.status !== ParsingStatus.VERIFIED) {
|
||||||
toast.error(errors.typeStructureFailed);
|
toast.error(errors.typeStructureFailed);
|
||||||
return;
|
return;
|
||||||
|
@ -196,10 +196,8 @@ function FormConstituenta({
|
||||||
noOutline
|
noOutline
|
||||||
readOnly
|
readOnly
|
||||||
label='Типизация'
|
label='Типизация'
|
||||||
title='Отобразить структуру типизации'
|
|
||||||
value={typification}
|
value={typification}
|
||||||
colors='clr-app clr-text-default cursor-pointer'
|
colors='clr-app clr-text-default cursor-default'
|
||||||
onClick={event => handleTypificationClick(event)}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{state ? (
|
{state ? (
|
||||||
|
@ -225,10 +223,11 @@ function FormConstituenta({
|
||||||
activeCst={state}
|
activeCst={state}
|
||||||
disabled={disabled || state.is_inherited}
|
disabled={disabled || state.is_inherited}
|
||||||
toggleReset={toggleReset}
|
toggleReset={toggleReset}
|
||||||
onChange={newValue => setExpression(newValue)}
|
onChangeExpression={newValue => setExpression(newValue)}
|
||||||
setTypification={setTypification}
|
onChangeTypification={setTypification}
|
||||||
setLocalParse={setLocalParse}
|
onChangeLocalParse={setLocalParse}
|
||||||
onOpenEdit={onOpenEdit}
|
onOpenEdit={onOpenEdit}
|
||||||
|
onShowTypeGraph={handleTypeGraph}
|
||||||
/>
|
/>
|
||||||
</AnimateFade>
|
</AnimateFade>
|
||||||
<AnimateFade key='cst_definition_fade' hideContent={!state.definition_raw && isElementary}>
|
<AnimateFade key='cst_definition_fade' hideContent={!state.definition_raw && isElementary}>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
|
@ -39,10 +39,11 @@ interface EditorRSExpressionProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
toggleReset?: boolean;
|
toggleReset?: boolean;
|
||||||
|
|
||||||
setTypification: (typification: string) => void;
|
onChangeTypification: (typification: string) => void;
|
||||||
setLocalParse: React.Dispatch<React.SetStateAction<IExpressionParse | undefined>>;
|
onChangeLocalParse: (typification: IExpressionParse | undefined) => void;
|
||||||
onChange: (newValue: string) => void;
|
onChangeExpression: (newValue: string) => void;
|
||||||
onOpenEdit?: (cstID: ConstituentaID) => void;
|
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||||
|
onShowTypeGraph: (event: CProps.EventMouse) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorRSExpression({
|
function EditorRSExpression({
|
||||||
|
@ -50,10 +51,11 @@ function EditorRSExpression({
|
||||||
disabled,
|
disabled,
|
||||||
value,
|
value,
|
||||||
toggleReset,
|
toggleReset,
|
||||||
setTypification,
|
onChangeTypification,
|
||||||
setLocalParse,
|
onChangeLocalParse,
|
||||||
onChange,
|
onChangeExpression,
|
||||||
onOpenEdit,
|
onOpenEdit,
|
||||||
|
onShowTypeGraph,
|
||||||
...restProps
|
...restProps
|
||||||
}: EditorRSExpressionProps) {
|
}: EditorRSExpressionProps) {
|
||||||
const model = useRSForm();
|
const model = useRSForm();
|
||||||
|
@ -74,20 +76,20 @@ function EditorRSExpression({
|
||||||
}, [activeCst, resetParse, toggleReset]);
|
}, [activeCst, resetParse, toggleReset]);
|
||||||
|
|
||||||
function handleChange(newValue: string) {
|
function handleChange(newValue: string) {
|
||||||
onChange(newValue);
|
onChangeExpression(newValue);
|
||||||
setIsModified(newValue !== activeCst.definition_formal);
|
setIsModified(newValue !== activeCst.definition_formal);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCheckExpression(callback?: (parse: IExpressionParse) => void) {
|
function handleCheckExpression(callback?: (parse: IExpressionParse) => void) {
|
||||||
parser.checkConstituenta(value, activeCst, parse => {
|
parser.checkConstituenta(value, activeCst, parse => {
|
||||||
setLocalParse(parse);
|
onChangeLocalParse(parse);
|
||||||
if (parse.errors.length > 0) {
|
if (parse.errors.length > 0) {
|
||||||
onShowError(parse.errors[0], parse.prefixLen);
|
onShowError(parse.errors[0], parse.prefixLen);
|
||||||
} else {
|
} else {
|
||||||
rsInput.current?.view?.focus();
|
rsInput.current?.view?.focus();
|
||||||
}
|
}
|
||||||
setIsModified(false);
|
setIsModified(false);
|
||||||
setTypification(
|
onChangeTypification(
|
||||||
labelTypification({
|
labelTypification({
|
||||||
isValid: parse.parseResult,
|
isValid: parse.parseResult,
|
||||||
resultType: parse.typification,
|
resultType: parse.typification,
|
||||||
|
@ -171,6 +173,7 @@ function EditorRSExpression({
|
||||||
showControls={showControls}
|
showControls={showControls}
|
||||||
showAST={handleShowAST}
|
showAST={handleShowAST}
|
||||||
toggleControls={() => setShowControls(prev => !prev)}
|
toggleControls={() => setShowControls(prev => !prev)}
|
||||||
|
showTypeGraph={onShowTypeGraph}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Overlay
|
<Overlay
|
||||||
|
|
|
@ -54,7 +54,7 @@ function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }:
|
||||||
onClick={onAnalyze}
|
onClick={onAnalyze}
|
||||||
>
|
>
|
||||||
<AnimatePresence mode='wait'>
|
<AnimatePresence mode='wait'>
|
||||||
{processing ? <Loader key='status-loader' size={3} /> : null}
|
{processing ? <Loader key='status-loader' scale={3} /> : null}
|
||||||
{!processing ? (
|
{!processing ? (
|
||||||
<>
|
<>
|
||||||
<StatusIcon key='status-icon' size='1rem' value={status} />
|
<StatusIcon key='status-icon' size='1rem' value={status} />
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { IconControls, IconTree } from '@/components/Icons';
|
import { IconControls, IconTree, IconTypeGraph } from '@/components/Icons';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
@ -10,9 +10,16 @@ interface ToolbarRSExpressionProps {
|
||||||
|
|
||||||
toggleControls: () => void;
|
toggleControls: () => void;
|
||||||
showAST: (event: CProps.EventMouse) => void;
|
showAST: (event: CProps.EventMouse) => void;
|
||||||
|
showTypeGraph: (event: CProps.EventMouse) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarRSExpression({ disabled, showControls, toggleControls, showAST }: ToolbarRSExpressionProps) {
|
function ToolbarRSExpression({
|
||||||
|
disabled,
|
||||||
|
showControls,
|
||||||
|
showTypeGraph,
|
||||||
|
toggleControls,
|
||||||
|
showAST
|
||||||
|
}: ToolbarRSExpressionProps) {
|
||||||
const model = useRSForm();
|
const model = useRSForm();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -24,6 +31,11 @@ function ToolbarRSExpression({ disabled, showControls, toggleControls, showAST }
|
||||||
onClick={toggleControls}
|
onClick={toggleControls}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
<MiniButton
|
||||||
|
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
|
||||||
|
title='Граф ступеней типизации'
|
||||||
|
onClick={showTypeGraph}
|
||||||
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Дерево разбора выражения'
|
title='Дерево разбора выражения'
|
||||||
onClick={showAST}
|
onClick={showAST}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import ToolbarRSFormCard from './ToolbarRSFormCard';
|
||||||
|
|
||||||
interface EditorRSFormCardProps {
|
interface EditorRSFormCardProps {
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModified: (newValue: boolean) => void;
|
||||||
onDestroy: () => void;
|
onDestroy: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import ToolbarVersioning from './ToolbarVersioning';
|
||||||
interface FormRSFormProps {
|
interface FormRSFormProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModified: (newValue: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
|
|
|
@ -152,8 +152,8 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||||
id='constituents_search'
|
id='constituents_search'
|
||||||
noBorder
|
noBorder
|
||||||
className='w-[8rem]'
|
className='w-[8rem]'
|
||||||
value={filterText}
|
query={filterText}
|
||||||
onChange={setFilterText}
|
onChangeQuery={setFilterText}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -74,8 +74,8 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
|
||||||
id='constituents_search'
|
id='constituents_search'
|
||||||
noBorder
|
noBorder
|
||||||
className='min-w-[6rem] pr-2 flex-grow'
|
className='min-w-[6rem] pr-2 flex-grow'
|
||||||
value={filterText}
|
query={filterText}
|
||||||
onChange={setFilterText}
|
onChangeQuery={setFilterText}
|
||||||
/>
|
/>
|
||||||
{selectMatchMode}
|
{selectMatchMode}
|
||||||
{selectGraph}
|
{selectGraph}
|
||||||
|
|
|
@ -371,7 +371,7 @@ export function colorBgSyntaxTree(node: ISyntaxTreeNode, colors: IColorTheme): s
|
||||||
case TokenID.NT_FUNC_CALL:
|
case TokenID.NT_FUNC_CALL:
|
||||||
case TokenID.NT_ARGUMENTS:
|
case TokenID.NT_ARGUMENTS:
|
||||||
case TokenID.NT_RECURSIVE_SHORT:
|
case TokenID.NT_RECURSIVE_SHORT:
|
||||||
return '';
|
return colors.bgDisabled;
|
||||||
|
|
||||||
case TokenID.ASSIGN:
|
case TokenID.ASSIGN:
|
||||||
case TokenID.ITERATE:
|
case TokenID.ITERATE:
|
||||||
|
|
|
@ -126,7 +126,8 @@
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__node-step {
|
.react-flow__node-step,
|
||||||
|
.react-flow__node-token {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
|
|
|
@ -581,16 +581,16 @@ export function labelSyntaxTree(node: ISyntaxTreeNode): string {
|
||||||
case TokenID.NT_TUPLE: return 'TUPLE';
|
case TokenID.NT_TUPLE: return 'TUPLE';
|
||||||
case TokenID.NT_ENUMERATION: return 'ENUM';
|
case TokenID.NT_ENUMERATION: return 'ENUM';
|
||||||
|
|
||||||
case TokenID.NT_ENUM_DECL: return 'ENUM_DECLARATION';
|
case TokenID.NT_ENUM_DECL: return 'ENUM_DECLARE';
|
||||||
case TokenID.NT_TUPLE_DECL: return 'TUPLE_DECLARATION';
|
case TokenID.NT_TUPLE_DECL: return 'TUPLE_DECLARE';
|
||||||
case TokenID.PUNCTUATION_DEFINE: return 'DEFINITION';
|
case TokenID.PUNCTUATION_DEFINE: return 'DEFINITION';
|
||||||
case TokenID.PUNCTUATION_STRUCT: return 'STRUCTURE_DEFINITION';
|
case TokenID.PUNCTUATION_STRUCT: return 'STRUCTURE_DEFINE';
|
||||||
|
|
||||||
case TokenID.NT_ARG_DECL: return 'ARG';
|
case TokenID.NT_ARG_DECL: return 'ARG';
|
||||||
case TokenID.NT_FUNC_CALL: return 'CALL';
|
case TokenID.NT_FUNC_CALL: return 'CALL';
|
||||||
case TokenID.NT_ARGUMENTS: return 'ARGS';
|
case TokenID.NT_ARGUMENTS: return 'ARGS';
|
||||||
|
|
||||||
case TokenID.NT_FUNC_DEFINITION: return 'FUNCTION_DEFINITION';
|
case TokenID.NT_FUNC_DEFINITION: return 'FUNCTION_DEFINE';
|
||||||
|
|
||||||
case TokenID.NT_RECURSIVE_SHORT: return labelToken(TokenID.NT_RECURSIVE_FULL);
|
case TokenID.NT_RECURSIVE_SHORT: return labelToken(TokenID.NT_RECURSIVE_FULL);
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
# Run coverage analysis
|
# Run linters
|
||||||
|
|
||||||
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
||||||
|
$frontend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\frontend"
|
||||||
|
|
||||||
function RunLinters() {
|
function RunLinters() {
|
||||||
BackendLint
|
LintBackend
|
||||||
|
LintFrontend
|
||||||
}
|
}
|
||||||
|
|
||||||
function BackendLint() {
|
function LintBackend() {
|
||||||
$pylint = "$backend\venv\Scripts\pylint.exe"
|
$pylint = "$backend\venv\Scripts\pylint.exe"
|
||||||
$mypy = "$backend\venv\Scripts\mypy.exe"
|
$mypy = "$backend\venv\Scripts\mypy.exe"
|
||||||
|
|
||||||
|
@ -15,4 +18,9 @@ function BackendLint() {
|
||||||
& $mypy project apps
|
& $mypy project apps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LintFrontend() {
|
||||||
|
Set-Location $frontend
|
||||||
|
& npm run lint
|
||||||
|
}
|
||||||
|
|
||||||
RunLinters
|
RunLinters
|
Loading…
Reference in New Issue
Block a user