diff --git a/.vscode/settings.json b/.vscode/settings.json index 6f99defc..25dbb65d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,5 +29,13 @@ "name": "django", "depth": 5 } + ], + "colorize.include": [".tsx", ".jsx", ".ts", ".js"], + "colorize.languages": [ + "typescript", + "javascript", + "css", + "typescriptreact", + "javascriptreact" ] } \ No newline at end of file diff --git a/README.md b/README.md index 5dee022a..062a43b8 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ This readme file is used mostly to document project dependencies VS Code plugins
   - ESLint
-  - 
+  - Colorize
   
diff --git a/TODO.txt b/TODO.txt index fe89ffeb..497f986a 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,8 +4,6 @@ For more specific TODOs see comments in code [Functionality] - home page -- manuals -- текстовый модуль для разрешения отсылок - блок нотификаций пользователей - блок синтеза - блок организации библиотеки моделей @@ -13,9 +11,6 @@ For more specific TODOs see comments in code - обратная связь - система баг репортов [Tech] -- Use migtation/fixtures to provide initial data for testing -- USe migtation/fixtures to load example common data - - create custom Select component - reload react-data-table-component diff --git a/rsconcept/backend/Dockerfile b/rsconcept/backend/Dockerfile index bc6b9e88..8b0a9661 100644 --- a/rsconcept/backend/Dockerfile +++ b/rsconcept/backend/Dockerfile @@ -58,7 +58,7 @@ RUN pip install --no-cache /wheels/* && \ COPY apps/ ./apps COPY cctext/ ./cctext COPY project/ ./project -COPY data/ ./data +COPY fixtures/ ./fixtures COPY manage.py entrypoint.sh ./ RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \ chmod +x $APP_HOME/entrypoint.sh && \ diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index 7d45d86a..74ad2239 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -24,7 +24,7 @@ class LibraryActiveView(generics.ListAPIView): def get_queryset(self): user = self.request.user if not user.is_anonymous: - # pyling: disable=unsupported-binary-operation + # pylint: disable=unsupported-binary-operation return m.LibraryItem.objects.filter(Q(is_common=True) | Q(owner=user) | Q(subscription__user=user)) else: return m.LibraryItem.objects.filter(is_common=True) diff --git a/rsconcept/frontend/src/App.tsx b/rsconcept/frontend/src/App.tsx index d0969811..ffa2af93 100644 --- a/rsconcept/frontend/src/App.tsx +++ b/rsconcept/frontend/src/App.tsx @@ -16,7 +16,7 @@ import RSFormPage from './pages/RSFormPage'; import UserProfilePage from './pages/UserProfilePage'; function App () { - const { noNavigation, viewportHeight, mainHeight } = useConceptTheme(); + const { noNavigation, noFooter, viewportHeight, mainHeight } = useConceptTheme(); return (
@@ -45,7 +45,7 @@ function App () { } /> - {!noNavigation &&
); diff --git a/rsconcept/frontend/src/components/Common/ConceptDataTable.tsx b/rsconcept/frontend/src/components/Common/ConceptDataTable.tsx index 0de28159..261e4cb1 100644 --- a/rsconcept/frontend/src/components/Common/ConceptDataTable.tsx +++ b/rsconcept/frontend/src/components/Common/ConceptDataTable.tsx @@ -1,6 +1,7 @@ import DataTable, { createTheme, type TableProps } from 'react-data-table-component'; import { useConceptTheme } from '../../context/ThemeContext'; +import { dataTableDarkT, dataTableLightT } from '../../utils/color'; export interface SelectionInfo { allSelected: boolean @@ -8,47 +9,21 @@ export interface SelectionInfo { selectedRows: T[] } -createTheme('customDark', { - text: { - primary: 'rgba(228, 228, 231, 1)', - secondary: 'rgba(228, 228, 231, 0.87)', - disabled: 'rgba(228, 228, 231, 0.54)' - }, - background: { - default: '#111827' - }, - highlightOnHover: { - default: '#4d6080', - text: 'rgba(228, 228, 231, 1)' - }, - divider: { - default: '#6b6b6b' - }, - striped: { - default: '#374151', - text: 'rgba(228, 228, 231, 1)' - }, - selected: { - default: '#4d6080', - text: 'rgba(228, 228, 231, 1)' - } -}, 'dark'); +createTheme('customDark', dataTableDarkT, 'dark'); +createTheme('customLight', dataTableLightT, 'light'); -createTheme('customLight', { - divider: { - default: '#d1d5db' - }, - striped: { - default: '#f0f2f7' - }, -}, 'light'); +interface ConceptDataTableProps +extends Omit, 'paginationComponentOptions'> {} -function ConceptDataTable({ theme, ...props }: TableProps) { +function ConceptDataTable({ theme, ...props }: ConceptDataTableProps) { const { darkMode } = useConceptTheme(); return ( theme={ theme ?? (darkMode ? 'customDark' : 'customLight')} + paginationComponentOptions={{ + rowsPerPageText: 'строк на страницу' + }} {...props} /> ); diff --git a/rsconcept/frontend/src/components/Common/ConceptTab.tsx b/rsconcept/frontend/src/components/Common/ConceptTab.tsx index 7868a717..fca2e796 100644 --- a/rsconcept/frontend/src/components/Common/ConceptTab.tsx +++ b/rsconcept/frontend/src/components/Common/ConceptTab.tsx @@ -1,11 +1,15 @@ import type { TabProps } from 'react-tabs'; import { Tab } from 'react-tabs'; -function ConceptTab({ children, className, ...otherProps }: TabProps) { +interface ConceptTabProps +extends Omit { + className?: string +} + +function ConceptTab({ children, className, ...otherProps }: ConceptTabProps) { return ( {children} diff --git a/rsconcept/frontend/src/components/Common/Dropdown.tsx b/rsconcept/frontend/src/components/Common/Dropdown.tsx index 0f1f39b0..af7b538c 100644 --- a/rsconcept/frontend/src/components/Common/Dropdown.tsx +++ b/rsconcept/frontend/src/components/Common/Dropdown.tsx @@ -6,8 +6,8 @@ interface DropdownProps { function Dropdown({ children, widthClass = 'w-fit', stretchLeft }: DropdownProps) { return ( -
-
+
+
{children}
diff --git a/rsconcept/frontend/src/components/Common/Modal.tsx b/rsconcept/frontend/src/components/Common/Modal.tsx index 55a60ed5..1d94f9ca 100644 --- a/rsconcept/frontend/src/components/Common/Modal.tsx +++ b/rsconcept/frontend/src/components/Common/Modal.tsx @@ -46,7 +46,7 @@ function Modal({
{children}
-
+
{!readonly &&
- Центр Концепт +

Центр Концепт

© 2023 ЦИВТ КОНЦЕПТ

diff --git a/rsconcept/frontend/src/components/Help/HelpMain.tsx b/rsconcept/frontend/src/components/Help/HelpMain.tsx index be4abb6b..2a45baf3 100644 --- a/rsconcept/frontend/src/components/Help/HelpMain.tsx +++ b/rsconcept/frontend/src/components/Help/HelpMain.tsx @@ -1,4 +1,5 @@ import { urls } from '../../utils/constants'; +import { LibraryFilterStrategy } from '../../utils/models'; import TextURL from '../Common/TextURL'; function HelpMain() { @@ -9,8 +10,9 @@ function HelpMain() {

Навигация по порталу осуществляется верхнюю панель или ссылки в "подвале" страницы. Их можно скрыть с помощью кнопки в правом верхнем углу

В меню пользователя (правый верхний угол) редактирование данных пользователя и изменение цветовой темы.

Основные разделы Портала

-
  • - общедоступные схемы и инструменты поиска и навигации по ним
  • -
  • - отслеживаемые и редактируемые схемы. Основной рабочий раздел
  • +
  • - все схемы доступные пользователю
  • +
  • - общедоступные схемы и инструменты поиска и навигации по ним
  • +
  • - отслеживаемые и редактируемые схемы. Основной рабочий раздел
  • - данные пользователя и смена пароля
  • Поддержка Портала

    Портал разрабатывается и является проектом с открытым исходным кодом, доступным на .

    diff --git a/rsconcept/frontend/src/components/Help/HelpRSFormMeta.tsx b/rsconcept/frontend/src/components/Help/HelpRSFormMeta.tsx index 30a288d5..611dadf9 100644 --- a/rsconcept/frontend/src/components/Help/HelpRSFormMeta.tsx +++ b/rsconcept/frontend/src/components/Help/HelpRSFormMeta.tsx @@ -6,6 +6,7 @@ function HelpRSFormMeta() {

    Для общедоступных схем владельцем может стать любой пользователь

    Для библиотечных схем правом редактирования обладают только администраторы

    Клонировать - создать копию схемы для дальнейшего редактирования

    +

    Отслеживание - возможность видеть схему в Библиотеке и использовать фильтры

    Загрузить/Выгрузить схему - взаимодействие с Экстеор через файлы формата TRS

    ); diff --git a/rsconcept/frontend/src/components/Help/InfoCstClass.tsx b/rsconcept/frontend/src/components/Help/InfoCstClass.tsx index d1d39be3..262bd9a4 100644 --- a/rsconcept/frontend/src/components/Help/InfoCstClass.tsx +++ b/rsconcept/frontend/src/components/Help/InfoCstClass.tsx @@ -1,19 +1,26 @@ +import { useConceptTheme } from '../../context/ThemeContext'; import { prefixes } from '../../utils/constants'; -import { mapCstClassInfo } from '../../utils/staticUI'; +import { getCstClassColor, mapCstClassInfo } from '../../utils/staticUI'; interface InfoCstClassProps { title?: string } function InfoCstClass({ title }: InfoCstClassProps) { + const { colors } = useConceptTheme(); + return (
    { title &&

    {title}

    } - { [... mapCstClassInfo.values()].map( - (info, index) => { + { [... mapCstClassInfo.entries()].map( + ([cstClass, info], index) => { return (

    - + {info.text} - diff --git a/rsconcept/frontend/src/components/Help/InfoCstStatus.tsx b/rsconcept/frontend/src/components/Help/InfoCstStatus.tsx index d2e05005..76599c60 100644 --- a/rsconcept/frontend/src/components/Help/InfoCstStatus.tsx +++ b/rsconcept/frontend/src/components/Help/InfoCstStatus.tsx @@ -1,19 +1,25 @@ +import { useConceptTheme } from '../../context/ThemeContext'; import { prefixes } from '../../utils/constants'; -import { mapStatusInfo } from '../../utils/staticUI'; +import { getCstStatusColor, mapStatusInfo } from '../../utils/staticUI'; interface InfoCstStatusProps { title?: string } function InfoCstStatus({ title }: InfoCstStatusProps) { + const { colors } = useConceptTheme(); + return (

    { title &&

    {title}

    } - { [... mapStatusInfo.values()].map( - (info, index) => { + { [... mapStatusInfo.entries()].map( + ([status, info], index) => { return (

    - + {info.text} - diff --git a/rsconcept/frontend/src/components/Navigation/Logo.tsx b/rsconcept/frontend/src/components/Navigation/Logo.tsx index 1dfac433..106714dc 100644 --- a/rsconcept/frontend/src/components/Navigation/Logo.tsx +++ b/rsconcept/frontend/src/components/Navigation/Logo.tsx @@ -8,7 +8,7 @@ function Logo({ title }: LogoProps) { return ( - {title} + {title} ); } diff --git a/rsconcept/frontend/src/components/Navigation/Navigation.tsx b/rsconcept/frontend/src/components/Navigation/Navigation.tsx index 58714a02..fbf2c8a7 100644 --- a/rsconcept/frontend/src/components/Navigation/Navigation.tsx +++ b/rsconcept/frontend/src/components/Navigation/Navigation.tsx @@ -10,8 +10,8 @@ function Navigation () { const navigate = useNavigate(); const { noNavigation, toggleNoNavigation } = useConceptTheme(); - const navigateCommon = () => { navigate('/library?filter=common') }; - const navigateHelp = () => { navigate('/manuals') }; + const navigateLibrary = () => navigate('/library'); + const navigateHelp = () => navigate('/manuals'); return (

    { user && menu.isActive && { menu.hide(); }} + hideDropdown={() => menu.hide()} />}
    ); diff --git a/rsconcept/frontend/src/components/RSInput/bracketMatching.ts b/rsconcept/frontend/src/components/RSInput/bracketMatching.ts index 36003ffa..1b26fff7 100644 --- a/rsconcept/frontend/src/components/RSInput/bracketMatching.ts +++ b/rsconcept/frontend/src/components/RSInput/bracketMatching.ts @@ -1,6 +1,8 @@ import { bracketMatching, MatchResult } from '@codemirror/language'; import { Decoration, EditorView } from '@codemirror/view'; +import { bracketsDarkT, bracketsLightT } from '../../utils/color'; + const matchingMark = Decoration.mark({class: "cc-matchingBracket"}); const nonmatchingMark = Decoration.mark({class: "cc-nonmatchingBracket"}); @@ -14,31 +16,9 @@ function bracketRender(match: MatchResult) { return decorations; } -const darkTheme = EditorView.baseTheme({ - '.cc-matchingBracket': { - fontWeight: 600, - }, - '.cc-nonmatchingBracket': { - color: '#ef4444', - fontWeight: 700, - }, - '&.cm-focused .cc-matchingBracket': { - backgroundColor: '#734f00', - }, -}); +const darkTheme = EditorView.baseTheme(bracketsDarkT); -const lightTheme = EditorView.baseTheme({ - '.cc-matchingBracket': { - fontWeight: 600, - }, - '.cc-nonmatchingBracket': { - color: '#ef4444', - fontWeight: 700, - }, - '&.cm-focused .cc-matchingBracket': { - backgroundColor: '#dae6f2', - }, -}); +const lightTheme = EditorView.baseTheme(bracketsLightT); export function ccBracketMatching(darkMode: boolean) { return [bracketMatching({ renderMatch: bracketRender }), darkMode ? darkTheme : lightTheme]; diff --git a/rsconcept/frontend/src/components/RSInput/index.tsx b/rsconcept/frontend/src/components/RSInput/index.tsx index 05290fb0..4071a29d 100644 --- a/rsconcept/frontend/src/components/RSInput/index.tsx +++ b/rsconcept/frontend/src/components/RSInput/index.tsx @@ -54,7 +54,7 @@ function RSInput({ id, label, innerref, onChange, editable, ...props }: RSInputProps) { - const { darkMode } = useConceptTheme(); + const { darkMode, colors } = useConceptTheme(); const { schema } = useRSForm(); const internalRef = useRef(null); @@ -69,9 +69,9 @@ function RSInput({ theme: 'light', settings: { fontFamily: 'inherit', - background: editable ? '#ffffff' : '#f0f2f7', - foreground: '#000000', - selection: '#aacef2', + background: editable ? colors.input : colors.inputDisabled, + foreground: colors.text, + selection: colors.selection, caret: '#5d00ff', }, styles: [ @@ -83,16 +83,16 @@ function RSInput({ { tag: t.controlKeyword, class: 'font-semibold'}, // R | I | D { tag: t.unit, class: 'text-[0.75rem]' }, // indicies ] - }), [editable]); + }), [editable, colors]); const darkTheme: Extension = useMemo( () => createTheme({ theme: 'dark', settings: { fontFamily: 'inherit', - background: editable ? '#070b12' : '#374151', - foreground: '#e4e4e7', - selection: '#8c6000', + background: editable ? colors.input : colors.inputDisabled, + foreground: colors.text, + selection: colors.selection, caret: '#ffaa00' }, styles: [ @@ -104,7 +104,7 @@ function RSInput({ { tag: t.controlKeyword, class: 'font-semibold'}, // R | I | D { tag: t.unit, class: 'text-[0.75rem]' }, // indicies ] - }), [editable]); + }), [editable, colors]); const editorExtensions = useMemo( () => [ diff --git a/rsconcept/frontend/src/context/AuthContext.tsx b/rsconcept/frontend/src/context/AuthContext.tsx index b2b433c0..735470ff 100644 --- a/rsconcept/frontend/src/context/AuthContext.tsx +++ b/rsconcept/frontend/src/context/AuthContext.tsx @@ -3,7 +3,8 @@ import { createContext, useCallback, useContext, useLayoutEffect, useState } fro import { type ErrorInfo } from '../components/BackendError'; import useLocalStorage from '../hooks/useLocalStorage'; import { type DataCallback, getAuth, patchPassword,postLogin, postLogout, postSignup } from '../utils/backendAPI'; -import { ICurrentUser, IUserLoginData, IUserProfile, IUserSignupData, IUserUpdatePassword } from '../utils/models'; +import { ICurrentUser, IUserInfo, IUserLoginData, IUserProfile, IUserSignupData, IUserUpdatePassword } from '../utils/models'; +import { useUsers } from './UsersContext'; interface IAuthContext { user: ICurrentUser | undefined @@ -32,6 +33,7 @@ interface AuthStateProps { } export const AuthState = ({ children }: AuthStateProps) => { + const { users } = useUsers(); const [user, setUser] = useLocalStorage('user', undefined); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); @@ -82,6 +84,7 @@ export const AuthState = ({ children }: AuthStateProps) => { setLoading: setLoading, onError: error => setError(error), onSuccess: newData => reload(() => { + users.push(newData as IUserInfo); if (callback) callback(newData); }) }); diff --git a/rsconcept/frontend/src/context/LibraryContext.tsx b/rsconcept/frontend/src/context/LibraryContext.tsx index 5ff2c251..db4b3ae0 100644 --- a/rsconcept/frontend/src/context/LibraryContext.tsx +++ b/rsconcept/frontend/src/context/LibraryContext.tsx @@ -43,16 +43,23 @@ export const LibraryState = ({ children }: LibraryStateProps) => { const filter = useCallback( (params: ILibraryFilter) => { let result = items; - if (params.ownedBy) { - result = result.filter(item => - item.owner === params.ownedBy - || user?.subscriptions.includes(item.id)); + if (params.is_owned) { + result = result.filter(item => item.owner === user?.id); } if (params.is_common !== undefined) { result = result.filter(item => item.is_common === params.is_common); } - if (params.queryMeta) { - result = result.filter(item => matchLibraryItem(params.queryMeta!, item)); + if (params.is_canonical !== undefined) { + result = result.filter(item => item.is_canonical === params.is_canonical); + } + if (params.is_subscribed !== undefined) { + result = result.filter(item => user?.subscriptions.includes(item.id)); + } + if (params.is_personal !== undefined) { + result = result.filter(item => user?.subscriptions.includes(item.id) || item.owner === user?.id); + } + if (params.query) { + result = result.filter(item => matchLibraryItem(params.query!, item)); } return result; }, [items, user]); @@ -84,12 +91,14 @@ export const LibraryState = ({ children }: LibraryStateProps) => { showError: true, setLoading: setProcessing, onError: error => setError(error), - onSuccess: newSchema => { - reload(); + onSuccess: newSchema => reload(() => { + if (user && !user.subscriptions.includes(newSchema.id)) { + user.subscriptions.push(newSchema.id); + } if (callback) callback(newSchema); - } + }) }); - }, [reload]); + }, [reload, user]); const destroySchema = useCallback( (target: number, callback?: () => void) => { @@ -99,10 +108,13 @@ export const LibraryState = ({ children }: LibraryStateProps) => { setLoading: setProcessing, onError: error => setError(error), onSuccess: () => reload(() => { + if (user && user.subscriptions.includes(target)) { + user.subscriptions.splice(user.subscriptions.findIndex(item => item === target), 1); + } if (callback) callback(); }) }); - }, [setError, reload]); + }, [setError, reload, user]); const cloneSchema = useCallback( (target: number, data: IRSFormCreateData, callback: DataCallback) => { @@ -116,6 +128,9 @@ export const LibraryState = ({ children }: LibraryStateProps) => { setLoading: setProcessing, onError: error => setError(error), onSuccess: newSchema => reload(() => { + if (user && !user.subscriptions.includes(newSchema.id)) { + user.subscriptions.push(newSchema.id); + } if (callback) callback(newSchema); }) }); diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index 3c37893e..990a3305 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -15,6 +15,7 @@ import { ILibraryUpdateData, IRSForm, IRSFormUploadData } from '../utils/models' import { useAuth } from './AuthContext' +import { useLibrary } from './LibraryContext' interface IRSFormContext { schema?: IRSForm @@ -66,6 +67,7 @@ interface RSFormStateProps { } export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { + const library = useLibrary(); const { user } = useAuth(); const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID }); const [ processing, setProcessing ] = useState(false); @@ -114,10 +116,14 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { onError: error => setError(error), onSuccess: newData => { setSchema(Object.assign(schema, newData)); + const libraryItem = library.items.find(item => item.id === newData.id); + if (libraryItem) { + Object.assign(libraryItem, newData); + } if (callback) callback(newData); } }); - }, [schemaID, setError, setSchema, schema]); + }, [schemaID, setError, setSchema, schema, library]); const upload = useCallback( (data: IRSFormUploadData, callback?: () => void) => { @@ -132,10 +138,14 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { onError: error => setError(error), onSuccess: newData => { setSchema(newData); + const libraryItem = library.items.find(item => item.id === newData.id); + if (libraryItem) { + Object.assign(libraryItem, newData); + } if (callback) callback(); } }); - }, [schemaID, setError, setSchema, schema]); + }, [schemaID, setError, setSchema, schema, library]); const claim = useCallback( (callback?: DataCallback) => { @@ -149,10 +159,17 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { onError: error => setError(error), onSuccess: newData => { setSchema(Object.assign(schema, newData)); + const libraryItem = library.items.find(item => item.id === newData.id); + if (libraryItem) { + libraryItem.owner = user.id + } + if (!user.subscriptions.includes(schema.id)) { + user.subscriptions.push(schema.id); + } if (callback) callback(newData); } }); - }, [schemaID, setError, schema, user, setSchema]); + }, [schemaID, setError, schema, user, setSchema, library]); const subscribe = useCallback( (callback?: () => void) => { @@ -266,9 +283,9 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { showError: true, setLoading: setProcessing, onError: error => setError(error), - onSuccess: newData => { - reload(setProcessing, () => { if (callback) callback(newData); }) - } + onSuccess: newData => reload(setProcessing, () => { + if (callback) callback(newData); + }) }); }, [setError, reload]); diff --git a/rsconcept/frontend/src/context/ThemeContext.tsx b/rsconcept/frontend/src/context/ThemeContext.tsx index c7208b7e..ad14c34a 100644 --- a/rsconcept/frontend/src/context/ThemeContext.tsx +++ b/rsconcept/frontend/src/context/ThemeContext.tsx @@ -1,13 +1,20 @@ import { createContext, useContext, useLayoutEffect, useMemo, useState } from 'react'; import useLocalStorage from '../hooks/useLocalStorage'; +import { darkT, IColorTheme, lightT } from '../utils/color'; interface IThemeContext { - darkMode: boolean - noNavigation: boolean viewportHeight: string mainHeight: string + + colors: IColorTheme + + darkMode: boolean toggleDarkMode: () => void + + noNavigation: boolean + noFooter: boolean + setNoFooter: (value: boolean) => void toggleNoNavigation: () => void } @@ -15,9 +22,7 @@ const ThemeContext = createContext(null); export const useConceptTheme = () => { const context = useContext(ThemeContext); if (!context) { - throw new Error( - 'useConceptTheme has to be used within ' - ); + throw new Error('useConceptTheme has to be used within '); } return context; } @@ -28,9 +33,11 @@ interface ThemeStateProps { export const ThemeState = ({ children }: ThemeStateProps) => { const [darkMode, setDarkMode] = useLocalStorage('darkMode', false); + const [colors, setColors] = useState(lightT); const [noNavigation, setNoNavigation] = useState(false); + const [noFooter, setNoFooter] = useState(false); - const setDarkClass = (isDark: boolean) => { + function setDarkClass(isDark: boolean) { const root = window.document.documentElement; if (isDark) { root.classList.add('dark'); @@ -38,12 +45,16 @@ export const ThemeState = ({ children }: ThemeStateProps) => { root.classList.remove('dark'); } root.setAttribute('data-color-scheme', !isDark ? 'light' : 'dark'); - }; + } useLayoutEffect(() => { setDarkClass(darkMode) }, [darkMode]); + useLayoutEffect(() => { + setColors(darkMode ? darkT : lightT) + }, [darkMode, setColors]); + const mainHeight = useMemo( () => { return !noNavigation ? @@ -60,10 +71,11 @@ export const ThemeState = ({ children }: ThemeStateProps) => { return ( setDarkMode(prev => !prev), toggleNoNavigation: () => setNoNavigation(prev => !prev), + setNoFooter, viewportHeight, mainHeight }}> {children} diff --git a/rsconcept/frontend/src/context/UserProfileContext.tsx b/rsconcept/frontend/src/context/UserProfileContext.tsx index 19ec094e..ad67b40f 100644 --- a/rsconcept/frontend/src/context/UserProfileContext.tsx +++ b/rsconcept/frontend/src/context/UserProfileContext.tsx @@ -3,6 +3,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea import { ErrorInfo } from '../components/BackendError'; import { DataCallback, getProfile, patchProfile } from '../utils/backendAPI'; import { IUserProfile, IUserUpdateData } from '../utils/models'; +import { useUsers } from './UsersContext'; interface IUserProfileContext { user: IUserProfile | undefined @@ -30,39 +31,43 @@ interface UserProfileStateProps { } export const UserProfileState = ({ children }: UserProfileStateProps) => { + const { users } = useUsers(); const [user, setUser] = useState(undefined); const [loading, setLoading] = useState(false); const [processing, setProcessing] = useState(false); const [error, setError] = useState(undefined); const reload = useCallback( - () => { - setError(undefined); - setUser(undefined); - getProfile({ - showError: true, - setLoading: setLoading, - onError: error => { setError(error); }, - onSuccess: newData => { setUser(newData); } - }); - }, [setUser] - ); + () => { + setError(undefined); + setUser(undefined); + getProfile({ + showError: true, + setLoading: setLoading, + onError: error => setError(error), + onSuccess: newData => setUser(newData) + }); + }, [setUser]); const updateUser = useCallback( - (data: IUserUpdateData, callback?: DataCallback) => { - setError(undefined); - patchProfile({ - data: data, - showError: true, - setLoading: setProcessing, - onError: error => { setError(error); }, - onSuccess: newData => { - setUser(newData); - if (callback) callback(newData); + (data: IUserUpdateData, callback?: DataCallback) => { + setError(undefined); + patchProfile({ + data: data, + showError: true, + setLoading: setProcessing, + onError: error => setError(error), + onSuccess: newData => { + setUser(newData); + const libraryUser = users.find(item => item.id === user?.id); + if (libraryUser) { + libraryUser.first_name = newData.first_name; + libraryUser.last_name = newData.last_name; } - }); - }, [setUser] - ); + if (callback) callback(newData); + } + }); + }, [setUser, users]); useEffect(() => { reload(); diff --git a/rsconcept/frontend/src/context/UsersContext.tsx b/rsconcept/frontend/src/context/UsersContext.tsx index 93800892..aa093f5e 100644 --- a/rsconcept/frontend/src/context/UsersContext.tsx +++ b/rsconcept/frontend/src/context/UsersContext.tsx @@ -13,9 +13,7 @@ const UsersContext = createContext(null) export const useUsers = (): IUsersContext => { const context = useContext(UsersContext); if (context === null) { - throw new Error( - 'useUsers has to be used within ' - ); + throw new Error('useUsers has to be used within '); } return context; } diff --git a/rsconcept/frontend/src/hooks/useCheckExpression.ts b/rsconcept/frontend/src/hooks/useCheckExpression.ts index 9fdb713e..fcb7ba57 100644 --- a/rsconcept/frontend/src/hooks/useCheckExpression.ts +++ b/rsconcept/frontend/src/hooks/useCheckExpression.ts @@ -6,23 +6,52 @@ import { RSErrorType } from '../utils/enums'; import { CstType, IConstituenta, IExpressionParse, IFunctionArg, type IRSForm } from '../utils/models'; import { getCstExpressionPrefix } from '../utils/staticUI'; +const LOGIC_TYPIIFCATION = 'LOGIC'; + function checkTypeConsistency(type: CstType, typification: string, args: IFunctionArg[]): boolean { + console.log(typification) switch (type) { case CstType.BASE: case CstType.CONSTANT: case CstType.STRUCTURED: case CstType.TERM: - return typification !== '' && args.length === 0; + return typification !== LOGIC_TYPIIFCATION && args.length === 0; case CstType.AXIOM: case CstType.THEOREM: - return typification === '' && args.length === 0; + return typification === LOGIC_TYPIIFCATION && args.length === 0; case CstType.FUNCTION: - return typification !== '' && args.length !== 0; + return typification !== LOGIC_TYPIIFCATION && args.length !== 0; case CstType.PREDICATE: - return typification === '' && args.length !== 0; + return typification === LOGIC_TYPIIFCATION && args.length !== 0; + } +} + +function adjustResults(parse: IExpressionParse, emptyExpression: boolean, cstType: CstType) { + if (!parse.parseResult && parse.errors.length > 0 && parse.errors[0].errorType !== RSErrorType.syntax) { + return; + } + if (cstType === CstType.BASE || cstType === CstType.CONSTANT) { + if (!emptyExpression) { + parse.parseResult = false; + parse.errors.push({ + errorType: RSErrorType.globalNonemptyBase, + isCritical: true, + params: [], + position: 0 + }); + } + } + if (!checkTypeConsistency(cstType, parse.typification, parse.args)) { + parse.parseResult = false; + parse.errors.push({ + errorType: RSErrorType.globalUnexpectedType, + isCritical: true, + params: [], + position: 0 + }); } } @@ -31,7 +60,7 @@ function useCheckExpression({ schema }: { schema?: IRSForm }) { const [error, setError] = useState(undefined); const [parseData, setParseData] = useState(undefined); - const resetParse = useCallback(() => { setParseData(undefined); }, []); + const resetParse = useCallback(() => setParseData(undefined), []); function checkExpression(expression: string, activeCst?: IConstituenta, onSuccess?: DataCallback) { setError(undefined); @@ -39,29 +68,10 @@ function useCheckExpression({ schema }: { schema?: IRSForm }) { data: { expression: expression }, showError: true, setLoading, - onError: error => { setError(error); }, + onError: error => setError(error), onSuccess: parse => { - if (activeCst && parse.parseResult) { - if (activeCst.cstType == CstType.BASE || activeCst.cstType == CstType.CONSTANT) { - if (expression !== getCstExpressionPrefix(activeCst)) { - parse.parseResult = false; - parse.errors.push({ - errorType: RSErrorType.globalNonemptyBase, - isCritical: true, - params: [], - position: 0 - }); - } - } - if (!checkTypeConsistency(activeCst.cstType, parse.typification, parse.args)) { - parse.parseResult = false; - parse.errors.push({ - errorType: RSErrorType.globalUnexpectedType, - isCritical: true, - params: [], - position: 0 - }); - } + if (activeCst) { + adjustResults(parse, expression === getCstExpressionPrefix(activeCst), activeCst.cstType); } setParseData(parse); if (onSuccess) onSuccess(parse); diff --git a/rsconcept/frontend/src/hooks/useDropdown.ts b/rsconcept/frontend/src/hooks/useDropdown.ts index ef95f9eb..db18cfe6 100644 --- a/rsconcept/frontend/src/hooks/useDropdown.ts +++ b/rsconcept/frontend/src/hooks/useDropdown.ts @@ -6,14 +6,14 @@ function useDropdown() { const [isActive, setIsActive] = useState(false); const ref = useRef(null); - useClickedOutside({ ref, callback: () => { setIsActive(false); } }) + useClickedOutside({ ref, callback: () => setIsActive(false) }) return { ref, isActive, setIsActive, - toggle: () => { setIsActive(!isActive); }, - hide: () => { setIsActive(false); } + toggle: () => setIsActive(!isActive), + hide: () => setIsActive(false) }; } diff --git a/rsconcept/frontend/src/hooks/useRSFormDetails.ts b/rsconcept/frontend/src/hooks/useRSFormDetails.ts index 1fbf7b48..9b5e1d50 100644 --- a/rsconcept/frontend/src/hooks/useRSFormDetails.ts +++ b/rsconcept/frontend/src/hooks/useRSFormDetails.ts @@ -27,7 +27,10 @@ export function useRSFormDetails({ target }: { target?: string }) { getRSFormDetails(target, { showError: true, setLoading: setCustomLoading ?? setLoading, - onError: error => { setInnerSchema(undefined); setError(error); }, + onError: error => { + setInnerSchema(undefined); + setError(error); + }, onSuccess: schema => { setSchema(schema); if (callback) callback(); diff --git a/rsconcept/frontend/src/hooks/useResolveText.ts b/rsconcept/frontend/src/hooks/useResolveText.ts index 8816073d..90d38c86 100644 --- a/rsconcept/frontend/src/hooks/useResolveText.ts +++ b/rsconcept/frontend/src/hooks/useResolveText.ts @@ -9,7 +9,7 @@ function useResolveText({ schema }: { schema?: IRSForm }) { const [error, setError] = useState(undefined); const [refsData, setRefsData] = useState(undefined); - const resetData = useCallback(() => { setRefsData(undefined); }, []); + const resetData = useCallback(() => setRefsData(undefined), []); function resolveText(text: string, onSuccess?: DataCallback) { setError(undefined); @@ -17,7 +17,7 @@ function useResolveText({ schema }: { schema?: IRSForm }) { data: { text: text }, showError: true, setLoading, - onError: error => { setError(error); }, + onError: error => setError(error), onSuccess: data => { setRefsData(data); if (onSuccess) onSuccess(data); diff --git a/rsconcept/frontend/src/index.css b/rsconcept/frontend/src/index.css index a2613c74..52ff9193 100644 --- a/rsconcept/frontend/src/index.css +++ b/rsconcept/frontend/src/index.css @@ -42,6 +42,10 @@ @apply clr-border rounded } + .text-btn { + @apply text-gray-600 dark:text-zinc-200 dark:disabled:text-zinc-400 disabled:text-gray-400 + } + .clr-border { @apply border-gray-300 dark:border-[#434343] } @@ -102,7 +106,7 @@ @apply text-white bg-blue-400 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-400 dark:text-black disabled:bg-gray-400 dark:disabled:bg-gray-600 } .clr-btn-default { - @apply text-gray-600 dark:text-zinc-200 dark:disabled:text-zinc-400 disabled:text-gray-400 bg-[#f0f2f7] hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-400 + @apply bg-[#f0f2f7] hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-400 text-btn } /* Transparent button */ diff --git a/rsconcept/frontend/src/main.tsx b/rsconcept/frontend/src/main.tsx index f9dbaa1d..bd8722ca 100644 --- a/rsconcept/frontend/src/main.tsx +++ b/rsconcept/frontend/src/main.tsx @@ -35,15 +35,15 @@ ReactDOM.createRoot(document.getElementById('root')!).render( > - + - + diff --git a/rsconcept/frontend/src/pages/HomePage.tsx b/rsconcept/frontend/src/pages/HomePage.tsx index 3447ab7b..a1cb99a5 100644 --- a/rsconcept/frontend/src/pages/HomePage.tsx +++ b/rsconcept/frontend/src/pages/HomePage.tsx @@ -15,7 +15,7 @@ function HomePage() { }, TIMEOUT_UI_REFRESH); } else if(!user.is_staff) { setTimeout(() => { - navigate('/library?filter=personal'); + navigate('/library'); }, TIMEOUT_UI_REFRESH); } }, [navigate, user]) diff --git a/rsconcept/frontend/src/pages/LibraryPage/PickerStrategy.tsx b/rsconcept/frontend/src/pages/LibraryPage/PickerStrategy.tsx new file mode 100644 index 00000000..f0de720d --- /dev/null +++ b/rsconcept/frontend/src/pages/LibraryPage/PickerStrategy.tsx @@ -0,0 +1,89 @@ +import { useCallback } from 'react'; + +import Button from '../../components/Common/Button'; +import Checkbox from '../../components/Common/Checkbox'; +import Dropdown from '../../components/Common/Dropdown'; +import DropdownButton from '../../components/Common/DropdownButton'; +import { FilterCogIcon } from '../../components/Icons'; +import useDropdown from '../../hooks/useDropdown'; +import { LibraryFilterStrategy } from '../../utils/models'; + +interface PickerStrategyProps { + value: LibraryFilterStrategy + onChange: (value: LibraryFilterStrategy) => void +} + +function PickerStrategy({ value, onChange }: PickerStrategyProps) { + const pickerMenu = useDropdown(); + + const handleChange = useCallback( + (newValue: LibraryFilterStrategy) => { + pickerMenu.hide(); + onChange(newValue); + }, [pickerMenu, onChange]); + + return ( +
    +
    + ); +} + +export default PickerStrategy; diff --git a/rsconcept/frontend/src/pages/LibraryPage/SearchPanel.tsx b/rsconcept/frontend/src/pages/LibraryPage/SearchPanel.tsx index 532f20ca..c6b12108 100644 --- a/rsconcept/frontend/src/pages/LibraryPage/SearchPanel.tsx +++ b/rsconcept/frontend/src/pages/LibraryPage/SearchPanel.tsx @@ -1,70 +1,96 @@ -import { useLayoutEffect, useState } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useCallback, useLayoutEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; import { MagnifyingGlassIcon } from '../../components/Icons'; import { useAuth } from '../../context/AuthContext'; -import { ILibraryFilter } from '../../utils/models'; +import { ILibraryFilter, LibraryFilterStrategy } from '../../utils/models'; +import PickerStrategy from './PickerStrategy'; + + +function ApplyStrategy(strategy: LibraryFilterStrategy): ILibraryFilter { + switch (strategy) { + case LibraryFilterStrategy.MANUAL: return {}; + case LibraryFilterStrategy.COMMON: return { is_common: true }; + case LibraryFilterStrategy.CANONICAL: return { is_canonical: true }; + case LibraryFilterStrategy.PERSONAL: return { is_personal: true }; + case LibraryFilterStrategy.SUBSCRIBE: return { is_subscribed: true }; + case LibraryFilterStrategy.OWNED: return { is_owned: true }; + } +} interface SearchPanelProps { - filter: ILibraryFilter + total: number + filtered: number setFilter: React.Dispatch> } -function SearchPanel({ filter, setFilter }: SearchPanelProps) { +function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) { + const navigate = useNavigate(); const search = useLocation().search; const { user } = useAuth(); - const [query, setQuery] = useState('') + const [query, setQuery] = useState(''); + const [strategy, setStrategy] = useState(LibraryFilterStrategy.MANUAL); + + function handleChangeQuery(event: React.ChangeEvent) { + const newQuery = event.target.value; + setQuery(newQuery); + setFilter(prev => { + return { + query: newQuery, + is_owned: prev.is_owned, + is_common: prev.is_common, + is_canonical: prev.is_canonical, + is_subscribed: prev.is_subscribed, + is_personal: prev.is_personal + }; + }); + } useLayoutEffect(() => { - const filterType = new URLSearchParams(search).get('filter'); - if (filterType === 'common') { - setQuery(''); - setFilter({ - is_common: true - }); - } else if (filterType === 'personal' && user) { - setQuery(''); - setFilter({ - ownedBy: user.id! - }); - } + const searchFilter = new URLSearchParams(search).get('filter') as LibraryFilterStrategy | null; + const inputStrategy = searchFilter && Object.values(LibraryFilterStrategy).includes(searchFilter) ? searchFilter : LibraryFilterStrategy.MANUAL; + setQuery('') + setStrategy(inputStrategy) + setFilter(ApplyStrategy(inputStrategy)); }, [user, search, setQuery, setFilter]); + const handleChangeStrategy = useCallback( + (value: LibraryFilterStrategy) => { + if (value === strategy) { + return; + } + navigate(`/library?filter=${value}`) + }, [strategy, navigate]); + return ( -
    -
    -
    - -
    - setQuery(data.target.value)} +
    +
    + Фильтр + + {filtered} из {total} + +
    +
    + +
    +
    + +
    + +
    ); } export default SearchPanel; - - -{/*
    - - setFilterText(event.target.value)} - /> - -
    */} \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/LibraryPage/ViewLibrary.tsx b/rsconcept/frontend/src/pages/LibraryPage/ViewLibrary.tsx index 3b74064e..c1fa705c 100644 --- a/rsconcept/frontend/src/pages/LibraryPage/ViewLibrary.tsx +++ b/rsconcept/frontend/src/pages/LibraryPage/ViewLibrary.tsx @@ -124,7 +124,7 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {

    | - + | Очистить фильтр diff --git a/rsconcept/frontend/src/pages/LibraryPage/index.tsx b/rsconcept/frontend/src/pages/LibraryPage/index.tsx index 1c0f1974..0d050320 100644 --- a/rsconcept/frontend/src/pages/LibraryPage/index.tsx +++ b/rsconcept/frontend/src/pages/LibraryPage/index.tsx @@ -10,13 +10,13 @@ import ViewLibrary from './ViewLibrary'; function LibraryPage() { const library = useLibrary(); - const [ filterParams, setFilterParams ] = useState({}); + const [ filter, setFilter ] = useState({}); const [ items, setItems ] = useState([]); - useLayoutEffect(() => { - const filter = filterParams; + useLayoutEffect( + () => { setItems(library.filter(filter)); - }, [library, filterParams]); + }, [library, filter, filter.query]); return (

    @@ -24,12 +24,13 @@ function LibraryPage() { { library.error && } { !library.loading && library.items &&
    - setFilterParams({})} + cleanQuery={() => setFilter({})} items={items} />
    diff --git a/rsconcept/frontend/src/pages/LoginPage.tsx b/rsconcept/frontend/src/pages/LoginPage.tsx index dce900ab..303f62ad 100644 --- a/rsconcept/frontend/src/pages/LoginPage.tsx +++ b/rsconcept/frontend/src/pages/LoginPage.tsx @@ -34,28 +34,34 @@ function LoginPage() { username: username, password: password }; - login(data, () => { navigate('/library?filter=personal'); }); + login(data, () => navigate('/library')); } } return ( -
    { user +
    +
    { user ? {`Вы вошли в систему как ${user.username}`} - :
    + : + { setUsername(event.target.value); }} + onChange={event => setUsername(event.target.value)} /> { setPassword(event.target.value); }} + onChange={event => setPassword(event.target.value)} />
    @@ -72,6 +78,7 @@ function LoginPage() { { error && } }
    +
    ); } diff --git a/rsconcept/frontend/src/pages/RSFormPage/DlgCloneRSForm.tsx b/rsconcept/frontend/src/pages/RSFormPage/DlgCloneRSForm.tsx index 5edb0b4e..0e614216 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/DlgCloneRSForm.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/DlgCloneRSForm.tsx @@ -65,21 +65,21 @@ function DlgCloneRSForm({ hideWindow }: DlgCloneRSFormProps) { { setTitle(event.target.value); }} + onChange={event => setTitle(event.target.value)} /> { setAlias(event.target.value); }} + onChange={event => setAlias(event.target.value)} />