Compare commits

..

No commits in common. "7792a82bf7216cbec214200c7c3d683b8011e508" and "b90312868c7067bcd2b76faca2bec4448d22b345" have entirely different histories.

125 changed files with 974 additions and 1126 deletions

View File

@ -39,15 +39,12 @@ 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

View File

@ -103,7 +103,6 @@ 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)

View File

@ -101,10 +101,6 @@ 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()

View File

@ -9,7 +9,6 @@
"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",
@ -24,7 +23,6 @@
"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",
@ -35,7 +33,6 @@
"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": {
@ -1701,15 +1698,6 @@
"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",
@ -9113,22 +9101,6 @@
"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",
@ -10960,6 +10932,7 @@
"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"

View File

@ -13,7 +13,6 @@
}, },
"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",
@ -28,7 +27,6 @@
"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",
@ -39,7 +37,6 @@
"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": {

View File

@ -1,4 +1,5 @@
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';
@ -9,10 +10,22 @@ 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();
@ -22,6 +35,7 @@ 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
@ -55,6 +69,7 @@ function ApplicationLayout() {
</div> </div>
</div> </div>
</NavigationState> </NavigationState>
</ErrorBoundary>
); );
} }

View File

@ -1,19 +1,13 @@
import { useNavigate, useRouteError } from 'react-router'; import { type FallbackProps } from 'react-error-boundary';
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() { function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
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>
); );

View File

@ -13,14 +13,13 @@ 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: <ErrorFallback />, errorElement: <NotFoundPage />,
loader: prefetchAuth, loader: prefetchAuth,
hydrateFallbackElement: <Loader />, hydrateFallbackElement: <Loader />,
children: [ children: [

View File

@ -24,6 +24,18 @@ 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.
*/ */
@ -46,24 +58,12 @@ 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}`;
} }
}; };

View File

@ -53,11 +53,8 @@ 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) => {
if (error.name !== 'CanceledError') {
// Note: Ignore cancellation errors
toast.error(extractErrorMessage(error)); toast.error(extractErrorMessage(error));
console.error(error); console.error(error);
}
throw error; throw error;
}); });
} }

View File

@ -1,46 +1,25 @@
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, 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 { errors, information } from '@/utils/labels'; import { information } from '@/utils/labels';
/** /**
* Represents login data, used to authenticate users. * Represents login data, used to authenticate users.
*/ */
export const UserLoginSchema = z.object({ export interface IUserLoginDTO {
username: z.string().nonempty(errors.requiredField), username: string;
password: z.string().nonempty(errors.requiredField) password: string;
}); }
/**
* 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 const ChangePasswordSchema = z export interface IChangePasswordDTO {
.object({ old_password: string;
old_password: z.string().nonempty(errors.requiredField), new_password: string;
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.
@ -90,7 +69,7 @@ export const authApi = {
request: { data: data } request: { data: data }
}), }),
changePassword: (data: IChangePasswordDTO) => changePassword: (data: IChangePasswordDTO) =>
axiosPatch({ axiosPost({
endpoint: '/users/api/change-password', endpoint: '/users/api/change-password',
request: { request: {
data: data, data: data,

View File

@ -1,6 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authApi, IUserLoginDTO } from './api'; import { libraryApi } from '@/backend/library/api';
import { authApi } from './api';
export const useLogin = () => { export const useLogin = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -8,10 +10,14 @@ 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.resetQueries() onSuccess: () => client.removeQueries({ queryKey: [libraryApi.baseKey] })
}); });
return { return {
login: (data: IUserLoginDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }), login: (
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

View File

@ -7,7 +7,8 @@ export const useLogout = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: ['logout'], mutationKey: ['logout'],
mutationFn: authApi.logout, mutationFn: authApi.logout,
onSuccess: () => client.resetQueries() onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] }),
onSuccess: () => client.removeQueries()
}); });
return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) }; return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) };
}; };

View File

@ -21,7 +21,6 @@ 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
}; };
}; };

View File

@ -1,4 +1,5 @@
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.
@ -7,26 +8,11 @@ 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: IWordFormDTO) => inflectText: (data: IWordFormPlain) =>
axiosPost<IWordFormDTO, ITextResult>({ axiosPost<IWordFormPlain, ITextResult>({
endpoint: '/api/cctext/inflect', endpoint: '/api/cctext/inflect',
request: { data: data } request: { data: data }
}), }),
@ -36,7 +22,7 @@ export const cctextApi = {
request: { data: data } request: { data: data }
}), }),
generateLexeme: (data: { text: string }) => generateLexeme: (data: { text: string }) =>
axiosPost<{ text: string }, ILexemeResponse>({ axiosPost<{ text: string }, ILexemeData>({
endpoint: '/api/cctext/generate-lexeme', endpoint: '/api/cctext/generate-lexeme',
request: { data: data } request: { data: data }
}) })

View File

@ -1,8 +1,9 @@
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, ILexemeResponse } from './api'; import { cctextApi } from './api';
export const useGenerateLexeme = () => { export const useGenerateLexeme = () => {
const mutation = useMutation({ const mutation = useMutation({
@ -12,7 +13,7 @@ export const useGenerateLexeme = () => {
return { return {
generateLexeme: ( generateLexeme: (
data: { text: string }, // data: { text: string }, //
onSuccess?: DataCallback<ILexemeResponse> onSuccess?: DataCallback<ILexemeData>
) => mutation.mutate(data, { onSuccess }) ) => mutation.mutate(data, { onSuccess })
}; };
}; };

View File

@ -1,8 +1,9 @@
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, IWordFormDTO } from './api'; import { cctextApi, ITextResult } from './api';
export const useInflectText = () => { export const useInflectText = () => {
const mutation = useMutation({ const mutation = useMutation({
@ -11,7 +12,7 @@ export const useInflectText = () => {
}); });
return { return {
inflectText: ( inflectText: (
data: IWordFormDTO, // data: IWordFormPlain, //
onSuccess?: DataCallback<ITextResult> onSuccess?: DataCallback<ITextResult>
) => mutation.mutate(data, { onSuccess }) ) => mutation.mutate(data, { onSuccess })
}; };

View File

@ -1,10 +1,9 @@
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 { IRSFormDTO, rsformsApi } from '@/backend/rsform/api'; import { rsformsApi } from '@/backend/rsform/api';
import { import {
AccessPolicy, AccessPolicy,
ILibraryItem, ILibraryItem,
@ -14,10 +13,9 @@ import {
LibraryItemType, LibraryItemType,
VersionID VersionID
} from '@/models/library'; } from '@/models/library';
import { validateLocation } from '@/models/libraryAPI'; import { ConstituentaID, IRSFormData } from '@/models/rsform';
import { ConstituentaID } from '@/models/rsform';
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { errors, information } from '@/utils/labels'; import { information } from '@/utils/labels';
/** /**
* Represents update data for renaming Location. * Represents update data for renaming Location.
@ -30,49 +28,22 @@ export interface IRenameLocationDTO {
/** /**
* Represents data, used for cloning {@link IRSForm}. * Represents data, used for cloning {@link IRSForm}.
*/ */
export interface IRCloneLibraryItemDTO extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'owner'> { export interface IRSFormCloneDTO 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 const CreateLibraryItemSchema = z export interface ILibraryCreateDTO extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'id' | 'owner'> {
.object({ file?: File;
item_type: z.nativeEnum(LibraryItemType), fileName?: string;
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 IUpdateLibraryItemDTO export interface ILibraryUpdateDTO
extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'access_policy' | 'location' | 'owner'> {} extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'access_policy' | 'location' | 'owner'> {}
/** /**
@ -89,7 +60,7 @@ export interface IVersionCreateDTO {
*/ */
export interface IVersionCreatedResponse { export interface IVersionCreatedResponse {
version: number; version: number;
schema: IRSFormDTO; schema: IRSFormData;
} }
export const libraryApi = { export const libraryApi = {
@ -122,8 +93,8 @@ export const libraryApi = {
}) })
}), }),
createItem: (data: ICreateLibraryItemDTO) => createItem: (data: ILibraryCreateDTO) =>
axiosPost<ICreateLibraryItemDTO, ILibraryItem>({ axiosPost<ILibraryCreateDTO, 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,
@ -137,8 +108,8 @@ export const libraryApi = {
} }
} }
}), }),
updateItem: (data: IUpdateLibraryItemDTO) => updateItem: (data: ILibraryUpdateDTO) =>
axiosPatch<IUpdateLibraryItemDTO, ILibraryItem>({ axiosPatch<ILibraryUpdateDTO, ILibraryItem>({
endpoint: `/api/library/${data.id}`, endpoint: `/api/library/${data.id}`,
request: { request: {
data: data, data: data,
@ -185,8 +156,8 @@ export const libraryApi = {
successMessage: information.itemDestroyed successMessage: information.itemDestroyed
} }
}), }),
cloneItem: (data: IRCloneLibraryItemDTO) => cloneItem: (data: IRSFormCloneDTO) =>
axiosPost<IRCloneLibraryItemDTO, IRSFormDTO>({ axiosPost<IRSFormCloneDTO, IRSFormData>({
endpoint: `/api/library/${data.id}/clone`, endpoint: `/api/library/${data.id}/clone`,
request: { request: {
data: data, data: data,
@ -211,7 +182,7 @@ export const libraryApi = {
} }
}), }),
versionRestore: ({ versionID }: { versionID: VersionID }) => versionRestore: ({ versionID }: { versionID: VersionID }) =>
axiosPatch<undefined, IRSFormDTO>({ axiosPatch<undefined, IRSFormData>({
endpoint: `/api/versions/${versionID}/restore`, endpoint: `/api/versions/${versionID}/restore`,
request: { request: {
successMessage: information.versionRestored successMessage: information.versionRestored

View File

@ -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 { IRSFormDTO } from '../rsform/api'; import { IRSFormCloneDTO, libraryApi } from './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: IRCloneLibraryItemDTO, // data: IRSFormCloneDTO, //
onSuccess?: DataCallback<IRSFormDTO> onSuccess?: DataCallback<IRSFormData>
) => mutation.mutate(data, { onSuccess }) ) => mutation.mutate(data, { onSuccess })
}; };
}; };

View File

@ -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 { ICreateLibraryItemDTO, libraryApi } from './api'; import { ILibraryCreateDTO, 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: ICreateLibraryItemDTO, // data: ILibraryCreateDTO, //
onSuccess?: DataCallback<ILibraryItem> onSuccess?: DataCallback<ILibraryItem>
) => mutation.mutate(data, { onSuccess }), ) => mutation.mutate(data, { onSuccess }),
isPending: mutation.isPending, isPending: mutation.isPending,

View File

@ -2,8 +2,7 @@ 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 { LibraryItemID } from '@/models/library'; import { ILibraryItem, LibraryItemID } from '@/models/library';
import { PARAMETER } from '@/utils/constants';
import { libraryApi } from './api'; import { libraryApi } from './api';
@ -13,16 +12,13 @@ export const useDeleteItem = () => {
mutationKey: [libraryApi.baseKey, 'delete-item'], mutationKey: [libraryApi.baseKey, 'delete-item'],
mutationFn: libraryApi.deleteItem, mutationFn: libraryApi.deleteItem,
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }).catch(console.error); client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
setTimeout( prev?.filter(item => item.id !== variables)
() =>
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 {

View File

@ -1,8 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO, ossApi } from '@/backend/oss/api'; import { 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';
@ -13,7 +14,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: IOperationSchemaDTO | undefined = client.getQueryData(ossKey); const ossData: IOperationSchemaData | 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([

View File

@ -1,8 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO, 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 { ILibraryItem, LibraryItemID } from '@/models/library';
import { IOperationSchemaData } from '@/models/oss';
import { libraryApi } from './api'; import { libraryApi } from './api';
@ -13,7 +14,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: IOperationSchemaDTO | undefined = client.getQueryData(ossKey); const ossData: IOperationSchemaData | 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([

View File

@ -1,8 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO, 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 { 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';
@ -14,7 +15,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: IOperationSchemaDTO | undefined = client.getQueryData(ossKey); const ossData: IOperationSchemaData | 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([

View File

@ -1,10 +1,11 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO, ossApi } from '@/backend/oss/api'; import { 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 { IRSFormDTO } from '../rsform/api'; import { ILibraryUpdateDTO, libraryApi } from './api';
import { IUpdateLibraryItemDTO, libraryApi } from './api';
export const useUpdateItem = () => { export const useUpdateItem = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -16,11 +17,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: IRSFormDTO | IOperationSchemaDTO | undefined) => client.setQueryData(itemKey, (prev: IRSFormData | IOperationSchemaData | undefined) =>
!prev ? undefined : { ...prev, ...data } !prev ? undefined : { ...prev, ...data }
); );
if (data.item_type === LibraryItemType.RSFORM) { if (data.item_type === LibraryItemType.RSFORM) {
const schema: IRSFormDTO | undefined = client.getQueryData(itemKey); const schema: IRSFormData | undefined = client.getQueryData(itemKey);
if (schema) { if (schema) {
return Promise.allSettled( return Promise.allSettled(
schema.oss.map(item => schema.oss.map(item =>
@ -32,6 +33,6 @@ export const useUpdateItem = () => {
} }
}); });
return { return {
updateItem: (data: IUpdateLibraryItemDTO) => mutation.mutate(data) updateItem: (data: ILibraryUpdateDTO) => mutation.mutate(data)
}; };
}; };

View File

@ -1,7 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IRSFormDTO, rsformsApi } from '@/backend/rsform/api'; import { 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';
@ -13,7 +14,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: IRSFormDTO | undefined) => (prev: IRSFormData | undefined) =>
!prev !prev
? undefined ? undefined
: { : {

View File

@ -1,7 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IRSFormDTO, rsformsApi } from '@/backend/rsform/api'; import { 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';
@ -13,7 +14,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: IRSFormDTO | undefined) => (prev: IRSFormData | undefined) =>
!prev !prev
? undefined ? undefined
: { : {

View File

@ -2,33 +2,18 @@ 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, ILibraryItemData, LibraryItemID } from '@/models/library'; import { ILibraryItem, LibraryItemID } from '@/models/library';
import { import {
IArgument,
ICstSubstitute, ICstSubstitute,
ICstSubstituteEx, IOperationData,
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.
*/ */
@ -51,8 +36,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: IOperationDTO; new_operation: IOperationData;
oss: IOperationSchemaDTO; oss: IOperationSchemaData;
} }
/** /**
@ -76,7 +61,7 @@ export interface IOperationDeleteDTO extends ITargetOperation {
*/ */
export interface IInputCreatedResponse { export interface IInputCreatedResponse {
new_schema: ILibraryItem; new_schema: ILibraryItem;
oss: IOperationSchemaDTO; oss: IOperationSchemaData;
} }
/** /**
@ -117,7 +102,7 @@ export const ossApi = {
queryFn: meta => queryFn: meta =>
!itemID !itemID
? undefined ? undefined
: axiosGet<IOperationSchemaDTO>({ : axiosGet<IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/details`, endpoint: `/api/oss/${itemID}/details`,
options: { signal: meta.signal } options: { signal: meta.signal }
}) })
@ -150,7 +135,7 @@ export const ossApi = {
} }
}), }),
operationDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationDeleteDTO }) => operationDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationDeleteDTO }) =>
axiosDelete<IOperationDeleteDTO, IOperationSchemaDTO>({ axiosDelete<IOperationDeleteDTO, IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/delete-operation`, endpoint: `/api/oss/${itemID}/delete-operation`,
request: { request: {
data: data, data: data,
@ -166,7 +151,7 @@ export const ossApi = {
} }
}), }),
inputUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: IInputUpdateDTO }) => inputUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: IInputUpdateDTO }) =>
axiosPatch<IInputUpdateDTO, IOperationSchemaDTO>({ axiosPatch<IInputUpdateDTO, IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/set-input`, endpoint: `/api/oss/${itemID}/set-input`,
request: { request: {
data: data, data: data,
@ -174,7 +159,7 @@ export const ossApi = {
} }
}), }),
operationUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationUpdateDTO }) => operationUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationUpdateDTO }) =>
axiosPatch<IOperationUpdateDTO, IOperationSchemaDTO>({ axiosPatch<IOperationUpdateDTO, IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/update-operation`, endpoint: `/api/oss/${itemID}/update-operation`,
request: { request: {
data: data, data: data,
@ -182,7 +167,7 @@ export const ossApi = {
} }
}), }),
operationExecute: ({ itemID, data }: { itemID: LibraryItemID; data: ITargetOperation }) => operationExecute: ({ itemID, data }: { itemID: LibraryItemID; data: ITargetOperation }) =>
axiosPost<ITargetOperation, IOperationSchemaDTO>({ axiosPost<ITargetOperation, IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/execute-operation`, endpoint: `/api/oss/${itemID}/execute-operation`,
request: { request: {
data: data, data: data,
@ -191,7 +176,7 @@ export const ossApi = {
}), }),
relocateConstituents: ({ itemID, data }: { itemID: LibraryItemID; data: ICstRelocateDTO }) => relocateConstituents: ({ itemID, data }: { itemID: LibraryItemID; data: ICstRelocateDTO }) =>
axiosPost<ICstRelocateDTO, IOperationSchemaDTO>({ axiosPost<ICstRelocateDTO, IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/relocate-constituents`, endpoint: `/api/oss/${itemID}/relocate-constituents`,
request: { request: {
data: data, data: data,

View File

@ -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';

View File

@ -3,8 +3,9 @@ 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, IOperationDTO, ossApi } from './api'; import { IOperationCreateDTO, ossApi } from './api';
export const useOperationCreate = () => { export const useOperationCreate = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -23,7 +24,7 @@ export const useOperationCreate = () => {
itemID: LibraryItemID; // itemID: LibraryItemID; //
data: IOperationCreateDTO; data: IOperationCreateDTO;
}, },
onSuccess?: DataCallback<IOperationDTO> onSuccess?: DataCallback<IOperationData>
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_operation) }) ) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_operation) })
}; };
}; };

View File

@ -2,42 +2,20 @@ 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 { ILibraryItemReference, ILibraryItemVersioned, LibraryItemID, VersionID } from '@/models/library'; import { 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,
IInheritanceInfo, IRSFormData,
ITargetCst, ITargetCst,
TermForm TermForm
} from '@/models/rsform'; } from '@/models/rsform';
import { IArgumentInfo, IExpressionParse, ParsingStatus, ValueClass } from '@/models/rslang'; import { IExpressionParse } 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.
*/ */
@ -68,7 +46,7 @@ export interface ICstCreateDTO {
*/ */
export interface ICstCreatedResponse { export interface ICstCreatedResponse {
new_cst: IConstituentaMeta; new_cst: IConstituentaMeta;
schema: IRSFormDTO; schema: IRSFormData;
} }
/** /**
@ -107,7 +85,7 @@ export interface ICstMoveDTO {
*/ */
export interface IProduceStructureResponse { export interface IProduceStructureResponse {
cst_list: ConstituentaID[]; cst_list: ConstituentaID[];
schema: IRSFormDTO; schema: IRSFormData;
} }
/** /**
@ -139,7 +117,7 @@ export const rsformsApi = {
queryFn: meta => queryFn: meta =>
!itemID !itemID
? undefined ? undefined
: axiosGet<IRSFormDTO>({ : axiosGet<IRSFormData>({
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 }
}) })
@ -152,7 +130,7 @@ export const rsformsApi = {
options: { responseType: 'blob' } options: { responseType: 'blob' }
}), }),
upload: (data: IRSFormUploadDTO) => upload: (data: IRSFormUploadDTO) =>
axiosPatch<IRSFormUploadDTO, IRSFormDTO>({ axiosPatch<IRSFormUploadDTO, IRSFormData>({
endpoint: `/api/rsforms/${data.itemID}/load-trs`, endpoint: `/api/rsforms/${data.itemID}/load-trs`,
request: { request: {
data: data, data: data,
@ -182,7 +160,7 @@ export const rsformsApi = {
} }
}), }),
cstDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IConstituentaList }) => cstDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IConstituentaList }) =>
axiosDelete<IConstituentaList, IRSFormDTO>({ axiosDelete<IConstituentaList, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/delete-multiple-cst`, endpoint: `/api/rsforms/${itemID}/delete-multiple-cst`,
request: { request: {
data: data, data: data,
@ -198,7 +176,7 @@ export const rsformsApi = {
} }
}), }),
cstSubstitute: ({ itemID, data }: { itemID: LibraryItemID; data: ICstSubstitutions }) => cstSubstitute: ({ itemID, data }: { itemID: LibraryItemID; data: ICstSubstitutions }) =>
axiosPatch<ICstSubstitutions, IRSFormDTO>({ axiosPatch<ICstSubstitutions, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/substitute`, endpoint: `/api/rsforms/${itemID}/substitute`,
request: { request: {
data: data, data: data,
@ -206,7 +184,7 @@ export const rsformsApi = {
} }
}), }),
cstMove: ({ itemID, data }: { itemID: LibraryItemID; data: ICstMoveDTO }) => cstMove: ({ itemID, data }: { itemID: LibraryItemID; data: ICstMoveDTO }) =>
axiosPatch<ICstMoveDTO, IRSFormDTO>({ axiosPatch<ICstMoveDTO, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/move-cst`, endpoint: `/api/rsforms/${itemID}/move-cst`,
request: { data: data } request: { data: data }
}), }),
@ -220,7 +198,7 @@ export const rsformsApi = {
} }
}), }),
inlineSynthesis: ({ itemID, data }: { itemID: LibraryItemID; data: IInlineSynthesisDTO }) => inlineSynthesis: ({ itemID, data }: { itemID: LibraryItemID; data: IInlineSynthesisDTO }) =>
axiosPost<IInlineSynthesisDTO, IRSFormDTO>({ axiosPost<IInlineSynthesisDTO, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/inline-synthesis`, endpoint: `/api/rsforms/${itemID}/inline-synthesis`,
request: { request: {
data: data, data: data,
@ -228,12 +206,12 @@ export const rsformsApi = {
} }
}), }),
restoreOrder: ({ itemID }: { itemID: LibraryItemID }) => restoreOrder: ({ itemID }: { itemID: LibraryItemID }) =>
axiosPatch<undefined, IRSFormDTO>({ axiosPatch<undefined, IRSFormData>({
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, IRSFormDTO>({ axiosPatch<undefined, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/reset-aliases`, endpoint: `/api/rsforms/${itemID}/reset-aliases`,
request: { successMessage: information.reindexComplete } request: { successMessage: information.reindexComplete }
}), }),

View File

@ -4,8 +4,9 @@ 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, IRSFormDTO, rsformsApi } from './api'; import { IInlineSynthesisDTO, rsformsApi } from './api';
export const useInlineSynthesis = () => { export const useInlineSynthesis = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -32,7 +33,7 @@ export const useInlineSynthesis = () => {
itemID: LibraryItemID; // itemID: LibraryItemID; //
data: IInlineSynthesisDTO; data: IInlineSynthesisDTO;
}, },
onSuccess?: DataCallback<IRSFormDTO> onSuccess?: DataCallback<IRSFormData>
) => mutation.mutate(data, { onSuccess }) ) => mutation.mutate(data, { onSuccess })
}; };
}; };

View File

@ -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';

View File

@ -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';

View File

@ -1,45 +1,14 @@
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 { IUserInfo, IUserProfile } from '@/models/user'; import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user';
import { patterns } from '@/utils/constants'; import { information } from '@/utils/labels';
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 const UpdateProfileSchema = z.object({ export interface IUpdateProfileDTO extends Omit<IUser, 'is_staff' | 'id'> {}
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',
@ -64,8 +33,8 @@ export const usersApi = {
}) })
}), }),
signup: (data: IUserSignupDTO) => signup: (data: IUserSignupData) =>
axiosPost<IUserSignupDTO, IUserProfile>({ axiosPost<IUserSignupData, IUserProfile>({
endpoint: '/users/api/signup', endpoint: '/users/api/signup',
request: { request: {
data: data, data: data,

View File

@ -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 { IUserSignupDTO, usersApi } from '@/backend/users/api'; import { usersApi } from '@/backend/users/api';
import { IUserProfile } from '@/models/user'; import { IUserProfile, IUserSignupData } 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: IUserSignupDTO, // data: IUserSignupData, //
onSuccess?: DataCallback<IUserProfile> onSuccess?: DataCallback<IUserProfile>
) => mutation.mutate(data, { onSuccess }), ) => mutation.mutate(data, { onSuccess }),
isPending: mutation.isPending, isPending: mutation.isPending,

View File

@ -1,8 +1,5 @@
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 = () => {
@ -16,8 +13,7 @@ export const useUpdateProfile = () => {
} }
}); });
return { return {
updateProfile: (data: IUpdateProfileDTO, onSuccess?: DataCallback<IUserProfile>) => updateProfile: (data: IUpdateProfileDTO) => mutation.mutate(data),
mutation.mutate(data, { onSuccess }),
isPending: mutation.isPending, isPending: mutation.isPending,
error: mutation.error, error: mutation.error,
reset: mutation.reset reset: mutation.reset

View File

@ -1,6 +1,5 @@
// =========== 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 {
/** /**
@ -36,13 +35,6 @@ 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.
* *

View File

@ -17,9 +17,6 @@ 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;
@ -28,6 +25,9 @@ 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,
onChange, onSelectValue,
className, className,
...restProps ...restProps
}: PickConstituentaProps) { }: PickConstituentaProps) {
@ -110,7 +110,7 @@ function PickConstituenta({
<p>Измените параметры фильтра</p> <p>Измените параметры фильтра</p>
</NoData> </NoData>
} }
onRowClicked={onChange} onRowClicked={onSelectValue}
/> />
</div> </div>
); );

View File

@ -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,
value, selected,
onChange, setSelected,
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)] = value.includes(cst.id); newRowSelection[String(index)] = selected.includes(cst.id);
}); });
setRowSelection(newRowSelection); setRowSelection(newRowSelection);
}, [filtered, setRowSelection, value]); }, [filtered, setRowSelection, selected]);
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) {
onChange([]); setSelected([]);
} 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);
} }
}); });
onChange(prev => [...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)), ...newSelection]); setSelected(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 ? `Выбраны ${value.length} из ${data.length}` : 'Конституенты'} {data.length > 0 ? `Выбраны ${selected.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}
value={value} selected={selected}
onChange={onChange} setSelected={setSelected}
emptySelection={value.length === 0} emptySelection={selected.length === 0}
className='w-fit' className='w-fit'
/> />
</div> </div>

View File

@ -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 {
value: OperationID[]; rows?: number;
onChange: React.Dispatch<React.SetStateAction<OperationID[]>>;
items: IOperation[]; items: IOperation[];
rows?: number; selected: OperationID[];
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
} }
const columnHelper = createColumnHelper<IOperation>(); const columnHelper = createColumnHelper<IOperation>();
function PickMultiOperation({ rows, items, value, onChange, className, ...restProps }: PickMultiOperationProps) { function PickMultiOperation({ rows, items, selected, setSelected, className, ...restProps }: PickMultiOperationProps) {
const selectedItems = value.map(itemID => items.find(item => item.id === itemID)!); const selectedItems = selected.map(itemID => items.find(item => item.id === itemID)!);
const nonSelectedItems = items.filter(item => !value.includes(item.id)); const nonSelectedItems = items.filter(item => !selected.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) {
onChange(prev => prev.filter(item => item !== operation)); setSelected(prev => prev.filter(item => item !== operation));
} }
function handleSelect(operation?: IOperation) { function handleSelect(operation?: IOperation) {
if (operation) { if (operation) {
setLastSelected(operation); setLastSelected(operation);
onChange(prev => [...prev, operation.id]); setSelected(prev => [...prev, operation.id]);
setTimeout(() => setLastSelected(undefined), 1000); setTimeout(() => setLastSelected(undefined), 1000);
} }
} }
function handleMoveUp(operation: OperationID) { function handleMoveUp(operation: OperationID) {
const index = value.indexOf(operation); const index = selected.indexOf(operation);
if (index > 0) { if (index > 0) {
onChange(prev => { setSelected(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, value, onChange, className, ...restPr
} }
function handleMoveDown(operation: OperationID) { function handleMoveDown(operation: OperationID) {
const index = value.indexOf(operation); const index = selected.indexOf(operation);
if (index < value.length - 1) { if (index < selected.length - 1) {
onChange(prev => { setSelected(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, value, onChange, className, ...restPr
noBorder noBorder
items={nonSelectedItems} // prettier: split-line items={nonSelectedItems} // prettier: split-line
value={lastSelected} value={lastSelected}
onChange={handleSelect} onSelectValue={handleSelect}
/> />
<DataTable <DataTable
dense dense

View File

@ -19,15 +19,14 @@ 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>();
@ -39,7 +38,7 @@ function PickSchema({
items, items,
itemType, itemType,
value, value,
onChange, onSelectValue,
baseFilter, baseFilter,
className, className,
...restProps ...restProps
@ -157,7 +156,7 @@ function PickSchema({
<p>Измените параметры фильтра</p> <p>Измените параметры фильтра</p>
</FlexColumn> </FlexColumn>
} }
onRowClicked={rowData => onChange(rowData.id)} onRowClicked={rowData => onSelectValue(rowData.id)}
/> />
</div> </div>
); );

View File

@ -20,9 +20,8 @@ import { errors } from '@/utils/labels';
import SelectLibraryItem from './SelectLibraryItem'; import SelectLibraryItem from './SelectLibraryItem';
interface PickSubstitutionsProps extends CProps.Styling { interface PickSubstitutionsProps extends CProps.Styling {
value: ICstSubstitute[]; substitutions: ICstSubstitute[];
onChange: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>; setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
suggestions?: ICstSubstitute[]; suggestions?: ICstSubstitute[];
prefixID: string; prefixID: string;
@ -36,8 +35,8 @@ interface PickSubstitutionsProps extends CProps.Styling {
const columnHelper = createColumnHelper<IMultiSubstitution>(); const columnHelper = createColumnHelper<IMultiSubstitution>();
function PickSubstitutions({ function PickSubstitutions({
value, substitutions,
onChange, setSubstitutions,
suggestions, suggestions,
prefixID, prefixID,
rows, rows,
@ -67,7 +66,7 @@ function PickSubstitutions({
) ?? []; ) ?? [];
const substitutionData: IMultiSubstitution[] = [ const substitutionData: IMultiSubstitution[] = [
...value.map(item => ({ ...substitutions.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)!,
@ -111,8 +110,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 = value.map(item => item.original); const toDelete = substitutions.map(item => item.original);
const replacements = value.map(item => item.substitution); const replacements = substitutions.map(item => item.substitution);
if ( if (
toDelete.includes(newSubstitution.original) || toDelete.includes(newSubstitution.original) ||
toDelete.includes(newSubstitution.substitution) || toDelete.includes(newSubstitution.substitution) ||
@ -127,7 +126,7 @@ function PickSubstitutions({
return; return;
} }
} }
onChange(prev => [...prev, newSubstitution]); setSubstitutions(prev => [...prev, newSubstitution]);
setLeftCst(undefined); setLeftCst(undefined);
setRightCst(undefined); setRightCst(undefined);
} }
@ -137,12 +136,12 @@ function PickSubstitutions({
} }
function handleAcceptSuggestion(item: IMultiSubstitution) { function handleAcceptSuggestion(item: IMultiSubstitution) {
onChange(prev => [...prev, { original: item.original.id, substitution: item.substitution.id }]); setSubstitutions(prev => [...prev, { original: item.original.id, substitution: item.substitution.id }]);
} }
function handleDeleteSubstitution(target: IMultiSubstitution) { function handleDeleteSubstitution(target: IMultiSubstitution) {
handleDeclineSuggestion(target); handleDeclineSuggestion(target);
onChange(prev => { setSubstitutions(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) {
@ -231,15 +230,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}
onChange={setLeftArgument} onSelectValue={setLeftArgument}
/> />
<SelectConstituenta <SelectConstituenta
noBorder noBorder
items={(leftArgument as IRSForm)?.items.filter( items={(leftArgument as IRSForm)?.items.filter(
cst => !value.find(item => item.original === cst.id) && (!filter || filter(cst)) cst => !substitutions.find(item => item.original === cst.id) && (!filter || filter(cst))
)} )}
value={leftCst} value={leftCst}
onChange={setLeftCst} onSelectValue={setLeftCst}
/> />
</div> </div>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
@ -269,15 +268,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}
onChange={setRightArgument} onSelectValue={setRightArgument}
/> />
<SelectConstituenta <SelectConstituenta
noBorder noBorder
items={(rightArgument as IRSForm)?.items.filter( items={(rightArgument as IRSForm)?.items.filter(
cst => !value.find(item => item.original === cst.id) && (!filter || filter(cst)) cst => !substitutions.find(item => item.original === cst.id) && (!filter || filter(cst))
)} )}
value={rightCst} value={rightCst}
onChange={setRightCst} onSelectValue={setRightCst}
/> />
</div> </div>
</div> </div>

View File

@ -1,5 +1,7 @@
'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';
@ -13,7 +15,6 @@ 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;
} }
@ -21,12 +22,15 @@ 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();
function handleChange(newValue: AccessPolicy) { const handleChange = useCallback(
(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}>

View File

@ -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 {
value?: IConstituenta;
onChange: (newValue?: IConstituenta) => void;
items?: IConstituenta[]; items?: IConstituenta[];
value?: IConstituenta;
onSelectValue: (newValue?: IConstituenta) => void;
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
} }
@ -22,7 +22,7 @@ function SelectConstituenta({
className, className,
items, items,
value, value,
onChange, onSelectValue,
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 => onChange(items?.find(cst => cst.id === data?.value))} onChange={data => onSelectValue(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}

View File

@ -1,5 +1,7 @@
'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';
@ -13,18 +15,21 @@ import { describeCstSource, labelCstSource } from '@/utils/labels';
interface SelectGraphFilterProps extends CProps.Styling { interface SelectGraphFilterProps extends CProps.Styling {
value: DependencyMode; value: DependencyMode;
onChange: (value: DependencyMode) => void;
dense?: boolean; dense?: boolean;
onChange: (value: DependencyMode) => void;
} }
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();
function handleChange(newValue: DependencyMode) { const handleChange = useCallback(
(newValue: DependencyMode) => {
menu.hide(); menu.hide();
onChange(newValue); onChange(newValue);
} },
[menu, onChange]
);
return ( return (
<div ref={menu.ref} {...restProps}> <div ref={menu.ref} {...restProps}>

View File

@ -1,5 +1,7 @@
'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';
@ -20,12 +22,15 @@ 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();
function handleChange(newValue: LibraryItemType) { const handleChange = useCallback(
(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}>

View File

@ -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;
onChange: (newValue?: ILibraryItem) => void; onSelectValue: (newValue?: ILibraryItem) => void;
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
@ -20,7 +20,7 @@ function SelectLibraryItem({
className, className,
items, items,
value, value,
onChange, onSelectValue,
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 => onChange(items?.find(cst => cst.id === data?.value))} onChange={data => onSelectValue(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}

View File

@ -12,10 +12,9 @@ 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) {

View File

@ -1,6 +1,7 @@
'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';
@ -13,9 +14,10 @@ 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({
@ -27,12 +29,15 @@ function SelectLocationContext({
}: SelectLocationContextProps) { }: SelectLocationContextProps) {
const menu = useDropdown(); const menu = useDropdown();
function handleClick(event: CProps.EventMouse, newValue: string) { const handleClick = useCallback(
(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]'>

View File

@ -1,6 +1,7 @@
'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';
@ -21,10 +22,13 @@ 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();
function handleChange(newValue: LocationHead) { const handleChange = useCallback(
(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}>

View File

@ -1,5 +1,7 @@
'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';
@ -13,18 +15,21 @@ import { describeCstMatchMode, labelCstMatchMode } from '@/utils/labels';
interface SelectMatchModeProps extends CProps.Styling { interface SelectMatchModeProps extends CProps.Styling {
value: CstMatchMode; value: CstMatchMode;
onChange: (value: CstMatchMode) => void;
dense?: boolean; dense?: boolean;
onChange: (value: CstMatchMode) => void;
} }
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();
function handleChange(newValue: CstMatchMode) { const handleChange = useCallback(
(newValue: CstMatchMode) => {
menu.hide(); menu.hide();
onChange(newValue); onChange(newValue);
} },
[menu, onChange]
);
return ( return (
<div ref={menu.ref} {...restProps}> <div ref={menu.ref} {...restProps}>

View File

@ -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[];
onChange: (newValue: IGrammemeOption[]) => void; onChangeValue: (newValue: IGrammemeOption[]) => void;
placeholder?: string; placeholder?: string;
} }
function SelectMultiGrammeme({ value, onChange, ...restProps }: SelectMultiGrammemeProps) { function SelectMultiGrammeme({ value, onChangeValue, ...restProps }: SelectMultiGrammemeProps) {
const [options, setOptions] = useState<IGrammemeOption[]>([]); const [options, setOptions] = useState<IGrammemeOption[]>([]);
useEffect(() => { useEffect(() => {
@ -28,7 +28,7 @@ function SelectMultiGrammeme({ value, onChange, ...restProps }: SelectMultiGramm
<SelectMulti <SelectMulti
options={options} options={options}
value={value} value={value}
onChange={newValue => onChange([...newValue].sort(compareGrammemeOptions))} onChange={newValue => onChangeValue([...newValue].sort(compareGrammemeOptions))}
{...restProps} {...restProps}
/> />
); );

View File

@ -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;
onChange: (newValue?: IOperation) => void; onSelectValue: (newValue?: IOperation) => void;
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
@ -20,7 +20,7 @@ function SelectOperation({
className, className,
items, items,
value, value,
onChange, onSelectValue,
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 => onChange(items?.find(cst => cst.id === data?.value))} onChange={data => onSelectValue(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}

View File

@ -11,7 +11,7 @@ import { matchUser } from '@/models/userAPI';
interface SelectUserProps extends CProps.Styling { interface SelectUserProps extends CProps.Styling {
value?: UserID; value?: UserID;
onChange: (newValue: UserID) => void; onSelectValue: (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,
onChange, onSelectValue,
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) onChange(data.value); if (data?.value !== undefined) onSelectValue(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}

View File

@ -11,13 +11,13 @@ interface SelectVersionProps extends CProps.Styling {
id?: string; id?: string;
items?: IVersionInfo[]; items?: IVersionInfo[];
value?: VersionID; value?: VersionID;
onChange: (newValue?: VersionID) => void; onSelectValue: (newValue?: VersionID) => void;
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
} }
function SelectVersion({ id, className, items, value, onChange, ...restProps }: SelectVersionProps) { function SelectVersion({ id, className, items, value, onSelectValue, ...restProps }: SelectVersionProps) {
const options = [ const options = [
{ {
value: undefined, value: undefined,
@ -40,7 +40,7 @@ function SelectVersion({ id, className, items, value, onChange, ...restProps }:
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 => onChange(data?.value)} onChange={data => onSelectValue(data?.value)}
{...restProps} {...restProps}
/> />
); );

View File

@ -1,6 +1,7 @@
'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';
@ -9,14 +10,17 @@ 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 {
value: IGrammemeOption[]; selected: IGrammemeOption[];
onChange: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>; setSelected: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
} }
function SelectWordForm({ value, onChange, className, ...restProps }: SelectWordFormProps) { function SelectWordForm({ selected, setSelected, className, ...restProps }: SelectWordFormProps) {
function handleSelect(grams: Grammeme[]) { const handleSelect = useCallback(
onChange(SelectorGrammemes.filter(({ value }) => grams.includes(value as Grammeme))); (grams: 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}>
@ -26,7 +30,7 @@ function SelectWordForm({ value, onChange, className, ...restProps }: SelectWord
text={data.text} text={data.text}
example={data.example} example={data.example}
grams={data.grams} grams={data.grams}
isSelected={data.grams.every(gram => value.find(item => (item.value as Grammeme) === gram))} isSelected={data.grams.every(gram => selected.find(item => (item.value as Grammeme) === gram))}
onSelectGrams={handleSelect} onSelectGrams={handleSelect}
/> />
))} ))}

View File

@ -1,4 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback } from 'react';
import { import {
IconGraphCollapse, IconGraphCollapse,
@ -16,75 +17,81 @@ 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,
value: selected, selected,
isCore, isCore,
isOwned, isOwned,
onChange, setSelected,
emptySelection, emptySelection,
...restProps ...restProps
}: ToolbarGraphSelectionProps) { }: ToolbarGraphSelectionProps) {
function handleSelectCore() { const handleSelectCore = useCallback(() => {
const core = [...graph.nodes.keys()].filter(isCore); const core = [...graph.nodes.keys()].filter(isCore);
onChange([...core, ...graph.expandInputs(core)]); setSelected([...core, ...graph.expandInputs(core)]);
} }, [setSelected, graph, isCore]);
function handleSelectOwned() { const handleSelectOwned = useCallback(
if (isOwned) onChange([...graph.nodes.keys()].filter(isOwned)); () => (isOwned ? setSelected([...graph.nodes.keys()].filter(isOwned)) : undefined),
} [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={() => onChange([])} onClick={() => setSelected([])}
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={() => onChange([...selected, ...graph.expandAllInputs(selected)])} onClick={() => setSelected([...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={() => onChange([...selected, ...graph.expandAllOutputs(selected)])} onClick={() => setSelected([...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={() => onChange(graph.maximizePart(selected))} onClick={() => setSelected(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={() => onChange([...selected, ...graph.expandInputs(selected)])} onClick={() => setSelected([...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={() => onChange([...selected, ...graph.expandOutputs(selected)])} onClick={() => setSelected([...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={() => onChange([...graph.nodes.keys()].filter(item => !selected.includes(item)))} onClick={handleInvertSelection}
/> />
<MiniButton <MiniButton
titleHtml='Выделить ядро' titleHtml='Выделить ядро'

View File

@ -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' | 'onChange'> { export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick'> {
/** 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`. */
onChange?: (newValue: boolean) => void; setValue?: (newValue: boolean) => void;
} }
/** /**
@ -29,18 +29,18 @@ function Checkbox({
hideTitle, hideTitle,
className, className,
value, value,
onChange, setValue,
...restProps ...restProps
}: CheckboxProps) { }: CheckboxProps) {
const cursor = disabled ? 'cursor-arrow' : onChange ? 'cursor-pointer' : ''; const cursor = disabled ? 'cursor-arrow' : setValue ? 'cursor-pointer' : '';
function handleClick(event: CProps.EventMouse): void { function handleClick(event: CProps.EventMouse): void {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (disabled || !onChange) { if (disabled || !setValue) {
return; return;
} }
onChange(!value); setValue(!value);
} }
return ( return (

View File

@ -6,12 +6,12 @@ import { globals } from '@/utils/constants';
import { CheckboxProps } from './Checkbox'; import { CheckboxProps } from './Checkbox';
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> { export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'setValue'> {
/** 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`. */
onChange?: (newValue: boolean | null) => void; setValue?: (newValue: boolean | null) => void;
} }
/** /**
@ -25,23 +25,23 @@ function CheckboxTristate({
hideTitle, hideTitle,
className, className,
value, value,
onChange, setValue,
...restProps ...restProps
}: CheckboxTristateProps) { }: CheckboxTristateProps) {
const cursor = disabled ? 'cursor-arrow' : onChange ? 'cursor-pointer' : ''; const cursor = disabled ? 'cursor-arrow' : setValue ? 'cursor-pointer' : '';
function handleClick(event: CProps.EventMouse): void { function handleClick(event: CProps.EventMouse): void {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (disabled || !onChange) { if (disabled || !setValue) {
return; return;
} }
if (value === false) { if (value === false) {
onChange(null); setValue(null);
} else if (value === null) { } else if (value === null) {
onChange(true); setValue(true);
} else { } else {
onChange(false); setValue(false);
} }
} }

View File

@ -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()
} }
onChange={handleChange} setValue={handleChange}
/> />
); );
} }

View File

@ -15,7 +15,7 @@ function SelectRow<TData>({ row, onChangeLastSelected }: SelectRowProps<TData>)
row.toggleSelected(value); row.toggleSelected(value);
} }
return <Checkbox tabIndex={-1} value={row.getIsSelected()} onChange={handleChange} />; return <Checkbox tabIndex={-1} value={row.getIsSelected()} setValue={handleChange} />;
} }
export default SelectRow; export default SelectRow;

View File

@ -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({ onChange: setValue, disabled, ...restProps }: CheckboxProps) { function DropdownCheckbox({ setValue, disabled, ...restProps }: CheckboxProps) {
return ( return (
<div <div
className={clsx( className={clsx(
@ -13,7 +13,7 @@ function DropdownCheckbox({ onChange: setValue, disabled, ...restProps }: Checkb
!!setValue && !disabled && 'clr-hover' !!setValue && !disabled && 'clr-hover'
)} )}
> >
<Checkbox tabIndex={-1} disabled={disabled} onChange={setValue} {...restProps} /> <Checkbox tabIndex={-1} disabled={disabled} setValue={setValue} {...restProps} />
</div> </div>
); );
} }

View File

@ -1,24 +0,0 @@
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;

View File

@ -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. */
onChange: (newItem: ItemType) => void; onChangeValue: (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,
onChange, onChangeValue,
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();
onChange(target); onChangeValue(target);
} }
return ( return (

View File

@ -2,10 +2,9 @@ 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.ErrorProcessing, CProps.Colors, CProps.TextArea { export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea {
/** Indicates that padding should be minimal. */ /** Indicates that padding should be minimal. */
dense?: boolean; dense?: boolean;
@ -30,7 +29,6 @@ function TextArea({
noResize, noResize,
className, className,
fitContent, fitContent,
error,
colors = 'clr-input', colors = 'clr-input',
...restProps ...restProps
}: TextAreaProps) { }: TextAreaProps) {
@ -39,7 +37,7 @@ function TextArea({
className={clsx( className={clsx(
'w-full', 'w-full',
{ {
'flex flex-col': !dense, 'flex flex-col gap-2': !dense,
'flex flex-grow items-center gap-3': dense 'flex flex-grow items-center gap-3': dense
}, },
dense && className dense && className
@ -57,7 +55,6 @@ 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,
@ -67,7 +64,6 @@ function TextArea({
required={required} required={required}
{...restProps} {...restProps}
/> />
<ErrorField className='mt-1' error={error} />
</div> </div>
); );
} }

View File

@ -2,10 +2,9 @@ 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.ErrorProcessing, CProps.Colors, CProps.Input { interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input {
/** Indicates that padding should be minimal. */ /** Indicates that padding should be minimal. */
dense?: boolean; dense?: boolean;
@ -33,14 +32,13 @@ 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': !dense, 'flex flex-col gap-2': !dense,
'flex items-center gap-3': dense 'flex items-center gap-3': dense
}, },
dense && className dense && className
@ -55,7 +53,6 @@ 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
}, },
@ -66,7 +63,6 @@ function TextInput({
disabled={disabled} disabled={disabled}
{...restProps} {...restProps}
/> />
<ErrorField className='mt-1' error={error} />
</div> </div>
); );
} }

View File

@ -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
onChange={handleSelectLocation} onSelectValue={handleSelectLocation}
rows={14} rows={14}
baseFilter={baseFilter} baseFilter={baseFilter}
/> />

View File

@ -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]' />

View File

@ -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 { IRCloneLibraryItemDTO } from '@/backend/library/api'; import { IRSFormCloneDTO } 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: IRCloneLibraryItemDTO = { const data: IRSFormCloneDTO = {
id: base.id, id: base.id,
item_type: base.item_type, item_type: base.item_type,
title: title, title: title,
@ -119,7 +119,11 @@ 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 value={head} onChange={setHead} excluded={!user.is_staff ? [LocationHead.LIBRARY] : []} /> <SelectLocationHead
value={head}
onChange={setHead}
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
/>
</div> </div>
<SelectLocationContext value={location} onChange={handleSelectLocation} /> <SelectLocationContext value={location} onChange={handleSelectLocation} />
<TextArea <TextArea
@ -137,7 +141,7 @@ function DlgCloneLibraryItem() {
id='dlg_only_selected' id='dlg_only_selected'
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`} label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
value={onlySelected} value={onlySelected}
onChange={value => setOnlySelected(value)} setValue={value => setOnlySelected(value)}
/> />
</Modal> </Modal>
); );

View File

@ -98,7 +98,7 @@ function TabInputOperation({
</div> </div>
<Checkbox <Checkbox
value={createSchema} value={createSchema}
onChange={onChangeCreateSchema} setValue={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}
onChange={onChangeAttachedID} onSelectValue={onChangeAttachedID}
rows={8} rows={8}
baseFilter={baseFilter} baseFilter={baseFilter}
/> />

View File

@ -57,7 +57,7 @@ function TabSynthesisOperation({
<FlexColumn> <FlexColumn>
<Label text={`Выбор аргументов: [ ${inputs.length} ]`} /> <Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
<PickMultiOperation items={oss.items} value={inputs} onChange={setInputs} rows={6} /> <PickMultiOperation items={oss.items} selected={inputs} setSelected={setInputs} rows={6} />
</FlexColumn> </FlexColumn>
</div> </div>
); );

View File

@ -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}
onChange={value => setOnlySelected(value)} setValue={value => setOnlySelected(value)}
/> />
</Modal> </Modal>
); );

View File

@ -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}
onChange={handleSelectConstituenta} onSelectValue={handleSelectConstituenta}
prefixID={prefixes.cst_modal_list} prefixID={prefixes.cst_modal_list}
rows={7} rows={7}
/> />

View File

@ -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}
onChange={cst => partialUpdate({ prototype: cst })} onSelectValue={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}

View File

@ -55,7 +55,7 @@ function DlgDeleteCst() {
label='Удалить зависимые конституенты' label='Удалить зависимые конституенты'
className='mb-2' className='mb-2'
value={expandOut} value={expandOut}
onChange={value => setExpandOut(value)} setValue={value => setExpandOut(value)}
/> />
{hasInherited ? ( {hasInherited ? (
<p className='text-sm clr-text-red'>Внимание! Выбранные конституенты имеют наследников в ОСС</p> <p className='text-sm clr-text-red'>Внимание! Выбранные конституенты имеют наследников в ОСС</p>

View File

@ -39,7 +39,7 @@ function DlgDeleteOperation() {
label='Сохранить наследованные конституенты' label='Сохранить наследованные конституенты'
titleHtml='Наследованные конституенты <br/>превратятся в дописанные' titleHtml='Наследованные конституенты <br/>превратятся в дописанные'
value={keepConstituents} value={keepConstituents}
onChange={setKeepConstituents} setValue={setKeepConstituents}
disabled={target.result === null} disabled={target.result === null}
/> />
<Checkbox <Checkbox
@ -50,7 +50,7 @@ function DlgDeleteOperation() {
: 'Удалить схему вместе с операцией' : 'Удалить схему вместе с операцией'
} }
value={deleteSchema} value={deleteSchema}
onChange={setDeleteSchema} setValue={setDeleteSchema}
disabled={!target.is_owned || target.result === null} disabled={!target.is_owned || target.result === null}
/> />
</Modal> </Modal>

View File

@ -63,7 +63,7 @@ function DlgEditEditors() {
<SelectUser <SelectUser
filter={id => !selected.includes(id)} filter={id => !selected.includes(id)}
value={undefined} value={undefined}
onChange={onAddEditor} onSelectValue={onAddEditor}
className='w-[25rem]' className='w-[25rem]'
/> />
</div> </div>

View File

@ -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} value={inputs} onChange={setInputs} rows={8} /> <PickMultiOperation items={filtered} selected={inputs} setSelected={setInputs} rows={8} />
</FlexColumn> </FlexColumn>
</div> </div>
); );

View File

@ -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}
value={substitutions} substitutions={substitutions}
onChange={setSubstitutions} setSubstitutions={setSubstitutions}
suggestions={suggestions} suggestions={suggestions}
/> />
<TextArea <TextArea

View File

@ -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}
onChange={handleSelectConstituenta} onSelectValue={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 value={selectedGrams} onChange={setSelectedGrams} /> <SelectWordForm selected={selectedGrams} setSelected={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}
onChange={setSelectedGrams} onChangeValue={setSelectedGrams}
/> />
</div> </div>
</div> </div>

View File

@ -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 } from '@/models/language'; import { Grammeme, IWordForm, IWordFormPlain } 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,13 +79,11 @@ function DlgEditWordForms() {
} }
function handleInflect() { function handleInflect() {
inflectText( const data: IWordFormPlain = {
{
text: term, text: term,
grams: inputGrams.map(gram => gram.value).join(',') grams: inputGrams.map(gram => gram.value).join(',')
}, };
response => setInputText(response.result) inflectText(data, response => setInputText(response.result));
);
} }
function handleParse() { function handleParse() {
@ -172,7 +170,7 @@ function DlgEditWordForms() {
placeholder='Выберите граммемы' placeholder='Выберите граммемы'
className='min-w-[15rem] h-fit' className='min-w-[15rem] h-fit'
value={inputGrams} value={inputGrams}
onChange={setInputGrams} onChangeValue={setInputGrams}
/> />
</div> </div>

View File

@ -31,31 +31,31 @@ function DlgGraphParams() {
label='Скрыть текст' label='Скрыть текст'
title='Не отображать термины' title='Не отображать термины'
value={params.noText} value={params.noText}
onChange={value => updateParams({ noText: value })} setValue={value => updateParams({ noText: value })}
/> />
<Checkbox <Checkbox
label='Скрыть несвязанные' label='Скрыть несвязанные'
title='Неиспользуемые конституенты' title='Неиспользуемые конституенты'
value={params.noHermits} value={params.noHermits}
onChange={value => updateParams({ noHermits: value })} setValue={value => updateParams({ noHermits: value })}
/> />
<Checkbox <Checkbox
label='Скрыть шаблоны' label='Скрыть шаблоны'
titleHtml='Терм-функции и предикат-функции <br/>с параметризованными аргументами' titleHtml='Терм-функции и предикат-функции <br/>с параметризованными аргументами'
value={params.noTemplates} value={params.noTemplates}
onChange={value => updateParams({ noTemplates: value })} setValue={value => updateParams({ noTemplates: value })}
/> />
<Checkbox <Checkbox
label='Транзитивная редукция' label='Транзитивная редукция'
titleHtml='Удалить связи, образующие <br/>транзитивные пути в графе' titleHtml='Удалить связи, образующие <br/>транзитивные пути в графе'
value={params.noTransitive} value={params.noTransitive}
onChange={value => updateParams({ noTransitive: value })} setValue={value => updateParams({ noTransitive: value })}
/> />
<Checkbox <Checkbox
label='Свернуть порожденные' label='Свернуть порожденные'
title='Не отображать порожденные понятия' title='Не отображать порожденные понятия'
value={params.foldDerived} value={params.foldDerived}
onChange={value => updateParams({ foldDerived: value })} setValue={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}
onChange={value => updateParams({ allowBase: value })} setValue={value => updateParams({ allowBase: value })}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.STRUCTURED)} label={labelCstType(CstType.STRUCTURED)}
value={params.allowStruct} value={params.allowStruct}
onChange={value => updateParams({ allowStruct: value })} setValue={value => updateParams({ allowStruct: value })}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.TERM)} label={labelCstType(CstType.TERM)}
value={params.allowTerm} value={params.allowTerm}
onChange={value => updateParams({ allowTerm: value })} setValue={value => updateParams({ allowTerm: value })}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.AXIOM)} label={labelCstType(CstType.AXIOM)}
value={params.allowAxiom} value={params.allowAxiom}
onChange={value => updateParams({ allowAxiom: value })} setValue={value => updateParams({ allowAxiom: value })}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.FUNCTION)} label={labelCstType(CstType.FUNCTION)}
value={params.allowFunction} value={params.allowFunction}
onChange={value => updateParams({ allowFunction: value })} setValue={value => updateParams({ allowFunction: value })}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.PREDICATE)} label={labelCstType(CstType.PREDICATE)}
value={params.allowPredicate} value={params.allowPredicate}
onChange={value => updateParams({ allowPredicate: value })} setValue={value => updateParams({ allowPredicate: value })}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.CONSTANT)} label={labelCstType(CstType.CONSTANT)}
value={params.allowConstant} value={params.allowConstant}
onChange={value => updateParams({ allowConstant: value })} setValue={value => updateParams({ allowConstant: value })}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.THEOREM)} label={labelCstType(CstType.THEOREM)}
value={params.allowTheorem} value={params.allowTheorem}
onChange={value => updateParams({ allowTheorem: value })} setValue={value => updateParams({ allowTheorem: value })}
/> />
</div> </div>
</Modal> </Modal>

View File

@ -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}
value={selected} selected={selected}
onChange={setSelected} setSelected={setSelected}
/> />
); );
} }

View File

@ -25,7 +25,7 @@ function TabSource({ selected, receiver, setSelected }: TabSourceProps) {
itemType={LibraryItemType.RSFORM} itemType={LibraryItemType.RSFORM}
rows={14} rows={14}
value={selected} value={selected}
onChange={setSelected} onSelectValue={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>

View File

@ -22,8 +22,8 @@ function TabSubstitutions({ sourceID, receiver, selected, substitutions, setSubs
return ( return (
<PickSubstitutions <PickSubstitutions
value={substitutions} substitutions={substitutions}
onChange={setSubstitutions} setSubstitutions={setSubstitutions}
rows={10} rows={10}
prefixID={prefixes.cst_inline_synth_substitutes} prefixID={prefixes.cst_inline_synth_substitutes}
schemas={schemas} schemas={schemas}

View File

@ -103,7 +103,7 @@ function DlgRelocateConstituents() {
placeholder='Выберите исходную схему' placeholder='Выберите исходную схему'
items={sourceSchemas} items={sourceSchemas}
value={source} value={source}
onChange={handleSelectSource} onSelectValue={handleSelectSource}
/> />
<MiniButton <MiniButton
title='Направление перемещения' title='Направление перемещения'
@ -116,7 +116,7 @@ function DlgRelocateConstituents() {
placeholder='Выберите целевую схему' placeholder='Выберите целевую схему'
items={destinationSchemas} items={destinationSchemas}
value={destination} value={destination}
onChange={handleSelectDestination} onSelectValue={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}
value={selected} selected={selected}
onChange={setSelected} setSelected={setSelected}
/> />
) : null} ) : null}
</div> </div>

View File

@ -40,8 +40,8 @@ function DlgSubstituteCst() {
> >
<PickSubstitutions <PickSubstitutions
allowSelfSubstitution allowSelfSubstitution
value={substitutions} substitutions={substitutions}
onChange={setSubstitutions} setSubstitutions={setSubstitutions}
rows={6} rows={6}
prefixID={prefixes.dlg_cst_substitutes_list} prefixID={prefixes.dlg_cst_substitutes_list}
schemas={[schema]} schemas={[schema]}

View File

@ -53,7 +53,7 @@ function DlgUploadRSForm() {
label='Загружать название и комментарий' label='Загружать название и комментарий'
className='py-2' className='py-2'
value={loadMetadata} value={loadMetadata}
onChange={value => setLoadMetadata(value)} setValue={value => setLoadMetadata(value)}
/> />
</Modal> </Modal>
); );

View File

@ -2,24 +2,29 @@
* Module: OSS data loading and processing. * Module: OSS data loading and processing.
*/ */
import { Graph } from '@/models/Graph'; import { Graph } from './Graph';
import { ILibraryItem, LibraryItemID } from '@/models/library'; import { ILibraryItem, LibraryItemID } from './library';
import { IOperation, IOperationSchema, IOperationSchemaStats, OperationID, OperationType } from '@/models/oss'; import {
IOperation,
import { IOperationSchemaDTO } from './api'; IOperationSchema,
IOperationSchemaData,
IOperationSchemaStats,
OperationID,
OperationType
} from './oss';
/** /**
* Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}. * Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaData}.
* *
*/ */
export class OssLoader { export class OssLoader {
private oss: IOperationSchemaDTO; private oss: IOperationSchemaData;
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: IOperationSchemaDTO, items: ILibraryItem[]) { constructor(input: IOperationSchemaData, items: ILibraryItem[]) {
this.oss = input; this.oss = input;
this.items = items; this.items = items;
} }

View File

@ -2,29 +2,27 @@
* Module: RSForm data loading and processing. * Module: RSForm data loading and processing.
*/ */
import { Graph } from '@/models/Graph'; import { Graph } from './Graph';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from './library';
import { ConstituentaID, CstType, IConstituenta, IRSForm, IRSFormStats } from '@/models/rsform'; import { ConstituentaID, CstType, IConstituenta, IRSForm, IRSFormData, IRSFormStats } from './rsform';
import { inferClass, inferStatus, inferTemplate, isBaseSet, isFunctional } from '@/models/rsformAPI'; import { inferClass, inferStatus, inferTemplate, isBaseSet, isFunctional } from './rsformAPI';
import { ParsingStatus, ValueClass } from '@/models/rslang'; import { ParsingStatus, ValueClass } from './rslang';
import { extractGlobals, isSimpleExpression, splitTemplateDefinition } from '@/models/rslangAPI'; import { extractGlobals, isSimpleExpression, splitTemplateDefinition } from './rslangAPI';
import { IRSFormDTO } from './api';
/** /**
* Loads data into an {@link IRSForm} based on {@link IRSFormDTO}. * Loads data into an {@link IRSForm} based on {@link IRSFormData}.
* *
* @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: IRSFormDTO; private schema: IRSFormData;
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: IRSFormDTO) { constructor(input: IRSFormData) {
this.schema = input; this.schema = input;
} }
@ -161,8 +159,7 @@ export class RSFormLoader {
} else if (sources.size !== 1) { } else if (sources.size !== 1) {
return false; return false;
} else { } else {
const cstID = sources.values().next().value!; const base = this.cstByID.get(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;
} }
}; };

View File

@ -214,6 +214,21 @@ 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 =====
/** /**
@ -255,3 +270,11 @@ 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;
}

View File

@ -42,6 +42,11 @@ 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.
*/ */
@ -111,13 +116,19 @@ export interface IOperationSchemaStats {
} }
/** /**
* Represents OperationSchema. * Represents backend data for {@link IOperationSchema}.
*/ */
export interface IOperationSchema extends ILibraryItemData { export interface IOperationSchemaData extends ILibraryItemData {
items: IOperation[]; items: IOperationData[];
arguments: IArgument[]; arguments: IArgument[];
substitutions: ICstSubstituteEx[]; substitutions: ICstSubstituteEx[];
}
/**
* Represents OperationSchema.
*/
export interface IOperationSchema extends IOperationSchemaData {
items: IOperation[];
graph: Graph; graph: Graph;
schemas: LibraryItemID[]; schemas: LibraryItemID[];
stats: IOperationSchemaStats; stats: IOperationSchemaStats;

View File

@ -2,7 +2,6 @@
* 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';
@ -10,6 +9,7 @@ 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,6 +49,7 @@ 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.
*/ */
@ -458,6 +459,7 @@ 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);

View File

@ -83,9 +83,9 @@ export interface ITargetCst {
} }
/** /**
* Represents Constituenta. * Represents {@link IConstituenta} data from server.
*/ */
export interface IConstituenta extends IConstituentaMeta { export interface IConstituentaData extends IConstituentaMeta {
parse: { parse: {
status: ParsingStatus; status: ParsingStatus;
valueClass: ValueClass; valueClass: ValueClass;
@ -93,7 +93,12 @@ export interface IConstituenta 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;
@ -167,21 +172,27 @@ export interface IRSFormStats {
/** /**
* Represents inheritance data for {@link IRSForm}. * Represents inheritance data for {@link IRSForm}.
*/ */
export interface IInheritanceInfo { export interface IInheritanceData {
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 ILibraryItemVersioned { export interface IRSForm extends IRSFormData {
items: IConstituenta[]; items: IConstituenta[];
inheritance: IInheritanceInfo[];
oss: ILibraryItemReference[];
stats: IRSFormStats; stats: IRSFormStats;
graph: Graph; graph: Graph;
cstByAlias: Map<string, IConstituenta>; cstByAlias: Map<string, IConstituenta>;

View File

@ -32,6 +32,14 @@ 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}.
*/ */

View File

@ -1,14 +1,12 @@
'use client'; 'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { useRef } from 'react'; import { useCallback, useEffect, useRef, useState } 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 { CreateLibraryItemSchema, ICreateLibraryItemDTO } from '@/backend/library/api'; import { ILibraryCreateDTO } 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';
@ -25,43 +23,38 @@ 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 } from '@/models/libraryAPI'; import { combineLocation, validateLocation } 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: serverError, reset: clearServerError } = useCreateItem(); const { createItem, isPending, error, reset } = 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 { const [itemType, setItemType] = useState(LibraryItemType.RSFORM);
register, const [title, setTitle] = useState('');
handleSubmit, const [alias, setAlias] = useState('');
clearErrors, const [comment, setComment] = useState('');
setValue, const [visible, setVisible] = useState(true);
control, const [policy, setPolicy] = useState(AccessPolicy.PUBLIC);
formState: { errors }
} = useForm<ICreateLibraryItemDTO>({ const [head, setHead] = useState(LocationHead.USER);
resolver: zodResolver(CreateLibraryItemSchema), const [body, setBody] = useState('');
defaultValues: {
item_type: LibraryItemType.RSFORM, const location = combineLocation(head, body);
access_policy: AccessPolicy.PUBLIC, const isValid = validateLocation(location);
visible: true,
read_only: false, const [fileName, setFileName] = useState('');
location: !!searchLocation ? searchLocation : LocationHead.USER const [file, setFile] = useState<File | undefined>();
}
});
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);
function resetErrors() { useEffect(() => {
clearServerError(); reset();
clearErrors(); }, [title, alias, reset]);
}
function handleCancel() { function handleCancel() {
if (router.canBack()) { if (router.canBack()) {
@ -71,28 +64,26 @@ function FormCreateItem() {
} }
} }
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
if (event.target.files && event.target.files.length > 0) { event.preventDefault();
setValue('file', event.target.files[0]); if (isPending) {
setValue('fileName', event.target.files[0].name); return;
} else {
setValue('file', undefined);
setValue('fileName', '');
} }
} const data: ILibraryCreateDTO = {
item_type: itemType,
function handleItemTypeChange(value: LibraryItemType) { title: title,
if (value !== LibraryItemType.RSFORM) { alias: alias,
setValue('file', undefined); comment: comment,
setValue('fileName', ''); read_only: false,
} visible: visible,
setValue('item_type', value); access_policy: policy,
} location: location,
file: file,
function onSubmit(data: ICreateLibraryItemDTO) { fileName: file?.name
};
setSearchLocation(location);
createItem(data, newItem => { createItem(data, newItem => {
setSearchLocation(data.location); if (itemType == LibraryItemType.RSFORM) {
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));
@ -100,19 +91,43 @@ 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={event => void handleSubmit(onSubmit)(event)} onSubmit={handleSubmit}
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]'>
<Controller
control={control}
name='file'
render={() => (
<input <input
id='schema_file' id='schema_file'
ref={inputRef} ref={inputRef}
@ -121,8 +136,6 @@ function FormCreateItem() {
accept={EXTEOR_TRS_FILE} accept={EXTEOR_TRS_FILE}
onChange={handleFileChange} onChange={handleFileChange}
/> />
)}
/>
<MiniButton <MiniButton
title='Загрузить из Экстеор' title='Загрузить из Экстеор'
icon={<IconDownload size='1.25rem' className='icon-primary' />} icon={<IconDownload size='1.25rem' className='icon-primary' />}
@ -133,62 +146,40 @@ function FormCreateItem() {
Создание схемы Создание схемы
</h1> </h1>
{file ? <Label className='text-wrap' text={`Загружен файл: ${file.name}`} /> : null} {fileName ? <Label className='text-wrap' text={`Загружен файл: ${fileName}`} /> : null}
<TextInput <TextInput
id='schema_title' id='schema_title'
{...register('title')} required={!file}
label='Полное название' label='Полное название'
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
error={errors.title} value={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'
{...register('alias')} required={!file}
label='Сокращение' label='Сокращение'
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
className='w-[16rem]' className='w-[16rem]'
error={errors.alias} value={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' />
<Controller <SelectItemType value={itemType} onChange={setItemType} />
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'>
<Controller <SelectAccessPolicy value={policy} onChange={setPolicy} />
control={control} //
name='access_policy'
render={({ field }) => (
<SelectAccessPolicy
value={field.value} //
onChange={field.onChange}
/>
)}
/>
<Controller
control={control}
name='visible'
render={({ field }) => (
<MiniButton <MiniButton
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'} title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
icon={<VisibilityIcon value={field.value} />} icon={<VisibilityIcon value={visible} />}
onClick={() => field.onChange(!field.value)} onClick={() => setVisible(prev => !prev)}
/>
)}
/> />
</div> </div>
</div> </div>
@ -196,58 +187,36 @@ function FormCreateItem() {
<TextArea <TextArea
id='schema_comment' id='schema_comment'
{...register('comment')}
label='Описание' label='Описание'
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
error={errors.comment} value={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='Корень' />
<Controller
control={control} //
name='location'
render={({ field }) => (
<SelectLocationHead <SelectLocationHead
value={field.value.substring(0, 2) as LocationHead} value={head}
onChange={newValue => field.onChange(combineLocation(newValue, field.value.substring(3)))} onChange={setHead}
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []} excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
/>
)}
/> />
</div> </div>
<Controller <SelectLocationContext value={location} onChange={handleSelectLocation} />
control={control} //
name='location'
render={({ field }) => (
<SelectLocationContext
value={field.value} //
onChange={field.onChange}
/>
)}
/>
<Controller
control={control} //
name='location'
render={({ field }) => (
<TextArea <TextArea
id='dlg_cst_body' id='dlg_cst_body'
label='Путь' label='Путь'
rows={4} rows={4}
value={field.value.substring(3)} value={body}
onChange={event => field.onChange(combineLocation(field.value.substring(0, 2), event.target.value))} onChange={event => setBody(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]' /> <SubmitButton text='Создать схему' loading={isPending} className='min-w-[10rem]' disabled={!isValid} />
<Button text='Отмена' className='min-w-[10rem]' onClick={() => handleCancel()} /> <Button text='Отмена' className='min-w-[10rem]' onClick={() => handleCancel()} />
</div> </div>
{serverError ? <InfoError error={serverError} /> : null} {error ? <InfoError error={error} /> : null}
</form> </form>
); );
} }

View File

@ -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}
onChange={setFilterUser} onSelectValue={setFilterUser}
/> />
</Dropdown> </Dropdown>
</div> </div>

View File

@ -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) { if (location.length <= 3 || isAnonymous || !user) {
return false; return false;
} }
if (user.is_staff) { if (user.is_staff) {

View File

@ -1,13 +1,11 @@
'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 { useForm } from 'react-hook-form'; import { useEffect, 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 { 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';
@ -21,25 +19,22 @@ import { resources } from '@/utils/constants';
function LoginPage() { function LoginPage() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
const initialName = query.get('username') ?? ''; const userQuery = 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: serverError, reset: clearServerError } = useLogin(); const { login, isPending, error, reset } = useLogin();
function onSubmit(data: IUserLoginDTO) { const [username, setUsername] = useState(userQuery || '');
login(data, () => { const [password, setPassword] = useState('');
resetField('password');
useEffect(() => {
reset();
}, [username, password, reset]);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!isPending) {
login(username, password, () => {
if (router.canBack()) { if (router.canBack()) {
router.back(); router.back();
} else { } else {
@ -47,49 +42,47 @@ function LoginPage() {
} }
}); });
} }
function resetErrors() {
clearServerError();
clearErrors();
} }
if (!isAnonymous) { if (!isAnonymous) {
return <ExpectedAnonymous />; return <ExpectedAnonymous />;
} }
return ( return (
<form <form className={clsx('cc-column cc-fade-in', 'w-[24rem] mx-auto', 'pt-12 pb-6 px-6')} onSubmit={handleSubmit}>
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'
autoComplete='username'
label='Логин или email' label='Логин или email'
{...register('username')} autoComplete='username'
autoFocus autoFocus
required
allowEnter allowEnter
spellCheck={false} spellCheck={false}
defaultValue={initialName} value={username}
error={errors.username} onChange={event => setUsername(event.target.value)}
/> />
<TextInput <TextInput
id='password' id='password'
{...register('password')}
type='password' type='password'
autoComplete='current-password'
label='Пароль' label='Пароль'
autoComplete='current-password'
required
allowEnter allowEnter
error={errors.password} value={password}
onChange={event => setPassword(event.target.value)}
/> />
<SubmitButton text='Войти' className='self-center w-[12rem] mt-3' loading={isPending} /> <SubmitButton
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>
{serverError ? <ServerError error={serverError} /> : null} {error ? <ProcessError error={error} /> : null}
</form> </form>
); );
} }
@ -97,7 +90,7 @@ function LoginPage() {
export default LoginPage; export default LoginPage;
// ====== Internals ========= // ====== Internals =========
function ServerError({ error }: { error: ErrorData }): React.ReactElement | null { function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
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