Compare commits
16 Commits
b90312868c
...
7792a82bf7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7792a82bf7 | ||
![]() |
751d73a880 | ||
![]() |
29e49997c3 | ||
![]() |
336794ec6c | ||
![]() |
e0abbe6534 | ||
![]() |
4cf24d0200 | ||
![]() |
f5419472f5 | ||
![]() |
9aa23aedfb | ||
![]() |
18979dbaa3 | ||
![]() |
e1cc428459 | ||
![]() |
178d6c3ba7 | ||
![]() |
e68906f2f0 | ||
![]() |
f8758234f7 | ||
![]() |
d81e015be1 | ||
![]() |
d139c07b7a | ||
![]() |
532bf24df6 |
|
@ -39,12 +39,15 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- react-error-boundary
|
- react-error-boundary
|
||||||
- react-tooltip
|
- react-tooltip
|
||||||
- react-zoom-pan-pinch
|
- react-zoom-pan-pinch
|
||||||
|
- react-hook-form
|
||||||
- reactflow
|
- reactflow
|
||||||
- js-file-download
|
- js-file-download
|
||||||
- use-debounce
|
- use-debounce
|
||||||
- qrcode.react
|
- qrcode.react
|
||||||
- html-to-image
|
- html-to-image
|
||||||
- zustand
|
- zustand
|
||||||
|
- zod
|
||||||
|
- @hookform/resolvers
|
||||||
- @tanstack/react-table
|
- @tanstack/react-table
|
||||||
- @tanstack/react-query
|
- @tanstack/react-query
|
||||||
- @tanstack/react-query-devtools
|
- @tanstack/react-query-devtools
|
||||||
|
|
|
@ -103,6 +103,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
'first_name',
|
'first_name',
|
||||||
'last_name',
|
'last_name',
|
||||||
]
|
]
|
||||||
|
read_only_fields = ('id', 'username')
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
attrs = super().validate(attrs)
|
attrs = super().validate(attrs)
|
||||||
|
|
|
@ -101,6 +101,10 @@ class TestUserUserProfileAPIView(EndpointTester):
|
||||||
data = {'email': self.user2.email}
|
data = {'email': self.user2.email}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data = {'username': 'new_username'}
|
||||||
|
response = self.executeOK(data=data)
|
||||||
|
self.assertNotEqual(response.data['username'], data['username'])
|
||||||
|
|
||||||
self.logout()
|
self.logout()
|
||||||
self.executeForbidden()
|
self.executeForbidden()
|
||||||
|
|
||||||
|
|
29
rsconcept/frontend/package-lock.json
generated
29
rsconcept/frontend/package-lock.json
generated
|
@ -9,6 +9,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@tanstack/react-query": "^5.64.2",
|
"@tanstack/react-query": "^5.64.2",
|
||||||
"@tanstack/react-query-devtools": "^5.64.2",
|
"@tanstack/react-query-devtools": "^5.64.2",
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-error-boundary": "^5.0.0",
|
"react-error-boundary": "^5.0.0",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-intl": "^7.1.5",
|
"react-intl": "^7.1.5",
|
||||||
"react-router": "^7.1.3",
|
"react-router": "^7.1.3",
|
||||||
|
@ -33,6 +35,7 @@
|
||||||
"react-zoom-pan-pinch": "^3.6.1",
|
"react-zoom-pan-pinch": "^3.6.1",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
|
"zod": "^3.24.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1698,6 +1701,15 @@
|
||||||
"tslib": "2"
|
"tslib": "2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hookform/resolvers": {
|
||||||
|
"version": "3.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
|
||||||
|
"integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-hook-form": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
@ -9101,6 +9113,22 @@
|
||||||
"react": ">=16.13.1"
|
"react": ">=16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.54.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz",
|
||||||
|
"integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-icons": {
|
"node_modules/react-icons": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
|
||||||
|
@ -10932,7 +10960,6 @@
|
||||||
"version": "3.24.1",
|
"version": "3.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@tanstack/react-query": "^5.64.2",
|
"@tanstack/react-query": "^5.64.2",
|
||||||
"@tanstack/react-query-devtools": "^5.64.2",
|
"@tanstack/react-query-devtools": "^5.64.2",
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-error-boundary": "^5.0.0",
|
"react-error-boundary": "^5.0.0",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-intl": "^7.1.5",
|
"react-intl": "^7.1.5",
|
||||||
"react-router": "^7.1.3",
|
"react-router": "^7.1.3",
|
||||||
|
@ -37,6 +39,7 @@
|
||||||
"react-zoom-pan-pinch": "^3.6.1",
|
"react-zoom-pan-pinch": "^3.6.1",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
|
"zod": "^3.24.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
|
||||||
import { Outlet } from 'react-router';
|
import { Outlet } from 'react-router';
|
||||||
|
|
||||||
import ConceptToaster from '@/app/ConceptToaster';
|
import ConceptToaster from '@/app/ConceptToaster';
|
||||||
|
@ -10,22 +9,10 @@ import ModalLoader from '@/components/ui/ModalLoader';
|
||||||
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
|
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
|
||||||
import { globals } from '@/utils/constants';
|
import { globals } from '@/utils/constants';
|
||||||
|
|
||||||
import ErrorFallback from './ErrorFallback';
|
|
||||||
import { GlobalDialogs } from './GlobalDialogs';
|
import { GlobalDialogs } from './GlobalDialogs';
|
||||||
import { GlobalTooltips } from './GlobalTooltips';
|
import { GlobalTooltips } from './GlobalTooltips';
|
||||||
import { NavigationState } from './Navigation/NavigationContext';
|
import { NavigationState } from './Navigation/NavigationContext';
|
||||||
|
|
||||||
const resetState = () => {
|
|
||||||
console.log('Resetting state after error fallback');
|
|
||||||
};
|
|
||||||
|
|
||||||
const logError = (error: Error, info: { componentStack?: string | null | undefined }) => {
|
|
||||||
console.log('Error fallback: ' + error.message);
|
|
||||||
if (info.componentStack) {
|
|
||||||
console.log('Component stack: ' + info.componentStack);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function ApplicationLayout() {
|
function ApplicationLayout() {
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
const viewportHeight = useViewportHeight();
|
const viewportHeight = useViewportHeight();
|
||||||
|
@ -35,41 +22,39 @@ function ApplicationLayout() {
|
||||||
const noFooter = useAppLayoutStore(state => state.noFooter);
|
const noFooter = useAppLayoutStore(state => state.noFooter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError} onReset={resetState}>
|
<NavigationState>
|
||||||
<NavigationState>
|
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
||||||
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
<ConceptToaster
|
||||||
<ConceptToaster
|
className='text-[14px] cc-animate-position'
|
||||||
className='text-[14px] cc-animate-position'
|
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
||||||
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
autoClose={3000}
|
||||||
autoClose={3000}
|
draggable={false}
|
||||||
draggable={false}
|
pauseOnFocusLoss={false}
|
||||||
pauseOnFocusLoss={false}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<Suspense fallback={<ModalLoader />}>
|
<Suspense fallback={<ModalLoader />}>
|
||||||
<GlobalDialogs />
|
<GlobalDialogs />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<GlobalTooltips />
|
<GlobalTooltips />
|
||||||
|
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id={globals.main_scroll}
|
id={globals.main_scroll}
|
||||||
className='overflow-x-auto max-w-[100vw]'
|
className='overflow-x-auto max-w-[100vw]'
|
||||||
style={{
|
style={{
|
||||||
maxHeight: viewportHeight
|
maxHeight: viewportHeight
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
|
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
|
||||||
<Suspense fallback={<Loader />}>
|
<Suspense fallback={<Loader />}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</main>
|
</main>
|
||||||
{!noNavigation && !noFooter ? <Footer /> : null}
|
{!noNavigation && !noFooter ? <Footer /> : null}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</NavigationState>
|
</div>
|
||||||
</ErrorBoundary>
|
</NavigationState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
import { type FallbackProps } from 'react-error-boundary';
|
import { useNavigate, useRouteError } from 'react-router';
|
||||||
|
|
||||||
import InfoError from '@/components/info/InfoError';
|
import InfoError from '@/components/info/InfoError';
|
||||||
import Button from '@/components/ui/Button';
|
import Button from '@/components/ui/Button';
|
||||||
|
|
||||||
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
function ErrorFallback() {
|
||||||
|
const error = useRouteError();
|
||||||
|
const router = useNavigate();
|
||||||
|
|
||||||
|
function resetErrorBoundary() {
|
||||||
|
Promise.resolve(router('/')).catch(console.log);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-3 my-3 items-center antialiased' role='alert'>
|
<div className='flex flex-col gap-3 my-3 items-center antialiased' role='alert'>
|
||||||
<h1 className='my-2'>Что-то пошло не так!</h1>
|
<h1 className='my-2'>Что-то пошло не так!</h1>
|
||||||
<Button onClick={resetErrorBoundary} text='Попробовать еще раз' />
|
<Button onClick={resetErrorBoundary} text='Вернуться на главную' />
|
||||||
<InfoError error={error as Error} />
|
<InfoError error={error as Error} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,13 +13,14 @@ import LoginPage from '@/pages/LoginPage';
|
||||||
import NotFoundPage from '@/pages/NotFoundPage';
|
import NotFoundPage from '@/pages/NotFoundPage';
|
||||||
|
|
||||||
import ApplicationLayout from './ApplicationLayout';
|
import ApplicationLayout from './ApplicationLayout';
|
||||||
|
import ErrorFallback from './ErrorFallback';
|
||||||
import { routes } from './urls';
|
import { routes } from './urls';
|
||||||
|
|
||||||
export const Router = createBrowserRouter([
|
export const Router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <ApplicationLayout />,
|
element: <ApplicationLayout />,
|
||||||
errorElement: <NotFoundPage />,
|
errorElement: <ErrorFallback />,
|
||||||
loader: prefetchAuth,
|
loader: prefetchAuth,
|
||||||
hydrateFallbackElement: <Loader />,
|
hydrateFallbackElement: <Loader />,
|
||||||
children: [
|
children: [
|
||||||
|
|
|
@ -24,18 +24,6 @@ export const routes = {
|
||||||
database_schema: 'database-schema'
|
database_schema: 'database-schema'
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SchemaProps {
|
|
||||||
id: number | string;
|
|
||||||
tab: number;
|
|
||||||
version?: number | string;
|
|
||||||
active?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OssProps {
|
|
||||||
id: number | string;
|
|
||||||
tab: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal navigation URLs.
|
* Internal navigation URLs.
|
||||||
*/
|
*/
|
||||||
|
@ -58,12 +46,24 @@ export const urls = {
|
||||||
schema: (id: number | string, version?: number | string) =>
|
schema: (id: number | string, version?: number | string) =>
|
||||||
`/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''),
|
`/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''),
|
||||||
oss: (id: number | string, tab?: number) => `/oss/${id}` + (tab !== undefined ? `?tab=${tab}` : ''),
|
oss: (id: number | string, tab?: number) => `/oss/${id}` + (tab !== undefined ? `?tab=${tab}` : ''),
|
||||||
schema_props: ({ id, tab, version, active }: SchemaProps) => {
|
|
||||||
|
schema_props: ({
|
||||||
|
id,
|
||||||
|
tab,
|
||||||
|
version,
|
||||||
|
active
|
||||||
|
}: {
|
||||||
|
id: number | string;
|
||||||
|
tab: number;
|
||||||
|
version?: number | string;
|
||||||
|
active?: number | string;
|
||||||
|
}) => {
|
||||||
const versionStr = version !== undefined ? `v=${version}&` : '';
|
const versionStr = version !== undefined ? `v=${version}&` : '';
|
||||||
const activeStr = active !== undefined ? `&active=${active}` : '';
|
const activeStr = active !== undefined ? `&active=${active}` : '';
|
||||||
return `/rsforms/${id}?${versionStr}tab=${tab}${activeStr}`;
|
return `/rsforms/${id}?${versionStr}tab=${tab}${activeStr}`;
|
||||||
},
|
},
|
||||||
oss_props: ({ id, tab }: OssProps) => {
|
|
||||||
|
oss_props: ({ id, tab }: { id: number | string; tab: number }) => {
|
||||||
return `/oss/${id}?tab=${tab}`;
|
return `/oss/${id}?tab=${tab}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -53,8 +53,11 @@ export function axiosGet<ResponseData>({ endpoint, options }: IAxiosGetRequest)
|
||||||
.get<ResponseData>(endpoint, options)
|
.get<ResponseData>(endpoint, options)
|
||||||
.then(response => response.data)
|
.then(response => response.data)
|
||||||
.catch((error: Error | AxiosError) => {
|
.catch((error: Error | AxiosError) => {
|
||||||
toast.error(extractErrorMessage(error));
|
if (error.name !== 'CanceledError') {
|
||||||
console.error(error);
|
// Note: Ignore cancellation errors
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,46 @@
|
||||||
import { queryOptions } from '@tanstack/react-query';
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { axiosGet, axiosPost } from '@/backend/apiTransport';
|
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
||||||
import { DELAYS } from '@/backend/configuration';
|
import { DELAYS } from '@/backend/configuration';
|
||||||
import { ICurrentUser } from '@/models/user';
|
import { ICurrentUser } from '@/models/user';
|
||||||
import { information } from '@/utils/labels';
|
import { errors, information } from '@/utils/labels';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents login data, used to authenticate users.
|
* Represents login data, used to authenticate users.
|
||||||
*/
|
*/
|
||||||
export interface IUserLoginDTO {
|
export const UserLoginSchema = z.object({
|
||||||
username: string;
|
username: z.string().nonempty(errors.requiredField),
|
||||||
password: string;
|
password: z.string().nonempty(errors.requiredField)
|
||||||
}
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents login data, used to authenticate users.
|
||||||
|
*/
|
||||||
|
export type IUserLoginDTO = z.infer<typeof UserLoginSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents data needed to update password for current user.
|
* Represents data needed to update password for current user.
|
||||||
*/
|
*/
|
||||||
export interface IChangePasswordDTO {
|
export const ChangePasswordSchema = z
|
||||||
old_password: string;
|
.object({
|
||||||
new_password: string;
|
old_password: z.string().nonempty(errors.requiredField),
|
||||||
}
|
new_password: z.string().nonempty(errors.requiredField),
|
||||||
|
new_password2: z.string().nonempty(errors.requiredField)
|
||||||
|
})
|
||||||
|
.refine(schema => schema.new_password === schema.new_password2, {
|
||||||
|
path: ['new_password2'],
|
||||||
|
message: errors.passwordsMismatch
|
||||||
|
})
|
||||||
|
.refine(schema => schema.old_password !== schema.new_password, {
|
||||||
|
path: ['new_password'],
|
||||||
|
message: errors.passwordsSame
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents data needed to update password for current user.
|
||||||
|
*/
|
||||||
|
export type IChangePasswordDTO = z.infer<typeof ChangePasswordSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents password reset request data.
|
* Represents password reset request data.
|
||||||
|
@ -69,7 +90,7 @@ export const authApi = {
|
||||||
request: { data: data }
|
request: { data: data }
|
||||||
}),
|
}),
|
||||||
changePassword: (data: IChangePasswordDTO) =>
|
changePassword: (data: IChangePasswordDTO) =>
|
||||||
axiosPost({
|
axiosPatch({
|
||||||
endpoint: '/users/api/change-password',
|
endpoint: '/users/api/change-password',
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { libraryApi } from '@/backend/library/api';
|
import { authApi, IUserLoginDTO } from './api';
|
||||||
|
|
||||||
import { authApi } from './api';
|
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
@ -10,14 +8,10 @@ export const useLogin = () => {
|
||||||
mutationKey: ['login'],
|
mutationKey: ['login'],
|
||||||
mutationFn: authApi.login,
|
mutationFn: authApi.login,
|
||||||
onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] }),
|
onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] }),
|
||||||
onSuccess: () => client.removeQueries({ queryKey: [libraryApi.baseKey] })
|
onSuccess: () => client.resetQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
login: (
|
login: (data: IUserLoginDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }),
|
||||||
username: string, //
|
|
||||||
password: string,
|
|
||||||
onSuccess?: () => void
|
|
||||||
) => mutation.mutate({ username, password }, { onSuccess }),
|
|
||||||
isPending: mutation.isPending,
|
isPending: mutation.isPending,
|
||||||
error: mutation.error,
|
error: mutation.error,
|
||||||
reset: mutation.reset
|
reset: mutation.reset
|
||||||
|
|
|
@ -7,8 +7,7 @@ export const useLogout = () => {
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: ['logout'],
|
mutationKey: ['logout'],
|
||||||
mutationFn: authApi.logout,
|
mutationFn: authApi.logout,
|
||||||
onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] }),
|
onSuccess: () => client.resetQueries()
|
||||||
onSuccess: () => client.removeQueries()
|
|
||||||
});
|
});
|
||||||
return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) };
|
return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) };
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const useResetPassword = () => {
|
||||||
onSuccess?: () => void
|
onSuccess?: () => void
|
||||||
) => resetMutation.mutate(data, { onSuccess }),
|
) => resetMutation.mutate(data, { onSuccess }),
|
||||||
isPending: resetMutation.isPending || validateMutation.isPending,
|
isPending: resetMutation.isPending || validateMutation.isPending,
|
||||||
error: resetMutation.error ?? validateMutation.error
|
error: resetMutation.error ?? validateMutation.error,
|
||||||
|
reset: resetMutation.reset
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { axiosPost } from '@/backend/apiTransport';
|
import { axiosPost } from '@/backend/apiTransport';
|
||||||
import { ILexemeData, IWordFormPlain } from '@/models/language';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents API result for text output.
|
* Represents API result for text output.
|
||||||
|
@ -8,11 +7,26 @@ export interface ITextResult {
|
||||||
result: string;
|
result: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents wordform data used for backend communication.
|
||||||
|
*/
|
||||||
|
export interface IWordFormDTO {
|
||||||
|
text: string;
|
||||||
|
grams: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents lexeme response containing multiple {@link Wordform}s.
|
||||||
|
*/
|
||||||
|
export interface ILexemeResponse {
|
||||||
|
items: IWordFormDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
export const cctextApi = {
|
export const cctextApi = {
|
||||||
baseKey: 'cctext',
|
baseKey: 'cctext',
|
||||||
|
|
||||||
inflectText: (data: IWordFormPlain) =>
|
inflectText: (data: IWordFormDTO) =>
|
||||||
axiosPost<IWordFormPlain, ITextResult>({
|
axiosPost<IWordFormDTO, ITextResult>({
|
||||||
endpoint: '/api/cctext/inflect',
|
endpoint: '/api/cctext/inflect',
|
||||||
request: { data: data }
|
request: { data: data }
|
||||||
}),
|
}),
|
||||||
|
@ -22,7 +36,7 @@ export const cctextApi = {
|
||||||
request: { data: data }
|
request: { data: data }
|
||||||
}),
|
}),
|
||||||
generateLexeme: (data: { text: string }) =>
|
generateLexeme: (data: { text: string }) =>
|
||||||
axiosPost<{ text: string }, ILexemeData>({
|
axiosPost<{ text: string }, ILexemeResponse>({
|
||||||
endpoint: '/api/cctext/generate-lexeme',
|
endpoint: '/api/cctext/generate-lexeme',
|
||||||
request: { data: data }
|
request: { data: data }
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { DataCallback } from '@/backend/apiTransport';
|
import { DataCallback } from '@/backend/apiTransport';
|
||||||
import { ILexemeData } from '@/models/language';
|
|
||||||
|
|
||||||
import { cctextApi } from './api';
|
import { cctextApi, ILexemeResponse } from './api';
|
||||||
|
|
||||||
export const useGenerateLexeme = () => {
|
export const useGenerateLexeme = () => {
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
|
@ -13,7 +12,7 @@ export const useGenerateLexeme = () => {
|
||||||
return {
|
return {
|
||||||
generateLexeme: (
|
generateLexeme: (
|
||||||
data: { text: string }, //
|
data: { text: string }, //
|
||||||
onSuccess?: DataCallback<ILexemeData>
|
onSuccess?: DataCallback<ILexemeResponse>
|
||||||
) => mutation.mutate(data, { onSuccess })
|
) => mutation.mutate(data, { onSuccess })
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { DataCallback } from '@/backend/apiTransport';
|
import { DataCallback } from '@/backend/apiTransport';
|
||||||
import { IWordFormPlain } from '@/models/language';
|
|
||||||
|
|
||||||
import { cctextApi, ITextResult } from './api';
|
import { cctextApi, ITextResult, IWordFormDTO } from './api';
|
||||||
|
|
||||||
export const useInflectText = () => {
|
export const useInflectText = () => {
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
|
@ -12,7 +11,7 @@ export const useInflectText = () => {
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
inflectText: (
|
inflectText: (
|
||||||
data: IWordFormPlain, //
|
data: IWordFormDTO, //
|
||||||
onSuccess?: DataCallback<ITextResult>
|
onSuccess?: DataCallback<ITextResult>
|
||||||
) => mutation.mutate(data, { onSuccess })
|
) => mutation.mutate(data, { onSuccess })
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { queryOptions } from '@tanstack/react-query';
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
||||||
import { DELAYS } from '@/backend/configuration';
|
import { DELAYS } from '@/backend/configuration';
|
||||||
import { ossApi } from '@/backend/oss/api';
|
import { ossApi } from '@/backend/oss/api';
|
||||||
import { rsformsApi } from '@/backend/rsform/api';
|
import { IRSFormDTO, rsformsApi } from '@/backend/rsform/api';
|
||||||
import {
|
import {
|
||||||
AccessPolicy,
|
AccessPolicy,
|
||||||
ILibraryItem,
|
ILibraryItem,
|
||||||
|
@ -13,9 +14,10 @@ import {
|
||||||
LibraryItemType,
|
LibraryItemType,
|
||||||
VersionID
|
VersionID
|
||||||
} from '@/models/library';
|
} from '@/models/library';
|
||||||
import { ConstituentaID, IRSFormData } from '@/models/rsform';
|
import { validateLocation } from '@/models/libraryAPI';
|
||||||
|
import { ConstituentaID } from '@/models/rsform';
|
||||||
import { UserID } from '@/models/user';
|
import { UserID } from '@/models/user';
|
||||||
import { information } from '@/utils/labels';
|
import { errors, information } from '@/utils/labels';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents update data for renaming Location.
|
* Represents update data for renaming Location.
|
||||||
|
@ -28,22 +30,49 @@ export interface IRenameLocationDTO {
|
||||||
/**
|
/**
|
||||||
* Represents data, used for cloning {@link IRSForm}.
|
* Represents data, used for cloning {@link IRSForm}.
|
||||||
*/
|
*/
|
||||||
export interface IRSFormCloneDTO extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'owner'> {
|
export interface IRCloneLibraryItemDTO extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'owner'> {
|
||||||
items?: ConstituentaID[];
|
items?: ConstituentaID[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents data, used for creating {@link IRSForm}.
|
* Represents data, used for creating {@link IRSForm}.
|
||||||
*/
|
*/
|
||||||
export interface ILibraryCreateDTO extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'id' | 'owner'> {
|
export const CreateLibraryItemSchema = z
|
||||||
file?: File;
|
.object({
|
||||||
fileName?: string;
|
item_type: z.nativeEnum(LibraryItemType),
|
||||||
}
|
title: z.string().optional(),
|
||||||
|
alias: z.string().optional(),
|
||||||
|
comment: z.string(),
|
||||||
|
visible: z.boolean(),
|
||||||
|
read_only: z.boolean(),
|
||||||
|
location: z.string(),
|
||||||
|
access_policy: z.nativeEnum(AccessPolicy),
|
||||||
|
|
||||||
|
file: z.instanceof(File).optional(),
|
||||||
|
fileName: z.string().optional()
|
||||||
|
})
|
||||||
|
.refine(data => validateLocation(data.location), {
|
||||||
|
path: ['location'],
|
||||||
|
message: errors.invalidLocation
|
||||||
|
})
|
||||||
|
.refine(data => !!data.file || !!data.title, {
|
||||||
|
path: ['title'],
|
||||||
|
message: errors.requiredField
|
||||||
|
})
|
||||||
|
.refine(data => !!data.file || !!data.alias, {
|
||||||
|
path: ['alias'],
|
||||||
|
message: errors.requiredField
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents data, used for creating {@link IRSForm}.
|
||||||
|
*/
|
||||||
|
export type ICreateLibraryItemDTO = z.infer<typeof CreateLibraryItemSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents update data for editing {@link ILibraryItem}.
|
* Represents update data for editing {@link ILibraryItem}.
|
||||||
*/
|
*/
|
||||||
export interface ILibraryUpdateDTO
|
export interface IUpdateLibraryItemDTO
|
||||||
extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'access_policy' | 'location' | 'owner'> {}
|
extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'access_policy' | 'location' | 'owner'> {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,7 +89,7 @@ export interface IVersionCreateDTO {
|
||||||
*/
|
*/
|
||||||
export interface IVersionCreatedResponse {
|
export interface IVersionCreatedResponse {
|
||||||
version: number;
|
version: number;
|
||||||
schema: IRSFormData;
|
schema: IRSFormDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const libraryApi = {
|
export const libraryApi = {
|
||||||
|
@ -93,8 +122,8 @@ export const libraryApi = {
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createItem: (data: ILibraryCreateDTO) =>
|
createItem: (data: ICreateLibraryItemDTO) =>
|
||||||
axiosPost<ILibraryCreateDTO, ILibraryItem>({
|
axiosPost<ICreateLibraryItemDTO, ILibraryItem>({
|
||||||
endpoint: !data.file ? '/api/library' : '/api/rsforms/create-detailed',
|
endpoint: !data.file ? '/api/library' : '/api/rsforms/create-detailed',
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -108,8 +137,8 @@ export const libraryApi = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateItem: (data: ILibraryUpdateDTO) =>
|
updateItem: (data: IUpdateLibraryItemDTO) =>
|
||||||
axiosPatch<ILibraryUpdateDTO, ILibraryItem>({
|
axiosPatch<IUpdateLibraryItemDTO, ILibraryItem>({
|
||||||
endpoint: `/api/library/${data.id}`,
|
endpoint: `/api/library/${data.id}`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -156,8 +185,8 @@ export const libraryApi = {
|
||||||
successMessage: information.itemDestroyed
|
successMessage: information.itemDestroyed
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
cloneItem: (data: IRSFormCloneDTO) =>
|
cloneItem: (data: IRCloneLibraryItemDTO) =>
|
||||||
axiosPost<IRSFormCloneDTO, IRSFormData>({
|
axiosPost<IRCloneLibraryItemDTO, IRSFormDTO>({
|
||||||
endpoint: `/api/library/${data.id}/clone`,
|
endpoint: `/api/library/${data.id}/clone`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -182,7 +211,7 @@ export const libraryApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
versionRestore: ({ versionID }: { versionID: VersionID }) =>
|
versionRestore: ({ versionID }: { versionID: VersionID }) =>
|
||||||
axiosPatch<undefined, IRSFormData>({
|
axiosPatch<undefined, IRSFormDTO>({
|
||||||
endpoint: `/api/versions/${versionID}/restore`,
|
endpoint: `/api/versions/${versionID}/restore`,
|
||||||
request: {
|
request: {
|
||||||
successMessage: information.versionRestored
|
successMessage: information.versionRestored
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { DataCallback } from '@/backend/apiTransport';
|
import { DataCallback } from '@/backend/apiTransport';
|
||||||
import { IRSFormData } from '@/models/rsform';
|
|
||||||
|
|
||||||
import { IRSFormCloneDTO, libraryApi } from './api';
|
import { IRSFormDTO } from '../rsform/api';
|
||||||
|
import { IRCloneLibraryItemDTO, libraryApi } from './api';
|
||||||
|
|
||||||
export const useCloneItem = () => {
|
export const useCloneItem = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
@ -14,8 +14,8 @@ export const useCloneItem = () => {
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
cloneItem: (
|
cloneItem: (
|
||||||
data: IRSFormCloneDTO, //
|
data: IRCloneLibraryItemDTO, //
|
||||||
onSuccess?: DataCallback<IRSFormData>
|
onSuccess?: DataCallback<IRSFormDTO>
|
||||||
) => mutation.mutate(data, { onSuccess })
|
) => mutation.mutate(data, { onSuccess })
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { DataCallback } from '@/backend/apiTransport';
|
import { DataCallback } from '@/backend/apiTransport';
|
||||||
import { ILibraryItem } from '@/models/library';
|
import { ILibraryItem } from '@/models/library';
|
||||||
|
|
||||||
import { ILibraryCreateDTO, libraryApi } from './api';
|
import { ICreateLibraryItemDTO, libraryApi } from './api';
|
||||||
|
|
||||||
export const useCreateItem = () => {
|
export const useCreateItem = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
@ -14,7 +14,7 @@ export const useCreateItem = () => {
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
createItem: (
|
createItem: (
|
||||||
data: ILibraryCreateDTO, //
|
data: ICreateLibraryItemDTO, //
|
||||||
onSuccess?: DataCallback<ILibraryItem>
|
onSuccess?: DataCallback<ILibraryItem>
|
||||||
) => mutation.mutate(data, { onSuccess }),
|
) => mutation.mutate(data, { onSuccess }),
|
||||||
isPending: mutation.isPending,
|
isPending: mutation.isPending,
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { ossApi } from '@/backend/oss/api';
|
import { ossApi } from '@/backend/oss/api';
|
||||||
import { rsformsApi } from '@/backend/rsform/api';
|
import { rsformsApi } from '@/backend/rsform/api';
|
||||||
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
import { LibraryItemID } from '@/models/library';
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
|
|
||||||
|
@ -12,13 +13,16 @@ export const useDeleteItem = () => {
|
||||||
mutationKey: [libraryApi.baseKey, 'delete-item'],
|
mutationKey: [libraryApi.baseKey, 'delete-item'],
|
||||||
mutationFn: libraryApi.deleteItem,
|
mutationFn: libraryApi.deleteItem,
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }).catch(console.error);
|
||||||
prev?.filter(item => item.id !== variables)
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
void Promise.allSettled([
|
||||||
|
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
|
||||||
|
client.resetQueries({ queryKey: rsformsApi.getRSFormQueryOptions({ itemID: variables }).queryKey }),
|
||||||
|
client.resetQueries({ queryKey: ossApi.getOssQueryOptions({ itemID: variables }).queryKey })
|
||||||
|
]).catch(console.error),
|
||||||
|
PARAMETER.navigationDuration
|
||||||
);
|
);
|
||||||
return Promise.allSettled([
|
|
||||||
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
|
|
||||||
client.invalidateQueries({ queryKey: rsformsApi.getRSFormQueryOptions({ itemID: variables }).queryKey })
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { ossApi } from '@/backend/oss/api';
|
import { IOperationSchemaDTO, ossApi } from '@/backend/oss/api';
|
||||||
import { rsformsApi } from '@/backend/rsform/api';
|
import { rsformsApi } from '@/backend/rsform/api';
|
||||||
import { AccessPolicy, ILibraryItem, LibraryItemID } from '@/models/library';
|
import { AccessPolicy, ILibraryItem, LibraryItemID } from '@/models/library';
|
||||||
import { IOperationSchemaData } from '@/models/oss';
|
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
|
|
||||||
|
@ -14,7 +13,7 @@ export const useSetAccessPolicy = () => {
|
||||||
mutationFn: libraryApi.setAccessPolicy,
|
mutationFn: libraryApi.setAccessPolicy,
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
|
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||||
const ossData: IOperationSchemaData | undefined = client.getQueryData(ossKey);
|
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
|
||||||
if (ossData) {
|
if (ossData) {
|
||||||
client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
|
client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
|
||||||
return Promise.allSettled([
|
return Promise.allSettled([
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { ossApi } from '@/backend/oss/api';
|
import { IOperationSchemaDTO, ossApi } from '@/backend/oss/api';
|
||||||
import { rsformsApi } from '@/backend/rsform/api';
|
import { rsformsApi } from '@/backend/rsform/api';
|
||||||
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
||||||
import { IOperationSchemaData } from '@/models/oss';
|
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
|
|
||||||
|
@ -14,7 +13,7 @@ export const useSetLocation = () => {
|
||||||
mutationFn: libraryApi.setLocation,
|
mutationFn: libraryApi.setLocation,
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
|
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||||
const ossData: IOperationSchemaData | undefined = client.getQueryData(ossKey);
|
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
|
||||||
if (ossData) {
|
if (ossData) {
|
||||||
client.setQueryData(ossKey, { ...ossData, location: variables.location });
|
client.setQueryData(ossKey, { ...ossData, location: variables.location });
|
||||||
return Promise.allSettled([
|
return Promise.allSettled([
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { ossApi } from '@/backend/oss/api';
|
import { IOperationSchemaDTO, ossApi } from '@/backend/oss/api';
|
||||||
import { rsformsApi } from '@/backend/rsform/api';
|
import { rsformsApi } from '@/backend/rsform/api';
|
||||||
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
||||||
import { IOperationSchemaData } from '@/models/oss';
|
|
||||||
import { UserID } from '@/models/user';
|
import { UserID } from '@/models/user';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
|
@ -15,7 +14,7 @@ export const useSetOwner = () => {
|
||||||
mutationFn: libraryApi.setOwner,
|
mutationFn: libraryApi.setOwner,
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
|
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||||
const ossData: IOperationSchemaData | undefined = client.getQueryData(ossKey);
|
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
|
||||||
if (ossData) {
|
if (ossData) {
|
||||||
client.setQueryData(ossKey, { ...ossData, owner: variables.owner });
|
client.setQueryData(ossKey, { ...ossData, owner: variables.owner });
|
||||||
return Promise.allSettled([
|
return Promise.allSettled([
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { ossApi } from '@/backend/oss/api';
|
import { IOperationSchemaDTO, ossApi } from '@/backend/oss/api';
|
||||||
import { ILibraryItem, LibraryItemType } from '@/models/library';
|
import { ILibraryItem, LibraryItemType } from '@/models/library';
|
||||||
import { IOperationSchemaData } from '@/models/oss';
|
|
||||||
import { IRSFormData } from '@/models/rsform';
|
|
||||||
|
|
||||||
import { ILibraryUpdateDTO, libraryApi } from './api';
|
import { IRSFormDTO } from '../rsform/api';
|
||||||
|
import { IUpdateLibraryItemDTO, libraryApi } from './api';
|
||||||
|
|
||||||
export const useUpdateItem = () => {
|
export const useUpdateItem = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
@ -17,11 +16,11 @@ export const useUpdateItem = () => {
|
||||||
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
||||||
prev?.map(item => (item.id === data.id ? data : item))
|
prev?.map(item => (item.id === data.id ? data : item))
|
||||||
);
|
);
|
||||||
client.setQueryData(itemKey, (prev: IRSFormData | IOperationSchemaData | undefined) =>
|
client.setQueryData(itemKey, (prev: IRSFormDTO | IOperationSchemaDTO | undefined) =>
|
||||||
!prev ? undefined : { ...prev, ...data }
|
!prev ? undefined : { ...prev, ...data }
|
||||||
);
|
);
|
||||||
if (data.item_type === LibraryItemType.RSFORM) {
|
if (data.item_type === LibraryItemType.RSFORM) {
|
||||||
const schema: IRSFormData | undefined = client.getQueryData(itemKey);
|
const schema: IRSFormDTO | undefined = client.getQueryData(itemKey);
|
||||||
if (schema) {
|
if (schema) {
|
||||||
return Promise.allSettled(
|
return Promise.allSettled(
|
||||||
schema.oss.map(item =>
|
schema.oss.map(item =>
|
||||||
|
@ -33,6 +32,6 @@ export const useUpdateItem = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
updateItem: (data: ILibraryUpdateDTO) => mutation.mutate(data)
|
updateItem: (data: IUpdateLibraryItemDTO) => mutation.mutate(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { rsformsApi } from '@/backend/rsform/api';
|
import { IRSFormDTO, rsformsApi } from '@/backend/rsform/api';
|
||||||
import { LibraryItemID, VersionID } from '@/models/library';
|
import { LibraryItemID, VersionID } from '@/models/library';
|
||||||
import { IRSFormData } from '@/models/rsform';
|
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
|
|
||||||
|
@ -14,7 +13,7 @@ export const useVersionDelete = () => {
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
client.setQueryData(
|
client.setQueryData(
|
||||||
rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey,
|
rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey,
|
||||||
(prev: IRSFormData | undefined) =>
|
(prev: IRSFormDTO | undefined) =>
|
||||||
!prev
|
!prev
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { rsformsApi } from '@/backend/rsform/api';
|
import { IRSFormDTO, rsformsApi } from '@/backend/rsform/api';
|
||||||
import { IVersionData, VersionID } from '@/models/library';
|
import { IVersionData, VersionID } from '@/models/library';
|
||||||
import { IRSFormData } from '@/models/rsform';
|
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
|
|
||||||
|
@ -14,7 +13,7 @@ export const useVersionUpdate = () => {
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
client.setQueryData(
|
client.setQueryData(
|
||||||
rsformsApi.getRSFormQueryOptions({ itemID: data.item }).queryKey,
|
rsformsApi.getRSFormQueryOptions({ itemID: data.item }).queryKey,
|
||||||
(prev: IRSFormData | undefined) =>
|
(prev: IRSFormDTO | undefined) =>
|
||||||
!prev
|
!prev
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
|
|
|
@ -2,29 +2,24 @@
|
||||||
* Module: OSS data loading and processing.
|
* Module: OSS data loading and processing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Graph } from './Graph';
|
import { Graph } from '@/models/Graph';
|
||||||
import { ILibraryItem, LibraryItemID } from './library';
|
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
||||||
import {
|
import { IOperation, IOperationSchema, IOperationSchemaStats, OperationID, OperationType } from '@/models/oss';
|
||||||
IOperation,
|
|
||||||
IOperationSchema,
|
import { IOperationSchemaDTO } from './api';
|
||||||
IOperationSchemaData,
|
|
||||||
IOperationSchemaStats,
|
|
||||||
OperationID,
|
|
||||||
OperationType
|
|
||||||
} from './oss';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaData}.
|
* Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class OssLoader {
|
export class OssLoader {
|
||||||
private oss: IOperationSchemaData;
|
private oss: IOperationSchemaDTO;
|
||||||
private graph: Graph = new Graph();
|
private graph: Graph = new Graph();
|
||||||
private operationByID = new Map<OperationID, IOperation>();
|
private operationByID = new Map<OperationID, IOperation>();
|
||||||
private schemaIDs: LibraryItemID[] = [];
|
private schemaIDs: LibraryItemID[] = [];
|
||||||
private items: ILibraryItem[];
|
private items: ILibraryItem[];
|
||||||
|
|
||||||
constructor(input: IOperationSchemaData, items: ILibraryItem[]) {
|
constructor(input: IOperationSchemaDTO, items: ILibraryItem[]) {
|
||||||
this.oss = input;
|
this.oss = input;
|
||||||
this.items = items;
|
this.items = items;
|
||||||
}
|
}
|
|
@ -2,18 +2,33 @@ import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
||||||
import { DELAYS } from '@/backend/configuration';
|
import { DELAYS } from '@/backend/configuration';
|
||||||
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
import { ILibraryItem, ILibraryItemData, LibraryItemID } from '@/models/library';
|
||||||
import {
|
import {
|
||||||
|
IArgument,
|
||||||
ICstSubstitute,
|
ICstSubstitute,
|
||||||
IOperationData,
|
ICstSubstituteEx,
|
||||||
|
IOperation,
|
||||||
IOperationPosition,
|
IOperationPosition,
|
||||||
IOperationSchemaData,
|
|
||||||
OperationID,
|
OperationID,
|
||||||
OperationType
|
OperationType
|
||||||
} from '@/models/oss';
|
} from '@/models/oss';
|
||||||
import { ConstituentaID, IConstituentaReference, ITargetCst } from '@/models/rsform';
|
import { ConstituentaID, IConstituentaReference, ITargetCst } from '@/models/rsform';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents {@link IOperation} data from server.
|
||||||
|
*/
|
||||||
|
export interface IOperationDTO extends Omit<IOperation, 'substitutions' | 'arguments'> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents backend data for {@link IOperationSchema}.
|
||||||
|
*/
|
||||||
|
export interface IOperationSchemaDTO extends ILibraryItemData {
|
||||||
|
items: IOperationDTO[];
|
||||||
|
arguments: IArgument[];
|
||||||
|
substitutions: ICstSubstituteEx[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents {@link IOperation} data, used in creation process.
|
* Represents {@link IOperation} data, used in creation process.
|
||||||
*/
|
*/
|
||||||
|
@ -36,8 +51,8 @@ export interface IOperationCreateDTO {
|
||||||
* Represents data response when creating {@link IOperation}.
|
* Represents data response when creating {@link IOperation}.
|
||||||
*/
|
*/
|
||||||
export interface IOperationCreatedResponse {
|
export interface IOperationCreatedResponse {
|
||||||
new_operation: IOperationData;
|
new_operation: IOperationDTO;
|
||||||
oss: IOperationSchemaData;
|
oss: IOperationSchemaDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,7 +76,7 @@ export interface IOperationDeleteDTO extends ITargetOperation {
|
||||||
*/
|
*/
|
||||||
export interface IInputCreatedResponse {
|
export interface IInputCreatedResponse {
|
||||||
new_schema: ILibraryItem;
|
new_schema: ILibraryItem;
|
||||||
oss: IOperationSchemaData;
|
oss: IOperationSchemaDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -102,7 +117,7 @@ export const ossApi = {
|
||||||
queryFn: meta =>
|
queryFn: meta =>
|
||||||
!itemID
|
!itemID
|
||||||
? undefined
|
? undefined
|
||||||
: axiosGet<IOperationSchemaData>({
|
: axiosGet<IOperationSchemaDTO>({
|
||||||
endpoint: `/api/oss/${itemID}/details`,
|
endpoint: `/api/oss/${itemID}/details`,
|
||||||
options: { signal: meta.signal }
|
options: { signal: meta.signal }
|
||||||
})
|
})
|
||||||
|
@ -135,7 +150,7 @@ export const ossApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
operationDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationDeleteDTO }) =>
|
operationDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationDeleteDTO }) =>
|
||||||
axiosDelete<IOperationDeleteDTO, IOperationSchemaData>({
|
axiosDelete<IOperationDeleteDTO, IOperationSchemaDTO>({
|
||||||
endpoint: `/api/oss/${itemID}/delete-operation`,
|
endpoint: `/api/oss/${itemID}/delete-operation`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -151,7 +166,7 @@ export const ossApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
inputUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: IInputUpdateDTO }) =>
|
inputUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: IInputUpdateDTO }) =>
|
||||||
axiosPatch<IInputUpdateDTO, IOperationSchemaData>({
|
axiosPatch<IInputUpdateDTO, IOperationSchemaDTO>({
|
||||||
endpoint: `/api/oss/${itemID}/set-input`,
|
endpoint: `/api/oss/${itemID}/set-input`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -159,7 +174,7 @@ export const ossApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
operationUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationUpdateDTO }) =>
|
operationUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationUpdateDTO }) =>
|
||||||
axiosPatch<IOperationUpdateDTO, IOperationSchemaData>({
|
axiosPatch<IOperationUpdateDTO, IOperationSchemaDTO>({
|
||||||
endpoint: `/api/oss/${itemID}/update-operation`,
|
endpoint: `/api/oss/${itemID}/update-operation`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -167,7 +182,7 @@ export const ossApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
operationExecute: ({ itemID, data }: { itemID: LibraryItemID; data: ITargetOperation }) =>
|
operationExecute: ({ itemID, data }: { itemID: LibraryItemID; data: ITargetOperation }) =>
|
||||||
axiosPost<ITargetOperation, IOperationSchemaData>({
|
axiosPost<ITargetOperation, IOperationSchemaDTO>({
|
||||||
endpoint: `/api/oss/${itemID}/execute-operation`,
|
endpoint: `/api/oss/${itemID}/execute-operation`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -176,7 +191,7 @@ export const ossApi = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
relocateConstituents: ({ itemID, data }: { itemID: LibraryItemID; data: ICstRelocateDTO }) =>
|
relocateConstituents: ({ itemID, data }: { itemID: LibraryItemID; data: ICstRelocateDTO }) =>
|
||||||
axiosPost<ICstRelocateDTO, IOperationSchemaData>({
|
axiosPost<ICstRelocateDTO, IOperationSchemaDTO>({
|
||||||
endpoint: `/api/oss/${itemID}/relocate-constituents`,
|
endpoint: `/api/oss/${itemID}/relocate-constituents`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary';
|
import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary';
|
||||||
|
import { OssLoader } from '@/backend/oss/OssLoader';
|
||||||
import { LibraryItemID } from '@/models/library';
|
import { LibraryItemID } from '@/models/library';
|
||||||
import { OssLoader } from '@/models/OssLoader';
|
|
||||||
|
|
||||||
import { queryClient } from '../queryClient';
|
import { queryClient } from '../queryClient';
|
||||||
import { ossApi } from './api';
|
import { ossApi } from './api';
|
||||||
|
|
|
@ -3,9 +3,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { DataCallback } from '@/backend/apiTransport';
|
import { DataCallback } from '@/backend/apiTransport';
|
||||||
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
|
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
|
||||||
import { LibraryItemID } from '@/models/library';
|
import { LibraryItemID } from '@/models/library';
|
||||||
import { IOperationData } from '@/models/oss';
|
|
||||||
|
|
||||||
import { IOperationCreateDTO, ossApi } from './api';
|
import { IOperationCreateDTO, IOperationDTO, ossApi } from './api';
|
||||||
|
|
||||||
export const useOperationCreate = () => {
|
export const useOperationCreate = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
@ -24,7 +23,7 @@ export const useOperationCreate = () => {
|
||||||
itemID: LibraryItemID; //
|
itemID: LibraryItemID; //
|
||||||
data: IOperationCreateDTO;
|
data: IOperationCreateDTO;
|
||||||
},
|
},
|
||||||
onSuccess?: DataCallback<IOperationData>
|
onSuccess?: DataCallback<IOperationDTO>
|
||||||
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_operation) })
|
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_operation) })
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,27 +2,29 @@
|
||||||
* Module: RSForm data loading and processing.
|
* Module: RSForm data loading and processing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Graph } from './Graph';
|
import { Graph } from '@/models/Graph';
|
||||||
import { LibraryItemID } from './library';
|
import { LibraryItemID } from '@/models/library';
|
||||||
import { ConstituentaID, CstType, IConstituenta, IRSForm, IRSFormData, IRSFormStats } from './rsform';
|
import { ConstituentaID, CstType, IConstituenta, IRSForm, IRSFormStats } from '@/models/rsform';
|
||||||
import { inferClass, inferStatus, inferTemplate, isBaseSet, isFunctional } from './rsformAPI';
|
import { inferClass, inferStatus, inferTemplate, isBaseSet, isFunctional } from '@/models/rsformAPI';
|
||||||
import { ParsingStatus, ValueClass } from './rslang';
|
import { ParsingStatus, ValueClass } from '@/models/rslang';
|
||||||
import { extractGlobals, isSimpleExpression, splitTemplateDefinition } from './rslangAPI';
|
import { extractGlobals, isSimpleExpression, splitTemplateDefinition } from '@/models/rslangAPI';
|
||||||
|
|
||||||
|
import { IRSFormDTO } from './api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads data into an {@link IRSForm} based on {@link IRSFormData}.
|
* Loads data into an {@link IRSForm} based on {@link IRSFormDTO}.
|
||||||
*
|
*
|
||||||
* @remarks
|
* @remarks
|
||||||
* This function processes the provided input, initializes the IRSForm, and calculates statistics
|
* This function processes the provided input, initializes the IRSForm, and calculates statistics
|
||||||
* based on the loaded data. It also establishes dependencies between concepts in the graph.
|
* based on the loaded data. It also establishes dependencies between concepts in the graph.
|
||||||
*/
|
*/
|
||||||
export class RSFormLoader {
|
export class RSFormLoader {
|
||||||
private schema: IRSFormData;
|
private schema: IRSFormDTO;
|
||||||
private graph: Graph = new Graph();
|
private graph: Graph = new Graph();
|
||||||
private cstByAlias = new Map<string, IConstituenta>();
|
private cstByAlias = new Map<string, IConstituenta>();
|
||||||
private cstByID = new Map<ConstituentaID, IConstituenta>();
|
private cstByID = new Map<ConstituentaID, IConstituenta>();
|
||||||
|
|
||||||
constructor(input: IRSFormData) {
|
constructor(input: IRSFormDTO) {
|
||||||
this.schema = input;
|
this.schema = input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +161,8 @@ export class RSFormLoader {
|
||||||
} else if (sources.size !== 1) {
|
} else if (sources.size !== 1) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
const base = this.cstByID.get(sources.values().next().value!)!;
|
const cstID = sources.values().next().value!;
|
||||||
|
const base = this.cstByID.get(cstID)!;
|
||||||
return !isFunctional(base.cst_type) || splitTemplateDefinition(base.definition_formal).head !== expression.head;
|
return !isFunctional(base.cst_type) || splitTemplateDefinition(base.definition_formal).head !== expression.head;
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -2,20 +2,42 @@ import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
||||||
import { DELAYS } from '@/backend/configuration';
|
import { DELAYS } from '@/backend/configuration';
|
||||||
import { LibraryItemID, VersionID } from '@/models/library';
|
import { ILibraryItemReference, ILibraryItemVersioned, LibraryItemID, VersionID } from '@/models/library';
|
||||||
import { ICstSubstitute, ICstSubstitutions } from '@/models/oss';
|
import { ICstSubstitute, ICstSubstitutions } from '@/models/oss';
|
||||||
import {
|
import {
|
||||||
ConstituentaID,
|
ConstituentaID,
|
||||||
CstType,
|
CstType,
|
||||||
IConstituentaList,
|
IConstituentaList,
|
||||||
IConstituentaMeta,
|
IConstituentaMeta,
|
||||||
IRSFormData,
|
IInheritanceInfo,
|
||||||
ITargetCst,
|
ITargetCst,
|
||||||
TermForm
|
TermForm
|
||||||
} from '@/models/rsform';
|
} from '@/models/rsform';
|
||||||
import { IExpressionParse } from '@/models/rslang';
|
import { IArgumentInfo, IExpressionParse, ParsingStatus, ValueClass } from '@/models/rslang';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents {@link IConstituenta} data from server.
|
||||||
|
*/
|
||||||
|
export interface IConstituentaDTO extends IConstituentaMeta {
|
||||||
|
parse: {
|
||||||
|
status: ParsingStatus;
|
||||||
|
valueClass: ValueClass;
|
||||||
|
typification: string;
|
||||||
|
syntaxTree: string;
|
||||||
|
args: IArgumentInfo[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents data for {@link IRSForm} provided by backend.
|
||||||
|
*/
|
||||||
|
export interface IRSFormDTO extends ILibraryItemVersioned {
|
||||||
|
items: IConstituentaDTO[];
|
||||||
|
inheritance: IInheritanceInfo[];
|
||||||
|
oss: ILibraryItemReference[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents data, used for uploading {@link IRSForm} as file.
|
* Represents data, used for uploading {@link IRSForm} as file.
|
||||||
*/
|
*/
|
||||||
|
@ -46,7 +68,7 @@ export interface ICstCreateDTO {
|
||||||
*/
|
*/
|
||||||
export interface ICstCreatedResponse {
|
export interface ICstCreatedResponse {
|
||||||
new_cst: IConstituentaMeta;
|
new_cst: IConstituentaMeta;
|
||||||
schema: IRSFormData;
|
schema: IRSFormDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,7 +107,7 @@ export interface ICstMoveDTO {
|
||||||
*/
|
*/
|
||||||
export interface IProduceStructureResponse {
|
export interface IProduceStructureResponse {
|
||||||
cst_list: ConstituentaID[];
|
cst_list: ConstituentaID[];
|
||||||
schema: IRSFormData;
|
schema: IRSFormDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,7 +139,7 @@ export const rsformsApi = {
|
||||||
queryFn: meta =>
|
queryFn: meta =>
|
||||||
!itemID
|
!itemID
|
||||||
? undefined
|
? undefined
|
||||||
: axiosGet<IRSFormData>({
|
: axiosGet<IRSFormDTO>({
|
||||||
endpoint: version ? `/api/library/${itemID}/versions/${version}` : `/api/rsforms/${itemID}/details`,
|
endpoint: version ? `/api/library/${itemID}/versions/${version}` : `/api/rsforms/${itemID}/details`,
|
||||||
options: { signal: meta.signal }
|
options: { signal: meta.signal }
|
||||||
})
|
})
|
||||||
|
@ -130,7 +152,7 @@ export const rsformsApi = {
|
||||||
options: { responseType: 'blob' }
|
options: { responseType: 'blob' }
|
||||||
}),
|
}),
|
||||||
upload: (data: IRSFormUploadDTO) =>
|
upload: (data: IRSFormUploadDTO) =>
|
||||||
axiosPatch<IRSFormUploadDTO, IRSFormData>({
|
axiosPatch<IRSFormUploadDTO, IRSFormDTO>({
|
||||||
endpoint: `/api/rsforms/${data.itemID}/load-trs`,
|
endpoint: `/api/rsforms/${data.itemID}/load-trs`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -160,7 +182,7 @@ export const rsformsApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
cstDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IConstituentaList }) =>
|
cstDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IConstituentaList }) =>
|
||||||
axiosDelete<IConstituentaList, IRSFormData>({
|
axiosDelete<IConstituentaList, IRSFormDTO>({
|
||||||
endpoint: `/api/rsforms/${itemID}/delete-multiple-cst`,
|
endpoint: `/api/rsforms/${itemID}/delete-multiple-cst`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -176,7 +198,7 @@ export const rsformsApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
cstSubstitute: ({ itemID, data }: { itemID: LibraryItemID; data: ICstSubstitutions }) =>
|
cstSubstitute: ({ itemID, data }: { itemID: LibraryItemID; data: ICstSubstitutions }) =>
|
||||||
axiosPatch<ICstSubstitutions, IRSFormData>({
|
axiosPatch<ICstSubstitutions, IRSFormDTO>({
|
||||||
endpoint: `/api/rsforms/${itemID}/substitute`,
|
endpoint: `/api/rsforms/${itemID}/substitute`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -184,7 +206,7 @@ export const rsformsApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
cstMove: ({ itemID, data }: { itemID: LibraryItemID; data: ICstMoveDTO }) =>
|
cstMove: ({ itemID, data }: { itemID: LibraryItemID; data: ICstMoveDTO }) =>
|
||||||
axiosPatch<ICstMoveDTO, IRSFormData>({
|
axiosPatch<ICstMoveDTO, IRSFormDTO>({
|
||||||
endpoint: `/api/rsforms/${itemID}/move-cst`,
|
endpoint: `/api/rsforms/${itemID}/move-cst`,
|
||||||
request: { data: data }
|
request: { data: data }
|
||||||
}),
|
}),
|
||||||
|
@ -198,7 +220,7 @@ export const rsformsApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
inlineSynthesis: ({ itemID, data }: { itemID: LibraryItemID; data: IInlineSynthesisDTO }) =>
|
inlineSynthesis: ({ itemID, data }: { itemID: LibraryItemID; data: IInlineSynthesisDTO }) =>
|
||||||
axiosPost<IInlineSynthesisDTO, IRSFormData>({
|
axiosPost<IInlineSynthesisDTO, IRSFormDTO>({
|
||||||
endpoint: `/api/rsforms/${itemID}/inline-synthesis`,
|
endpoint: `/api/rsforms/${itemID}/inline-synthesis`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -206,12 +228,12 @@ export const rsformsApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
restoreOrder: ({ itemID }: { itemID: LibraryItemID }) =>
|
restoreOrder: ({ itemID }: { itemID: LibraryItemID }) =>
|
||||||
axiosPatch<undefined, IRSFormData>({
|
axiosPatch<undefined, IRSFormDTO>({
|
||||||
endpoint: `/api/rsforms/${itemID}/restore-order`,
|
endpoint: `/api/rsforms/${itemID}/restore-order`,
|
||||||
request: { successMessage: information.reorderComplete }
|
request: { successMessage: information.reorderComplete }
|
||||||
}),
|
}),
|
||||||
resetAliases: ({ itemID }: { itemID: LibraryItemID }) =>
|
resetAliases: ({ itemID }: { itemID: LibraryItemID }) =>
|
||||||
axiosPatch<undefined, IRSFormData>({
|
axiosPatch<undefined, IRSFormDTO>({
|
||||||
endpoint: `/api/rsforms/${itemID}/reset-aliases`,
|
endpoint: `/api/rsforms/${itemID}/reset-aliases`,
|
||||||
request: { successMessage: information.reindexComplete }
|
request: { successMessage: information.reindexComplete }
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -4,9 +4,8 @@ import { DataCallback } from '@/backend/apiTransport';
|
||||||
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
|
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
|
||||||
import { ossApi } from '@/backend/oss/api';
|
import { ossApi } from '@/backend/oss/api';
|
||||||
import { LibraryItemID } from '@/models/library';
|
import { LibraryItemID } from '@/models/library';
|
||||||
import { IRSFormData } from '@/models/rsform';
|
|
||||||
|
|
||||||
import { IInlineSynthesisDTO, rsformsApi } from './api';
|
import { IInlineSynthesisDTO, IRSFormDTO, rsformsApi } from './api';
|
||||||
|
|
||||||
export const useInlineSynthesis = () => {
|
export const useInlineSynthesis = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
@ -33,7 +32,7 @@ export const useInlineSynthesis = () => {
|
||||||
itemID: LibraryItemID; //
|
itemID: LibraryItemID; //
|
||||||
data: IInlineSynthesisDTO;
|
data: IInlineSynthesisDTO;
|
||||||
},
|
},
|
||||||
onSuccess?: DataCallback<IRSFormData>
|
onSuccess?: DataCallback<IRSFormDTO>
|
||||||
) => mutation.mutate(data, { onSuccess })
|
) => mutation.mutate(data, { onSuccess })
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { RSFormLoader } from '@/backend/rsform/RSFormLoader';
|
||||||
import { LibraryItemID, VersionID } from '@/models/library';
|
import { LibraryItemID, VersionID } from '@/models/library';
|
||||||
import { RSFormLoader } from '@/models/RSFormLoader';
|
|
||||||
|
|
||||||
import { queryClient } from '../queryClient';
|
import { queryClient } from '../queryClient';
|
||||||
import { rsformsApi } from './api';
|
import { rsformsApi } from './api';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useQueries } from '@tanstack/react-query';
|
import { useQueries } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { RSFormLoader } from '@/backend/rsform/RSFormLoader';
|
||||||
import { LibraryItemID } from '@/models/library';
|
import { LibraryItemID } from '@/models/library';
|
||||||
import { RSFormLoader } from '@/models/RSFormLoader';
|
|
||||||
|
|
||||||
import { DELAYS } from '../configuration';
|
import { DELAYS } from '../configuration';
|
||||||
import { rsformsApi } from './api';
|
import { rsformsApi } from './api';
|
||||||
|
|
|
@ -1,14 +1,45 @@
|
||||||
import { queryOptions } from '@tanstack/react-query';
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
||||||
import { DELAYS } from '@/backend/configuration';
|
import { DELAYS } from '@/backend/configuration';
|
||||||
import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user';
|
import { IUserInfo, IUserProfile } from '@/models/user';
|
||||||
import { information } from '@/utils/labels';
|
import { patterns } from '@/utils/constants';
|
||||||
|
import { errors, information } from '@/utils/labels';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents signup data, used to create new users.
|
||||||
|
*/
|
||||||
|
export const UserSignupSchema = z
|
||||||
|
.object({
|
||||||
|
username: z.string().nonempty(errors.requiredField).regex(RegExp(patterns.login), errors.loginFormat),
|
||||||
|
email: z.string().email(errors.emailField),
|
||||||
|
first_name: z.string(),
|
||||||
|
last_name: z.string(),
|
||||||
|
|
||||||
|
password: z.string().nonempty(errors.requiredField),
|
||||||
|
password2: z.string().nonempty(errors.requiredField)
|
||||||
|
})
|
||||||
|
.refine(schema => schema.password === schema.password2, { path: ['password2'], message: errors.passwordsMismatch });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents signup data, used to create new users.
|
||||||
|
*/
|
||||||
|
export type IUserSignupDTO = z.infer<typeof UserSignupSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents user data, intended to update user profile in persistent storage.
|
* Represents user data, intended to update user profile in persistent storage.
|
||||||
*/
|
*/
|
||||||
export interface IUpdateProfileDTO extends Omit<IUser, 'is_staff' | 'id'> {}
|
export const UpdateProfileSchema = z.object({
|
||||||
|
email: z.string().email(errors.emailField),
|
||||||
|
first_name: z.string(),
|
||||||
|
last_name: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents user data, intended to update user profile in persistent storage.
|
||||||
|
*/
|
||||||
|
export type IUpdateProfileDTO = z.infer<typeof UpdateProfileSchema>;
|
||||||
|
|
||||||
export const usersApi = {
|
export const usersApi = {
|
||||||
baseKey: 'users',
|
baseKey: 'users',
|
||||||
|
@ -33,8 +64,8 @@ export const usersApi = {
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
signup: (data: IUserSignupData) =>
|
signup: (data: IUserSignupDTO) =>
|
||||||
axiosPost<IUserSignupData, IUserProfile>({
|
axiosPost<IUserSignupDTO, IUserProfile>({
|
||||||
endpoint: '/users/api/signup',
|
endpoint: '/users/api/signup',
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { DataCallback } from '@/backend/apiTransport';
|
import { DataCallback } from '@/backend/apiTransport';
|
||||||
import { usersApi } from '@/backend/users/api';
|
import { IUserSignupDTO, usersApi } from '@/backend/users/api';
|
||||||
import { IUserProfile, IUserSignupData } from '@/models/user';
|
import { IUserProfile } from '@/models/user';
|
||||||
|
|
||||||
export const useSignup = () => {
|
export const useSignup = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
@ -13,7 +13,7 @@ export const useSignup = () => {
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
signup: (
|
signup: (
|
||||||
data: IUserSignupData, //
|
data: IUserSignupDTO, //
|
||||||
onSuccess?: DataCallback<IUserProfile>
|
onSuccess?: DataCallback<IUserProfile>
|
||||||
) => mutation.mutate(data, { onSuccess }),
|
) => mutation.mutate(data, { onSuccess }),
|
||||||
isPending: mutation.isPending,
|
isPending: mutation.isPending,
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { IUserProfile } from '@/models/user';
|
||||||
|
|
||||||
|
import { DataCallback } from '../apiTransport';
|
||||||
import { IUpdateProfileDTO, usersApi } from './api';
|
import { IUpdateProfileDTO, usersApi } from './api';
|
||||||
|
|
||||||
export const useUpdateProfile = () => {
|
export const useUpdateProfile = () => {
|
||||||
|
@ -13,7 +16,8 @@ export const useUpdateProfile = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
updateProfile: (data: IUpdateProfileDTO) => mutation.mutate(data),
|
updateProfile: (data: IUpdateProfileDTO, onSuccess?: DataCallback<IUserProfile>) =>
|
||||||
|
mutation.mutate(data, { onSuccess }),
|
||||||
isPending: mutation.isPending,
|
isPending: mutation.isPending,
|
||||||
error: mutation.error,
|
error: mutation.error,
|
||||||
reset: mutation.reset
|
reset: mutation.reset
|
||||||
|
|
8
rsconcept/frontend/src/components/props.d.ts
vendored
8
rsconcept/frontend/src/components/props.d.ts
vendored
|
@ -1,5 +1,6 @@
|
||||||
// =========== Module contains interfaces for common UI elements. ==========
|
// =========== Module contains interfaces for common UI elements. ==========
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { FieldError } from 'react-hook-form';
|
||||||
|
|
||||||
export namespace CProps {
|
export namespace CProps {
|
||||||
/**
|
/**
|
||||||
|
@ -35,6 +36,13 @@ export namespace CProps {
|
||||||
hideTitle?: boolean;
|
hideTitle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an object that can have an error message.
|
||||||
|
*/
|
||||||
|
export interface ErrorProcessing {
|
||||||
|
error?: FieldError;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents `control` component with optional title and configuration options.
|
* Represents `control` component with optional title and configuration options.
|
||||||
*
|
*
|
||||||
|
|
|
@ -17,6 +17,9 @@ import { describeConstituenta } from '@/utils/labels';
|
||||||
|
|
||||||
interface PickConstituentaProps extends CProps.Styling {
|
interface PickConstituentaProps extends CProps.Styling {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
value?: IConstituenta;
|
||||||
|
onChange: (newValue: IConstituenta) => void;
|
||||||
|
|
||||||
prefixID: string;
|
prefixID: string;
|
||||||
data?: IConstituenta[];
|
data?: IConstituenta[];
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
@ -25,9 +28,6 @@ interface PickConstituentaProps extends CProps.Styling {
|
||||||
onBeginFilter?: (cst: IConstituenta) => boolean;
|
onBeginFilter?: (cst: IConstituenta) => boolean;
|
||||||
describeFunc?: (cst: IConstituenta) => string;
|
describeFunc?: (cst: IConstituenta) => string;
|
||||||
matchFunc?: (cst: IConstituenta, filter: string) => boolean;
|
matchFunc?: (cst: IConstituenta, filter: string) => boolean;
|
||||||
|
|
||||||
value?: IConstituenta;
|
|
||||||
onSelectValue: (newValue: IConstituenta) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<IConstituenta>();
|
const columnHelper = createColumnHelper<IConstituenta>();
|
||||||
|
@ -42,7 +42,7 @@ function PickConstituenta({
|
||||||
describeFunc = describeConstituenta,
|
describeFunc = describeConstituenta,
|
||||||
matchFunc = (cst, filter) => matchConstituenta(cst, filter, CstMatchMode.ALL),
|
matchFunc = (cst, filter) => matchConstituenta(cst, filter, CstMatchMode.ALL),
|
||||||
onBeginFilter,
|
onBeginFilter,
|
||||||
onSelectValue,
|
onChange,
|
||||||
className,
|
className,
|
||||||
...restProps
|
...restProps
|
||||||
}: PickConstituentaProps) {
|
}: PickConstituentaProps) {
|
||||||
|
@ -110,7 +110,7 @@ function PickConstituenta({
|
||||||
<p>Измените параметры фильтра</p>
|
<p>Измените параметры фильтра</p>
|
||||||
</NoData>
|
</NoData>
|
||||||
}
|
}
|
||||||
onRowClicked={onSelectValue}
|
onRowClicked={onChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,15 +18,15 @@ import ToolbarGraphSelection from './ToolbarGraphSelection';
|
||||||
|
|
||||||
interface PickMultiConstituentaProps extends CProps.Styling {
|
interface PickMultiConstituentaProps extends CProps.Styling {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
value: ConstituentaID[];
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||||
|
|
||||||
schema: IRSForm;
|
schema: IRSForm;
|
||||||
data: IConstituenta[];
|
data: IConstituenta[];
|
||||||
|
|
||||||
prefixID: string;
|
prefixID: string;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
|
|
||||||
selected: ConstituentaID[];
|
|
||||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<IConstituenta>();
|
const columnHelper = createColumnHelper<IConstituenta>();
|
||||||
|
@ -38,8 +38,8 @@ function PickMultiConstituenta({
|
||||||
prefixID,
|
prefixID,
|
||||||
rows,
|
rows,
|
||||||
noBorder,
|
noBorder,
|
||||||
selected,
|
value,
|
||||||
setSelected,
|
onChange,
|
||||||
className,
|
className,
|
||||||
...restProps
|
...restProps
|
||||||
}: PickMultiConstituentaProps) {
|
}: PickMultiConstituentaProps) {
|
||||||
|
@ -74,10 +74,10 @@ function PickMultiConstituenta({
|
||||||
}
|
}
|
||||||
const newRowSelection: RowSelectionState = {};
|
const newRowSelection: RowSelectionState = {};
|
||||||
filtered.forEach((cst, index) => {
|
filtered.forEach((cst, index) => {
|
||||||
newRowSelection[String(index)] = selected.includes(cst.id);
|
newRowSelection[String(index)] = value.includes(cst.id);
|
||||||
});
|
});
|
||||||
setRowSelection(newRowSelection);
|
setRowSelection(newRowSelection);
|
||||||
}, [filtered, setRowSelection, selected]);
|
}, [filtered, setRowSelection, value]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
|
@ -91,7 +91,7 @@ function PickMultiConstituenta({
|
||||||
|
|
||||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
setSelected([]);
|
onChange([]);
|
||||||
} else {
|
} else {
|
||||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||||
const newSelection: ConstituentaID[] = [];
|
const newSelection: ConstituentaID[] = [];
|
||||||
|
@ -100,7 +100,7 @@ function PickMultiConstituenta({
|
||||||
newSelection.push(cst.id);
|
newSelection.push(cst.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setSelected(prev => [...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)), ...newSelection]);
|
onChange(prev => [...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)), ...newSelection]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ function PickMultiConstituenta({
|
||||||
<div className={clsx(noBorder ? '' : 'border', className)} {...restProps}>
|
<div className={clsx(noBorder ? '' : 'border', className)} {...restProps}>
|
||||||
<div className={clsx('px-3 flex justify-between items-center', 'clr-input', 'border-b', 'rounded-t-md')}>
|
<div className={clsx('px-3 flex justify-between items-center', 'clr-input', 'border-b', 'rounded-t-md')}>
|
||||||
<div className='w-[24ch] select-none whitespace-nowrap'>
|
<div className='w-[24ch] select-none whitespace-nowrap'>
|
||||||
{data.length > 0 ? `Выбраны ${selected.length} из ${data.length}` : 'Конституенты'}
|
{data.length > 0 ? `Выбраны ${value.length} из ${data.length}` : 'Конституенты'}
|
||||||
</div>
|
</div>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
id='dlg_constituents_search'
|
id='dlg_constituents_search'
|
||||||
|
@ -135,9 +135,9 @@ function PickMultiConstituenta({
|
||||||
graph={foldedGraph}
|
graph={foldedGraph}
|
||||||
isCore={cstID => isBasicConcept(schema.cstByID.get(cstID)?.cst_type)}
|
isCore={cstID => isBasicConcept(schema.cstByID.get(cstID)?.cst_type)}
|
||||||
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
||||||
selected={selected}
|
value={value}
|
||||||
setSelected={setSelected}
|
onChange={onChange}
|
||||||
emptySelection={selected.length === 0}
|
emptySelection={value.length === 0}
|
||||||
className='w-fit'
|
className='w-fit'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,36 +12,36 @@ import NoData from '@/components/ui/NoData';
|
||||||
import { IOperation, OperationID } from '@/models/oss';
|
import { IOperation, OperationID } from '@/models/oss';
|
||||||
|
|
||||||
interface PickMultiOperationProps extends CProps.Styling {
|
interface PickMultiOperationProps extends CProps.Styling {
|
||||||
rows?: number;
|
value: OperationID[];
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||||
|
|
||||||
items: IOperation[];
|
items: IOperation[];
|
||||||
selected: OperationID[];
|
rows?: number;
|
||||||
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<IOperation>();
|
const columnHelper = createColumnHelper<IOperation>();
|
||||||
|
|
||||||
function PickMultiOperation({ rows, items, selected, setSelected, className, ...restProps }: PickMultiOperationProps) {
|
function PickMultiOperation({ rows, items, value, onChange, className, ...restProps }: PickMultiOperationProps) {
|
||||||
const selectedItems = selected.map(itemID => items.find(item => item.id === itemID)!);
|
const selectedItems = value.map(itemID => items.find(item => item.id === itemID)!);
|
||||||
const nonSelectedItems = items.filter(item => !selected.includes(item.id));
|
const nonSelectedItems = items.filter(item => !value.includes(item.id));
|
||||||
const [lastSelected, setLastSelected] = useState<IOperation | undefined>(undefined);
|
const [lastSelected, setLastSelected] = useState<IOperation | undefined>(undefined);
|
||||||
|
|
||||||
function handleDelete(operation: OperationID) {
|
function handleDelete(operation: OperationID) {
|
||||||
setSelected(prev => prev.filter(item => item !== operation));
|
onChange(prev => prev.filter(item => item !== operation));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelect(operation?: IOperation) {
|
function handleSelect(operation?: IOperation) {
|
||||||
if (operation) {
|
if (operation) {
|
||||||
setLastSelected(operation);
|
setLastSelected(operation);
|
||||||
setSelected(prev => [...prev, operation.id]);
|
onChange(prev => [...prev, operation.id]);
|
||||||
setTimeout(() => setLastSelected(undefined), 1000);
|
setTimeout(() => setLastSelected(undefined), 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMoveUp(operation: OperationID) {
|
function handleMoveUp(operation: OperationID) {
|
||||||
const index = selected.indexOf(operation);
|
const index = value.indexOf(operation);
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
setSelected(prev => {
|
onChange(prev => {
|
||||||
const newSelected = [...prev];
|
const newSelected = [...prev];
|
||||||
newSelected[index] = newSelected[index - 1];
|
newSelected[index] = newSelected[index - 1];
|
||||||
newSelected[index - 1] = operation;
|
newSelected[index - 1] = operation;
|
||||||
|
@ -51,9 +51,9 @@ function PickMultiOperation({ rows, items, selected, setSelected, className, ...
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMoveDown(operation: OperationID) {
|
function handleMoveDown(operation: OperationID) {
|
||||||
const index = selected.indexOf(operation);
|
const index = value.indexOf(operation);
|
||||||
if (index < selected.length - 1) {
|
if (index < value.length - 1) {
|
||||||
setSelected(prev => {
|
onChange(prev => {
|
||||||
const newSelected = [...prev];
|
const newSelected = [...prev];
|
||||||
newSelected[index] = newSelected[index + 1];
|
newSelected[index] = newSelected[index + 1];
|
||||||
newSelected[index + 1] = operation;
|
newSelected[index + 1] = operation;
|
||||||
|
@ -118,7 +118,7 @@ function PickMultiOperation({ rows, items, selected, setSelected, className, ...
|
||||||
noBorder
|
noBorder
|
||||||
items={nonSelectedItems} // prettier: split-line
|
items={nonSelectedItems} // prettier: split-line
|
||||||
value={lastSelected}
|
value={lastSelected}
|
||||||
onSelectValue={handleSelect}
|
onChange={handleSelect}
|
||||||
/>
|
/>
|
||||||
<DataTable
|
<DataTable
|
||||||
dense
|
dense
|
||||||
|
|
|
@ -19,14 +19,15 @@ import SelectLocation from './SelectLocation';
|
||||||
|
|
||||||
interface PickSchemaProps extends CProps.Styling {
|
interface PickSchemaProps extends CProps.Styling {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
value?: LibraryItemID;
|
||||||
|
onChange: (newValue: LibraryItemID) => void;
|
||||||
|
|
||||||
initialFilter?: string;
|
initialFilter?: string;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
|
||||||
items: ILibraryItem[];
|
items: ILibraryItem[];
|
||||||
itemType: LibraryItemType;
|
itemType: LibraryItemType;
|
||||||
value?: LibraryItemID;
|
|
||||||
baseFilter?: (target: ILibraryItem) => boolean;
|
baseFilter?: (target: ILibraryItem) => boolean;
|
||||||
onSelectValue: (newValue: LibraryItemID) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<ILibraryItem>();
|
const columnHelper = createColumnHelper<ILibraryItem>();
|
||||||
|
@ -38,7 +39,7 @@ function PickSchema({
|
||||||
items,
|
items,
|
||||||
itemType,
|
itemType,
|
||||||
value,
|
value,
|
||||||
onSelectValue,
|
onChange,
|
||||||
baseFilter,
|
baseFilter,
|
||||||
className,
|
className,
|
||||||
...restProps
|
...restProps
|
||||||
|
@ -156,7 +157,7 @@ function PickSchema({
|
||||||
<p>Измените параметры фильтра</p>
|
<p>Измените параметры фильтра</p>
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
}
|
}
|
||||||
onRowClicked={rowData => onSelectValue(rowData.id)}
|
onRowClicked={rowData => onChange(rowData.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,8 +20,9 @@ import { errors } from '@/utils/labels';
|
||||||
import SelectLibraryItem from './SelectLibraryItem';
|
import SelectLibraryItem from './SelectLibraryItem';
|
||||||
|
|
||||||
interface PickSubstitutionsProps extends CProps.Styling {
|
interface PickSubstitutionsProps extends CProps.Styling {
|
||||||
substitutions: ICstSubstitute[];
|
value: ICstSubstitute[];
|
||||||
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
|
onChange: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
|
||||||
|
|
||||||
suggestions?: ICstSubstitute[];
|
suggestions?: ICstSubstitute[];
|
||||||
|
|
||||||
prefixID: string;
|
prefixID: string;
|
||||||
|
@ -35,8 +36,8 @@ interface PickSubstitutionsProps extends CProps.Styling {
|
||||||
const columnHelper = createColumnHelper<IMultiSubstitution>();
|
const columnHelper = createColumnHelper<IMultiSubstitution>();
|
||||||
|
|
||||||
function PickSubstitutions({
|
function PickSubstitutions({
|
||||||
substitutions,
|
value,
|
||||||
setSubstitutions,
|
onChange,
|
||||||
suggestions,
|
suggestions,
|
||||||
prefixID,
|
prefixID,
|
||||||
rows,
|
rows,
|
||||||
|
@ -66,7 +67,7 @@ function PickSubstitutions({
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const substitutionData: IMultiSubstitution[] = [
|
const substitutionData: IMultiSubstitution[] = [
|
||||||
...substitutions.map(item => ({
|
...value.map(item => ({
|
||||||
original_source: getSchemaByCst(item.original)!,
|
original_source: getSchemaByCst(item.original)!,
|
||||||
original: getConstituenta(item.original)!,
|
original: getConstituenta(item.original)!,
|
||||||
substitution: getConstituenta(item.substitution)!,
|
substitution: getConstituenta(item.substitution)!,
|
||||||
|
@ -110,8 +111,8 @@ function PickSubstitutions({
|
||||||
original: deleteRight ? rightCst.id : leftCst.id,
|
original: deleteRight ? rightCst.id : leftCst.id,
|
||||||
substitution: deleteRight ? leftCst.id : rightCst.id
|
substitution: deleteRight ? leftCst.id : rightCst.id
|
||||||
};
|
};
|
||||||
const toDelete = substitutions.map(item => item.original);
|
const toDelete = value.map(item => item.original);
|
||||||
const replacements = substitutions.map(item => item.substitution);
|
const replacements = value.map(item => item.substitution);
|
||||||
if (
|
if (
|
||||||
toDelete.includes(newSubstitution.original) ||
|
toDelete.includes(newSubstitution.original) ||
|
||||||
toDelete.includes(newSubstitution.substitution) ||
|
toDelete.includes(newSubstitution.substitution) ||
|
||||||
|
@ -126,7 +127,7 @@ function PickSubstitutions({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSubstitutions(prev => [...prev, newSubstitution]);
|
onChange(prev => [...prev, newSubstitution]);
|
||||||
setLeftCst(undefined);
|
setLeftCst(undefined);
|
||||||
setRightCst(undefined);
|
setRightCst(undefined);
|
||||||
}
|
}
|
||||||
|
@ -136,12 +137,12 @@ function PickSubstitutions({
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAcceptSuggestion(item: IMultiSubstitution) {
|
function handleAcceptSuggestion(item: IMultiSubstitution) {
|
||||||
setSubstitutions(prev => [...prev, { original: item.original.id, substitution: item.substitution.id }]);
|
onChange(prev => [...prev, { original: item.original.id, substitution: item.substitution.id }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteSubstitution(target: IMultiSubstitution) {
|
function handleDeleteSubstitution(target: IMultiSubstitution) {
|
||||||
handleDeclineSuggestion(target);
|
handleDeclineSuggestion(target);
|
||||||
setSubstitutions(prev => {
|
onChange(prev => {
|
||||||
const newItems: ICstSubstitute[] = [];
|
const newItems: ICstSubstitute[] = [];
|
||||||
prev.forEach(item => {
|
prev.forEach(item => {
|
||||||
if (item.original !== target.original.id || item.substitution !== target.substitution.id) {
|
if (item.original !== target.original.id || item.substitution !== target.substitution.id) {
|
||||||
|
@ -230,15 +231,15 @@ function PickSubstitutions({
|
||||||
placeholder='Выберите аргумент'
|
placeholder='Выберите аргумент'
|
||||||
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== rightArgument?.id)}
|
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== rightArgument?.id)}
|
||||||
value={leftArgument}
|
value={leftArgument}
|
||||||
onSelectValue={setLeftArgument}
|
onChange={setLeftArgument}
|
||||||
/>
|
/>
|
||||||
<SelectConstituenta
|
<SelectConstituenta
|
||||||
noBorder
|
noBorder
|
||||||
items={(leftArgument as IRSForm)?.items.filter(
|
items={(leftArgument as IRSForm)?.items.filter(
|
||||||
cst => !substitutions.find(item => item.original === cst.id) && (!filter || filter(cst))
|
cst => !value.find(item => item.original === cst.id) && (!filter || filter(cst))
|
||||||
)}
|
)}
|
||||||
value={leftCst}
|
value={leftCst}
|
||||||
onSelectValue={setLeftCst}
|
onChange={setLeftCst}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
|
@ -268,15 +269,15 @@ function PickSubstitutions({
|
||||||
placeholder='Выберите аргумент'
|
placeholder='Выберите аргумент'
|
||||||
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== leftArgument?.id)}
|
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== leftArgument?.id)}
|
||||||
value={rightArgument}
|
value={rightArgument}
|
||||||
onSelectValue={setRightArgument}
|
onChange={setRightArgument}
|
||||||
/>
|
/>
|
||||||
<SelectConstituenta
|
<SelectConstituenta
|
||||||
noBorder
|
noBorder
|
||||||
items={(rightArgument as IRSForm)?.items.filter(
|
items={(rightArgument as IRSForm)?.items.filter(
|
||||||
cst => !substitutions.find(item => item.original === cst.id) && (!filter || filter(cst))
|
cst => !value.find(item => item.original === cst.id) && (!filter || filter(cst))
|
||||||
)}
|
)}
|
||||||
value={rightCst}
|
value={rightCst}
|
||||||
onSelectValue={setRightCst}
|
onChange={setRightCst}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { PolicyIcon } from '@/components/DomainIcons';
|
import { PolicyIcon } from '@/components/DomainIcons';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
|
@ -15,6 +13,7 @@ import { describeAccessPolicy, labelAccessPolicy } from '@/utils/labels';
|
||||||
interface SelectAccessPolicyProps extends CProps.Styling {
|
interface SelectAccessPolicyProps extends CProps.Styling {
|
||||||
value: AccessPolicy;
|
value: AccessPolicy;
|
||||||
onChange: (value: AccessPolicy) => void;
|
onChange: (value: AccessPolicy) => void;
|
||||||
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
stretchLeft?: boolean;
|
stretchLeft?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -22,15 +21,12 @@ interface SelectAccessPolicyProps extends CProps.Styling {
|
||||||
function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
|
function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
|
|
||||||
const handleChange = useCallback(
|
function handleChange(newValue: AccessPolicy) {
|
||||||
(newValue: AccessPolicy) => {
|
menu.hide();
|
||||||
menu.hide();
|
if (newValue !== value) {
|
||||||
if (newValue !== value) {
|
onChange(newValue);
|
||||||
onChange(newValue);
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[menu, value, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} {...restProps}>
|
<div ref={menu.ref} {...restProps}>
|
||||||
|
|
|
@ -10,10 +10,10 @@ import { matchConstituenta } from '@/models/rsformAPI';
|
||||||
import { describeConstituenta, describeConstituentaTerm } from '@/utils/labels';
|
import { describeConstituenta, describeConstituentaTerm } from '@/utils/labels';
|
||||||
|
|
||||||
interface SelectConstituentaProps extends CProps.Styling {
|
interface SelectConstituentaProps extends CProps.Styling {
|
||||||
items?: IConstituenta[];
|
|
||||||
value?: IConstituenta;
|
value?: IConstituenta;
|
||||||
onSelectValue: (newValue?: IConstituenta) => void;
|
onChange: (newValue?: IConstituenta) => void;
|
||||||
|
|
||||||
|
items?: IConstituenta[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ function SelectConstituenta({
|
||||||
className,
|
className,
|
||||||
items,
|
items,
|
||||||
value,
|
value,
|
||||||
onSelectValue,
|
onChange,
|
||||||
placeholder = 'Выберите конституенту',
|
placeholder = 'Выберите конституенту',
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectConstituentaProps) {
|
}: SelectConstituentaProps) {
|
||||||
|
@ -42,7 +42,7 @@ function SelectConstituenta({
|
||||||
className={clsx('text-ellipsis', className)}
|
className={clsx('text-ellipsis', className)}
|
||||||
options={options}
|
options={options}
|
||||||
value={value ? { value: value.id, label: `${value.alias}: ${describeConstituentaTerm(value)}` } : null}
|
value={value ? { value: value.id, label: `${value.alias}: ${describeConstituentaTerm(value)}` } : null}
|
||||||
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
|
onChange={data => onChange(items?.find(cst => cst.id === data?.value))}
|
||||||
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||||
filterOption={filter}
|
filterOption={filter}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { DependencyIcon } from '@/components/DomainIcons';
|
import { DependencyIcon } from '@/components/DomainIcons';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
|
@ -15,21 +13,18 @@ import { describeCstSource, labelCstSource } from '@/utils/labels';
|
||||||
|
|
||||||
interface SelectGraphFilterProps extends CProps.Styling {
|
interface SelectGraphFilterProps extends CProps.Styling {
|
||||||
value: DependencyMode;
|
value: DependencyMode;
|
||||||
dense?: boolean;
|
|
||||||
onChange: (value: DependencyMode) => void;
|
onChange: (value: DependencyMode) => void;
|
||||||
|
dense?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectGraphFilter({ value, dense, onChange, ...restProps }: SelectGraphFilterProps) {
|
function SelectGraphFilter({ value, dense, onChange, ...restProps }: SelectGraphFilterProps) {
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
const size = useWindowSize();
|
const size = useWindowSize();
|
||||||
|
|
||||||
const handleChange = useCallback(
|
function handleChange(newValue: DependencyMode) {
|
||||||
(newValue: DependencyMode) => {
|
menu.hide();
|
||||||
menu.hide();
|
onChange(newValue);
|
||||||
onChange(newValue);
|
}
|
||||||
},
|
|
||||||
[menu, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} {...restProps}>
|
<div ref={menu.ref} {...restProps}>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { ItemTypeIcon } from '@/components/DomainIcons';
|
import { ItemTypeIcon } from '@/components/DomainIcons';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
|
@ -22,15 +20,12 @@ interface SelectItemTypeProps extends CProps.Styling {
|
||||||
function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
|
function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
|
|
||||||
const handleChange = useCallback(
|
function handleChange(newValue: LibraryItemType) {
|
||||||
(newValue: LibraryItemType) => {
|
menu.hide();
|
||||||
menu.hide();
|
if (newValue !== value) {
|
||||||
if (newValue !== value) {
|
onChange(newValue);
|
||||||
onChange(newValue);
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[menu, value, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} {...restProps}>
|
<div ref={menu.ref} {...restProps}>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { matchLibraryItem } from '@/models/libraryAPI';
|
||||||
interface SelectLibraryItemProps extends CProps.Styling {
|
interface SelectLibraryItemProps extends CProps.Styling {
|
||||||
items?: ILibraryItem[];
|
items?: ILibraryItem[];
|
||||||
value?: ILibraryItem;
|
value?: ILibraryItem;
|
||||||
onSelectValue: (newValue?: ILibraryItem) => void;
|
onChange: (newValue?: ILibraryItem) => void;
|
||||||
|
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
|
@ -20,7 +20,7 @@ function SelectLibraryItem({
|
||||||
className,
|
className,
|
||||||
items,
|
items,
|
||||||
value,
|
value,
|
||||||
onSelectValue,
|
onChange,
|
||||||
placeholder = 'Выберите схему',
|
placeholder = 'Выберите схему',
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectLibraryItemProps) {
|
}: SelectLibraryItemProps) {
|
||||||
|
@ -40,7 +40,7 @@ function SelectLibraryItem({
|
||||||
className={clsx('text-ellipsis', className)}
|
className={clsx('text-ellipsis', className)}
|
||||||
options={options}
|
options={options}
|
||||||
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null}
|
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null}
|
||||||
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
|
onChange={data => onChange(items?.find(cst => cst.id === data?.value))}
|
||||||
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||||
filterOption={filter}
|
filterOption={filter}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|
|
@ -12,9 +12,10 @@ import { labelFolderNode } from '@/utils/labels';
|
||||||
|
|
||||||
interface SelectLocationProps extends CProps.Styling {
|
interface SelectLocationProps extends CProps.Styling {
|
||||||
value: string;
|
value: string;
|
||||||
|
onClick: (event: CProps.EventMouse, target: FolderNode) => void;
|
||||||
|
|
||||||
prefix: string;
|
prefix: string;
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
onClick: (event: CProps.EventMouse, target: FolderNode) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectLocation({ value, dense, prefix, onClick, className, style }: SelectLocationProps) {
|
function SelectLocation({ value, dense, prefix, onClick, className, style }: SelectLocationProps) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { IconFolderTree } from '@/components/Icons';
|
import { IconFolderTree } from '@/components/Icons';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
|
@ -14,10 +13,9 @@ import SelectLocation from './SelectLocation';
|
||||||
|
|
||||||
interface SelectLocationContextProps extends CProps.Styling {
|
interface SelectLocationContextProps extends CProps.Styling {
|
||||||
value: string;
|
value: string;
|
||||||
|
onChange: (newValue: string) => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
stretchTop?: boolean;
|
stretchTop?: boolean;
|
||||||
|
|
||||||
onChange: (newValue: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectLocationContext({
|
function SelectLocationContext({
|
||||||
|
@ -29,15 +27,12 @@ function SelectLocationContext({
|
||||||
}: SelectLocationContextProps) {
|
}: SelectLocationContextProps) {
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
|
|
||||||
const handleClick = useCallback(
|
function handleClick(event: CProps.EventMouse, newValue: string) {
|
||||||
(event: CProps.EventMouse, newValue: string) => {
|
event.preventDefault();
|
||||||
event.preventDefault();
|
event.stopPropagation();
|
||||||
event.stopPropagation();
|
menu.hide();
|
||||||
menu.hide();
|
onChange(newValue);
|
||||||
onChange(newValue);
|
}
|
||||||
},
|
|
||||||
[menu, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} className='h-full text-right self-start mt-[-0.25rem] ml-[-1.5rem]'>
|
<div ref={menu.ref} className='h-full text-right self-start mt-[-0.25rem] ml-[-1.5rem]'>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { LocationIcon } from '@/components/DomainIcons';
|
import { LocationIcon } from '@/components/DomainIcons';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
|
@ -22,13 +21,10 @@ interface SelectLocationHeadProps extends CProps.Styling {
|
||||||
function SelectLocationHead({ value, excluded = [], onChange, className, ...restProps }: SelectLocationHeadProps) {
|
function SelectLocationHead({ value, excluded = [], onChange, className, ...restProps }: SelectLocationHeadProps) {
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
|
|
||||||
const handleChange = useCallback(
|
function handleChange(newValue: LocationHead) {
|
||||||
(newValue: LocationHead) => {
|
menu.hide();
|
||||||
menu.hide();
|
onChange(newValue);
|
||||||
onChange(newValue);
|
}
|
||||||
},
|
|
||||||
[menu, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} className={clsx('h-full text-right', className)} {...restProps}>
|
<div ref={menu.ref} className={clsx('h-full text-right', className)} {...restProps}>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { MatchModeIcon } from '@/components/DomainIcons';
|
import { MatchModeIcon } from '@/components/DomainIcons';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
|
@ -15,21 +13,18 @@ import { describeCstMatchMode, labelCstMatchMode } from '@/utils/labels';
|
||||||
|
|
||||||
interface SelectMatchModeProps extends CProps.Styling {
|
interface SelectMatchModeProps extends CProps.Styling {
|
||||||
value: CstMatchMode;
|
value: CstMatchMode;
|
||||||
dense?: boolean;
|
|
||||||
onChange: (value: CstMatchMode) => void;
|
onChange: (value: CstMatchMode) => void;
|
||||||
|
dense?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectMatchMode({ value, dense, onChange, ...restProps }: SelectMatchModeProps) {
|
function SelectMatchMode({ value, dense, onChange, ...restProps }: SelectMatchModeProps) {
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
const size = useWindowSize();
|
const size = useWindowSize();
|
||||||
|
|
||||||
const handleChange = useCallback(
|
function handleChange(newValue: CstMatchMode) {
|
||||||
(newValue: CstMatchMode) => {
|
menu.hide();
|
||||||
menu.hide();
|
onChange(newValue);
|
||||||
onChange(newValue);
|
}
|
||||||
},
|
|
||||||
[menu, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} {...restProps}>
|
<div ref={menu.ref} {...restProps}>
|
||||||
|
|
|
@ -10,11 +10,11 @@ interface SelectMultiGrammemeProps
|
||||||
extends Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'>,
|
extends Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'>,
|
||||||
CProps.Styling {
|
CProps.Styling {
|
||||||
value: IGrammemeOption[];
|
value: IGrammemeOption[];
|
||||||
onChangeValue: (newValue: IGrammemeOption[]) => void;
|
onChange: (newValue: IGrammemeOption[]) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectMultiGrammeme({ value, onChangeValue, ...restProps }: SelectMultiGrammemeProps) {
|
function SelectMultiGrammeme({ value, onChange, ...restProps }: SelectMultiGrammemeProps) {
|
||||||
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -28,7 +28,7 @@ function SelectMultiGrammeme({ value, onChangeValue, ...restProps }: SelectMulti
|
||||||
<SelectMulti
|
<SelectMulti
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={newValue => onChangeValue([...newValue].sort(compareGrammemeOptions))}
|
onChange={newValue => onChange([...newValue].sort(compareGrammemeOptions))}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { matchOperation } from '@/models/ossAPI';
|
||||||
interface SelectOperationProps extends CProps.Styling {
|
interface SelectOperationProps extends CProps.Styling {
|
||||||
items?: IOperation[];
|
items?: IOperation[];
|
||||||
value?: IOperation;
|
value?: IOperation;
|
||||||
onSelectValue: (newValue?: IOperation) => void;
|
onChange: (newValue?: IOperation) => void;
|
||||||
|
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
|
@ -20,7 +20,7 @@ function SelectOperation({
|
||||||
className,
|
className,
|
||||||
items,
|
items,
|
||||||
value,
|
value,
|
||||||
onSelectValue,
|
onChange,
|
||||||
placeholder = 'Выберите операцию',
|
placeholder = 'Выберите операцию',
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectOperationProps) {
|
}: SelectOperationProps) {
|
||||||
|
@ -40,7 +40,7 @@ function SelectOperation({
|
||||||
className={clsx('text-ellipsis', className)}
|
className={clsx('text-ellipsis', className)}
|
||||||
options={options}
|
options={options}
|
||||||
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null}
|
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null}
|
||||||
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
|
onChange={data => onChange(items?.find(cst => cst.id === data?.value))}
|
||||||
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||||
filterOption={filter}
|
filterOption={filter}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { matchUser } from '@/models/userAPI';
|
||||||
|
|
||||||
interface SelectUserProps extends CProps.Styling {
|
interface SelectUserProps extends CProps.Styling {
|
||||||
value?: UserID;
|
value?: UserID;
|
||||||
onSelectValue: (newValue: UserID) => void;
|
onChange: (newValue: UserID) => void;
|
||||||
filter?: (userID: UserID) => boolean;
|
filter?: (userID: UserID) => boolean;
|
||||||
|
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
@ -22,7 +22,7 @@ function SelectUser({
|
||||||
className,
|
className,
|
||||||
filter,
|
filter,
|
||||||
value,
|
value,
|
||||||
onSelectValue,
|
onChange,
|
||||||
placeholder = 'Выберите пользователя',
|
placeholder = 'Выберите пользователя',
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectUserProps) {
|
}: SelectUserProps) {
|
||||||
|
@ -46,7 +46,7 @@ function SelectUser({
|
||||||
options={options}
|
options={options}
|
||||||
value={value ? { value: value, label: getUserLabel(value) } : null}
|
value={value ? { value: value, label: getUserLabel(value) } : null}
|
||||||
onChange={data => {
|
onChange={data => {
|
||||||
if (data?.value !== undefined) onSelectValue(data.value);
|
if (data?.value !== undefined) onChange(data.value);
|
||||||
}}
|
}}
|
||||||
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||||
filterOption={filterLabel}
|
filterOption={filterLabel}
|
||||||
|
|
|
@ -11,13 +11,13 @@ interface SelectVersionProps extends CProps.Styling {
|
||||||
id?: string;
|
id?: string;
|
||||||
items?: IVersionInfo[];
|
items?: IVersionInfo[];
|
||||||
value?: VersionID;
|
value?: VersionID;
|
||||||
onSelectValue: (newValue?: VersionID) => void;
|
onChange: (newValue?: VersionID) => void;
|
||||||
|
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectVersion({ id, className, items, value, onSelectValue, ...restProps }: SelectVersionProps) {
|
function SelectVersion({ id, className, items, value, onChange, ...restProps }: SelectVersionProps) {
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
value: undefined,
|
value: undefined,
|
||||||
|
@ -40,7 +40,7 @@ function SelectVersion({ id, className, items, value, onSelectValue, ...restProp
|
||||||
className={clsx('min-w-[12rem] text-ellipsis', className)}
|
className={clsx('min-w-[12rem] text-ellipsis', className)}
|
||||||
options={options}
|
options={options}
|
||||||
value={{ value: value, label: valueLabel }}
|
value={{ value: value, label: valueLabel }}
|
||||||
onChange={data => onSelectValue(data?.value)}
|
onChange={data => onChange(data?.value)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import WordformButton from '@/dialogs/DlgEditReference/WordformButton';
|
import WordformButton from '@/dialogs/DlgEditReference/WordformButton';
|
||||||
|
@ -10,17 +9,14 @@ import { prefixes } from '@/utils/constants';
|
||||||
import { DefaultWordForms, IGrammemeOption, SelectorGrammemes } from '@/utils/selectors';
|
import { DefaultWordForms, IGrammemeOption, SelectorGrammemes } from '@/utils/selectors';
|
||||||
|
|
||||||
interface SelectWordFormProps extends CProps.Styling {
|
interface SelectWordFormProps extends CProps.Styling {
|
||||||
selected: IGrammemeOption[];
|
value: IGrammemeOption[];
|
||||||
setSelected: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
|
onChange: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectWordForm({ selected, setSelected, className, ...restProps }: SelectWordFormProps) {
|
function SelectWordForm({ value, onChange, className, ...restProps }: SelectWordFormProps) {
|
||||||
const handleSelect = useCallback(
|
function handleSelect(grams: Grammeme[]) {
|
||||||
(grams: Grammeme[]) => {
|
onChange(SelectorGrammemes.filter(({ value }) => grams.includes(value as Grammeme)));
|
||||||
setSelected(SelectorGrammemes.filter(({ value }) => grams.includes(value as Grammeme)));
|
}
|
||||||
},
|
|
||||||
[setSelected]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('text-xs sm:text-sm', className)} {...restProps}>
|
<div className={clsx('text-xs sm:text-sm', className)} {...restProps}>
|
||||||
|
@ -30,7 +26,7 @@ function SelectWordForm({ selected, setSelected, className, ...restProps }: Sele
|
||||||
text={data.text}
|
text={data.text}
|
||||||
example={data.example}
|
example={data.example}
|
||||||
grams={data.grams}
|
grams={data.grams}
|
||||||
isSelected={data.grams.every(gram => selected.find(item => (item.value as Grammeme) === gram))}
|
isSelected={data.grams.every(gram => value.find(item => (item.value as Grammeme) === gram))}
|
||||||
onSelectGrams={handleSelect}
|
onSelectGrams={handleSelect}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IconGraphCollapse,
|
IconGraphCollapse,
|
||||||
|
@ -17,81 +16,75 @@ import MiniButton from '@/components/ui/MiniButton';
|
||||||
import { Graph } from '@/models/Graph';
|
import { Graph } from '@/models/Graph';
|
||||||
|
|
||||||
interface ToolbarGraphSelectionProps extends CProps.Styling {
|
interface ToolbarGraphSelectionProps extends CProps.Styling {
|
||||||
|
value: number[];
|
||||||
|
onChange: (newSelection: number[]) => void;
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
selected: number[];
|
|
||||||
isCore: (item: number) => boolean;
|
isCore: (item: number) => boolean;
|
||||||
isOwned?: (item: number) => boolean;
|
isOwned?: (item: number) => boolean;
|
||||||
setSelected: (newSelection: number[]) => void;
|
|
||||||
emptySelection?: boolean;
|
emptySelection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarGraphSelection({
|
function ToolbarGraphSelection({
|
||||||
className,
|
className,
|
||||||
graph,
|
graph,
|
||||||
selected,
|
value: selected,
|
||||||
isCore,
|
isCore,
|
||||||
isOwned,
|
isOwned,
|
||||||
setSelected,
|
onChange,
|
||||||
emptySelection,
|
emptySelection,
|
||||||
...restProps
|
...restProps
|
||||||
}: ToolbarGraphSelectionProps) {
|
}: ToolbarGraphSelectionProps) {
|
||||||
const handleSelectCore = useCallback(() => {
|
function handleSelectCore() {
|
||||||
const core = [...graph.nodes.keys()].filter(isCore);
|
const core = [...graph.nodes.keys()].filter(isCore);
|
||||||
setSelected([...core, ...graph.expandInputs(core)]);
|
onChange([...core, ...graph.expandInputs(core)]);
|
||||||
}, [setSelected, graph, isCore]);
|
}
|
||||||
|
|
||||||
const handleSelectOwned = useCallback(
|
function handleSelectOwned() {
|
||||||
() => (isOwned ? setSelected([...graph.nodes.keys()].filter(isOwned)) : undefined),
|
if (isOwned) onChange([...graph.nodes.keys()].filter(isOwned));
|
||||||
[setSelected, graph, isOwned]
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const handleInvertSelection = useCallback(
|
|
||||||
() => setSelected([...graph.nodes.keys()].filter(item => !selected.includes(item))),
|
|
||||||
[setSelected, selected, graph]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('cc-icons', className)} {...restProps}>
|
<div className={clsx('cc-icons', className)} {...restProps}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Сбросить выделение'
|
titleHtml='Сбросить выделение'
|
||||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected([])}
|
onClick={() => onChange([])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить все влияющие'
|
titleHtml='Выделить все влияющие'
|
||||||
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected([...selected, ...graph.expandAllInputs(selected)])}
|
onClick={() => onChange([...selected, ...graph.expandAllInputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить все зависимые'
|
titleHtml='Выделить все зависимые'
|
||||||
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected([...selected, ...graph.expandAllOutputs(selected)])}
|
onClick={() => onChange([...selected, ...graph.expandAllOutputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
|
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
|
||||||
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(graph.maximizePart(selected))}
|
onClick={() => onChange(graph.maximizePart(selected))}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить поставщиков'
|
titleHtml='Выделить поставщиков'
|
||||||
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected([...selected, ...graph.expandInputs(selected)])}
|
onClick={() => onChange([...selected, ...graph.expandInputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить потребителей'
|
titleHtml='Выделить потребителей'
|
||||||
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected([...selected, ...graph.expandOutputs(selected)])}
|
onClick={() => onChange([...selected, ...graph.expandOutputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Инвертировать'
|
titleHtml='Инвертировать'
|
||||||
icon={<IconGraphInverse size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphInverse size='1.25rem' className='icon-primary' />}
|
||||||
onClick={handleInvertSelection}
|
onClick={() => onChange([...graph.nodes.keys()].filter(item => !selected.includes(item)))}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить ядро'
|
titleHtml='Выделить ядро'
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { CheckboxChecked } from '@/components/Icons';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import { globals } from '@/utils/constants';
|
import { globals } from '@/utils/constants';
|
||||||
|
|
||||||
export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick'> {
|
export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick' | 'onChange'> {
|
||||||
/** Label to display next to the checkbox. */
|
/** Label to display next to the checkbox. */
|
||||||
label?: string;
|
label?: string;
|
||||||
|
|
||||||
|
@ -12,10 +12,10 @@ export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick'>
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
/** Current value - `true` or `false`. */
|
/** Current value - `true` or `false`. */
|
||||||
value: boolean;
|
value?: boolean;
|
||||||
|
|
||||||
/** Callback to set the `value`. */
|
/** Callback to set the `value`. */
|
||||||
setValue?: (newValue: boolean) => void;
|
onChange?: (newValue: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,18 +29,18 @@ function Checkbox({
|
||||||
hideTitle,
|
hideTitle,
|
||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
setValue,
|
onChange,
|
||||||
...restProps
|
...restProps
|
||||||
}: CheckboxProps) {
|
}: CheckboxProps) {
|
||||||
const cursor = disabled ? 'cursor-arrow' : setValue ? 'cursor-pointer' : '';
|
const cursor = disabled ? 'cursor-arrow' : onChange ? 'cursor-pointer' : '';
|
||||||
|
|
||||||
function handleClick(event: CProps.EventMouse): void {
|
function handleClick(event: CProps.EventMouse): void {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (disabled || !setValue) {
|
if (disabled || !onChange) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setValue(!value);
|
onChange(!value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -6,12 +6,12 @@ import { globals } from '@/utils/constants';
|
||||||
|
|
||||||
import { CheckboxProps } from './Checkbox';
|
import { CheckboxProps } from './Checkbox';
|
||||||
|
|
||||||
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'setValue'> {
|
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> {
|
||||||
/** Current value - `null`, `true` or `false`. */
|
/** Current value - `null`, `true` or `false`. */
|
||||||
value: boolean | null;
|
value: boolean | null;
|
||||||
|
|
||||||
/** Callback to set the `value`. */
|
/** Callback to set the `value`. */
|
||||||
setValue?: (newValue: boolean | null) => void;
|
onChange?: (newValue: boolean | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,23 +25,23 @@ function CheckboxTristate({
|
||||||
hideTitle,
|
hideTitle,
|
||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
setValue,
|
onChange,
|
||||||
...restProps
|
...restProps
|
||||||
}: CheckboxTristateProps) {
|
}: CheckboxTristateProps) {
|
||||||
const cursor = disabled ? 'cursor-arrow' : setValue ? 'cursor-pointer' : '';
|
const cursor = disabled ? 'cursor-arrow' : onChange ? 'cursor-pointer' : '';
|
||||||
|
|
||||||
function handleClick(event: CProps.EventMouse): void {
|
function handleClick(event: CProps.EventMouse): void {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (disabled || !setValue) {
|
if (disabled || !onChange) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (value === false) {
|
if (value === false) {
|
||||||
setValue(null);
|
onChange(null);
|
||||||
} else if (value === null) {
|
} else if (value === null) {
|
||||||
setValue(true);
|
onChange(true);
|
||||||
} else {
|
} else {
|
||||||
setValue(false);
|
onChange(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ function SelectAll<TData>({ table, resetLastSelected }: SelectAllProps<TData>) {
|
||||||
value={
|
value={
|
||||||
!table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected() ? null : table.getIsAllPageRowsSelected()
|
!table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected() ? null : table.getIsAllPageRowsSelected()
|
||||||
}
|
}
|
||||||
setValue={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ function SelectRow<TData>({ row, onChangeLastSelected }: SelectRowProps<TData>)
|
||||||
row.toggleSelected(value);
|
row.toggleSelected(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Checkbox tabIndex={-1} value={row.getIsSelected()} setValue={handleChange} />;
|
return <Checkbox tabIndex={-1} value={row.getIsSelected()} onChange={handleChange} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectRow;
|
export default SelectRow;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx';
|
||||||
import Checkbox, { CheckboxProps } from './Checkbox';
|
import Checkbox, { CheckboxProps } from './Checkbox';
|
||||||
|
|
||||||
/** Animated {@link Checkbox} inside a {@link Dropdown} item. */
|
/** Animated {@link Checkbox} inside a {@link Dropdown} item. */
|
||||||
function DropdownCheckbox({ setValue, disabled, ...restProps }: CheckboxProps) {
|
function DropdownCheckbox({ onChange: setValue, disabled, ...restProps }: CheckboxProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -13,7 +13,7 @@ function DropdownCheckbox({ setValue, disabled, ...restProps }: CheckboxProps) {
|
||||||
!!setValue && !disabled && 'clr-hover'
|
!!setValue && !disabled && 'clr-hover'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Checkbox tabIndex={-1} disabled={disabled} setValue={setValue} {...restProps} />
|
<Checkbox tabIndex={-1} disabled={disabled} onChange={setValue} {...restProps} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
24
rsconcept/frontend/src/components/ui/ErrorField.tsx
Normal file
24
rsconcept/frontend/src/components/ui/ErrorField.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { FieldError, GlobalError } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { CProps } from '../props';
|
||||||
|
|
||||||
|
interface ErrorFieldProps extends CProps.Styling {
|
||||||
|
error?: FieldError | GlobalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an error message for input field.
|
||||||
|
*/
|
||||||
|
function ErrorField({ error, className, ...restProps }: ErrorFieldProps): React.ReactElement | null {
|
||||||
|
if (!error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={clsx('text-sm text-warn-600 select-none', className)} {...restProps}>
|
||||||
|
{error.message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorField;
|
|
@ -19,7 +19,7 @@ interface SelectTreeProps<ItemType> extends CProps.Styling {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
|
|
||||||
/** Callback to be called when the value changes. */
|
/** Callback to be called when the value changes. */
|
||||||
onChangeValue: (newItem: ItemType) => void;
|
onChange: (newItem: ItemType) => void;
|
||||||
|
|
||||||
/** Callback providing the parent of the item. */
|
/** Callback providing the parent of the item. */
|
||||||
getParent: (item: ItemType) => ItemType;
|
getParent: (item: ItemType) => ItemType;
|
||||||
|
@ -40,7 +40,7 @@ function SelectTree<ItemType>({
|
||||||
getParent,
|
getParent,
|
||||||
getLabel,
|
getLabel,
|
||||||
getDescription,
|
getDescription,
|
||||||
onChangeValue,
|
onChange,
|
||||||
prefix,
|
prefix,
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectTreeProps<ItemType>) {
|
}: SelectTreeProps<ItemType>) {
|
||||||
|
@ -75,7 +75,7 @@ function SelectTree<ItemType>({
|
||||||
function handleSetValue(event: CProps.EventMouse, target: ItemType) {
|
function handleSetValue(event: CProps.EventMouse, target: ItemType) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onChangeValue(target);
|
onChange(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,9 +2,10 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
|
|
||||||
|
import ErrorField from './ErrorField';
|
||||||
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.ErrorProcessing, CProps.Colors, CProps.TextArea {
|
||||||
/** Indicates that padding should be minimal. */
|
/** Indicates that padding should be minimal. */
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ function TextArea({
|
||||||
noResize,
|
noResize,
|
||||||
className,
|
className,
|
||||||
fitContent,
|
fitContent,
|
||||||
|
error,
|
||||||
colors = 'clr-input',
|
colors = 'clr-input',
|
||||||
...restProps
|
...restProps
|
||||||
}: TextAreaProps) {
|
}: TextAreaProps) {
|
||||||
|
@ -37,7 +39,7 @@ function TextArea({
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full',
|
'w-full',
|
||||||
{
|
{
|
||||||
'flex flex-col gap-2': !dense,
|
'flex flex-col': !dense,
|
||||||
'flex flex-grow items-center gap-3': dense
|
'flex flex-grow items-center gap-3': dense
|
||||||
},
|
},
|
||||||
dense && className
|
dense && className
|
||||||
|
@ -55,6 +57,7 @@ function TextArea({
|
||||||
'resize-none': noResize,
|
'resize-none': noResize,
|
||||||
'border': !noBorder,
|
'border': !noBorder,
|
||||||
'flex-grow max-w-full': dense,
|
'flex-grow max-w-full': dense,
|
||||||
|
'mt-2': !dense,
|
||||||
'clr-outline': !noOutline
|
'clr-outline': !noOutline
|
||||||
},
|
},
|
||||||
colors,
|
colors,
|
||||||
|
@ -64,6 +67,7 @@ function TextArea({
|
||||||
required={required}
|
required={required}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
<ErrorField className='mt-1' error={error} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,10 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
|
|
||||||
|
import ErrorField from './ErrorField';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input {
|
interface TextInputProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.Input {
|
||||||
/** Indicates that padding should be minimal. */
|
/** Indicates that padding should be minimal. */
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
|
|
||||||
|
@ -32,13 +33,14 @@ function TextInput({
|
||||||
className,
|
className,
|
||||||
colors = 'clr-input',
|
colors = 'clr-input',
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
error,
|
||||||
...restProps
|
...restProps
|
||||||
}: TextInputProps) {
|
}: TextInputProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
{
|
{
|
||||||
'flex flex-col gap-2': !dense,
|
'flex flex-col': !dense,
|
||||||
'flex items-center gap-3': dense
|
'flex items-center gap-3': dense
|
||||||
},
|
},
|
||||||
dense && className
|
dense && className
|
||||||
|
@ -53,6 +55,7 @@ function TextInput({
|
||||||
{
|
{
|
||||||
'px-3': !noBorder || !disabled,
|
'px-3': !noBorder || !disabled,
|
||||||
'flex-grow max-w-full': dense,
|
'flex-grow max-w-full': dense,
|
||||||
|
'mt-2': !dense,
|
||||||
'border': !noBorder,
|
'border': !noBorder,
|
||||||
'clr-outline': !noOutline
|
'clr-outline': !noOutline
|
||||||
},
|
},
|
||||||
|
@ -63,6 +66,7 @@ function TextInput({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
<ErrorField className='mt-1' error={error} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ function DlgChangeInputSchema() {
|
||||||
items={sortedItems}
|
items={sortedItems}
|
||||||
itemType={LibraryItemType.RSFORM}
|
itemType={LibraryItemType.RSFORM}
|
||||||
value={selected} // prettier: split-line
|
value={selected} // prettier: split-line
|
||||||
onSelectValue={handleSelectLocation}
|
onChange={handleSelectLocation}
|
||||||
rows={14}
|
rows={14}
|
||||||
baseFilter={baseFilter}
|
baseFilter={baseFilter}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -48,7 +48,7 @@ function DlgChangeLocation() {
|
||||||
<SelectLocationHead
|
<SelectLocationHead
|
||||||
value={head} // prettier: split-lines
|
value={head} // prettier: split-lines
|
||||||
onChange={setHead}
|
onChange={setHead}
|
||||||
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
|
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectLocationContext value={location} onChange={handleSelectLocation} className='max-h-[9.2rem]' />
|
<SelectLocationContext value={location} onChange={handleSelectLocation} className='max-h-[9.2rem]' />
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { useState } from 'react';
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { useAuthSuspense } from '@/backend/auth/useAuth';
|
import { useAuthSuspense } from '@/backend/auth/useAuth';
|
||||||
import { IRSFormCloneDTO } from '@/backend/library/api';
|
import { IRCloneLibraryItemDTO } from '@/backend/library/api';
|
||||||
import { useCloneItem } from '@/backend/library/useCloneItem';
|
import { useCloneItem } from '@/backend/library/useCloneItem';
|
||||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||||
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
|
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
|
||||||
|
@ -59,7 +59,7 @@ function DlgCloneLibraryItem() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
const data: IRSFormCloneDTO = {
|
const data: IRCloneLibraryItemDTO = {
|
||||||
id: base.id,
|
id: base.id,
|
||||||
item_type: base.item_type,
|
item_type: base.item_type,
|
||||||
title: title,
|
title: title,
|
||||||
|
@ -119,11 +119,7 @@ function DlgCloneLibraryItem() {
|
||||||
<div className='flex justify-between gap-3'>
|
<div className='flex justify-between gap-3'>
|
||||||
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
||||||
<Label text='Корень' />
|
<Label text='Корень' />
|
||||||
<SelectLocationHead
|
<SelectLocationHead value={head} onChange={setHead} excluded={!user.is_staff ? [LocationHead.LIBRARY] : []} />
|
||||||
value={head}
|
|
||||||
onChange={setHead}
|
|
||||||
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<SelectLocationContext value={location} onChange={handleSelectLocation} />
|
<SelectLocationContext value={location} onChange={handleSelectLocation} />
|
||||||
<TextArea
|
<TextArea
|
||||||
|
@ -141,7 +137,7 @@ function DlgCloneLibraryItem() {
|
||||||
id='dlg_only_selected'
|
id='dlg_only_selected'
|
||||||
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
|
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
|
||||||
value={onlySelected}
|
value={onlySelected}
|
||||||
setValue={value => setOnlySelected(value)}
|
onChange={value => setOnlySelected(value)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -98,7 +98,7 @@ function TabInputOperation({
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={createSchema}
|
value={createSchema}
|
||||||
setValue={onChangeCreateSchema}
|
onChange={onChangeCreateSchema}
|
||||||
label='Создать новую схему'
|
label='Создать новую схему'
|
||||||
titleHtml='Создать пустую схему для загрузки'
|
titleHtml='Создать пустую схему для загрузки'
|
||||||
/>
|
/>
|
||||||
|
@ -108,7 +108,7 @@ function TabInputOperation({
|
||||||
items={sortedItems}
|
items={sortedItems}
|
||||||
value={attachedID}
|
value={attachedID}
|
||||||
itemType={LibraryItemType.RSFORM}
|
itemType={LibraryItemType.RSFORM}
|
||||||
onSelectValue={onChangeAttachedID}
|
onChange={onChangeAttachedID}
|
||||||
rows={8}
|
rows={8}
|
||||||
baseFilter={baseFilter}
|
baseFilter={baseFilter}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -57,7 +57,7 @@ function TabSynthesisOperation({
|
||||||
|
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
<Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
|
<Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
|
||||||
<PickMultiOperation items={oss.items} selected={inputs} setSelected={setInputs} rows={6} />
|
<PickMultiOperation items={oss.items} value={inputs} onChange={setInputs} rows={6} />
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -64,7 +64,7 @@ function DlgCreateVersion() {
|
||||||
id='dlg_only_selected'
|
id='dlg_only_selected'
|
||||||
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
|
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
|
||||||
value={onlySelected}
|
value={onlySelected}
|
||||||
setValue={value => setOnlySelected(value)}
|
onChange={value => setOnlySelected(value)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -190,7 +190,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
|
||||||
id='dlg_argument_picker'
|
id='dlg_argument_picker'
|
||||||
value={selectedCst}
|
value={selectedCst}
|
||||||
data={schema.items}
|
data={schema.items}
|
||||||
onSelectValue={handleSelectConstituenta}
|
onChange={handleSelectConstituenta}
|
||||||
prefixID={prefixes.cst_modal_list}
|
prefixID={prefixes.cst_modal_list}
|
||||||
rows={7}
|
rows={7}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -102,7 +102,7 @@ function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps)
|
||||||
id='dlg_template_picker'
|
id='dlg_template_picker'
|
||||||
value={state.prototype}
|
value={state.prototype}
|
||||||
data={filteredData}
|
data={filteredData}
|
||||||
onSelectValue={cst => partialUpdate({ prototype: cst })}
|
onChange={cst => partialUpdate({ prototype: cst })}
|
||||||
prefixID={prefixes.cst_template_ist}
|
prefixID={prefixes.cst_template_ist}
|
||||||
className='rounded-t-none'
|
className='rounded-t-none'
|
||||||
rows={8}
|
rows={8}
|
||||||
|
|
|
@ -55,7 +55,7 @@ function DlgDeleteCst() {
|
||||||
label='Удалить зависимые конституенты'
|
label='Удалить зависимые конституенты'
|
||||||
className='mb-2'
|
className='mb-2'
|
||||||
value={expandOut}
|
value={expandOut}
|
||||||
setValue={value => setExpandOut(value)}
|
onChange={value => setExpandOut(value)}
|
||||||
/>
|
/>
|
||||||
{hasInherited ? (
|
{hasInherited ? (
|
||||||
<p className='text-sm clr-text-red'>Внимание! Выбранные конституенты имеют наследников в ОСС</p>
|
<p className='text-sm clr-text-red'>Внимание! Выбранные конституенты имеют наследников в ОСС</p>
|
||||||
|
|
|
@ -39,7 +39,7 @@ function DlgDeleteOperation() {
|
||||||
label='Сохранить наследованные конституенты'
|
label='Сохранить наследованные конституенты'
|
||||||
titleHtml='Наследованные конституенты <br/>превратятся в дописанные'
|
titleHtml='Наследованные конституенты <br/>превратятся в дописанные'
|
||||||
value={keepConstituents}
|
value={keepConstituents}
|
||||||
setValue={setKeepConstituents}
|
onChange={setKeepConstituents}
|
||||||
disabled={target.result === null}
|
disabled={target.result === null}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
@ -50,7 +50,7 @@ function DlgDeleteOperation() {
|
||||||
: 'Удалить схему вместе с операцией'
|
: 'Удалить схему вместе с операцией'
|
||||||
}
|
}
|
||||||
value={deleteSchema}
|
value={deleteSchema}
|
||||||
setValue={setDeleteSchema}
|
onChange={setDeleteSchema}
|
||||||
disabled={!target.is_owned || target.result === null}
|
disabled={!target.is_owned || target.result === null}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -63,7 +63,7 @@ function DlgEditEditors() {
|
||||||
<SelectUser
|
<SelectUser
|
||||||
filter={id => !selected.includes(id)}
|
filter={id => !selected.includes(id)}
|
||||||
value={undefined}
|
value={undefined}
|
||||||
onSelectValue={onAddEditor}
|
onChange={onAddEditor}
|
||||||
className='w-[25rem]'
|
className='w-[25rem]'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,7 +19,7 @@ function TabArguments({ oss, inputs, target, setInputs }: TabArgumentsProps) {
|
||||||
<div className='cc-fade-in cc-column'>
|
<div className='cc-fade-in cc-column'>
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
<Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
|
<Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
|
||||||
<PickMultiOperation items={filtered} selected={inputs} setSelected={setInputs} rows={8} />
|
<PickMultiOperation items={filtered} value={inputs} onChange={setInputs} rows={8} />
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -29,8 +29,8 @@ function TabSynthesis({
|
||||||
schemas={schemas}
|
schemas={schemas}
|
||||||
prefixID={prefixes.dlg_cst_substitutes_list}
|
prefixID={prefixes.dlg_cst_substitutes_list}
|
||||||
rows={8}
|
rows={8}
|
||||||
substitutions={substitutions}
|
value={substitutions}
|
||||||
setSubstitutions={setSubstitutions}
|
onChange={setSubstitutions}
|
||||||
suggestions={suggestions}
|
suggestions={suggestions}
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
|
|
@ -64,7 +64,7 @@ function TabEntityReference({ initial, schema, onChangeValid, onChangeReference
|
||||||
initialFilter={initial.text}
|
initialFilter={initial.text}
|
||||||
value={selectedCst}
|
value={selectedCst}
|
||||||
data={schema.items}
|
data={schema.items}
|
||||||
onSelectValue={handleSelectConstituenta}
|
onChange={handleSelectConstituenta}
|
||||||
prefixID={prefixes.cst_modal_list}
|
prefixID={prefixes.cst_modal_list}
|
||||||
describeFunc={cst => cst.term_resolved}
|
describeFunc={cst => cst.term_resolved}
|
||||||
matchFunc={(cst, filter) => matchConstituenta(cst, filter, CstMatchMode.TERM)}
|
matchFunc={(cst, filter) => matchConstituenta(cst, filter, CstMatchMode.TERM)}
|
||||||
|
@ -94,7 +94,7 @@ function TabEntityReference({ initial, schema, onChangeValid, onChangeReference
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelectWordForm selected={selectedGrams} setSelected={setSelectedGrams} />
|
<SelectWordForm value={selectedGrams} onChange={setSelectedGrams} />
|
||||||
|
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<Label text='Словоформа' />
|
<Label text='Словоформа' />
|
||||||
|
@ -104,7 +104,7 @@ function TabEntityReference({ initial, schema, onChangeValid, onChangeReference
|
||||||
className='flex-grow'
|
className='flex-grow'
|
||||||
menuPlacement='top'
|
menuPlacement='top'
|
||||||
value={selectedGrams}
|
value={selectedGrams}
|
||||||
onChangeValue={setSelectedGrams}
|
onChange={setSelectedGrams}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,7 @@ import Label from '@/components/ui/Label';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import { Grammeme, IWordForm, IWordFormPlain } from '@/models/language';
|
import { Grammeme, IWordForm } from '@/models/language';
|
||||||
import { parseGrammemes, wordFormEquals } from '@/models/languageAPI';
|
import { parseGrammemes, wordFormEquals } from '@/models/languageAPI';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { IConstituenta, TermForm } from '@/models/rsform';
|
import { IConstituenta, TermForm } from '@/models/rsform';
|
||||||
|
@ -79,11 +79,13 @@ function DlgEditWordForms() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInflect() {
|
function handleInflect() {
|
||||||
const data: IWordFormPlain = {
|
inflectText(
|
||||||
text: term,
|
{
|
||||||
grams: inputGrams.map(gram => gram.value).join(',')
|
text: term,
|
||||||
};
|
grams: inputGrams.map(gram => gram.value).join(',')
|
||||||
inflectText(data, response => setInputText(response.result));
|
},
|
||||||
|
response => setInputText(response.result)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleParse() {
|
function handleParse() {
|
||||||
|
@ -170,7 +172,7 @@ function DlgEditWordForms() {
|
||||||
placeholder='Выберите граммемы'
|
placeholder='Выберите граммемы'
|
||||||
className='min-w-[15rem] h-fit'
|
className='min-w-[15rem] h-fit'
|
||||||
value={inputGrams}
|
value={inputGrams}
|
||||||
onChangeValue={setInputGrams}
|
onChange={setInputGrams}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -31,31 +31,31 @@ function DlgGraphParams() {
|
||||||
label='Скрыть текст'
|
label='Скрыть текст'
|
||||||
title='Не отображать термины'
|
title='Не отображать термины'
|
||||||
value={params.noText}
|
value={params.noText}
|
||||||
setValue={value => updateParams({ noText: value })}
|
onChange={value => updateParams({ noText: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label='Скрыть несвязанные'
|
label='Скрыть несвязанные'
|
||||||
title='Неиспользуемые конституенты'
|
title='Неиспользуемые конституенты'
|
||||||
value={params.noHermits}
|
value={params.noHermits}
|
||||||
setValue={value => updateParams({ noHermits: value })}
|
onChange={value => updateParams({ noHermits: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label='Скрыть шаблоны'
|
label='Скрыть шаблоны'
|
||||||
titleHtml='Терм-функции и предикат-функции <br/>с параметризованными аргументами'
|
titleHtml='Терм-функции и предикат-функции <br/>с параметризованными аргументами'
|
||||||
value={params.noTemplates}
|
value={params.noTemplates}
|
||||||
setValue={value => updateParams({ noTemplates: value })}
|
onChange={value => updateParams({ noTemplates: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label='Транзитивная редукция'
|
label='Транзитивная редукция'
|
||||||
titleHtml='Удалить связи, образующие <br/>транзитивные пути в графе'
|
titleHtml='Удалить связи, образующие <br/>транзитивные пути в графе'
|
||||||
value={params.noTransitive}
|
value={params.noTransitive}
|
||||||
setValue={value => updateParams({ noTransitive: value })}
|
onChange={value => updateParams({ noTransitive: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label='Свернуть порожденные'
|
label='Свернуть порожденные'
|
||||||
title='Не отображать порожденные понятия'
|
title='Не отображать порожденные понятия'
|
||||||
value={params.foldDerived}
|
value={params.foldDerived}
|
||||||
setValue={value => updateParams({ foldDerived: value })}
|
onChange={value => updateParams({ foldDerived: value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
|
@ -63,42 +63,42 @@ function DlgGraphParams() {
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={labelCstType(CstType.BASE)}
|
label={labelCstType(CstType.BASE)}
|
||||||
value={params.allowBase}
|
value={params.allowBase}
|
||||||
setValue={value => updateParams({ allowBase: value })}
|
onChange={value => updateParams({ allowBase: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={labelCstType(CstType.STRUCTURED)}
|
label={labelCstType(CstType.STRUCTURED)}
|
||||||
value={params.allowStruct}
|
value={params.allowStruct}
|
||||||
setValue={value => updateParams({ allowStruct: value })}
|
onChange={value => updateParams({ allowStruct: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={labelCstType(CstType.TERM)}
|
label={labelCstType(CstType.TERM)}
|
||||||
value={params.allowTerm}
|
value={params.allowTerm}
|
||||||
setValue={value => updateParams({ allowTerm: value })}
|
onChange={value => updateParams({ allowTerm: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={labelCstType(CstType.AXIOM)}
|
label={labelCstType(CstType.AXIOM)}
|
||||||
value={params.allowAxiom}
|
value={params.allowAxiom}
|
||||||
setValue={value => updateParams({ allowAxiom: value })}
|
onChange={value => updateParams({ allowAxiom: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={labelCstType(CstType.FUNCTION)}
|
label={labelCstType(CstType.FUNCTION)}
|
||||||
value={params.allowFunction}
|
value={params.allowFunction}
|
||||||
setValue={value => updateParams({ allowFunction: value })}
|
onChange={value => updateParams({ allowFunction: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={labelCstType(CstType.PREDICATE)}
|
label={labelCstType(CstType.PREDICATE)}
|
||||||
value={params.allowPredicate}
|
value={params.allowPredicate}
|
||||||
setValue={value => updateParams({ allowPredicate: value })}
|
onChange={value => updateParams({ allowPredicate: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={labelCstType(CstType.CONSTANT)}
|
label={labelCstType(CstType.CONSTANT)}
|
||||||
value={params.allowConstant}
|
value={params.allowConstant}
|
||||||
setValue={value => updateParams({ allowConstant: value })}
|
onChange={value => updateParams({ allowConstant: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={labelCstType(CstType.THEOREM)}
|
label={labelCstType(CstType.THEOREM)}
|
||||||
value={params.allowTheorem}
|
value={params.allowTheorem}
|
||||||
setValue={value => updateParams({ allowTheorem: value })}
|
onChange={value => updateParams({ allowTheorem: value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -21,8 +21,8 @@ function TabConstituents({ itemID, selected, setSelected }: TabConstituentsProps
|
||||||
data={schema.items}
|
data={schema.items}
|
||||||
rows={13}
|
rows={13}
|
||||||
prefixID={prefixes.cst_inline_synth_list}
|
prefixID={prefixes.cst_inline_synth_list}
|
||||||
selected={selected}
|
value={selected}
|
||||||
setSelected={setSelected}
|
onChange={setSelected}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ function TabSource({ selected, receiver, setSelected }: TabSourceProps) {
|
||||||
itemType={LibraryItemType.RSFORM}
|
itemType={LibraryItemType.RSFORM}
|
||||||
rows={14}
|
rows={14}
|
||||||
value={selected}
|
value={selected}
|
||||||
onSelectValue={setSelected}
|
onChange={setSelected}
|
||||||
/>
|
/>
|
||||||
<div className='flex items-center gap-6 '>
|
<div className='flex items-center gap-6 '>
|
||||||
<span className='select-none'>Выбрана</span>
|
<span className='select-none'>Выбрана</span>
|
||||||
|
|
|
@ -22,8 +22,8 @@ function TabSubstitutions({ sourceID, receiver, selected, substitutions, setSubs
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PickSubstitutions
|
<PickSubstitutions
|
||||||
substitutions={substitutions}
|
value={substitutions}
|
||||||
setSubstitutions={setSubstitutions}
|
onChange={setSubstitutions}
|
||||||
rows={10}
|
rows={10}
|
||||||
prefixID={prefixes.cst_inline_synth_substitutes}
|
prefixID={prefixes.cst_inline_synth_substitutes}
|
||||||
schemas={schemas}
|
schemas={schemas}
|
||||||
|
|
|
@ -103,7 +103,7 @@ function DlgRelocateConstituents() {
|
||||||
placeholder='Выберите исходную схему'
|
placeholder='Выберите исходную схему'
|
||||||
items={sourceSchemas}
|
items={sourceSchemas}
|
||||||
value={source}
|
value={source}
|
||||||
onSelectValue={handleSelectSource}
|
onChange={handleSelectSource}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Направление перемещения'
|
title='Направление перемещения'
|
||||||
|
@ -116,7 +116,7 @@ function DlgRelocateConstituents() {
|
||||||
placeholder='Выберите целевую схему'
|
placeholder='Выберите целевую схему'
|
||||||
items={destinationSchemas}
|
items={destinationSchemas}
|
||||||
value={destination}
|
value={destination}
|
||||||
onSelectValue={handleSelectDestination}
|
onChange={handleSelectDestination}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{sourceData.isLoading ? <Loader /> : null}
|
{sourceData.isLoading ? <Loader /> : null}
|
||||||
|
@ -127,8 +127,8 @@ function DlgRelocateConstituents() {
|
||||||
data={filteredConstituents}
|
data={filteredConstituents}
|
||||||
rows={12}
|
rows={12}
|
||||||
prefixID={prefixes.dlg_cst_constituents_list}
|
prefixID={prefixes.dlg_cst_constituents_list}
|
||||||
selected={selected}
|
value={selected}
|
||||||
setSelected={setSelected}
|
onChange={setSelected}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -40,8 +40,8 @@ function DlgSubstituteCst() {
|
||||||
>
|
>
|
||||||
<PickSubstitutions
|
<PickSubstitutions
|
||||||
allowSelfSubstitution
|
allowSelfSubstitution
|
||||||
substitutions={substitutions}
|
value={substitutions}
|
||||||
setSubstitutions={setSubstitutions}
|
onChange={setSubstitutions}
|
||||||
rows={6}
|
rows={6}
|
||||||
prefixID={prefixes.dlg_cst_substitutes_list}
|
prefixID={prefixes.dlg_cst_substitutes_list}
|
||||||
schemas={[schema]}
|
schemas={[schema]}
|
||||||
|
|
|
@ -53,7 +53,7 @@ function DlgUploadRSForm() {
|
||||||
label='Загружать название и комментарий'
|
label='Загружать название и комментарий'
|
||||||
className='py-2'
|
className='py-2'
|
||||||
value={loadMetadata}
|
value={loadMetadata}
|
||||||
setValue={value => setLoadMetadata(value)}
|
onChange={value => setLoadMetadata(value)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -214,21 +214,6 @@ export interface IWordForm {
|
||||||
grams: GramData[];
|
grams: GramData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents wordform data used for backend communication.
|
|
||||||
*/
|
|
||||||
export interface IWordFormPlain {
|
|
||||||
text: string;
|
|
||||||
grams: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents lexeme response containing multiple {@link Wordform}s.
|
|
||||||
*/
|
|
||||||
export interface ILexemeData {
|
|
||||||
items: IWordFormPlain[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====== Reference resolution =====
|
// ====== Reference resolution =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -270,11 +255,3 @@ export interface IReference {
|
||||||
type: ReferenceType;
|
type: ReferenceType;
|
||||||
data: IEntityReference | ISyntacticReference;
|
data: IEntityReference | ISyntacticReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents single resolved reference data.
|
|
||||||
*/
|
|
||||||
export interface IResolvedReference extends IReference {
|
|
||||||
pos_input: ITextPosition;
|
|
||||||
pos_output: ITextPosition;
|
|
||||||
}
|
|
||||||
|
|
|
@ -42,11 +42,6 @@ export interface IOperation {
|
||||||
arguments: OperationID[];
|
arguments: OperationID[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents {@link IOperation} data from server.
|
|
||||||
*/
|
|
||||||
export interface IOperationData extends Omit<IOperation, 'substitutions' | 'arguments'> {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents {@link IOperation} position.
|
* Represents {@link IOperation} position.
|
||||||
*/
|
*/
|
||||||
|
@ -115,20 +110,14 @@ export interface IOperationSchemaStats {
|
||||||
count_owned: number;
|
count_owned: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents backend data for {@link IOperationSchema}.
|
|
||||||
*/
|
|
||||||
export interface IOperationSchemaData extends ILibraryItemData {
|
|
||||||
items: IOperationData[];
|
|
||||||
arguments: IArgument[];
|
|
||||||
substitutions: ICstSubstituteEx[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents OperationSchema.
|
* Represents OperationSchema.
|
||||||
*/
|
*/
|
||||||
export interface IOperationSchema extends IOperationSchemaData {
|
export interface IOperationSchema extends ILibraryItemData {
|
||||||
items: IOperation[];
|
items: IOperation[];
|
||||||
|
arguments: IArgument[];
|
||||||
|
substitutions: ICstSubstituteEx[];
|
||||||
|
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
schemas: LibraryItemID[];
|
schemas: LibraryItemID[];
|
||||||
stats: IOperationSchemaStats;
|
stats: IOperationSchemaStats;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* Module: API for OperationSystem.
|
* Module: API for OperationSystem.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ConstituentaID, CstClass, CstType, IConstituenta, IRSForm } from '@/models/rsform';
|
||||||
import { limits } from '@/utils/constants';
|
import { limits } from '@/utils/constants';
|
||||||
import { describeSubstitutionError, information } from '@/utils/labels';
|
import { describeSubstitutionError, information } from '@/utils/labels';
|
||||||
import { TextMatcher } from '@/utils/utils';
|
import { TextMatcher } from '@/utils/utils';
|
||||||
|
@ -9,7 +10,6 @@ import { TextMatcher } from '@/utils/utils';
|
||||||
import { Graph } from './Graph';
|
import { Graph } from './Graph';
|
||||||
import { ILibraryItem, LibraryItemID } from './library';
|
import { ILibraryItem, LibraryItemID } from './library';
|
||||||
import { ICstSubstitute, IOperation, IOperationSchema, OperationID, SubstitutionErrorType } from './oss';
|
import { ICstSubstitute, IOperation, IOperationSchema, OperationID, SubstitutionErrorType } from './oss';
|
||||||
import { ConstituentaID, CstClass, CstType, IConstituenta, IRSForm } from './rsform';
|
|
||||||
import { AliasMapping, ParsingStatus } from './rslang';
|
import { AliasMapping, ParsingStatus } from './rslang';
|
||||||
import { applyAliasMapping, applyTypificationMapping, extractGlobals, isSetTypification } from './rslangAPI';
|
import { applyAliasMapping, applyTypificationMapping, extractGlobals, isSetTypification } from './rslangAPI';
|
||||||
|
|
||||||
|
@ -49,7 +49,6 @@ export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): I
|
||||||
|
|
||||||
type CrossMapping = Map<LibraryItemID, AliasMapping>;
|
type CrossMapping = Map<LibraryItemID, AliasMapping>;
|
||||||
|
|
||||||
// TODO: test validator
|
|
||||||
/**
|
/**
|
||||||
* Validator for Substitution table.
|
* Validator for Substitution table.
|
||||||
*/
|
*/
|
||||||
|
@ -459,7 +458,6 @@ export function getRelocateCandidates(
|
||||||
const original = oss.substitutions.find(sub => sub.substitution === parent)?.original;
|
const original = oss.substitutions.find(sub => sub.substitution === parent)?.original;
|
||||||
if (original) {
|
if (original) {
|
||||||
continue;
|
continue;
|
||||||
// TODO: test if original schema is destination schema
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unreachableBases.push(cst.id);
|
unreachableBases.push(cst.id);
|
||||||
|
|
|
@ -83,9 +83,9 @@ export interface ITargetCst {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents {@link IConstituenta} data from server.
|
* Represents Constituenta.
|
||||||
*/
|
*/
|
||||||
export interface IConstituentaData extends IConstituentaMeta {
|
export interface IConstituenta extends IConstituentaMeta {
|
||||||
parse: {
|
parse: {
|
||||||
status: ParsingStatus;
|
status: ParsingStatus;
|
||||||
valueClass: ValueClass;
|
valueClass: ValueClass;
|
||||||
|
@ -93,12 +93,7 @@ export interface IConstituentaData extends IConstituentaMeta {
|
||||||
syntaxTree: string;
|
syntaxTree: string;
|
||||||
args: IArgumentInfo[];
|
args: IArgumentInfo[];
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents Constituenta.
|
|
||||||
*/
|
|
||||||
export interface IConstituenta extends IConstituentaData {
|
|
||||||
/** {@link LibraryItemID} of this {@link IConstituenta}. */
|
/** {@link LibraryItemID} of this {@link IConstituenta}. */
|
||||||
schema: LibraryItemID;
|
schema: LibraryItemID;
|
||||||
|
|
||||||
|
@ -172,27 +167,21 @@ export interface IRSFormStats {
|
||||||
/**
|
/**
|
||||||
* Represents inheritance data for {@link IRSForm}.
|
* Represents inheritance data for {@link IRSForm}.
|
||||||
*/
|
*/
|
||||||
export interface IInheritanceData {
|
export interface IInheritanceInfo {
|
||||||
child: ConstituentaID;
|
child: ConstituentaID;
|
||||||
child_source: LibraryItemID;
|
child_source: LibraryItemID;
|
||||||
parent: ConstituentaID;
|
parent: ConstituentaID;
|
||||||
parent_source: LibraryItemID;
|
parent_source: LibraryItemID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents data for {@link IRSForm} provided by backend.
|
|
||||||
*/
|
|
||||||
export interface IRSFormData extends ILibraryItemVersioned {
|
|
||||||
items: IConstituentaData[];
|
|
||||||
inheritance: IInheritanceData[];
|
|
||||||
oss: ILibraryItemReference[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents formal explication for set of concepts.
|
* Represents formal explication for set of concepts.
|
||||||
*/
|
*/
|
||||||
export interface IRSForm extends IRSFormData {
|
export interface IRSForm extends ILibraryItemVersioned {
|
||||||
items: IConstituenta[];
|
items: IConstituenta[];
|
||||||
|
inheritance: IInheritanceInfo[];
|
||||||
|
oss: ILibraryItemReference[];
|
||||||
|
|
||||||
stats: IRSFormStats;
|
stats: IRSFormStats;
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
cstByAlias: Map<string, IConstituenta>;
|
cstByAlias: Map<string, IConstituenta>;
|
||||||
|
|
|
@ -32,14 +32,6 @@ export interface ICurrentUser {
|
||||||
editor: LibraryItemID[];
|
editor: LibraryItemID[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents signup data, used to create new users.
|
|
||||||
*/
|
|
||||||
export interface IUserSignupData extends Omit<IUser, 'is_staff' | 'id'> {
|
|
||||||
password: string;
|
|
||||||
password2: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents user profile for viewing and editing {@link IUser}.
|
* Represents user profile for viewing and editing {@link IUser}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { useAuthSuspense } from '@/backend/auth/useAuth';
|
import { useAuthSuspense } from '@/backend/auth/useAuth';
|
||||||
import { ILibraryCreateDTO } from '@/backend/library/api';
|
import { CreateLibraryItemSchema, ICreateLibraryItemDTO } from '@/backend/library/api';
|
||||||
import { useCreateItem } from '@/backend/library/useCreateItem';
|
import { useCreateItem } from '@/backend/library/useCreateItem';
|
||||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||||
import { IconDownload } from '@/components/Icons';
|
import { IconDownload } from '@/components/Icons';
|
||||||
|
@ -23,38 +25,43 @@ import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||||
import { combineLocation, validateLocation } from '@/models/libraryAPI';
|
import { combineLocation } from '@/models/libraryAPI';
|
||||||
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
||||||
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
||||||
|
|
||||||
function FormCreateItem() {
|
function FormCreateItem() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user } = useAuthSuspense();
|
const { user } = useAuthSuspense();
|
||||||
const { createItem, isPending, error, reset } = useCreateItem();
|
const { createItem, isPending, error: serverError, reset: clearServerError } = useCreateItem();
|
||||||
|
|
||||||
const searchLocation = useLibrarySearchStore(state => state.location);
|
const searchLocation = useLibrarySearchStore(state => state.location);
|
||||||
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
|
|
||||||
const [itemType, setItemType] = useState(LibraryItemType.RSFORM);
|
const {
|
||||||
const [title, setTitle] = useState('');
|
register,
|
||||||
const [alias, setAlias] = useState('');
|
handleSubmit,
|
||||||
const [comment, setComment] = useState('');
|
clearErrors,
|
||||||
const [visible, setVisible] = useState(true);
|
setValue,
|
||||||
const [policy, setPolicy] = useState(AccessPolicy.PUBLIC);
|
control,
|
||||||
|
formState: { errors }
|
||||||
const [head, setHead] = useState(LocationHead.USER);
|
} = useForm<ICreateLibraryItemDTO>({
|
||||||
const [body, setBody] = useState('');
|
resolver: zodResolver(CreateLibraryItemSchema),
|
||||||
|
defaultValues: {
|
||||||
const location = combineLocation(head, body);
|
item_type: LibraryItemType.RSFORM,
|
||||||
const isValid = validateLocation(location);
|
access_policy: AccessPolicy.PUBLIC,
|
||||||
|
visible: true,
|
||||||
const [fileName, setFileName] = useState('');
|
read_only: false,
|
||||||
const [file, setFile] = useState<File | undefined>();
|
location: !!searchLocation ? searchLocation : LocationHead.USER
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const itemType = useWatch({ control, name: 'item_type' });
|
||||||
|
const file = useWatch({ control, name: 'file' });
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
function resetErrors() {
|
||||||
reset();
|
clearServerError();
|
||||||
}, [title, alias, reset]);
|
clearErrors();
|
||||||
|
}
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
if (router.canBack()) {
|
if (router.canBack()) {
|
||||||
|
@ -64,26 +71,28 @@ function FormCreateItem() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
event.preventDefault();
|
if (event.target.files && event.target.files.length > 0) {
|
||||||
if (isPending) {
|
setValue('file', event.target.files[0]);
|
||||||
return;
|
setValue('fileName', event.target.files[0].name);
|
||||||
|
} else {
|
||||||
|
setValue('file', undefined);
|
||||||
|
setValue('fileName', '');
|
||||||
}
|
}
|
||||||
const data: ILibraryCreateDTO = {
|
}
|
||||||
item_type: itemType,
|
|
||||||
title: title,
|
function handleItemTypeChange(value: LibraryItemType) {
|
||||||
alias: alias,
|
if (value !== LibraryItemType.RSFORM) {
|
||||||
comment: comment,
|
setValue('file', undefined);
|
||||||
read_only: false,
|
setValue('fileName', '');
|
||||||
visible: visible,
|
}
|
||||||
access_policy: policy,
|
setValue('item_type', value);
|
||||||
location: location,
|
}
|
||||||
file: file,
|
|
||||||
fileName: file?.name
|
function onSubmit(data: ICreateLibraryItemDTO) {
|
||||||
};
|
|
||||||
setSearchLocation(location);
|
|
||||||
createItem(data, newItem => {
|
createItem(data, newItem => {
|
||||||
if (itemType == LibraryItemType.RSFORM) {
|
setSearchLocation(data.location);
|
||||||
|
if (newItem.item_type == LibraryItemType.RSFORM) {
|
||||||
router.push(urls.schema(newItem.id));
|
router.push(urls.schema(newItem.id));
|
||||||
} else {
|
} else {
|
||||||
router.push(urls.oss(newItem.id));
|
router.push(urls.oss(newItem.id));
|
||||||
|
@ -91,50 +100,28 @@ function FormCreateItem() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
if (event.target.files && event.target.files.length > 0) {
|
|
||||||
setFileName(event.target.files[0].name);
|
|
||||||
setFile(event.target.files[0]);
|
|
||||||
} else {
|
|
||||||
setFileName('');
|
|
||||||
setFile(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectLocation = useCallback((newValue: string) => {
|
|
||||||
setHead(newValue.substring(0, 2) as LocationHead);
|
|
||||||
setBody(newValue.length > 3 ? newValue.substring(3) : '');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!searchLocation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleSelectLocation(searchLocation);
|
|
||||||
}, [searchLocation, handleSelectLocation]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (itemType !== LibraryItemType.RSFORM) {
|
|
||||||
setFile(undefined);
|
|
||||||
setFileName('');
|
|
||||||
}
|
|
||||||
}, [itemType]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className={clsx('cc-fade-in cc-column', 'min-w-[30rem] max-w-[30rem] mx-auto', 'px-6 py-3')}
|
className={clsx('cc-fade-in cc-column', 'min-w-[30rem] max-w-[30rem] mx-auto', 'px-6 py-3')}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
|
onChange={resetErrors}
|
||||||
>
|
>
|
||||||
<h1 className='select-none'>
|
<h1 className='select-none'>
|
||||||
{itemType == LibraryItemType.RSFORM ? (
|
{itemType == LibraryItemType.RSFORM ? (
|
||||||
<Overlay position='top-0 right-[0.5rem]'>
|
<Overlay position='top-0 right-[0.5rem]'>
|
||||||
<input
|
<Controller
|
||||||
id='schema_file'
|
control={control}
|
||||||
ref={inputRef}
|
name='file'
|
||||||
type='file'
|
render={() => (
|
||||||
style={{ display: 'none' }}
|
<input
|
||||||
accept={EXTEOR_TRS_FILE}
|
id='schema_file'
|
||||||
onChange={handleFileChange}
|
ref={inputRef}
|
||||||
|
type='file'
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
accept={EXTEOR_TRS_FILE}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Загрузить из Экстеор'
|
title='Загрузить из Экстеор'
|
||||||
|
@ -146,40 +133,62 @@ function FormCreateItem() {
|
||||||
Создание схемы
|
Создание схемы
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{fileName ? <Label className='text-wrap' text={`Загружен файл: ${fileName}`} /> : null}
|
{file ? <Label className='text-wrap' text={`Загружен файл: ${file.name}`} /> : null}
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
id='schema_title'
|
id='schema_title'
|
||||||
required={!file}
|
{...register('title')}
|
||||||
label='Полное название'
|
label='Полное название'
|
||||||
placeholder={file && 'Загрузить из файла'}
|
placeholder={file && 'Загрузить из файла'}
|
||||||
value={title}
|
error={errors.title}
|
||||||
onChange={event => setTitle(event.target.value)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex justify-between gap-3'>
|
<div className='flex justify-between gap-3'>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='schema_alias'
|
id='schema_alias'
|
||||||
required={!file}
|
{...register('alias')}
|
||||||
label='Сокращение'
|
label='Сокращение'
|
||||||
placeholder={file && 'Загрузить из файла'}
|
placeholder={file && 'Загрузить из файла'}
|
||||||
className='w-[16rem]'
|
className='w-[16rem]'
|
||||||
value={alias}
|
error={errors.alias}
|
||||||
onChange={event => setAlias(event.target.value)}
|
|
||||||
/>
|
/>
|
||||||
<div className='flex flex-col items-center gap-2'>
|
<div className='flex flex-col items-center gap-2'>
|
||||||
<Label text='Тип схемы' className='self-center select-none' />
|
<Label text='Тип схемы' className='self-center select-none' />
|
||||||
<SelectItemType value={itemType} onChange={setItemType} />
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name='item_type'
|
||||||
|
render={({ field }) => (
|
||||||
|
<SelectItemType
|
||||||
|
value={field.value} //
|
||||||
|
onChange={handleItemTypeChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
<Label text='Доступ' className='self-center select-none' />
|
<Label text='Доступ' className='self-center select-none' />
|
||||||
<div className='ml-auto cc-icons'>
|
<div className='ml-auto cc-icons'>
|
||||||
<SelectAccessPolicy value={policy} onChange={setPolicy} />
|
<Controller
|
||||||
<MiniButton
|
control={control} //
|
||||||
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
name='access_policy'
|
||||||
icon={<VisibilityIcon value={visible} />}
|
render={({ field }) => (
|
||||||
onClick={() => setVisible(prev => !prev)}
|
<SelectAccessPolicy
|
||||||
|
value={field.value} //
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name='visible'
|
||||||
|
render={({ field }) => (
|
||||||
|
<MiniButton
|
||||||
|
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||||
|
icon={<VisibilityIcon value={field.value} />}
|
||||||
|
onClick={() => field.onChange(!field.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -187,36 +196,58 @@ function FormCreateItem() {
|
||||||
|
|
||||||
<TextArea
|
<TextArea
|
||||||
id='schema_comment'
|
id='schema_comment'
|
||||||
|
{...register('comment')}
|
||||||
label='Описание'
|
label='Описание'
|
||||||
placeholder={file && 'Загрузить из файла'}
|
placeholder={file && 'Загрузить из файла'}
|
||||||
value={comment}
|
error={errors.comment}
|
||||||
onChange={event => setComment(event.target.value)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex justify-between gap-3 flex-grow'>
|
<div className='flex justify-between gap-3 flex-grow'>
|
||||||
<div className='flex flex-col gap-2 min-w-[7rem] h-min'>
|
<div className='flex flex-col gap-2 min-w-[7rem] h-min'>
|
||||||
<Label text='Корень' />
|
<Label text='Корень' />
|
||||||
<SelectLocationHead
|
<Controller
|
||||||
value={head}
|
control={control} //
|
||||||
onChange={setHead}
|
name='location'
|
||||||
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
|
render={({ field }) => (
|
||||||
|
<SelectLocationHead
|
||||||
|
value={field.value.substring(0, 2) as LocationHead}
|
||||||
|
onChange={newValue => field.onChange(combineLocation(newValue, field.value.substring(3)))}
|
||||||
|
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectLocationContext value={location} onChange={handleSelectLocation} />
|
<Controller
|
||||||
<TextArea
|
control={control} //
|
||||||
id='dlg_cst_body'
|
name='location'
|
||||||
label='Путь'
|
render={({ field }) => (
|
||||||
rows={4}
|
<SelectLocationContext
|
||||||
value={body}
|
value={field.value} //
|
||||||
onChange={event => setBody(event.target.value)}
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control} //
|
||||||
|
name='location'
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextArea
|
||||||
|
id='dlg_cst_body'
|
||||||
|
label='Путь'
|
||||||
|
rows={4}
|
||||||
|
value={field.value.substring(3)}
|
||||||
|
onChange={event => field.onChange(combineLocation(field.value.substring(0, 2), event.target.value))}
|
||||||
|
error={errors.location}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-around gap-6 py-3'>
|
<div className='flex justify-around gap-6 py-3'>
|
||||||
<SubmitButton text='Создать схему' loading={isPending} className='min-w-[10rem]' disabled={!isValid} />
|
<SubmitButton text='Создать схему' loading={isPending} className='min-w-[10rem]' />
|
||||||
<Button text='Отмена' className='min-w-[10rem]' onClick={() => handleCancel()} />
|
<Button text='Отмена' className='min-w-[10rem]' onClick={() => handleCancel()} />
|
||||||
</div>
|
</div>
|
||||||
{error ? <InfoError error={error} /> : null}
|
{serverError ? <InfoError error={serverError} /> : null}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@ function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
placeholder='Выберите владельца'
|
placeholder='Выберите владельца'
|
||||||
className='min-w-[15rem] text-sm mx-1 mb-1'
|
className='min-w-[15rem] text-sm mx-1 mb-1'
|
||||||
value={filterUser}
|
value={filterUser}
|
||||||
onSelectValue={setFilterUser}
|
onChange={setFilterUser}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,7 +34,7 @@ function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocationProps
|
||||||
const toggleSubfolders = useLibrarySearchStore(state => state.toggleSubfolders);
|
const toggleSubfolders = useLibrarySearchStore(state => state.toggleSubfolders);
|
||||||
|
|
||||||
const canRename = (() => {
|
const canRename = (() => {
|
||||||
if (location.length <= 3 || isAnonymous || !user) {
|
if (location.length <= 3 || isAnonymous) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (user.is_staff) {
|
if (user.is_staff) {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { IUserLoginDTO, UserLoginSchema } from '@/backend/auth/api';
|
||||||
import { useAuthSuspense } from '@/backend/auth/useAuth';
|
import { useAuthSuspense } from '@/backend/auth/useAuth';
|
||||||
import { useLogin } from '@/backend/auth/useLogin';
|
import { useLogin } from '@/backend/auth/useLogin';
|
||||||
import ExpectedAnonymous from '@/components/ExpectedAnonymous';
|
import ExpectedAnonymous from '@/components/ExpectedAnonymous';
|
||||||
|
@ -19,70 +21,75 @@ import { resources } from '@/utils/constants';
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
const userQuery = query.get('username');
|
const initialName = query.get('username') ?? '';
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
clearErrors,
|
||||||
|
resetField,
|
||||||
|
formState: { errors }
|
||||||
|
} = useForm({
|
||||||
|
resolver: zodResolver(UserLoginSchema),
|
||||||
|
defaultValues: { username: initialName, password: '' }
|
||||||
|
});
|
||||||
|
|
||||||
const { isAnonymous } = useAuthSuspense();
|
const { isAnonymous } = useAuthSuspense();
|
||||||
const { login, isPending, error, reset } = useLogin();
|
const { login, isPending, error: serverError, reset: clearServerError } = useLogin();
|
||||||
|
|
||||||
const [username, setUsername] = useState(userQuery || '');
|
function onSubmit(data: IUserLoginDTO) {
|
||||||
const [password, setPassword] = useState('');
|
login(data, () => {
|
||||||
|
resetField('password');
|
||||||
|
if (router.canBack()) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push(urls.library);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
function resetErrors() {
|
||||||
reset();
|
clearServerError();
|
||||||
}, [username, password, reset]);
|
clearErrors();
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!isPending) {
|
|
||||||
login(username, password, () => {
|
|
||||||
if (router.canBack()) {
|
|
||||||
router.back();
|
|
||||||
} else {
|
|
||||||
router.push(urls.library);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAnonymous) {
|
if (!isAnonymous) {
|
||||||
return <ExpectedAnonymous />;
|
return <ExpectedAnonymous />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<form className={clsx('cc-column cc-fade-in', 'w-[24rem] mx-auto', 'pt-12 pb-6 px-6')} onSubmit={handleSubmit}>
|
<form
|
||||||
|
className={clsx('cc-column cc-fade-in', 'w-[24rem] mx-auto', 'pt-12 pb-6 px-6')}
|
||||||
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
|
onChange={resetErrors}
|
||||||
|
>
|
||||||
<img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' />
|
<img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' />
|
||||||
<TextInput
|
<TextInput
|
||||||
id='username'
|
id='username'
|
||||||
label='Логин или email'
|
|
||||||
autoComplete='username'
|
autoComplete='username'
|
||||||
|
label='Логин или email'
|
||||||
|
{...register('username')}
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
|
||||||
allowEnter
|
allowEnter
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
value={username}
|
defaultValue={initialName}
|
||||||
onChange={event => setUsername(event.target.value)}
|
error={errors.username}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='password'
|
id='password'
|
||||||
|
{...register('password')}
|
||||||
type='password'
|
type='password'
|
||||||
label='Пароль'
|
|
||||||
autoComplete='current-password'
|
autoComplete='current-password'
|
||||||
required
|
label='Пароль'
|
||||||
allowEnter
|
allowEnter
|
||||||
value={password}
|
error={errors.password}
|
||||||
onChange={event => setPassword(event.target.value)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SubmitButton
|
<SubmitButton text='Войти' className='self-center w-[12rem] mt-3' loading={isPending} />
|
||||||
text='Войти'
|
|
||||||
className='self-center w-[12rem] mt-3'
|
|
||||||
loading={isPending}
|
|
||||||
disabled={!username || !password}
|
|
||||||
/>
|
|
||||||
<div className='flex flex-col text-sm'>
|
<div className='flex flex-col text-sm'>
|
||||||
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
||||||
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
|
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
|
||||||
</div>
|
</div>
|
||||||
{error ? <ProcessError error={error} /> : null}
|
{serverError ? <ServerError error={serverError} /> : null}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -90,7 +97,7 @@ function LoginPage() {
|
||||||
export default LoginPage;
|
export default LoginPage;
|
||||||
|
|
||||||
// ====== Internals =========
|
// ====== Internals =========
|
||||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
function ServerError({ error }: { error: ErrorData }): React.ReactElement | null {
|
||||||
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
|
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
|
||||||
return (
|
return (
|
||||||
<div className='text-sm select-text text-warn-600'>
|
<div className='text-sm select-text text-warn-600'>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user