diff --git a/README.md b/README.md index 7dd55b54..3a84bd47 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ This readme file is used mostly to document project dependencies and conventions - html-to-image - zustand - @tanstack/react-table + - @tanstack/react-query + - @tanstack/react-query-devtools - @uiw/react-codemirror - @uiw/codemirror-themes - @lezer/lr diff --git a/rsconcept/frontend/package-lock.json b/rsconcept/frontend/package-lock.json index ac6b637c..0ae583a1 100644 --- a/rsconcept/frontend/package-lock.json +++ b/rsconcept/frontend/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@dagrejs/dagre": "^1.1.4", "@lezer/lr": "^1.4.2", + "@tanstack/react-query": "^5.64.1", + "@tanstack/react-query-devtools": "^5.64.1", "@tanstack/react-table": "^8.20.6", "@uiw/codemirror-themes": "^4.23.7", "@uiw/react-codemirror": "^4.23.7", @@ -2994,6 +2996,59 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz", + "integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.62.16", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz", + "integrity": "sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz", + "integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.64.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.64.1.tgz", + "integrity": "sha512-8ajcGE3wXYlb4KuJnkFYkJwJKc/qmPNTpQD7YTgLRMBPTGGp1xk7VMzxL87DoXuweO8luplUUblJJ3noVs/luQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.62.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.64.1", + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-table": { "version": "8.20.6", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz", diff --git a/rsconcept/frontend/package.json b/rsconcept/frontend/package.json index 6e8d36a6..a3343052 100644 --- a/rsconcept/frontend/package.json +++ b/rsconcept/frontend/package.json @@ -14,6 +14,8 @@ "dependencies": { "@dagrejs/dagre": "^1.1.4", "@lezer/lr": "^1.4.2", + "@tanstack/react-query": "^5.64.1", + "@tanstack/react-query-devtools": "^5.64.1", "@tanstack/react-table": "^8.20.6", "@uiw/codemirror-themes": "^4.23.7", "@uiw/react-codemirror": "^4.23.7", diff --git a/rsconcept/frontend/src/app/ApplicationLayout.tsx b/rsconcept/frontend/src/app/ApplicationLayout.tsx index 29f67481..dc506950 100644 --- a/rsconcept/frontend/src/app/ApplicationLayout.tsx +++ b/rsconcept/frontend/src/app/ApplicationLayout.tsx @@ -5,7 +5,7 @@ import ConceptToaster from '@/app/ConceptToaster'; import Footer from '@/app/Footer'; import Navigation from '@/app/Navigation'; import Loader from '@/components/ui/Loader'; -import { NavigationState } from '@/context/NavigationContext'; +import { NavigationState } from '@/app/Navigation/NavigationContext'; import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout'; import { globals } from '@/utils/constants'; @@ -19,6 +19,9 @@ function ApplicationLayout() { const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation); const noNavigation = useAppLayoutStore(state => state.noNavigation); const noFooter = useAppLayoutStore(state => state.noFooter); + + // TODO: prefetch data + return (
diff --git a/rsconcept/frontend/src/app/GlobalDialogs.tsx b/rsconcept/frontend/src/app/GlobalDialogs.tsx index a20ce7db..25560b80 100644 --- a/rsconcept/frontend/src/app/GlobalDialogs.tsx +++ b/rsconcept/frontend/src/app/GlobalDialogs.tsx @@ -1,7 +1,7 @@ 'use client'; import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema'; -import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; +import DlgChangeLocation from '@/pages/OssPage/DlgChangeLocation'; import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem'; import DlgCreateCst from '@/dialogs/DlgCreateCst'; import DlgCreateOperation from '@/dialogs/DlgCreateOperation'; diff --git a/rsconcept/frontend/src/app/GlobalProviders.tsx b/rsconcept/frontend/src/app/GlobalProviders.tsx index d3cf7043..21f05748 100644 --- a/rsconcept/frontend/src/app/GlobalProviders.tsx +++ b/rsconcept/frontend/src/app/GlobalProviders.tsx @@ -1,12 +1,11 @@ 'use client'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ErrorBoundary } from 'react-error-boundary'; import { IntlProvider } from 'react-intl'; -import { AuthState } from '@/context/AuthContext'; -import { GlobalOssState } from '@/context/GlobalOssContext'; -import { LibraryState } from '@/context/LibraryContext'; -import { UsersState } from '@/context/UsersContext'; +import { queryClient } from '@/backend/queryClient'; import ErrorFallback from './ErrorFallback'; @@ -30,18 +29,12 @@ function GlobalProviders({ children }: React.PropsWithChildren) { onError={logError} > - - - - + - + {children} - - - - + ); } diff --git a/rsconcept/frontend/src/app/Navigation/Navigation.tsx b/rsconcept/frontend/src/app/Navigation/Navigation.tsx index 224c1231..b39f15e6 100644 --- a/rsconcept/frontend/src/app/Navigation/Navigation.tsx +++ b/rsconcept/frontend/src/app/Navigation/Navigation.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons'; import { CProps } from '@/components/props'; -import { useConceptNavigation } from '@/context/NavigationContext'; +import { useConceptNavigation } from '@/app/Navigation/NavigationContext'; import useWindowSize from '@/hooks/useWindowSize'; import { useAppLayoutStore } from '@/stores/appLayout'; import { PARAMETER } from '@/utils/constants'; @@ -27,15 +27,16 @@ function Navigation() { return (
diff --git a/rsconcept/frontend/src/app/Navigation/NavigationButton.tsx b/rsconcept/frontend/src/app/Navigation/NavigationButton.tsx index b16970d7..93eab2b8 100644 --- a/rsconcept/frontend/src/app/Navigation/NavigationButton.tsx +++ b/rsconcept/frontend/src/app/Navigation/NavigationButton.tsx @@ -29,7 +29,7 @@ function NavigationButton({ data-tooltip-hidden={hideTitle} onClick={onClick} className={clsx( - 'mr-1 h-full', // prettier: split lines + 'mr-1 h-full', // 'flex items-center gap-1', 'clr-btn-nav cc-animate-color duration-500', 'rounded-xl', diff --git a/rsconcept/frontend/src/context/NavigationContext.tsx b/rsconcept/frontend/src/app/Navigation/NavigationContext.tsx similarity index 100% rename from rsconcept/frontend/src/context/NavigationContext.tsx rename to rsconcept/frontend/src/app/Navigation/NavigationContext.tsx diff --git a/rsconcept/frontend/src/app/Navigation/UserButton.tsx b/rsconcept/frontend/src/app/Navigation/UserButton.tsx new file mode 100644 index 00000000..ff13baf0 --- /dev/null +++ b/rsconcept/frontend/src/app/Navigation/UserButton.tsx @@ -0,0 +1,35 @@ +import { useAuthSuspense } from '@/backend/auth/useAuth'; +import { IconLogin, IconUser2 } from '@/components/Icons'; +import { usePreferencesStore } from '@/stores/preferences'; + +import NavigationButton from './NavigationButton'; + +interface UserButtonProps { + onLogin: () => void; + onClickUser: () => void; +} + +function UserButton({ onLogin, onClickUser }: UserButtonProps) { + const { user } = useAuthSuspense(); + const adminMode = usePreferencesStore(state => state.adminMode); + if (!user) { + return ( + } + onClick={onLogin} + /> + ); + } else { + return ( + } + onClick={onClickUser} + /> + ); + } +} + +export default UserButton; diff --git a/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx b/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx index 4284f515..6924d207 100644 --- a/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx +++ b/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx @@ -1,3 +1,5 @@ +import { useAuth } from '@/backend/auth/useAuth'; +import { useLogout } from '@/backend/auth/useLogout'; import { IconAdmin, IconAdminOff, @@ -15,8 +17,7 @@ import { import { CProps } from '@/components/props'; import Dropdown from '@/components/ui/Dropdown'; import DropdownButton from '@/components/ui/DropdownButton'; -import { useAuth } from '@/context/AuthContext'; -import { useConceptNavigation } from '@/context/NavigationContext'; +import { useConceptNavigation } from '@/app/Navigation/NavigationContext'; import { usePreferencesStore } from '@/stores/preferences'; import { urls } from '../urls'; @@ -28,7 +29,8 @@ interface UserDropdownProps { function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { const router = useConceptNavigation(); - const { user, logout } = useAuth(); + const { user } = useAuth(); + const { logout } = useLogout(); const darkMode = usePreferencesStore(state => state.darkMode); const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode); diff --git a/rsconcept/frontend/src/app/Navigation/UserMenu.tsx b/rsconcept/frontend/src/app/Navigation/UserMenu.tsx index 236336da..f4ea4b79 100644 --- a/rsconcept/frontend/src/app/Navigation/UserMenu.tsx +++ b/rsconcept/frontend/src/app/Navigation/UserMenu.tsx @@ -1,41 +1,22 @@ -import { IconLogin, IconUser2 } from '@/components/Icons'; +import { Suspense } from 'react'; + import Loader from '@/components/ui/Loader'; -import { useAuth } from '@/context/AuthContext'; -import { useConceptNavigation } from '@/context/NavigationContext'; +import { useConceptNavigation } from '@/app/Navigation/NavigationContext'; import useDropdown from '@/hooks/useDropdown'; -import { usePreferencesStore } from '@/stores/preferences'; import { urls } from '../urls'; -import NavigationButton from './NavigationButton'; +import UserButton from './UserButton'; import UserDropdown from './UserDropdown'; function UserMenu() { const router = useConceptNavigation(); - const { user, loading } = useAuth(); - const adminMode = usePreferencesStore(state => state.adminMode); const menu = useDropdown(); - - const navigateLogin = () => router.push(urls.login); - return (
- {loading ? : null} - {!user && !loading ? ( - } - onClick={navigateLogin} - /> - ) : null} - {user && !loading ? ( - } - onClick={menu.toggle} - /> - ) : null} - menu.hide()} /> + }> + router.push(urls.login)} onClickUser={menu.toggle} /> + + menu.hide()} />
); } diff --git a/rsconcept/frontend/src/backend/apiTransport.ts b/rsconcept/frontend/src/backend/apiTransport.ts index 3e29bc54..6916e3c3 100644 --- a/rsconcept/frontend/src/backend/apiTransport.ts +++ b/rsconcept/frontend/src/backend/apiTransport.ts @@ -7,7 +7,7 @@ import { toast } from 'react-toastify'; import { ErrorData } from '@/components/info/InfoError'; import { extractErrorMessage } from '@/utils/utils'; -import { axiosInstance } from './apiConfiguration'; +import { axiosInstance } from './axiosInstance'; // ================ Data transfer types ================ export type DataCallback = (data: ResponseData) => void; @@ -42,17 +42,17 @@ export interface IAxiosRequest { // ================ Transport API calls ================ export function AxiosGet({ endpoint, request, options }: IAxiosRequest) { - if (request.setLoading) request.setLoading(true); + request.setLoading?.(true); axiosInstance .get(endpoint, options) .then(response => { - if (request.setLoading) request.setLoading(false); - if (request.onSuccess) request.onSuccess(response.data); + request.setLoading?.(false); + request.onSuccess?.(response.data); }) .catch((error: Error | AxiosError) => { - if (request.setLoading) request.setLoading(false); + request.setLoading?.(false); if (request.showError) toast.error(extractErrorMessage(error)); - if (request.onError) request.onError(error); + request.onError?.(error); }); } @@ -61,17 +61,17 @@ export function AxiosPost({ request, options }: IAxiosRequest) { - if (request.setLoading) request.setLoading(true); + request.setLoading?.(true); axiosInstance .post(endpoint, request.data, options) .then(response => { - if (request.setLoading) request.setLoading(false); - if (request.onSuccess) request.onSuccess(response.data); + request.setLoading?.(false); + request.onSuccess?.(response.data); }) .catch((error: Error | AxiosError) => { - if (request.setLoading) request.setLoading(false); + request.setLoading?.(false); if (request.showError) toast.error(extractErrorMessage(error)); - if (request.onError) request.onError(error); + request.onError?.(error); }); } @@ -80,17 +80,17 @@ export function AxiosDelete({ request, options }: IAxiosRequest) { - if (request.setLoading) request.setLoading(true); + request.setLoading?.(true); axiosInstance .delete(endpoint, options) .then(response => { - if (request.setLoading) request.setLoading(false); - if (request.onSuccess) request.onSuccess(response.data); + request.setLoading?.(false); + request.onSuccess?.(response.data); }) .catch((error: Error | AxiosError) => { - if (request.setLoading) request.setLoading(false); + request.setLoading?.(false); if (request.showError) toast.error(extractErrorMessage(error)); - if (request.onError) request.onError(error); + request.onError?.(error); }); } @@ -99,17 +99,17 @@ export function AxiosPatch({ request, options }: IAxiosRequest) { - if (request.setLoading) request.setLoading(true); + request.setLoading?.(true); axiosInstance .patch(endpoint, request.data, options) .then(response => { - if (request.setLoading) request.setLoading(false); - if (request.onSuccess) request.onSuccess(response.data); + request.setLoading?.(false); + request.onSuccess?.(response.data); return response.data; }) .catch((error: Error | AxiosError) => { - if (request.setLoading) request.setLoading(false); + request.setLoading?.(false); if (request.showError) toast.error(extractErrorMessage(error)); - if (request.onError) request.onError(error); + request.onError?.(error); }); } diff --git a/rsconcept/frontend/src/backend/auth/api.ts b/rsconcept/frontend/src/backend/auth/api.ts new file mode 100644 index 00000000..340d4e8a --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/api.ts @@ -0,0 +1,71 @@ +import { queryOptions } from '@tanstack/react-query'; + +import { axiosInstance } from '@/backend/axiosInstance'; +import { DELAYS } from '@/backend/configuration'; +import { ICurrentUser } from '@/models/user'; + +/** + * Represents login data, used to authenticate users. + */ +export interface IUserLoginDTO { + username: string; + password: string; +} + +/** + * Represents data needed to update password for current user. + */ +export interface IChangePasswordDTO { + old_password: string; + new_password: string; +} + +/** + * Represents password reset request data. + */ +export interface IRequestPasswordDTO { + email: string; +} + +/** + * Represents password reset data. + */ +export interface IResetPasswordDTO { + password: string; + token: string; +} + +/** + * Represents password token data. + */ +export interface IPasswordTokenDTO { + token: string; +} + +/** + * Authentication API. + */ +export const authApi = { + baseKey: 'auth', + + getAuthQueryOptions: () => { + return queryOptions({ + queryKey: [authApi.baseKey, 'user'], + staleTime: DELAYS.staleLong, + queryFn: meta => + axiosInstance + .get('/users/api/auth', { + signal: meta.signal + }) + .then(response => (response.data.id === null ? null : response.data)), + placeholderData: null + }); + }, + + logout: () => axiosInstance.post('/users/api/logout'), + login: (data: IUserLoginDTO) => axiosInstance.post('/users/api/login', data), + changePassword: (data: IChangePasswordDTO) => axiosInstance.post('/users/api/change-password', data), + requestPasswordReset: (data: IRequestPasswordDTO) => axiosInstance.post('/users/api/password-reset', data), + validatePasswordToken: (data: IPasswordTokenDTO) => axiosInstance.post('/users/api/password-reset/validate', data), + resetPassword: (data: IResetPasswordDTO) => axiosInstance.post('/users/api/password-reset/confirm', data) +}; diff --git a/rsconcept/frontend/src/backend/auth/useAuth.tsx b/rsconcept/frontend/src/backend/auth/useAuth.tsx new file mode 100644 index 00000000..14f86f2e --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useAuth.tsx @@ -0,0 +1,21 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { authApi } from './api'; + +export function useAuth() { + const { + data: user, + isLoading, + error + } = useQuery({ + ...authApi.getAuthQueryOptions() + }); + return { user, isLoading, error }; +} + +export function useAuthSuspense() { + const { data: user } = useSuspenseQuery({ + ...authApi.getAuthQueryOptions() + }); + return { user }; +} diff --git a/rsconcept/frontend/src/backend/auth/useChangePassword.tsx b/rsconcept/frontend/src/backend/auth/useChangePassword.tsx new file mode 100644 index 00000000..39f9af58 --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useChangePassword.tsx @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { authApi, IChangePasswordDTO } from './api'; + +export const useChangePassword = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: ['change-password'], + mutationFn: authApi.changePassword, + onSettled: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] }) + }); + return { + changePassword: ( + data: IChangePasswordDTO, // + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/auth/useLogin.tsx b/rsconcept/frontend/src/backend/auth/useLogin.tsx new file mode 100644 index 00000000..a61beb2a --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useLogin.tsx @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { authApi } from './api'; + +export const useLogin = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: ['login'], + mutationFn: authApi.login, + onSettled: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] }) + }); + return { + login: ( + username: string, // + password: string, + onSuccess?: () => void + ) => mutation.mutate({ username, password }, { onSuccess }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/auth/useLogout.tsx b/rsconcept/frontend/src/backend/auth/useLogout.tsx new file mode 100644 index 00000000..9bb5e3cd --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useLogout.tsx @@ -0,0 +1,13 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { authApi } from './api'; + +export const useLogout = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: ['logout'], + mutationFn: authApi.logout, + onSettled: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] }) + }); + return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) }; +}; diff --git a/rsconcept/frontend/src/backend/auth/useRequestPasswordReset.tsx b/rsconcept/frontend/src/backend/auth/useRequestPasswordReset.tsx new file mode 100644 index 00000000..cd6668a7 --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useRequestPasswordReset.tsx @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; + +import { authApi, IRequestPasswordDTO } from './api'; + +export const useRequestPasswordReset = () => { + const mutation = useMutation({ + mutationKey: ['request-password-reset'], + mutationFn: authApi.requestPasswordReset + }); + return { + requestPasswordReset: ( + data: IRequestPasswordDTO, // + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/auth/useResetPassword.tsx b/rsconcept/frontend/src/backend/auth/useResetPassword.tsx new file mode 100644 index 00000000..17f05316 --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useResetPassword.tsx @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { authApi, IPasswordTokenDTO, IResetPasswordDTO } from './api'; + +export const useResetPassword = () => { + const client = useQueryClient(); + const validateMutation = useMutation({ + mutationKey: ['reset-password'], + mutationFn: authApi.validatePasswordToken, + onSuccess: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] }) + }); + const resetMutation = useMutation({ + mutationKey: ['reset-password'], + mutationFn: authApi.resetPassword, + onSuccess: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] }) + }); + return { + validateToken: ( + data: IPasswordTokenDTO, // + onSuccess?: () => void + ) => validateMutation.mutate(data, { onSuccess }), + resetPassword: ( + data: IResetPasswordDTO, // + onSuccess?: () => void + ) => resetMutation.mutate(data, { onSuccess }), + isPending: resetMutation.isPending, + error: resetMutation.error, + reset: resetMutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/apiConfiguration.ts b/rsconcept/frontend/src/backend/axiosInstance.ts similarity index 100% rename from rsconcept/frontend/src/backend/apiConfiguration.ts rename to rsconcept/frontend/src/backend/axiosInstance.ts diff --git a/rsconcept/frontend/src/backend/cctext.ts b/rsconcept/frontend/src/backend/cctext.ts deleted file mode 100644 index 46b064ca..00000000 --- a/rsconcept/frontend/src/backend/cctext.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Endpoints: cctext. - */ - -import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language'; - -import { AxiosPost, FrontExchange } from './apiTransport'; - -export function postInflectText(request: FrontExchange) { - AxiosPost({ - endpoint: `/api/cctext/inflect`, - request: request - }); -} - -export function postParseText(request: FrontExchange) { - AxiosPost({ - endpoint: `/api/cctext/parse`, - request: request - }); -} - -export function postGenerateLexeme(request: FrontExchange) { - AxiosPost({ - endpoint: `/api/cctext/generate-lexeme`, - request: request - }); -} diff --git a/rsconcept/frontend/src/backend/cctext/api.ts b/rsconcept/frontend/src/backend/cctext/api.ts new file mode 100644 index 00000000..b968ed91 --- /dev/null +++ b/rsconcept/frontend/src/backend/cctext/api.ts @@ -0,0 +1,26 @@ +import { axiosInstance } from '@/backend/axiosInstance'; +import { ILexemeData, IWordFormPlain } from '@/models/language'; + +/** + * Represents API result for text output. + */ +export interface ITextResult { + result: string; +} + +export const cctextApi = { + baseKey: 'cctext', + + inflectText: (data: IWordFormPlain) => + axiosInstance // + .post('/api/cctext/inflect', data) + .then(response => response.data), + parseText: (data: { text: string }) => + axiosInstance // + .post('/api/cctext/parse', data) + .then(response => response.data), + generateLexeme: (data: { text: string }) => + axiosInstance // + .post('/api/cctext/generate-lexeme', data) + .then(response => response.data) +}; diff --git a/rsconcept/frontend/src/backend/cctext/useGenerateLexeme.tsx b/rsconcept/frontend/src/backend/cctext/useGenerateLexeme.tsx new file mode 100644 index 00000000..e04f01f9 --- /dev/null +++ b/rsconcept/frontend/src/backend/cctext/useGenerateLexeme.tsx @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; + +import { ILexemeData } from '@/models/language'; + +import { DataCallback } from '../apiTransport'; +import { cctextApi } from './api'; + +export const useGenerateLexeme = () => { + const mutation = useMutation({ + mutationKey: [cctextApi.baseKey, 'generate-lexeme'], + mutationFn: cctextApi.generateLexeme + }); + return { + generateLexeme: ( + data: { text: string }, // + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/cctext/useInflectText.tsx b/rsconcept/frontend/src/backend/cctext/useInflectText.tsx new file mode 100644 index 00000000..73a4dfb6 --- /dev/null +++ b/rsconcept/frontend/src/backend/cctext/useInflectText.tsx @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; + +import { IWordFormPlain } from '@/models/language'; + +import { DataCallback } from '../apiTransport'; +import { cctextApi, ITextResult } from './api'; + +export const useInflectText = () => { + const mutation = useMutation({ + mutationKey: [cctextApi.baseKey, 'inflect-text'], + mutationFn: cctextApi.inflectText + }); + return { + inflectText: ( + data: IWordFormPlain, // + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/cctext/useIsProcessingCctext.tsx b/rsconcept/frontend/src/backend/cctext/useIsProcessingCctext.tsx new file mode 100644 index 00000000..401a032e --- /dev/null +++ b/rsconcept/frontend/src/backend/cctext/useIsProcessingCctext.tsx @@ -0,0 +1,8 @@ +import { useIsMutating } from '@tanstack/react-query'; + +import { cctextApi } from './api'; + +export const useIsProcessingCctext = () => { + const countMutations = useIsMutating({ mutationKey: [cctextApi.baseKey] }); + return countMutations !== 0; +}; diff --git a/rsconcept/frontend/src/backend/cctext/useParseText.tsx b/rsconcept/frontend/src/backend/cctext/useParseText.tsx new file mode 100644 index 00000000..463878a2 --- /dev/null +++ b/rsconcept/frontend/src/backend/cctext/useParseText.tsx @@ -0,0 +1,17 @@ +import { useMutation } from '@tanstack/react-query'; + +import { DataCallback } from '../apiTransport'; +import { cctextApi, ITextResult } from './api'; + +export const useParseText = () => { + const mutation = useMutation({ + mutationKey: [cctextApi.baseKey, 'parse-text'], + mutationFn: cctextApi.parseText + }); + return { + parseText: ( + data: { text: string }, // + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/configuration.ts b/rsconcept/frontend/src/backend/configuration.ts new file mode 100644 index 00000000..fdb5d8a9 --- /dev/null +++ b/rsconcept/frontend/src/backend/configuration.ts @@ -0,0 +1,9 @@ +/** Timing constants for API requests. */ +export const DELAYS = { + garbageCollection: 1 * 60 * 60 * 1000, + staleDefault: 5 * 60 * 1000, + + staleShort: 5 * 60 * 1000, + staleMedium: 1 * 60 * 60 * 1000, + staleLong: 24 * 60 * 60 * 1000 +}; diff --git a/rsconcept/frontend/src/backend/library.ts b/rsconcept/frontend/src/backend/library.ts deleted file mode 100644 index a8990727..00000000 --- a/rsconcept/frontend/src/backend/library.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Endpoints: library. - */ - -import { - ILibraryCreateData, - ILibraryItem, - ILibraryUpdateData, - IRenameLocationData, - ITargetAccessPolicy, - ITargetLocation, - IVersionCreateData -} from '@/models/library'; -import { IRSFormCloneData, IRSFormData, IVersionCreatedResponse } from '@/models/rsform'; -import { ITargetUser, ITargetUsers } from '@/models/user'; - -import { - AxiosDelete, - AxiosGet, - AxiosPatch, - AxiosPost, - FrontAction, - FrontExchange, - FrontPull, - FrontPush -} from './apiTransport'; - -export function getLibrary(request: FrontPull) { - AxiosGet({ - endpoint: '/api/library/active', - request: request - }); -} - -export function getAdminLibrary(request: FrontPull) { - AxiosGet({ - endpoint: '/api/library/all', - request: request - }); -} - -export function getTemplates(request: FrontPull) { - AxiosGet({ - endpoint: '/api/library/templates', - request: request - }); -} - -export function postCreateLibraryItem(request: FrontExchange) { - AxiosPost({ - endpoint: '/api/library', - request: request - }); -} - -export function postCloneLibraryItem(target: string, request: FrontExchange) { - AxiosPost({ - endpoint: `/api/library/${target}/clone`, - request: request - }); -} - -export function patchLibraryItem(target: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/library/${target}`, - request: request - }); -} - -export function deleteLibraryItem(target: string, request: FrontAction) { - AxiosDelete({ - endpoint: `/api/library/${target}`, - request: request - }); -} - -export function patchSetOwner(target: string, request: FrontPush) { - AxiosPatch({ - endpoint: `/api/library/${target}/set-owner`, - request: request - }); -} - -export function patchSetAccessPolicy(target: string, request: FrontPush) { - AxiosPatch({ - endpoint: `/api/library/${target}/set-access-policy`, - request: request - }); -} - -export function patchSetLocation(target: string, request: FrontPush) { - AxiosPatch({ - endpoint: `/api/library/${target}/set-location`, - request: request - }); -} - -export function patchRenameLocation(request: FrontPush) { - AxiosPatch({ - endpoint: `/api/library/rename-location`, - request: request - }); -} - -export function patchSetEditors(target: string, request: FrontPush) { - AxiosPatch({ - endpoint: `/api/library/${target}/set-editors`, - request: request - }); -} - -export function postCreateVersion(target: string, request: FrontExchange) { - AxiosPost({ - endpoint: `/api/library/${target}/create-version`, - request: request - }); -} diff --git a/rsconcept/frontend/src/backend/library/api.ts b/rsconcept/frontend/src/backend/library/api.ts new file mode 100644 index 00000000..79f5b8d2 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/api.ts @@ -0,0 +1,145 @@ +import { queryOptions } from '@tanstack/react-query'; + +import { axiosInstance } from '@/backend/axiosInstance'; +import { DELAYS } from '@/backend/configuration'; +import { AccessPolicy, ILibraryItem, IVersionData, LibraryItemID, LibraryItemType, VersionID } from '@/models/library'; +import { ConstituentaID, IRSFormData } from '@/models/rsform'; +import { UserID } from '@/models/user'; + +import { ossApi } from '../oss/api'; +import { rsformsApi } from '../rsform/api'; + +/** + * Represents update data for renaming Location. + */ +export interface IRenameLocationDTO { + target: string; + new_location: string; +} + +/** + * Represents data, used for cloning {@link IRSForm}. + */ +export interface IRSFormCloneDTO extends Omit { + items?: ConstituentaID[]; +} + +/** + * Represents data, used for creating {@link IRSForm}. + */ +export interface ILibraryCreateDTO extends Omit { + file?: File; + fileName?: string; +} + +/** + * Represents update data for editing {@link ILibraryItem}. + */ +export interface ILibraryUpdateDTO + extends Omit {} + +/** + * Create version metadata in persistent storage. + */ +export interface IVersionCreateDTO { + version: string; + description: string; + items?: ConstituentaID[]; +} + +/** + * Represents data response when creating {@link IVersionInfo}. + */ +export interface IVersionCreatedResponse { + version: number; + schema: IRSFormData; +} + +export const libraryApi = { + baseKey: 'library', + libraryListKey: ['library', 'list'], + + getLibraryQueryOptions: ({ isAdmin }: { isAdmin: boolean }) => + queryOptions({ + queryKey: libraryApi.libraryListKey, + staleTime: DELAYS.staleMedium, + queryFn: meta => + axiosInstance + .get(isAdmin ? '/api/library/all' : '/api/library/active', { + signal: meta.signal + }) + .then(response => response.data) + }), + getItemQueryOptions: ({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) => { + return itemType === LibraryItemType.RSFORM + ? rsformsApi.getRSFormQueryOptions({ itemID }) + : ossApi.getOssQueryOptions({ itemID }); + }, + getTemplatesQueryOptions: () => + queryOptions({ + queryKey: [libraryApi.baseKey, 'templates'], + staleTime: DELAYS.staleMedium, + queryFn: meta => + axiosInstance + .get('/api/library/templates', { + signal: meta.signal + }) + .then(response => response.data) + }), + + createItem: (data: ILibraryCreateDTO) => + data.file + ? axiosInstance + .post('/api/rsforms/create-detailed', data, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + .then(response => response.data) + : axiosInstance // + .post('/api/library', data) + .then(response => response.data), + + updateItem: (data: ILibraryUpdateDTO) => + axiosInstance // + .patch(`/api/library/${data.id}`, data) + .then(response => response.data), + setOwner: (data: { itemID: LibraryItemID; owner: UserID }) => + axiosInstance // + .patch(`/api/library/${data.itemID}/set-owner`, { user: data.owner }), + setLocation: (data: { itemID: LibraryItemID; location: string }) => + axiosInstance // + .patch(`/api/library/${data.itemID}/set-location`, { location: data.location }), + setAccessPolicy: (data: { itemID: LibraryItemID; policy: AccessPolicy }) => + axiosInstance // + .patch(`/api/library/${data.itemID}/set-access-policy`, { access_policy: data.policy }), + setEditors: (data: { itemID: LibraryItemID; editors: UserID[] }) => + axiosInstance // + .patch(`/api/library/${data.itemID}/set-editors`, { users: data.editors }), + + deleteItem: (target: LibraryItemID) => + axiosInstance // + .delete(`/api/library/${target}`), + cloneItem: (data: IRSFormCloneDTO) => + axiosInstance // + .post(`/api/library/${data.id}/clone`, data) + .then(response => response.data), + renameLocation: (data: IRenameLocationDTO) => + axiosInstance // + .patch('/api/library/rename-location', data), + + versionCreate: (data: { itemID: LibraryItemID; data: IVersionData }) => + axiosInstance // + .post(`/api/library/${data.itemID}/versions`, data.data) + .then(response => response.data), + versionRestore: (data: { itemID: LibraryItemID; versionID: VersionID }) => + axiosInstance // + .patch(`/api/versions/${data.versionID}/restore`) + .then(response => response.data), + versionUpdate: (data: { itemID: LibraryItemID; versionID: VersionID; data: IVersionData }) => + axiosInstance // + .patch(`/api/versions/${data.versionID}`, data.data), + versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) => + axiosInstance // + .delete(`/api/versions/${data.versionID}`) +}; diff --git a/rsconcept/frontend/src/backend/library/useApplyLibraryFilter.tsx b/rsconcept/frontend/src/backend/library/useApplyLibraryFilter.tsx new file mode 100644 index 00000000..4281e2ef --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useApplyLibraryFilter.tsx @@ -0,0 +1,47 @@ +import { useAuth } from '@/backend/auth/useAuth'; +import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI'; +import { ILibraryFilter } from '@/models/miscellaneous'; + +import { useLibrary } from './useLibrary'; + +export function useApplyLibraryFilter(filter: ILibraryFilter) { + const { items } = useLibrary(); + const { user } = useAuth(); + + let result = items; + if (!filter.folderMode && filter.head) { + result = result.filter(item => item.location.startsWith(filter.head!)); + } + if (filter.folderMode && filter.location) { + if (filter.subfolders) { + result = result.filter( + item => item.location == filter.location || item.location.startsWith(filter.location! + '/') + ); + } else { + result = result.filter(item => item.location == filter.location); + } + } + if (filter.type) { + result = result.filter(item => item.item_type === filter.type); + } + if (filter.isVisible !== undefined) { + result = result.filter(item => filter.isVisible === item.visible); + } + if (filter.isOwned !== undefined) { + result = result.filter(item => filter.isOwned === (item.owner === user?.id)); + } + if (filter.isEditor !== undefined) { + result = result.filter(item => filter.isEditor == user?.editor.includes(item.id)); + } + if (filter.filterUser !== undefined) { + result = result.filter(item => filter.filterUser === item.owner); + } + if (!filter.folderMode && filter.path) { + result = result.filter(item => matchLibraryItemLocation(item, filter.path!)); + } + if (filter.query) { + result = result.filter(item => matchLibraryItem(item, filter.query!)); + } + + return { filtered: result }; +} diff --git a/rsconcept/frontend/src/backend/library/useCloneItem.tsx b/rsconcept/frontend/src/backend/library/useCloneItem.tsx new file mode 100644 index 00000000..0609a435 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useCloneItem.tsx @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { IRSFormData } from '@/models/rsform'; + +import { IRSFormCloneDTO, libraryApi } from './api'; + +export const useCloneItem = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'clone-item'], + mutationFn: libraryApi.cloneItem, + onSuccess: async () => await client.invalidateQueries({ queryKey: [libraryApi.baseKey] }) + }); + return { + cloneItem: ( + data: IRSFormCloneDTO, // + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useCreateItem.tsx b/rsconcept/frontend/src/backend/library/useCreateItem.tsx new file mode 100644 index 00000000..a61c2a88 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useCreateItem.tsx @@ -0,0 +1,24 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { ILibraryItem } from '@/models/library'; + +import { ILibraryCreateDTO, libraryApi } from './api'; + +export const useCreateItem = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'create-item'], + mutationFn: libraryApi.createItem, + onSuccess: () => client.invalidateQueries({ queryKey: [libraryApi.baseKey] }) + }); + return { + createItem: ( + data: ILibraryCreateDTO, // + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useDeleteItem.tsx b/rsconcept/frontend/src/backend/library/useDeleteItem.tsx new file mode 100644 index 00000000..b44e6f6b --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useDeleteItem.tsx @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { ILibraryItem, LibraryItemID } from '@/models/library'; + +import { libraryApi } from './api'; + +export const useDeleteItem = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'delete-item'], + mutationFn: libraryApi.deleteItem, + onSuccess: async (_, variables) => { + await client.cancelQueries({ queryKey: [libraryApi.libraryListKey] }); + client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => + prev?.filter(item => item.id !== variables) + ); + } + }); + return { + deleteItem: ( + target: LibraryItemID, // + onSuccess?: () => void + ) => mutation.mutate(target, { onSuccess }), + isPending: mutation.isPending + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useFolders.tsx b/rsconcept/frontend/src/backend/library/useFolders.tsx new file mode 100644 index 00000000..d793a111 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useFolders.tsx @@ -0,0 +1,17 @@ +import { FolderTree } from '@/models/FolderTree'; +import { LocationHead } from '@/models/library'; + +import { useLibrary } from './useLibrary'; + +export function useFolders() { + const { items } = useLibrary(); + + const result = new FolderTree(); + result.addPath(LocationHead.USER, 0); + result.addPath(LocationHead.COMMON, 0); + result.addPath(LocationHead.LIBRARY, 0); + result.addPath(LocationHead.PROJECTS, 0); + items.forEach(item => result.addPath(item.location)); + + return { folders: result }; +} diff --git a/rsconcept/frontend/src/backend/library/useIsProcessingLibrary.tsx b/rsconcept/frontend/src/backend/library/useIsProcessingLibrary.tsx new file mode 100644 index 00000000..d906cc9b --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useIsProcessingLibrary.tsx @@ -0,0 +1,12 @@ +import { useIsMutating } from '@tanstack/react-query'; + +import { ossApi } from '../oss/api'; +import { rsformsApi } from '../rsform/api'; +import { libraryApi } from './api'; + +export const useIsProcessingLibrary = () => { + const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] }); + const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] }); + const countRSForm = useIsMutating({ mutationKey: [rsformsApi.baseKey] }); + return countMutations + countOss + countRSForm !== 0; +}; diff --git a/rsconcept/frontend/src/backend/library/useLibrary.tsx b/rsconcept/frontend/src/backend/library/useLibrary.tsx new file mode 100644 index 00000000..6fb112e7 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useLibrary.tsx @@ -0,0 +1,21 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { useAuthSuspense } from '@/backend/auth/useAuth'; +import { libraryApi } from './api'; + +export function useLibrarySuspense() { + const { user } = useAuthSuspense(); + const { data: items } = useSuspenseQuery({ + ...libraryApi.getLibraryQueryOptions({ isAdmin: user?.is_staff ?? false }) + }); + return { items }; +} + +export function useLibrary() { + // NOTE: Using suspense here to avoid duplicated library data requests + const { user } = useAuthSuspense(); + const { data: items, isLoading } = useQuery({ + ...libraryApi.getLibraryQueryOptions({ isAdmin: user?.is_staff ?? false }) + }); + return { items: items ?? [], isLoading }; +} diff --git a/rsconcept/frontend/src/backend/library/useLibraryItem.tsx b/rsconcept/frontend/src/backend/library/useLibraryItem.tsx new file mode 100644 index 00000000..340a973d --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useLibraryItem.tsx @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; + +import { ILibraryItemVersioned, LibraryItemID, LibraryItemType } from '@/models/library'; + +import { ossApi } from '../oss/api'; +import { rsformsApi } from '../rsform/api'; + +export function useLibraryItem({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) { + const { data: rsForm } = useQuery({ + ...rsformsApi.getRSFormQueryOptions({ itemID }), + enabled: itemType === LibraryItemType.RSFORM + }); + const { data: oss } = useQuery({ + ...ossApi.getOssQueryOptions({ itemID }), + enabled: itemType === LibraryItemType.OSS + }); + return { + item: + itemType === LibraryItemType.RSFORM + ? (rsForm as ILibraryItemVersioned | undefined) + : (oss as ILibraryItemVersioned | undefined) + }; +} diff --git a/rsconcept/frontend/src/backend/library/useRenameLocation.tsx b/rsconcept/frontend/src/backend/library/useRenameLocation.tsx new file mode 100644 index 00000000..0767c7e8 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useRenameLocation.tsx @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { IRenameLocationDTO, libraryApi } from './api'; + +export const useRenameLocation = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'rename-location'], + mutationFn: libraryApi.renameLocation, + onSuccess: () => client.invalidateQueries({ queryKey: [libraryApi.baseKey] }) + }); + return { + renameLocation: ( + data: IRenameLocationDTO, // + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useSetAccessPolicy.tsx b/rsconcept/frontend/src/backend/library/useSetAccessPolicy.tsx new file mode 100644 index 00000000..6d26c1a0 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useSetAccessPolicy.tsx @@ -0,0 +1,41 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { rsformsApi } from '@/backend/rsform/api'; +import { AccessPolicy, ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; + +import { libraryApi } from './api'; + +export const useSetAccessPolicy = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'set-location'], + mutationFn: libraryApi.setAccessPolicy, + onSuccess: (_, variables) => { + client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => + prev?.map(item => (item.id === variables.itemID ? { ...item, access_policy: variables.policy } : item)) + ); + client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey, prev => { + if (!prev) { + return undefined; + } + if (prev.item_type === LibraryItemType.OSS) { + client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }).catch(console.error); + } + return { + ...prev, + access_policy: variables.policy + }; + }); + } + }); + + return { + setAccessPolicy: ( + data: { + itemID: LibraryItemID; // + policy: AccessPolicy; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useSetEditors.tsx b/rsconcept/frontend/src/backend/library/useSetEditors.tsx new file mode 100644 index 00000000..89b066f1 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useSetEditors.tsx @@ -0,0 +1,39 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { rsformsApi } from '@/backend/rsform/api'; +import { ILibraryItem, LibraryItemID } from '@/models/library'; +import { UserID } from '@/models/user'; + +import { libraryApi } from './api'; + +export const useSetEditors = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'set-location'], + mutationFn: libraryApi.setEditors, + onSuccess: (_, variables) => { + client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => + prev?.map(item => (item.id === variables.itemID ? { ...item, editors: variables.editors } : item)) + ); + client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey, prev => { + if (!prev) { + return undefined; + } + return { + ...prev, + editors: variables.editors + }; + }); + } + }); + + return { + setEditors: ( + data: { + itemID: LibraryItemID; // + editors: UserID[]; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useSetLocation.tsx b/rsconcept/frontend/src/backend/library/useSetLocation.tsx new file mode 100644 index 00000000..ae143d49 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useSetLocation.tsx @@ -0,0 +1,41 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { rsformsApi } from '@/backend/rsform/api'; +import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; + +import { libraryApi } from './api'; + +export const useSetLocation = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'set-location'], + mutationFn: libraryApi.setLocation, + onSuccess: (_, variables) => { + client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => + prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item)) + ); + client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey, prev => { + if (!prev) { + return undefined; + } + if (prev.item_type === LibraryItemType.OSS) { + client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }).catch(console.error); + } + return { + ...prev, + location: variables.location + }; + }); + } + }); + + return { + setLocation: ( + data: { + itemID: LibraryItemID; // + location: string; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useSetOwner.tsx b/rsconcept/frontend/src/backend/library/useSetOwner.tsx new file mode 100644 index 00000000..96dbcc89 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useSetOwner.tsx @@ -0,0 +1,42 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { rsformsApi } from '@/backend/rsform/api'; +import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; +import { UserID } from '@/models/user'; + +import { libraryApi } from './api'; + +export const useSetOwner = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'set-owner'], + mutationFn: libraryApi.setOwner, + onSuccess: (_, variables) => { + client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => + prev?.map(item => (item.id === variables.itemID ? { ...item, owner: variables.owner } : item)) + ); + client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey, prev => { + if (!prev) { + return undefined; + } + if (prev.item_type === LibraryItemType.OSS) { + client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }).catch(console.error); + } + return { + ...prev, + owner: variables.owner + }; + }); + } + }); + + return { + setOwner: ( + data: { + itemID: LibraryItemID; // + owner: UserID; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useTemplates.tsx b/rsconcept/frontend/src/backend/library/useTemplates.tsx new file mode 100644 index 00000000..3af2c235 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useTemplates.tsx @@ -0,0 +1,17 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { libraryApi } from './api'; + +export function useTemplatesSuspense() { + const { data: templates } = useSuspenseQuery({ + ...libraryApi.getTemplatesQueryOptions() + }); + return { templates }; +} + +export function useTemplates() { + const { data: templates } = useQuery({ + ...libraryApi.getTemplatesQueryOptions() + }); + return { templates: templates ?? [] }; +} diff --git a/rsconcept/frontend/src/backend/library/useUpdateItem.tsx b/rsconcept/frontend/src/backend/library/useUpdateItem.tsx new file mode 100644 index 00000000..53e0d80b --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useUpdateItem.tsx @@ -0,0 +1,34 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { rsformsApi } from '@/backend/rsform/api'; +import { ILibraryItem } from '@/models/library'; + +import { ILibraryUpdateDTO, libraryApi } from './api'; + +export const useUpdateItem = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'update-item'], + mutationFn: libraryApi.updateItem, + onSuccess: (data: ILibraryItem) => { + client + .cancelQueries({ queryKey: [libraryApi.libraryListKey] }) + .then(async () => { + client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => + prev?.map(item => (item.id === data.id ? data : item)) + ); + await client.invalidateQueries({ + queryKey: [rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey] + }); + }) + .catch(console.error); + } + }); + return { + updateItem: ( + data: ILibraryUpdateDTO, // + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useUpdateTimestamp.tsx b/rsconcept/frontend/src/backend/library/useUpdateTimestamp.tsx new file mode 100644 index 00000000..1e82548c --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useUpdateTimestamp.tsx @@ -0,0 +1,17 @@ +import { useQueryClient } from '@tanstack/react-query'; + +import { ILibraryItem, LibraryItemID } from '@/models/library'; + +import { libraryApi } from './api'; + +export function useUpdateTimestamp() { + const client = useQueryClient(); + return { + updateTimestamp: (target: LibraryItemID) => + client.setQueryData( + libraryApi.libraryListKey, // + (prev: ILibraryItem[] | undefined) => + prev?.map(item => (item.id === target ? { ...item, time_update: Date() } : item)) + ) + }; +} diff --git a/rsconcept/frontend/src/backend/library/useVersionCreate.tsx b/rsconcept/frontend/src/backend/library/useVersionCreate.tsx new file mode 100644 index 00000000..94bfee8a --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useVersionCreate.tsx @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { rsformsApi } from '@/backend/rsform/api'; +import { IVersionData, LibraryItemID } from '@/models/library'; + +import { libraryApi } from './api'; + +export const useVersionCreate = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'create-version'], + mutationFn: libraryApi.versionCreate, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey], data.schema); + updateTimestamp(data.schema.id); + } + }); + return { + versionCreate: ( + data: { + itemID: LibraryItemID; // + data: IVersionData; + }, + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess: () => onSuccess?.(data.data) }) + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useVersionDelete.tsx b/rsconcept/frontend/src/backend/library/useVersionDelete.tsx new file mode 100644 index 00000000..e13de4f0 --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useVersionDelete.tsx @@ -0,0 +1,33 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { rsformsApi } from '@/backend/rsform/api'; +import { LibraryItemID, VersionID } from '@/models/library'; +import { IRSFormData } from '@/models/rsform'; + +import { libraryApi } from './api'; + +export const useVersionDelete = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'delete-version'], + mutationFn: libraryApi.versionDelete, + onSuccess: (_, variables) => { + client.setQueryData( + [rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey], + (prev: IRSFormData) => ({ + ...prev, + versions: prev.versions.filter(version => version.id !== variables.versionID) + }) + ); + } + }); + return { + versionDelete: ( + data: { + itemID: LibraryItemID; // + versionID: VersionID; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useVersionRestore.tsx b/rsconcept/frontend/src/backend/library/useVersionRestore.tsx new file mode 100644 index 00000000..e5cf8aeb --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useVersionRestore.tsx @@ -0,0 +1,29 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { rsformsApi } from '@/backend/rsform/api'; +import { LibraryItemID, VersionID } from '@/models/library'; + +import { libraryApi } from './api'; + +export const useVersionRestore = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'restore-version'], + mutationFn: libraryApi.versionRestore, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data); + updateTimestamp(data.id); + } + }); + return { + versionRestore: ( + data: { + itemID: LibraryItemID; // + versionID: VersionID; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/library/useVersionUpdate.tsx b/rsconcept/frontend/src/backend/library/useVersionUpdate.tsx new file mode 100644 index 00000000..c6d8d8ec --- /dev/null +++ b/rsconcept/frontend/src/backend/library/useVersionUpdate.tsx @@ -0,0 +1,38 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { rsformsApi } from '@/backend/rsform/api'; +import { IVersionData, LibraryItemID, VersionID } from '@/models/library'; +import { IRSFormData } from '@/models/rsform'; + +import { libraryApi } from './api'; + +export const useVersionUpdate = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [libraryApi.baseKey, 'update-version'], + mutationFn: libraryApi.versionUpdate, + onSuccess: (_, variables) => { + client.setQueryData( + [rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey], + (prev: IRSFormData) => ({ + ...prev, + versions: prev.versions.map(version => + version.id === variables.versionID + ? { ...version, description: variables.data.description, version: variables.data.version } + : version + ) + }) + ); + } + }); + return { + versionUpdate: ( + data: { + itemID: LibraryItemID; // + versionID: VersionID; + data: IVersionData; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/oss.ts b/rsconcept/frontend/src/backend/oss.ts deleted file mode 100644 index a9f63858..00000000 --- a/rsconcept/frontend/src/backend/oss.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Endpoints: oss. - */ - -import { - ICstRelocateData, - IInputCreatedResponse, - IOperationCreateData, - IOperationCreatedResponse, - IOperationDeleteData, - IOperationSchemaData, - IOperationSetInputData, - IOperationUpdateData, - IPositionsData, - ITargetOperation -} from '@/models/oss'; -import { IConstituentaReference, ITargetCst } from '@/models/rsform'; - -import { AxiosGet, AxiosPatch, AxiosPost, FrontExchange, FrontPull, FrontPush } from './apiTransport'; - -export function getOssDetails(target: string, request: FrontPull) { - AxiosGet({ - endpoint: `/api/oss/${target}/details`, - request: request - }); -} - -export function patchUpdatePositions(oss: string, request: FrontPush) { - AxiosPatch({ - endpoint: `/api/oss/${oss}/update-positions`, - request: request - }); -} - -export function postCreateOperation( - oss: string, - request: FrontExchange -) { - AxiosPost({ - endpoint: `/api/oss/${oss}/create-operation`, - request: request - }); -} - -export function patchDeleteOperation(oss: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/oss/${oss}/delete-operation`, - request: request - }); -} - -export function patchCreateInput(oss: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/oss/${oss}/create-input`, - request: request - }); -} - -export function patchSetInput(oss: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/oss/${oss}/set-input`, - request: request - }); -} - -export function patchUpdateOperation(oss: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/oss/${oss}/update-operation`, - request: request - }); -} - -export function postExecuteOperation(oss: string, request: FrontExchange) { - AxiosPost({ - endpoint: `/api/oss/${oss}/execute-operation`, - request: request - }); -} - -export function postRelocateConstituents(request: FrontPush) { - AxiosPost({ - endpoint: `/api/oss/relocate-constituents`, - request: request - }); -} - -export function postFindPredecessor(request: FrontExchange) { - AxiosPost({ - endpoint: `/api/oss/get-predecessor`, - request: request - }); -} diff --git a/rsconcept/frontend/src/backend/oss/api.ts b/rsconcept/frontend/src/backend/oss/api.ts new file mode 100644 index 00000000..7e2820fb --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/api.ts @@ -0,0 +1,148 @@ +import { queryOptions } from '@tanstack/react-query'; + +import { axiosInstance } from '@/backend/axiosInstance'; +import { DELAYS } from '@/backend/configuration'; +import { ILibraryItem, LibraryItemID } from '@/models/library'; +import { + ICstSubstitute, + IOperationData, + IOperationPosition, + IOperationSchemaData, + OperationID, + OperationType +} from '@/models/oss'; +import { ConstituentaID, IConstituentaReference, ITargetCst } from '@/models/rsform'; + +/** + * Represents {@link IOperation} data, used in creation process. + */ +export interface IOperationCreateDTO { + positions: IOperationPosition[]; + item_data: { + alias: string; + operation_type: OperationType; + title: string; + comment: string; + position_x: number; + position_y: number; + result: LibraryItemID | null; + }; + arguments: OperationID[] | undefined; + create_schema: boolean; +} + +/** + * Represents data response when creating {@link IOperation}. + */ +export interface IOperationCreatedResponse { + new_operation: IOperationData; + oss: IOperationSchemaData; +} + +/** + * Represents target {@link IOperation}. + */ +export interface ITargetOperation { + positions: IOperationPosition[]; + target: OperationID; +} + +/** + * Represents {@link IOperation} data, used in destruction process. + */ +export interface IOperationDeleteDTO extends ITargetOperation { + keep_constituents: boolean; + delete_schema: boolean; +} + +/** + * Represents data response when creating {@link IRSForm} for Input {@link IOperation}. + */ +export interface IInputCreatedResponse { + new_schema: ILibraryItem; + oss: IOperationSchemaData; +} + +/** + * Represents {@link IOperation} data, used in setInput process. + */ +export interface IInputUpdateDTO extends ITargetOperation { + input: LibraryItemID | null; +} + +/** + * Represents {@link IOperation} data, used in update process. + */ +export interface IOperationUpdateDTO extends ITargetOperation { + item_data: { + alias: string; + title: string; + comment: string; + }; + arguments: OperationID[] | undefined; + substitutions: ICstSubstitute[] | undefined; +} + +/** + * Represents data, used relocating {@link IConstituenta}s between {@link ILibraryItem}s. + */ +export interface ICstRelocateDTO { + destination: LibraryItemID; + items: ConstituentaID[]; +} + +export const ossApi = { + baseKey: 'oss', + + getOssQueryOptions: ({ itemID }: { itemID?: LibraryItemID }) => { + return queryOptions({ + queryKey: [ossApi.baseKey, 'item', itemID], + staleTime: DELAYS.staleShort, + queryFn: meta => + !itemID + ? undefined + : axiosInstance + .get(`/api/oss/${itemID}/details`, { + signal: meta.signal + }) + .then(response => response.data) + }); + }, + + updatePositions: (data: { itemID: LibraryItemID; positions: IOperationPosition[] }) => + axiosInstance // + .patch(`/api/oss/${data.itemID}/update-positions`, { positions: data.positions }), + operationCreate: (data: { itemID: LibraryItemID; data: IOperationCreateDTO }) => + axiosInstance // + .post(`/api/oss/${data.itemID}/create-operation`, data.data) + .then(response => response.data), + operationDelete: (data: { itemID: LibraryItemID; data: IOperationDeleteDTO }) => + axiosInstance // + .patch(`/api/oss/${data.itemID}/delete-operation`, data.data) + .then(response => response.data), + inputCreate: (data: { itemID: LibraryItemID; data: ITargetOperation }) => + axiosInstance // + .patch(`/api/oss/${data.itemID}/create-input`, data.data) + .then(response => response.data), + inputUpdate: (data: { itemID: LibraryItemID; data: IInputUpdateDTO }) => + axiosInstance // + .patch(`/api/oss/${data.itemID}/set-input`, data.data) + .then(response => response.data), + operationUpdate: (data: { itemID: LibraryItemID; data: IOperationUpdateDTO }) => + axiosInstance // + .patch(`/api/oss/${data.itemID}/update-operation`, data.data) + .then(response => response.data), + operationExecute: (data: { itemID: LibraryItemID; data: ITargetOperation }) => + axiosInstance // + .post(`/api/oss/${data.itemID}/execute-operation`, data.data) + .then(response => response.data), + + relocateConstituents: (data: { itemID: LibraryItemID; data: ICstRelocateDTO }) => + axiosInstance // + .post(`/api/oss/${data.itemID}/relocate-constituents`, data.data) + .then(response => response.data), + getPredecessor: (data: ITargetCst) => + axiosInstance // + .post(`/api/oss/get-predecessor`, data) + .then(response => response.data) +}; diff --git a/rsconcept/frontend/src/backend/oss/useFindPredecessor.tsx b/rsconcept/frontend/src/backend/oss/useFindPredecessor.tsx new file mode 100644 index 00000000..7e5527f5 --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useFindPredecessor.tsx @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { IConstituentaReference, ITargetCst } from '@/models/rsform'; + +import { ossApi } from './api'; + +export const useFindPredecessor = () => { + const mutation = useMutation({ + mutationKey: [ossApi.baseKey, 'find-predecessor'], + mutationFn: ossApi.getPredecessor + }); + return { + findPredecessor: ( + data: ITargetCst, // + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/oss/useInputCreate.tsx b/rsconcept/frontend/src/backend/oss/useInputCreate.tsx new file mode 100644 index 00000000..b92c0c50 --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useInputCreate.tsx @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { libraryApi } from '@/backend/library/api'; +import { ILibraryItem, LibraryItemID } from '@/models/library'; + +import { DataCallback } from '../apiTransport'; +import { ITargetOperation, ossApi } from './api'; + +export const useInputCreate = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [ossApi.baseKey, 'input-create'], + mutationFn: ossApi.inputCreate, + onSuccess: async data => { + client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey], data.oss); + await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); + } + }); + return { + inputCreate: ( + data: { + itemID: LibraryItemID; // + data: ITargetOperation; + }, + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_schema) }) + }; +}; diff --git a/rsconcept/frontend/src/backend/oss/useInputUpdate.tsx b/rsconcept/frontend/src/backend/oss/useInputUpdate.tsx new file mode 100644 index 00000000..03e59114 --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useInputUpdate.tsx @@ -0,0 +1,27 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { libraryApi } from '@/backend/library/api'; +import { LibraryItemID } from '@/models/library'; + +import { IInputUpdateDTO, ossApi } from './api'; + +export const useInputUpdate = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [ossApi.baseKey, 'input-update'], + mutationFn: ossApi.inputUpdate, + onSuccess: async data => { + client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.id }).queryKey], data); + await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); + } + }); + return { + inputUpdate: ( + data: { + itemID: LibraryItemID; // + data: IInputUpdateDTO; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/oss/useIsProcessingOss.tsx b/rsconcept/frontend/src/backend/oss/useIsProcessingOss.tsx new file mode 100644 index 00000000..eabf01fb --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useIsProcessingOss.tsx @@ -0,0 +1,11 @@ +import { useIsMutating } from '@tanstack/react-query'; + +import { libraryApi } from '@/backend/library/api'; + +import { ossApi } from './api'; + +export const useIsProcessingOss = () => { + const countLibrary = useIsMutating({ mutationKey: [libraryApi.baseKey] }); + const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] }); + return countLibrary + countOss !== 0; +}; diff --git a/rsconcept/frontend/src/backend/oss/useOSS.tsx b/rsconcept/frontend/src/backend/oss/useOSS.tsx new file mode 100644 index 00000000..b00e8d2f --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useOSS.tsx @@ -0,0 +1,26 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary'; +import { LibraryItemID } from '@/models/library'; +import { OssLoader } from '@/models/OssLoader'; + +import { ossApi } from './api'; + +export function useOss({ itemID }: { itemID?: LibraryItemID }) { + const { items: libraryItems, isLoading: libraryLoading } = useLibrary(); + const { data, isLoading, error } = useQuery({ + ...ossApi.getOssQueryOptions({ itemID }) + }); + + const schema = data && !libraryLoading ? new OssLoader(data, libraryItems).produceOSS() : undefined; + return { schema: schema, isLoading: isLoading || libraryLoading, error: error }; +} + +export function useOssSuspense({ itemID }: { itemID: LibraryItemID }) { + const { items: libraryItems } = useLibrarySuspense(); + const { data } = useSuspenseQuery({ + ...ossApi.getOssQueryOptions({ itemID }) + }); + const schema = new OssLoader(data!, libraryItems).produceOSS(); + return { schema }; +} diff --git a/rsconcept/frontend/src/backend/oss/useOperationCreate.tsx b/rsconcept/frontend/src/backend/oss/useOperationCreate.tsx new file mode 100644 index 00000000..84bf6f9b --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useOperationCreate.tsx @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; +import { IOperationData } from '@/models/oss'; + +import { IOperationCreateDTO, ossApi } from './api'; + +export const useOperationCreate = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [ossApi.baseKey, 'operation-create'], + mutationFn: ossApi.operationCreate, + onSuccess: data => { + client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey], data.oss); + updateTimestamp(data.oss.id); + } + }); + return { + operationCreate: ( + data: { + itemID: LibraryItemID; // + data: IOperationCreateDTO; + }, + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_operation) }) + }; +}; diff --git a/rsconcept/frontend/src/backend/oss/useOperationDelete.tsx b/rsconcept/frontend/src/backend/oss/useOperationDelete.tsx new file mode 100644 index 00000000..9ec316b4 --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useOperationDelete.tsx @@ -0,0 +1,27 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { libraryApi } from '@/backend/library/api'; +import { LibraryItemID } from '@/models/library'; + +import { IOperationDeleteDTO, ossApi } from './api'; + +export const useOperationDelete = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [ossApi.baseKey, 'operation-delete'], + mutationFn: ossApi.operationDelete, + onSuccess: async data => { + client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.id }).queryKey], data); + await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); + } + }); + return { + operationDelete: ( + data: { + itemID: LibraryItemID; // + data: IOperationDeleteDTO; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/oss/useOperationExecute.tsx b/rsconcept/frontend/src/backend/oss/useOperationExecute.tsx new file mode 100644 index 00000000..a618d21f --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useOperationExecute.tsx @@ -0,0 +1,27 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { libraryApi } from '@/backend/library/api'; +import { LibraryItemID } from '@/models/library'; + +import { ITargetOperation, ossApi } from './api'; + +export const useOperationExecute = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [ossApi.baseKey, 'operation-execute'], + mutationFn: ossApi.operationExecute, + onSuccess: async data => { + client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.id }).queryKey], data); + await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); + } + }); + return { + operationExecute: ( + data: { + itemID: LibraryItemID; // + data: ITargetOperation; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/oss/useOperationUpdate.tsx b/rsconcept/frontend/src/backend/oss/useOperationUpdate.tsx new file mode 100644 index 00000000..340b19ef --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useOperationUpdate.tsx @@ -0,0 +1,27 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { libraryApi } from '@/backend/library/api'; +import { LibraryItemID } from '@/models/library'; + +import { IOperationUpdateDTO, ossApi } from './api'; + +export const useOperationUpdate = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [ossApi.baseKey, 'operation-update'], + mutationFn: ossApi.operationUpdate, + onSuccess: async data => { + client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.id }).queryKey], data); + await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); + } + }); + return { + operationUpdate: ( + data: { + itemID: LibraryItemID; // + data: IOperationUpdateDTO; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/oss/useRelocateConstituents.tsx b/rsconcept/frontend/src/backend/oss/useRelocateConstituents.tsx new file mode 100644 index 00000000..374ccbb3 --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useRelocateConstituents.tsx @@ -0,0 +1,27 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { libraryApi } from '@/backend/library/api'; +import { LibraryItemID } from '@/models/library'; + +import { ICstRelocateDTO, ossApi } from './api'; + +export const useRelocateConstituents = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [ossApi.baseKey, 'relocate-constituents'], + mutationFn: ossApi.relocateConstituents, + onSuccess: async data => { + client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.id }).queryKey], data); + await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); + } + }); + return { + relocateConstituents: ( + data: { + itemID: LibraryItemID; // + data: ICstRelocateDTO; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/oss/useUpdatePositions.tsx b/rsconcept/frontend/src/backend/oss/useUpdatePositions.tsx new file mode 100644 index 00000000..22cdbe4d --- /dev/null +++ b/rsconcept/frontend/src/backend/oss/useUpdatePositions.tsx @@ -0,0 +1,25 @@ +import { useMutation } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; +import { IOperationPosition } from '@/models/oss'; + +import { ossApi } from './api'; + +export const useUpdatePositions = () => { + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [ossApi.baseKey, 'update-positions'], + mutationFn: ossApi.updatePositions, + onSuccess: (_, variables) => updateTimestamp(variables.itemID) + }); + return { + updatePositions: ( + data: { + itemID: LibraryItemID; // + positions: IOperationPosition[]; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/queryClient.ts b/rsconcept/frontend/src/backend/queryClient.ts new file mode 100644 index 00000000..d36b81b0 --- /dev/null +++ b/rsconcept/frontend/src/backend/queryClient.ts @@ -0,0 +1,23 @@ +import { QueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; + +import { DELAYS } from './configuration'; + +declare module '@tanstack/react-query' { + interface Register { + defaultError: AxiosError; + } +} + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: DELAYS.staleDefault, + gcTime: DELAYS.garbageCollection, + retry: 3, + refetchOnWindowFocus: true, + refetchOnMount: true, + refetchOnReconnect: true + } + } +}); diff --git a/rsconcept/frontend/src/backend/rsform/api.ts b/rsconcept/frontend/src/backend/rsform/api.ts new file mode 100644 index 00000000..35f1993b --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/api.ts @@ -0,0 +1,191 @@ +import { queryOptions } from '@tanstack/react-query'; + +import { axiosInstance } from '@/backend/axiosInstance'; +import { DELAYS } from '@/backend/configuration'; +import { LibraryItemID, VersionID } from '@/models/library'; +import { ICstSubstitute, ICstSubstitutions } from '@/models/oss'; +import { + ConstituentaID, + CstType, + IConstituentaList, + IConstituentaMeta, + IRSFormData, + ITargetCst, + TermForm +} from '@/models/rsform'; +import { IExpressionParse } from '@/models/rslang'; + +/** + * Represents data, used for uploading {@link IRSForm} as file. + */ +export interface IRSFormUploadDTO { + itemID: LibraryItemID; + load_metadata: boolean; + file: File; + fileName: string; +} + +/** + * Represents {@link IConstituenta} data, used in creation process. + */ +export interface ICstCreateDTO { + alias: string; + cst_type: CstType; + definition_raw: string; + term_raw: string; + convention: string; + definition_formal: string; + term_forms: TermForm[]; + + insert_after: ConstituentaID | null; +} + +/** + * Represents data response when creating {@link IConstituenta}. + */ +export interface ICstCreatedResponse { + new_cst: IConstituentaMeta; + schema: IRSFormData; +} + +/** + * Represents data, used in updating persistent attributes in {@link IConstituenta}. + */ +export interface ICstUpdateDTO { + target: ConstituentaID; + item_data: { + convention?: string; + definition_formal?: string; + definition_raw?: string; + term_raw?: string; + term_forms?: TermForm[]; + }; +} + +/** + * Represents data, used in renaming {@link IConstituenta}. + */ +export interface ICstRenameDTO { + alias: string; + cst_type: CstType; + target: ConstituentaID; +} + +/** + * Represents data, used in ordering a list of {@link IConstituenta}. + */ +export interface ICstMoveDTO { + items: ConstituentaID[]; + move_to: number; // Note: 0-base index +} + +/** + * Represents data response when creating producing structure of {@link IConstituenta}. + */ +export interface IProduceStructureResponse { + cst_list: ConstituentaID[]; + schema: IRSFormData; +} + +/** + * Represents input data for inline synthesis. + */ +export interface IInlineSynthesisDTO { + receiver: LibraryItemID; + source: LibraryItemID; + items: ConstituentaID[]; + substitutions: ICstSubstitute[]; +} + +/** + * Represents {@link IConstituenta} data, used for checking expression. + */ +export interface ICheckConstituentaDTO { + alias: string; + cst_type: CstType; + definition_formal: string; +} + +export const rsformsApi = { + baseKey: 'rsform', + + getRSFormQueryOptions: ({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) => { + return queryOptions({ + queryKey: [rsformsApi.baseKey, 'item', itemID, version ?? ''], + staleTime: DELAYS.staleShort, + queryFn: meta => + !itemID + ? undefined + : axiosInstance + .get( + version ? `/api/library/${itemID}/versions/${version}` : `/api/rsforms/${itemID}/details`, + { + signal: meta.signal + } + ) + .then(response => response.data) + }); + }, + + download: ({ itemID, version }: { itemID: LibraryItemID; version?: VersionID }) => + axiosInstance // + .get(version ? `/api/versions/${version}/export-file` : `/api/rsforms/${itemID}/export-trs`, { + responseType: 'blob' + }) + .then(response => response.data), + upload: (data: IRSFormUploadDTO) => + axiosInstance // + .patch(`/api/rsforms/${data.itemID}/load-trs`, data, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + .then(response => response.data), + + cstCreate: ({ itemID, data }: { itemID: LibraryItemID; data: ICstCreateDTO }) => + axiosInstance // + .post(`/api/rsforms/${itemID}/create-cst`, data) + .then(response => response.data), + cstUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: ICstUpdateDTO }) => + axiosInstance // + .patch(`/api/rsforms/${itemID}/update-cst`, data) + .then(response => response.data), + cstDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IConstituentaList }) => + axiosInstance // + .patch(`/api/rsforms/${itemID}/delete-multiple-cst`, data) + .then(response => response.data), + cstRename: ({ itemID, data }: { itemID: LibraryItemID; data: ICstRenameDTO }) => + axiosInstance // + .patch(`/api/rsforms/${itemID}/rename-cst`, data) + .then(response => response.data), + cstSubstitute: ({ itemID, data }: { itemID: LibraryItemID; data: ICstSubstitutions }) => + axiosInstance // + .patch(`/api/rsforms/${itemID}/substitute`, data) + .then(response => response.data), + cstMove: ({ itemID, data }: { itemID: LibraryItemID; data: ICstMoveDTO }) => + axiosInstance // + .patch(`/api/rsforms/${itemID}/move-cst`, data) + .then(response => response.data), + + produceStructure: ({ itemID, data }: { itemID: LibraryItemID; data: ITargetCst }) => + axiosInstance // + .post(`/api/rsforms/${itemID}/produce-structure`, data) + .then(response => response.data), + inlineSynthesis: ({ itemID, data }: { itemID: LibraryItemID; data: IInlineSynthesisDTO }) => + axiosInstance // + .post(`/api/rsforms/${itemID}/inline-synthesis`, data) + .then(response => response.data), + restoreOrder: (itemID: LibraryItemID) => + axiosInstance // + .patch(`/api/rsforms/${itemID}/restore-order`) + .then(response => response.data), + resetAliases: (itemID: LibraryItemID) => + axiosInstance // + .patch(`/api/rsforms/${itemID}/reset-aliases`) + .then(response => response.data), + + checkConstituenta: ({ itemID, data }: { itemID: LibraryItemID; data: ICheckConstituentaDTO }) => + axiosInstance // + .post(`/api/rsforms/${itemID}/check-constituenta`, data) + .then(response => response.data) +}; diff --git a/rsconcept/frontend/src/backend/rsform/useCheckConstituenta.tsx b/rsconcept/frontend/src/backend/rsform/useCheckConstituenta.tsx new file mode 100644 index 00000000..18117d77 --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useCheckConstituenta.tsx @@ -0,0 +1,25 @@ +import { useMutation } from '@tanstack/react-query'; + +import { LibraryItemID } from '@/models/library'; +import { IExpressionParse } from '@/models/rslang'; + +import { DataCallback } from '../apiTransport'; +import { ICheckConstituentaDTO, rsformsApi } from './api'; + +export const useCheckConstituenta = () => { + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'check-constituenta'], + mutationFn: rsformsApi.checkConstituenta + }); + return { + checkConstituenta: ( + data: { + itemID: LibraryItemID; // + data: ICheckConstituentaDTO; + }, + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }), + isPending: mutation.isPending, + error: mutation.error + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useCstCreate.tsx b/rsconcept/frontend/src/backend/rsform/useCstCreate.tsx new file mode 100644 index 00000000..1f069803 --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useCstCreate.tsx @@ -0,0 +1,31 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; +import { IConstituentaMeta } from '@/models/rsform'; + +import { ICstCreateDTO, rsformsApi } from './api'; + +export const useCstCreate = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'create-cst'], + mutationFn: rsformsApi.cstCreate, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey], data.schema); + updateTimestamp(data.schema.id); + // TODO: invalidate OSS? + } + }); + return { + cstCreate: ( + data: { + itemID: LibraryItemID; // + data: ICstCreateDTO; + }, + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_cst) }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useCstDelete.tsx b/rsconcept/frontend/src/backend/rsform/useCstDelete.tsx new file mode 100644 index 00000000..3871015a --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useCstDelete.tsx @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; +import { IConstituentaList } from '@/models/rsform'; + +import { rsformsApi } from './api'; + +export const useCstDelete = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'delete-multiple-cst'], + mutationFn: rsformsApi.cstDelete, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data); + updateTimestamp(data.id); + // TODO: invalidate OSS? + } + }); + return { + cstDelete: ( + data: { + itemID: LibraryItemID; // + data: IConstituentaList; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useCstMove.tsx b/rsconcept/frontend/src/backend/rsform/useCstMove.tsx new file mode 100644 index 00000000..3f0cb9f8 --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useCstMove.tsx @@ -0,0 +1,29 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; + +import { ICstMoveDTO, rsformsApi } from './api'; + +export const useCstMove = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'move-cst'], + mutationFn: rsformsApi.cstMove, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data); + updateTimestamp(data.id); + // TODO: invalidate OSS? + } + }); + return { + cstMove: ( + data: { + itemID: LibraryItemID; // + data: ICstMoveDTO; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useCstRename.tsx b/rsconcept/frontend/src/backend/rsform/useCstRename.tsx new file mode 100644 index 00000000..0d6e627f --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useCstRename.tsx @@ -0,0 +1,31 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; +import { IConstituentaMeta } from '@/models/rsform'; + +import { DataCallback } from '../apiTransport'; +import { ICstRenameDTO, rsformsApi } from './api'; + +export const useCstRename = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'rename-cst'], + mutationFn: rsformsApi.cstRename, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey], data.schema); + updateTimestamp(data.schema.id); + // TODO: invalidate OSS? + } + }); + return { + cstRename: ( + data: { + itemID: LibraryItemID; // + data: ICstRenameDTO; + }, + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_cst) }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useCstSubstitute.tsx b/rsconcept/frontend/src/backend/rsform/useCstSubstitute.tsx new file mode 100644 index 00000000..fe23e90d --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useCstSubstitute.tsx @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; +import { ICstSubstitutions } from '@/models/oss'; + +import { rsformsApi } from './api'; + +export const useCstSubstitute = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'substitute-cst'], + mutationFn: rsformsApi.cstSubstitute, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data); + updateTimestamp(data.id); + // TODO: invalidate OSS? + } + }); + return { + cstSubstitute: ( + data: { + itemID: LibraryItemID; // + data: ICstSubstitutions; + }, + onSuccess?: () => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useCstUpdate.tsx b/rsconcept/frontend/src/backend/rsform/useCstUpdate.tsx new file mode 100644 index 00000000..17aa8bd6 --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useCstUpdate.tsx @@ -0,0 +1,33 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; +import { IConstituentaMeta } from '@/models/rsform'; + +import { ICstUpdateDTO, rsformsApi } from './api'; + +export const useCstUpdate = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'update-cst'], + mutationFn: rsformsApi.cstUpdate, + onSuccess: async (_, variables) => { + updateTimestamp(variables.itemID); + await client.invalidateQueries({ + queryKey: [rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey] + }); + // TODO: invalidate OSS? + } + }); + return { + cstUpdate: ( + data: { + itemID: LibraryItemID; // + data: ICstUpdateDTO; + }, + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useDownloadRSForm.tsx b/rsconcept/frontend/src/backend/rsform/useDownloadRSForm.tsx new file mode 100644 index 00000000..53eb9f7b --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useDownloadRSForm.tsx @@ -0,0 +1,21 @@ +import { useMutation } from '@tanstack/react-query'; + +import { LibraryItemID, VersionID } from '@/models/library'; + +import { rsformsApi } from './api'; + +export const useDownloadRSForm = () => { + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'download'], + mutationFn: rsformsApi.download + }); + return { + download: ( + data: { + itemID: LibraryItemID; // + version?: VersionID; + }, + onSuccess?: (data: Blob) => void + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useInlineSynthesis.tsx b/rsconcept/frontend/src/backend/rsform/useInlineSynthesis.tsx new file mode 100644 index 00000000..913341ee --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useInlineSynthesis.tsx @@ -0,0 +1,31 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; +import { IRSFormData } from '@/models/rsform'; + +import { DataCallback } from '../apiTransport'; +import { IInlineSynthesisDTO, rsformsApi } from './api'; + +export const useInlineSynthesis = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'inline-synthesis'], + mutationFn: rsformsApi.inlineSynthesis, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data); + updateTimestamp(data.id); + // TODO: invalidate OSS? + } + }); + return { + inlineSynthesis: ( + data: { + itemID: LibraryItemID; // + data: IInlineSynthesisDTO; + }, + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useIsProcessingRSForm.tsx b/rsconcept/frontend/src/backend/rsform/useIsProcessingRSForm.tsx new file mode 100644 index 00000000..48a6a777 --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useIsProcessingRSForm.tsx @@ -0,0 +1,11 @@ +import { useIsMutating } from '@tanstack/react-query'; + +import { libraryApi } from '@/backend/library/api'; + +import { rsformsApi } from './api'; + +export const useIsProcessingRSForm = () => { + const countLibrary = useIsMutating({ mutationKey: [libraryApi.baseKey] }); + const countRsform = useIsMutating({ mutationKey: [rsformsApi.baseKey] }); + return countLibrary + countRsform !== 0; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useProduceStructure.tsx b/rsconcept/frontend/src/backend/rsform/useProduceStructure.tsx new file mode 100644 index 00000000..6df5802c --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useProduceStructure.tsx @@ -0,0 +1,31 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; +import { ConstituentaID, ITargetCst } from '@/models/rsform'; + +import { rsformsApi } from './api'; + +export const useProduceStructure = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'produce-structure'], + mutationFn: rsformsApi.produceStructure, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey], data.schema); + updateTimestamp(data.schema.id); + // TODO: invalidate OSS? + } + }); + return { + produceStructure: ( + data: { + itemID: LibraryItemID; // + data: ITargetCst; + }, + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.cst_list) }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useRSForm.tsx b/rsconcept/frontend/src/backend/rsform/useRSForm.tsx new file mode 100644 index 00000000..86334a5f --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useRSForm.tsx @@ -0,0 +1,34 @@ +import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; + +import { LibraryItemID, VersionID } from '@/models/library'; +import { IRSForm, IRSFormData } from '@/models/rsform'; +import { RSFormLoader } from '@/models/RSFormLoader'; + +import { rsformsApi } from './api'; + +export function useRSForm({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) { + const { data, isLoading, error } = useQuery({ + ...rsformsApi.getRSFormQueryOptions({ itemID, version }) + }); + + const schema = data ? new RSFormLoader(data).produceRSForm() : undefined; + return { schema, isLoading, error }; +} + +export function useRSFormSuspense({ itemID, version }: { itemID: LibraryItemID; version?: VersionID }) { + const { data } = useSuspenseQuery({ + ...rsformsApi.getRSFormQueryOptions({ itemID, version }) + }); + const schema = new RSFormLoader(data!).produceRSForm(); + return { schema }; +} + +export function useRSFormUpdate({ itemID }: { itemID: LibraryItemID }) { + const client = useQueryClient(); + const queryKey = [rsformsApi.getRSFormQueryOptions({ itemID }).queryKey]; + return { + update: (data: IRSFormData) => client.setQueryData(queryKey, data), + partialUpdate: (data: Partial) => + client.setQueryData(queryKey, (prev: IRSForm) => (prev ? { ...prev, ...data } : prev)) + }; +} diff --git a/rsconcept/frontend/src/backend/rsform/useRSForms.tsx b/rsconcept/frontend/src/backend/rsform/useRSForms.tsx new file mode 100644 index 00000000..deb6804f --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useRSForms.tsx @@ -0,0 +1,23 @@ +import { useQueries } from '@tanstack/react-query'; + +import { LibraryItemID } from '@/models/library'; +import { RSFormLoader } from '@/models/RSFormLoader'; + +import { DELAYS } from '../configuration'; +import { rsformsApi } from './api'; + +export function useRSForms(itemIDs: LibraryItemID[]) { + const results = useQueries({ + queries: itemIDs.map(itemID => ({ + ...rsformsApi.getRSFormQueryOptions({ itemID }), + enabled: itemIDs.length > 0, + staleTime: DELAYS.staleShort + })) + }); + + const schemas = results + .map(result => result.data) + .filter(data => data !== undefined) + .map(data => new RSFormLoader(data).produceRSForm()); + return schemas; +} diff --git a/rsconcept/frontend/src/backend/rsform/useResetAliases.tsx b/rsconcept/frontend/src/backend/rsform/useResetAliases.tsx new file mode 100644 index 00000000..16801841 --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useResetAliases.tsx @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; + +import { rsformsApi } from './api'; + +export const useResetAliases = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'reset-aliases'], + mutationFn: rsformsApi.resetAliases, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data); + updateTimestamp(data.id); + // TODO: invalidate OSS? + } + }); + return { + resetAliases: ( + itemID: LibraryItemID, // + onSuccess?: () => void + ) => mutation.mutate(itemID, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useRestoreOrder.tsx b/rsconcept/frontend/src/backend/rsform/useRestoreOrder.tsx new file mode 100644 index 00000000..64dde162 --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useRestoreOrder.tsx @@ -0,0 +1,25 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; +import { LibraryItemID } from '@/models/library'; + +import { rsformsApi } from './api'; + +export const useRestoreOrder = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'restore-order'], + mutationFn: rsformsApi.restoreOrder, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data); + updateTimestamp(data.id); + } + }); + return { + restoreOrder: ( + itemID: LibraryItemID, // + onSuccess?: () => void + ) => mutation.mutate(itemID, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsform/useUploadTRS.tsx b/rsconcept/frontend/src/backend/rsform/useUploadTRS.tsx new file mode 100644 index 00000000..587f3bdd --- /dev/null +++ b/rsconcept/frontend/src/backend/rsform/useUploadTRS.tsx @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { libraryApi } from '@/backend/library/api'; +import { ILibraryItem } from '@/models/library'; +import { IRSFormData } from '@/models/rsform'; + +import { IRSFormUploadDTO, rsformsApi } from './api'; + +export const useUploadTRS = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: [rsformsApi.baseKey, 'load-trs'], + mutationFn: rsformsApi.upload, + onSuccess: data => { + client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data); + client.setQueryData([libraryApi.libraryListKey], (prev: ILibraryItem[] | undefined) => + prev?.map(item => (item.id === data.id ? data : item)) + ); + } + }); + return { + upload: ( + data: IRSFormUploadDTO, // + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess }) + }; +}; diff --git a/rsconcept/frontend/src/backend/rsforms.ts b/rsconcept/frontend/src/backend/rsforms.ts deleted file mode 100644 index bd2d689e..00000000 --- a/rsconcept/frontend/src/backend/rsforms.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Endpoints: rsforms. - */ - -import { ILibraryCreateData, ILibraryItem } from '@/models/library'; -import { ICstSubstituteData } from '@/models/oss'; -import { - ICheckConstituentaData, - IConstituentaList, - IConstituentaMeta, - ICstCreateData, - ICstCreatedResponse, - ICstMovetoData, - ICstRenameData, - ICstUpdateData, - IInlineSynthesisData, - IProduceStructureResponse, - IRSFormData, - IRSFormUploadData, - ITargetCst -} from '@/models/rsform'; -import { IExpressionParse } from '@/models/rslang'; - -import { AxiosGet, AxiosPatch, AxiosPost, FrontExchange, FrontPull } from './apiTransport'; - -export function postRSFormFromFile(request: FrontExchange) { - AxiosPost({ - endpoint: '/api/rsforms/create-detailed', - request: request, - options: { - headers: { - 'Content-Type': 'multipart/form-data' - } - } - }); -} - -export function getRSFormDetails(target: string, version: string, request: FrontPull) { - if (!version) { - AxiosGet({ - endpoint: `/api/rsforms/${target}/details`, - request: request - }); - } else { - AxiosGet({ - endpoint: `/api/library/${target}/versions/${version}`, - request: request - }); - } -} - -export function getTRSFile(target: string, version: string, request: FrontPull) { - if (!version) { - AxiosGet({ - endpoint: `/api/rsforms/${target}/export-trs`, - request: request, - options: { responseType: 'blob' } - }); - } else { - AxiosGet({ - endpoint: `/api/versions/${version}/export-file`, - request: request, - options: { responseType: 'blob' } - }); - } -} - -export function postCreateConstituenta(schema: string, request: FrontExchange) { - AxiosPost({ - endpoint: `/api/rsforms/${schema}/create-cst`, - request: request - }); -} - -export function patchUpdateConstituenta(schema: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/rsforms/${schema}/update-cst`, - request: request - }); -} - -export function patchDeleteConstituenta(schema: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/rsforms/${schema}/delete-multiple-cst`, - request: request - }); -} - -export function patchRenameConstituenta(schema: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/rsforms/${schema}/rename-cst`, - request: request - }); -} - -export function patchProduceStructure(schema: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/rsforms/${schema}/produce-structure`, - request: request - }); -} - -export function patchSubstituteConstituents(schema: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/rsforms/${schema}/substitute`, - request: request - }); -} - -export function patchMoveConstituenta(schema: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/rsforms/${schema}/move-cst`, - request: request - }); -} - -export function postCheckConstituenta( - schema: string, - request: FrontExchange -) { - AxiosPost({ - endpoint: `/api/rsforms/${schema}/check-constituenta`, - request: request - }); -} - -export function patchResetAliases(target: string, request: FrontPull) { - AxiosPatch({ - endpoint: `/api/rsforms/${target}/reset-aliases`, - request: request - }); -} - -export function patchRestoreOrder(target: string, request: FrontPull) { - AxiosPatch({ - endpoint: `/api/rsforms/${target}/restore-order`, - request: request - }); -} - -export function patchUploadTRS(target: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/rsforms/${target}/load-trs`, - request: request, - options: { - headers: { - 'Content-Type': 'multipart/form-data' - } - } - }); -} - -export function patchInlineSynthesis(request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/rsforms/inline-synthesis`, - request: request - }); -} diff --git a/rsconcept/frontend/src/backend/users.ts b/rsconcept/frontend/src/backend/users.ts deleted file mode 100644 index eda6ef97..00000000 --- a/rsconcept/frontend/src/backend/users.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Endpoints: users. - */ - -import { - ICurrentUser, - IPasswordTokenData, - IRequestPasswordData, - IResetPasswordData, - IUserInfo, - IUserLoginData, - IUserProfile, - IUserSignupData, - IUserUpdateData, - IUserUpdatePassword -} from '@/models/user'; - -import { AxiosGet, AxiosPatch, AxiosPost, FrontAction, FrontExchange, FrontPull, FrontPush } from './apiTransport'; - -export function getAuth(request: FrontPull) { - AxiosGet({ - endpoint: `/users/api/auth`, - request: request - }); -} - -export function postLogin(request: FrontPush) { - AxiosPost({ - endpoint: '/users/api/login', - request: request - }); -} - -export function postLogout(request: FrontAction) { - AxiosPost({ - endpoint: '/users/api/logout', - request: request - }); -} - -export function postSignup(request: FrontExchange) { - AxiosPost({ - endpoint: '/users/api/signup', - request: request - }); -} - -export function getProfile(request: FrontPull) { - AxiosGet({ - endpoint: '/users/api/profile', - request: request - }); -} - -export function patchProfile(request: FrontExchange) { - AxiosPatch({ - endpoint: '/users/api/profile', - request: request - }); -} - -export function patchPassword(request: FrontPush) { - AxiosPatch({ - endpoint: '/users/api/change-password', - request: request - }); -} - -export function postRequestPasswordReset(request: FrontPush) { - // title: 'Request password reset', - AxiosPost({ - endpoint: '/users/api/password-reset', - request: request - }); -} - -export function postValidatePasswordToken(request: FrontPush) { - // title: 'Validate password token', - AxiosPost({ - endpoint: '/users/api/password-reset/validate', - request: request - }); -} - -export function postResetPassword(request: FrontPush) { - // title: 'Reset password', - AxiosPost({ - endpoint: '/users/api/password-reset/confirm', - request: request - }); -} - -export function getActiveUsers(request: FrontPull) { - // title: 'Active users list', - AxiosGet({ - endpoint: '/users/api/active-users', - request: request - }); -} diff --git a/rsconcept/frontend/src/backend/users/api.ts b/rsconcept/frontend/src/backend/users/api.ts new file mode 100644 index 00000000..c1aeb469 --- /dev/null +++ b/rsconcept/frontend/src/backend/users/api.ts @@ -0,0 +1,39 @@ +import { queryOptions } from '@tanstack/react-query'; + +import { axiosInstance } from '@/backend/axiosInstance'; +import { DELAYS } from '@/backend/configuration'; +import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user'; + +/** + * Represents user data, intended to update user profile in persistent storage. + */ +export interface IUpdateProfileDTO extends Omit {} + +export const usersApi = { + baseKey: 'users', + getUsersQueryOptions: () => + queryOptions({ + queryKey: [usersApi.baseKey, 'list'], + staleTime: DELAYS.staleMedium, + queryFn: meta => + axiosInstance + .get('/users/api/active-users', { + signal: meta.signal + }) + .then(response => response.data) + }), + getProfileQueryOptions: () => + queryOptions({ + queryKey: [usersApi.baseKey, 'profile'], + staleTime: DELAYS.staleShort, + queryFn: meta => + axiosInstance + .get('/users/api/profile', { + signal: meta.signal + }) + .then(response => response.data) + }), + + signup: (data: IUserSignupData) => axiosInstance.post('/users/api/signup', data), + updateProfile: (data: IUpdateProfileDTO) => axiosInstance.patch('/users/api/profile', data) +}; diff --git a/rsconcept/frontend/src/backend/users/useLabelUser.tsx b/rsconcept/frontend/src/backend/users/useLabelUser.tsx new file mode 100644 index 00000000..fc191bf0 --- /dev/null +++ b/rsconcept/frontend/src/backend/users/useLabelUser.tsx @@ -0,0 +1,25 @@ +import { useUsers } from './useUsers'; + +export function useLabelUser() { + const { users } = useUsers(); + + function getUserLabel(userID: number | null): string { + const user = users.find(({ id }) => id === userID); + if (!user || userID === null) { + return userID ? `Аноним ${userID.toString()}` : 'Отсутствует'; + } + const hasFirstName = user.first_name !== ''; + const hasLastName = user.last_name !== ''; + if (hasFirstName || hasLastName) { + if (!hasLastName) { + return user.first_name; + } + if (!hasFirstName) { + return user.last_name; + } + return user.last_name + ' ' + user.first_name; + } + return `Аноним ${userID.toString()}`; + } + return getUserLabel; +} diff --git a/rsconcept/frontend/src/backend/users/useProfile.tsx b/rsconcept/frontend/src/backend/users/useProfile.tsx new file mode 100644 index 00000000..f74802a3 --- /dev/null +++ b/rsconcept/frontend/src/backend/users/useProfile.tsx @@ -0,0 +1,21 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { usersApi } from './api'; + +export function useProfile() { + const { + data: profile, + isLoading, + error + } = useQuery({ + ...usersApi.getProfileQueryOptions() + }); + return { profile, isLoading, error }; +} + +export function useProfileSuspense() { + const { data: profile } = useSuspenseQuery({ + ...usersApi.getProfileQueryOptions() + }); + return { profile }; +} diff --git a/rsconcept/frontend/src/backend/users/useSignup.tsx b/rsconcept/frontend/src/backend/users/useSignup.tsx new file mode 100644 index 00000000..5065d13b --- /dev/null +++ b/rsconcept/frontend/src/backend/users/useSignup.tsx @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { usersApi } from '@/backend/users/api'; +import { IUserProfile, IUserSignupData } from '@/models/user'; + +export const useSignup = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: ['signup'], + mutationFn: usersApi.signup, + onSuccess: async () => await client.invalidateQueries({ queryKey: [usersApi.baseKey] }) + }); + return { + signup: ( + data: IUserSignupData, // + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.data as IUserProfile) }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/users/useUpdateProfile.tsx b/rsconcept/frontend/src/backend/users/useUpdateProfile.tsx new file mode 100644 index 00000000..507c1fd6 --- /dev/null +++ b/rsconcept/frontend/src/backend/users/useUpdateProfile.tsx @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { DataCallback } from '@/backend/apiTransport'; +import { IUserProfile } from '@/models/user'; + +import { IUpdateProfileDTO, usersApi } from './api'; + +// TODO: reload users / optimistic update + +export const useUpdateProfile = () => { + const client = useQueryClient(); + const mutation = useMutation({ + mutationKey: ['update-profile'], + mutationFn: usersApi.updateProfile, + onSuccess: async () => await client.invalidateQueries({ queryKey: [usersApi.baseKey] }) + }); + return { + updateProfile: ( + data: IUpdateProfileDTO, // + onSuccess?: DataCallback + ) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.data as IUserProfile) }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/users/useUsers.tsx b/rsconcept/frontend/src/backend/users/useUsers.tsx new file mode 100644 index 00000000..18222a1c --- /dev/null +++ b/rsconcept/frontend/src/backend/users/useUsers.tsx @@ -0,0 +1,17 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { usersApi } from './api'; + +export function useUsersSuspense() { + const { data: users } = useSuspenseQuery({ + ...usersApi.getUsersQueryOptions() + }); + return { users }; +} + +export function useUsers() { + const { data: users } = useQuery({ + ...usersApi.getUsersQueryOptions() + }); + return { users: users ?? [] }; +} diff --git a/rsconcept/frontend/src/backend/versions.ts b/rsconcept/frontend/src/backend/versions.ts deleted file mode 100644 index e04e5206..00000000 --- a/rsconcept/frontend/src/backend/versions.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Endpoints: versions. - */ - -import { IVersionData } from '@/models/library'; -import { IRSFormData } from '@/models/rsform'; - -import { AxiosDelete, AxiosPatch, FrontAction, FrontPull, FrontPush } from './apiTransport'; - -export function patchVersion(target: string, request: FrontPush) { - // title: `Version id=${target}`, - AxiosPatch({ - endpoint: `/api/versions/${target}`, - request: request - }); -} - -export function patchRestoreVersion(target: string, request: FrontPull) { - AxiosPatch({ - endpoint: `/api/versions/${target}/restore`, - request: request - }); -} - -export function deleteVersion(target: string, request: FrontAction) { - AxiosDelete({ - endpoint: `/api/versions/${target}`, - request: request - }); -} diff --git a/rsconcept/frontend/src/components/RSInput/RSInput.tsx b/rsconcept/frontend/src/components/RSInput/RSInput.tsx index a1791edf..a368e8d4 100644 --- a/rsconcept/frontend/src/components/RSInput/RSInput.tsx +++ b/rsconcept/frontend/src/components/RSInput/RSInput.tsx @@ -47,7 +47,7 @@ interface RSInputProps const RSInput = forwardRef( ( { - id, // prettier: split lines + id, // label, disabled, noTooltip, diff --git a/rsconcept/frontend/src/components/info/BadgeGrammeme.tsx b/rsconcept/frontend/src/components/info/BadgeGrammeme.tsx index 8dea738d..2ad49f5f 100644 --- a/rsconcept/frontend/src/components/info/BadgeGrammeme.tsx +++ b/rsconcept/frontend/src/components/info/BadgeGrammeme.tsx @@ -16,7 +16,7 @@ function BadgeGrammeme({ grammeme }: BadgeGrammemeProps) { return (
{header ?

{header}

: null} diff --git a/rsconcept/frontend/src/components/select/PickSchema.tsx b/rsconcept/frontend/src/components/select/PickSchema.tsx index eb555af8..ffd25b03 100644 --- a/rsconcept/frontend/src/components/select/PickSchema.tsx +++ b/rsconcept/frontend/src/components/select/PickSchema.tsx @@ -4,7 +4,6 @@ import { useIntl } from 'react-intl'; import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable'; import SearchBar from '@/components/ui/SearchBar'; -import { useLibrary } from '@/context/LibraryContext'; import useDropdown from '@/hooks/useDropdown'; import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; import { matchLibraryItem } from '@/models/libraryAPI'; @@ -45,8 +44,6 @@ function PickSchema({ ...restProps }: PickSchemaProps) { const intl = useIntl(); - const { folders } = useLibrary(); - const [filterText, setFilterText] = useState(initialFilter); const [filterLocation, setFilterLocation] = useState(''); const [filtered, setFiltered] = useState([]); @@ -128,7 +125,6 @@ function PickSchema({ /> void; } -function SelectLocation({ value, folderTree, dense, prefix, onClick, className, style }: SelectLocationProps) { - const activeNode = folderTree.at(value); - const items = folderTree.getTree(); +function SelectLocation({ value, dense, prefix, onClick, className, style }: SelectLocationProps) { + const { folders } = useFolders(); + const activeNode = folders.at(value); + const items = folders.getTree(); const [folded, setFolded] = useState(items); useEffect(() => { diff --git a/rsconcept/frontend/src/components/select/SelectLocationContext.tsx b/rsconcept/frontend/src/components/select/SelectLocationContext.tsx index 65af7646..9f6d8c9d 100644 --- a/rsconcept/frontend/src/components/select/SelectLocationContext.tsx +++ b/rsconcept/frontend/src/components/select/SelectLocationContext.tsx @@ -4,7 +4,6 @@ import clsx from 'clsx'; import { useCallback } from 'react'; import useDropdown from '@/hooks/useDropdown'; -import { FolderTree } from '@/models/FolderTree'; import { prefixes } from '@/utils/constants'; import { IconFolderTree } from '../Icons'; @@ -16,7 +15,6 @@ import SelectLocation from './SelectLocation'; interface SelectLocationContextProps extends CProps.Styling { value: string; title?: string; - folderTree: FolderTree; stretchTop?: boolean; onChange: (newValue: string) => void; @@ -25,7 +23,6 @@ interface SelectLocationContextProps extends CProps.Styling { function SelectLocationContext({ value, title = 'Проводник...', - folderTree, onChange, className, style @@ -56,7 +53,6 @@ function SelectLocationContext({ style={style} > void; + filter?: (userID: UserID) => boolean; placeholder?: string; noBorder?: boolean; @@ -20,20 +21,23 @@ interface SelectUserProps extends CProps.Styling { function SelectUser({ className, - items, + filter, value, onSelectValue, placeholder = 'Выберите пользователя', ...restProps }: SelectUserProps) { - const { getUserLabel } = useUsers(); + const { users } = useUsers(); + const getUserLabel = useLabelUser(); + + const items = filter ? users.filter(user => filter(user.id)) : users; const options = items?.map(user => ({ value: user.id, label: getUserLabel(user.id) })) ?? []; - function filter(option: { value: UserID | undefined; label: string }, inputValue: string) { + function filterLabel(option: { value: UserID | undefined; label: string }, inputValue: string) { const user = items?.find(item => item.id === option.value); return !user ? false : matchUser(user, inputValue); } @@ -47,7 +51,7 @@ function SelectUser({ if (data?.value !== undefined) onSelectValue(data.value); }} // @ts-expect-error: TODO: use type definitions from react-select in filter object - filterOption={filter} + filterOption={filterLabel} placeholder={placeholder} {...restProps} /> diff --git a/rsconcept/frontend/src/components/ui/Checkbox.tsx b/rsconcept/frontend/src/components/ui/Checkbox.tsx index 5b221673..f6328d0e 100644 --- a/rsconcept/frontend/src/components/ui/Checkbox.tsx +++ b/rsconcept/frontend/src/components/ui/Checkbox.tsx @@ -48,7 +48,7 @@ function Checkbox({