From 6963647c4655f4c365f36672a81d2392667eba36 Mon Sep 17 00:00:00 2001 From: "ma.derouich" Date: Thu, 26 Sep 2024 17:14:36 +0100 Subject: [PATCH 01/72] add redux and fix it --- package.json | 1 + src/App.js | 26 ++++++++------------------ src/PrivateRute.js | 11 +++++++++++ src/actions/appActions.js | 13 +++++++++++++ src/actions/authActions.js | 27 +++++++++++++++++++++++++++ src/components/AppHeader.js | 29 ++++++++++++++++++----------- src/components/AppSidebar.js | 30 +++++++++++++++--------------- src/index.js | 19 +++++++++++-------- src/reducers/appReducer.js | 30 ++++++++++++++++++++++++++++++ src/reducers/authReducer.js | 26 ++++++++++++++++++++++++++ src/reducers/index.js | 11 +++++++++++ src/store.js | 27 +++++++++++++-------------- 12 files changed, 184 insertions(+), 66 deletions(-) create mode 100644 src/PrivateRute.js create mode 100644 src/actions/appActions.js create mode 100644 src/actions/authActions.js create mode 100644 src/reducers/appReducer.js create mode 100644 src/reducers/authReducer.js create mode 100644 src/reducers/index.js diff --git a/package.json b/package.json index fde78d803..06457dbea 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@coreui/react-chartjs": "^3.0.0", "@coreui/utils": "^2.0.2", "@popperjs/core": "^2.11.8", + "axios": "^1.7.7", "chart.js": "^4.4.4", "classnames": "^2.5.1", "core-js": "^3.38.1", diff --git a/src/App.js b/src/App.js index 7f8e6d7ea..385392105 100644 --- a/src/App.js +++ b/src/App.js @@ -1,9 +1,10 @@ import React, { Suspense, useEffect } from 'react' import { HashRouter, Route, Routes } from 'react-router-dom' -import { useSelector } from 'react-redux' -import { CSpinner, useColorModes } from '@coreui/react' +import { CSpinner } from '@coreui/react' import './scss/style.scss' +import PrivateRoute from './PrivateRute' +import { isLogged } from './actions/authActions' // Containers const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout')) @@ -15,22 +16,9 @@ const Page404 = React.lazy(() => import('./views/pages/page404/Page404')) const Page500 = React.lazy(() => import('./views/pages/page500/Page500')) const App = () => { - const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') - const storedTheme = useSelector((state) => state.theme) - useEffect(() => { - const urlParams = new URLSearchParams(window.location.href.split('?')[1]) - const theme = urlParams.get('theme') && urlParams.get('theme').match(/^[A-Za-z0-9\s]+/)[0] - if (theme) { - setColorMode(theme) - } - - if (isColorModeSet()) { - return - } - - setColorMode(storedTheme) - }, []) // eslint-disable-line react-hooks/exhaustive-deps + isLogged() + }, []) return ( @@ -46,7 +34,9 @@ const App = () => { } /> } /> } /> - } /> + }> + } /> + diff --git a/src/PrivateRute.js b/src/PrivateRute.js new file mode 100644 index 000000000..f080f6150 --- /dev/null +++ b/src/PrivateRute.js @@ -0,0 +1,11 @@ +/* eslint-disable prettier/prettier */ +import React from 'react' +import { Navigate, Outlet } from 'react-router-dom' +import { useSelector } from 'react-redux' + +const PrivateRoute = () => { + const isAuthenticated = useSelector((state) => state.auth.isAuthenticated) + return isAuthenticated ? : +} + +export default PrivateRoute diff --git a/src/actions/appActions.js b/src/actions/appActions.js new file mode 100644 index 000000000..1c8daca8d --- /dev/null +++ b/src/actions/appActions.js @@ -0,0 +1,13 @@ +/* eslint-disable prettier/prettier */ +export const toggleSideBar = () => ({ + type: 'TOGGLE_SIDEBAR', +}) + +export const toggleUnfoldable = () => ({ + type: 'TOGGLE_UNFOLDABLE', +}) + +export const switchThemeMode = (theme) => ({ + type: 'CHANGE_THEME', + payload: theme +}) \ No newline at end of file diff --git a/src/actions/authActions.js b/src/actions/authActions.js new file mode 100644 index 000000000..606cf32f6 --- /dev/null +++ b/src/actions/authActions.js @@ -0,0 +1,27 @@ +/* eslint-disable prettier/prettier */ +import React from 'react' +import axios from 'axios' + +const checkAuthentication = async () => { + try { + const response = await axios.get('http://localhost:8081/auth/check-auth'); + console.log(response.data); + } catch (error) { + console.error('Error checking authentication status:', error); + console.log({ error: true, message: 'An error occurred' }); + } +}; + +export const login = (user) => ({ + type: 'LOGIN', + payload: user, +}) + +export const logout = () => ({ + type: 'LOGOUT', +}) + +export const isLogged = () => { + checkAuthentication(); + +} \ No newline at end of file diff --git a/src/components/AppHeader.js b/src/components/AppHeader.js index b10bd7e12..4b3556050 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader.js @@ -27,13 +27,12 @@ import { import { AppBreadcrumb } from './index' import { AppHeaderDropdown } from './header/index' - +import { switchThemeMode, toggleSideBar } from '../actions/appActions' const AppHeader = () => { const headerRef = useRef() const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') - const dispatch = useDispatch() - const sidebarShow = useSelector((state) => state.sidebarShow) + const { theme } = useSelector((state) => state.app) useEffect(() => { document.addEventListener('scroll', () => { @@ -42,11 +41,19 @@ const AppHeader = () => { }) }, []) + const switchColorMode = (color) => { + dispatch(switchThemeMode(color)) + } + + useEffect(() => { + setColorMode(theme) + }, [theme]) + return ( dispatch({ type: 'set', sidebarShow: !sidebarShow })} + onClick={() => dispatch(toggleSideBar())} style={{ marginInlineStart: '-14px' }} > @@ -89,9 +96,9 @@ const AppHeader = () => { {colorMode === 'dark' ? ( - ) : colorMode === 'auto' ? ( - ) : ( + // ) : colorMode === 'auto' ? ( + // )} @@ -101,7 +108,7 @@ const AppHeader = () => { className="d-flex align-items-center" as="button" type="button" - onClick={() => setColorMode('light')} + onClick={() => switchColorMode('light')} > Light @@ -110,19 +117,19 @@ const AppHeader = () => { className="d-flex align-items-center" as="button" type="button" - onClick={() => setColorMode('dark')} + onClick={() => switchColorMode('dark')} > Dark - setColorMode('auto')} + onClick={() => switchColorMode('auto')} > Auto - + */}
  • diff --git a/src/components/AppSidebar.js b/src/components/AppSidebar.js index 021cb52c3..bf4c0e320 100644 --- a/src/components/AppSidebar.js +++ b/src/components/AppSidebar.js @@ -16,41 +16,41 @@ import { AppSidebarNav } from './AppSidebarNav' import { logo } from 'src/assets/brand/logo' import { sygnet } from 'src/assets/brand/sygnet' -// sidebar nav config import navigation from '../_nav' +import { toggleSideBar, toggleUnfoldable } from '../actions/appActions' const AppSidebar = () => { const dispatch = useDispatch() - const unfoldable = useSelector((state) => state.sidebarUnfoldable) - const sidebarShow = useSelector((state) => state.sidebarShow) + const { sidebarUnfoldable, sidebarShow } = useSelector((state) => state.app) + + const handleVisibleChange = (visible) => { + if (visible !== sidebarShow) { + dispatch(toggleSideBar()) + } + } + + const handleUnfoldableChange = () => { + dispatch(toggleUnfoldable()) + } return ( { - dispatch({ type: 'set', sidebarShow: visible }) - }} + onVisibleChange={handleVisibleChange} > - dispatch({ type: 'set', sidebarShow: false })} - /> - dispatch({ type: 'set', sidebarUnfoldable: !unfoldable })} - /> + ) diff --git a/src/index.js b/src/index.js index 11d6e8658..cedd0ceed 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,16 @@ import React from 'react' +import ReactDOM from 'react-dom' +import StoreProvider from './store' +import App from './App' import { createRoot } from 'react-dom/client' -import { Provider } from 'react-redux' -import 'core-js' -import App from './App' -import store from './store' +const container = document.getElementById('root') +const root = createRoot(container) -createRoot(document.getElementById('root')).render( - - - , +root.render( + + + + + , ) diff --git a/src/reducers/appReducer.js b/src/reducers/appReducer.js new file mode 100644 index 000000000..b3e64eaca --- /dev/null +++ b/src/reducers/appReducer.js @@ -0,0 +1,30 @@ +/* eslint-disable prettier/prettier */ +const initialState = { + sidebarShow: true, + sidebarUnfoldable: false, + theme: 'light', +} + +const appReducer = (state = initialState, action) => { + switch (action.type) { + case 'TOGGLE_SIDEBAR': + return { + ...state, + sidebarShow: !state.sidebarShow + } + case 'TOGGLE_UNFOLDABLE': + return { + ...state, + sidebarUnfoldable: !state.sidebarUnfoldable + } + case 'CHANGE_THEME': + return { + ...state, + theme: action.payload + } + default: + return state + } +} + +export default appReducer diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js new file mode 100644 index 000000000..6d538eb2e --- /dev/null +++ b/src/reducers/authReducer.js @@ -0,0 +1,26 @@ +/* eslint-disable prettier/prettier */ +const initialState = { + isAuthenticated: false, + user: null, +} + +const authReducer = (state = initialState, action) => { + switch (action.type) { + case 'LOGIN': + return { + ...state, + isAuthenticated: true, + user: action.payload, + } + case 'LOGOUT': + return { + ...state, + isAuthenticated: false, + user: null, + } + default: + return state + } +} + +export default authReducer diff --git a/src/reducers/index.js b/src/reducers/index.js new file mode 100644 index 000000000..1844743f7 --- /dev/null +++ b/src/reducers/index.js @@ -0,0 +1,11 @@ +/* eslint-disable prettier/prettier */ +import { combineReducers } from 'redux' +import authReducer from './authReducer' +import appReducer from './appReducer' + +const rootReducer = combineReducers({ + auth: authReducer, + app: appReducer +}) + +export default rootReducer diff --git a/src/store.js b/src/store.js index 8ad30dad6..ad85962d0 100644 --- a/src/store.js +++ b/src/store.js @@ -1,18 +1,17 @@ +import React from 'react' import { legacy_createStore as createStore } from 'redux' +import { Provider } from 'react-redux' +import PropTypes from 'prop-types' +import rootReducer from './reducers' -const initialState = { - sidebarShow: true, - theme: 'light', -} +const store = createStore( + rootReducer, + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), +) -const changeState = (state = initialState, { type, ...rest }) => { - switch (type) { - case 'set': - return { ...state, ...rest } - default: - return state - } -} +const StoreProvider = ({ children }) => {children} -const store = createStore(changeState) -export default store +StoreProvider.propTypes = { + children: PropTypes.node, +} +export default StoreProvider From 571acbb5d7c8466e949411a52e7adbc59759c254 Mon Sep 17 00:00:00 2001 From: maderouich Date: Fri, 4 Apr 2025 16:48:19 +0100 Subject: [PATCH 02/72] chore: update project name and version in package.json --- package.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 06457dbea..b2365e647 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@coreui/coreui-free-react-admin-template", - "version": "5.2.0", + "name": "taketit", + "version": "0.0.0", "description": "CoreUI Free React Admin Template", "homepage": ".", "bugs": { @@ -28,6 +28,8 @@ "@coreui/utils": "^2.0.2", "@popperjs/core": "^2.11.8", "axios": "^1.7.7", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", "chart.js": "^4.4.4", "classnames": "^2.5.1", "core-js": "^3.38.1", @@ -37,6 +39,8 @@ "react-redux": "^9.1.2", "react-router-dom": "^6.26.2", "redux": "5.0.1", + "redux-devtools-extension": "^2.13.9", + "redux-thunk": "^3.1.0", "simplebar-react": "^3.2.6" }, "devDependencies": { @@ -50,6 +54,6 @@ "postcss": "^8.4.47", "prettier": "3.3.3", "sass": "^1.79.3", - "vite": "^5.4.8" + "vite": "^6.2.0" } } From 66731741c0a4aa03347a17836c4bba9f2f7273e0 Mon Sep 17 00:00:00 2001 From: maderouich Date: Fri, 4 Apr 2025 16:48:38 +0100 Subject: [PATCH 03/72] feat: implement authentication flow with Redux and improve login UI --- src/App.js | 10 ++- src/actions/authActions.js | 71 +++++++++++----- src/assets/images/logo.png | Bin 0 -> 46795 bytes src/reducers/authReducer.js | 59 +++++++++---- src/scss/_mixins.scss | 23 +++++ src/scss/_theme.scss | 15 ++-- src/scss/_variables.scss | 1 + src/scss/style.scss | 16 ++-- src/services/authService.js | 41 +++++++++ src/store.js | 17 ++-- src/views/pages/login/Login.js | 149 +++++++++++++++++---------------- 11 files changed, 272 insertions(+), 130 deletions(-) create mode 100644 src/assets/images/logo.png create mode 100644 src/scss/_mixins.scss create mode 100644 src/services/authService.js diff --git a/src/App.js b/src/App.js index 385392105..a940ae6f7 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,10 @@ import React, { Suspense, useEffect } from 'react' import { HashRouter, Route, Routes } from 'react-router-dom' - +import { useDispatch } from 'react-redux' import { CSpinner } from '@coreui/react' import './scss/style.scss' import PrivateRoute from './PrivateRute' -import { isLogged } from './actions/authActions' +import { checkAuthentication } from './actions/authActions' // Containers const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout')) @@ -16,9 +16,11 @@ const Page404 = React.lazy(() => import('./views/pages/page404/Page404')) const Page500 = React.lazy(() => import('./views/pages/page500/Page500')) const App = () => { + const dispatch = useDispatch() + useEffect(() => { - isLogged() - }, []) + dispatch(checkAuthentication()) + }, [dispatch]) return ( diff --git a/src/actions/authActions.js b/src/actions/authActions.js index 606cf32f6..f8c5ab2f1 100644 --- a/src/actions/authActions.js +++ b/src/actions/authActions.js @@ -1,27 +1,60 @@ /* eslint-disable prettier/prettier */ import React from 'react' -import axios from 'axios' - -const checkAuthentication = async () => { - try { - const response = await axios.get('http://localhost:8081/auth/check-auth'); - console.log(response.data); - } catch (error) { - console.error('Error checking authentication status:', error); - console.log({ error: true, message: 'An error occurred' }); - } -}; - -export const login = (user) => ({ - type: 'LOGIN', - payload: user, +import authService from '../services/authService' + +export const loginRequest = () => ({ + type: 'LOGIN_REQUEST', +}) + +export const loginSuccess = (user) => ({ + type: 'LOGIN_SUCCESS', + payload: user, +}) + +export const loginFailure = (error) => ({ + type: 'LOGIN_FAILURE', + payload: error, }) -export const logout = () => ({ +export const login = (username, password) => async (dispatch) => { + dispatch(loginRequest()) + try { + const user = await authService.login(username, password) + dispatch(loginSuccess(user)) + } catch (error) { + dispatch(loginFailure(error)) + console.error('Error logging in:', error) + } +} + +export const logout = () => (dispatch) => { + authService.logout() + dispatch({ type: 'LOGOUT', + }) +} + +export const checkAuthRequest = () => ({ + type: 'AUTH_CHECK_REQUEST', }) -export const isLogged = () => { - checkAuthentication(); +export const checkAuthSuccess = (data) => ({ + type: 'AUTH_CHECK_SUCCESS', + payload: data, +}) + +export const checkAuthFailure = (error) => ({ + type: 'AUTH_CHECK_FAILURE', + payload: error, +}) -} \ No newline at end of file +export const checkAuthentication = () => async (dispatch) => { + dispatch(checkAuthRequest()) + try { + const response = await authService.checkAuth() + dispatch(checkAuthSuccess(response.data)) + } catch (error) { + dispatch(checkAuthFailure(error)) + console.error('Error checking authentication status:', error) + } +} diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4260d2f294eb9ba2f9b3e7c5ef9c4a08e952efa7 GIT binary patch literal 46795 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 Q1N+~q%9={42h6 { - switch (action.type) { - case 'LOGIN': - return { - ...state, - isAuthenticated: true, - user: action.payload, - } - case 'LOGOUT': - return { - ...state, - isAuthenticated: false, - user: null, - } - default: - return state - } + switch (action.type) { + case 'LOGIN_REQUEST': + case 'AUTH_CHECK_REQUEST': + return { + ...state, + loading: true, + } + case 'LOGIN_SUCCESS': + return { + ...state, + isAuthenticated: true, + user: action.payload, + loading: false, + } + case 'LOGIN_FAILURE': + case 'AUTH_CHECK_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'LOGOUT': + return { + ...state, + isAuthenticated: false, + user: null, + } + case 'AUTH_CHECK_SUCCESS': + return { + ...state, + isAuthenticated: true, + user: action.payload, + loading: false, + } + default: + return state + } } export default authReducer diff --git a/src/scss/_mixins.scss b/src/scss/_mixins.scss new file mode 100644 index 000000000..0ab7c900f --- /dev/null +++ b/src/scss/_mixins.scss @@ -0,0 +1,23 @@ +@mixin ltr-rtl($property, $value) { + #{$property}: $value; + [dir="rtl"] & { + #{$property}-right: $value; + #{$property}-left: auto; + } +} + +@mixin transition($transition...) { + transition: $transition; +} + +@mixin color-mode($mode) { + @if $mode == dark { + @media (prefers-color-scheme: dark) { + @content; + } + } @else if $mode == light { + @media (prefers-color-scheme: light) { + @content; + } + } +} diff --git a/src/scss/_theme.scss b/src/scss/_theme.scss index 49e1c79e6..940a7bba9 100644 --- a/src/scss/_theme.scss +++ b/src/scss/_theme.scss @@ -1,13 +1,16 @@ +@use 'variables' as *; +@use 'mixins'; + body { background-color: var(--cui-tertiary-bg); } .wrapper { width: 100%; - @include ltr-rtl("padding-left", var(--cui-sidebar-occupy-start, 0)); - @include ltr-rtl("padding-right", var(--cui-sidebar-occupy-end, 0)); + @include mixins.ltr-rtl("padding-left", var(--cui-sidebar-occupy-start, 0)); + @include mixins.ltr-rtl("padding-right", var(--cui-sidebar-occupy-end, 0)); will-change: auto; - @include transition(padding .15s); + @include mixins.transition(padding .15s); } .header > .container-fluid, @@ -33,13 +36,13 @@ body { } .sidebar-toggler { - @include ltr-rtl("margin-left", auto); + @include mixins.ltr-rtl("margin-left", auto); } .sidebar-narrow, .sidebar-narrow-unfoldable:not(:hover) { .sidebar-toggler { - @include ltr-rtl("margin-right", auto); + @include mixins.ltr-rtl("margin-right", auto); } } @@ -52,7 +55,7 @@ body { } @if $enable-dark-mode { - @include color-mode(dark) { + @include mixins.color-mode(dark) { body { background-color: var(--cui-dark-bg-subtle); } diff --git a/src/scss/_variables.scss b/src/scss/_variables.scss index b0f8a52a3..6b4b1671a 100644 --- a/src/scss/_variables.scss +++ b/src/scss/_variables.scss @@ -3,3 +3,4 @@ // If you want to customize your project please add your variables below. $enable-deprecation-messages: false !default; +$enable-dark-mode: true; diff --git a/src/scss/style.scss b/src/scss/style.scss index 4fbc82356..d286fe0c9 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -1,15 +1,19 @@ // If you want to override variables do it here -@import "variables"; +@use "variables" as *; // Import styles -@import "@coreui/coreui/scss/coreui"; -@import "@coreui/chartjs/scss/coreui-chartjs"; +@use "@coreui/coreui/scss/coreui" as coreui; +@use "@coreui/chartjs/scss/coreui-chartjs" as chartjs; // Vendors -@import "vendors/simplebar"; +@use "vendors/simplebar" as simplebar; // Custom styles for this theme -@import "theme"; +@use "theme" as theme; // If you want to add custom CSS you can put it here -@import "custom"; +@use "custom" as custom; + +// Import Bootstrap CSS and Bootstrap Icons CSS +@import 'bootstrap/dist/css/bootstrap.min.css'; +@import 'bootstrap-icons/font/bootstrap-icons.css'; diff --git a/src/services/authService.js b/src/services/authService.js new file mode 100644 index 000000000..598e77ed9 --- /dev/null +++ b/src/services/authService.js @@ -0,0 +1,41 @@ +import axios from 'axios' + +const API_URL = 'http://localhost:8081/auth/' + +const login = async (email, password) => { + try { + const response = await axios.post(`${API_URL}signin`, { email, password }) + if (response.data.token) { + localStorage.setItem('user', JSON.stringify(response.data)) + } + return response.data + } catch (error) { + console.error('Error logging in:', error) + throw error + } +} + +const logout = () => { + localStorage.removeItem('user') +} + +const checkAuth = async () => { + try { + const response = await axios.get(`${API_URL}check-auth`) + return response + } catch (error) { + console.error('Error checking authentication status:', error) + throw error + } +} + +const getCurrentUser = () => { + return JSON.parse(localStorage.getItem('user')) +} + +export default { + login, + logout, + checkAuth, + getCurrentUser, +} diff --git a/src/store.js b/src/store.js index ad85962d0..8016125b1 100644 --- a/src/store.js +++ b/src/store.js @@ -1,13 +1,18 @@ import React from 'react' -import { legacy_createStore as createStore } from 'redux' +import { createStore, applyMiddleware, combineReducers } from 'redux' +import { thunk } from 'redux-thunk' +import { composeWithDevTools } from 'redux-devtools-extension' import { Provider } from 'react-redux' import PropTypes from 'prop-types' -import rootReducer from './reducers' +import authReducer from './reducers/authReducer' +import dataReducer from './reducers/appReducer' -const store = createStore( - rootReducer, - window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), -) +const rootReducer = combineReducers({ + auth: authReducer, + data: dataReducer, +}) + +const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk))) const StoreProvider = ({ children }) => {children} diff --git a/src/views/pages/login/Login.js b/src/views/pages/login/Login.js index 1b2ee0baa..dae3ffbe1 100644 --- a/src/views/pages/login/Login.js +++ b/src/views/pages/login/Login.js @@ -1,84 +1,91 @@ -import React from 'react' -import { Link } from 'react-router-dom' -import { - CButton, - CCard, - CCardBody, - CCardGroup, - CCol, - CContainer, - CForm, - CFormInput, - CInputGroup, - CInputGroupText, - CRow, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' -import { cilLockLocked, cilUser } from '@coreui/icons' +import React, { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { login, checkAuthentication } from '../../../actions/authActions' +import { useNavigate } from 'react-router-dom' +import logo from '../../../assets/images/logo.png' const Login = () => { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const dispatch = useDispatch() + const navigate = useNavigate() + const { isAuthenticated } = useSelector((state) => state.auth) + + const handleLogin = async (e) => { + e.preventDefault() + await dispatch(login(username, password)) + dispatch(checkAuthentication()) + if (isAuthenticated) { + navigate('/') + } + } + return ( -
    - - - - - - - +
    +
    +
    +
    +
    +
    +
    +

    Login

    -

    Sign In to your account

    - - - - - - - - - - - Sign In to your account

    +
    +
    + + + +
    + setUsername(e.target.value)} + /> +
    +
    +
    + + + +
    + setPassword(e.target.value)} /> - - - - +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + Logo +
    +
    +
    +
    +
    ) } From 811350fb37dc6c300aeee131172ad60022792b33 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Fri, 25 Apr 2025 10:50:39 +0100 Subject: [PATCH 04/72] chore: update index.html metadata and title for TakeIt project --- index.html | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/index.html b/index.html index ca596acc7..d8cf93a43 100644 --- a/index.html +++ b/index.html @@ -1,20 +1,13 @@ - - + - CoreUI Free React.js Admin Template + TakeIt From efc07f5ead5da1cbce2851ec7881de774a6f5504 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Fri, 25 Apr 2025 10:51:06 +0100 Subject: [PATCH 05/72] feat: add react-toastify for error notifications on login failure --- package.json | 1 + src/index.js | 2 ++ src/views/pages/login/Login.js | 3 +++ 3 files changed, 6 insertions(+) diff --git a/package.json b/package.json index b2365e647..ff7995be0 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react-dom": "^18.3.1", "react-redux": "^9.1.2", "react-router-dom": "^6.26.2", + "react-toastify": "^11.0.5", "redux": "5.0.1", "redux-devtools-extension": "^2.13.9", "redux-thunk": "^3.1.0", diff --git a/src/index.js b/src/index.js index cedd0ceed..1705d642d 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom' import StoreProvider from './store' import App from './App' import { createRoot } from 'react-dom/client' +import { ToastContainer } from 'react-toastify' const container = document.getElementById('root') const root = createRoot(container) @@ -10,6 +11,7 @@ const root = createRoot(container) root.render( + , diff --git a/src/views/pages/login/Login.js b/src/views/pages/login/Login.js index dae3ffbe1..d274c5c76 100644 --- a/src/views/pages/login/Login.js +++ b/src/views/pages/login/Login.js @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux' import { login, checkAuthentication } from '../../../actions/authActions' import { useNavigate } from 'react-router-dom' import logo from '../../../assets/images/logo.png' +import { toast } from 'react-toastify' const Login = () => { const [username, setUsername] = useState('') @@ -17,6 +18,8 @@ const Login = () => { dispatch(checkAuthentication()) if (isAuthenticated) { navigate('/') + } else { + toast.error('Invalid username or password') } } From ba5f70642aaf3a68c28cc4e4d791a44a0dbf6630 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Fri, 25 Apr 2025 10:53:29 +0100 Subject: [PATCH 06/72] feat: enhance ToastContainer with closeOnClick option for better user experience --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 1705d642d..75efbe22b 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ const root = createRoot(container) root.render( - + , From 4b0f002aa0da243a2b986deaab577f5bd1ab647e Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Fri, 25 Apr 2025 11:03:28 +0100 Subject: [PATCH 07/72] chore: comment out AppSidebar and AppHeader in DefaultLayout for cleaner layout --- src/layout/DefaultLayout.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layout/DefaultLayout.js b/src/layout/DefaultLayout.js index 19fbf225f..535d78cc2 100644 --- a/src/layout/DefaultLayout.js +++ b/src/layout/DefaultLayout.js @@ -4,9 +4,9 @@ import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/inde const DefaultLayout = () => { return (
    - + {/* */}
    - + {/* */}
    From f11a901c052a4c61f49d56a78f781d814a678a8d Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Fri, 25 Apr 2025 11:17:00 +0100 Subject: [PATCH 08/72] feat: refactor authentication check and replace HashRouter with BrowserRouter for improved routing --- src/App.js | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/App.js b/src/App.js index a940ae6f7..bc8547705 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,5 @@ -import React, { Suspense, useEffect } from 'react' -import { HashRouter, Route, Routes } from 'react-router-dom' +import React, { Suspense, useEffect, useState } from 'react' +import { BrowserRouter, Route, Routes } from 'react-router-dom' import { useDispatch } from 'react-redux' import { CSpinner } from '@coreui/react' import './scss/style.scss' @@ -17,13 +17,31 @@ const Page500 = React.lazy(() => import('./views/pages/page500/Page500')) const App = () => { const dispatch = useDispatch() + const [isChecking, setIsChecking] = useState(true) useEffect(() => { - dispatch(checkAuthentication()) + const checkAuth = async () => { + try { + await dispatch(checkAuthentication()) + } catch (error) { + console.error('Authentication check failed:', error) + } finally { + setIsChecking(false) + } + } + checkAuth() }, [dispatch]) + if (isChecking) { + return ( +
    + +
    + ) + } + return ( - + @@ -32,16 +50,17 @@ const App = () => { } > - } /> - } /> - } /> - } /> - }> + } /> + } /> + } /> + } /> + } exact> + } /> } /> - + ) } From 7c87f9d5f1b1de6871385bea904e4860f8f56743 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Fri, 25 Apr 2025 11:31:12 +0100 Subject: [PATCH 09/72] fix: update state selector in AppHeader and AppSidebar to use 'data' instead of 'app' chore: uncomment AppSidebar and AppHeader in DefaultLayout for proper rendering --- src/components/AppHeader.js | 2 +- src/components/AppSidebar.js | 2 +- src/layout/DefaultLayout.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/AppHeader.js b/src/components/AppHeader.js index 4b3556050..70beef626 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader.js @@ -32,7 +32,7 @@ const AppHeader = () => { const headerRef = useRef() const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') const dispatch = useDispatch() - const { theme } = useSelector((state) => state.app) + const { theme } = useSelector((state) => state.data) useEffect(() => { document.addEventListener('scroll', () => { diff --git a/src/components/AppSidebar.js b/src/components/AppSidebar.js index bf4c0e320..5e13bdaa6 100644 --- a/src/components/AppSidebar.js +++ b/src/components/AppSidebar.js @@ -21,7 +21,7 @@ import { toggleSideBar, toggleUnfoldable } from '../actions/appActions' const AppSidebar = () => { const dispatch = useDispatch() - const { sidebarUnfoldable, sidebarShow } = useSelector((state) => state.app) + const { sidebarUnfoldable, sidebarShow } = useSelector((state) => state.data) const handleVisibleChange = (visible) => { if (visible !== sidebarShow) { diff --git a/src/layout/DefaultLayout.js b/src/layout/DefaultLayout.js index 535d78cc2..19fbf225f 100644 --- a/src/layout/DefaultLayout.js +++ b/src/layout/DefaultLayout.js @@ -4,9 +4,9 @@ import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/inde const DefaultLayout = () => { return (
    - {/* */} +
    - {/* */} +
    From 1ab36e2d163e7b406501e016ff6889d5984505f5 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Fri, 25 Apr 2025 12:14:02 +0100 Subject: [PATCH 10/72] feat: add sidebar styles and RTL support - Introduced new sidebar SCSS file with comprehensive styles for sidebar components including fixed, overlaid, and responsive behaviors. - Added RTL support with a new bootstrap.rtl.scss file. - Created a new bootstrap.scss file to define core color and theme variables. - Implemented utility API for responsive utilities and RFS (Responsive Font Sizes) functionality. - Updated main style.scss to import coreui styles from local paths. --- src/scss/scss/_accordion.import.scss | 1 + src/scss/scss/_accordion.scss | 161 ++ src/scss/scss/_alert.import.scss | 1 + src/scss/scss/_alert.scss | 72 + src/scss/scss/_avatar.import.scss | 1 + src/scss/scss/_avatar.scss | 66 + src/scss/scss/_badge.import.scss | 1 + src/scss/scss/_badge.scss | 52 + src/scss/scss/_banner.scss | 7 + src/scss/scss/_breadcrumb.import.scss | 1 + src/scss/scss/_breadcrumb.scss | 51 + src/scss/scss/_button-group.import.scss | 1 + src/scss/scss/_button-group.scss | 152 ++ src/scss/scss/_buttons.import.scss | 1 + src/scss/scss/_buttons.scss | 256 ++ src/scss/scss/_callout.import.scss | 1 + src/scss/scss/_callout.scss | 31 + src/scss/scss/_card.import.scss | 1 + src/scss/scss/_card.scss | 243 ++ src/scss/scss/_carousel.import.scss | 1 + src/scss/scss/_carousel.scss | 234 ++ src/scss/scss/_close.import.scss | 1 + src/scss/scss/_close.scss | 72 + src/scss/scss/_containers.import.scss | 1 + src/scss/scss/_containers.scss | 45 + src/scss/scss/_dropdown.import.scss | 1 + src/scss/scss/_dropdown.scss | 259 +++ src/scss/scss/_footer.import.scss | 1 + src/scss/scss/_footer.scss | 37 + src/scss/scss/_forms.import.scss | 9 + src/scss/scss/_forms.scss | 9 + src/scss/scss/_functions.import.scss | 1 + src/scss/scss/_functions.scss | 12 + src/scss/scss/_grid.import.scss | 1 + src/scss/scss/_grid.scss | 63 + src/scss/scss/_header.import.scss | 1 + src/scss/scss/_header.scss | 191 ++ src/scss/scss/_helpers.import.scss | 1 + src/scss/scss/_helpers.scss | 12 + src/scss/scss/_icon.import.scss | 1 + src/scss/scss/_icon.scss | 35 + src/scss/scss/_images.import.scss | 1 + src/scss/scss/_images.scss | 48 + src/scss/scss/_list-group.import.scss | 1 + src/scss/scss/_list-group.scss | 204 ++ src/scss/scss/_maps.import.scss | 1 + src/scss/scss/_maps.scss | 186 ++ src/scss/scss/_mixins.import.scss | 1 + src/scss/scss/_mixins.scss | 48 + src/scss/scss/_modal.import.scss | 1 + src/scss/scss/_modal.scss | 246 ++ src/scss/scss/_nav.import.scss | 1 + src/scss/scss/_nav.scss | 245 ++ src/scss/scss/_navbar.import.scss | 1 + src/scss/scss/_navbar.scss | 301 +++ src/scss/scss/_offcanvas.import.scss | 1 + src/scss/scss/_offcanvas.scss | 153 ++ src/scss/scss/_pagination.import.scss | 1 + src/scss/scss/_pagination.scss | 117 + src/scss/scss/_placeholders.import.scss | 1 + src/scss/scss/_placeholders.scss | 53 + src/scss/scss/_popover.import.scss | 1 + src/scss/scss/_popover.scss | 202 ++ src/scss/scss/_progress.import.scss | 1 + src/scss/scss/_progress.scss | 118 + src/scss/scss/_reboot.import.scss | 1 + src/scss/scss/_reboot.scss | 613 +++++ src/scss/scss/_root.import.scss | 1 + src/scss/scss/_root.scss | 238 ++ src/scss/scss/_sidebar.import.scss | 3 + src/scss/scss/_sidebar.scss | 3 + src/scss/scss/_spinners.import.scss | 1 + src/scss/scss/_spinners.scss | 87 + src/scss/scss/_tables.import.scss | 1 + src/scss/scss/_tables.scss | 176 ++ src/scss/scss/_toasts.import.scss | 1 + src/scss/scss/_toasts.scss | 76 + src/scss/scss/_tooltip.import.scss | 1 + src/scss/scss/_tooltip.scss | 125 + src/scss/scss/_transitions.import.scss | 1 + src/scss/scss/_transitions.scss | 30 + src/scss/scss/_type.import.scss | 1 + src/scss/scss/_type.scss | 111 + src/scss/scss/_utilities.import.scss | 1 + src/scss/scss/_utilities.scss | 865 +++++++ src/scss/scss/_variables-dark.import.scss | 1 + src/scss/scss/_variables-dark.scss | 165 ++ src/scss/scss/_variables.import.scss | 1 + src/scss/scss/_variables.scss | 2070 +++++++++++++++++ src/scss/scss/coreui-grid.rtl.scss | 4 + src/scss/scss/coreui-grid.scss | 54 + src/scss/scss/coreui-reboot.rtl.scss | 4 + src/scss/scss/coreui-reboot.scss | 9 + src/scss/scss/coreui-utilities.rtl.scss | 4 + src/scss/scss/coreui-utilities.scss | 16 + src/scss/scss/coreui.rtl.scss | 4 + src/scss/scss/coreui.scss | 55 + .../scss/forms/_floating-labels.import.scss | 1 + src/scss/scss/forms/_floating-labels.scss | 101 + src/scss/scss/forms/_form-check.import.scss | 1 + src/scss/scss/forms/_form-check.scss | 214 ++ src/scss/scss/forms/_form-control.import.scss | 1 + src/scss/scss/forms/_form-control.scss | 222 ++ src/scss/scss/forms/_form-range.import.scss | 1 + src/scss/scss/forms/_form-range.scss | 98 + src/scss/scss/forms/_form-select.import.scss | 1 + src/scss/scss/forms/_form-select.scss | 93 + src/scss/scss/forms/_form-text.import.scss | 1 + src/scss/scss/forms/_form-text.scss | 14 + src/scss/scss/forms/_input-group.import.scss | 1 + src/scss/scss/forms/_input-group.scss | 138 ++ src/scss/scss/forms/_labels.import.scss | 1 + src/scss/scss/forms/_labels.scss | 40 + src/scss/scss/forms/_validation.import.scss | 1 + src/scss/scss/forms/_validation.scss | 15 + .../scss/functions/_assert-ascending.scss | 19 + .../functions/_assert-starts-at-zero.scss | 14 + .../functions/_color-contrast-variables.scss | 23 + src/scss/scss/functions/_color-contrast.scss | 27 + src/scss/scss/functions/_color.scss | 18 + src/scss/scss/functions/_contrast-ratio.scss | 35 + src/scss/scss/functions/_escape-svg.scss | 22 + src/scss/scss/functions/_maps.scss | 57 + src/scss/scss/functions/_math.scss | 87 + src/scss/scss/functions/_rgba-css-var.scss | 9 + src/scss/scss/functions/_str-replace.scss | 19 + src/scss/scss/functions/_to-rgb.scss | 5 + src/scss/scss/helpers/_clearfix.scss | 5 + src/scss/scss/helpers/_color-bg.scss | 25 + src/scss/scss/helpers/_colored-links.scss | 34 + src/scss/scss/helpers/_focus-ring.scss | 7 + src/scss/scss/helpers/_icon-link.scss | 28 + src/scss/scss/helpers/_position.scss | 40 + src/scss/scss/helpers/_ratio.scss | 28 + src/scss/scss/helpers/_stacks.scss | 15 + src/scss/scss/helpers/_stretched-link.scss | 17 + src/scss/scss/helpers/_text-truncation.scss | 9 + src/scss/scss/helpers/_visually-hidden.scss | 10 + src/scss/scss/helpers/_vr.scss | 11 + src/scss/scss/mixins/_alert.scss | 21 + src/scss/scss/mixins/_avatar.scss | 12 + src/scss/scss/mixins/_backdrop.scss | 16 + src/scss/scss/mixins/_border-radius.scss | 82 + src/scss/scss/mixins/_box-shadow.scss | 21 + src/scss/scss/mixins/_breakpoints.scss | 204 ++ src/scss/scss/mixins/_buttons.scss | 103 + src/scss/scss/mixins/_caret.scss | 71 + src/scss/scss/mixins/_clearfix.scss | 9 + src/scss/scss/mixins/_color-mode.scss | 23 + src/scss/scss/mixins/_color-scheme.scss | 7 + src/scss/scss/mixins/_container.scss | 13 + src/scss/scss/mixins/_deprecate.scss | 12 + src/scss/scss/mixins/_forms.scss | 170 ++ src/scss/scss/mixins/_gradients.scss | 49 + src/scss/scss/mixins/_grid.scss | 220 ++ src/scss/scss/mixins/_icon.scss | 6 + src/scss/scss/mixins/_image.scss | 16 + src/scss/scss/mixins/_list-group.scss | 22 + src/scss/scss/mixins/_lists.scss | 7 + src/scss/scss/mixins/_ltr-rtl.scss | 118 + src/scss/scss/mixins/_pagination.scss | 13 + src/scss/scss/mixins/_reset-text.scss | 18 + src/scss/scss/mixins/_resize.scss | 6 + src/scss/scss/mixins/_table-variants.scss | 30 + src/scss/scss/mixins/_text-truncate.scss | 8 + src/scss/scss/mixins/_transition.scss | 29 + src/scss/scss/mixins/_utilities.import.scss | 1 + src/scss/scss/mixins/_utilities.scss | 140 ++ src/scss/scss/mixins/_visually-hidden.scss | 33 + src/scss/scss/sidebar/_sidebar-narrow.scss | 102 + src/scss/scss/sidebar/_sidebar-nav.scss | 272 +++ src/scss/scss/sidebar/_sidebar.scss | 278 +++ .../scss/themes/bootstrap/bootstrap.rtl.scss | 4 + src/scss/scss/themes/bootstrap/bootstrap.scss | 125 + src/scss/scss/utilities/_api.import.scss | 1 + src/scss/scss/utilities/_api.scss | 55 + src/scss/scss/vendor/_rfs.scss | 354 +++ src/scss/style.scss | 6 +- 178 files changed, 12927 insertions(+), 2 deletions(-) create mode 100644 src/scss/scss/_accordion.import.scss create mode 100644 src/scss/scss/_accordion.scss create mode 100644 src/scss/scss/_alert.import.scss create mode 100644 src/scss/scss/_alert.scss create mode 100644 src/scss/scss/_avatar.import.scss create mode 100644 src/scss/scss/_avatar.scss create mode 100644 src/scss/scss/_badge.import.scss create mode 100644 src/scss/scss/_badge.scss create mode 100644 src/scss/scss/_banner.scss create mode 100644 src/scss/scss/_breadcrumb.import.scss create mode 100644 src/scss/scss/_breadcrumb.scss create mode 100644 src/scss/scss/_button-group.import.scss create mode 100644 src/scss/scss/_button-group.scss create mode 100644 src/scss/scss/_buttons.import.scss create mode 100644 src/scss/scss/_buttons.scss create mode 100644 src/scss/scss/_callout.import.scss create mode 100644 src/scss/scss/_callout.scss create mode 100644 src/scss/scss/_card.import.scss create mode 100644 src/scss/scss/_card.scss create mode 100644 src/scss/scss/_carousel.import.scss create mode 100644 src/scss/scss/_carousel.scss create mode 100644 src/scss/scss/_close.import.scss create mode 100644 src/scss/scss/_close.scss create mode 100644 src/scss/scss/_containers.import.scss create mode 100644 src/scss/scss/_containers.scss create mode 100644 src/scss/scss/_dropdown.import.scss create mode 100644 src/scss/scss/_dropdown.scss create mode 100644 src/scss/scss/_footer.import.scss create mode 100644 src/scss/scss/_footer.scss create mode 100644 src/scss/scss/_forms.import.scss create mode 100644 src/scss/scss/_forms.scss create mode 100644 src/scss/scss/_functions.import.scss create mode 100644 src/scss/scss/_functions.scss create mode 100644 src/scss/scss/_grid.import.scss create mode 100644 src/scss/scss/_grid.scss create mode 100644 src/scss/scss/_header.import.scss create mode 100644 src/scss/scss/_header.scss create mode 100644 src/scss/scss/_helpers.import.scss create mode 100644 src/scss/scss/_helpers.scss create mode 100644 src/scss/scss/_icon.import.scss create mode 100644 src/scss/scss/_icon.scss create mode 100644 src/scss/scss/_images.import.scss create mode 100644 src/scss/scss/_images.scss create mode 100644 src/scss/scss/_list-group.import.scss create mode 100644 src/scss/scss/_list-group.scss create mode 100644 src/scss/scss/_maps.import.scss create mode 100644 src/scss/scss/_maps.scss create mode 100644 src/scss/scss/_mixins.import.scss create mode 100644 src/scss/scss/_mixins.scss create mode 100644 src/scss/scss/_modal.import.scss create mode 100644 src/scss/scss/_modal.scss create mode 100644 src/scss/scss/_nav.import.scss create mode 100644 src/scss/scss/_nav.scss create mode 100644 src/scss/scss/_navbar.import.scss create mode 100644 src/scss/scss/_navbar.scss create mode 100644 src/scss/scss/_offcanvas.import.scss create mode 100644 src/scss/scss/_offcanvas.scss create mode 100644 src/scss/scss/_pagination.import.scss create mode 100644 src/scss/scss/_pagination.scss create mode 100644 src/scss/scss/_placeholders.import.scss create mode 100644 src/scss/scss/_placeholders.scss create mode 100644 src/scss/scss/_popover.import.scss create mode 100644 src/scss/scss/_popover.scss create mode 100644 src/scss/scss/_progress.import.scss create mode 100644 src/scss/scss/_progress.scss create mode 100644 src/scss/scss/_reboot.import.scss create mode 100644 src/scss/scss/_reboot.scss create mode 100644 src/scss/scss/_root.import.scss create mode 100644 src/scss/scss/_root.scss create mode 100644 src/scss/scss/_sidebar.import.scss create mode 100644 src/scss/scss/_sidebar.scss create mode 100644 src/scss/scss/_spinners.import.scss create mode 100644 src/scss/scss/_spinners.scss create mode 100644 src/scss/scss/_tables.import.scss create mode 100644 src/scss/scss/_tables.scss create mode 100644 src/scss/scss/_toasts.import.scss create mode 100644 src/scss/scss/_toasts.scss create mode 100644 src/scss/scss/_tooltip.import.scss create mode 100644 src/scss/scss/_tooltip.scss create mode 100644 src/scss/scss/_transitions.import.scss create mode 100644 src/scss/scss/_transitions.scss create mode 100644 src/scss/scss/_type.import.scss create mode 100644 src/scss/scss/_type.scss create mode 100644 src/scss/scss/_utilities.import.scss create mode 100644 src/scss/scss/_utilities.scss create mode 100644 src/scss/scss/_variables-dark.import.scss create mode 100644 src/scss/scss/_variables-dark.scss create mode 100644 src/scss/scss/_variables.import.scss create mode 100644 src/scss/scss/_variables.scss create mode 100644 src/scss/scss/coreui-grid.rtl.scss create mode 100644 src/scss/scss/coreui-grid.scss create mode 100644 src/scss/scss/coreui-reboot.rtl.scss create mode 100644 src/scss/scss/coreui-reboot.scss create mode 100644 src/scss/scss/coreui-utilities.rtl.scss create mode 100644 src/scss/scss/coreui-utilities.scss create mode 100644 src/scss/scss/coreui.rtl.scss create mode 100644 src/scss/scss/coreui.scss create mode 100644 src/scss/scss/forms/_floating-labels.import.scss create mode 100644 src/scss/scss/forms/_floating-labels.scss create mode 100644 src/scss/scss/forms/_form-check.import.scss create mode 100644 src/scss/scss/forms/_form-check.scss create mode 100644 src/scss/scss/forms/_form-control.import.scss create mode 100644 src/scss/scss/forms/_form-control.scss create mode 100644 src/scss/scss/forms/_form-range.import.scss create mode 100644 src/scss/scss/forms/_form-range.scss create mode 100644 src/scss/scss/forms/_form-select.import.scss create mode 100644 src/scss/scss/forms/_form-select.scss create mode 100644 src/scss/scss/forms/_form-text.import.scss create mode 100644 src/scss/scss/forms/_form-text.scss create mode 100644 src/scss/scss/forms/_input-group.import.scss create mode 100644 src/scss/scss/forms/_input-group.scss create mode 100644 src/scss/scss/forms/_labels.import.scss create mode 100644 src/scss/scss/forms/_labels.scss create mode 100644 src/scss/scss/forms/_validation.import.scss create mode 100644 src/scss/scss/forms/_validation.scss create mode 100644 src/scss/scss/functions/_assert-ascending.scss create mode 100644 src/scss/scss/functions/_assert-starts-at-zero.scss create mode 100644 src/scss/scss/functions/_color-contrast-variables.scss create mode 100644 src/scss/scss/functions/_color-contrast.scss create mode 100644 src/scss/scss/functions/_color.scss create mode 100644 src/scss/scss/functions/_contrast-ratio.scss create mode 100644 src/scss/scss/functions/_escape-svg.scss create mode 100644 src/scss/scss/functions/_maps.scss create mode 100644 src/scss/scss/functions/_math.scss create mode 100644 src/scss/scss/functions/_rgba-css-var.scss create mode 100644 src/scss/scss/functions/_str-replace.scss create mode 100644 src/scss/scss/functions/_to-rgb.scss create mode 100644 src/scss/scss/helpers/_clearfix.scss create mode 100644 src/scss/scss/helpers/_color-bg.scss create mode 100644 src/scss/scss/helpers/_colored-links.scss create mode 100644 src/scss/scss/helpers/_focus-ring.scss create mode 100644 src/scss/scss/helpers/_icon-link.scss create mode 100644 src/scss/scss/helpers/_position.scss create mode 100644 src/scss/scss/helpers/_ratio.scss create mode 100644 src/scss/scss/helpers/_stacks.scss create mode 100644 src/scss/scss/helpers/_stretched-link.scss create mode 100644 src/scss/scss/helpers/_text-truncation.scss create mode 100644 src/scss/scss/helpers/_visually-hidden.scss create mode 100644 src/scss/scss/helpers/_vr.scss create mode 100644 src/scss/scss/mixins/_alert.scss create mode 100644 src/scss/scss/mixins/_avatar.scss create mode 100644 src/scss/scss/mixins/_backdrop.scss create mode 100644 src/scss/scss/mixins/_border-radius.scss create mode 100644 src/scss/scss/mixins/_box-shadow.scss create mode 100644 src/scss/scss/mixins/_breakpoints.scss create mode 100644 src/scss/scss/mixins/_buttons.scss create mode 100644 src/scss/scss/mixins/_caret.scss create mode 100644 src/scss/scss/mixins/_clearfix.scss create mode 100644 src/scss/scss/mixins/_color-mode.scss create mode 100644 src/scss/scss/mixins/_color-scheme.scss create mode 100644 src/scss/scss/mixins/_container.scss create mode 100644 src/scss/scss/mixins/_deprecate.scss create mode 100644 src/scss/scss/mixins/_forms.scss create mode 100644 src/scss/scss/mixins/_gradients.scss create mode 100644 src/scss/scss/mixins/_grid.scss create mode 100644 src/scss/scss/mixins/_icon.scss create mode 100644 src/scss/scss/mixins/_image.scss create mode 100644 src/scss/scss/mixins/_list-group.scss create mode 100644 src/scss/scss/mixins/_lists.scss create mode 100644 src/scss/scss/mixins/_ltr-rtl.scss create mode 100644 src/scss/scss/mixins/_pagination.scss create mode 100644 src/scss/scss/mixins/_reset-text.scss create mode 100644 src/scss/scss/mixins/_resize.scss create mode 100644 src/scss/scss/mixins/_table-variants.scss create mode 100644 src/scss/scss/mixins/_text-truncate.scss create mode 100644 src/scss/scss/mixins/_transition.scss create mode 100644 src/scss/scss/mixins/_utilities.import.scss create mode 100644 src/scss/scss/mixins/_utilities.scss create mode 100644 src/scss/scss/mixins/_visually-hidden.scss create mode 100644 src/scss/scss/sidebar/_sidebar-narrow.scss create mode 100644 src/scss/scss/sidebar/_sidebar-nav.scss create mode 100644 src/scss/scss/sidebar/_sidebar.scss create mode 100644 src/scss/scss/themes/bootstrap/bootstrap.rtl.scss create mode 100644 src/scss/scss/themes/bootstrap/bootstrap.scss create mode 100644 src/scss/scss/utilities/_api.import.scss create mode 100644 src/scss/scss/utilities/_api.scss create mode 100644 src/scss/scss/vendor/_rfs.scss diff --git a/src/scss/scss/_accordion.import.scss b/src/scss/scss/_accordion.import.scss new file mode 100644 index 000000000..65bc6f97a --- /dev/null +++ b/src/scss/scss/_accordion.import.scss @@ -0,0 +1 @@ +@forward "accordion"; diff --git a/src/scss/scss/_accordion.scss b/src/scss/scss/_accordion.scss new file mode 100644 index 000000000..28bc11efc --- /dev/null +++ b/src/scss/scss/_accordion.scss @@ -0,0 +1,161 @@ +@use "functions/escape-svg" as *; +@use "mixins/border-radius" as *; +@use "mixins/color-mode" as *; +@use "mixins/transition" as *; +@use "vendor/rfs" as *; +@use "variables" as *; +@use "variables-dark" as *; + +// +// Base styles +// + +.accordion { + // scss-docs-start accordion-css-vars + --#{$prefix}accordion-color: #{$accordion-color}; + --#{$prefix}accordion-bg: #{$accordion-bg}; + --#{$prefix}accordion-transition: #{$accordion-transition}; + --#{$prefix}accordion-border-color: #{$accordion-border-color}; + --#{$prefix}accordion-border-width: #{$accordion-border-width}; + --#{$prefix}accordion-border-radius: #{$accordion-border-radius}; + --#{$prefix}accordion-inner-border-radius: #{$accordion-inner-border-radius}; + --#{$prefix}accordion-btn-padding-x: #{$accordion-button-padding-x}; + --#{$prefix}accordion-btn-padding-y: #{$accordion-button-padding-y}; + --#{$prefix}accordion-btn-color: #{$accordion-button-color}; + --#{$prefix}accordion-btn-bg: #{$accordion-button-bg}; + --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon)}; + --#{$prefix}accordion-btn-icon-width: #{$accordion-icon-width}; + --#{$prefix}accordion-btn-icon-transform: #{$accordion-icon-transform}; + --#{$prefix}accordion-btn-icon-transition: #{$accordion-icon-transition}; + --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon)}; + --#{$prefix}accordion-btn-focus-box-shadow: #{$accordion-button-focus-box-shadow}; + --#{$prefix}accordion-body-padding-x: #{$accordion-body-padding-x}; + --#{$prefix}accordion-body-padding-y: #{$accordion-body-padding-y}; + --#{$prefix}accordion-active-color: #{$accordion-button-active-color}; + --#{$prefix}accordion-active-bg: #{$accordion-button-active-bg}; + // scss-docs-end accordion-css-vars +} + +.accordion-button { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: var(--#{$prefix}accordion-btn-padding-y) var(--#{$prefix}accordion-btn-padding-x); + @include font-size($font-size-base); + color: var(--#{$prefix}accordion-btn-color); + text-align: start; // Reset button style + background-color: var(--#{$prefix}accordion-btn-bg); + border: 0; + @include border-radius(0); + overflow-anchor: none; + @include transition(var(--#{$prefix}accordion-transition)); + + &:not(.collapsed) { + color: var(--#{$prefix}accordion-active-color); + background-color: var(--#{$prefix}accordion-active-bg); + box-shadow: inset 0 calc(-1 * var(--#{$prefix}accordion-border-width)) 0 var(--#{$prefix}accordion-border-color); // stylelint-disable-line function-disallowed-list + + &::after { + background-image: var(--#{$prefix}accordion-btn-active-icon); + transform: var(--#{$prefix}accordion-btn-icon-transform); + } + } + + // Accordion icon + &::after { + flex-shrink: 0; + width: var(--#{$prefix}accordion-btn-icon-width); + height: var(--#{$prefix}accordion-btn-icon-width); + margin-inline-start: auto; + content: ""; + background-image: var(--#{$prefix}accordion-btn-icon); + background-repeat: no-repeat; + background-size: var(--#{$prefix}accordion-btn-icon-width); + @include transition(var(--#{$prefix}accordion-btn-icon-transition)); + } + + &:hover { + z-index: 2; + } + + &:focus { + z-index: 3; + outline: 0; + box-shadow: var(--#{$prefix}accordion-btn-focus-box-shadow); + } +} + +.accordion-header { + margin-bottom: 0; +} + +.accordion-item { + color: var(--#{$prefix}accordion-color); + background-color: var(--#{$prefix}accordion-bg); + border: var(--#{$prefix}accordion-border-width) solid var(--#{$prefix}accordion-border-color); + + &:first-of-type { + @include border-top-radius(var(--#{$prefix}accordion-border-radius)); + + > .accordion-header .accordion-button { + @include border-top-radius(var(--#{$prefix}accordion-inner-border-radius)); + } + } + + &:not(:first-of-type) { + border-top: 0; + } + + // Only set a border-radius on the last item if the accordion is collapsed + &:last-of-type { + @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); + + > .accordion-header .accordion-button { + &.collapsed { + @include border-bottom-radius(var(--#{$prefix}accordion-inner-border-radius)); + } + } + + > .accordion-collapse { + @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); + } + } +} + +.accordion-body { + padding: var(--#{$prefix}accordion-body-padding-y) var(--#{$prefix}accordion-body-padding-x); +} + + +// Flush accordion items +// +// Remove borders and border-radius to keep accordion items edge-to-edge. + +.accordion-flush { + > .accordion-item { + border-right: 0; + border-left: 0; + @include border-radius(0); + + &:first-child { border-top: 0; } + &:last-child { border-bottom: 0; } + + // stylelint-disable selector-max-class + > .accordion-collapse, + > .accordion-header .accordion-button, + > .accordion-header .accordion-button.collapsed { + @include border-radius(0); + } + // stylelint-enable selector-max-class + } +} + +@if $enable-dark-mode { + @include color-mode(dark) { + .accordion-button::after { + --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon-dark)}; + --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon-dark)}; + } + } +} diff --git a/src/scss/scss/_alert.import.scss b/src/scss/scss/_alert.import.scss new file mode 100644 index 000000000..faaf45081 --- /dev/null +++ b/src/scss/scss/_alert.import.scss @@ -0,0 +1 @@ +@forward "alert"; diff --git a/src/scss/scss/_alert.scss b/src/scss/scss/_alert.scss new file mode 100644 index 000000000..0bfe341c3 --- /dev/null +++ b/src/scss/scss/_alert.scss @@ -0,0 +1,72 @@ +@use "sass:map"; +@use "mixins/border-radius" as *; +@use "variables" as *; + +// +// Base styles +// + +.alert { + // scss-docs-start alert-css-vars + --#{$prefix}alert-bg: transparent; + --#{$prefix}alert-padding-x: #{$alert-padding-x}; + --#{$prefix}alert-padding-y: #{$alert-padding-y}; + --#{$prefix}alert-margin-bottom: #{$alert-margin-bottom}; + --#{$prefix}alert-color: inherit; + --#{$prefix}alert-border-color: transparent; + --#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color); + --#{$prefix}alert-border-radius: #{$alert-border-radius}; + --#{$prefix}alert-link-color: inherit; + // scss-docs-end alert-css-vars + + position: relative; + padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x); + margin-bottom: var(--#{$prefix}alert-margin-bottom); + color: var(--#{$prefix}alert-color); + background-color: var(--#{$prefix}alert-bg); + border: var(--#{$prefix}alert-border); + @include border-radius(var(--#{$prefix}alert-border-radius)); +} + +// Headings for larger alerts +.alert-heading { + // Specified to prevent conflicts of changing $headings-color + color: inherit; +} + +// Provide class for links that match alerts +.alert-link { + font-weight: $alert-link-font-weight; + color: var(--#{$prefix}alert-link-color); +} + + +// Dismissible alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissible { + padding-inline-end: $alert-dismissible-padding-r; + + // Adjust close link position + .btn-close { + position: absolute; + inset-inline-end: 0; + top: 0; + z-index: $stretched-link-z-index + 1; + padding: $alert-padding-y * 1.25 $alert-padding-x; + } +} + + +// scss-docs-start alert-modifiers +// Generate contextual modifier classes for colorizing the alert. +@each $state in map.keys($theme-colors) { + .alert-#{$state} { + --#{$prefix}alert-color: var(--#{$prefix}#{$state}-text-emphasis); + --#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle); + --#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle); + --#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text-emphasis); + } +} +// scss-docs-end alert-modifiers diff --git a/src/scss/scss/_avatar.import.scss b/src/scss/scss/_avatar.import.scss new file mode 100644 index 000000000..347d5ad64 --- /dev/null +++ b/src/scss/scss/_avatar.import.scss @@ -0,0 +1 @@ +@forward "avatar"; diff --git a/src/scss/scss/_avatar.scss b/src/scss/scss/_avatar.scss new file mode 100644 index 000000000..d2ce21f6e --- /dev/null +++ b/src/scss/scss/_avatar.scss @@ -0,0 +1,66 @@ +@use "sass:map"; +@use "mixins/border-radius" as *; +@use "mixins/transition" as *; +@use "variables" as *; + +.avatar { + // scss-docs-start avatar-css-vars + --#{$prefix}avatar-width: #{$avatar-width}; + --#{$prefix}avatar-height: #{$avatar-height}; + --#{$prefix}avatar-font-size: #{$avatar-font-size}; + --#{$prefix}avatar-border-radius: #{$avatar-border-radius}; + --#{$prefix}avatar-status-width: #{$avatar-status-width}; + --#{$prefix}avatar-status-height: #{$avatar-status-height}; + --#{$prefix}avatar-status-border-radius: #{$avatar-status-border-radius}; + // scss-docs-end avatar-css-vars + + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--#{$prefix}avatar-width); + height: var(--#{$prefix}avatar-height); + font-size: var(--#{$prefix}avatar-font-size); + vertical-align: middle; + @include border-radius(var(--#{$prefix}avatar-border-radius)); + @include transition($avatar-transition); +} + +.avatar-img { + width: 100%; + height: auto; + @include border-radius(var(--#{$prefix}avatar-border-radius)); +} + +.avatar-status { + position: absolute; + inset-inline-end: 0; + bottom: 0; + display: block; + width: var(--#{$prefix}avatar-status-width); + height: var(--#{$prefix}avatar-status-height); + border: 1px solid $white; + @include border-radius(var(--#{$prefix}avatar-status-border-radius)); +} + +@each $size, $map in $avatar-sizes { + .avatar-#{$size} { + --#{$prefix}avatar-width: #{map.get($map, "width")}; + --#{$prefix}avatar-height: #{map.get($map, "height")}; + --#{$prefix}avatar-font-size: #{map.get($map, "font-size")}; + --#{$prefix}avatar-status-width: #{map.get($map, "status-width")}; + --#{$prefix}avatar-status-height: #{map.get($map, "status-height")}; + } +} + +.avatars-stack { + display: flex; + + .avatar { + margin-inline-end: calc(-.4 * var(--#{$prefix}avatar-width)); // stylelint-disable-line function-disallowed-list + + &:hover { + margin-inline-end: 0; + } + } +} diff --git a/src/scss/scss/_badge.import.scss b/src/scss/scss/_badge.import.scss new file mode 100644 index 000000000..97e7f4439 --- /dev/null +++ b/src/scss/scss/_badge.import.scss @@ -0,0 +1 @@ +@forward "badge"; diff --git a/src/scss/scss/_badge.scss b/src/scss/scss/_badge.scss new file mode 100644 index 000000000..f205fc5d9 --- /dev/null +++ b/src/scss/scss/_badge.scss @@ -0,0 +1,52 @@ +@use "mixins/gradients" as *; +@use "vendor/rfs" as *; +@use "variables" as *; + +// Base class +// +// Requires one of the contextual, color modifier classes for `color` and +// `background-color`. + +.badge { + // scss-docs-start badge-css-vars + --#{$prefix}badge-padding-x: #{$badge-padding-x}; + --#{$prefix}badge-padding-y: #{$badge-padding-y}; + @include rfs($badge-font-size, --#{$prefix}badge-font-size); + --#{$prefix}badge-font-weight: #{$badge-font-weight}; + --#{$prefix}badge-color: #{$badge-color}; + --#{$prefix}badge-border-radius: #{$badge-border-radius}; + // scss-docs-end badge-css-vars + + display: inline-block; + padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x); + @include font-size(var(--#{$prefix}badge-font-size)); + font-weight: var(--#{$prefix}badge-font-weight); + line-height: 1; + color: var(--#{$prefix}badge-color); + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: var(--#{$prefix}badge-border-radius, 0); // stylelint-disable-line property-disallowed-list + @include gradient-bg(); + + // Empty badges collapse automatically + &:empty { + display: none; + } +} + +// Quick fix for badges in buttons +.btn .badge { + position: relative; + top: -1px; +} + +// +// Badge Sizes +// + +.badge-sm { + --#{$prefix}badge-padding-x: #{$badge-padding-x-sm}; + --#{$prefix}badge-padding-y: #{$badge-padding-y-sm}; + @include font-size($badge-font-size-sm); +} diff --git a/src/scss/scss/_banner.scss b/src/scss/scss/_banner.scss new file mode 100644 index 000000000..ded260d4d --- /dev/null +++ b/src/scss/scss/_banner.scss @@ -0,0 +1,7 @@ +$file: "" !default; + +/*! + * CoreUI #{$file} v5.3.2 (https://coreui.io) + * Copyright (c) 2025 creativeLabs Łukasz Holeczek + * Licensed under MIT (https://github.com/coreui/coreui/blob/main/LICENSE) + */ diff --git a/src/scss/scss/_breadcrumb.import.scss b/src/scss/scss/_breadcrumb.import.scss new file mode 100644 index 000000000..39683620c --- /dev/null +++ b/src/scss/scss/_breadcrumb.import.scss @@ -0,0 +1 @@ +@forward "breadcrumb"; diff --git a/src/scss/scss/_breadcrumb.scss b/src/scss/scss/_breadcrumb.scss new file mode 100644 index 000000000..e4d23d2a0 --- /dev/null +++ b/src/scss/scss/_breadcrumb.scss @@ -0,0 +1,51 @@ +@use "functions/escape-svg" as *; +@use "mixins/border-radius" as *; +@use "mixins/ltr-rtl" as *; +@use "vendor/rfs" as *; +@use "variables" as *; + +.breadcrumb { + // scss-docs-start breadcrumb-css-vars + --#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x}; + --#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y}; + --#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom}; + @include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size); + --#{$prefix}breadcrumb-bg: #{$breadcrumb-bg}; + --#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius}; + --#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color}; + --#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x}; + --#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color}; + // scss-docs-end breadcrumb-css-vars + + display: flex; + flex-wrap: wrap; + padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x); + margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom); + @include font-size(var(--#{$prefix}breadcrumb-font-size)); + list-style: none; + background-color: var(--#{$prefix}breadcrumb-bg); + @include border-radius(var(--#{$prefix}breadcrumb-border-radius)); +} + +.breadcrumb-item { + // The separator between breadcrumbs (by default, a forward-slash: "/") + + .breadcrumb-item { + padding-inline-start: var(--#{$prefix}breadcrumb-item-padding-x); + + &::before { + float: inline-start; // Suppress inline spacings and underlining of the separator + padding-inline-end: var(--#{$prefix}breadcrumb-item-padding-x); + color: var(--#{$prefix}breadcrumb-divider-color); + @include ltr-rtl( + "content", + var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)), + null, + var(--#{$prefix}breadcrumb-divider-flipped, escape-svg($breadcrumb-divider-flipped)) + ); + } + } + + &.active { + color: var(--#{$prefix}breadcrumb-item-active-color); + } +} diff --git a/src/scss/scss/_button-group.import.scss b/src/scss/scss/_button-group.import.scss new file mode 100644 index 000000000..9e34c905a --- /dev/null +++ b/src/scss/scss/_button-group.import.scss @@ -0,0 +1 @@ +@forward "button-group"; diff --git a/src/scss/scss/_button-group.scss b/src/scss/scss/_button-group.scss new file mode 100644 index 000000000..0efee627f --- /dev/null +++ b/src/scss/scss/_button-group.scss @@ -0,0 +1,152 @@ +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "buttons" as *; +@use "variables" as *; + +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; // match .btn alignment given font-size hack above + + > .btn { + position: relative; + flex: 1 1 auto; + } + + // Bring the hover, focused, and "active" buttons to the front to overlay + // the borders properly + > .btn-check:checked + .btn, + > .btn-check:focus + .btn, + > .btn:hover, + > .btn:focus, + > .btn:active, + > .btn.active { + z-index: 1; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + + .input-group { + width: auto; + } +} + +.btn-group { + @include border-radius($btn-border-radius); + + // Prevent double borders when buttons are next to each other + > :not(.btn-check:first-child) + .btn, + > .btn-group:not(:first-child) { + margin-inline-start: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list + } + + // Reset rounded corners + > .btn:not(:last-child):not(.dropdown-toggle), + > .btn.dropdown-toggle-split:first-child, + > .btn-group:not(:last-child) > .btn { + @include border-end-radius(0); + } + + // The left radius should be 0 if the button is: + // - the "third or more" child + // - the second child and the previous element isn't `.btn-check` (making it the first child visually) + // - part of a btn-group which isn't the first child + > .btn:nth-child(n + 3), + > :not(.btn-check) + .btn, + > .btn-group:not(:first-child) > .btn { + @include border-start-radius(0); + } +} + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-sm > .btn { @extend .btn-sm; } +.btn-group-lg > .btn { @extend .btn-lg; } + + +// +// Split button dropdowns +// + +.dropdown-toggle-split { + padding-right: $btn-padding-x * .75; + padding-left: $btn-padding-x * .75; + + &::after, + .dropup &::after, + .dropend &::after { + margin-inline-start: 0; + } + + .dropstart &::before { + margin-inline-end: 0; + } +} + +.btn-sm + .dropdown-toggle-split { + padding-right: $btn-padding-x-sm * .75; + padding-left: $btn-padding-x-sm * .75; +} + +.btn-lg + .dropdown-toggle-split { + padding-right: $btn-padding-x-lg * .75; + padding-left: $btn-padding-x-lg * .75; +} + + +// The clickable button for toggling the menu +// Set the same inset shadow as the :active state +.btn-group.show .dropdown-toggle { + @include box-shadow($btn-active-box-shadow); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + @include box-shadow(none); + } +} + + +// +// Vertical button groups +// + +.btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; + + > .btn, + > .btn-group { + width: 100%; + } + + > .btn:not(:first-child), + > .btn-group:not(:first-child) { + margin-top: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list + } + + // Reset rounded corners + > .btn:not(:last-child):not(.dropdown-toggle), + > .btn-group:not(:last-child) > .btn { + @include border-bottom-radius(0); + } + + // The top radius should be 0 if the button is: + // - the "third or more" child + // - the second child and the previous element isn't `.btn-check` (making it the first child visually) + // - part of a btn-group which isn't the first child + > .btn:nth-child(n + 3), + > :not(.btn-check) + .btn, + > .btn-group:not(:first-child) > .btn { + @include border-top-radius(0); + } +} diff --git a/src/scss/scss/_buttons.import.scss b/src/scss/scss/_buttons.import.scss new file mode 100644 index 000000000..73468922f --- /dev/null +++ b/src/scss/scss/_buttons.import.scss @@ -0,0 +1 @@ +@forward "buttons"; diff --git a/src/scss/scss/_buttons.scss b/src/scss/scss/_buttons.scss new file mode 100644 index 000000000..10c9343da --- /dev/null +++ b/src/scss/scss/_buttons.scss @@ -0,0 +1,256 @@ +@use "functions/color" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/buttons" as *; +@use "mixins/color-mode" as *; +@use "mixins/gradients" as *; +@use "mixins/transition" as *; +@use "vendor/rfs" as *; +@use "variables" as *; +@use "variables-dark" as *; + +// +// Base styles +// + +.btn { + // scss-docs-start btn-css-vars + --#{$prefix}btn-padding-x: #{$btn-padding-x}; + --#{$prefix}btn-padding-y: #{$btn-padding-y}; + --#{$prefix}btn-font-family: #{$btn-font-family}; + @include rfs($btn-font-size, --#{$prefix}btn-font-size); + --#{$prefix}btn-font-weight: #{$btn-font-weight}; + --#{$prefix}btn-line-height: #{$btn-line-height}; + --#{$prefix}btn-color: #{$btn-color}; + --#{$prefix}btn-bg: transparent; + --#{$prefix}btn-border-width: #{$btn-border-width}; + --#{$prefix}btn-border-color: transparent; + --#{$prefix}btn-border-radius: #{$btn-border-radius}; + --#{$prefix}btn-hover-border-color: transparent; + --#{$prefix}btn-box-shadow: #{$btn-box-shadow}; + --#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity}; + --#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5); + // scss-docs-end btn-css-vars + + display: inline-block; + padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x); + font-family: var(--#{$prefix}btn-font-family); + @include font-size(var(--#{$prefix}btn-font-size)); + font-weight: var(--#{$prefix}btn-font-weight); + line-height: var(--#{$prefix}btn-line-height); + color: var(--#{$prefix}btn-color); + text-align: center; + text-decoration: if($link-decoration == none, null, none); + white-space: $btn-white-space; + vertical-align: middle; + cursor: if($enable-button-pointers, pointer, null); + user-select: none; + border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color); + @include border-radius(var(--#{$prefix}btn-border-radius)); + @include gradient-bg(var(--#{$prefix}btn-bg)); + @include box-shadow(var(--#{$prefix}btn-box-shadow)); + @include transition($btn-transition); + + &:hover { + color: var(--#{$prefix}btn-hover-color); + text-decoration: if($link-hover-decoration == underline, none, null); + background-color: var(--#{$prefix}btn-hover-bg); + border-color: var(--#{$prefix}btn-hover-border-color); + } + + .btn-check + &:hover { + // override for the checkbox/radio buttons + color: var(--#{$prefix}btn-color); + background-color: var(--#{$prefix}btn-bg); + border-color: var(--#{$prefix}btn-border-color); + } + + &:focus-visible { + color: var(--#{$prefix}btn-hover-color); + @include gradient-bg(var(--#{$prefix}btn-hover-bg)); + border-color: var(--#{$prefix}btn-hover-border-color); + outline: 0; + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + + .btn-check:focus-visible + & { + border-color: var(--#{$prefix}btn-hover-border-color); + outline: 0; + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + + .btn-check:checked + &, + :not(.btn-check) + &:active, + &:first-child:active, + &.active, + &.show { + color: var(--#{$prefix}btn-active-color); + background-color: var(--#{$prefix}btn-active-bg); + // Remove CSS gradients if they're enabled + background-image: if($enable-gradients, none, null); + border-color: var(--#{$prefix}btn-active-border-color); + @include box-shadow(var(--#{$prefix}btn-active-shadow)); + + &:focus-visible { + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + } + + .btn-check:checked:focus-visible + & { + // Avoid using mixin so we can pass custom focus shadow properly + @if $enable-shadows { + box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); + } @else { + box-shadow: var(--#{$prefix}btn-focus-box-shadow); + } + } + + &:disabled, + &.disabled, + fieldset:disabled & { + color: var(--#{$prefix}btn-disabled-color); + pointer-events: none; + background-color: var(--#{$prefix}btn-disabled-bg); + background-image: if($enable-gradients, none, null); + border-color: var(--#{$prefix}btn-disabled-border-color); + opacity: var(--#{$prefix}btn-disabled-opacity); + @include box-shadow(none); + } +} + + +// +// Alternate buttons +// + +.btn-transparent { + --#{$prefix}btn-active-border-color: transparent; + --#{$prefix}btn-disabled-border-color: transparent; + --#{$prefix}btn-hover-border-color: transparent; +} + +// scss-docs-start btn-variant-loops +@each $color, $value in $theme-colors { + .btn-#{$color} { + @if $color == "light" { + @include button-variant( + $value, + $value, + $hover-background: shade-color($value, $btn-hover-bg-shade-amount), + $hover-border: shade-color($value, $btn-hover-border-shade-amount), + $active-background: shade-color($value, $btn-active-bg-shade-amount), + $active-border: shade-color($value, $btn-active-border-shade-amount) + ); + } @else if $color == "dark" { + @include button-variant( + $value, + $value, + $hover-background: tint-color($value, $btn-hover-bg-tint-amount), + $hover-border: tint-color($value, $btn-hover-border-tint-amount), + $active-background: tint-color($value, $btn-active-bg-tint-amount), + $active-border: tint-color($value, $btn-active-border-tint-amount) + ); + } @else { + @include button-variant($value, $value); + } + } +} + +@each $color, $value in $theme-colors { + .btn-outline-#{$color} { + @include button-outline-variant($value); + } +} + +@each $color, $value in $theme-colors { + .btn-ghost-#{$color} { + @include button-ghost-variant($value); + } +} +// scss-docs-end btn-variant-loops + + +// +// Link buttons +// + +// Make a button look and behave like a link +.btn-link { + --#{$prefix}btn-font-weight: #{$font-weight-normal}; + --#{$prefix}btn-color: #{$btn-link-color}; + --#{$prefix}btn-bg: transparent; + --#{$prefix}btn-border-color: transparent; + --#{$prefix}btn-hover-color: #{$btn-link-hover-color}; + --#{$prefix}btn-hover-border-color: transparent; + --#{$prefix}btn-active-border-color: transparent; + --#{$prefix}btn-disabled-color: #{$btn-link-disabled-color}; + --#{$prefix}btn-disabled-border-color: transparent; + --#{$prefix}btn-box-shadow: none; + --#{$prefix}btn-focus-shadow-rgb: #{$btn-link-focus-shadow-rgb}; + + text-decoration: $link-decoration; + @if $enable-gradients { + background-image: none; + } + + &:hover, + &:focus-visible { + text-decoration: $link-hover-decoration; + } + + &:focus-visible { + color: var(--#{$prefix}btn-color); + } + + // No need for an active state here +} + + +// +// Button Sizes +// + +.btn-lg { + @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); +} + +.btn-sm { + @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); +} + +@if $enable-dark-mode { + @include color-mode(dark) { + @each $color, $value in $theme-colors-dark { + .btn-#{$color} { + @include button-variant($value, $value); + } + } + + @each $color, $value in $theme-colors-dark { + .btn-outline-#{$color} { + @include button-outline-variant($value); + } + } + + @each $color, $value in $theme-colors-dark { + .btn-ghost-#{$color} { + @include button-ghost-variant($value); + } + } + } +} diff --git a/src/scss/scss/_callout.import.scss b/src/scss/scss/_callout.import.scss new file mode 100644 index 000000000..975f078be --- /dev/null +++ b/src/scss/scss/_callout.import.scss @@ -0,0 +1 @@ +@use "callout"; diff --git a/src/scss/scss/_callout.scss b/src/scss/scss/_callout.scss new file mode 100644 index 000000000..9f5630c5a --- /dev/null +++ b/src/scss/scss/_callout.scss @@ -0,0 +1,31 @@ +@use "mixins/border-radius" as *; +@use "variables" as *; + +.callout { + // scss-docs-start callout-css-vars + --#{$prefix}callout-padding-x: #{$callout-padding-x}; + --#{$prefix}callout-padding-y: #{$callout-padding-y}; + --#{$prefix}callout-margin-x: #{$callout-margin-x}; + --#{$prefix}callout-margin-y: #{$callout-margin-y}; + --#{$prefix}callout-border-width: #{$callout-border-width}; + --#{$prefix}callout-border-color: #{$callout-border-color}; + --#{$prefix}callout-border-left-width: #{$callout-border-left-width}; + --#{$prefix}callout-border-radius: #{$callout-border-radius}; + // scss-docs-end callout-css-vars + + padding: var(--#{$prefix}callout-padding-y) var(--#{$prefix}callout-padding-x); + margin: var(--#{$prefix}callout-margin-y) var(--#{$prefix}callout-margin-x); + border: var(--#{$prefix}callout-border-width) solid var(--#{$prefix}callout-border-color); + border-inline-start-color: var(--#{$prefix}callout-border-left-color); + border-inline-start-width: var(--#{$prefix}callout-border-left-width); + @include border-radius(var(--#{$prefix}callout-border-radius)); +} + +// scss-docs-start callout-modifiers +// Generate contextual modifier classes for colorizing the collor. +@each $state, $value in $callout-variants { + .callout-#{$state} { + --#{$prefix}callout-border-left-color: #{$value}; + } +} +// scss-docs-end callout-modifiers diff --git a/src/scss/scss/_card.import.scss b/src/scss/scss/_card.import.scss new file mode 100644 index 000000000..00085495f --- /dev/null +++ b/src/scss/scss/_card.import.scss @@ -0,0 +1 @@ +@forward "card"; diff --git a/src/scss/scss/_card.scss b/src/scss/scss/_card.scss new file mode 100644 index 000000000..02df3a401 --- /dev/null +++ b/src/scss/scss/_card.scss @@ -0,0 +1,243 @@ +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/breakpoints" as *; +@use "variables" as *; + +// +// Base styles +// + +.card { + // scss-docs-start card-css-vars + --#{$prefix}card-spacer-y: #{$card-spacer-y}; + --#{$prefix}card-spacer-x: #{$card-spacer-x}; + --#{$prefix}card-title-spacer-y: #{$card-title-spacer-y}; + --#{$prefix}card-title-color: #{$card-title-color}; + --#{$prefix}card-subtitle-color: #{$card-subtitle-color}; + --#{$prefix}card-border-width: #{$card-border-width}; + --#{$prefix}card-border-color: #{$card-border-color}; + --#{$prefix}card-border-radius: #{$card-border-radius}; + --#{$prefix}card-box-shadow: #{$card-box-shadow}; + --#{$prefix}card-inner-border-radius: #{$card-inner-border-radius}; + --#{$prefix}card-cap-padding-y: #{$card-cap-padding-y}; + --#{$prefix}card-cap-padding-x: #{$card-cap-padding-x}; + --#{$prefix}card-cap-bg: #{$card-cap-bg}; + --#{$prefix}card-cap-color: #{$card-cap-color}; + --#{$prefix}card-height: #{$card-height}; + --#{$prefix}card-color: #{$card-color}; + --#{$prefix}card-bg: #{$card-bg}; + --#{$prefix}card-img-overlay-padding: #{$card-img-overlay-padding}; + --#{$prefix}card-group-margin: #{$card-group-margin}; + // scss-docs-end card-css-vars + + position: relative; + display: flex; + flex-direction: column; + min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 + height: var(--#{$prefix}card-height); + color: var(--#{$prefix}body-color); + word-wrap: break-word; + background-color: var(--#{$prefix}card-bg); + background-clip: border-box; + border: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + @include border-radius(var(--#{$prefix}card-border-radius)); + @include box-shadow(var(--#{$prefix}card-box-shadow)); + + > hr { + margin-right: 0; + margin-left: 0; + } + + > .list-group { + border-top: inherit; + border-bottom: inherit; + + &:first-child { + border-top-width: 0; + @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); + } + + &:last-child { + border-bottom-width: 0; + @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); + } + } + + // Due to specificity of the above selector (`.card > .list-group`), we must + // use a child selector here to prevent double borders. + > .card-header + .list-group, + > .list-group + .card-footer { + border-top: 0; + } +} + +.card-body { + // Enable `flex-grow: 1` for decks and groups so that card blocks take up + // as much space as possible, ensuring footers are aligned to the bottom. + flex: 1 1 auto; + padding: var(--#{$prefix}card-spacer-y) var(--#{$prefix}card-spacer-x); + color: var(--#{$prefix}card-color); +} + +.card-title { + margin-bottom: var(--#{$prefix}card-title-spacer-y); + color: var(--#{$prefix}card-title-color); +} + +.card-subtitle { + margin-top: calc(-.5 * var(--#{$prefix}card-title-spacer-y)); // stylelint-disable-line function-disallowed-list + margin-bottom: 0; + color: var(--#{$prefix}card-subtitle-color); +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link { + &:hover { + text-decoration: if($link-hover-decoration == underline, none, null); + } + + + .card-link { + margin-inline-start: var(--#{$prefix}card-spacer-x); + } +} + +// +// Optional textual caps +// + +.card-header { + padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); + margin-bottom: 0; // Removes the default margin-bottom of + color: var(--#{$prefix}card-cap-color); + background-color: var(--#{$prefix}card-cap-bg); + border-bottom: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + + &:first-child { + @include border-radius(var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius) 0 0); + } +} + +.card-footer { + padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); + color: var(--#{$prefix}card-cap-color); + background-color: var(--#{$prefix}card-cap-bg); + border-top: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + + &:last-child { + @include border-radius(0 0 var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius)); + } +} + + +// +// Header navs +// + +.card-header-tabs { + margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list + margin-bottom: calc(-1 * var(--#{$prefix}card-cap-padding-y)); // stylelint-disable-line function-disallowed-list + margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list + border-bottom: 0; + + .nav-link.active { + background-color: var(--#{$prefix}card-bg); + border-bottom-color: var(--#{$prefix}card-bg); + } +} + +.card-header-pills { + margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list + margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list +} + +// Card image +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: var(--#{$prefix}card-img-overlay-padding); + @include border-radius(var(--#{$prefix}card-inner-border-radius)); +} + +.card-img, +.card-img-top, +.card-img-bottom { + width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch +} + +.card-img, +.card-img-top { + @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); +} + +.card-img, +.card-img-bottom { + @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); +} + + +// +// Card groups +// + +.card-group { + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + margin-bottom: var(--#{$prefix}card-group-margin); + } + + @include media-breakpoint-up(sm) { + display: flex; + flex-flow: row wrap; + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + flex: 1 0 0; + margin-bottom: 0; + + + .card { + margin-inline-start: 0; + border-inline-start: 0; + } + + // Handle rounded corners + @if $enable-rounded { + &:not(:last-child) { + @include border-end-radius(0); + + .card-img-top, + .card-header { + // stylelint-disable-next-line property-disallowed-list + border-top-right-radius: 0; + } + .card-img-bottom, + .card-footer { + // stylelint-disable-next-line property-disallowed-list + border-bottom-right-radius: 0; + } + } + + &:not(:first-child) { + @include border-start-radius(0); + + .card-img-top, + .card-header { + // stylelint-disable-next-line property-disallowed-list + border-top-left-radius: 0; + } + .card-img-bottom, + .card-footer { + // stylelint-disable-next-line property-disallowed-list + border-bottom-left-radius: 0; + } + } + } + } + } +} diff --git a/src/scss/scss/_carousel.import.scss b/src/scss/scss/_carousel.import.scss new file mode 100644 index 000000000..6ee740934 --- /dev/null +++ b/src/scss/scss/_carousel.import.scss @@ -0,0 +1 @@ +@forward "carousel"; diff --git a/src/scss/scss/_carousel.scss b/src/scss/scss/_carousel.scss new file mode 100644 index 000000000..32e659a17 --- /dev/null +++ b/src/scss/scss/_carousel.scss @@ -0,0 +1,234 @@ +@use "functions/escape-svg" as *; +@use "mixins/clearfix" as *; +@use "mixins/color-mode" as *; +@use "mixins/ltr-rtl" as *; +@use "mixins/transition" as *; +@use "variables" as *; +@use "variables-dark" as *; + +// Notes on the classes: +// +// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically) +// even when their scroll action started on a carousel, but for compatibility (with Firefox) +// we're preventing all actions instead +// 2. The .carousel-item-start and .carousel-item-end is used to indicate where +// the active slide is heading. +// 3. .active.carousel-item is the current slide. +// 4. .active.carousel-item-start and .active.carousel-item-end is the current +// slide in its in-transition state. Only one of these occurs at a time. +// 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end +// is the upcoming slide in transition. + +.carousel { + position: relative; +} + +.carousel.pointer-event { + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; + @include clearfix(); +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + backface-visibility: hidden; + @include transition($carousel-transition); +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-start), +.active.carousel-item-end { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-end), +.active.carousel-item-start { + transform: translateX(-100%); +} + + +// +// Alternate transitions +// + +.carousel-fade { + .carousel-item { + opacity: 0; + transition-property: opacity; + transform: none; + } + + .carousel-item.active, + .carousel-item-next.carousel-item-start, + .carousel-item-prev.carousel-item-end { + z-index: 1; + opacity: 1; + } + + .active.carousel-item-start, + .active.carousel-item-end { + z-index: 0; + opacity: 0; + @include transition(opacity 0s $carousel-transition-duration); + } +} + + +// +// Left/right controls for nav +// + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + // Use flex for alignment (1-3) + display: flex; // 1. allow flex styles + align-items: center; // 2. vertically center contents + justify-content: center; // 3. horizontally center contents + width: $carousel-control-width; + padding: 0; + color: $carousel-control-color; + text-align: center; + background: none; + filter: var(--#{$prefix}carousel-control-icon-filter); + border: 0; + opacity: $carousel-control-opacity; + @include transition($carousel-control-transition); + + // Hover/focus state + &:hover, + &:focus { + color: $carousel-control-color; + text-decoration: none; + outline: 0; + opacity: $carousel-control-hover-opacity; + } +} +.carousel-control-prev { + left: 0; + background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null); +} +.carousel-control-next { + right: 0; + background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null); +} + +// Icons for within +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: $carousel-control-icon-width; + height: $carousel-control-icon-width; + background-repeat: no-repeat; + background-position: 50%; + background-size: 100% 100%; +} + +.carousel-control-prev-icon { + @include ltr-rtl-value-only("background-image", escape-svg($carousel-control-prev-icon-bg), escape-svg($carousel-control-next-icon-bg)); +} +.carousel-control-next-icon { + @include ltr-rtl-value-only("background-image", escape-svg($carousel-control-next-icon-bg), escape-svg($carousel-control-prev-icon-bg)); +} + +// Optional indicator pips/controls +// +// Add a container (such as a list) with the following class and add an item (ideally a focusable control, +// like a button) with data#{$data-infix}target for each slide your carousel holds. + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + display: flex; + justify-content: center; + padding: 0; + // Use the .carousel-control's width as margin so we don't overlay those + margin-right: $carousel-control-width; + margin-bottom: 1rem; + margin-left: $carousel-control-width; + + [data#{$data-infix}target] { + box-sizing: content-box; + flex: 0 1 auto; + width: $carousel-indicator-width; + height: $carousel-indicator-height; + padding: 0; + margin-right: $carousel-indicator-spacer; + margin-left: $carousel-indicator-spacer; + text-indent: -999px; + cursor: pointer; + background-color: $carousel-indicator-active-bg; + background-clip: padding-box; + border: 0; + // Use transparent borders to increase the hit area by 10px on top and bottom. + border-top: $carousel-indicator-hit-area-height solid transparent; + border-bottom: $carousel-indicator-hit-area-height solid transparent; + opacity: $carousel-indicator-opacity; + @include transition($carousel-indicator-transition); + } + + .active { + opacity: $carousel-indicator-active-opacity; + } +} + + +// Optional captions +// +// + +.carousel-caption { + position: absolute; + right: (100% - $carousel-caption-width) * .5; + bottom: $carousel-caption-spacer; + left: (100% - $carousel-caption-width) * .5; + padding-top: $carousel-caption-padding-y; + padding-bottom: $carousel-caption-padding-y; + color: var(--#{$prefix}carousel-caption-color); + text-align: center; +} + +// Dark mode carousel + +@mixin carousel-dark() { + --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg-dark}; + --#{$prefix}carousel-caption-color: #{$carousel-caption-color-dark}; + --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter-dark}; +} + +.carousel-dark { + @include carousel-dark(); +} + +:root, +[data#{$data-infix}theme="light"] { + --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg}; + --#{$prefix}carousel-caption-color: #{$carousel-caption-color}; + --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter}; +} + +@if $enable-dark-mode { + @include color-mode(dark, true) { + @include carousel-dark(); + } +} diff --git a/src/scss/scss/_close.import.scss b/src/scss/scss/_close.import.scss new file mode 100644 index 000000000..13ede5120 --- /dev/null +++ b/src/scss/scss/_close.import.scss @@ -0,0 +1 @@ +@forward "close"; diff --git a/src/scss/scss/_close.scss b/src/scss/scss/_close.scss new file mode 100644 index 000000000..15ec7e7ac --- /dev/null +++ b/src/scss/scss/_close.scss @@ -0,0 +1,72 @@ +@use "functions/escape-svg" as *; +@use "mixins/border-radius" as *; +@use "mixins/color-mode" as *; +@use "variables" as *; +@use "variables-dark" as *; + +// Transparent background and border properties included for button version. +// iOS requires the button element instead of an anchor tag. +// If you want the anchor version, it requires `href="#"`. +// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + +.btn-close { + // scss-docs-start close-css-vars + --#{$prefix}btn-close-color: #{$btn-close-color}; + --#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg) }; + --#{$prefix}btn-close-opacity: #{$btn-close-opacity}; + --#{$prefix}btn-close-hover-opacity: #{$btn-close-hover-opacity}; + --#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow}; + --#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity}; + --#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity}; + // scss-docs-end close-css-vars + + box-sizing: content-box; + width: $btn-close-width; + height: $btn-close-height; + padding: $btn-close-padding-y $btn-close-padding-x; + color: var(--#{$prefix}btn-close-color); + background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements + filter: var(--#{$prefix}btn-close-filter); + border: 0; // for button elements + @include border-radius(); + opacity: var(--#{$prefix}btn-close-opacity); + + // Override 's hover style + &:hover { + color: var(--#{$prefix}btn-close-color); + text-decoration: none; + opacity: var(--#{$prefix}btn-close-hover-opacity); + } + + &:focus { + outline: 0; + box-shadow: var(--#{$prefix}btn-close-focus-shadow); + opacity: var(--#{$prefix}btn-close-focus-opacity); + } + + &:disabled, + &.disabled { + pointer-events: none; + user-select: none; + opacity: var(--#{$prefix}btn-close-disabled-opacity); + } +} + +@mixin btn-close-white() { + --#{$prefix}btn-close-filter: #{$btn-close-filter-dark}; +} + +.btn-close-white { + @include btn-close-white(); +} + +:root, +[data#{$data-infix}theme="light"] { + --#{$prefix}btn-close-filter: #{$btn-close-filter}; +} + +@if $enable-dark-mode { + @include color-mode(dark, true) { + @include btn-close-white(); + } +} diff --git a/src/scss/scss/_containers.import.scss b/src/scss/scss/_containers.import.scss new file mode 100644 index 000000000..724ca90c0 --- /dev/null +++ b/src/scss/scss/_containers.import.scss @@ -0,0 +1 @@ +@forward "containers"; diff --git a/src/scss/scss/_containers.scss b/src/scss/scss/_containers.scss new file mode 100644 index 000000000..b2e7760d8 --- /dev/null +++ b/src/scss/scss/_containers.scss @@ -0,0 +1,45 @@ +@use "mixins/breakpoints" as *; +@use "mixins/container" as *; +@use "variables" as *; + +// Container widths +// +// Set the container width, and override it for fixed navbars in media queries. + +@if $enable-container-classes { + // Single container class with breakpoint max-widths + .container, + // 100% wide container at all breakpoints + .container-fluid { + @include make-container(); + } + + // Responsive containers that are 100% wide until a breakpoint + @each $breakpoint, $container-max-width in $container-max-widths { + .container-#{$breakpoint} { + @extend .container-fluid; + } + + @include media-breakpoint-up($breakpoint, $grid-breakpoints) { + %responsive-container-#{$breakpoint} { + max-width: $container-max-width; + } + + // Extend each breakpoint which is smaller or equal to the current breakpoint + $extend-breakpoint: true; + + @each $name, $width in $grid-breakpoints { + @if ($extend-breakpoint) { + .container#{breakpoint-infix($name, $grid-breakpoints)} { + @extend %responsive-container-#{$breakpoint}; + } + + // Once the current breakpoint is reached, stop extending + @if ($breakpoint == $name) { + $extend-breakpoint: false; + } + } + } + } + } +} diff --git a/src/scss/scss/_dropdown.import.scss b/src/scss/scss/_dropdown.import.scss new file mode 100644 index 000000000..1fe96c2f8 --- /dev/null +++ b/src/scss/scss/_dropdown.import.scss @@ -0,0 +1 @@ +@forward "dropdown"; diff --git a/src/scss/scss/_dropdown.scss b/src/scss/scss/_dropdown.scss new file mode 100644 index 000000000..be187725e --- /dev/null +++ b/src/scss/scss/_dropdown.scss @@ -0,0 +1,259 @@ +@use "sass:map"; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/breakpoints" as *; +@use "mixins/caret" as *; +@use "mixins/gradients" as *; +@use "vendor/rfs" as *; +@use "variables" as *; + +// The dropdown wrapper (`
    `) +.dropup, +.dropend, +.dropdown, +.dropstart, +.dropup-center, +.dropdown-center { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; + + // Generate the caret automatically + @include caret(); +} + +// The dropdown menu +.dropdown-menu { + // scss-docs-start dropdown-css-vars + --#{$prefix}dropdown-zindex: #{$zindex-dropdown}; + --#{$prefix}dropdown-min-width: #{$dropdown-min-width}; + --#{$prefix}dropdown-padding-x: #{$dropdown-padding-x}; + --#{$prefix}dropdown-padding-y: #{$dropdown-padding-y}; + --#{$prefix}dropdown-spacer: #{$dropdown-spacer}; + @include rfs($dropdown-font-size, --#{$prefix}dropdown-font-size); + --#{$prefix}dropdown-color: #{$dropdown-color}; + --#{$prefix}dropdown-bg: #{$dropdown-bg}; + --#{$prefix}dropdown-border-color: #{$dropdown-border-color}; + --#{$prefix}dropdown-border-radius: #{$dropdown-border-radius}; + --#{$prefix}dropdown-border-width: #{$dropdown-border-width}; + --#{$prefix}dropdown-inner-border-radius: #{$dropdown-inner-border-radius}; + --#{$prefix}dropdown-divider-bg: #{$dropdown-divider-bg}; + --#{$prefix}dropdown-divider-margin-y: #{$dropdown-divider-margin-y}; + --#{$prefix}dropdown-box-shadow: #{$dropdown-box-shadow}; + --#{$prefix}dropdown-link-color: #{$dropdown-link-color}; + --#{$prefix}dropdown-link-hover-color: #{$dropdown-link-hover-color}; + --#{$prefix}dropdown-link-hover-bg: #{$dropdown-link-hover-bg}; + --#{$prefix}dropdown-link-active-color: #{$dropdown-link-active-color}; + --#{$prefix}dropdown-link-active-bg: #{$dropdown-link-active-bg}; + --#{$prefix}dropdown-link-disabled-color: #{$dropdown-link-disabled-color}; + --#{$prefix}dropdown-item-padding-x: #{$dropdown-item-padding-x}; + --#{$prefix}dropdown-item-padding-y: #{$dropdown-item-padding-y}; + --#{$prefix}dropdown-header-color: #{$dropdown-header-color}; + --#{$prefix}dropdown-header-padding-x: #{$dropdown-header-padding-x}; + --#{$prefix}dropdown-header-padding-y: #{$dropdown-header-padding-y}; + // scss-docs-end dropdown-css-vars + + position: absolute; + z-index: var(--#{$prefix}dropdown-zindex); + display: none; // none by default, but block on "open" of the menu + min-width: var(--#{$prefix}dropdown-min-width); + padding: var(--#{$prefix}dropdown-padding-y) var(--#{$prefix}dropdown-padding-x); + margin: 0; // Override default margin of ul + @include font-size(var(--#{$prefix}dropdown-font-size)); + color: var(--#{$prefix}dropdown-color); + text-align: start; // Ensures proper alignment if parent has it changed (e.g., modal footer) + list-style: none; + background-color: var(--#{$prefix}dropdown-bg); + background-clip: padding-box; + border: var(--#{$prefix}dropdown-border-width) solid var(--#{$prefix}dropdown-border-color); + @include border-radius(var(--#{$prefix}dropdown-border-radius)); + @include box-shadow(var(--#{$prefix}dropdown-box-shadow)); + + &[data#{$data-infix}popper] { + inset-inline-start: 0; + top: 100%; + margin-top: var(--#{$prefix}dropdown-spacer); + } + + @if $dropdown-padding-y == 0 { + > .dropdown-item:first-child, + > li:first-child .dropdown-item { + @include border-top-radius(var(--#{$prefix}dropdown-inner-border-radius)); + } + > .dropdown-item:last-child, + > li:last-child .dropdown-item { + @include border-bottom-radius(var(--#{$prefix}dropdown-inner-border-radius)); + } + + } +} + +// scss-docs-start responsive-breakpoints +// We deliberately hardcode the `cui-` prefix because we check +// this custom property in JS to determine Popper's positioning + +@each $breakpoint in map.keys($grid-breakpoints) { + @include media-breakpoint-up($breakpoint) { + $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + + .dropdown-menu#{$infix}-start { + --#{$prefix}position: start; + + &[data#{$data-infix}popper] { + inset-inline-start: 0; + inset-inline-end: auto; + } + } + + .dropdown-menu#{$infix}-end { + --#{$prefix}position: end; + + &[data#{$data-infix}popper] { + inset-inline-start: auto; + inset-inline-end: 0; + } + } + } +} +// scss-docs-end responsive-breakpoints + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// Just add .dropup after the standard .dropdown class and you're set. +.dropup { + .dropdown-menu[data#{$data-infix}popper] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: var(--#{$prefix}dropdown-spacer); + } + + .dropdown-toggle { + @include caret(up); + } +} + +.dropend { + .dropdown-menu[data#{$data-infix}popper] { + inset-inline-start: 100%; + inset-inline-end: auto; + top: 0; + margin-inline-start: var(--#{$prefix}dropdown-spacer); + margin-top: 0; + } + + .dropdown-toggle { + @include caret(end); + &::after { + vertical-align: 0; + } + } +} + +.dropstart { + .dropdown-menu[data#{$data-infix}popper] { + inset-inline-start: auto; + inset-inline-end: 100%; + top: 0; + margin-inline-end: var(--#{$prefix}dropdown-spacer); + margin-top: 0; + } + + .dropdown-toggle { + @include caret(start); + &::before { + vertical-align: 0; + } + } +} + + +// Dividers (basically an `
    `) within the dropdown +.dropdown-divider { + height: 0; + margin: var(--#{$prefix}dropdown-divider-margin-y) 0; + overflow: hidden; + border-top: 1px solid var(--#{$prefix}dropdown-divider-bg); + opacity: 1; // Revisit in v6 to de-dupe styles that conflict with
    element +} + +// Links, buttons, and more within the dropdown menu +// +// `
    -
    -
    - -
    -
    - -
    -
    -
    - Logo -
    -
    -
    - - + + + + + + + + + + + + + + + ) } From 1d4e7bcf3be7e260ae15f2608388530cd49f0dd0 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Fri, 25 Apr 2025 14:41:21 +0100 Subject: [PATCH 13/72] feat: implement logout functionality with async actions and update auth reducer --- src/actions/authActions.js | 28 ++++++++++++++++++---- src/components/header/AppHeaderDropdown.js | 18 +++++++++++--- src/reducers/authReducer.js | 22 +++++++++++++++++ src/services/authService.js | 12 ++++++++-- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/actions/authActions.js b/src/actions/authActions.js index f8c5ab2f1..3dc582dd9 100644 --- a/src/actions/authActions.js +++ b/src/actions/authActions.js @@ -16,6 +16,20 @@ export const loginFailure = (error) => ({ payload: error, }) +export const logoutRequest = () => ({ + type: 'LOGOUT_REQUEST', +}) + +export const logoutSuccess = (user) => ({ + type: 'LOGOUT_SUCCESS', + payload: user, +}) + +export const logoutFailure = (error) => ({ + type: 'LOGOUT_FAILURE', + payload: error, +}) + export const login = (username, password) => async (dispatch) => { dispatch(loginRequest()) try { @@ -27,11 +41,15 @@ export const login = (username, password) => async (dispatch) => { } } -export const logout = () => (dispatch) => { - authService.logout() - dispatch({ - type: 'LOGOUT', - }) +export const logout = () => async (dispatch) => { + dispatch(logoutRequest()) + try { + const user = await authService.logout() + dispatch(logoutSuccess(user)) + } catch (error) { + dispatch(logoutFailure(error)) + console.error('Error logging out:', error) + } } export const checkAuthRequest = () => ({ diff --git a/src/components/header/AppHeaderDropdown.js b/src/components/header/AppHeaderDropdown.js index 30c0df82b..6d660fe70 100644 --- a/src/components/header/AppHeaderDropdown.js +++ b/src/components/header/AppHeaderDropdown.js @@ -19,12 +19,24 @@ import { cilSettings, cilTask, cilUser, + cilAccountLogout, } from '@coreui/icons' import CIcon from '@coreui/icons-react' import avatar8 from './../../assets/images/avatars/8.jpg' +import { useDispatch } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { logout } from '../../actions/authActions' const AppHeaderDropdown = () => { + const dispatch = useDispatch() + const navigate = useNavigate() + + const handleLogout = async (e) => { + e.preventDefault() + await dispatch(logout()) + await dispatch(checkAuthentication()) + } return ( @@ -84,9 +96,9 @@ const AppHeaderDropdown = () => { - - - Lock Account + + + Logout diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js index 453da6082..0dbae757b 100644 --- a/src/reducers/authReducer.js +++ b/src/reducers/authReducer.js @@ -9,6 +9,28 @@ const initialState = { const authReducer = (state = initialState, action) => { switch (action.type) { case 'LOGIN_REQUEST': + return { + ...state, + loading: true, + } + case 'LOGOUT_REQUEST': + return { + ...state, + loading: true, + } + case 'LOGOUT_SUCCESS': + return { + ...state, + isAuthenticated: false, + user: null, + loading: false, + } + case 'LOGOUT_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } case 'AUTH_CHECK_REQUEST': return { ...state, diff --git a/src/services/authService.js b/src/services/authService.js index 598e77ed9..fe69fd045 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -15,8 +15,16 @@ const login = async (email, password) => { } } -const logout = () => { - localStorage.removeItem('user') +const logout = async () => { + try { + const response = await axios.post(`${API_URL}logout`) + console.log(response) + localStorage.removeItem('user') + return response.data + } catch (error) { + console.error('Error logout in:', error) + throw error + } } const checkAuth = async () => { From 9c3e4d1faf1e96a69d4d862eec626d714b9f54ba Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Mon, 28 Apr 2025 11:28:55 +0100 Subject: [PATCH 14/72] fix: handle login errors and ensure authentication check is awaited --- src/actions/authActions.js | 7 ++++++- src/views/pages/login/Login.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/actions/authActions.js b/src/actions/authActions.js index 3dc582dd9..805bd16c4 100644 --- a/src/actions/authActions.js +++ b/src/actions/authActions.js @@ -34,7 +34,12 @@ export const login = (username, password) => async (dispatch) => { dispatch(loginRequest()) try { const user = await authService.login(username, password) - dispatch(loginSuccess(user)) + if (user.error) { + dispatch(loginFailure(error)) + throw new Error(user.error) + } else { + dispatch(loginSuccess(user)) + } } catch (error) { dispatch(loginFailure(error)) console.error('Error logging in:', error) diff --git a/src/views/pages/login/Login.js b/src/views/pages/login/Login.js index e0af4d4be..f107de86a 100644 --- a/src/views/pages/login/Login.js +++ b/src/views/pages/login/Login.js @@ -32,7 +32,7 @@ const Login = () => { const handleLogin = async (e) => { e.preventDefault() await dispatch(login(username, password)) - dispatch(checkAuthentication()) + await dispatch(checkAuthentication()) if (isAuthenticated) { navigate('/') } else { From 1f6ac36c4c5e02e77e3e4982e4bb89e137516b49 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Mon, 28 Apr 2025 16:21:45 +0100 Subject: [PATCH 15/72] feat: implement role-based layout and header components for employees and managers --- src/App.js | 9 +- .../AppHeaderEmployee.js} | 25 +-- src/components/AppHeader/AppHeaderManager.js | 154 ++++++++++++++++++ src/components/AppHeader/index.js | 11 ++ src/components/index.js | 2 +- src/layout/DefaultLayoutEmployee.js | 22 +++ ...faultLayout.js => DefaultLayoutManager.js} | 3 + src/views/pages/login/Login.js | 8 +- 8 files changed, 217 insertions(+), 17 deletions(-) rename src/components/{AppHeader.js => AppHeader/AppHeaderEmployee.js} (88%) create mode 100644 src/components/AppHeader/AppHeaderManager.js create mode 100644 src/components/AppHeader/index.js create mode 100644 src/layout/DefaultLayoutEmployee.js rename src/layout/{DefaultLayout.js => DefaultLayoutManager.js} (84%) diff --git a/src/App.js b/src/App.js index bc8547705..a0f2e0f66 100644 --- a/src/App.js +++ b/src/App.js @@ -1,13 +1,18 @@ import React, { Suspense, useEffect, useState } from 'react' import { BrowserRouter, Route, Routes } from 'react-router-dom' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { CSpinner } from '@coreui/react' import './scss/style.scss' import PrivateRoute from './PrivateRute' import { checkAuthentication } from './actions/authActions' // Containers -const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout')) +const DefaultLayoutEmployee = React.lazy(() => import('./layout/DefaultLayoutEmployee')) +const DefaultLayoutManager = React.lazy(() => import('./layout/DefaultLayoutManager')) +const DefaultLayout = () => { + const { user } = useSelector((state) => state.auth) + return user.user.IsEmployee ? : +} // Pages const Login = React.lazy(() => import('./views/pages/login/Login')) diff --git a/src/components/AppHeader.js b/src/components/AppHeader/AppHeaderEmployee.js similarity index 88% rename from src/components/AppHeader.js rename to src/components/AppHeader/AppHeaderEmployee.js index 70beef626..f4db3684a 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader/AppHeaderEmployee.js @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react' -import { NavLink } from 'react-router-dom' +import { NavLink, useLocation } from 'react-router-dom' import { useSelector, useDispatch } from 'react-redux' import { CContainer, @@ -25,14 +25,16 @@ import { cilSun, } from '@coreui/icons' -import { AppBreadcrumb } from './index' -import { AppHeaderDropdown } from './header/index' -import { switchThemeMode, toggleSideBar } from '../actions/appActions' -const AppHeader = () => { +import { AppBreadcrumb } from '../index' +import { AppHeaderDropdown } from '../header/index' +import { switchThemeMode, toggleSideBar } from '../../actions/appActions' +const AppHeaderEmployee = () => { + const location = useLocation() const headerRef = useRef() const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') const dispatch = useDispatch() const { theme } = useSelector((state) => state.data) + const { user } = useSelector((state) => state.auth) useEffect(() => { document.addEventListener('scroll', () => { @@ -52,12 +54,12 @@ const AppHeader = () => { return ( - dispatch(toggleSideBar())} style={{ marginInlineStart: '-14px' }} > - + */} @@ -65,10 +67,9 @@ const AppHeader = () => { - Users - - - Settings + + Tickets + @@ -145,4 +146,4 @@ const AppHeader = () => { ) } -export default AppHeader +export default AppHeaderEmployee diff --git a/src/components/AppHeader/AppHeaderManager.js b/src/components/AppHeader/AppHeaderManager.js new file mode 100644 index 000000000..8865b4ff7 --- /dev/null +++ b/src/components/AppHeader/AppHeaderManager.js @@ -0,0 +1,154 @@ +import React, { useEffect, useRef } from 'react' +import { NavLink, useLocation } from 'react-router-dom' +import { useSelector, useDispatch } from 'react-redux' +import { + CContainer, + CDropdown, + CDropdownItem, + CDropdownMenu, + CDropdownToggle, + CHeader, + CHeaderNav, + CHeaderToggler, + CNavLink, + CNavItem, + useColorModes, +} from '@coreui/react' +import CIcon from '@coreui/icons-react' +import { + cilBell, + cilContrast, + cilEnvelopeOpen, + cilList, + cilMenu, + cilMoon, + cilSun, +} from '@coreui/icons' + +import { AppBreadcrumb } from '../index' +import { AppHeaderDropdown } from '../header/index' +import { switchThemeMode, toggleSideBar } from '../../actions/appActions' +const AppHeaderManager = () => { + const location = useLocation() + const headerRef = useRef() + const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') + const dispatch = useDispatch() + const { theme } = useSelector((state) => state.data) + const { user } = useSelector((state) => state.auth) + + useEffect(() => { + document.addEventListener('scroll', () => { + headerRef.current && + headerRef.current.classList.toggle('shadow-sm', document.documentElement.scrollTop > 0) + }) + }, []) + + const switchColorMode = (color) => { + dispatch(switchThemeMode(color)) + } + + useEffect(() => { + setColorMode(theme) + }, [theme]) + + return ( + + + dispatch(toggleSideBar())} + style={{ marginInlineStart: '-14px' }} + > + + + + + + Dashboard + + + + + Employees + + + + + Projects + + + + + + + + + + + + + + + + + + + + + +
  • +
    +
  • + + + {colorMode === 'dark' ? ( + + ) : ( + // ) : colorMode === 'auto' ? ( + // + + )} + + + switchColorMode('light')} + > + Light + + switchColorMode('dark')} + > + Dark + + {/* switchColorMode('auto')} + > + Auto + */} + + +
  • +
    +
  • + + +
    + + + +
    + ) +} + +export default AppHeaderManager diff --git a/src/components/AppHeader/index.js b/src/components/AppHeader/index.js new file mode 100644 index 000000000..f46a31290 --- /dev/null +++ b/src/components/AppHeader/index.js @@ -0,0 +1,11 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import AppHeaderEmployee from './AppHeaderEmployee' +import AppHeaderManager from './AppHeaderManager' + +const AppHeader = () => { + const { user } = useSelector((state) => state.auth) + return user.user.IsEmployee ? : +} + +export default AppHeader diff --git a/src/components/index.js b/src/components/index.js index 6cdf33563..f8276eb38 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,7 +1,7 @@ import AppBreadcrumb from './AppBreadcrumb' import AppContent from './AppContent' import AppFooter from './AppFooter' -import AppHeader from './AppHeader' +import AppHeader from './AppHeader/index' import AppHeaderDropdown from './header/AppHeaderDropdown' import AppSidebar from './AppSidebar' import DocsCallout from './DocsCallout' diff --git a/src/layout/DefaultLayoutEmployee.js b/src/layout/DefaultLayoutEmployee.js new file mode 100644 index 000000000..49ee45d04 --- /dev/null +++ b/src/layout/DefaultLayoutEmployee.js @@ -0,0 +1,22 @@ +import React from 'react' +import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index' +import { useLocation } from 'react-router-dom' + +const DefaultLayout = () => { + const location = useLocation() + + return ( +
    + {location.pathname === '/' && } +
    + +
    + +
    + +
    +
    + ) +} + +export default DefaultLayout diff --git a/src/layout/DefaultLayout.js b/src/layout/DefaultLayoutManager.js similarity index 84% rename from src/layout/DefaultLayout.js rename to src/layout/DefaultLayoutManager.js index 19fbf225f..d26f41d2a 100644 --- a/src/layout/DefaultLayout.js +++ b/src/layout/DefaultLayoutManager.js @@ -1,7 +1,10 @@ import React from 'react' import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index' +import { useLocation } from 'react-router-dom' const DefaultLayout = () => { + const location = useLocation() + return (
    diff --git a/src/views/pages/login/Login.js b/src/views/pages/login/Login.js index f107de86a..c42995247 100644 --- a/src/views/pages/login/Login.js +++ b/src/views/pages/login/Login.js @@ -27,14 +27,18 @@ const Login = () => { const [password, setPassword] = useState('') const dispatch = useDispatch() const navigate = useNavigate() - const { isAuthenticated } = useSelector((state) => state.auth) + const { isAuthenticated, user } = useSelector((state) => state.auth) const handleLogin = async (e) => { e.preventDefault() await dispatch(login(username, password)) await dispatch(checkAuthentication()) if (isAuthenticated) { - navigate('/') + if (user.user.IsEmployee) { + navigate('/') + } else if (user.user.IsManager) { + navigate('/') + } } else { toast.error('Invalid username or password') } From 02d19166eaf23ef854d6cfe451ade5f6165b52e0 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Mon, 28 Apr 2025 16:21:58 +0100 Subject: [PATCH 16/72] feat: add Tickets page and update routing to include it --- src/components/header/AppHeaderDropdown.js | 34 +++++++++++++--------- src/routes.js | 4 +++ src/views/pages/Tickets/TicketsHome.js | 11 +++++++ 3 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 src/views/pages/Tickets/TicketsHome.js diff --git a/src/components/header/AppHeaderDropdown.js b/src/components/header/AppHeaderDropdown.js index 6d660fe70..09ccfee70 100644 --- a/src/components/header/AppHeaderDropdown.js +++ b/src/components/header/AppHeaderDropdown.js @@ -1,36 +1,37 @@ import React from 'react' import { - CAvatar, - CBadge, - CDropdown, CDropdownDivider, + CDropdownToggle, CDropdownHeader, - CDropdownItem, CDropdownMenu, - CDropdownToggle, + CDropdownItem, + CDropdown, + CAvatar, + CBadge, + CCol, } from '@coreui/react' import { - cilBell, - cilCreditCard, cilCommentSquare, + cilAccountLogout, cilEnvelopeOpen, - cilFile, - cilLockLocked, + cilCreditCard, cilSettings, + cilBell, + cilFile, cilTask, cilUser, - cilAccountLogout, } from '@coreui/icons' import CIcon from '@coreui/icons-react' import avatar8 from './../../assets/images/avatars/8.jpg' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' import { logout } from '../../actions/authActions' const AppHeaderDropdown = () => { const dispatch = useDispatch() const navigate = useNavigate() + const { user } = useSelector((state) => state.auth) const handleLogout = async (e) => { e.preventDefault() @@ -39,9 +40,14 @@ const AppHeaderDropdown = () => { } return ( - - - + +
    + {user.user.FirstName} {user.user.LastName} +
    + + + +
    Account diff --git a/src/routes.js b/src/routes.js index d2e9d6479..c93fcf30c 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,6 +1,9 @@ import React from 'react' const Dashboard = React.lazy(() => import('./views/dashboard/Dashboard')) + +const Tickets = React.lazy(() => import('./views/pages/Tickets/TicketsHome')) + const Colors = React.lazy(() => import('./views/theme/colors/Colors')) const Typography = React.lazy(() => import('./views/theme/typography/Typography')) @@ -54,6 +57,7 @@ const Widgets = React.lazy(() => import('./views/widgets/Widgets')) const routes = [ { path: '/', exact: true, name: 'Home' }, { path: '/dashboard', name: 'Dashboard', element: Dashboard }, + { path: '/tickets', name: 'Tickets', element: Tickets }, { path: '/theme', name: 'Theme', element: Colors, exact: true }, { path: '/theme/colors', name: 'Colors', element: Colors }, { path: '/theme/typography', name: 'Typography', element: Typography }, diff --git a/src/views/pages/Tickets/TicketsHome.js b/src/views/pages/Tickets/TicketsHome.js new file mode 100644 index 000000000..24cd1a251 --- /dev/null +++ b/src/views/pages/Tickets/TicketsHome.js @@ -0,0 +1,11 @@ +import React from 'react' + +const Tickets = () => { + return ( + <> +

    Tickets

    + + ) +} + +export default Tickets From c70f83ed4a7011527b0ebf288ddc8dbff2d807be Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Tue, 29 Apr 2025 11:54:19 +0100 Subject: [PATCH 17/72] feat: improve authentication flow with enhanced error handling and redirection logic --- src/App.js | 6 +++- src/actions/authActions.js | 50 ++++++++++++++++++++-------------- src/reducers/authReducer.js | 2 +- src/services/authService.js | 18 ++++++------ src/views/pages/login/Login.js | 37 ++++++++++++++++--------- 5 files changed, 70 insertions(+), 43 deletions(-) diff --git a/src/App.js b/src/App.js index a0f2e0f66..f0c0ffdf4 100644 --- a/src/App.js +++ b/src/App.js @@ -11,7 +11,11 @@ const DefaultLayoutEmployee = React.lazy(() => import('./layout/DefaultLayoutEmp const DefaultLayoutManager = React.lazy(() => import('./layout/DefaultLayoutManager')) const DefaultLayout = () => { const { user } = useSelector((state) => state.auth) - return user.user.IsEmployee ? : + return user !== null && user.user.IsEmployee ? ( + + ) : ( + + ) } // Pages diff --git a/src/actions/authActions.js b/src/actions/authActions.js index 805bd16c4..fbf56dc7c 100644 --- a/src/actions/authActions.js +++ b/src/actions/authActions.js @@ -30,20 +30,23 @@ export const logoutFailure = (error) => ({ payload: error, }) -export const login = (username, password) => async (dispatch) => { +export const login = (username, password) => (dispatch) => { dispatch(loginRequest()) - try { - const user = await authService.login(username, password) - if (user.error) { - dispatch(loginFailure(error)) - throw new Error(user.error) + return authService + .login(username, password) + .then((response) => { + if (response.error) { + dispatch(loginFailure(response.error)) + throw new Error(response.error) } else { - dispatch(loginSuccess(user)) + dispatch(loginSuccess(response)) + return response } - } catch (error) { + }) + .catch((error) => { dispatch(loginFailure(error)) - console.error('Error logging in:', error) - } + throw new Error(error) + }) } export const logout = () => async (dispatch) => { @@ -66,18 +69,25 @@ export const checkAuthSuccess = (data) => ({ payload: data, }) -export const checkAuthFailure = (error) => ({ +export const checkAuthFailure = () => ({ type: 'AUTH_CHECK_FAILURE', - payload: error, }) -export const checkAuthentication = () => async (dispatch) => { +export const checkAuthentication = () => (dispatch) => { dispatch(checkAuthRequest()) - try { - const response = await authService.checkAuth() - dispatch(checkAuthSuccess(response.data)) - } catch (error) { - dispatch(checkAuthFailure(error)) - console.error('Error checking authentication status:', error) - } + return authService + .checkAuth() + .then((response) => { + if (response.data.error) { + dispatch(checkAuthFailure()) + throw new Error(response.error) + } else { + dispatch(checkAuthSuccess(response.data)) + return response + } + }) + .catch((error) => { + dispatch(checkAuthFailure()) + throw new Error(error) + }) } diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js index 0dbae757b..e8c55b1e6 100644 --- a/src/reducers/authReducer.js +++ b/src/reducers/authReducer.js @@ -47,8 +47,8 @@ const authReducer = (state = initialState, action) => { case 'AUTH_CHECK_FAILURE': return { ...state, + isAuthenticated: false, loading: false, - error: action.payload, } case 'LOGOUT': return { diff --git a/src/services/authService.js b/src/services/authService.js index fe69fd045..bfbdeac3a 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -27,14 +27,16 @@ const logout = async () => { } } -const checkAuth = async () => { - try { - const response = await axios.get(`${API_URL}check-auth`) - return response - } catch (error) { - console.error('Error checking authentication status:', error) - throw error - } +const checkAuth = () => { + return axios + .get(`${API_URL}check-auth`) + .then((response) => { + return response + }) + .catch((error) => { + console.error('Error checking authentication:') + return error + }) } const getCurrentUser = () => { diff --git a/src/views/pages/login/Login.js b/src/views/pages/login/Login.js index c42995247..cb712039e 100644 --- a/src/views/pages/login/Login.js +++ b/src/views/pages/login/Login.js @@ -27,22 +27,33 @@ const Login = () => { const [password, setPassword] = useState('') const dispatch = useDispatch() const navigate = useNavigate() - const { isAuthenticated, user } = useSelector((state) => state.auth) - const handleLogin = async (e) => { - e.preventDefault() - await dispatch(login(username, password)) - await dispatch(checkAuthentication()) - if (isAuthenticated) { - if (user.user.IsEmployee) { - navigate('/') - } else if (user.user.IsManager) { - navigate('/') - } - } else { - toast.error('Invalid username or password') + const redirect = (user) => { + if (user.IsEmployee) { + navigate('/') + } else if (user.IsManager) { + navigate('/') } } + const handleLogin = (e) => { + e.preventDefault() + dispatch(login(username, password)) + .then((response) => { + console.log({ response }) + if (response.error) { + toast.error('Invalid username or password') + } + if (response) { + toast.success('Login successful') + redirect(response.user) + } + }) + .then(() => dispatch(checkAuthentication())) + .catch((error) => { + console.error('Login error:', error) + toast.error('Invalid username or password') + }) + } return (
    From 0de653107f8191c45523e4d2e3c3d4bf64e04ddc Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Tue, 29 Apr 2025 12:01:30 +0100 Subject: [PATCH 18/72] feat: add authentication check to AppHeaderDropdown component --- src/components/header/AppHeaderDropdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/header/AppHeaderDropdown.js b/src/components/header/AppHeaderDropdown.js index 09ccfee70..232d6b9c0 100644 --- a/src/components/header/AppHeaderDropdown.js +++ b/src/components/header/AppHeaderDropdown.js @@ -26,7 +26,7 @@ import CIcon from '@coreui/icons-react' import avatar8 from './../../assets/images/avatars/8.jpg' import { useDispatch, useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' -import { logout } from '../../actions/authActions' +import { checkAuthentication, logout } from '../../actions/authActions' const AppHeaderDropdown = () => { const dispatch = useDispatch() From 05ed3a01c5b578d27531364576cd0d9b817b6b5e Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 30 Apr 2025 17:35:48 +0100 Subject: [PATCH 19/72] feat: implement create ticket modal with open/close functionality --- src/actions/appActions.js | 10 +++- src/components/AppHeader/AppHeaderEmployee.js | 42 ++++---------- src/components/Modal/ModalCreateTicket.js | 34 ++++++++++++ src/layout/DefaultLayoutEmployee.js | 2 + src/reducers/appReducer.js | 55 +++++++++++-------- 5 files changed, 89 insertions(+), 54 deletions(-) create mode 100644 src/components/Modal/ModalCreateTicket.js diff --git a/src/actions/appActions.js b/src/actions/appActions.js index 1c8daca8d..c5b4901f6 100644 --- a/src/actions/appActions.js +++ b/src/actions/appActions.js @@ -10,4 +10,12 @@ export const toggleUnfoldable = () => ({ export const switchThemeMode = (theme) => ({ type: 'CHANGE_THEME', payload: theme -}) \ No newline at end of file +}) + +export const toggleCreateTicketModalOpen = () => ({ + type: 'TOGGLE_CREATE_TICKET_MODAL_OPEN', +}) + +export const toggleCreateTicketModalClose = () => ({ + type: 'TOGGLE_CREATE_TICKET_MODAL_CLOSE', +}) diff --git a/src/components/AppHeader/AppHeaderEmployee.js b/src/components/AppHeader/AppHeaderEmployee.js index f4db3684a..160376c4c 100644 --- a/src/components/AppHeader/AppHeaderEmployee.js +++ b/src/components/AppHeader/AppHeaderEmployee.js @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react' -import { NavLink, useLocation } from 'react-router-dom' +import { NavLink } from 'react-router-dom' import { useSelector, useDispatch } from 'react-redux' import { CContainer, @@ -9,32 +9,22 @@ import { CDropdownToggle, CHeader, CHeaderNav, - CHeaderToggler, CNavLink, CNavItem, useColorModes, + CButton, } from '@coreui/react' import CIcon from '@coreui/icons-react' -import { - cilBell, - cilContrast, - cilEnvelopeOpen, - cilList, - cilMenu, - cilMoon, - cilSun, -} from '@coreui/icons' +import { cilBell, cilEnvelopeOpen, cilList, cilMoon, cilSun } from '@coreui/icons' import { AppBreadcrumb } from '../index' import { AppHeaderDropdown } from '../header/index' -import { switchThemeMode, toggleSideBar } from '../../actions/appActions' +import { switchThemeMode, toggleCreateTicketModalOpen } from '../../actions/appActions' const AppHeaderEmployee = () => { - const location = useLocation() const headerRef = useRef() const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') const dispatch = useDispatch() const { theme } = useSelector((state) => state.data) - const { user } = useSelector((state) => state.auth) useEffect(() => { document.addEventListener('scroll', () => { @@ -54,12 +44,6 @@ const AppHeaderEmployee = () => { return ( - {/* dispatch(toggleSideBar())} - style={{ marginInlineStart: '-14px' }} - > - - */} @@ -73,6 +57,13 @@ const AppHeaderEmployee = () => { + + dispatch(toggleCreateTicketModalOpen())}> + + Créer + + + @@ -98,8 +89,6 @@ const AppHeaderEmployee = () => { {colorMode === 'dark' ? ( ) : ( - // ) : colorMode === 'auto' ? ( - // )} @@ -122,15 +111,6 @@ const AppHeaderEmployee = () => { > Dark - {/* switchColorMode('auto')} - > - Auto - */}
  • diff --git a/src/components/Modal/ModalCreateTicket.js b/src/components/Modal/ModalCreateTicket.js new file mode 100644 index 000000000..7a0844614 --- /dev/null +++ b/src/components/Modal/ModalCreateTicket.js @@ -0,0 +1,34 @@ +import { CButton, CModal, CModalBody, CModalFooter, CModalHeader } from '@coreui/react' +import React from 'react' +import { toggleCreateTicketModalClose } from '../../actions/appActions' +import { useDispatch, useSelector } from 'react-redux' + +const ModalCreateTicket = () => { + const { isCreateTicketModalOpen } = useSelector((state) => state.data) + const dispatch = useDispatch() + return ( + dispatch(toggleCreateTicketModalClose())} + backdrop="static" + aria-labelledby="ScrollingLongContentExampleLabel LiveDemoExampleLabel" + scrollable + alignment="center" + > + dispatch(toggleCreateTicketModalClose())}> + Créer un nouveau ticket + + +

    Les champs obligatoires sont marqués d'un astérisque *

    +
    + + dispatch(toggleCreateTicketModalClose())}> + Fermer + + Sauvegarder + +
    + ) +} + +export default ModalCreateTicket diff --git a/src/layout/DefaultLayoutEmployee.js b/src/layout/DefaultLayoutEmployee.js index 49ee45d04..c561beb1c 100644 --- a/src/layout/DefaultLayoutEmployee.js +++ b/src/layout/DefaultLayoutEmployee.js @@ -1,6 +1,7 @@ import React from 'react' import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index' import { useLocation } from 'react-router-dom' +import ModalCreateTicket from '../components/Modal/ModalCreateTicket' const DefaultLayout = () => { const location = useLocation() @@ -12,6 +13,7 @@ const DefaultLayout = () => {
    +
  • diff --git a/src/reducers/appReducer.js b/src/reducers/appReducer.js index b3e64eaca..54d471cc2 100644 --- a/src/reducers/appReducer.js +++ b/src/reducers/appReducer.js @@ -1,30 +1,41 @@ /* eslint-disable prettier/prettier */ const initialState = { - sidebarShow: true, - sidebarUnfoldable: false, - theme: 'light', + sidebarShow: true, + sidebarUnfoldable: false, + theme: 'light', + isCreateTicketModalOpen: true, } const appReducer = (state = initialState, action) => { - switch (action.type) { - case 'TOGGLE_SIDEBAR': - return { - ...state, - sidebarShow: !state.sidebarShow - } - case 'TOGGLE_UNFOLDABLE': - return { - ...state, - sidebarUnfoldable: !state.sidebarUnfoldable - } - case 'CHANGE_THEME': - return { - ...state, - theme: action.payload - } - default: - return state - } + switch (action.type) { + case 'TOGGLE_SIDEBAR': + return { + ...state, + sidebarShow: !state.sidebarShow, + } + case 'TOGGLE_UNFOLDABLE': + return { + ...state, + sidebarUnfoldable: !state.sidebarUnfoldable, + } + case 'CHANGE_THEME': + return { + ...state, + theme: action.payload, + } + case 'TOGGLE_CREATE_TICKET_MODAL_OPEN': + return { + ...state, + isCreateTicketModalOpen: true, + } + case 'TOGGLE_CREATE_TICKET_MODAL_CLOSE': + return { + ...state, + isCreateTicketModalOpen: false, + } + default: + return state + } } export default appReducer From e35a642fff8ed771013462f2bf78905fa85779d5 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Thu, 1 May 2025 14:52:20 +0100 Subject: [PATCH 20/72] feat: refactor authentication and layout components; add Jira configuration page and related services --- src/App.js | 37 ++--- src/actions/authActions.js | 29 ++-- src/actions/jiraActions.js | 35 +++++ .../AppHeaderManager.js => AppHeader.js} | 41 +++--- src/components/AppHeader/AppHeaderEmployee.js | 129 ------------------ src/components/AppHeader/index.js | 11 -- src/components/header/AppHeaderDropdown.js | 10 +- .../header/AppHeaderDropdownManager.js | 96 +++++++++++++ src/components/header/index.js | 3 +- src/components/index.js | 2 +- ...aultLayoutEmployee.js => DefaultLayout.js} | 2 +- src/layout/DefaultLayoutManager.js | 22 --- src/reducers/appReducer.js | 2 +- src/reducers/authReducer.js | 1 - src/reducers/index.js | 4 +- src/reducers/jiraReducer.js | 31 +++++ src/routes.js | 4 + src/services/authService.js | 33 ++++- src/services/jiraService.js | 23 ++++ src/store.js | 2 + src/views/pages/jira/ConfigJiraApi.js | 80 +++++++++++ 21 files changed, 364 insertions(+), 233 deletions(-) create mode 100644 src/actions/jiraActions.js rename src/components/{AppHeader/AppHeaderManager.js => AppHeader.js} (81%) delete mode 100644 src/components/AppHeader/AppHeaderEmployee.js delete mode 100644 src/components/AppHeader/index.js create mode 100644 src/components/header/AppHeaderDropdownManager.js rename src/layout/{DefaultLayoutEmployee.js => DefaultLayout.js} (91%) delete mode 100644 src/layout/DefaultLayoutManager.js create mode 100644 src/reducers/jiraReducer.js create mode 100644 src/services/jiraService.js create mode 100644 src/views/pages/jira/ConfigJiraApi.js diff --git a/src/App.js b/src/App.js index f0c0ffdf4..38c585f0e 100644 --- a/src/App.js +++ b/src/App.js @@ -1,22 +1,13 @@ -import React, { Suspense, useEffect, useState } from 'react' +import React, { Suspense, useCallback, useEffect, useState } from 'react' import { BrowserRouter, Route, Routes } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import { CSpinner } from '@coreui/react' import './scss/style.scss' import PrivateRoute from './PrivateRute' import { checkAuthentication } from './actions/authActions' // Containers -const DefaultLayoutEmployee = React.lazy(() => import('./layout/DefaultLayoutEmployee')) -const DefaultLayoutManager = React.lazy(() => import('./layout/DefaultLayoutManager')) -const DefaultLayout = () => { - const { user } = useSelector((state) => state.auth) - return user !== null && user.user.IsEmployee ? ( - - ) : ( - - ) -} +const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout')) // Pages const Login = React.lazy(() => import('./views/pages/login/Login')) @@ -28,19 +19,21 @@ const App = () => { const dispatch = useDispatch() const [isChecking, setIsChecking] = useState(true) - useEffect(() => { - const checkAuth = async () => { - try { - await dispatch(checkAuthentication()) - } catch (error) { - console.error('Authentication check failed:', error) - } finally { - setIsChecking(false) - } + const checkAuth = useCallback(async () => { + console.log('checkAuth') + try { + await dispatch(checkAuthentication()) + } catch (error) { + console.error('Authentication check failed:', error) + } finally { + setIsChecking(false) } - checkAuth() }, [dispatch]) + useEffect(() => { + checkAuth() + }, [checkAuth]) + if (isChecking) { return (
    diff --git a/src/actions/authActions.js b/src/actions/authActions.js index fbf56dc7c..b53d1c2f7 100644 --- a/src/actions/authActions.js +++ b/src/actions/authActions.js @@ -1,4 +1,3 @@ -/* eslint-disable prettier/prettier */ import React from 'react' import authService from '../services/authService' @@ -33,20 +32,20 @@ export const logoutFailure = (error) => ({ export const login = (username, password) => (dispatch) => { dispatch(loginRequest()) return authService - .login(username, password) - .then((response) => { - if (response.error) { - dispatch(loginFailure(response.error)) - throw new Error(response.error) - } else { - dispatch(loginSuccess(response)) - return response - } - }) - .catch((error) => { - dispatch(loginFailure(error)) - throw new Error(error) - }) + .login(username, password) + .then((response) => { + if (response.error) { + dispatch(loginFailure(response.error)) + throw new Error(response.error) + } else { + dispatch(loginSuccess(response)) + return response + } + }) + .catch((error) => { + dispatch(loginFailure(error)) + throw new Error(error) + }) } export const logout = () => async (dispatch) => { diff --git a/src/actions/jiraActions.js b/src/actions/jiraActions.js new file mode 100644 index 000000000..793800a47 --- /dev/null +++ b/src/actions/jiraActions.js @@ -0,0 +1,35 @@ +import React from 'react' +import jiraService from '../services/jiraService' + +export const GetAllConfigJiraRequest = () => ({ + type: 'GET_ALL_CONFIG_JIRA_REQUEST', +}) + +export const GET_ALL_CONFIG_JIRA_SUCCESS = (user) => ({ + type: 'GET_ALL_CONFIG_JIRA_SUCCESS', + payload: user, +}) + +export const GET_ALL_CONFIG_JIRA_FAILURE = (error) => ({ + type: 'GET_ALL_CONFIG_JIRA_FAILURE', + payload: error, +}) + +export const getAllConfigJiraAPI = () => (dispatch) => { + dispatch(GetAllConfigJiraRequest()) + return jiraService + .getAllConfigJira() + .then((response) => { + if (response.error) { + dispatch(GET_ALL_CONFIG_JIRA_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(GET_ALL_CONFIG_JIRA_SUCCESS(response.data.data)) + return response + } + }) + .catch((error) => { + dispatch(GET_ALL_CONFIG_JIRA_FAILURE(error)) + throw new Error(error) + }) +} diff --git a/src/components/AppHeader/AppHeaderManager.js b/src/components/AppHeader.js similarity index 81% rename from src/components/AppHeader/AppHeaderManager.js rename to src/components/AppHeader.js index 8865b4ff7..047033853 100644 --- a/src/components/AppHeader/AppHeaderManager.js +++ b/src/components/AppHeader.js @@ -13,6 +13,7 @@ import { CNavLink, CNavItem, useColorModes, + CButton, } from '@coreui/react' import CIcon from '@coreui/icons-react' import { @@ -25,10 +26,10 @@ import { cilSun, } from '@coreui/icons' -import { AppBreadcrumb } from '../index' -import { AppHeaderDropdown } from '../header/index' -import { switchThemeMode, toggleSideBar } from '../../actions/appActions' -const AppHeaderManager = () => { +import { AppBreadcrumb } from './index' +import { AppHeaderDropdown, AppHeaderDropdownManager } from './header/index' +import { switchThemeMode, toggleCreateTicketModalOpen, toggleSideBar } from '../actions/appActions' +const AppHeader = () => { const location = useLocation() const headerRef = useRef() const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') @@ -76,8 +77,20 @@ const AppHeaderManager = () => { Projects + + + Tickets + + + + dispatch(toggleCreateTicketModalOpen())}> + + Créer + + + @@ -103,8 +116,6 @@ const AppHeaderManager = () => { {colorMode === 'dark' ? ( ) : ( - // ) : colorMode === 'auto' ? ( - // )} @@ -127,21 +138,17 @@ const AppHeaderManager = () => { > Dark - {/* switchColorMode('auto')} - > - Auto - */}
  • - + {user !== null && user.user.IsEmployee && !user.user.IsManager ? ( + + ) : user !== null && !user.user.IsEmployee && user.user.IsManager ? ( + + ) : null} + {/* */}
    @@ -151,4 +158,4 @@ const AppHeaderManager = () => { ) } -export default AppHeaderManager +export default AppHeader diff --git a/src/components/AppHeader/AppHeaderEmployee.js b/src/components/AppHeader/AppHeaderEmployee.js deleted file mode 100644 index 160376c4c..000000000 --- a/src/components/AppHeader/AppHeaderEmployee.js +++ /dev/null @@ -1,129 +0,0 @@ -import React, { useEffect, useRef } from 'react' -import { NavLink } from 'react-router-dom' -import { useSelector, useDispatch } from 'react-redux' -import { - CContainer, - CDropdown, - CDropdownItem, - CDropdownMenu, - CDropdownToggle, - CHeader, - CHeaderNav, - CNavLink, - CNavItem, - useColorModes, - CButton, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' -import { cilBell, cilEnvelopeOpen, cilList, cilMoon, cilSun } from '@coreui/icons' - -import { AppBreadcrumb } from '../index' -import { AppHeaderDropdown } from '../header/index' -import { switchThemeMode, toggleCreateTicketModalOpen } from '../../actions/appActions' -const AppHeaderEmployee = () => { - const headerRef = useRef() - const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') - const dispatch = useDispatch() - const { theme } = useSelector((state) => state.data) - - useEffect(() => { - document.addEventListener('scroll', () => { - headerRef.current && - headerRef.current.classList.toggle('shadow-sm', document.documentElement.scrollTop > 0) - }) - }, []) - - const switchColorMode = (color) => { - dispatch(switchThemeMode(color)) - } - - useEffect(() => { - setColorMode(theme) - }, [theme]) - - return ( - - - - - - Dashboard - - - - - Tickets - - - - - - dispatch(toggleCreateTicketModalOpen())}> - - Créer - - - - - - - - - - - - - - - - - - - - -
  • -
    -
  • - - - {colorMode === 'dark' ? ( - - ) : ( - - )} - - - switchColorMode('light')} - > - Light - - switchColorMode('dark')} - > - Dark - - - -
  • -
    -
  • - -
    -
    - - - -
    - ) -} - -export default AppHeaderEmployee diff --git a/src/components/AppHeader/index.js b/src/components/AppHeader/index.js deleted file mode 100644 index f46a31290..000000000 --- a/src/components/AppHeader/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' -import AppHeaderEmployee from './AppHeaderEmployee' -import AppHeaderManager from './AppHeaderManager' - -const AppHeader = () => { - const { user } = useSelector((state) => state.auth) - return user.user.IsEmployee ? : -} - -export default AppHeader diff --git a/src/components/header/AppHeaderDropdown.js b/src/components/header/AppHeaderDropdown.js index 232d6b9c0..fdfedd295 100644 --- a/src/components/header/AppHeaderDropdown.js +++ b/src/components/header/AppHeaderDropdown.js @@ -36,7 +36,7 @@ const AppHeaderDropdown = () => { const handleLogout = async (e) => { e.preventDefault() await dispatch(logout()) - await dispatch(checkAuthentication()) + // await dispatch(checkAuthentication()) } return ( @@ -87,20 +87,20 @@ const AppHeaderDropdown = () => { Settings - + {/* Payments 42 - - + */} + {/* Projects 42 - + */} diff --git a/src/components/header/AppHeaderDropdownManager.js b/src/components/header/AppHeaderDropdownManager.js new file mode 100644 index 000000000..046a2b17b --- /dev/null +++ b/src/components/header/AppHeaderDropdownManager.js @@ -0,0 +1,96 @@ +import React from 'react' +import { + CDropdownDivider, + CDropdownToggle, + CDropdownHeader, + CDropdownMenu, + CDropdownItem, + CDropdown, + CAvatar, + CBadge, + CCol, +} from '@coreui/react' +import { + cilCommentSquare, + cilAccountLogout, + cilEnvelopeOpen, + cilCreditCard, + cilSettings, + cilBell, + cilFile, + cilTask, + cilUser, +} from '@coreui/icons' +import CIcon from '@coreui/icons-react' + +import avatar8 from './../../assets/images/avatars/8.jpg' +import { useDispatch, useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { checkAuthentication, logout } from '../../actions/authActions' + +const AppHeaderDropdownManager = () => { + const dispatch = useDispatch() + const navigate = useNavigate() + const { user } = useSelector((state) => state.auth) + + const handleLogout = async (e) => { + e.preventDefault() + await dispatch(logout()) + // await dispatch(checkAuthentication()) + } + return ( + + +
    + {user.user.FirstName} {user.user.LastName} +
    + + + +
    + + Account + + + Updates + + 42 + + + + + Messages + + 42 + + + + + Tasks + + 42 + + + + + Comments + + 42 + + + Settings + Profile + Settings + configuration jira API + Projects + + + + Logout + + +
    + ) +} + +export default AppHeaderDropdownManager diff --git a/src/components/header/index.js b/src/components/header/index.js index bf8af6c1c..5e5673927 100644 --- a/src/components/header/index.js +++ b/src/components/header/index.js @@ -1,3 +1,4 @@ import AppHeaderDropdown from './AppHeaderDropdown' +import AppHeaderDropdownManager from './AppHeaderDropdownManager' -export { AppHeaderDropdown } +export { AppHeaderDropdown, AppHeaderDropdownManager } diff --git a/src/components/index.js b/src/components/index.js index f8276eb38..6cdf33563 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,7 +1,7 @@ import AppBreadcrumb from './AppBreadcrumb' import AppContent from './AppContent' import AppFooter from './AppFooter' -import AppHeader from './AppHeader/index' +import AppHeader from './AppHeader' import AppHeaderDropdown from './header/AppHeaderDropdown' import AppSidebar from './AppSidebar' import DocsCallout from './DocsCallout' diff --git a/src/layout/DefaultLayoutEmployee.js b/src/layout/DefaultLayout.js similarity index 91% rename from src/layout/DefaultLayoutEmployee.js rename to src/layout/DefaultLayout.js index c561beb1c..e17b83b3a 100644 --- a/src/layout/DefaultLayoutEmployee.js +++ b/src/layout/DefaultLayout.js @@ -8,7 +8,7 @@ const DefaultLayout = () => { return (
    - {location.pathname === '/' && } +
    diff --git a/src/layout/DefaultLayoutManager.js b/src/layout/DefaultLayoutManager.js deleted file mode 100644 index d26f41d2a..000000000 --- a/src/layout/DefaultLayoutManager.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react' -import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index' -import { useLocation } from 'react-router-dom' - -const DefaultLayout = () => { - const location = useLocation() - - return ( -
    - -
    - -
    - -
    - -
    -
    - ) -} - -export default DefaultLayout diff --git a/src/reducers/appReducer.js b/src/reducers/appReducer.js index 54d471cc2..4ae7f6374 100644 --- a/src/reducers/appReducer.js +++ b/src/reducers/appReducer.js @@ -3,7 +3,7 @@ const initialState = { sidebarShow: true, sidebarUnfoldable: false, theme: 'light', - isCreateTicketModalOpen: true, + isCreateTicketModalOpen: false, } const appReducer = (state = initialState, action) => { diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js index e8c55b1e6..7126f1d75 100644 --- a/src/reducers/authReducer.js +++ b/src/reducers/authReducer.js @@ -1,4 +1,3 @@ -/* eslint-disable prettier/prettier */ const initialState = { isAuthenticated: false, user: null, diff --git a/src/reducers/index.js b/src/reducers/index.js index 1844743f7..18a6c4e8f 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -2,10 +2,12 @@ import { combineReducers } from 'redux' import authReducer from './authReducer' import appReducer from './appReducer' +import jiraReducer from './jiraReducer' const rootReducer = combineReducers({ auth: authReducer, - app: appReducer + app: appReducer, + jira: jiraReducer, }) export default rootReducer diff --git a/src/reducers/jiraReducer.js b/src/reducers/jiraReducer.js new file mode 100644 index 000000000..7a6f6bf36 --- /dev/null +++ b/src/reducers/jiraReducer.js @@ -0,0 +1,31 @@ +const initialState = { + jiraConfigList: [], + loading: false, + error: null, +} + +const jiraReducer = (state = initialState, action) => { + switch (action.type) { + case 'GET_ALL_CONFIG_JIRA_REQUEST': + return { + ...state, + loading: true, + } + case 'GET_ALL_CONFIG_JIRA_SUCCESS': + return { + ...state, + jiraConfigList: action.payload, + loading: false, + } + case 'GET_ALL_CONFIG_JIRA_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + default: + return state + } +} + +export default jiraReducer diff --git a/src/routes.js b/src/routes.js index c93fcf30c..6de66cd9c 100644 --- a/src/routes.js +++ b/src/routes.js @@ -54,10 +54,14 @@ const Toasts = React.lazy(() => import('./views/notifications/toasts/Toasts')) const Widgets = React.lazy(() => import('./views/widgets/Widgets')) +const ConfigJiraApi = React.lazy(() => import('./views/pages/jira/ConfigJiraApi')) + const routes = [ { path: '/', exact: true, name: 'Home' }, { path: '/dashboard', name: 'Dashboard', element: Dashboard }, { path: '/tickets', name: 'Tickets', element: Tickets }, + { path: '/jira', name: 'Jira', element: ConfigJiraApi, exact: true }, + { path: '/jira/config-jira-api', name: 'Configuration Jira API', element: ConfigJiraApi }, { path: '/theme', name: 'Theme', element: Colors, exact: true }, { path: '/theme/colors', name: 'Colors', element: Colors }, { path: '/theme/typography', name: 'Typography', element: Typography }, diff --git a/src/services/authService.js b/src/services/authService.js index bfbdeac3a..a9be6f39a 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -6,7 +6,7 @@ const login = async (email, password) => { try { const response = await axios.post(`${API_URL}signin`, { email, password }) if (response.data.token) { - localStorage.setItem('user', JSON.stringify(response.data)) + localStorage.setItem('token', response.data.token) } return response.data } catch (error) { @@ -17,19 +17,40 @@ const login = async (email, password) => { const logout = async () => { try { - const response = await axios.post(`${API_URL}logout`) - console.log(response) - localStorage.removeItem('user') + const response = await axios.post( + `${API_URL}logout`, + {}, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }, + ) + if (response.data.clearToken) { + localStorage.removeItem('user') + localStorage.removeItem('token') + // Clear Axios default headers + delete axios.defaults.headers.common['Authorization'] + // Redirect to login page or update UI state + } return response.data } catch (error) { - console.error('Error logout in:', error) + if (error.response?.status === 401) { + // Token already invalid/expired, clean up anyway + localStorage.removeItem('token') + delete axios.defaults.headers.common['Authorization'] + } throw error } } const checkAuth = () => { return axios - .get(`${API_URL}check-auth`) + .get(`${API_URL}check-auth`, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }) .then((response) => { return response }) diff --git a/src/services/jiraService.js b/src/services/jiraService.js new file mode 100644 index 000000000..85167cce2 --- /dev/null +++ b/src/services/jiraService.js @@ -0,0 +1,23 @@ +import axios from 'axios' + +const API_URL = 'http://localhost:8081/jira_config/' + +const getAllConfigJira = () => { + return axios + .get(`${API_URL}getAllConfig`, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }) + .then((response) => { + return response + }) + .catch((error) => { + console.error('Error fetching all config Jira:', error) + return error + }) +} + +export default { + getAllConfigJira, +} diff --git a/src/store.js b/src/store.js index 8016125b1..245c065d2 100644 --- a/src/store.js +++ b/src/store.js @@ -6,10 +6,12 @@ import { Provider } from 'react-redux' import PropTypes from 'prop-types' import authReducer from './reducers/authReducer' import dataReducer from './reducers/appReducer' +import jiraReducer from './reducers/jiraReducer' const rootReducer = combineReducers({ auth: authReducer, data: dataReducer, + jira: jiraReducer, }) const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk))) diff --git a/src/views/pages/jira/ConfigJiraApi.js b/src/views/pages/jira/ConfigJiraApi.js new file mode 100644 index 000000000..33be641dd --- /dev/null +++ b/src/views/pages/jira/ConfigJiraApi.js @@ -0,0 +1,80 @@ +import React, { useEffect, useState } from 'react' +import { getAllConfigJiraAPI } from '../../../actions/jiraActions' +import { useDispatch, useSelector } from 'react-redux' +import { CTable, CButton } from '@coreui/react' + +const columns = [ + { + key: 'Host', + label: 'Host', + _props: { scope: 'col' }, + }, + { + key: 'Username', + label: 'Username', + _props: { scope: 'col' }, + }, + { + key: 'Protocol', + label: 'Protocol', + _props: { scope: 'col' }, + }, + { + key: 'API Version', + label: 'API Version', + _props: { scope: 'col' }, + }, + { + key: 'Strict SSL', + label: 'Strict SSL', + _props: { scope: 'col' }, + }, +] + +const ConfigJiraApi = () => { + const dispatch = useDispatch() + const { jiraConfigList } = useSelector((state) => state.jira) + const [configItems, setConfigItems] = useState([]) + + useEffect(() => { + if (configItems.length === 0) { + dispatch(getAllConfigJiraAPI()) + } + }, [dispatch]) + + useEffect(() => { + if (jiraConfigList) { + console.log('Jira Config:', jiraConfigList) + jiraConfigList.map((item) => { + setConfigItems((prev) => [ + ...prev, + { + Host: item.host, + Username: item.username, + Protocol: item.protocol, + 'API Version': item.apiVersion, + 'Strict SSL': item.strictSSL, + }, + ]) + }) + } + }, [jiraConfigList]) + + return ( +
    +
    +

    Configuration Jira API

    +

    Current Jira API configuration settings

    +
    +
    + +
    +
    + Edit Configuration + Test Connection +
    +
    + ) +} + +export default React.memo(ConfigJiraApi) From 0d90ea2cb67a069784b442a0554b504b994dbfbc Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sat, 3 May 2025 17:35:38 +0100 Subject: [PATCH 21/72] feat: remove debug log from authentication check function --- src/App.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.js b/src/App.js index 38c585f0e..dd304e11a 100644 --- a/src/App.js +++ b/src/App.js @@ -20,7 +20,6 @@ const App = () => { const [isChecking, setIsChecking] = useState(true) const checkAuth = useCallback(async () => { - console.log('checkAuth') try { await dispatch(checkAuthentication()) } catch (error) { From 3304b3c1b377f916ca6cda28323aec1de9cfd3e0 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sat, 3 May 2025 17:35:48 +0100 Subject: [PATCH 22/72] feat: format ToastContainer props for improved readability --- src/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 75efbe22b..41ccaf58e 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,14 @@ const root = createRoot(container) root.render( - + , From 7bed9f8c2c78a83f116c1b01b489bfe0d303f195 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sat, 3 May 2025 17:36:06 +0100 Subject: [PATCH 23/72] feat: set initial sidebarShow state to false --- src/reducers/appReducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reducers/appReducer.js b/src/reducers/appReducer.js index 4ae7f6374..14ec40146 100644 --- a/src/reducers/appReducer.js +++ b/src/reducers/appReducer.js @@ -1,6 +1,6 @@ /* eslint-disable prettier/prettier */ const initialState = { - sidebarShow: true, + sidebarShow: false, sidebarUnfoldable: false, theme: 'light', isCreateTicketModalOpen: false, From 8d4882be53141045bea24d80cfbdf1bfcd306398 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sat, 3 May 2025 17:36:21 +0100 Subject: [PATCH 24/72] feat: add Jira API connection and configuration management features --- src/actions/jiraActions.js | 58 +++++++++ src/reducers/jiraReducer.js | 35 ++++++ src/services/jiraService.js | 42 +++++++ src/views/forms/addNewConfigJira.js | 170 ++++++++++++++++++++++++++ src/views/pages/jira/ConfigJiraApi.js | 99 ++++++++++----- 5 files changed, 373 insertions(+), 31 deletions(-) create mode 100644 src/views/forms/addNewConfigJira.js diff --git a/src/actions/jiraActions.js b/src/actions/jiraActions.js index 793800a47..3cff91400 100644 --- a/src/actions/jiraActions.js +++ b/src/actions/jiraActions.js @@ -15,6 +15,32 @@ export const GET_ALL_CONFIG_JIRA_FAILURE = (error) => ({ payload: error, }) +export const CHECK_CONNECTION_JIRA_API_REQUEST = () => ({ + type: 'CHECK_CONNECTION_JIRA_API_REQUEST', +}) + +export const CHECK_CONNECTION_JIRA_API_SUCCESS = () => ({ + type: 'CHECK_CONNECTION_JIRA_API_SUCCESS', +}) + +export const CHECK_CONNECTION_JIRA_API_FAILURE = (error) => ({ + type: 'CHECK_CONNECTION_JIRA_API_FAILURE', + payload: error, +}) + +export const ADD_NEW_CONFIG_JIRA_API_REQUEST = () => ({ + type: 'ADD_NEW_CONFIG_JIRA_API_REQUEST', +}) + +export const ADD_NEW_CONFIG_JIRA_API_SUCCESS = () => ({ + type: 'ADD_NEW_CONFIG_JIRA_API_SUCCESS', +}) + +export const ADD_NEW_CONFIG_JIRA_API_FAILURE = (error) => ({ + type: 'ADD_NEW_CONFIG_JIRA_API_FAILURE', + payload: error, +}) + export const getAllConfigJiraAPI = () => (dispatch) => { dispatch(GetAllConfigJiraRequest()) return jiraService @@ -33,3 +59,35 @@ export const getAllConfigJiraAPI = () => (dispatch) => { throw new Error(error) }) } + +export const checkConnectionJiraAPI = + (protocol, host, username, password, apiVersion, strictSSL) => (dispatch) => { + dispatch(CHECK_CONNECTION_JIRA_API_REQUEST()) + return jiraService + .checkConnectionJiraApi(protocol, host, username, password, apiVersion, strictSSL) + .then((response) => { + console.log(response) + dispatch(CHECK_CONNECTION_JIRA_API_SUCCESS()) + return response + }) + .catch((error) => { + dispatch(CHECK_CONNECTION_JIRA_API_FAILURE(error)) + throw new Error(error) + }) + } + +export const addNewConfigJiraAPI = + (protocol, host, username, password, apiVersion, strictSSL) => (dispatch) => { + dispatch(CHECK_CONNECTION_JIRA_API_REQUEST()) + return jiraService + .addNewConfigJiraAPI(protocol, host, username, password, apiVersion, strictSSL) + .then((response) => { + console.log(response) + dispatch(CHECK_CONNECTION_JIRA_API_SUCCESS()) + return response + }) + .catch((error) => { + dispatch(CHECK_CONNECTION_JIRA_API_FAILURE(error)) + throw new Error(error) + }) + } diff --git a/src/reducers/jiraReducer.js b/src/reducers/jiraReducer.js index 7a6f6bf36..bd66c443c 100644 --- a/src/reducers/jiraReducer.js +++ b/src/reducers/jiraReducer.js @@ -1,4 +1,5 @@ const initialState = { + configCanbeAdded: false, jiraConfigList: [], loading: false, error: null, @@ -23,6 +24,40 @@ const jiraReducer = (state = initialState, action) => { loading: false, error: action.payload, } + case 'CHECK_CONNECTION_JIRA_API_REQUEST': + return { + ...state, + loading: true, + } + case 'CHECK_CONNECTION_JIRA_API_SUCCESS': + return { + ...state, + configCanbeAdded: true, + loading: false, + } + case 'CHECK_CONNECTION_JIRA_API_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + configCanbeAdded: false, + } + case 'ADD_NEW_CONFIG_JIRA_API_REQUEST': + return { + ...state, + loading: true, + } + case 'ADD_NEW_CONFIG_JIRA_API_SUCCESS': + return { + ...state, + loading: false, + } + case 'ADD_NEW_CONFIG_JIRA_API_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } default: return state } diff --git a/src/services/jiraService.js b/src/services/jiraService.js index 85167cce2..81c99698b 100644 --- a/src/services/jiraService.js +++ b/src/services/jiraService.js @@ -18,6 +18,48 @@ const getAllConfigJira = () => { }) } +const checkConnectionJiraApi = (protocol, host, username, password, apiVersion, strictSSL) => { + return axios + .post( + `${API_URL}checkConnection`, + { protocol, host, username, password, apiVersion, strictSSL }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }, + ) + .then((response) => { + return response + }) + .catch((error) => { + console.log('Error checking connection Api Jira:', error) + return error + }) +} + +const addNewConfigJiraAPI = (protocol, host, username, password, apiVersion, strictSSL) => { + return axios + .post( + `${API_URL}addConfig`, + { protocol, host, username, password, apiVersion, strictSSL }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }, + ) + .then((response) => { + return response + }) + .catch((error) => { + console.log('Error adding new config Api Jira:', error) + return error + }) +} + export default { getAllConfigJira, + checkConnectionJiraApi, + addNewConfigJiraAPI, } diff --git a/src/views/forms/addNewConfigJira.js b/src/views/forms/addNewConfigJira.js new file mode 100644 index 000000000..f7819d028 --- /dev/null +++ b/src/views/forms/addNewConfigJira.js @@ -0,0 +1,170 @@ +import React, { useState } from 'react' +import { CButton, CCallout, CForm, CFormCheck, CFormInput } from '@coreui/react' +import { addNewConfigJiraAPI, checkConnectionJiraAPI, getAllConfigJiraAPI } from '../../actions/jiraActions' +import { useDispatch, useSelector } from 'react-redux' +import { toast } from 'react-toastify' + +const AddNewConfigJira = () => { + const dispatch = useDispatch() + const { configCanbeAdded } = useSelector((state) => state.jira) + + const [FormControlInputHostURL, setFormControlInputHostURL] = useState('') + const [RadioOptionProtocol, setRadioOptionProtocol] = useState('https') + const [FormControlInputUsername, setFormControlInputUsername] = useState('') + const [FormControlInputPassword, setFormControlInputPassword] = useState('') + const [FormControlInputAPIVersion, setFormControlInputAPIVersion] = useState(2) + const [CheckStrictSSL, setCheckStrictSSL] = useState(false) + + const checkConnection = () => { + dispatch( + checkConnectionJiraAPI( + RadioOptionProtocol, + FormControlInputHostURL, + FormControlInputUsername, + FormControlInputPassword, + FormControlInputAPIVersion, + CheckStrictSSL, + ), + ) + .then((response) => { + if (response) { + if (response.data.error) { + toast.error('Connection failed') + } else { + toast.success('Connection successful') + } + } + }) + .catch((error) => { + console.error('Error checking connection:', error) + toast.error('Connection failed') + }) + } + + const handleFormSubmit = (e) => { + e.preventDefault() + if (configCanbeAdded) { + dispatch( + addNewConfigJiraAPI( + RadioOptionProtocol, + FormControlInputHostURL, + FormControlInputUsername, + FormControlInputPassword, + FormControlInputAPIVersion, + CheckStrictSSL, + ), + ) + .then((response) => { + if (response) { + console.log(response) + if (response.data.error) { + toast.error('adding failed') + } else { + toast.success('successful adding') + } + } + }) + .then(() => { + dispatch(getAllConfigJiraAPI()) + }) + .catch((error) => { + console.error('Error checking connection:', error) + toast.error('Connection failed') + }) + } else { + toast.error('please check the connection before adding a configuration') + } + } + return ( + + setFormControlInputHostURL(e.target.value)} + value={FormControlInputHostURL} + /> + setRadioOptionProtocol(e.target.value)} + /> + setRadioOptionProtocol(e.target.value)} + /> +
    + setFormControlInputUsername(e.target.value)} + value={FormControlInputUsername} + /> + setFormControlInputPassword(e.target.value)} + value={FormControlInputPassword} + /> + setFormControlInputAPIVersion(e.target.value)} + value={FormControlInputAPIVersion} + /> + setCheckStrictSSL(e.target.checked)} + checked={CheckStrictSSL} + /> + + before adding a configuration, please make sure that the host url is reachable and the + username and password are correct.
    + Note: please check the Connection before adding a configuration. +
    +
    + handleFormSubmit(e)}> + Add Configuration + + checkConnection()}> + Test Connection + +
    +
    + ) +} + +export default AddNewConfigJira diff --git a/src/views/pages/jira/ConfigJiraApi.js b/src/views/pages/jira/ConfigJiraApi.js index 33be641dd..ac19aa083 100644 --- a/src/views/pages/jira/ConfigJiraApi.js +++ b/src/views/pages/jira/ConfigJiraApi.js @@ -1,8 +1,21 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { getAllConfigJiraAPI } from '../../../actions/jiraActions' import { useDispatch, useSelector } from 'react-redux' -import { CTable, CButton } from '@coreui/react' - +import { + CTable, + CButton, + CCol, + CRow, + CContainer, + CCollapse, + CCard, + CCardBody, + CForm, + CFormInput, + CFormSelect, + CFormCheck, +} from '@coreui/react' +import AddNewConfigJira from '../../forms/addNewConfigJira' const columns = [ { key: 'Host', @@ -24,56 +37,80 @@ const columns = [ label: 'API Version', _props: { scope: 'col' }, }, - { - key: 'Strict SSL', - label: 'Strict SSL', - _props: { scope: 'col' }, - }, + // { + // key: 'Strict SSL', + // label: 'Strict SSL', + // _props: { scope: 'col' }, + // }, ] const ConfigJiraApi = () => { const dispatch = useDispatch() + + const isFirstRender = useRef(true) + const { jiraConfigList } = useSelector((state) => state.jira) + + const [visible, setVisible] = useState(true) const [configItems, setConfigItems] = useState([]) + const handleClickAjouterConfiguration = (event) => { + event.preventDefault() + setVisible(!visible) + } + useEffect(() => { - if (configItems.length === 0) { + if (isFirstRender.current) { dispatch(getAllConfigJiraAPI()) + isFirstRender.current = false } }, [dispatch]) useEffect(() => { - if (jiraConfigList) { - console.log('Jira Config:', jiraConfigList) - jiraConfigList.map((item) => { - setConfigItems((prev) => [ - ...prev, - { - Host: item.host, - Username: item.username, - Protocol: item.protocol, - 'API Version': item.apiVersion, - 'Strict SSL': item.strictSSL, - }, - ]) - }) + if (jiraConfigList && jiraConfigList.length > 0) { + const transformedItems = jiraConfigList.map((item) => ({ + Host: item.host, + Username: item.username, + Protocol: item.protocol, + 'API Version': item.apiVersion, + 'Strict SSL': item.strictSSL, + })) + setConfigItems(transformedItems) } }, [jiraConfigList]) return ( -
    -
    -

    Configuration Jira API

    -

    Current Jira API configuration settings

    -
    + + + +

    Configuration Jira API

    +

    Current Jira API configuration settings

    +
    + + handleClickAjouterConfiguration(event)} + > + Ajouter Configuration + + +
    + + + + + + +
    -
    + {/*
    Edit Configuration Test Connection -
    -
    +
    */} + ) } From baf77326653b9d698f60e5664af1d2d87a5f8b31 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 7 May 2025 14:56:08 +0100 Subject: [PATCH 25/72] feat: update ESLint configuration and dependencies for improved linting support --- .eslintrc.js | 18 +++++++++++++----- package.json | 7 ++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e9c55eec0..69faf7ea1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,16 @@ module.exports = { + env: { + browser: true, // Enables browser global variables + es2021: true, // Enables ES2021 global variables + node: true, // Enables Node.js global variables + jest: true, // Enables Jest global variables + }, + extends: [ + 'eslint:recommended', // Uses the recommended rules from ESLint + 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react + 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. + 'plugin:react-hooks/recommended', // Enforces the Rules of Hooks + ], // parser: '@typescript-eslint/parser', // Specifies the ESLint parser parserOptions: { ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features @@ -7,16 +19,12 @@ module.exports = { jsx: true, // Allows for the parsing of JSX }, }, + plugins: ['react', 'react-hooks'], settings: { react: { version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use }, }, - extends: [ - 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react - 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. - ], - plugins: ['react', 'react-hooks'], rules: { // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs // e.g. "@typescript-eslint/explicit-function-return-type": "off", diff --git a/package.json b/package.json index 84faa58bc..154ec7b53 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "author": "The CoreUI Team (https://github.com/orgs/coreui/people)", "scripts": { "build": "vite build", - "lint": "eslint \"src/**/*.js\"", + "lint": "eslint src/**/*.{js,jsx}", + "lint:fix": "eslint src/**/*.{js,jsx} --fix", "serve": "vite preview", "start": "vite" }, @@ -47,10 +48,10 @@ "devDependencies": { "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", - "eslint": "^8.57.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-react": "^7.36.1", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^4.6.2", "postcss": "^8.4.47", "prettier": "3.3.3", From 587170b99a18b925b365e85cca55f83785e75c83 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 7 May 2025 14:57:02 +0100 Subject: [PATCH 26/72] feat: implement CRUD operations for Jira configuration management and enhance modal functionality --- src/actions/jiraActions.js | 66 +++++- src/components/AppHeader.js | 15 +- src/components/Modal/ModalEditConfigJira.js | 216 ++++++++++++++++++ .../header/AppHeaderDropdownManager.js | 2 +- src/layout/DefaultLayout.js | 2 + src/reducers/jiraReducer.js | 45 ++++ src/services/jiraService.js | 42 ++++ src/views/forms/addNewConfigJira.js | 9 +- src/views/pages/jira/ConfigJiraApi.js | 93 +++++++- 9 files changed, 465 insertions(+), 25 deletions(-) create mode 100644 src/components/Modal/ModalEditConfigJira.js diff --git a/src/actions/jiraActions.js b/src/actions/jiraActions.js index 3cff91400..27a8ff8c1 100644 --- a/src/actions/jiraActions.js +++ b/src/actions/jiraActions.js @@ -1,4 +1,3 @@ -import React from 'react' import jiraService from '../services/jiraService' export const GetAllConfigJiraRequest = () => ({ @@ -41,6 +40,39 @@ export const ADD_NEW_CONFIG_JIRA_API_FAILURE = (error) => ({ payload: error, }) +export const DELETE_CONFIG_JIRA_API_REQUEST = () => ({ + type: 'DELETE_CONFIG_JIRA_API_REQUEST', +}) + +export const DELETE_CONFIG_JIRA_API_SUCCESS = () => ({ + type: 'DELETE_CONFIG_JIRA_API_SUCCESS', +}) + +export const DELETE_CONFIG_JIRA_API_FAILURE = (error) => ({ + type: 'DELETE_CONFIG_JIRA_API_FAILURE', + payload: error, +}) + +export const toggleEditConfigJiraModalOpen = (configId) => ({ + type: 'TOGGLE_EDIT_CONFIG_JIRA_MODAL_OPEN', + payload: configId, +}) + +export const toggleEditConfigJiraModalClose = () => ({ + type: 'TOGGLE_EDIT_CONFIG_JIRA_MODAL_CLOSE', +}) + +export const EDIT_CONFIG_JIRA_API_REQUEST = () => ({ + type: 'EDIT_CONFIG_JIRA_API_REQUEST', +}) +export const EDIT_CONFIG_JIRA_API_SUCCESS = () => ({ + type: 'EDIT_CONFIG_JIRA_API_SUCCESS', +}) +export const EDIT_CONFIG_JIRA_API_FAILURE = (error) => ({ + type: 'EDIT_CONFIG_JIRA_API_FAILURE', + payload: error, +}) + export const getAllConfigJiraAPI = () => (dispatch) => { dispatch(GetAllConfigJiraRequest()) return jiraService @@ -66,7 +98,6 @@ export const checkConnectionJiraAPI = return jiraService .checkConnectionJiraApi(protocol, host, username, password, apiVersion, strictSSL) .then((response) => { - console.log(response) dispatch(CHECK_CONNECTION_JIRA_API_SUCCESS()) return response }) @@ -91,3 +122,34 @@ export const addNewConfigJiraAPI = throw new Error(error) }) } + +export const deleteConfigJiraAPI = (id) => (dispatch) => { + dispatch(DELETE_CONFIG_JIRA_API_REQUEST()) + return jiraService + .deleteConfigJiraAPI(id) + .then((response) => { + console.log(response) + dispatch(DELETE_CONFIG_JIRA_API_SUCCESS()) + return response + }) + .catch((error) => { + dispatch(DELETE_CONFIG_JIRA_API_FAILURE(error)) + throw new Error(error) + }) +} + +export const editConfigJiraAPI = + (id, protocol, host, username, password, apiVersion, strictSSL) => (dispatch) => { + dispatch(EDIT_CONFIG_JIRA_API_REQUEST()) + return jiraService + .editConfigJiraAPI(id, protocol, host, username, password, apiVersion, strictSSL) + .then((response) => { + console.log(response) + dispatch(EDIT_CONFIG_JIRA_API_SUCCESS()) + return response + }) + .catch((error) => { + dispatch(EDIT_CONFIG_JIRA_API_FAILURE(error)) + throw new Error(error) + }) + } diff --git a/src/components/AppHeader.js b/src/components/AppHeader.js index 047033853..4609e25f8 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader.js @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react' -import { NavLink, useLocation } from 'react-router-dom' +import { NavLink } from 'react-router-dom' import { useSelector, useDispatch } from 'react-redux' import { CContainer, @@ -16,21 +16,12 @@ import { CButton, } from '@coreui/react' import CIcon from '@coreui/icons-react' -import { - cilBell, - cilContrast, - cilEnvelopeOpen, - cilList, - cilMenu, - cilMoon, - cilSun, -} from '@coreui/icons' +import { cilBell, cilEnvelopeOpen, cilList, cilMenu, cilMoon, cilSun } from '@coreui/icons' import { AppBreadcrumb } from './index' import { AppHeaderDropdown, AppHeaderDropdownManager } from './header/index' import { switchThemeMode, toggleCreateTicketModalOpen, toggleSideBar } from '../actions/appActions' const AppHeader = () => { - const location = useLocation() const headerRef = useRef() const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') const dispatch = useDispatch() @@ -50,7 +41,7 @@ const AppHeader = () => { useEffect(() => { setColorMode(theme) - }, [theme]) + }, [theme, setColorMode]) return ( diff --git a/src/components/Modal/ModalEditConfigJira.js b/src/components/Modal/ModalEditConfigJira.js new file mode 100644 index 000000000..8ff3e8602 --- /dev/null +++ b/src/components/Modal/ModalEditConfigJira.js @@ -0,0 +1,216 @@ +import React, { useState, useEffect } from 'react' +import { + CButton, + CModal, + CModalBody, + CModalFooter, + CModalHeader, + CForm, + CFormInput, + CFormCheck, + CCallout, +} from '@coreui/react' +import { + editConfigJiraAPI, + checkConnectionJiraAPI, + getAllConfigJiraAPI, + toggleEditConfigJiraModalClose, +} from '../../actions/jiraActions' +import { useDispatch, useSelector } from 'react-redux' +import { toast } from 'react-toastify' + +const ModalEditConfigJira = () => { + const dispatch = useDispatch() + const { configCanbeAdded, isEditConfigJiraModalOpen, configIdToEdit, jiraConfigList } = + useSelector((state) => state.jira) + + const [FormControlInputHostURL, setFormControlInputHostURL] = useState('') + const [RadioOptionProtocol, setRadioOptionProtocol] = useState('https') + const [FormControlInputUsername, setFormControlInputUsername] = useState('') + const [FormControlInputPassword, setFormControlInputPassword] = useState('') + const [FormControlInputAPIVersion, setFormControlInputAPIVersion] = useState(2) + const [CheckStrictSSL, setCheckStrictSSL] = useState(true) + + useEffect(() => { + if (configIdToEdit !== null) { + const configToEdit = jiraConfigList.find((config) => config.id === configIdToEdit) + if (configToEdit) { + setFormControlInputHostURL(configToEdit.host) + setRadioOptionProtocol(configToEdit.protocol) + setFormControlInputUsername(configToEdit.username) + setFormControlInputPassword(configToEdit.password) + setFormControlInputAPIVersion(configToEdit.apiVersion) + setCheckStrictSSL(configToEdit.strictSSL) + } + } + }, [configIdToEdit, jiraConfigList]) + + const checkConnection = (e) => { + e.preventDefault() + dispatch( + checkConnectionJiraAPI( + RadioOptionProtocol, + FormControlInputHostURL, + FormControlInputUsername, + FormControlInputPassword, + FormControlInputAPIVersion, + CheckStrictSSL, + ), + ) + .then((response) => { + if (response) { + if (response.data.error) { + toast.error('Connection failed') + } else { + toast.success('Connection successful') + } + } + }) + .catch((error) => { + console.error('Error checking connection:', error) + toast.error('Connection failed') + }) + } + + const handleFormSubmit = (e) => { + e.preventDefault() + if (configCanbeAdded) { + dispatch( + editConfigJiraAPI( + configIdToEdit, + RadioOptionProtocol, + FormControlInputHostURL, + FormControlInputUsername, + FormControlInputPassword, + FormControlInputAPIVersion, + CheckStrictSSL, + ), + ) + .then((response) => { + if (response) { + console.log(response) + if (response.data.error) { + toast.error('edited failed') + } else { + toast.success('successful edited') + dispatch(toggleEditConfigJiraModalClose()) + } + } + }) + .then(() => { + dispatch(getAllConfigJiraAPI()) + }) + .catch((error) => { + console.error('Error checking connection:', error) + toast.error('Connection failed') + }) + } else { + toast.error('please check the connection before adding a configuration') + } + } + return ( + dispatch(toggleEditConfigJiraModalClose())} + backdrop="static" + aria-labelledby="ScrollingLongContentExampleLabel LiveDemoExampleLabel" + scrollable + alignment="center" + > + dispatch(toggleEditConfigJiraModalClose())}> + Modifier configuration Jira + + + + setFormControlInputHostURL(e.target.value)} + value={FormControlInputHostURL} + /> + setRadioOptionProtocol(e.target.value)} + /> + setRadioOptionProtocol(e.target.value)} + /> +
    + setFormControlInputUsername(e.target.value)} + value={FormControlInputUsername} + /> + setFormControlInputPassword(e.target.value)} + value={FormControlInputPassword} + /> + setFormControlInputAPIVersion(e.target.value)} + value={FormControlInputAPIVersion} + /> + setCheckStrictSSL(e.target.checked)} + checked={CheckStrictSSL} + /> + + before editing a configuration, please make sure that the host url is reachable and the + username and password are correct.
    + Note: please check the Connection before editing a configuration. +
    +
    +
    + + checkConnection(e)}> + Test Connection + + handleFormSubmit(e)}> + Edit Configuration + + +
    + ) +} + +export default ModalEditConfigJira diff --git a/src/components/header/AppHeaderDropdownManager.js b/src/components/header/AppHeaderDropdownManager.js index 046a2b17b..9436d0be4 100644 --- a/src/components/header/AppHeaderDropdownManager.js +++ b/src/components/header/AppHeaderDropdownManager.js @@ -45,7 +45,7 @@ const AppHeaderDropdownManager = () => { {user.user.FirstName} {user.user.LastName}
    - + diff --git a/src/layout/DefaultLayout.js b/src/layout/DefaultLayout.js index e17b83b3a..e94820473 100644 --- a/src/layout/DefaultLayout.js +++ b/src/layout/DefaultLayout.js @@ -2,6 +2,7 @@ import React from 'react' import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index' import { useLocation } from 'react-router-dom' import ModalCreateTicket from '../components/Modal/ModalCreateTicket' +import ModalEditConfigJira from '../components/Modal/ModalEditConfigJira' const DefaultLayout = () => { const location = useLocation() @@ -14,6 +15,7 @@ const DefaultLayout = () => {
    +
    diff --git a/src/reducers/jiraReducer.js b/src/reducers/jiraReducer.js index bd66c443c..b1a67d7e6 100644 --- a/src/reducers/jiraReducer.js +++ b/src/reducers/jiraReducer.js @@ -3,6 +3,8 @@ const initialState = { jiraConfigList: [], loading: false, error: null, + isEditConfigJiraModalOpen: false, + configIdToEdit: null, } const jiraReducer = (state = initialState, action) => { @@ -58,6 +60,49 @@ const jiraReducer = (state = initialState, action) => { loading: false, error: action.payload, } + case 'DELETE_CONFIG_JIRA_API_REQUEST': + return { + ...state, + loading: true, + } + case 'DELETE_CONFIG_JIRA_API_SUCCESS': + return { + ...state, + loading: false, + } + case 'DELETE_CONFIG_JIRA_API_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'TOGGLE_EDIT_CONFIG_JIRA_MODAL_OPEN': + return { + ...state, + isEditConfigJiraModalOpen: true, + configIdToEdit: action.payload, + } + case 'TOGGLE_EDIT_CONFIG_JIRA_MODAL_CLOSE': + return { + ...state, + isEditConfigJiraModalOpen: false, + } + case 'EDIT_CONFIG_JIRA_API_REQUEST': + return { + ...state, + loading: true, + } + case 'EDIT_CONFIG_JIRA_API_SUCCESS': + return { + ...state, + loading: false, + } + case 'EDIT_CONFIG_JIRA_API_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } default: return state } diff --git a/src/services/jiraService.js b/src/services/jiraService.js index 81c99698b..ef3da048c 100644 --- a/src/services/jiraService.js +++ b/src/services/jiraService.js @@ -58,8 +58,50 @@ const addNewConfigJiraAPI = (protocol, host, username, password, apiVersion, str }) } +const deleteConfigJiraAPI = (idList) => { + return axios + .post( + `${API_URL}deleteConfigByID`, + { ids: idList }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }, + ) + .then((response) => { + return response + }) + .catch((error) => { + console.log('Error deleting config Api Jira:', error) + return error + }) +} + +const editConfigJiraAPI = (id, protocol, host, username, password, apiVersion, strictSSL) => { + return axios + .post( + `${API_URL}updateConfigByID`, + { id, protocol, host, username, password, apiVersion, strictSSL }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }, + ) + .then((response) => { + return response + }) + .catch((error) => { + console.log('Error editing config Api Jira:', error) + return error + }) +} + export default { getAllConfigJira, checkConnectionJiraApi, addNewConfigJiraAPI, + deleteConfigJiraAPI, + editConfigJiraAPI, } diff --git a/src/views/forms/addNewConfigJira.js b/src/views/forms/addNewConfigJira.js index f7819d028..69bdc0fe9 100644 --- a/src/views/forms/addNewConfigJira.js +++ b/src/views/forms/addNewConfigJira.js @@ -1,6 +1,10 @@ import React, { useState } from 'react' import { CButton, CCallout, CForm, CFormCheck, CFormInput } from '@coreui/react' -import { addNewConfigJiraAPI, checkConnectionJiraAPI, getAllConfigJiraAPI } from '../../actions/jiraActions' +import { + addNewConfigJiraAPI, + checkConnectionJiraAPI, + getAllConfigJiraAPI, +} from '../../actions/jiraActions' import { useDispatch, useSelector } from 'react-redux' import { toast } from 'react-toastify' @@ -13,7 +17,7 @@ const AddNewConfigJira = () => { const [FormControlInputUsername, setFormControlInputUsername] = useState('') const [FormControlInputPassword, setFormControlInputPassword] = useState('') const [FormControlInputAPIVersion, setFormControlInputAPIVersion] = useState(2) - const [CheckStrictSSL, setCheckStrictSSL] = useState(false) + const [CheckStrictSSL, setCheckStrictSSL] = useState(true) const checkConnection = () => { dispatch( @@ -145,7 +149,6 @@ const AddNewConfigJira = () => { setCheckStrictSSL(e.target.checked)} checked={CheckStrictSSL} diff --git a/src/views/pages/jira/ConfigJiraApi.js b/src/views/pages/jira/ConfigJiraApi.js index ac19aa083..ae8b722d3 100644 --- a/src/views/pages/jira/ConfigJiraApi.js +++ b/src/views/pages/jira/ConfigJiraApi.js @@ -1,5 +1,5 @@ -import React, { useEffect, useRef, useState } from 'react' -import { getAllConfigJiraAPI } from '../../../actions/jiraActions' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { deleteConfigJiraAPI, getAllConfigJiraAPI } from '../../../actions/jiraActions' import { useDispatch, useSelector } from 'react-redux' import { CTable, @@ -10,13 +10,18 @@ import { CCollapse, CCard, CCardBody, - CForm, - CFormInput, - CFormSelect, CFormCheck, + CButtonGroup, } from '@coreui/react' import AddNewConfigJira from '../../forms/addNewConfigJira' +import { toast } from 'react-toastify' +import { toggleEditConfigJiraModalOpen } from '../../../actions/jiraActions' const columns = [ + { + key: 'select', + label: 'Select', + _props: { scope: 'col' }, + }, { key: 'Host', label: 'Host', @@ -37,6 +42,11 @@ const columns = [ label: 'API Version', _props: { scope: 'col' }, }, + { + key: 'Actions', + label: 'actions', + _props: { scope: 'col' }, + }, // { // key: 'Strict SSL', // label: 'Strict SSL', @@ -51,7 +61,7 @@ const ConfigJiraApi = () => { const { jiraConfigList } = useSelector((state) => state.jira) - const [visible, setVisible] = useState(true) + const [visible, setVisible] = useState(false) const [configItems, setConfigItems] = useState([]) const handleClickAjouterConfiguration = (event) => { @@ -59,6 +69,46 @@ const ConfigJiraApi = () => { setVisible(!visible) } + const handleClickDeleteConfiguration = useCallback( + (event) => { + event.preventDefault() + const configId = event.target.id.split('-')[1] + // Call the delete action here + const deleteList = [] + deleteList.push(configId) + dispatch(deleteConfigJiraAPI(deleteList)) + .then((response) => { + if (response) { + console.log(response) + if (response.data.error) { + toast.error('delete failed') + } else { + toast.success('successful deleted') + } + } + }) + .then(() => { + dispatch(getAllConfigJiraAPI()) + }) + .catch((error) => { + console.error('Error checking connection:', error) + toast.error('Connection failed') + }) + console.log('Delete config with ID:', configId) + }, + [dispatch], + ) + + const handleClickEditConfiguration = useCallback( + (event) => { + event.preventDefault() + const configId = event.target.id.split('-')[1] + // Call the edit action here + dispatch(toggleEditConfigJiraModalOpen(configId)) + }, + [dispatch], + ) + useEffect(() => { if (isFirstRender.current) { dispatch(getAllConfigJiraAPI()) @@ -69,15 +119,44 @@ const ConfigJiraApi = () => { useEffect(() => { if (jiraConfigList && jiraConfigList.length > 0) { const transformedItems = jiraConfigList.map((item) => ({ + select: ( + + ), + id: item.id, Host: item.host, Username: item.username, Protocol: item.protocol, 'API Version': item.apiVersion, 'Strict SSL': item.strictSSL, + Actions: ( + + handleClickDeleteConfiguration(e)} + id={`delete-${item.id}`} + > + delete + + handleClickEditConfiguration(e)} + id={`edit-${item.id}`} + > + Edit + + + ), })) setConfigItems(transformedItems) } - }, [jiraConfigList]) + }, [jiraConfigList, handleClickDeleteConfiguration, handleClickEditConfiguration]) return ( From dca03ff3fd10ca11f8822a68e857d9d852d57e72 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 7 May 2025 16:15:04 +0100 Subject: [PATCH 27/72] feat: update ESLint rules to disable console usage and warn on unused variables --- .eslintrc.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 69faf7ea1..9d508f78c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,6 +26,8 @@ module.exports = { }, }, rules: { + console: 'off', // Disables the no-console rule + 'no-unused-vars': 'warn', // Warns about unused variables // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs // e.g. "@typescript-eslint/explicit-function-return-type": "off", }, From d270dc90b046b6d5169efee66c9abd9245cd5d9c Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 7 May 2025 16:15:45 +0100 Subject: [PATCH 28/72] feat: enhance Jira configuration management by adding enable/disable functionality and updating edit action --- src/actions/jiraActions.js | 13 +++- src/components/Modal/ModalEditConfigJira.js | 4 ++ src/services/jiraService.js | 13 +++- src/views/pages/jira/ConfigJiraApi.js | 79 +++++++++++++++++---- 4 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/actions/jiraActions.js b/src/actions/jiraActions.js index 27a8ff8c1..110fa2f97 100644 --- a/src/actions/jiraActions.js +++ b/src/actions/jiraActions.js @@ -139,10 +139,19 @@ export const deleteConfigJiraAPI = (id) => (dispatch) => { } export const editConfigJiraAPI = - (id, protocol, host, username, password, apiVersion, strictSSL) => (dispatch) => { + (id, protocol, host, username, password, apiVersion, strictSSL, enableConfig) => (dispatch) => { dispatch(EDIT_CONFIG_JIRA_API_REQUEST()) return jiraService - .editConfigJiraAPI(id, protocol, host, username, password, apiVersion, strictSSL) + .editConfigJiraAPI( + id, + protocol, + host, + username, + password, + apiVersion, + strictSSL, + enableConfig, + ) .then((response) => { console.log(response) dispatch(EDIT_CONFIG_JIRA_API_SUCCESS()) diff --git a/src/components/Modal/ModalEditConfigJira.js b/src/components/Modal/ModalEditConfigJira.js index 8ff3e8602..e2d27c64b 100644 --- a/src/components/Modal/ModalEditConfigJira.js +++ b/src/components/Modal/ModalEditConfigJira.js @@ -30,17 +30,20 @@ const ModalEditConfigJira = () => { const [FormControlInputPassword, setFormControlInputPassword] = useState('') const [FormControlInputAPIVersion, setFormControlInputAPIVersion] = useState(2) const [CheckStrictSSL, setCheckStrictSSL] = useState(true) + const [enableConfig, setEnableConfig] = useState(true) useEffect(() => { if (configIdToEdit !== null) { const configToEdit = jiraConfigList.find((config) => config.id === configIdToEdit) if (configToEdit) { + console.log(configToEdit) setFormControlInputHostURL(configToEdit.host) setRadioOptionProtocol(configToEdit.protocol) setFormControlInputUsername(configToEdit.username) setFormControlInputPassword(configToEdit.password) setFormControlInputAPIVersion(configToEdit.apiVersion) setCheckStrictSSL(configToEdit.strictSSL) + setEnableConfig(configToEdit.enableConfig) } } }, [configIdToEdit, jiraConfigList]) @@ -84,6 +87,7 @@ const ModalEditConfigJira = () => { FormControlInputPassword, FormControlInputAPIVersion, CheckStrictSSL, + enableConfig, ), ) .then((response) => { diff --git a/src/services/jiraService.js b/src/services/jiraService.js index ef3da048c..e87f0e2b7 100644 --- a/src/services/jiraService.js +++ b/src/services/jiraService.js @@ -78,11 +78,20 @@ const deleteConfigJiraAPI = (idList) => { }) } -const editConfigJiraAPI = (id, protocol, host, username, password, apiVersion, strictSSL) => { +const editConfigJiraAPI = ( + id, + protocol, + host, + username, + password, + apiVersion, + strictSSL, + enableConfig, +) => { return axios .post( `${API_URL}updateConfigByID`, - { id, protocol, host, username, password, apiVersion, strictSSL }, + { id, protocol, host, username, password, apiVersion, strictSSL, enableConfig }, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, diff --git a/src/views/pages/jira/ConfigJiraApi.js b/src/views/pages/jira/ConfigJiraApi.js index ae8b722d3..2ca951642 100644 --- a/src/views/pages/jira/ConfigJiraApi.js +++ b/src/views/pages/jira/ConfigJiraApi.js @@ -1,5 +1,9 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' -import { deleteConfigJiraAPI, getAllConfigJiraAPI } from '../../../actions/jiraActions' +import { + deleteConfigJiraAPI, + editConfigJiraAPI, + getAllConfigJiraAPI, +} from '../../../actions/jiraActions' import { useDispatch, useSelector } from 'react-redux' import { CTable, @@ -10,16 +14,16 @@ import { CCollapse, CCard, CCardBody, - CFormCheck, CButtonGroup, + CBadge, } from '@coreui/react' import AddNewConfigJira from '../../forms/addNewConfigJira' import { toast } from 'react-toastify' import { toggleEditConfigJiraModalOpen } from '../../../actions/jiraActions' const columns = [ { - key: 'select', - label: 'Select', + key: 'status', + label: 'Status', _props: { scope: 'col' }, }, { @@ -79,7 +83,6 @@ const ConfigJiraApi = () => { dispatch(deleteConfigJiraAPI(deleteList)) .then((response) => { if (response) { - console.log(response) if (response.data.error) { toast.error('delete failed') } else { @@ -94,11 +97,47 @@ const ConfigJiraApi = () => { console.error('Error checking connection:', error) toast.error('Connection failed') }) - console.log('Delete config with ID:', configId) }, [dispatch], ) + const handleChangeStatusConfiguration = useCallback( + (event) => { + event.preventDefault() + const configId = event.target.id.split('-')[1] + // Call the edit action here + const configToEdit = jiraConfigList.find((config) => config.id === configId) + dispatch( + editConfigJiraAPI( + configId, + configToEdit.protocol, + configToEdit.host, + configToEdit.username, + configToEdit.password, + configToEdit.apiVersion, + configToEdit.strictSSL, + !configToEdit.enableConfig, + ), + ) + .then((response) => { + if (response) { + if (response.data.error) { + toast.error('update failed') + } else { + toast.success('successful updated') + } + } + }) + .then(() => { + dispatch(getAllConfigJiraAPI()) + }) + .catch((error) => { + toast.error('Connection failed') + }) + }, + [dispatch, jiraConfigList], + ) + const handleClickEditConfiguration = useCallback( (event) => { event.preventDefault() @@ -119,15 +158,12 @@ const ConfigJiraApi = () => { useEffect(() => { if (jiraConfigList && jiraConfigList.length > 0) { const transformedItems = jiraConfigList.map((item) => ({ - select: ( - - ), id: item.id, + status: item.enableConfig ? ( + Enabled + ) : ( + Disabled + ), Host: item.host, Username: item.username, Protocol: item.protocol, @@ -151,12 +187,25 @@ const ConfigJiraApi = () => { > Edit + handleChangeStatusConfiguration(e)} + id={`status-${item.id}`} + > + {item.enableConfig ? 'Disable' : 'Enable'} + ), })) setConfigItems(transformedItems) } - }, [jiraConfigList, handleClickDeleteConfiguration, handleClickEditConfiguration]) + }, [ + jiraConfigList, + handleClickDeleteConfiguration, + handleClickEditConfiguration, + handleChangeStatusConfiguration, + ]) return ( From db8b36824eb74d9a94556b015d231730ac6041ac Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 7 May 2025 19:41:09 +0100 Subject: [PATCH 29/72] feat: update login actions and reducer to handle user roles on login success --- src/actions/authActions.js | 12 +++++++++--- src/reducers/authReducer.js | 4 +++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/actions/authActions.js b/src/actions/authActions.js index b53d1c2f7..ddbe8757b 100644 --- a/src/actions/authActions.js +++ b/src/actions/authActions.js @@ -5,9 +5,9 @@ export const loginRequest = () => ({ type: 'LOGIN_REQUEST', }) -export const loginSuccess = (user) => ({ +export const loginSuccess = (user, role) => ({ type: 'LOGIN_SUCCESS', - payload: user, + payload: { user, role }, }) export const loginFailure = (error) => ({ @@ -38,7 +38,13 @@ export const login = (username, password) => (dispatch) => { dispatch(loginFailure(response.error)) throw new Error(response.error) } else { - dispatch(loginSuccess(response)) + const user = response.user + if (user.IsEmployee && user.IsManager === false) { + dispatch(loginSuccess(response, 'employee')) + } + if (user.IsManager && user.IsEmployee === false) { + dispatch(loginSuccess(response, 'manager')) + } return response } }) diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js index 7126f1d75..6f1b114ce 100644 --- a/src/reducers/authReducer.js +++ b/src/reducers/authReducer.js @@ -3,6 +3,7 @@ const initialState = { user: null, loading: false, error: null, + role: null, } const authReducer = (state = initialState, action) => { @@ -39,7 +40,8 @@ const authReducer = (state = initialState, action) => { return { ...state, isAuthenticated: true, - user: action.payload, + user: action.payload.user, + role: action.payload.role, loading: false, } case 'LOGIN_FAILURE': From 0750d3ddef27aff72e61c9618152f08ebd087c17 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Thu, 8 May 2025 11:17:53 +0100 Subject: [PATCH 30/72] feat: add Projet page and update routing to include project list --- src/components/AppHeader.js | 2 +- src/routes.js | 8 ++++++++ src/views/pages/projet/Projet.js | 11 +++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/views/pages/projet/Projet.js diff --git a/src/components/AppHeader.js b/src/components/AppHeader.js index 4609e25f8..cd752c4c4 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader.js @@ -64,7 +64,7 @@ const AppHeader = () => { - + Projects diff --git a/src/routes.js b/src/routes.js index 6de66cd9c..634bbdc6c 100644 --- a/src/routes.js +++ b/src/routes.js @@ -56,12 +56,20 @@ const Widgets = React.lazy(() => import('./views/widgets/Widgets')) const ConfigJiraApi = React.lazy(() => import('./views/pages/jira/ConfigJiraApi')) +const Projet = React.lazy(() => import('./views/pages/projet/projet')) + const routes = [ { path: '/', exact: true, name: 'Home' }, { path: '/dashboard', name: 'Dashboard', element: Dashboard }, + { path: '/tickets', name: 'Tickets', element: Tickets }, + { path: '/jira', name: 'Jira', element: ConfigJiraApi, exact: true }, { path: '/jira/config-jira-api', name: 'Configuration Jira API', element: ConfigJiraApi }, + + { path: '/projet', element: Projet, name: 'Projet', exact: true }, + { path: '/projet/list', name: 'liste des projets', element: Projet }, + { path: '/theme', name: 'Theme', element: Colors, exact: true }, { path: '/theme/colors', name: 'Colors', element: Colors }, { path: '/theme/typography', name: 'Typography', element: Typography }, diff --git a/src/views/pages/projet/Projet.js b/src/views/pages/projet/Projet.js new file mode 100644 index 000000000..2b94179d2 --- /dev/null +++ b/src/views/pages/projet/Projet.js @@ -0,0 +1,11 @@ +import React from 'react' + +const Projet = () => { + return ( + <> +

    projet

    + + ) +} + +export default Projet From 92bdd901cc76bbc91db71f9b6989227894dd57b7 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Thu, 8 May 2025 14:32:53 +0100 Subject: [PATCH 31/72] feat: implement ticket management with CRUD operations and modal functionality --- src/actions/appActions.js | 8 ----- src/actions/ticketActions.js | 42 +++++++++++++++++++++++ src/components/AppHeader.js | 4 ++- src/components/Modal/ModalCreateTicket.js | 4 +-- src/reducers/appReducer.js | 11 ------ src/reducers/index.js | 2 ++ src/reducers/ticketReducer.js | 42 +++++++++++++++++++++++ src/services/ticketService.js | 23 +++++++++++++ src/store.js | 2 ++ src/views/pages/Tickets/TicketsHome.js | 14 +++++++- 10 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 src/actions/ticketActions.js create mode 100644 src/reducers/ticketReducer.js create mode 100644 src/services/ticketService.js diff --git a/src/actions/appActions.js b/src/actions/appActions.js index c5b4901f6..b9200f0b0 100644 --- a/src/actions/appActions.js +++ b/src/actions/appActions.js @@ -11,11 +11,3 @@ export const switchThemeMode = (theme) => ({ type: 'CHANGE_THEME', payload: theme }) - -export const toggleCreateTicketModalOpen = () => ({ - type: 'TOGGLE_CREATE_TICKET_MODAL_OPEN', -}) - -export const toggleCreateTicketModalClose = () => ({ - type: 'TOGGLE_CREATE_TICKET_MODAL_CLOSE', -}) diff --git a/src/actions/ticketActions.js b/src/actions/ticketActions.js new file mode 100644 index 000000000..17a74084c --- /dev/null +++ b/src/actions/ticketActions.js @@ -0,0 +1,42 @@ +import ticketService from '../services/ticketService' + +export const toggleCreateTicketModalOpen = () => ({ + type: 'TOGGLE_CREATE_TICKET_MODAL_OPEN', +}) + +export const toggleCreateTicketModalClose = () => ({ + type: 'TOGGLE_CREATE_TICKET_MODAL_CLOSE', +}) + +export const GET_ALL_TICKETS_REQUEST = () => ({ + type: 'GET_ALL_TICKETS_REQUEST', +}) + +export const GET_ALL_TICKETS_SUCCESS = (tickets) => ({ + type: 'GET_ALL_TICKETS_SUCCESS', + payload: tickets, +}) + +export const GET_ALL_TICKETS_FAILURE = (error) => ({ + type: 'GET_ALL_TICKETS_FAILURE', + payload: error, +}) + +export const getAllTicketAPI = () => (dispatch) => { + dispatch(GET_ALL_TICKETS_REQUEST()) + ticketService + .getAllTickets() + .then((response) => { + if (response.error) { + dispatch(GET_ALL_TICKETS_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(GET_ALL_TICKETS_SUCCESS(response.data.results)) + return response + } + }) + .catch((error) => { + dispatch(GET_ALL_TICKETS_FAILURE(error)) + throw new Error(error) + }) +} diff --git a/src/components/AppHeader.js b/src/components/AppHeader.js index cd752c4c4..f53d527ce 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader.js @@ -20,7 +20,9 @@ import { cilBell, cilEnvelopeOpen, cilList, cilMenu, cilMoon, cilSun } from '@co import { AppBreadcrumb } from './index' import { AppHeaderDropdown, AppHeaderDropdownManager } from './header/index' -import { switchThemeMode, toggleCreateTicketModalOpen, toggleSideBar } from '../actions/appActions' +import { switchThemeMode, toggleSideBar } from '../actions/appActions' +import { toggleCreateTicketModalOpen } from '../actions/ticketActions' + const AppHeader = () => { const headerRef = useRef() const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') diff --git a/src/components/Modal/ModalCreateTicket.js b/src/components/Modal/ModalCreateTicket.js index 7a0844614..d9b4130c3 100644 --- a/src/components/Modal/ModalCreateTicket.js +++ b/src/components/Modal/ModalCreateTicket.js @@ -1,10 +1,10 @@ import { CButton, CModal, CModalBody, CModalFooter, CModalHeader } from '@coreui/react' import React from 'react' -import { toggleCreateTicketModalClose } from '../../actions/appActions' +import { toggleCreateTicketModalClose } from '../../actions/ticketActions' import { useDispatch, useSelector } from 'react-redux' const ModalCreateTicket = () => { - const { isCreateTicketModalOpen } = useSelector((state) => state.data) + const { isCreateTicketModalOpen } = useSelector((state) => state.ticket) const dispatch = useDispatch() return ( { @@ -23,16 +22,6 @@ const appReducer = (state = initialState, action) => { ...state, theme: action.payload, } - case 'TOGGLE_CREATE_TICKET_MODAL_OPEN': - return { - ...state, - isCreateTicketModalOpen: true, - } - case 'TOGGLE_CREATE_TICKET_MODAL_CLOSE': - return { - ...state, - isCreateTicketModalOpen: false, - } default: return state } diff --git a/src/reducers/index.js b/src/reducers/index.js index 18a6c4e8f..67855ee80 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -3,11 +3,13 @@ import { combineReducers } from 'redux' import authReducer from './authReducer' import appReducer from './appReducer' import jiraReducer from './jiraReducer' +import ticketReducer from './ticketReducer' const rootReducer = combineReducers({ auth: authReducer, app: appReducer, jira: jiraReducer, + ticket: ticketReducer, }) export default rootReducer diff --git a/src/reducers/ticketReducer.js b/src/reducers/ticketReducer.js new file mode 100644 index 000000000..a55359019 --- /dev/null +++ b/src/reducers/ticketReducer.js @@ -0,0 +1,42 @@ +const initialState = { + ticketList: [], + loading: false, + error: null, + isCreateTicketModalOpen: false, +} + +const ticketReducer = (state = initialState, action) => { + switch (action.type) { + case 'TOGGLE_CREATE_TICKET_MODAL_OPEN': + return { + ...state, + isCreateTicketModalOpen: true, + } + case 'TOGGLE_CREATE_TICKET_MODAL_CLOSE': + return { + ...state, + isCreateTicketModalOpen: false, + } + case 'GET_ALL_TICKETS_REQUEST': + return { + ...state, + loading: true, + } + case 'GET_ALL_TICKETS_SUCCESS': + return { + ...state, + loading: false, + ticketList: action.payload, + } + case 'GET_ALL_TICKETS_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + default: + return state + } +} + +export default ticketReducer diff --git a/src/services/ticketService.js b/src/services/ticketService.js new file mode 100644 index 000000000..6a5cafbe7 --- /dev/null +++ b/src/services/ticketService.js @@ -0,0 +1,23 @@ +import axios from 'axios' + +const API_URL = 'http://localhost:8081/ticket/' + +const getAllTickets = () => { + return axios + .get(`${API_URL}getAllTicket`, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }) + .then((response) => { + return response + }) + .catch((error) => { + console.error('Error fetching all config Jira:', error) + return error + }) +} + +export default { + getAllTickets, +} diff --git a/src/store.js b/src/store.js index 245c065d2..63934175b 100644 --- a/src/store.js +++ b/src/store.js @@ -7,11 +7,13 @@ import PropTypes from 'prop-types' import authReducer from './reducers/authReducer' import dataReducer from './reducers/appReducer' import jiraReducer from './reducers/jiraReducer' +import ticketReducer from './reducers/ticketReducer' const rootReducer = combineReducers({ auth: authReducer, data: dataReducer, jira: jiraReducer, + ticket: ticketReducer, }) const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk))) diff --git a/src/views/pages/Tickets/TicketsHome.js b/src/views/pages/Tickets/TicketsHome.js index 24cd1a251..1955753f7 100644 --- a/src/views/pages/Tickets/TicketsHome.js +++ b/src/views/pages/Tickets/TicketsHome.js @@ -1,6 +1,18 @@ -import React from 'react' +import React, { useEffect, useRef } from 'react' +import { useDispatch } from 'react-redux' +import { getAllTicketAPI } from '../../../actions/ticketActions' const Tickets = () => { + const dispatch = useDispatch() + const isFirstRender = useRef(true) + + useEffect(() => { + if (isFirstRender.current) { + dispatch(getAllTicketAPI()) + isFirstRender.current = false + } + }, [dispatch]) + return ( <>

    Tickets

    From 5722ca937b2b05dce58cefbe34817d23a62ec3b4 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Thu, 8 May 2025 15:29:47 +0100 Subject: [PATCH 32/72] feat: enhance Tickets component to fetch and display ticket data in a table --- src/views/pages/Tickets/TicketsHome.js | 63 ++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/src/views/pages/Tickets/TicketsHome.js b/src/views/pages/Tickets/TicketsHome.js index 1955753f7..96bf8e03c 100644 --- a/src/views/pages/Tickets/TicketsHome.js +++ b/src/views/pages/Tickets/TicketsHome.js @@ -1,10 +1,41 @@ -import React, { useEffect, useRef } from 'react' -import { useDispatch } from 'react-redux' +import React, { useEffect, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import { getAllTicketAPI } from '../../../actions/ticketActions' +import { CCol, CContainer, CRow, CTable } from '@coreui/react' +import { elementType } from 'prop-types' +const columns = [ + { + key: 'key', + label: 'key', + _props: { scope: 'col' }, + }, + { + key: 'summary', + label: 'summary', + _props: { scope: 'col' }, + }, + // { + // key: 'Username', + // label: 'Username', + // _props: { scope: 'col' }, + // }, + // { + // key: 'Protocol', + // label: 'Protocol', + // _props: { scope: 'col' }, + // }, + // { + // key: 'API Version', + // label: 'API Version', + // _props: { scope: 'col' }, + // }, +] const Tickets = () => { const dispatch = useDispatch() const isFirstRender = useRef(true) + const { ticketList } = useSelector((state) => state.ticket) + const [ticketsItems, setTicketsItems] = useState([]) useEffect(() => { if (isFirstRender.current) { @@ -13,10 +44,34 @@ const Tickets = () => { } }, [dispatch]) + useEffect(() => { + if (ticketList) { + const list = [] + console.log(ticketList) + const tickets = ticketList.map((element) => element.tickets) + tickets.map((element) => { + element.map((el) => { + console.log('el', el) + list.push({ + key: el.key, + summary: el.fields.summary, + }) + }) + }) + setTicketsItems(list) + } + }, [ticketList]) + return ( - <> +

    Tickets

    - + + + + + 2 of 2 + +
    ) } From 842bc6516862832876a0dca801b08e943a03e09d Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 11 May 2025 13:49:34 +0100 Subject: [PATCH 33/72] feat: add Material-UI core and icons, and jsPDF libraries to dependencies --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 154ec7b53..26d741cc2 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@coreui/react": "^5.4.0", "@coreui/react-chartjs": "^3.0.0", "@coreui/utils": "^2.0.2", + "@material-ui/core": "^4.12.4", + "@material-ui/icons": "^4.11.3", "@popperjs/core": "^2.11.8", "axios": "^1.7.7", "bootstrap": "^5.3.3", @@ -34,6 +36,9 @@ "chart.js": "^4.4.4", "classnames": "^2.5.1", "core-js": "^3.38.1", + "jspdf": "^3.0.1", + "jspdf-autotable": "^5.0.2", + "material-table": "^1.63.0", "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", From 0bfaa614e6c58e8a7f569303f9cd99261148a212 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 11 May 2025 13:53:24 +0100 Subject: [PATCH 34/72] refactor: optimize AppContent component structure for better readability --- src/components/AppContent.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/AppContent.js b/src/components/AppContent.js index b9a39ef50..823dbff16 100644 --- a/src/components/AppContent.js +++ b/src/components/AppContent.js @@ -1,14 +1,14 @@ import React, { Suspense } from 'react' import { Navigate, Route, Routes } from 'react-router-dom' -import { CContainer, CSpinner } from '@coreui/react' +import CircularProgress from '@material-ui/core/CircularProgress' // routes config import routes from '../routes' const AppContent = () => { return ( - - }> +
    + }> {routes.map((route, idx) => { return ( @@ -26,7 +26,7 @@ const AppContent = () => { } /> - +
    ) } From 1233dd85f8d544d158f808d151c770a54d731bb9 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 11 May 2025 13:53:55 +0100 Subject: [PATCH 35/72] feat: add MaterialTableIcons component for table icon customization --- src/views/icons/MaterialTableIcons.js | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/views/icons/MaterialTableIcons.js diff --git a/src/views/icons/MaterialTableIcons.js b/src/views/icons/MaterialTableIcons.js new file mode 100644 index 000000000..ce369424c --- /dev/null +++ b/src/views/icons/MaterialTableIcons.js @@ -0,0 +1,40 @@ +/* eslint-disable react/display-name */ +import React, { forwardRef } from 'react' + +import AddBox from '@material-ui/icons/AddBox' +import ArrowDownward from '@material-ui/icons/ArrowDownward' +import Check from '@material-ui/icons/Check' +import ChevronLeft from '@material-ui/icons/ChevronLeft' +import ChevronRight from '@material-ui/icons/ChevronRight' +import Clear from '@material-ui/icons/Clear' +import DeleteOutline from '@material-ui/icons/DeleteOutline' +import Edit from '@material-ui/icons/Edit' +import FilterList from '@material-ui/icons/FilterList' +import FirstPage from '@material-ui/icons/FirstPage' +import LastPage from '@material-ui/icons/LastPage' +import Remove from '@material-ui/icons/Remove' +import SaveAlt from '@material-ui/icons/SaveAlt' +import Search from '@material-ui/icons/Search' +import ViewColumn from '@material-ui/icons/ViewColumn' + +const tableIcons = { + Add: forwardRef((props, ref) => ), + Check: forwardRef((props, ref) => ), + Clear: forwardRef((props, ref) => ), + Delete: forwardRef((props, ref) => ), + DetailPanel: forwardRef((props, ref) => ), + Edit: forwardRef((props, ref) => ), + Export: forwardRef((props, ref) => ), + Filter: forwardRef((props, ref) => ), + FirstPage: forwardRef((props, ref) => ), + LastPage: forwardRef((props, ref) => ), + NextPage: forwardRef((props, ref) => ), + PreviousPage: forwardRef((props, ref) => ), + ResetSearch: forwardRef((props, ref) => ), + Search: forwardRef((props, ref) => ), + SortArrow: forwardRef((props, ref) => ), + ThirdStateCheck: forwardRef((props, ref) => ), + ViewColumn: forwardRef((props, ref) => ), +} + +export default tableIcons From d081d335bf4bdac86e9cd7922d707bb6f78600eb Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 11 May 2025 13:54:20 +0100 Subject: [PATCH 36/72] feat: implement DetailPanelTableTicket component and integrate with MaterialTable in Tickets view --- .../DetailPanel/DetailPanelTableTicket.js | 164 ++++++++++++++++++ src/views/pages/Tickets/TicketsHome.js | 128 +++++++------- 2 files changed, 234 insertions(+), 58 deletions(-) create mode 100644 src/components/DetailPanel/DetailPanelTableTicket.js diff --git a/src/components/DetailPanel/DetailPanelTableTicket.js b/src/components/DetailPanel/DetailPanelTableTicket.js new file mode 100644 index 000000000..e7de24a52 --- /dev/null +++ b/src/components/DetailPanel/DetailPanelTableTicket.js @@ -0,0 +1,164 @@ +/* eslint-disable react/prop-types */ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Button, + ButtonGroup, + Card, + TextField, + Typography, +} from '@material-ui/core' +import { makeStyles } from '@material-ui/core/styles' + +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import ExpandMoreIcon from '@material-ui/icons/ExpandMore' +import { CCol, CRow } from '@coreui/react' + +const useStyles = makeStyles((theme) => ({ + root: { + width: '100%', + }, + heading: { + fontSize: theme.typography.pxToRem(15), + fontWeight: theme.typography.fontWeightRegular, + }, +})) + +const DetailPanelTableTicket = ({ rowData }) => { + const [summaryFocus, setSummaryFocus] = useState(false) + const [loading, setLoading] = useState(false) + + const handleUpdateTicket = (rowData) => { + console.log('update ticket', rowData) + setSummaryFocus(false) + setLoading(true) + } + + function jiraToHtml(text) { + // Échappement de base + text = text.replace(/&/g, '&').replace(//g, '>') + + // Couleur {color:#xxxxxx} + text = text.replace( + /\{color:(#[0-9a-fA-F]{6})\}([\s\S]*?)\{color\}/g, + (_, color, content) => `${content}`, + ) + + // Gras *texte* + text = text.replace(/\*(.*?)\*/g, '$1') + + // Italique _texte_ + text = text.replace(/_(.*?)_/g, '$1') + + // Souligné +texte+ + text = text.replace(/\+(.*?)\+/g, '$1') + + // Barré -texte- + text = text.replace(/-(.*?)-/g, '$1') + + // Monospace ~texte~ + text = text.replace(/~(.*?)~/g, '$1') + + // Exposant ^texte^ + text = text.replace(/\^(.*?)\^/g, '$1') + + // Lien [texte|url] + text = text.replace(/\[(.+?)\|(.+?)\]/g, `
    $1`) + + // Mention [~accountid:xxxxx] → nom générique + text = text.replace(/\[~accountid:[a-zA-Z0-9]*\]/g, `@utilisateur`) + + // Listes à puces * + text = text.replace(/(^|\n)\* (.*?)(?=\n|$)/g, '$1
    • $2
    ') + text = text.replace(/<\/ul>\s*
      /g, '') // merge lists + + // Listes numérotées # + text = text.replace(/(^|\n)# (.*?)(?=\n|$)/g, '$1
      1. $2
      ') + text = text.replace(/<\/ol>\s*
        /g, '') // merge lists + + // Tableaux + // En-tête ||header||header|| + text = text.replace(/\|\|(.+?)\|\|/g, (_, headers) => { + const cols = headers + .split('||') + .filter(Boolean) + .map((h) => `${h.trim()}`) + .join('') + return `${cols}` + }) + + // Lignes |col|col| + text = text.replace(/\n\|(.+?)\|/g, (_, row) => { + const cols = row + .split('|') + .map((c) => ``) + .join('') + return `${cols}` + }) + + // Fin de tableau + text = text.replace(/<\/tbody>(?!<\/table>)/g, '
        ${c.trim()}
        ') + + // Bloc {noformat} + text = text.replace(/\{noformat\}([\s\S]*?)\{noformat\}/g, '
        $1
        ') + + // Retour à la ligne double + text = text.replace(/\n{2,}/g, '

        ') + + return text + } + const classes = useStyles() + + return ( +
        +
        +
        + setSummaryFocus(true)} + onBlur={() => handleUpdateTicket()} + value={rowData.fields.summary} + InputProps={{ + disableUnderline: true, + style: { + border: 'none', + fontWeight: 'bold', + }, + }} + /> +
        + Description +
        +
        +
        +
        + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + Détails + + + + Personne assignée + {rowData.fields.assignee.displayName} + + + +
        +
        + + {/* {parseJiraMarkup(rowData.fields.description)} + */} +
        + ) +} + +export default DetailPanelTableTicket diff --git a/src/views/pages/Tickets/TicketsHome.js b/src/views/pages/Tickets/TicketsHome.js index 96bf8e03c..c2880b39f 100644 --- a/src/views/pages/Tickets/TicketsHome.js +++ b/src/views/pages/Tickets/TicketsHome.js @@ -1,41 +1,18 @@ -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' +import { CBadge, CContainer } from '@coreui/react' +import { Paper } from '@material-ui/core' +import MaterialTable from 'material-table' + import { getAllTicketAPI } from '../../../actions/ticketActions' -import { CCol, CContainer, CRow, CTable } from '@coreui/react' -import { elementType } from 'prop-types' -const columns = [ - { - key: 'key', - label: 'key', - _props: { scope: 'col' }, - }, - { - key: 'summary', - label: 'summary', - _props: { scope: 'col' }, - }, - // { - // key: 'Username', - // label: 'Username', - // _props: { scope: 'col' }, - // }, - // { - // key: 'Protocol', - // label: 'Protocol', - // _props: { scope: 'col' }, - // }, - // { - // key: 'API Version', - // label: 'API Version', - // _props: { scope: 'col' }, - // }, -] +import tableIcons from '../../icons/MaterialTableIcons' +import DetailPanelTableTicket from '../../../components/DetailPanel/DetailPanelTableTicket' + const Tickets = () => { const dispatch = useDispatch() const isFirstRender = useRef(true) const { ticketList } = useSelector((state) => state.ticket) - const [ticketsItems, setTicketsItems] = useState([]) useEffect(() => { if (isFirstRender.current) { @@ -44,34 +21,69 @@ const Tickets = () => { } }, [dispatch]) - useEffect(() => { - if (ticketList) { - const list = [] - console.log(ticketList) - const tickets = ticketList.map((element) => element.tickets) - tickets.map((element) => { - element.map((el) => { - console.log('el', el) - list.push({ - key: el.key, - summary: el.fields.summary, - }) - }) - }) - setTicketsItems(list) - } - }, [ticketList]) - return ( - -

        Tickets

        - - - - - 2 of 2 - -
        + <> + + rowData.configId ? ( + + externe + + ) : ( + + interne + + ), + }, + { + title: 'key', + field: 'key', + cellStyle: { width: '10%' }, + headerStyle: { width: '10%' }, + }, + { + title: 'summary', + field: 'fields.summary', + cellStyle: { width: '80%' }, + headerStyle: { width: '80%' }, + }, + { + title: 'status', + field: 'fields.status.name', + cellStyle: { width: '10%' }, + headerStyle: { width: '10%' }, + }, + ]} + data={ticketList} + components={{ + Container: (props) => ( + + ), + }} + detailPanel={[ + { + tooltip: 'Show more info', + render: (rowData) => , + }, + ]} + /> + ) } From f4f238e966bdf7eea0bb7256b2d47e82273d9619 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 11 May 2025 15:51:22 +0100 Subject: [PATCH 37/72] refactor: simplify DetailPanelTableTicket layout and improve code readability --- .../DetailPanel/DetailPanelTableTicket.js | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/DetailPanel/DetailPanelTableTicket.js b/src/components/DetailPanel/DetailPanelTableTicket.js index e7de24a52..844a813cd 100644 --- a/src/components/DetailPanel/DetailPanelTableTicket.js +++ b/src/components/DetailPanel/DetailPanelTableTicket.js @@ -3,19 +3,15 @@ import { Accordion, AccordionDetails, AccordionSummary, - Button, - ButtonGroup, - Card, + Grid, TextField, Typography, } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' -import React, { useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' +import React, { useState } from 'react' import ExpandMoreIcon from '@material-ui/icons/ExpandMore' -import { CCol, CRow } from '@coreui/react' const useStyles = makeStyles((theme) => ({ root: { @@ -23,7 +19,7 @@ const useStyles = makeStyles((theme) => ({ }, heading: { fontSize: theme.typography.pxToRem(15), - fontWeight: theme.typography.fontWeightRegular, + fontWeight: theme.typography.fontWeightBold, }, })) @@ -37,7 +33,7 @@ const DetailPanelTableTicket = ({ rowData }) => { setLoading(true) } - function jiraToHtml(text) { + const jiraToHtml = (text) => { // Échappement de base text = text.replace(/&/g, '&').replace(//g, '>') @@ -110,6 +106,7 @@ const DetailPanelTableTicket = ({ rowData }) => { return text } + const classes = useStyles() return ( @@ -146,17 +143,18 @@ const DetailPanelTableTicket = ({ rowData }) => { Détails - - Personne assignée - {rowData.fields.assignee.displayName} - + + +
        Personne assignée
        +
        + +
        {rowData.fields.assignee.displayName}
        +
        +
    - - {/* {parseJiraMarkup(rowData.fields.description)} - */}
    ) } From c157227df2dacae856e07133865cd6e6dab235c7 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 21 May 2025 10:10:11 +0100 Subject: [PATCH 38/72] refactor: enhance authentication check logic in App component --- src/App.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/App.js b/src/App.js index dd304e11a..b7549e768 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import React, { Suspense, useCallback, useEffect, useState } from 'react' +import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react' import { BrowserRouter, Route, Routes } from 'react-router-dom' import { useDispatch } from 'react-redux' import { CSpinner } from '@coreui/react' @@ -19,6 +19,8 @@ const App = () => { const dispatch = useDispatch() const [isChecking, setIsChecking] = useState(true) + const isFirstRender = useRef(true) + const checkAuth = useCallback(async () => { try { await dispatch(checkAuthentication()) @@ -30,7 +32,10 @@ const App = () => { }, [dispatch]) useEffect(() => { - checkAuth() + if (isFirstRender.current) { + checkAuth() + isFirstRender.current = false + } }, [checkAuth]) if (isChecking) { From 6a963867ad40aa29982afbd471fa036dc8a08ea6 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 21 May 2025 10:10:35 +0100 Subject: [PATCH 39/72] feat: enhance DetailPanelTableTicket component with conditional rendering and accordion state management --- .../DetailPanel/DetailPanelTableTicket.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/DetailPanel/DetailPanelTableTicket.js b/src/components/DetailPanel/DetailPanelTableTicket.js index 844a813cd..b7bbb1da3 100644 --- a/src/components/DetailPanel/DetailPanelTableTicket.js +++ b/src/components/DetailPanel/DetailPanelTableTicket.js @@ -26,6 +26,7 @@ const useStyles = makeStyles((theme) => ({ const DetailPanelTableTicket = ({ rowData }) => { const [summaryFocus, setSummaryFocus] = useState(false) const [loading, setLoading] = useState(false) + const [expanded, setExpanded] = useState(true) const handleUpdateTicket = (rowData) => { console.log('update ticket', rowData) @@ -130,11 +131,13 @@ const DetailPanelTableTicket = ({ rowData }) => { />
    Description -
    + {rowData.fields.description && ( +
    + )}
    - + setExpanded(!expanded)}> } aria-controls="panel1a-content" @@ -148,7 +151,11 @@ const DetailPanelTableTicket = ({ rowData }) => {
    Personne assignée
    -
    {rowData.fields.assignee.displayName}
    + {rowData.fields.assignee ? ( +
    {rowData.fields.assignee.displayName}
    + ) : ( +
    not assigned
    + )}
    From 41eef2bd5a362cf6e406b19463e1c42748c7fa37 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 21 May 2025 10:10:53 +0100 Subject: [PATCH 40/72] feat: implement user management actions, reducer, and service for fetching all users --- src/actions/userActions.js | 34 ++++++++++++++++++++++++++++++++++ src/reducers/userReducer.js | 31 +++++++++++++++++++++++++++++++ src/services/userService.js | 23 +++++++++++++++++++++++ src/store.js | 2 ++ 4 files changed, 90 insertions(+) create mode 100644 src/actions/userActions.js create mode 100644 src/reducers/userReducer.js create mode 100644 src/services/userService.js diff --git a/src/actions/userActions.js b/src/actions/userActions.js new file mode 100644 index 000000000..d492116dd --- /dev/null +++ b/src/actions/userActions.js @@ -0,0 +1,34 @@ +import ticketService from '../services/userService' + +export const GET_ALL_USERS_REQUEST = () => ({ + type: 'GET_ALL_USERS_REQUEST', +}) + +export const GET_ALL_USERS_SUCCESS = (users) => ({ + type: 'GET_ALL_USERS_SUCCESS', + payload: users, +}) + +export const GET_ALL_USERS_FAILURE = (error) => ({ + type: 'GET_ALL_USERS_FAILURE', + payload: error, +}) + +export const getAllUsersAPI = () => (dispatch) => { + dispatch(GET_ALL_USERS_REQUEST()) + ticketService + .getAllUsers() + .then((response) => { + if (response.error) { + dispatch(GET_ALL_USERS_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(GET_ALL_USERS_SUCCESS(response.data.results)) + return response + } + }) + .catch((error) => { + dispatch(GET_ALL_USERS_FAILURE(error)) + throw new Error(error) + }) +} diff --git a/src/reducers/userReducer.js b/src/reducers/userReducer.js new file mode 100644 index 000000000..3db17d963 --- /dev/null +++ b/src/reducers/userReducer.js @@ -0,0 +1,31 @@ +const initialState = { + usersList: [], + loading: false, + error: null, +} + +const userReducer = (state = initialState, action) => { + switch (action.type) { + case 'GET_ALL_USERS_REQUEST': + return { + ...state, + loading: true, + } + case 'GET_ALL_USERS_SUCCESS': + return { + ...state, + loading: false, + usersList: action.payload, + } + case 'GET_ALL_USERS_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + default: + return state + } +} + +export default userReducer diff --git a/src/services/userService.js b/src/services/userService.js new file mode 100644 index 000000000..4f1bd8342 --- /dev/null +++ b/src/services/userService.js @@ -0,0 +1,23 @@ +import axios from 'axios' + +const API_URL = 'http://localhost:8081/user/' + +const getAllUsers = () => { + return axios + .get(`${API_URL}getAllUsers`, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }) + .then((response) => { + return response + }) + .catch((error) => { + console.error('Error fetching all users:', error) + return error + }) +} + +export default { + getAllUsers, +} diff --git a/src/store.js b/src/store.js index 63934175b..28bf5b743 100644 --- a/src/store.js +++ b/src/store.js @@ -8,12 +8,14 @@ import authReducer from './reducers/authReducer' import dataReducer from './reducers/appReducer' import jiraReducer from './reducers/jiraReducer' import ticketReducer from './reducers/ticketReducer' +import userReducer from './reducers/userReducer' const rootReducer = combineReducers({ auth: authReducer, data: dataReducer, jira: jiraReducer, ticket: ticketReducer, + user: userReducer, }) const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk))) From 8c93b05894e6ae45853d76c66561b177e0230809 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 21 May 2025 10:45:19 +0100 Subject: [PATCH 41/72] refactor: replace CSpinner with CircularProgress for consistent loading indicators --- src/App.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/App.js b/src/App.js index b7549e768..445fbf33a 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,7 @@ import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react' import { BrowserRouter, Route, Routes } from 'react-router-dom' import { useDispatch } from 'react-redux' -import { CSpinner } from '@coreui/react' +import { CircularProgress } from '@mui/material' import './scss/style.scss' import PrivateRoute from './PrivateRute' import { checkAuthentication } from './actions/authActions' @@ -41,7 +41,7 @@ const App = () => { if (isChecking) { return (
    - +
    ) } @@ -51,7 +51,7 @@ const App = () => { - +
    } > From 8c3796b891e93ed34cbfd975674a90d376d8fd31 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 21 May 2025 12:47:05 +0100 Subject: [PATCH 42/72] feat: add Emotion and MUI dependencies for enhanced styling capabilities --- package.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 26d741cc2..2c7feca5f 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,13 @@ "@coreui/react": "^5.4.0", "@coreui/react-chartjs": "^3.0.0", "@coreui/utils": "^2.0.2", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", "@material-ui/core": "^4.12.4", "@material-ui/icons": "^4.11.3", + "@mui/icons-material": "^7.1.0", + "@mui/material": "^7.1.0", + "@mui/styled-engine-sc": "^7.1.0", "@popperjs/core": "^2.11.8", "axios": "^1.7.7", "bootstrap": "^5.3.3", @@ -48,7 +53,8 @@ "redux": "5.0.1", "redux-devtools-extension": "^2.13.9", "redux-thunk": "^3.1.0", - "simplebar-react": "^3.2.6" + "simplebar-react": "^3.2.6", + "styled-components": "^6.1.18" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.1", From 3585d4e772ffc6e8c10ba84c3ae6137f08fd45d4 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 21 May 2025 12:47:25 +0100 Subject: [PATCH 43/72] feat: create BugIssueForm component and integrate it into ModalCreateTicket for enhanced ticket creation --- .../Modal/ModalBody/BugIssueForm.js | 25 +++++ src/components/Modal/ModalCreateTicket.js | 102 ++++++++++++++---- src/reducers/ticketReducer.js | 2 +- src/utils/TicketsConsts.js | 60 +++++++++++ 4 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 src/components/Modal/ModalBody/BugIssueForm.js create mode 100644 src/utils/TicketsConsts.js diff --git a/src/components/Modal/ModalBody/BugIssueForm.js b/src/components/Modal/ModalBody/BugIssueForm.js new file mode 100644 index 000000000..1573737bf --- /dev/null +++ b/src/components/Modal/ModalBody/BugIssueForm.js @@ -0,0 +1,25 @@ +import React from 'react' + +import { TextField } from '@material-ui/core' +import { Grid, Typography } from '@mui/material' + +const BugIssueForm = () => { + return ( + + + Summary* + + + + + + Description* + + + + + + ) +} + +export default BugIssueForm diff --git a/src/components/Modal/ModalCreateTicket.js b/src/components/Modal/ModalCreateTicket.js index d9b4130c3..407da2e42 100644 --- a/src/components/Modal/ModalCreateTicket.js +++ b/src/components/Modal/ModalCreateTicket.js @@ -1,33 +1,93 @@ -import { CButton, CModal, CModalBody, CModalFooter, CModalHeader } from '@coreui/react' import React from 'react' -import { toggleCreateTicketModalClose } from '../../actions/ticketActions' import { useDispatch, useSelector } from 'react-redux' +import { MenuItem, TextField } from '@material-ui/core' +import { + Dialog, + Button, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Divider, + Grid, + Typography, +} from '@mui/material' +import { toggleCreateTicketModalClose } from '../../actions/ticketActions' +import BugIssueForm from './ModalBody/BugIssueForm' +import { projects, issueTypes } from '../../utils/TicketsConsts' const ModalCreateTicket = () => { const { isCreateTicketModalOpen } = useSelector((state) => state.ticket) + const [project, setProject] = React.useState(projects[0].value) + const [issueType, setIssueType] = React.useState(issueTypes[0].value) const dispatch = useDispatch() + const handleClose = () => { + dispatch(toggleCreateTicketModalClose()) + } return ( - dispatch(toggleCreateTicketModalClose())} - backdrop="static" - aria-labelledby="ScrollingLongContentExampleLabel LiveDemoExampleLabel" - scrollable - alignment="center" + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + maxWidth="md" + fullWidth > - dispatch(toggleCreateTicketModalClose())}> - Créer un nouveau ticket - - -

    Les champs obligatoires sont marqués d'un astérisque *

    -
    - - dispatch(toggleCreateTicketModalClose())}> - Fermer - - Sauvegarder - -
    + Créer un nouveau ticket + + + Les champs obligatoires sont marqués d'un astérisque * + + + + Projet* + + + setProject(event.target.value)} + helperText="Please select your project" + > + {projects.map((option) => ( + + {option.label} + + ))} + + + + + + Issue Type* + + + setIssueType(event.target.value)} + helperText="Please select the issue type" + > + {issueTypes.map((option) => ( + + {option.label} + + ))} + + + + + {issueType === 'Bug' && } + + + + + + ) } diff --git a/src/reducers/ticketReducer.js b/src/reducers/ticketReducer.js index a55359019..fb512d86e 100644 --- a/src/reducers/ticketReducer.js +++ b/src/reducers/ticketReducer.js @@ -2,7 +2,7 @@ const initialState = { ticketList: [], loading: false, error: null, - isCreateTicketModalOpen: false, + isCreateTicketModalOpen: true, } const ticketReducer = (state = initialState, action) => { diff --git a/src/utils/TicketsConsts.js b/src/utils/TicketsConsts.js new file mode 100644 index 000000000..49c7c0664 --- /dev/null +++ b/src/utils/TicketsConsts.js @@ -0,0 +1,60 @@ +export const projects = [ + { + value: 'interne', + label: 'Interne', + }, +] +export const issueTypes = [ + { + value: 'Bug', + label: 'Bug', + }, + { + value: 'Task', + label: 'Task', + }, + { + value: 'Story', + label: 'Story', + }, + { + value: 'Epic', + label: 'Epic', + }, + // { + // value: 'Sub-task', + // label: 'Sub-task', + // }, + // { + // value: 'Improvement', + // label: 'Improvement', + // }, + // { + // value: 'New Feature', + // label: 'New Feature', + // }, + // { + // value: 'Test', + // label: 'Test', + // }, + // { + // value: 'Documentation', + // label: 'Documentation', + // }, + // { + // value: 'Test Execution', + // label: 'Test Execution', + // }, + // { + // value: 'Pre-Condition', + // label: 'Pre-Condition', + // }, + // { + // value: 'Test Plan', + // label: 'Test Plan', + // }, + // { + // value: 'Incident', + // label: 'Incident', + // }, +] From 7f7fc8eecfd8814be2855c626912e242d6037f68 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 21 May 2025 17:22:32 +0100 Subject: [PATCH 44/72] feat: add new dependencies for date handling and rich text editing --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 2c7feca5f..0e68c2c0f 100644 --- a/package.json +++ b/package.json @@ -34,13 +34,16 @@ "@mui/icons-material": "^7.1.0", "@mui/material": "^7.1.0", "@mui/styled-engine-sc": "^7.1.0", + "@mui/x-date-pickers": "^8.3.1", "@popperjs/core": "^2.11.8", + "@tinymce/tinymce-react": "^6.1.0", "axios": "^1.7.7", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", "chart.js": "^4.4.4", "classnames": "^2.5.1", "core-js": "^3.38.1", + "date-fns": "^4.1.0", "jspdf": "^3.0.1", "jspdf-autotable": "^5.0.2", "material-table": "^1.63.0", From f66465cdc84ce3dd08972ee85c0336140aa26434 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 21 May 2025 17:22:43 +0100 Subject: [PATCH 45/72] feat: wrap ToastContainer with StyledEngineProvider for improved styling integration --- src/index.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/index.js b/src/index.js index 41ccaf58e..1d0b1b66f 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ import StoreProvider from './store' import App from './App' import { createRoot } from 'react-dom/client' import { ToastContainer } from 'react-toastify' +import { StyledEngineProvider } from '@mui/material/styles' const container = document.getElementById('root') const root = createRoot(container) @@ -11,15 +12,17 @@ const root = createRoot(container) root.render( - - + + + + , ) From 7a070a4e54a3c65f8893e9af56624200f4733f7a Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 21 May 2025 17:22:52 +0100 Subject: [PATCH 46/72] feat: enhance BugIssueForm with rich text editor, priority selection, and date pickers --- .../Modal/ModalBody/BugIssueForm.js | 64 ++++++++++++++++++- src/components/Modal/ModalCreateTicket.js | 19 ++++-- src/utils/TicketsConsts.js | 15 +++++ 3 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/components/Modal/ModalBody/BugIssueForm.js b/src/components/Modal/ModalBody/BugIssueForm.js index 1573737bf..294b19682 100644 --- a/src/components/Modal/ModalBody/BugIssueForm.js +++ b/src/components/Modal/ModalBody/BugIssueForm.js @@ -1,9 +1,19 @@ import React from 'react' -import { TextField } from '@material-ui/core' +import { MenuItem, TextField } from '@material-ui/core' import { Grid, Typography } from '@mui/material' +import { Editor } from '@tinymce/tinymce-react' +import { Prioritys } from '../../../utils/TicketsConsts' +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' +import { DatePicker } from '@mui/x-date-pickers/DatePicker' +import { DemoContainer } from '@mui/x-date-pickers/internals/demo' const BugIssueForm = () => { + const [Priority, setPriority] = React.useState(Prioritys[0].value) + const handleEditorChange = (content, editor) => { + console.log('Content:', content) + } return ( @@ -16,7 +26,57 @@ const BugIssueForm = () => { Description* - + + + + Priority + + + setPriority(event.target.value)} + helperText="Please select the issue Priority" + > + {Prioritys.map((option) => ( + + {option.label} + + ))} + + + + Date debut + + + + + + + + Date fin + + + + + ) diff --git a/src/components/Modal/ModalCreateTicket.js b/src/components/Modal/ModalCreateTicket.js index 407da2e42..df3033448 100644 --- a/src/components/Modal/ModalCreateTicket.js +++ b/src/components/Modal/ModalCreateTicket.js @@ -11,11 +11,15 @@ import { Divider, Grid, Typography, + Slide, } from '@mui/material' import { toggleCreateTicketModalClose } from '../../actions/ticketActions' import BugIssueForm from './ModalBody/BugIssueForm' import { projects, issueTypes } from '../../utils/TicketsConsts' +const Transition = React.forwardRef(function Transition(props, ref) { + return +}) const ModalCreateTicket = () => { const { isCreateTicketModalOpen } = useSelector((state) => state.ticket) const [project, setProject] = React.useState(projects[0].value) @@ -26,15 +30,22 @@ const ModalCreateTicket = () => { } return ( dispatch(toggleCreateTicketModalClose())} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" + // aria-labelledby="alert-dialog-title" + // aria-describedby="alert-dialog-description" + aria-labelledby="scroll-dialog-title" + aria-describedby="scroll-dialog-description" maxWidth="md" fullWidth > - Créer un nouveau ticket - + Créer un nouveau ticket + Les champs obligatoires sont marqués d'un astérisque * diff --git a/src/utils/TicketsConsts.js b/src/utils/TicketsConsts.js index 49c7c0664..2f5551b3f 100644 --- a/src/utils/TicketsConsts.js +++ b/src/utils/TicketsConsts.js @@ -58,3 +58,18 @@ export const issueTypes = [ // label: 'Incident', // }, ] + +export const Prioritys = [ + { + value: 'P1', + label: 'High', + }, + { + value: 'P2', + label: 'Medium', + }, + { + value: 'P3', + label: 'Low', + }, +] From 07b522fbb79d790e73352eaae8e1f4d36a68afc3 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Thu, 22 May 2025 11:36:24 +0100 Subject: [PATCH 47/72] feat: remove unused Page404, Page500, and Register components --- src/views/pages/page404/Page404.js | 41 ---------------- src/views/pages/page500/Page500.js | 41 ---------------- src/views/pages/register/Register.js | 71 ---------------------------- 3 files changed, 153 deletions(-) delete mode 100644 src/views/pages/page404/Page404.js delete mode 100644 src/views/pages/page500/Page500.js delete mode 100644 src/views/pages/register/Register.js diff --git a/src/views/pages/page404/Page404.js b/src/views/pages/page404/Page404.js deleted file mode 100644 index d7fe9a0a2..000000000 --- a/src/views/pages/page404/Page404.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react' -import { - CButton, - CCol, - CContainer, - CFormInput, - CInputGroup, - CInputGroupText, - CRow, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' -import { cilMagnifyingGlass } from '@coreui/icons' - -const Page404 = () => { - return ( -
    - - - -
    -

    404

    -

    Oops! You{"'"}re lost.

    -

    - The page you are looking for was not found. -

    -
    - - - - - - Search - -
    -
    -
    -
    - ) -} - -export default Page404 diff --git a/src/views/pages/page500/Page500.js b/src/views/pages/page500/Page500.js deleted file mode 100644 index ea11a0cb2..000000000 --- a/src/views/pages/page500/Page500.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react' -import { - CButton, - CCol, - CContainer, - CFormInput, - CInputGroup, - CInputGroupText, - CRow, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' -import { cilMagnifyingGlass } from '@coreui/icons' - -const Page500 = () => { - return ( -
    - - - - -

    500

    -

    Houston, we have a problem!

    -

    - The page you are looking for is temporarily unavailable. -

    -
    - - - - - - Search - -
    -
    -
    -
    - ) -} - -export default Page500 diff --git a/src/views/pages/register/Register.js b/src/views/pages/register/Register.js deleted file mode 100644 index d78b24c8f..000000000 --- a/src/views/pages/register/Register.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react' -import { - CButton, - CCard, - CCardBody, - CCol, - CContainer, - CForm, - CFormInput, - CInputGroup, - CInputGroupText, - CRow, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' -import { cilLockLocked, cilUser } from '@coreui/icons' - -const Register = () => { - return ( -
    - - - - - - -

    Register

    -

    Create your account

    - - - - - - - - @ - - - - - - - - - - - - - - -
    - Create Account -
    -
    -
    -
    -
    -
    -
    -
    - ) -} - -export default Register From c792d9e0eb229b1207da755be923393c0f2c5339 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Thu, 22 May 2025 11:36:59 +0100 Subject: [PATCH 48/72] feat: remove AppSidebar component and related references for a cleaner layout --- src/_nav.js | 37 ---------------------- src/actions/appActions.js | 4 --- src/components/AppHeader.js | 40 +----------------------- src/components/AppSidebar.js | 59 ------------------------------------ src/components/index.js | 2 -- src/layout/DefaultLayout.js | 5 +-- src/reducers/appReducer.js | 12 -------- 7 files changed, 2 insertions(+), 157 deletions(-) delete mode 100644 src/components/AppSidebar.js diff --git a/src/_nav.js b/src/_nav.js index db78c49bd..4a96965b5 100644 --- a/src/_nav.js +++ b/src/_nav.js @@ -268,43 +268,6 @@ const _nav = [ text: 'NEW', }, }, - { - component: CNavTitle, - name: 'Extras', - }, - { - component: CNavGroup, - name: 'Pages', - icon: , - items: [ - { - component: CNavItem, - name: 'Login', - to: '/login', - }, - { - component: CNavItem, - name: 'Register', - to: '/register', - }, - { - component: CNavItem, - name: 'Error 404', - to: '/404', - }, - { - component: CNavItem, - name: 'Error 500', - to: '/500', - }, - ], - }, - { - component: CNavItem, - name: 'Docs', - href: 'https://coreui.io/react/docs/templates/installation/', - icon: , - }, ] export default _nav diff --git a/src/actions/appActions.js b/src/actions/appActions.js index b9200f0b0..ee333eb36 100644 --- a/src/actions/appActions.js +++ b/src/actions/appActions.js @@ -1,8 +1,4 @@ /* eslint-disable prettier/prettier */ -export const toggleSideBar = () => ({ - type: 'TOGGLE_SIDEBAR', -}) - export const toggleUnfoldable = () => ({ type: 'TOGGLE_UNFOLDABLE', }) diff --git a/src/components/AppHeader.js b/src/components/AppHeader.js index f53d527ce..bd3231063 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader.js @@ -20,7 +20,7 @@ import { cilBell, cilEnvelopeOpen, cilList, cilMenu, cilMoon, cilSun } from '@co import { AppBreadcrumb } from './index' import { AppHeaderDropdown, AppHeaderDropdownManager } from './header/index' -import { switchThemeMode, toggleSideBar } from '../actions/appActions' +import { switchThemeMode } from '../actions/appActions' import { toggleCreateTicketModalOpen } from '../actions/ticketActions' const AppHeader = () => { @@ -48,12 +48,6 @@ const AppHeader = () => { return ( - dispatch(toggleSideBar())} - style={{ marginInlineStart: '-14px' }} - > - - @@ -101,38 +95,6 @@ const AppHeader = () => { -
  • -
    -
  • - - - {colorMode === 'dark' ? ( - - ) : ( - - )} - - - switchColorMode('light')} - > - Light - - switchColorMode('dark')} - > - Dark - - -
  • diff --git a/src/components/AppSidebar.js b/src/components/AppSidebar.js deleted file mode 100644 index 5e13bdaa6..000000000 --- a/src/components/AppSidebar.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react' -import { useSelector, useDispatch } from 'react-redux' - -import { - CCloseButton, - CSidebar, - CSidebarBrand, - CSidebarFooter, - CSidebarHeader, - CSidebarToggler, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' - -import { AppSidebarNav } from './AppSidebarNav' - -import { logo } from 'src/assets/brand/logo' -import { sygnet } from 'src/assets/brand/sygnet' - -import navigation from '../_nav' -import { toggleSideBar, toggleUnfoldable } from '../actions/appActions' - -const AppSidebar = () => { - const dispatch = useDispatch() - const { sidebarUnfoldable, sidebarShow } = useSelector((state) => state.data) - - const handleVisibleChange = (visible) => { - if (visible !== sidebarShow) { - dispatch(toggleSideBar()) - } - } - - const handleUnfoldableChange = () => { - dispatch(toggleUnfoldable()) - } - - return ( - - - - - - - - - - - - - ) -} - -export default React.memo(AppSidebar) diff --git a/src/components/index.js b/src/components/index.js index 6cdf33563..571e49a58 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -3,7 +3,6 @@ import AppContent from './AppContent' import AppFooter from './AppFooter' import AppHeader from './AppHeader' import AppHeaderDropdown from './header/AppHeaderDropdown' -import AppSidebar from './AppSidebar' import DocsCallout from './DocsCallout' import DocsLink from './DocsLink' import DocsExample from './DocsExample' @@ -14,7 +13,6 @@ export { AppFooter, AppHeader, AppHeaderDropdown, - AppSidebar, DocsCallout, DocsLink, DocsExample, diff --git a/src/layout/DefaultLayout.js b/src/layout/DefaultLayout.js index e94820473..d033b9cee 100644 --- a/src/layout/DefaultLayout.js +++ b/src/layout/DefaultLayout.js @@ -1,15 +1,12 @@ import React from 'react' -import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index' -import { useLocation } from 'react-router-dom' +import { AppContent, AppFooter, AppHeader } from '../components/index' import ModalCreateTicket from '../components/Modal/ModalCreateTicket' import ModalEditConfigJira from '../components/Modal/ModalEditConfigJira' const DefaultLayout = () => { - const location = useLocation() return (
    -
    diff --git a/src/reducers/appReducer.js b/src/reducers/appReducer.js index de7287a1a..f88185550 100644 --- a/src/reducers/appReducer.js +++ b/src/reducers/appReducer.js @@ -1,27 +1,15 @@ /* eslint-disable prettier/prettier */ const initialState = { - sidebarShow: false, sidebarUnfoldable: false, - theme: 'light', } const appReducer = (state = initialState, action) => { switch (action.type) { - case 'TOGGLE_SIDEBAR': - return { - ...state, - sidebarShow: !state.sidebarShow, - } case 'TOGGLE_UNFOLDABLE': return { ...state, sidebarUnfoldable: !state.sidebarUnfoldable, } - case 'CHANGE_THEME': - return { - ...state, - theme: action.payload, - } default: return state } From 20b87060f34b98df92349225535a3021a3df2cb1 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Thu, 22 May 2025 11:37:08 +0100 Subject: [PATCH 49/72] feat: remove unused Register, Page404, and Page500 components for a cleaner codebase --- src/App.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/App.js b/src/App.js index 445fbf33a..d289e2aa2 100644 --- a/src/App.js +++ b/src/App.js @@ -11,9 +11,6 @@ const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout')) // Pages const Login = React.lazy(() => import('./views/pages/login/Login')) -const Register = React.lazy(() => import('./views/pages/register/Register')) -const Page404 = React.lazy(() => import('./views/pages/page404/Page404')) -const Page500 = React.lazy(() => import('./views/pages/page500/Page500')) const App = () => { const dispatch = useDispatch() @@ -57,9 +54,6 @@ const App = () => { > } /> - } /> - } /> - } /> } exact> } /> } /> From ccb13cfe35188ad6dc1e61e53cfb6b5ae0e88ea2 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Thu, 22 May 2025 11:37:14 +0100 Subject: [PATCH 50/72] feat: integrate Toolpad for enhanced login functionality and add authProviders --- package.json | 1 + src/utils/authProviders.js | 4 + src/views/pages/login/Login.js | 156 ++++++++++++--------------------- 3 files changed, 59 insertions(+), 102 deletions(-) create mode 100644 src/utils/authProviders.js diff --git a/package.json b/package.json index 0e68c2c0f..efb54822b 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@mui/x-date-pickers": "^8.3.1", "@popperjs/core": "^2.11.8", "@tinymce/tinymce-react": "^6.1.0", + "@toolpad/core": "^0.15.0", "axios": "^1.7.7", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", diff --git a/src/utils/authProviders.js b/src/utils/authProviders.js new file mode 100644 index 000000000..99dd4f1fd --- /dev/null +++ b/src/utils/authProviders.js @@ -0,0 +1,4 @@ +export const providers = [ + { id: 'SSO', name: 'SSO' }, + { id: 'credentials', name: 'Email and Password' }, +] diff --git a/src/views/pages/login/Login.js b/src/views/pages/login/Login.js index cb712039e..b6882bb82 100644 --- a/src/views/pages/login/Login.js +++ b/src/views/pages/login/Login.js @@ -1,118 +1,70 @@ -import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { login, checkAuthentication } from '../../../actions/authActions' +import React from 'react' +import { AppProvider } from '@toolpad/core/AppProvider' +import { SignInPage } from '@toolpad/core/SignInPage' +import { useTheme } from '@mui/material/styles' import { useNavigate } from 'react-router-dom' +import { useDispatch } from 'react-redux' + +import { login, checkAuthentication } from '../../../actions/authActions' +import { providers } from '../../../utils/authProviders' import logo from '../../../assets/images/logo.png' -import { toast } from 'react-toastify' -import { - CButton, - CCard, - CCardBody, - CCardGroup, - CCol, - CContainer, - CForm, - CFormInput, - CImage, - CInputGroup, - CInputGroupText, - CRow, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' -import { cilLockLocked, cilUser } from '@coreui/icons' +const BRANDING = { + logo: Takeit logo, + title: 'Takeit', +} const Login = () => { - const [username, setUsername] = useState('') - const [password, setPassword] = useState('') + const theme = useTheme() const dispatch = useDispatch() const navigate = useNavigate() const redirect = (user) => { - if (user.IsEmployee) { - navigate('/') - } else if (user.IsManager) { + if (user.IsEmployee || user.IsManager) { navigate('/') } } - const handleLogin = (e) => { - e.preventDefault() - dispatch(login(username, password)) - .then((response) => { - console.log({ response }) - if (response.error) { - toast.error('Invalid username or password') - } - if (response) { - toast.success('Login successful') - redirect(response.user) - } - }) - .then(() => dispatch(checkAuthentication())) - .catch((error) => { - console.error('Login error:', error) - toast.error('Invalid username or password') - }) - } + return ( -
    - - - - - - - -

    Login

    -

    Sign In to your account

    - - - - - setUsername(e.target.value)} - /> - - - - - - setPassword(e.target.value)} - /> - - - - - Login - - - - - Forgot password? - - - -
    -
    -
    - - - - - -
    -
    -
    -
    -
    + + { + if (provider.id === 'credentials') { + try { + const email = formData.get('email') + const password = formData.get('password') + const loginResponse = await dispatch(login(email, password)) + + if (loginResponse.error) { + return { error: 'Invalid username or password' } + } + + await dispatch(checkAuthentication()) + + if (loginResponse && loginResponse.user) { + redirect(loginResponse.user) + return { success: true, user: loginResponse.user } + } + + return { error: 'Unexpected response format' } + } catch (error) { + console.error('Login error:', error) + return { error: 'Authentication failed' } + } + } + + return { error: "Cette fonctionnalité n'est pas disponible pour le moment." } + }} + slotProps={{ + form: { noValidate: false }, + emailField: { variant: 'standard', autoFocus: false }, + passwordField: { variant: 'standard' }, + submitButton: { variant: 'outlined' }, + oAuthButton: { variant: 'contained' }, + }} + providers={providers} + /> + ) } From d9d6c8d34398490d35c76dc0ecf3ae9510491c49 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sat, 31 May 2025 11:34:06 +0100 Subject: [PATCH 51/72] feat: add SVG icons for issue types (bug, epic, story, task) and priority levels (high, highest, low, lowest, medium) --- src/assets/images/issuetype/10303.svg | 15 +++++++++++++++ src/assets/images/issuetype/10307.svg | 17 +++++++++++++++++ src/assets/images/issuetype/10315.svg | 15 +++++++++++++++ src/assets/images/issuetype/10318.svg | 18 ++++++++++++++++++ src/assets/images/priorities/high.svg | 3 +++ src/assets/images/priorities/highest.svg | 4 ++++ src/assets/images/priorities/low.svg | 3 +++ src/assets/images/priorities/lowest.svg | 4 ++++ src/assets/images/priorities/medium.svg | 16 ++++++++++++++++ 9 files changed, 95 insertions(+) create mode 100644 src/assets/images/issuetype/10303.svg create mode 100644 src/assets/images/issuetype/10307.svg create mode 100644 src/assets/images/issuetype/10315.svg create mode 100644 src/assets/images/issuetype/10318.svg create mode 100644 src/assets/images/priorities/high.svg create mode 100644 src/assets/images/priorities/highest.svg create mode 100644 src/assets/images/priorities/low.svg create mode 100644 src/assets/images/priorities/lowest.svg create mode 100644 src/assets/images/priorities/medium.svg diff --git a/src/assets/images/issuetype/10303.svg b/src/assets/images/issuetype/10303.svg new file mode 100644 index 000000000..3b3ea8732 --- /dev/null +++ b/src/assets/images/issuetype/10303.svg @@ -0,0 +1,15 @@ + + + + bug + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/issuetype/10307.svg b/src/assets/images/issuetype/10307.svg new file mode 100644 index 000000000..8d3949b40 --- /dev/null +++ b/src/assets/images/issuetype/10307.svg @@ -0,0 +1,17 @@ + + + + epic + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/issuetype/10315.svg b/src/assets/images/issuetype/10315.svg new file mode 100644 index 000000000..c1516b2ae --- /dev/null +++ b/src/assets/images/issuetype/10315.svg @@ -0,0 +1,15 @@ + + + + story + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/issuetype/10318.svg b/src/assets/images/issuetype/10318.svg new file mode 100644 index 000000000..e2759677c --- /dev/null +++ b/src/assets/images/issuetype/10318.svg @@ -0,0 +1,18 @@ + + + + task + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/priorities/high.svg b/src/assets/images/priorities/high.svg new file mode 100644 index 000000000..3607057e0 --- /dev/null +++ b/src/assets/images/priorities/high.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/priorities/highest.svg b/src/assets/images/priorities/highest.svg new file mode 100644 index 000000000..dd3a70cbe --- /dev/null +++ b/src/assets/images/priorities/highest.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/images/priorities/low.svg b/src/assets/images/priorities/low.svg new file mode 100644 index 000000000..04260f447 --- /dev/null +++ b/src/assets/images/priorities/low.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/priorities/lowest.svg b/src/assets/images/priorities/lowest.svg new file mode 100644 index 000000000..275df1ba2 --- /dev/null +++ b/src/assets/images/priorities/lowest.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/images/priorities/medium.svg b/src/assets/images/priorities/medium.svg new file mode 100644 index 000000000..0b532817a --- /dev/null +++ b/src/assets/images/priorities/medium.svg @@ -0,0 +1,16 @@ + + + + +icon/16px/medium-priority +Created with Sketch. + + + + + + From a0df46a941b1ea9bf58834cce08d95e710a0f5c7 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sat, 31 May 2025 11:34:18 +0100 Subject: [PATCH 52/72] feat: refactor issue forms and add emptyIssue utility for ticket creation --- src/components/AppContent.js | 2 +- .../Modal/ModalBody/BugIssueForm.js | 1 - .../Modal/ModalBody/StoryIssueForm.js | 84 +++++++++ .../Modal/ModalBody/TaskIssueForm.js | 84 +++++++++ src/components/Modal/ModalCreateTicket.js | 132 +++++++++++++- src/utils/emptyIssue.js | 165 ++++++++++++++++++ 6 files changed, 460 insertions(+), 8 deletions(-) create mode 100644 src/components/Modal/ModalBody/StoryIssueForm.js create mode 100644 src/components/Modal/ModalBody/TaskIssueForm.js create mode 100644 src/utils/emptyIssue.js diff --git a/src/components/AppContent.js b/src/components/AppContent.js index 823dbff16..180b55b2a 100644 --- a/src/components/AppContent.js +++ b/src/components/AppContent.js @@ -1,6 +1,6 @@ import React, { Suspense } from 'react' import { Navigate, Route, Routes } from 'react-router-dom' -import CircularProgress from '@material-ui/core/CircularProgress' +import { CircularProgress } from '@mui/material' // routes config import routes from '../routes' diff --git a/src/components/Modal/ModalBody/BugIssueForm.js b/src/components/Modal/ModalBody/BugIssueForm.js index 294b19682..21e70c5e2 100644 --- a/src/components/Modal/ModalBody/BugIssueForm.js +++ b/src/components/Modal/ModalBody/BugIssueForm.js @@ -7,7 +7,6 @@ import { Prioritys } from '../../../utils/TicketsConsts' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' import { DatePicker } from '@mui/x-date-pickers/DatePicker' -import { DemoContainer } from '@mui/x-date-pickers/internals/demo' const BugIssueForm = () => { const [Priority, setPriority] = React.useState(Prioritys[0].value) diff --git a/src/components/Modal/ModalBody/StoryIssueForm.js b/src/components/Modal/ModalBody/StoryIssueForm.js new file mode 100644 index 000000000..fc3b889e7 --- /dev/null +++ b/src/components/Modal/ModalBody/StoryIssueForm.js @@ -0,0 +1,84 @@ +import React from 'react' + +import { MenuItem, TextField } from '@material-ui/core' +import { Grid, Typography } from '@mui/material' +import { Editor } from '@tinymce/tinymce-react' +import { Prioritys } from '../../../utils/TicketsConsts' +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' +import { DatePicker } from '@mui/x-date-pickers/DatePicker' + +const StoryIssueForm = () => { + const [Priority, setPriority] = React.useState(Prioritys[0].value) + const handleEditorChange = (content, editor) => { + console.log('Content:', content) + } + return ( + + + Summary* + + + + + + Description* + + + + + {/* + Priority + */} + {/* + setPriority(event.target.value)} + helperText="Please select the issue Priority" + > + {Prioritys.map((option) => ( + + {option.label} + + ))} + + */} + {/* + Date debut + + + + + + + + Date fin + + + + + + */} + + ) +} + +export default StoryIssueForm diff --git a/src/components/Modal/ModalBody/TaskIssueForm.js b/src/components/Modal/ModalBody/TaskIssueForm.js new file mode 100644 index 000000000..c8b30a6bc --- /dev/null +++ b/src/components/Modal/ModalBody/TaskIssueForm.js @@ -0,0 +1,84 @@ +import React from 'react' + +import { MenuItem, TextField } from '@material-ui/core' +import { Grid, Typography } from '@mui/material' +import { Editor } from '@tinymce/tinymce-react' +import { Prioritys } from '../../../utils/TicketsConsts' +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' +import { DatePicker } from '@mui/x-date-pickers/DatePicker' + +const TaskIssueForm = () => { + const [Priority, setPriority] = React.useState(Prioritys[0].value) + const handleEditorChange = (content, editor) => { + console.log('Content:', content) + } + return ( + + + Summary* + + + + + + Description* + + + + + {/* + Priority + */} + {/* + setPriority(event.target.value)} + helperText="Please select the issue Priority" + > + {Prioritys.map((option) => ( + + {option.label} + + ))} + + */} + + Date debut + + + + + + + + Date fin + + + + + + + + ) +} + +export default TaskIssueForm diff --git a/src/components/Modal/ModalCreateTicket.js b/src/components/Modal/ModalCreateTicket.js index df3033448..e0b591098 100644 --- a/src/components/Modal/ModalCreateTicket.js +++ b/src/components/Modal/ModalCreateTicket.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { MenuItem, TextField } from '@material-ui/core' import { @@ -15,19 +15,137 @@ import { } from '@mui/material' import { toggleCreateTicketModalClose } from '../../actions/ticketActions' import BugIssueForm from './ModalBody/BugIssueForm' +import TaskIssueForm from './ModalBody/TaskIssueForm' +import StoryIssueForm from './ModalBody/StoryIssueForm' import { projects, issueTypes } from '../../utils/TicketsConsts' +import { emptyIssue } from '../../utils/emptyIssue' const Transition = React.forwardRef(function Transition(props, ref) { return }) const ModalCreateTicket = () => { const { isCreateTicketModalOpen } = useSelector((state) => state.ticket) - const [project, setProject] = React.useState(projects[0].value) - const [issueType, setIssueType] = React.useState(issueTypes[0].value) + const [project, setProject] = useState(projects[0].value) + const [issueType, setIssueType] = useState(issueTypes[0].value) + const [newIssue, setNewIssue] = useState(emptyIssue) const dispatch = useDispatch() const handleClose = () => { dispatch(toggleCreateTicketModalClose()) } + + const generateId = () => { + let id = '' + for (let i = 0; i < 8; i++) { + id += Math.floor(Math.random() * 10) + } + return id + } + useEffect(() => { + const now = new Date().toISOString() + setNewIssue({ + ...emptyIssue, + id: generateId(), + fields: { + ...emptyIssue.fields, + created: now, + lastViewed: now, + updated: now, + statuscategorychangedate: now, + }, + }) + }, [isCreateTicketModalOpen]) + + useEffect(() => { + switch (issueType) { + case 'Bug': + setNewIssue({ + ...emptyIssue, + fields: { + ...emptyIssue.fields, + issuetype: { + id: '10002', + hierarchyLevel: 0, + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium', + avatarId: 10303, + subtask: false, + description: 'Un problème ou une erreur.', + entityId: 'b6942a7a-0278-49e3-89d3-85295176d3e8', + name: 'Bug', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10002', + }, + }, + }) + break + + case 'Task': + setNewIssue({ + ...emptyIssue, + fields: { + ...emptyIssue.fields, + issuetype: { + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10001', + name: 'Tâche', + description: 'Une tâche distincte.', + id: '10001', + entityId: 'ca1798d2-e4c6-4758-bfaf-7e38a85cd0ea', + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium', + avatarId: 10318, + subtask: false, + hierarchyLevel: 0, + }, + }, + }) + break + + case 'Story': + setNewIssue({ + ...emptyIssue, + fields: { + ...emptyIssue.fields, + issuetype: { + hierarchyLevel: 0, + subtask: false, + description: "Une fonctionnalité exprimée sous la forme d'un objectif utilisateur.", + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium', + avatarId: 10315, + entityId: '6b7e8da8-f84c-41ac-80ac-ba881764a634', + name: 'Story', + id: '10003', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10003', + }, + }, + }) + break + + case 'Epic': + setNewIssue({ + ...emptyIssue, + fields: { + ...emptyIssue.fields, + issuetype: { + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10004', + id: '10004', + description: 'Une collection de bugs, stories et tâches connexes.', + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10307?size=medium', + name: 'Epic', + subtask: false, + avatarId: 10307, + entityId: 'd1c66b55-cd69-4aba-b239-665a2e2f6af3', + hierarchyLevel: 1, + }, + }, + }) + break + + default: + break + } + }, [issueType]) + return ( { Créer un nouveau ticket - Les champs obligatoires sont marqués d'un astérisque * + Les champs obligatoires sont marqués d'un astérisque * @@ -64,7 +182,7 @@ const ModalCreateTicket = () => { > {projects.map((option) => ( - {option.label} + {option.label} ))} @@ -85,7 +203,7 @@ const ModalCreateTicket = () => { > {issueTypes.map((option) => ( - {option.label} + {option.label} ))} @@ -93,6 +211,8 @@ const ModalCreateTicket = () => { {issueType === 'Bug' && } + {issueType === 'Task' && } + {issueType === 'Story' && } diff --git a/src/utils/emptyIssue.js b/src/utils/emptyIssue.js new file mode 100644 index 000000000..2a241b576 --- /dev/null +++ b/src/utils/emptyIssue.js @@ -0,0 +1,165 @@ +export const emptyIssue = { + fields: { + components: [], + statuscategorychangedate: '', + workratio: -1, + assignee: null, + aggregatetimeoriginalestimate: null, + status: { + name: 'En cours', + description: 'Ce ticket est en cours de traitement par la personne assignée.', + statusCategory: { + id: 4, + name: 'En cours', + colorName: 'yellow', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/statuscategory/4', + key: 'indeterminate', + }, + id: '10001', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/status/10001', + iconUrl: 'https://sesame-team-pfe.atlassian.net/', + }, + aggregatetimespent: null, + watches: { + isWatching: false, + watchCount: 0, + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issue/SCRUM-2/watchers', + }, + reporter: { + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/user?accountId=62c81fef7273faf658f2068d', + emailAddress: 'mohamedamine.derouich@sesame.com.tn', + accountType: 'atlassian', + accountId: '62c81fef7273faf658f2068d', + avatarUrls: { + '32x32': + 'https://secure.gravatar.com/avatar/fba9f0dcfc7a2b63c55bfb9d22cde300?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMD-3.png', + '16x16': + 'https://secure.gravatar.com/avatar/fba9f0dcfc7a2b63c55bfb9d22cde300?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMD-3.png', + '24x24': + 'https://secure.gravatar.com/avatar/fba9f0dcfc7a2b63c55bfb9d22cde300?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMD-3.png', + '48x48': + 'https://secure.gravatar.com/avatar/fba9f0dcfc7a2b63c55bfb9d22cde300?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMD-3.png', + }, + timeZone: 'Etc/GMT-1', + active: true, + displayName: 'Mohamed Amine DEROUICH', + }, + timespent: null, + customfield_10001: null, + resolution: null, + labels: [], + progress: { + total: 0, + progress: 0, + }, + aggregateprogress: { + total: 0, + progress: 0, + }, + resolutiondate: null, + environment: null, + duedate: null, + votes: { + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issue/SCRUM-2/votes', + hasVoted: false, + votes: 0, + }, + customfield_10020: null, + security: null, + description: null, + customfield_10021: null, + issuelinks: [], + subtasks: [], + created: '', + lastViewed: '', + creator: { + displayName: 'Mohamed Amine DEROUICH', + timeZone: 'Etc/GMT-1', + accountType: 'atlassian', + active: true, + emailAddress: 'mohamedamine.derouich@sesame.com.tn', + avatarUrls: { + '24x24': + 'https://secure.gravatar.com/avatar/fba9f0dcfc7a2b63c55bfb9d22cde300?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMD-3.png', + '16x16': + 'https://secure.gravatar.com/avatar/fba9f0dcfc7a2b63c55bfb9d22cde300?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMD-3.png', + '32x32': + 'https://secure.gravatar.com/avatar/fba9f0dcfc7a2b63c55bfb9d22cde300?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMD-3.png', + '48x48': + 'https://secure.gravatar.com/avatar/fba9f0dcfc7a2b63c55bfb9d22cde300?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMD-3.png', + }, + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/user?accountId=62c81fef7273faf658f2068d', + accountId: '62c81fef7273faf658f2068d', + }, + timeoriginalestimate: null, + fixVersions: [], + summary: 'fix connection JIRA avec le backend pour recuperer la liste des tickets JIRA sallami', + updated: '', + issuetype: { + id: '10002', + hierarchyLevel: 0, + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium', + avatarId: 10303, + subtask: false, + description: 'Un problème ou une erreur.', + entityId: 'b6942a7a-0278-49e3-89d3-85295176d3e8', + name: 'Bug', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10002', + }, + aggregatetimeestimate: null, + customfield_10016: null, + customfield_10032: null, + customfield_10019: '0|i00007:', + project: { + name: 'backendTakeIT', + simplified: true, + id: '10000', + avatarUrls: { + '16x16': + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10412?size=xsmall', + '48x48': + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10412', + '24x24': + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10412?size=small', + '32x32': + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10412?size=medium', + }, + key: 'SCRUM', + projectTypeKey: 'software', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/project/10000', + }, + statusCategory: { + colorName: 'yellow', + id: 4, + key: 'indeterminate', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/statuscategory/4', + name: 'En cours', + }, + versions: [], + timeestimate: null, + priority: { + id: '3', + name: 'Medium', + iconUrl: 'https://sesame-team-pfe.atlassian.net/images/icons/priorities/medium.svg', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/priority/3', + }, + }, + id: '', + key: 'SCRUM-interne', + self: '', + // createdAt: { + // seconds: 1746709762, + // nanoseconds: 95000000, + // }, + configId: '', + // lastSync: { + // seconds: 1746801743, + // nanoseconds: 830000000, + // }, + // updatedAt: { + // seconds: 1746801743, + // nanoseconds: 830000000, + // }, + expand: 'operations,versionedRepresentations,editmeta,changelog,renderedFields', +} From 238b578fa94c70813a90b27ffcaa0ebcde4ba5dd Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 1 Jun 2025 16:44:40 +0100 Subject: [PATCH 53/72] feat: implement add new ticket functionality with API integration and update related components --- src/actions/ticketActions.js | 32 +++++++++++++++++ .../Modal/ModalBody/BugIssueForm.js | 31 +++++++++++++++-- src/components/Modal/ModalCreateTicket.js | 34 +++++++++++-------- src/reducers/ticketReducer.js | 16 +++++++++ src/services/ticketService.js | 21 ++++++++++++ src/utils/emptyIssue.js | 2 +- 6 files changed, 117 insertions(+), 19 deletions(-) diff --git a/src/actions/ticketActions.js b/src/actions/ticketActions.js index 17a74084c..27b460c67 100644 --- a/src/actions/ticketActions.js +++ b/src/actions/ticketActions.js @@ -22,6 +22,19 @@ export const GET_ALL_TICKETS_FAILURE = (error) => ({ payload: error, }) +export const ADD_NEW_TICKET_REQUEST = () => ({ + type: 'ADD_NEW_TICKET_REQUEST', +}) + +export const ADD_NEW_TICKET_SUCCESS = () => ({ + type: 'ADD_NEW_TICKET_SUCCESS', +}) + +export const ADD_NEW_TICKET_FAILURE = (error) => ({ + type: 'ADD_NEW_TICKET_FAILURE', + payload: error, +}) + export const getAllTicketAPI = () => (dispatch) => { dispatch(GET_ALL_TICKETS_REQUEST()) ticketService @@ -40,3 +53,22 @@ export const getAllTicketAPI = () => (dispatch) => { throw new Error(error) }) } + +export const addNewTicketAPI = (ticketData) => (dispatch) => { + dispatch(ADD_NEW_TICKET_REQUEST()) + return ticketService + .addNewTicket(ticketData) + .then((response) => { + if (response.error) { + dispatch(ADD_NEW_TICKET_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(ADD_NEW_TICKET_SUCCESS()) + return response + } + }) + .catch((error) => { + dispatch(ADD_NEW_TICKET_FAILURE(error)) + throw new Error(error) + }) +} diff --git a/src/components/Modal/ModalBody/BugIssueForm.js b/src/components/Modal/ModalBody/BugIssueForm.js index 21e70c5e2..e8c22a4ef 100644 --- a/src/components/Modal/ModalBody/BugIssueForm.js +++ b/src/components/Modal/ModalBody/BugIssueForm.js @@ -1,25 +1,45 @@ import React from 'react' +import PropTypes from 'prop-types' import { MenuItem, TextField } from '@material-ui/core' import { Grid, Typography } from '@mui/material' import { Editor } from '@tinymce/tinymce-react' -import { Prioritys } from '../../../utils/TicketsConsts' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' import { DatePicker } from '@mui/x-date-pickers/DatePicker' -const BugIssueForm = () => { +import { Prioritys } from '../../../utils/TicketsConsts' + +const BugIssueForm = ({ newIssue, setNewIssue }) => { const [Priority, setPriority] = React.useState(Prioritys[0].value) const handleEditorChange = (content, editor) => { console.log('Content:', content) } + + const handleChangeSummary = (event) => { + console.log(event.target.value) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + summary: event.target.value, + }, + }) + } + return ( Summary* - + handleChangeSummary(event)} + value={newIssue.fields.summary} + /> Description* @@ -81,4 +101,9 @@ const BugIssueForm = () => { ) } +BugIssueForm.propTypes = { + newIssue: PropTypes.object.isRequired, + setNewIssue: PropTypes.func.isRequired, +} + export default BugIssueForm diff --git a/src/components/Modal/ModalCreateTicket.js b/src/components/Modal/ModalCreateTicket.js index e0b591098..4cb2c9e10 100644 --- a/src/components/Modal/ModalCreateTicket.js +++ b/src/components/Modal/ModalCreateTicket.js @@ -13,7 +13,7 @@ import { Typography, Slide, } from '@mui/material' -import { toggleCreateTicketModalClose } from '../../actions/ticketActions' +import { addNewTicketAPI, toggleCreateTicketModalClose } from '../../actions/ticketActions' import BugIssueForm from './ModalBody/BugIssueForm' import TaskIssueForm from './ModalBody/TaskIssueForm' import StoryIssueForm from './ModalBody/StoryIssueForm' @@ -33,6 +33,10 @@ const ModalCreateTicket = () => { dispatch(toggleCreateTicketModalClose()) } + const handleSubmitTicket = () => { + dispatch(addNewTicketAPI(newIssue)) + } + const generateId = () => { let id = '' for (let i = 0; i < 8; i++) { @@ -43,10 +47,10 @@ const ModalCreateTicket = () => { useEffect(() => { const now = new Date().toISOString() setNewIssue({ - ...emptyIssue, + ...newIssue, id: generateId(), fields: { - ...emptyIssue.fields, + ...newIssue.fields, created: now, lastViewed: now, updated: now, @@ -59,9 +63,9 @@ const ModalCreateTicket = () => { switch (issueType) { case 'Bug': setNewIssue({ - ...emptyIssue, + ...newIssue, fields: { - ...emptyIssue.fields, + ...newIssue.fields, issuetype: { id: '10002', hierarchyLevel: 0, @@ -80,9 +84,9 @@ const ModalCreateTicket = () => { case 'Task': setNewIssue({ - ...emptyIssue, + ...newIssue, fields: { - ...emptyIssue.fields, + ...newIssue.fields, issuetype: { self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10001', name: 'Tâche', @@ -101,9 +105,9 @@ const ModalCreateTicket = () => { case 'Story': setNewIssue({ - ...emptyIssue, + ...newIssue, fields: { - ...emptyIssue.fields, + ...newIssue.fields, issuetype: { hierarchyLevel: 0, subtask: false, @@ -122,9 +126,9 @@ const ModalCreateTicket = () => { case 'Epic': setNewIssue({ - ...emptyIssue, + ...newIssue, fields: { - ...emptyIssue.fields, + ...newIssue.fields, issuetype: { self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10004', id: '10004', @@ -210,13 +214,13 @@ const ModalCreateTicket = () => { - {issueType === 'Bug' && } - {issueType === 'Task' && } - {issueType === 'Story' && } + {issueType === 'Bug' && } + {issueType === 'Task' && } + {issueType === 'Story' && } - + ) diff --git a/src/reducers/ticketReducer.js b/src/reducers/ticketReducer.js index fb512d86e..6cd0e1f59 100644 --- a/src/reducers/ticketReducer.js +++ b/src/reducers/ticketReducer.js @@ -34,6 +34,22 @@ const ticketReducer = (state = initialState, action) => { loading: false, error: action.payload, } + case 'ADD_NEW_TICKET_REQUEST': + return { + ...state, + loading: true, + } + case 'ADD_NEW_TICKET_SUCCESS': + return { + ...state, + loading: false, + } + case 'ADD_NEW_TICKET_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } default: return state } diff --git a/src/services/ticketService.js b/src/services/ticketService.js index 6a5cafbe7..8ab8e009a 100644 --- a/src/services/ticketService.js +++ b/src/services/ticketService.js @@ -18,6 +18,27 @@ const getAllTickets = () => { }) } +const addNewTicket = (ticketData) => { + return axios + .post( + `${API_URL}addNewTicket`, + { ticket: ticketData }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }, + ) + .then((response) => { + return response + }) + .catch((error) => { + console.error('Error fetching all config Jira:', error) + return error + }) +} + export default { getAllTickets, + addNewTicket, } diff --git a/src/utils/emptyIssue.js b/src/utils/emptyIssue.js index 2a241b576..b9a5e82e1 100644 --- a/src/utils/emptyIssue.js +++ b/src/utils/emptyIssue.js @@ -93,7 +93,7 @@ export const emptyIssue = { }, timeoriginalestimate: null, fixVersions: [], - summary: 'fix connection JIRA avec le backend pour recuperer la liste des tickets JIRA sallami', + summary: '', updated: '', issuetype: { id: '10002', From 99e296413e7b40293c4b508dcddb86a2dc230493 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sat, 14 Jun 2025 17:56:45 +0100 Subject: [PATCH 54/72] feat: implement project management features including CRUD operations and integrate project view --- package.json | 2 +- src/actions/projectActions.js | 100 +++++++++++++++++ src/reducers/projectReducer.js | 31 ++++++ src/reducers/ticketReducer.js | 2 +- src/routes.js | 4 +- src/services/projectService.js | 75 +++++++++++++ src/store.js | 2 + src/views/forms/addNewProject.js | 111 +++++++++++++++++++ src/views/pages/Tickets/TicketView.js | 11 ++ src/views/pages/Tickets/TicketsHome.js | 7 +- src/views/pages/projet/Projet.js | 143 ++++++++++++++++++++++++- 11 files changed, 479 insertions(+), 9 deletions(-) create mode 100644 src/actions/projectActions.js create mode 100644 src/reducers/projectReducer.js create mode 100644 src/services/projectService.js create mode 100644 src/views/forms/addNewProject.js create mode 100644 src/views/pages/Tickets/TicketView.js diff --git a/package.json b/package.json index efb54822b..398b4752b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@coreui/utils": "^2.0.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@material-table/core": "^6.4.4", "@material-ui/core": "^4.12.4", "@material-ui/icons": "^4.11.3", "@mui/icons-material": "^7.1.0", @@ -47,7 +48,6 @@ "date-fns": "^4.1.0", "jspdf": "^3.0.1", "jspdf-autotable": "^5.0.2", - "material-table": "^1.63.0", "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/actions/projectActions.js b/src/actions/projectActions.js new file mode 100644 index 000000000..ef7eb7732 --- /dev/null +++ b/src/actions/projectActions.js @@ -0,0 +1,100 @@ +import projectService from '../services/projectService' + +export const GET_ALL_PROJECTS_REQUEST = () => ({ + type: 'GET_ALL_PROJECTS_REQUEST', +}) + +export const GET_ALL_PROJECTS_SUCCESS = (projects) => ({ + type: 'GET_ALL_PROJECTS_SUCCESS', + payload: projects, +}) + +export const GET_ALL_PROJECTS_FAILURE = (error) => ({ + type: 'GET_ALL_PROJECTS_FAILURE', + payload: error, +}) + +export const getAllProjectAPI = () => (dispatch) => { + dispatch(GET_ALL_PROJECTS_REQUEST()) + projectService + .getAllProjects() + .then((response) => { + if (response.error) { + dispatch(GET_ALL_PROJECTS_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(GET_ALL_PROJECTS_SUCCESS(response.data.data)) + return response + } + }) + .catch((error) => { + dispatch(GET_ALL_PROJECTS_FAILURE(error)) + throw new Error(error) + }) +} + +export const ADD_NEW_PROJECT_REQUEST = () => ({ + type: 'ADD_NEW_PROJECT_REQUEST', +}) + +export const ADD_NEW_PROJECT_SUCCESS = (project) => ({ + type: 'ADD_NEW_PROJECT_SUCCESS', + payload: project, +}) + +export const ADD_NEW_PROJECT_FAILURE = (error) => ({ + type: 'ADD_NEW_PROJECT_FAILURE', + payload: error, +}) + +export const addNewProjectAPI = (projectData) => (dispatch) => { + dispatch(ADD_NEW_PROJECT_REQUEST()) + projectService + .addNewProject(projectData, dispatch) + .then((response) => { + if (response.error) { + dispatch(ADD_NEW_PROJECT_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(ADD_NEW_PROJECT_SUCCESS(response.data.data)) + return response + } + }) + .catch((error) => { + dispatch(ADD_NEW_PROJECT_FAILURE(error)) + throw new Error(error) + }) +} + +export const DELETE_PROJECT_REQUEST = () => ({ + type: 'DELETE_PROJECT_REQUEST', +}) + +export const DELETE_PROJECT_SUCCESS = (projectId) => ({ + type: 'DELETE_PROJECT_SUCCESS', + payload: projectId, +}) + +export const DELETE_PROJECT_FAILURE = (error) => ({ + type: 'DELETE_PROJECT_FAILURE', + payload: error, +}) + +export const deleteProjectAPI = (projectId) => (dispatch) => { + dispatch(DELETE_PROJECT_REQUEST()) + projectService + .deleteProject(projectId, dispatch) + .then((response) => { + if (response.error) { + dispatch(DELETE_PROJECT_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(DELETE_PROJECT_SUCCESS(projectId)) + return response + } + }) + .catch((error) => { + dispatch(DELETE_PROJECT_FAILURE(error)) + throw new Error(error) + }) +} diff --git a/src/reducers/projectReducer.js b/src/reducers/projectReducer.js new file mode 100644 index 000000000..e371ad5dd --- /dev/null +++ b/src/reducers/projectReducer.js @@ -0,0 +1,31 @@ +const initialState = { + projectList: [], + loading: false, + error: null, +} + +const projectReducer = (state = initialState, action) => { + switch (action.type) { + case 'GET_ALL_PROJECTS_REQUEST': + return { + ...state, + loading: true, + } + case 'GET_ALL_PROJECTS_SUCCESS': + return { + ...state, + loading: false, + projectList: action.payload, + } + case 'GET_ALL_PROJECTS_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + default: + return state + } +} + +export default projectReducer diff --git a/src/reducers/ticketReducer.js b/src/reducers/ticketReducer.js index 6cd0e1f59..c376ba575 100644 --- a/src/reducers/ticketReducer.js +++ b/src/reducers/ticketReducer.js @@ -2,7 +2,7 @@ const initialState = { ticketList: [], loading: false, error: null, - isCreateTicketModalOpen: true, + isCreateTicketModalOpen: false, } const ticketReducer = (state = initialState, action) => { diff --git a/src/routes.js b/src/routes.js index 634bbdc6c..4d9d17a72 100644 --- a/src/routes.js +++ b/src/routes.js @@ -3,6 +3,7 @@ import React from 'react' const Dashboard = React.lazy(() => import('./views/dashboard/Dashboard')) const Tickets = React.lazy(() => import('./views/pages/Tickets/TicketsHome')) +const TicketView = React.lazy(() => import('./views/pages/Tickets/TicketView')) const Colors = React.lazy(() => import('./views/theme/colors/Colors')) const Typography = React.lazy(() => import('./views/theme/typography/Typography')) @@ -56,13 +57,14 @@ const Widgets = React.lazy(() => import('./views/widgets/Widgets')) const ConfigJiraApi = React.lazy(() => import('./views/pages/jira/ConfigJiraApi')) -const Projet = React.lazy(() => import('./views/pages/projet/projet')) +const Projet = React.lazy(() => import('./views/pages/projet/Projet')) const routes = [ { path: '/', exact: true, name: 'Home' }, { path: '/dashboard', name: 'Dashboard', element: Dashboard }, { path: '/tickets', name: 'Tickets', element: Tickets }, + { path: '/ticket/:code', name: 'Ticket View', element: TicketView }, { path: '/jira', name: 'Jira', element: ConfigJiraApi, exact: true }, { path: '/jira/config-jira-api', name: 'Configuration Jira API', element: ConfigJiraApi }, diff --git a/src/services/projectService.js b/src/services/projectService.js new file mode 100644 index 000000000..00482ba7e --- /dev/null +++ b/src/services/projectService.js @@ -0,0 +1,75 @@ +import axios from 'axios' +import { toast } from 'react-toastify' +import { getAllProjectAPI } from '../actions/projectActions' + +const API_URL = 'http://localhost:8081/project/' + +const getAllProjects = async () => { + try { + const response = await axios.get(`${API_URL}getAllProject`, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }) + return response + } catch (error) { + console.error('Error fetching all config Jira:', error) + return error + } +} + +const addNewProject = async (projectData, dispatch) => { + try { + const response = await axios.post(`${API_URL}addNewProject`, projectData, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }) + if (response.status === 201 && !response.data.error) { + toast.success(response.data.message || 'Project added successfully') + if (dispatch) { + setTimeout(() => { + dispatch(getAllProjectAPI()) + }, 2000) + } + } + return response + } catch (error) { + console.error('Error adding new project:', error) + toast.error(error.response?.data?.message || 'Failed to add new project') + return error + } +} + +const deleteProject = async (projectId, dispatch) => { + try { + const response = await axios.post( + `${API_URL}deleteProjectByID`, + { ids: projectId }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }, + ) + if (response.status === 200 && !response.data.error) { + toast.success(response.message || 'Project deleted successfully') + if (dispatch) { + setTimeout(() => { + dispatch(getAllProjectAPI()) + }, 2000) + } + } + return response + } catch (error) { + console.error('Error deleting project:', error) + toast.error(error.response?.data?.message || 'Failed to delete project') + return error + } +} + +export default { + getAllProjects, + addNewProject, + deleteProject, +} diff --git a/src/store.js b/src/store.js index 28bf5b743..035aee198 100644 --- a/src/store.js +++ b/src/store.js @@ -9,6 +9,7 @@ import dataReducer from './reducers/appReducer' import jiraReducer from './reducers/jiraReducer' import ticketReducer from './reducers/ticketReducer' import userReducer from './reducers/userReducer' +import projectReducer from './reducers/projectReducer' const rootReducer = combineReducers({ auth: authReducer, @@ -16,6 +17,7 @@ const rootReducer = combineReducers({ jira: jiraReducer, ticket: ticketReducer, user: userReducer, + project: projectReducer, }) const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk))) diff --git a/src/views/forms/addNewProject.js b/src/views/forms/addNewProject.js new file mode 100644 index 000000000..7358a109c --- /dev/null +++ b/src/views/forms/addNewProject.js @@ -0,0 +1,111 @@ +import React, { useEffect, useState } from 'react' +import { CButton, CCallout, CCol, CForm, CFormInput, CFormSelect, CRow } from '@coreui/react' +import { useDispatch, useSelector } from 'react-redux' +import { toast } from 'react-toastify' +import { addNewProjectAPI } from '../../actions/projectActions' + +const AddNewProject = () => { + const { user } = useSelector((state) => state.auth.user) + const [projectName, setProjectName] = useState('') + const [key, setKey] = useState('') + const [projectType, setProjectType] = useState('') + const dispatch = useDispatch() + const handleFormSubmit = (e) => { + e.preventDefault() + if (!projectName || !key || !projectType) { + toast.error('Please fill in all required fields') + return + } + if (key.length < 3) { + toast.error('Key must be at least 3 characters long') + return + } + if (!/^[A-Z]+$/.test(key)) { + toast.error('Key must contain only uppercase letters') + return + } + dispatch( + addNewProjectAPI({ + projectName, + key, + projectType, + projectCategory: 'No category', + projectLead: user.uid, + }), + ) + } + + return ( + + + + setProjectName(e.target.value)} + value={projectName} + /> + + + setKey(e.target.value.toUpperCase())} + /> + + + + + e.target.value && setProjectType(e.target.value)} + value={projectType} + /> + + + + + + +
    +
    + handleFormSubmit(e)}> + Add new Project + +
    +
    + ) +} +export default AddNewProject diff --git a/src/views/pages/Tickets/TicketView.js b/src/views/pages/Tickets/TicketView.js new file mode 100644 index 000000000..bb1620dc0 --- /dev/null +++ b/src/views/pages/Tickets/TicketView.js @@ -0,0 +1,11 @@ +import React from 'react' + +const TicketView = () => { + return ( +
    +

    Ticket View

    +

    This is the ticket view page.

    +
    + ) +} +export default TicketView diff --git a/src/views/pages/Tickets/TicketsHome.js b/src/views/pages/Tickets/TicketsHome.js index c2880b39f..e7f2ddd74 100644 --- a/src/views/pages/Tickets/TicketsHome.js +++ b/src/views/pages/Tickets/TicketsHome.js @@ -1,9 +1,10 @@ import React, { useEffect, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' -import { CBadge, CContainer } from '@coreui/react' +import { CBadge } from '@coreui/react' import { Paper } from '@material-ui/core' -import MaterialTable from 'material-table' +import MaterialTable from '@material-table/core' import { getAllTicketAPI } from '../../../actions/ticketActions' import tableIcons from '../../icons/MaterialTableIcons' @@ -11,6 +12,7 @@ import DetailPanelTableTicket from '../../../components/DetailPanel/DetailPanelT const Tickets = () => { const dispatch = useDispatch() + const navigate = useNavigate() const isFirstRender = useRef(true) const { ticketList } = useSelector((state) => state.ticket) @@ -30,6 +32,7 @@ const Tickets = () => { options={{ paging: false, }} + onRowClick={(event, rowData) => navigate(`/ticket/${rowData.key}`)} columns={[ { title: 'From', diff --git a/src/views/pages/projet/Projet.js b/src/views/pages/projet/Projet.js index 2b94179d2..160ea133a 100644 --- a/src/views/pages/projet/Projet.js +++ b/src/views/pages/projet/Projet.js @@ -1,10 +1,145 @@ -import React from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { useDispatch } from 'react-redux' +import { useSelector } from 'react-redux' +import { + CButton, + CButtonGroup, + CCard, + CCardBody, + CCol, + CCollapse, + CContainer, + CRow, + CTable, +} from '@coreui/react' + +import { getAllProjectAPI, deleteProjectAPI } from '../../../actions/projectActions' +import AddNewProject from '../../forms/addNewProject' + +const columns = [ + { + key: 'projectName', + label: 'Project Name', + _props: { scope: 'col' }, + }, + { + key: 'key', + label: 'Key', + _props: { scope: 'col' }, + }, + { + key: 'projectType', + label: 'Project Type', + _props: { scope: 'col' }, + }, + { + key: 'projectLead', + label: 'Project Lead', + _props: { scope: 'col' }, + }, + { + key: 'projectCategory', + label: 'Project Category', + _props: { scope: 'col' }, + }, + { + key: 'actions', + label: 'Actions', + _props: { scope: 'col' }, + }, +] const Projet = () => { + const { projectList } = useSelector((state) => state.project) + const isFirstRender = useRef(true) + const [visible, setVisible] = useState(false) + const [projectItems, setProjectItems] = useState([]) + const dispatch = useDispatch() + + useEffect(() => { + if (isFirstRender.current) { + dispatch(getAllProjectAPI()) + isFirstRender.current = false + } + }, [dispatch]) + + const handleClickDelete = useCallback( + (event) => { + event.preventDefault() + const projectId = event.target.id.split('-')[1] + // Call the delete action here + const deleteList = [] + deleteList.push(projectId) + dispatch(deleteProjectAPI(deleteList)) + }, + [dispatch], + ) + + useEffect(() => { + if (projectList && projectList.length > 0) { + const transformedItems = projectList.map((item) => ({ + id: item.id, + projectName: item.projectName, + key: item.key, + projectType: item.projectType, + projectLead: item.projectLead, + projectCategory: item.projectCategory, + actions: ( + + handleClickDelete(e)} + id={`delete-${item.id}`} + > + delete + + handleClickEditConfiguration(e)} + id={`edit-${item.id}`} + > + Edit + + + ), + })) + setProjectItems(transformedItems) + } + }, [projectList, setProjectItems, handleClickDelete]) + + const handleClickAjouterProject = (event) => { + event.preventDefault() + setVisible(!visible) + } + return ( - <> -

    projet

    - + + + +

    All project types

    + {/*

    Current Jira API configuration settings

    */} +
    + + handleClickAjouterProject(event)} + > + Ajouter Project + + +
    + + + + + + + + +
    ) } From 81374eaa5c9cd8d92940437104e10b2023831722 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 15 Jun 2025 15:49:49 +0100 Subject: [PATCH 55/72] feat: add edit project functionality with modal integration and API handling --- src/actions/projectActions.js | 42 +++++++ src/components/Modal/ModalEditProject.js | 145 +++++++++++++++++++++++ src/layout/DefaultLayout.js | 3 +- src/reducers/projectReducer.js | 62 ++++++++++ src/services/projectService.js | 31 ++++- src/views/pages/projet/Projet.js | 21 +++- 6 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 src/components/Modal/ModalEditProject.js diff --git a/src/actions/projectActions.js b/src/actions/projectActions.js index ef7eb7732..aa1599a89 100644 --- a/src/actions/projectActions.js +++ b/src/actions/projectActions.js @@ -1,5 +1,14 @@ import projectService from '../services/projectService' +export const toggleEditProjectModalOpen = (projectId) => ({ + type: 'TOGGLE_EDIT_PROJECT_MODAL_OPEN', + payload: projectId, +}) + +export const toggleEditProjectModalClose = () => ({ + type: 'TOGGLE_EDIT_PROJECT_MODAL_CLOSE', +}) + export const GET_ALL_PROJECTS_REQUEST = () => ({ type: 'GET_ALL_PROJECTS_REQUEST', }) @@ -98,3 +107,36 @@ export const deleteProjectAPI = (projectId) => (dispatch) => { throw new Error(error) }) } + +export const EDIT_PROJECT_REQUEST = () => ({ + type: 'EDIT_PROJECT_REQUEST', +}) + +export const EDIT_PROJECT_SUCCESS = (projectId) => ({ + type: 'EDIT_PROJECT_SUCCESS', + payload: projectId, +}) + +export const EDIT_PROJECT_FAILURE = (error) => ({ + type: 'EDIT_PROJECT_FAILURE', + payload: error, +}) + +export const editProjectAPI = (projectId, projectData) => (dispatch) => { + dispatch(EDIT_PROJECT_REQUEST()) + projectService + .editProject(projectId, projectData, dispatch) + .then((response) => { + if (response.error) { + dispatch(EDIT_PROJECT_FAILURE(response.error)) + throw new Error(response.error) + } else { + dispatch(EDIT_PROJECT_SUCCESS(projectId)) + return response + } + }) + .catch((error) => { + dispatch(EDIT_PROJECT_FAILURE(error)) + throw new Error(error) + }) +} diff --git a/src/components/Modal/ModalEditProject.js b/src/components/Modal/ModalEditProject.js new file mode 100644 index 000000000..59bac1953 --- /dev/null +++ b/src/components/Modal/ModalEditProject.js @@ -0,0 +1,145 @@ +import { + CButton, + CCallout, + CCol, + CForm, + CFormInput, + CFormSelect, + CModal, + CModalBody, + CModalFooter, + CModalHeader, + CRow, +} from '@coreui/react' +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { toggleEditProjectModalClose, editProjectAPI } from '../../actions/projectActions' +import { toast } from 'react-toastify' + +const ModalEditProject = () => { + const { user } = useSelector((state) => state.auth.user) + + const dispatch = useDispatch() + const { projectIdToEdit, projectList, isEditProjectModalOpen } = useSelector( + (state) => state.project, + ) + + const [key, setKey] = useState('') + const [projectName, setProjectName] = useState('') + const [projectType, setProjectType] = useState('') + + useEffect(() => { + if (projectIdToEdit !== null) { + const projectToEdit = projectList.find((config) => config.id === projectIdToEdit) + if (projectToEdit) { + setKey(projectToEdit.key) + setProjectName(projectToEdit.projectName) + setProjectType(projectToEdit.projectType) + } + } + }, [projectIdToEdit, projectList]) + + const handleFormSubmit = (e) => { + e.preventDefault() + if (!projectName || !projectType) { + toast.error('Please fill in all required fields') + return + } + dispatch( + editProjectAPI(projectIdToEdit, { + projectName, + key, + projectType, + projectCategory: 'No category', + projectLead: user.uid, + }), + ) + } + return ( + dispatch(toggleEditProjectModalClose())} + backdrop="static" + aria-labelledby="ScrollingLongContentExampleLabel LiveDemoExampleLabel" + scrollable + alignment="center" + > + dispatch(toggleEditProjectModalClose())}> + Modifier configuration Jira + + + + + + setProjectName(e.target.value)} + value={projectName} + /> + + + + + + + + e.target.value && setProjectType(e.target.value)} + value={projectType} + /> + + + + + + + + + + handleFormSubmit(e)}> + Edit Project + + + + ) +} + +export default ModalEditProject diff --git a/src/layout/DefaultLayout.js b/src/layout/DefaultLayout.js index d033b9cee..8fc39096e 100644 --- a/src/layout/DefaultLayout.js +++ b/src/layout/DefaultLayout.js @@ -2,15 +2,16 @@ import React from 'react' import { AppContent, AppFooter, AppHeader } from '../components/index' import ModalCreateTicket from '../components/Modal/ModalCreateTicket' import ModalEditConfigJira from '../components/Modal/ModalEditConfigJira' +import ModalEditProject from '../components/Modal/ModalEditProject' const DefaultLayout = () => { - return (
    +
    diff --git a/src/reducers/projectReducer.js b/src/reducers/projectReducer.js index e371ad5dd..a76ba24c4 100644 --- a/src/reducers/projectReducer.js +++ b/src/reducers/projectReducer.js @@ -2,10 +2,24 @@ const initialState = { projectList: [], loading: false, error: null, + isEditProjectModalOpen: false, + projectIdToEdit: null, } const projectReducer = (state = initialState, action) => { switch (action.type) { + case 'TOGGLE_EDIT_PROJECT_MODAL_OPEN': + return { + ...state, + isEditProjectModalOpen: true, + projectIdToEdit: action.payload, + } + case 'TOGGLE_EDIT_PROJECT_MODAL_CLOSE': + return { + ...state, + isEditProjectModalOpen: false, + projectIdToEdit: null, + } case 'GET_ALL_PROJECTS_REQUEST': return { ...state, @@ -23,6 +37,54 @@ const projectReducer = (state = initialState, action) => { loading: false, error: action.payload, } + case 'ADD_NEW_PROJECT_REQUEST': + return { + ...state, + loading: true, + } + case 'ADD_NEW_PROJECT_SUCCESS': + return { + ...state, + loading: false, + } + case 'ADD_NEW_PROJECT_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'DELETE_PROJECT_REQUEST': + return { + ...state, + loading: true, + } + case 'DELETE_PROJECT_SUCCESS': + return { + ...state, + loading: false, + } + case 'DELETE_PROJECT_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } + case 'EDIT_PROJECT_REQUEST': + return { + ...state, + loading: true, + } + case 'EDIT_PROJECT_SUCCESS': + return { + ...state, + loading: false, + } + case 'EDIT_PROJECT_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + } default: return state } diff --git a/src/services/projectService.js b/src/services/projectService.js index 00482ba7e..3f532125c 100644 --- a/src/services/projectService.js +++ b/src/services/projectService.js @@ -30,7 +30,7 @@ const addNewProject = async (projectData, dispatch) => { if (dispatch) { setTimeout(() => { dispatch(getAllProjectAPI()) - }, 2000) + }, 1000) } } return response @@ -57,7 +57,7 @@ const deleteProject = async (projectId, dispatch) => { if (dispatch) { setTimeout(() => { dispatch(getAllProjectAPI()) - }, 2000) + }, 1000) } } return response @@ -68,8 +68,35 @@ const deleteProject = async (projectId, dispatch) => { } } +const editProject = async (projectId, projectData, dispatch) => { + try { + const response = await axios.post( + `${API_URL}editProject`, + { projectId, projectData }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }, + ) + if (response.status === 200 && !response.data.error) { + toast.success(response.data.message || 'Project edited successfully') + if (dispatch) { + setTimeout(() => { + dispatch(getAllProjectAPI()) + }, 1000) + } + } + return response + } catch (error) { + console.error('Error editing project:', error) + toast.error(error.response?.data?.message || 'Failed to edit project') + return error + } +} export default { getAllProjects, addNewProject, deleteProject, + editProject, } diff --git a/src/views/pages/projet/Projet.js b/src/views/pages/projet/Projet.js index 160ea133a..9a35523b7 100644 --- a/src/views/pages/projet/Projet.js +++ b/src/views/pages/projet/Projet.js @@ -1,6 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' -import { useDispatch } from 'react-redux' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { CButton, CButtonGroup, @@ -13,7 +12,11 @@ import { CTable, } from '@coreui/react' -import { getAllProjectAPI, deleteProjectAPI } from '../../../actions/projectActions' +import { + getAllProjectAPI, + deleteProjectAPI, + toggleEditProjectModalOpen, +} from '../../../actions/projectActions' import AddNewProject from '../../forms/addNewProject' const columns = [ @@ -67,7 +70,6 @@ const Projet = () => { (event) => { event.preventDefault() const projectId = event.target.id.split('-')[1] - // Call the delete action here const deleteList = [] deleteList.push(projectId) dispatch(deleteProjectAPI(deleteList)) @@ -75,6 +77,15 @@ const Projet = () => { [dispatch], ) + const handleClickEdit = useCallback( + (event) => { + event.preventDefault() + const projectId = event.target.id.split('-')[1] + dispatch(toggleEditProjectModalOpen(projectId)) + }, + [dispatch], + ) + useEffect(() => { if (projectList && projectList.length > 0) { const transformedItems = projectList.map((item) => ({ @@ -97,7 +108,7 @@ const Projet = () => { handleClickEditConfiguration(e)} + onClick={(e) => handleClickEdit(e)} id={`edit-${item.id}`} > Edit From 2a1be2cb3aa0b5a4a5466c1d6c0aaa7eefdc1f93 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 15 Jun 2025 15:50:58 +0100 Subject: [PATCH 56/72] fix: update editProject API endpoint to use correct URL for project updates --- src/services/projectService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/projectService.js b/src/services/projectService.js index 3f532125c..9fe8bc938 100644 --- a/src/services/projectService.js +++ b/src/services/projectService.js @@ -71,7 +71,7 @@ const deleteProject = async (projectId, dispatch) => { const editProject = async (projectId, projectData, dispatch) => { try { const response = await axios.post( - `${API_URL}editProject`, + `${API_URL}updateProjectByID`, { projectId, projectData }, { headers: { From d5205935dff8a876be7a660026e90ca74b350adf Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 15 Jun 2025 16:37:06 +0100 Subject: [PATCH 57/72] feat: close edit project modal after successful project update --- src/actions/projectActions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/actions/projectActions.js b/src/actions/projectActions.js index aa1599a89..bcf55d8c2 100644 --- a/src/actions/projectActions.js +++ b/src/actions/projectActions.js @@ -132,6 +132,7 @@ export const editProjectAPI = (projectId, projectData) => (dispatch) => { throw new Error(response.error) } else { dispatch(EDIT_PROJECT_SUCCESS(projectId)) + dispatch(toggleEditProjectModalClose()) return response } }) From 48573076f468dbc2c7a748da98b003af9e4305c2 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 15 Jun 2025 16:37:16 +0100 Subject: [PATCH 58/72] feat: close add new project modal after successfully updating project items --- src/views/pages/projet/Projet.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/views/pages/projet/Projet.js b/src/views/pages/projet/Projet.js index 9a35523b7..7fd37c917 100644 --- a/src/views/pages/projet/Projet.js +++ b/src/views/pages/projet/Projet.js @@ -117,8 +117,9 @@ const Projet = () => { ), })) setProjectItems(transformedItems) + setVisible(false) } - }, [projectList, setProjectItems, handleClickDelete]) + }, [projectList, setProjectItems, handleClickDelete, handleClickEdit]) const handleClickAjouterProject = (event) => { event.preventDefault() From 4274f1266bb689a19e5d1bb2d95a53409758f3b9 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 15 Jun 2025 16:37:27 +0100 Subject: [PATCH 59/72] fix: update tickets link to point to the correct tickets list route --- src/components/AppHeader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AppHeader.js b/src/components/AppHeader.js index bd3231063..6be404a0a 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader.js @@ -65,7 +65,7 @@ const AppHeader = () => { - + Tickets From 1ab76d58887076020b5f785390c123198b6d6ddd Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 15 Jun 2025 16:37:37 +0100 Subject: [PATCH 60/72] feat: add exact route for tickets and new route for tickets list --- src/routes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes.js b/src/routes.js index 4d9d17a72..cae6dd4a0 100644 --- a/src/routes.js +++ b/src/routes.js @@ -63,7 +63,8 @@ const routes = [ { path: '/', exact: true, name: 'Home' }, { path: '/dashboard', name: 'Dashboard', element: Dashboard }, - { path: '/tickets', name: 'Tickets', element: Tickets }, + { path: '/tickets', name: 'Tickets', element: Tickets, exact: true }, + { path: '/tickets/list', name: 'liste des Tickets', element: Tickets }, { path: '/ticket/:code', name: 'Ticket View', element: TicketView }, { path: '/jira', name: 'Jira', element: ConfigJiraApi, exact: true }, From 04975f105af1323bf630d09aaf383c3263cf5c80 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Sun, 15 Jun 2025 16:38:06 +0100 Subject: [PATCH 61/72] refactor: improve breadcrumb logic and simplify route matching --- src/components/AppBreadcrumb.js | 56 +++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/components/AppBreadcrumb.js b/src/components/AppBreadcrumb.js index d37de8ce9..3ab19aa21 100644 --- a/src/components/AppBreadcrumb.js +++ b/src/components/AppBreadcrumb.js @@ -1,31 +1,41 @@ import React from 'react' -import { useLocation } from 'react-router-dom' - +import { useLocation, matchPath } from 'react-router-dom' import routes from '../routes' - import { CBreadcrumb, CBreadcrumbItem } from '@coreui/react' const AppBreadcrumb = () => { - const currentLocation = useLocation().pathname + const location = useLocation() + const currentLocation = location.pathname const getRouteName = (pathname, routes) => { - const currentRoute = routes.find((route) => route.path === pathname) - return currentRoute ? currentRoute.name : false + for (const route of routes) { + const match = matchPath({ path: route.path, end: true }, pathname) + if (match) { + return route.name + } + } + return null } const getBreadcrumbs = (location) => { const breadcrumbs = [] - location.split('/').reduce((prev, curr, index, array) => { - const currentPathname = `${prev}/${curr}` - const routeName = getRouteName(currentPathname, routes) - routeName && + const pathnames = location.split('/').filter(Boolean) + + pathnames.reduce((prevPath, currSegment, index) => { + const currentPath = `${prevPath}/${currSegment}`.replace(/\/+/g, '/') + const routeName = getRouteName(currentPath, routes) + + if (routeName) { breadcrumbs.push({ - pathname: currentPathname, + pathname: currentPath, name: routeName, - active: index + 1 === array.length ? true : false, + active: index === pathnames.length - 1, }) - return currentPathname - }) + } + + return currentPath + }, '') + return breadcrumbs } @@ -34,16 +44,14 @@ const AppBreadcrumb = () => { return ( Home - {breadcrumbs.map((breadcrumb, index) => { - return ( - - {breadcrumb.name} - - ) - })} + {breadcrumbs.map((breadcrumb, index) => ( + + {breadcrumb.name} + + ))} ) } From a117cca3535effeb0f9efab50fe39e6467be2d8d Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Mon, 23 Jun 2025 11:13:39 +0100 Subject: [PATCH 62/72] fix: update footer links and copyright year in AppFooter component --- src/components/AppFooter.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/AppFooter.js b/src/components/AppFooter.js index fd126f460..590cd82be 100644 --- a/src/components/AppFooter.js +++ b/src/components/AppFooter.js @@ -5,15 +5,15 @@ const AppFooter = () => { return (
    - - CoreUI + + PFE - © 2024 creativeLabs. + © 2025
    From df83b66068ce7f55c0e40b41f090ddaf32619011 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Mon, 23 Jun 2025 11:13:57 +0100 Subject: [PATCH 63/72] fix: replace CircularProgress with CSpinner for consistent loading indicator --- src/App.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/App.js b/src/App.js index d289e2aa2..5bf5e5ccb 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,10 @@ import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react' import { BrowserRouter, Route, Routes } from 'react-router-dom' import { useDispatch } from 'react-redux' -import { CircularProgress } from '@mui/material' import './scss/style.scss' import PrivateRoute from './PrivateRute' import { checkAuthentication } from './actions/authActions' +import { CSpinner } from '@coreui/react' // Containers const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout')) @@ -38,7 +38,7 @@ const App = () => { if (isChecking) { return (
    - +
    ) } @@ -48,7 +48,7 @@ const App = () => { - +
    } > From f0599e823081aa619f668472bd977dae3307965b Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Mon, 23 Jun 2025 11:15:11 +0100 Subject: [PATCH 64/72] feat: add loading spinner to Projet component for better user experience --- src/views/pages/projet/Projet.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/views/pages/projet/Projet.js b/src/views/pages/projet/Projet.js index 7fd37c917..28c5fe579 100644 --- a/src/views/pages/projet/Projet.js +++ b/src/views/pages/projet/Projet.js @@ -10,6 +10,7 @@ import { CContainer, CRow, CTable, + CSpinner, } from '@coreui/react' import { @@ -53,7 +54,7 @@ const columns = [ ] const Projet = () => { - const { projectList } = useSelector((state) => state.project) + const { projectList, loading } = useSelector((state) => state.project) const isFirstRender = useRef(true) const [visible, setVisible] = useState(false) const [projectItems, setProjectItems] = useState([]) @@ -126,6 +127,14 @@ const Projet = () => { setVisible(!visible) } + if (loading) { + return ( +
    + +
    + ) + } + return ( From 573ec7a7113df929d8cdd5669985b4136904e64b Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Mon, 23 Jun 2025 11:15:25 +0100 Subject: [PATCH 65/72] feat: enhance TicketsHome component with loading state and improved layout --- src/views/pages/Tickets/TicketsHome.js | 145 +++++++++++++------------ 1 file changed, 76 insertions(+), 69 deletions(-) diff --git a/src/views/pages/Tickets/TicketsHome.js b/src/views/pages/Tickets/TicketsHome.js index e7f2ddd74..2e7eb40c3 100644 --- a/src/views/pages/Tickets/TicketsHome.js +++ b/src/views/pages/Tickets/TicketsHome.js @@ -1,20 +1,29 @@ -import React, { useEffect, useRef } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' -import { CBadge } from '@coreui/react' -import { Paper } from '@material-ui/core' -import MaterialTable from '@material-table/core' +import { + CBadge, + CButton, + CCol, + CContainer, + CRow, + CSpinner, + CTable, + CTableBody, + CTableDataCell, + CTableHead, + CTableHeaderCell, + CTableRow, +} from '@coreui/react' import { getAllTicketAPI } from '../../../actions/ticketActions' -import tableIcons from '../../icons/MaterialTableIcons' -import DetailPanelTableTicket from '../../../components/DetailPanel/DetailPanelTableTicket' const Tickets = () => { const dispatch = useDispatch() const navigate = useNavigate() const isFirstRender = useRef(true) - const { ticketList } = useSelector((state) => state.ticket) + const { ticketList, loading } = useSelector((state) => state.ticket) useEffect(() => { if (isFirstRender.current) { @@ -23,70 +32,68 @@ const Tickets = () => { } }, [dispatch]) + const handleClickAjouterTicket = (event) => { + event.preventDefault() + navigate('/ticket/add') + } + + const handleRowClick = (user) => { + console.log('Utilisateur cliqué :', user) + } + + if (loading) { + return ( +
    + +
    + ) + } return ( - <> - navigate(`/ticket/${rowData.key}`)} - columns={[ - { - title: 'From', - field: 'configId', - cellStyle: { width: '10%' }, - headerStyle: { width: '10%' }, - render: (rowData) => - rowData.configId ? ( - - externe - - ) : ( - - interne + + + +

    All Ticket View

    + {/*

    Current Jira API configuration settings

    */} +
    + + handleClickAjouterTicket(event)} + > + Ajouter Ticket + + +
    + + + + From + Key + Summary + Status + + + + {ticketList.map((item, index) => ( + handleRowClick(item)} + style={{ cursor: 'pointer' }} + > + + + {item.configId ? 'externe' : 'interne'} - ), - }, - { - title: 'key', - field: 'key', - cellStyle: { width: '10%' }, - headerStyle: { width: '10%' }, - }, - { - title: 'summary', - field: 'fields.summary', - cellStyle: { width: '80%' }, - headerStyle: { width: '80%' }, - }, - { - title: 'status', - field: 'fields.status.name', - cellStyle: { width: '10%' }, - headerStyle: { width: '10%' }, - }, - ]} - data={ticketList} - components={{ - Container: (props) => ( - - ), - }} - detailPanel={[ - { - tooltip: 'Show more info', - render: (rowData) => , - }, - ]} - /> - + + {item.key} + {item.fields.summary} + {item.fields.status.name} + + ))} + + +
    ) } From cb156062324689d773d1e66197fce20b6369c5fc Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 2 Jul 2025 14:33:31 +0100 Subject: [PATCH 66/72] feat: integrate toggleCreateTicketModalOpen action in Tickets component --- src/views/pages/Tickets/TicketsHome.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/pages/Tickets/TicketsHome.js b/src/views/pages/Tickets/TicketsHome.js index 2e7eb40c3..6b2e57247 100644 --- a/src/views/pages/Tickets/TicketsHome.js +++ b/src/views/pages/Tickets/TicketsHome.js @@ -17,7 +17,7 @@ import { CTableRow, } from '@coreui/react' -import { getAllTicketAPI } from '../../../actions/ticketActions' +import { getAllTicketAPI, toggleCreateTicketModalOpen } from '../../../actions/ticketActions' const Tickets = () => { const dispatch = useDispatch() @@ -34,7 +34,7 @@ const Tickets = () => { const handleClickAjouterTicket = (event) => { event.preventDefault() - navigate('/ticket/add') + dispatch(toggleCreateTicketModalOpen()) } const handleRowClick = (user) => { From 896a12c7b27325ef88e6cfbea3c37d43fb0dac87 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 2 Jul 2025 15:50:38 +0100 Subject: [PATCH 67/72] Refactor ticket creation modal and forms to use CoreUI components - Updated BugIssueForm, StoryIssueForm, and TaskIssueForm to replace Material-UI components with CoreUI components for consistency. - Enhanced state management for new issue fields including priority, dates, and descriptions. - Improved user experience by adding form validation and helper texts. - Refactored ModalCreateTicket to dynamically set project based on URL path and manage issue types more efficiently. --- .../Modal/ModalBody/BugIssueForm.js | 229 ++++++---- .../Modal/ModalBody/StoryIssueForm.js | 280 +++++++++---- .../Modal/ModalBody/TaskIssueForm.js | 303 ++++++++++---- src/components/Modal/ModalCreateTicket.js | 392 ++++++++++-------- 4 files changed, 815 insertions(+), 389 deletions(-) diff --git a/src/components/Modal/ModalBody/BugIssueForm.js b/src/components/Modal/ModalBody/BugIssueForm.js index e8c22a4ef..a1484d6b1 100644 --- a/src/components/Modal/ModalBody/BugIssueForm.js +++ b/src/components/Modal/ModalBody/BugIssueForm.js @@ -1,8 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' - -import { MenuItem, TextField } from '@material-ui/core' -import { Grid, Typography } from '@mui/material' +import { CCol, CFormInput, CFormLabel, CFormSelect, CRow } from '@coreui/react' import { Editor } from '@tinymce/tinymce-react' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' @@ -12,12 +10,20 @@ import { Prioritys } from '../../../utils/TicketsConsts' const BugIssueForm = ({ newIssue, setNewIssue }) => { const [Priority, setPriority] = React.useState(Prioritys[0].value) - const handleEditorChange = (content, editor) => { - console.log('Content:', content) + const [startDate, setStartDate] = React.useState(null) + const [endDate, setEndDate] = React.useState(null) + + const handleEditorChange = (content) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + description: content, + }, + }) } const handleChangeSummary = (event) => { - console.log(event.target.value) setNewIssue({ ...newIssue, fields: { @@ -27,77 +33,148 @@ const BugIssueForm = ({ newIssue, setNewIssue }) => { }) } + const handlePriorityChange = (event) => { + const selectedPriority = event.target.value + setPriority(selectedPriority) + + // Trouve l'objet priority complet basé sur la valeur sélectionnée + const priorityObj = Prioritys.find((p) => p.value === selectedPriority) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + priority: priorityObj, + }, + }) + } + + const handleStartDateChange = (date) => { + setStartDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10015: date ? date.toISOString() : null, // Date de début + }, + }) + } + + const handleEndDateChange = (date) => { + setEndDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10016: date ? date.toISOString() : null, // Date de fin + }, + }) + } + return ( - - - Summary* - - - handleChangeSummary(event)} - value={newIssue.fields.summary} - /> - - - Description* - - - - - - Priority - - - setPriority(event.target.value)} - helperText="Please select the issue Priority" - > - {Prioritys.map((option) => ( - - {option.label} - - ))} - - - - Date debut - - - - - - - - Date fin - - - - - - - +
    + + + Summary* + + + + + + + + + Description* + + + + + + + + + Priorité + + + + {Prioritys.map((option) => ( + + ))} + + + Veuillez sélectionner la priorité du bug + + + + + + + Date début + + + + + + + + + + + Date fin + + + + + + + +
    ) } diff --git a/src/components/Modal/ModalBody/StoryIssueForm.js b/src/components/Modal/ModalBody/StoryIssueForm.js index fc3b889e7..ff506e67e 100644 --- a/src/components/Modal/ModalBody/StoryIssueForm.js +++ b/src/components/Modal/ModalBody/StoryIssueForm.js @@ -1,84 +1,224 @@ import React from 'react' - -import { MenuItem, TextField } from '@material-ui/core' -import { Grid, Typography } from '@mui/material' +import PropTypes from 'prop-types' +import { CCol, CFormInput, CFormLabel, CFormSelect, CRow } from '@coreui/react' import { Editor } from '@tinymce/tinymce-react' import { Prioritys } from '../../../utils/TicketsConsts' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' import { DatePicker } from '@mui/x-date-pickers/DatePicker' -const StoryIssueForm = () => { +const StoryIssueForm = ({ newIssue, setNewIssue }) => { const [Priority, setPriority] = React.useState(Prioritys[0].value) - const handleEditorChange = (content, editor) => { - console.log('Content:', content) + const [startDate, setStartDate] = React.useState(null) + const [endDate, setEndDate] = React.useState(null) + const [storyPoints, setStoryPoints] = React.useState('') + + const handleEditorChange = (content) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + description: content, + }, + }) + } + + const handleChangeSummary = (event) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + summary: event.target.value, + }, + }) + } + + const handlePriorityChange = (event) => { + const selectedPriority = event.target.value + setPriority(selectedPriority) + + // Trouve l'objet priority complet basé sur la valeur sélectionnée + const priorityObj = Prioritys.find((p) => p.value === selectedPriority) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + priority: priorityObj, + }, + }) + } + + const handleStoryPointsChange = (event) => { + const value = event.target.value + setStoryPoints(value) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10028: value ? parseInt(value, 10) : null, // Story Points + }, + }) } + + const handleStartDateChange = (date) => { + setStartDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10015: date ? date.toISOString() : null, // Date de début + }, + }) + } + + const handleEndDateChange = (date) => { + setEndDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10016: date ? date.toISOString() : null, // Date de fin + }, + }) + } + return ( - - - Summary* - - - - - - Description* - - - - - {/* - Priority - */} - {/* - setPriority(event.target.value)} - helperText="Please select the issue Priority" - > - {Prioritys.map((option) => ( - - {option.label} - - ))} - - */} - {/* - Date debut - - - - - - - - Date fin - - - - - - */} - +
    + + + Summary* + + + + + + + + + Description* + + + + + + + + + Story Points + + + + + + + + + + + + + + Estimation de l'effort en points de story + + + + + + + Priorité + + + + {Prioritys.map((option) => ( + + ))} + + + Veuillez sélectionner la priorité de la story + + + + + + + Date début + + + + + + + + + + + Date fin + + + + + + + +
    ) } +StoryIssueForm.propTypes = { + newIssue: PropTypes.object.isRequired, + setNewIssue: PropTypes.func.isRequired, +} + export default StoryIssueForm diff --git a/src/components/Modal/ModalBody/TaskIssueForm.js b/src/components/Modal/ModalBody/TaskIssueForm.js index c8b30a6bc..6399cb46f 100644 --- a/src/components/Modal/ModalBody/TaskIssueForm.js +++ b/src/components/Modal/ModalBody/TaskIssueForm.js @@ -1,84 +1,247 @@ import React from 'react' - -import { MenuItem, TextField } from '@material-ui/core' -import { Grid, Typography } from '@mui/material' +import PropTypes from 'prop-types' +import { CCol, CFormInput, CFormLabel, CFormSelect, CRow } from '@coreui/react' import { Editor } from '@tinymce/tinymce-react' import { Prioritys } from '../../../utils/TicketsConsts' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' import { DatePicker } from '@mui/x-date-pickers/DatePicker' -const TaskIssueForm = () => { +const TaskIssueForm = ({ newIssue, setNewIssue }) => { const [Priority, setPriority] = React.useState(Prioritys[0].value) - const handleEditorChange = (content, editor) => { - console.log('Content:', content) + const [startDate, setStartDate] = React.useState(null) + const [endDate, setEndDate] = React.useState(null) + const [assignee, setAssignee] = React.useState('') + const [estimation, setEstimation] = React.useState('') + + const handleEditorChange = (content) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + description: content, + }, + }) + } + + const handleChangeSummary = (event) => { + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + summary: event.target.value, + }, + }) + } + + const handlePriorityChange = (event) => { + const selectedPriority = event.target.value + setPriority(selectedPriority) + + // Trouve l'objet priority complet basé sur la valeur sélectionnée + const priorityObj = Prioritys.find((p) => p.value === selectedPriority) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + priority: priorityObj, + }, + }) + } + + const handleAssigneeChange = (event) => { + const value = event.target.value + setAssignee(value) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + assignee: value ? { displayName: value, emailAddress: value } : null, + }, + }) + } + + const handleEstimationChange = (event) => { + const value = event.target.value + setEstimation(value) + + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + timeoriginalestimate: value ? parseInt(value, 10) * 3600 : null, // Convertir heures en secondes + }, + }) } + + const handleStartDateChange = (date) => { + setStartDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + customfield_10015: date ? date.toISOString() : null, // Date de début + }, + }) + } + + const handleEndDateChange = (date) => { + setEndDate(date) + setNewIssue({ + ...newIssue, + fields: { + ...newIssue.fields, + duedate: date ? date.toISOString().split('T')[0] : null, // Date d'échéance + }, + }) + } + return ( - - - Summary* - - - - - - Description* - - - - - {/* - Priority - */} - {/* - setPriority(event.target.value)} - helperText="Please select the issue Priority" - > - {Prioritys.map((option) => ( - - {option.label} - - ))} - - */} - - Date debut - - - - - - - - Date fin - - - - - - - +
    + + + Summary* + + + + + + + + + Description* + + + + + + + + + Assigné à + + + + + Adresse email de la personne assignée à cette tâche + + + + + + + Estimation (heures) + + + + Estimation du temps nécessaire en heures + + + + + + Priorité + + + + {Prioritys.map((option) => ( + + ))} + + + Veuillez sélectionner la priorité de la tâche + + + + + + + Date début + + + + + + + + + + + Date d'échéance + + + + + + + +
    ) } +TaskIssueForm.propTypes = { + newIssue: PropTypes.object.isRequired, + setNewIssue: PropTypes.func.isRequired, +} + export default TaskIssueForm diff --git a/src/components/Modal/ModalCreateTicket.js b/src/components/Modal/ModalCreateTicket.js index 4cb2c9e10..97c0d9161 100644 --- a/src/components/Modal/ModalCreateTicket.js +++ b/src/components/Modal/ModalCreateTicket.js @@ -1,34 +1,56 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { MenuItem, TextField } from '@material-ui/core' +import { useLocation } from 'react-router-dom' import { - Dialog, - Button, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Divider, - Grid, - Typography, - Slide, -} from '@mui/material' + CButton, + CCol, + CForm, + CFormLabel, + CFormSelect, + CModal, + CModalBody, + CModalFooter, + CModalHeader, + CModalTitle, + CRow, + CCallout, +} from '@coreui/react' import { addNewTicketAPI, toggleCreateTicketModalClose } from '../../actions/ticketActions' import BugIssueForm from './ModalBody/BugIssueForm' import TaskIssueForm from './ModalBody/TaskIssueForm' import StoryIssueForm from './ModalBody/StoryIssueForm' import { projects, issueTypes } from '../../utils/TicketsConsts' import { emptyIssue } from '../../utils/emptyIssue' - -const Transition = React.forwardRef(function Transition(props, ref) { - return -}) const ModalCreateTicket = () => { const { isCreateTicketModalOpen } = useSelector((state) => state.ticket) + const location = useLocation() const [project, setProject] = useState(projects[0].value) const [issueType, setIssueType] = useState(issueTypes[0].value) const [newIssue, setNewIssue] = useState(emptyIssue) const dispatch = useDispatch() + + // Extraire le code du projet depuis le pathname + const getProjectFromPath = () => { + const pathSegments = location.pathname.split('/') + // Supposons que le format soit /project/{projectCode} ou similar + const projectCodeFromPath = pathSegments.find((segment) => + projects.some( + (p) => p.value === segment || p.label.toLowerCase().includes(segment.toLowerCase()), + ), + ) + + if (projectCodeFromPath) { + const foundProject = projects.find( + (p) => + p.value === projectCodeFromPath || + p.label.toLowerCase().includes(projectCodeFromPath.toLowerCase()), + ) + return foundProject ? foundProject.value : projects[0].value + } + + return projects[0].value + } + const handleClose = () => { dispatch(toggleCreateTicketModalClose()) } @@ -44,185 +66,209 @@ const ModalCreateTicket = () => { } return id } + + // Définir le projet basé sur le pathname lors de l'ouverture du modal + useEffect(() => { + if (isCreateTicketModalOpen) { + const projectFromPath = getProjectFromPath() + setProject(projectFromPath) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isCreateTicketModalOpen, location.pathname]) + useEffect(() => { const now = new Date().toISOString() - setNewIssue({ - ...newIssue, + setNewIssue((prevIssue) => ({ + ...prevIssue, id: generateId(), fields: { - ...newIssue.fields, + ...prevIssue.fields, created: now, lastViewed: now, updated: now, statuscategorychangedate: now, + project: { + key: project, + name: projects.find((p) => p.value === project)?.label || project, + }, }, - }) - }, [isCreateTicketModalOpen]) + })) + }, [isCreateTicketModalOpen, project]) useEffect(() => { - switch (issueType) { - case 'Bug': - setNewIssue({ - ...newIssue, - fields: { - ...newIssue.fields, - issuetype: { - id: '10002', - hierarchyLevel: 0, - iconUrl: - 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium', - avatarId: 10303, - subtask: false, - description: 'Un problème ou une erreur.', - entityId: 'b6942a7a-0278-49e3-89d3-85295176d3e8', - name: 'Bug', - self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10002', + setNewIssue((prevIssue) => { + let updatedIssue = { ...prevIssue } + + switch (issueType) { + case 'Bug': + updatedIssue = { + ...prevIssue, + fields: { + ...prevIssue.fields, + issuetype: { + id: '10002', + hierarchyLevel: 0, + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium', + avatarId: 10303, + subtask: false, + description: 'Un problème ou une erreur.', + entityId: 'b6942a7a-0278-49e3-89d3-85295176d3e8', + name: 'Bug', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10002', + }, }, - }, - }) - break - - case 'Task': - setNewIssue({ - ...newIssue, - fields: { - ...newIssue.fields, - issuetype: { - self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10001', - name: 'Tâche', - description: 'Une tâche distincte.', - id: '10001', - entityId: 'ca1798d2-e4c6-4758-bfaf-7e38a85cd0ea', - iconUrl: - 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium', - avatarId: 10318, - subtask: false, - hierarchyLevel: 0, + } + break + + case 'Task': + updatedIssue = { + ...prevIssue, + fields: { + ...prevIssue.fields, + issuetype: { + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10001', + name: 'Tâche', + description: 'Une tâche distincte.', + id: '10001', + entityId: 'ca1798d2-e4c6-4758-bfaf-7e38a85cd0ea', + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium', + avatarId: 10318, + subtask: false, + hierarchyLevel: 0, + }, }, - }, - }) - break - - case 'Story': - setNewIssue({ - ...newIssue, - fields: { - ...newIssue.fields, - issuetype: { - hierarchyLevel: 0, - subtask: false, - description: "Une fonctionnalité exprimée sous la forme d'un objectif utilisateur.", - iconUrl: - 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium', - avatarId: 10315, - entityId: '6b7e8da8-f84c-41ac-80ac-ba881764a634', - name: 'Story', - id: '10003', - self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10003', + } + break + + case 'Story': + updatedIssue = { + ...prevIssue, + fields: { + ...prevIssue.fields, + issuetype: { + hierarchyLevel: 0, + subtask: false, + description: "Une fonctionnalité exprimée sous la forme d'un objectif utilisateur.", + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium', + avatarId: 10315, + entityId: '6b7e8da8-f84c-41ac-80ac-ba881764a634', + name: 'Story', + id: '10003', + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10003', + }, }, - }, - }) - break - - case 'Epic': - setNewIssue({ - ...newIssue, - fields: { - ...newIssue.fields, - issuetype: { - self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10004', - id: '10004', - description: 'Une collection de bugs, stories et tâches connexes.', - iconUrl: - 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10307?size=medium', - name: 'Epic', - subtask: false, - avatarId: 10307, - entityId: 'd1c66b55-cd69-4aba-b239-665a2e2f6af3', - hierarchyLevel: 1, + } + break + + case 'Epic': + updatedIssue = { + ...prevIssue, + fields: { + ...prevIssue.fields, + issuetype: { + self: 'https://sesame-team-pfe.atlassian.net/rest/api/2/issuetype/10004', + id: '10004', + description: 'Une collection de bugs, stories et tâches connexes.', + iconUrl: + 'https://sesame-team-pfe.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10307?size=medium', + name: 'Epic', + subtask: false, + avatarId: 10307, + entityId: 'd1c66b55-cd69-4aba-b239-665a2e2f6af3', + hierarchyLevel: 1, + }, }, - }, - }) - break + } + break - default: - break - } + default: + break + } + + return updatedIssue + }) }, [issueType]) return ( - dispatch(toggleCreateTicketModalClose())} - // aria-labelledby="alert-dialog-title" - // aria-describedby="alert-dialog-description" - aria-labelledby="scroll-dialog-title" - aria-describedby="scroll-dialog-description" - maxWidth="md" - fullWidth + - Créer un nouveau ticket - - + + Créer un nouveau ticket + + + Les champs obligatoires sont marqués d'un astérisque * - - - - Projet* - - - setProject(event.target.value)} - helperText="Please select your project" - > - {projects.map((option) => ( - - {option.label} - - ))} - - - - - - Issue Type* - - - setIssueType(event.target.value)} - helperText="Please select the issue type" - > - {issueTypes.map((option) => ( - - {option.label} - - ))} - - - - + + + + + + Projet* + + + setProject(event.target.value)} + aria-describedby="project-help" + > + {projects.map((option) => ( + + ))} + + + Veuillez sélectionner votre projet + + + + + + + Type d'issue* + + + setIssueType(event.target.value)} + aria-describedby="issue-type-help" + > + {issueTypes.map((option) => ( + + ))} + + + Veuillez sélectionner le type d'issue + + + + + +
    + {issueType === 'Bug' && } - {issueType === 'Task' && } - {issueType === 'Story' && } -
    - - - - -
    + {issueType === 'Task' && } + {issueType === 'Story' && } + + + + Annuler + + + Créer + + + ) } From 42ad864b07618bef7a8610b48a4e1deb7c4d641c Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 2 Jul 2025 16:04:33 +0100 Subject: [PATCH 68/72] feat: add @tinymce/miniature dependency to enhance text editing capabilities --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 398b4752b..17acd6c42 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@mui/styled-engine-sc": "^7.1.0", "@mui/x-date-pickers": "^8.3.1", "@popperjs/core": "^2.11.8", + "@tinymce/miniature": "^6.0.0", "@tinymce/tinymce-react": "^6.1.0", "@toolpad/core": "^0.15.0", "axios": "^1.7.7", From abff6c4019b0f2772ba53aae4232d3a11a34c73d Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 2 Jul 2025 16:30:30 +0100 Subject: [PATCH 69/72] refactor: remove unused Material-UI dependencies and components --- package.json | 3 - .../DetailPanel/DetailPanelTableTicket.js | 169 ------------------ src/views/icons/MaterialTableIcons.js | 40 ----- 3 files changed, 212 deletions(-) delete mode 100644 src/components/DetailPanel/DetailPanelTableTicket.js delete mode 100644 src/views/icons/MaterialTableIcons.js diff --git a/package.json b/package.json index 17acd6c42..25cc9cccf 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,6 @@ "@coreui/utils": "^2.0.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", - "@material-table/core": "^6.4.4", - "@material-ui/core": "^4.12.4", - "@material-ui/icons": "^4.11.3", "@mui/icons-material": "^7.1.0", "@mui/material": "^7.1.0", "@mui/styled-engine-sc": "^7.1.0", diff --git a/src/components/DetailPanel/DetailPanelTableTicket.js b/src/components/DetailPanel/DetailPanelTableTicket.js deleted file mode 100644 index b7bbb1da3..000000000 --- a/src/components/DetailPanel/DetailPanelTableTicket.js +++ /dev/null @@ -1,169 +0,0 @@ -/* eslint-disable react/prop-types */ -import { - Accordion, - AccordionDetails, - AccordionSummary, - Grid, - TextField, - Typography, -} from '@material-ui/core' -import { makeStyles } from '@material-ui/core/styles' - -import React, { useState } from 'react' - -import ExpandMoreIcon from '@material-ui/icons/ExpandMore' - -const useStyles = makeStyles((theme) => ({ - root: { - width: '100%', - }, - heading: { - fontSize: theme.typography.pxToRem(15), - fontWeight: theme.typography.fontWeightBold, - }, -})) - -const DetailPanelTableTicket = ({ rowData }) => { - const [summaryFocus, setSummaryFocus] = useState(false) - const [loading, setLoading] = useState(false) - const [expanded, setExpanded] = useState(true) - - const handleUpdateTicket = (rowData) => { - console.log('update ticket', rowData) - setSummaryFocus(false) - setLoading(true) - } - - const jiraToHtml = (text) => { - // Échappement de base - text = text.replace(/&/g, '&').replace(//g, '>') - - // Couleur {color:#xxxxxx} - text = text.replace( - /\{color:(#[0-9a-fA-F]{6})\}([\s\S]*?)\{color\}/g, - (_, color, content) => `${content}`, - ) - - // Gras *texte* - text = text.replace(/\*(.*?)\*/g, '$1') - - // Italique _texte_ - text = text.replace(/_(.*?)_/g, '$1') - - // Souligné +texte+ - text = text.replace(/\+(.*?)\+/g, '$1') - - // Barré -texte- - text = text.replace(/-(.*?)-/g, '$1') - - // Monospace ~texte~ - text = text.replace(/~(.*?)~/g, '$1') - - // Exposant ^texte^ - text = text.replace(/\^(.*?)\^/g, '$1') - - // Lien [texte|url] - text = text.replace(/\[(.+?)\|(.+?)\]/g, `$1`) - - // Mention [~accountid:xxxxx] → nom générique - text = text.replace(/\[~accountid:[a-zA-Z0-9]*\]/g, `@utilisateur`) - - // Listes à puces * - text = text.replace(/(^|\n)\* (.*?)(?=\n|$)/g, '$1
    • $2
    ') - text = text.replace(/<\/ul>\s*
      /g, '') // merge lists - - // Listes numérotées # - text = text.replace(/(^|\n)# (.*?)(?=\n|$)/g, '$1
      1. $2
      ') - text = text.replace(/<\/ol>\s*
        /g, '') // merge lists - - // Tableaux - // En-tête ||header||header|| - text = text.replace(/\|\|(.+?)\|\|/g, (_, headers) => { - const cols = headers - .split('||') - .filter(Boolean) - .map((h) => `${h.trim()}`) - .join('') - return `${cols}` - }) - - // Lignes |col|col| - text = text.replace(/\n\|(.+?)\|/g, (_, row) => { - const cols = row - .split('|') - .map((c) => ``) - .join('') - return `${cols}` - }) - - // Fin de tableau - text = text.replace(/<\/tbody>(?!<\/table>)/g, '
        ${c.trim()}
        ') - - // Bloc {noformat} - text = text.replace(/\{noformat\}([\s\S]*?)\{noformat\}/g, '
        $1
        ') - - // Retour à la ligne double - text = text.replace(/\n{2,}/g, '

        ') - - return text - } - - const classes = useStyles() - - return ( -
        -
        -
        - setSummaryFocus(true)} - onBlur={() => handleUpdateTicket()} - value={rowData.fields.summary} - InputProps={{ - disableUnderline: true, - style: { - border: 'none', - fontWeight: 'bold', - }, - }} - /> -
        - Description - {rowData.fields.description && ( -
        - )} -
        -
        -
        - setExpanded(!expanded)}> - } - aria-controls="panel1a-content" - id="panel1a-header" - > - Détails - - - - -
        Personne assignée
        -
        - - {rowData.fields.assignee ? ( -
        {rowData.fields.assignee.displayName}
        - ) : ( -
        not assigned
        - )} -
        -
        -
        -
        -
        -
        -
        - ) -} - -export default DetailPanelTableTicket diff --git a/src/views/icons/MaterialTableIcons.js b/src/views/icons/MaterialTableIcons.js deleted file mode 100644 index ce369424c..000000000 --- a/src/views/icons/MaterialTableIcons.js +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable react/display-name */ -import React, { forwardRef } from 'react' - -import AddBox from '@material-ui/icons/AddBox' -import ArrowDownward from '@material-ui/icons/ArrowDownward' -import Check from '@material-ui/icons/Check' -import ChevronLeft from '@material-ui/icons/ChevronLeft' -import ChevronRight from '@material-ui/icons/ChevronRight' -import Clear from '@material-ui/icons/Clear' -import DeleteOutline from '@material-ui/icons/DeleteOutline' -import Edit from '@material-ui/icons/Edit' -import FilterList from '@material-ui/icons/FilterList' -import FirstPage from '@material-ui/icons/FirstPage' -import LastPage from '@material-ui/icons/LastPage' -import Remove from '@material-ui/icons/Remove' -import SaveAlt from '@material-ui/icons/SaveAlt' -import Search from '@material-ui/icons/Search' -import ViewColumn from '@material-ui/icons/ViewColumn' - -const tableIcons = { - Add: forwardRef((props, ref) => ), - Check: forwardRef((props, ref) => ), - Clear: forwardRef((props, ref) => ), - Delete: forwardRef((props, ref) => ), - DetailPanel: forwardRef((props, ref) => ), - Edit: forwardRef((props, ref) => ), - Export: forwardRef((props, ref) => ), - Filter: forwardRef((props, ref) => ), - FirstPage: forwardRef((props, ref) => ), - LastPage: forwardRef((props, ref) => ), - NextPage: forwardRef((props, ref) => ), - PreviousPage: forwardRef((props, ref) => ), - ResetSearch: forwardRef((props, ref) => ), - Search: forwardRef((props, ref) => ), - SortArrow: forwardRef((props, ref) => ), - ThirdStateCheck: forwardRef((props, ref) => ), - ViewColumn: forwardRef((props, ref) => ), -} - -export default tableIcons From 03b0708a9800845487e482d40e8deb027dd40e37 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Wed, 2 Jul 2025 16:50:27 +0100 Subject: [PATCH 70/72] feat: enhance ticket creation and viewing experience with improved form handling and navigation --- src/components/Modal/ModalCreateTicket.js | 36 +- src/utils/emptyIssue.js | 2 +- src/views/pages/Tickets/TicketView.js | 602 +++++++++++++++++++++- src/views/pages/Tickets/TicketsHome.js | 6 +- 4 files changed, 633 insertions(+), 13 deletions(-) diff --git a/src/components/Modal/ModalCreateTicket.js b/src/components/Modal/ModalCreateTicket.js index 97c0d9161..e8c754b1f 100644 --- a/src/components/Modal/ModalCreateTicket.js +++ b/src/components/Modal/ModalCreateTicket.js @@ -19,14 +19,17 @@ import { addNewTicketAPI, toggleCreateTicketModalClose } from '../../actions/tic import BugIssueForm from './ModalBody/BugIssueForm' import TaskIssueForm from './ModalBody/TaskIssueForm' import StoryIssueForm from './ModalBody/StoryIssueForm' +// import EpicIssueForm from './ModalBody/EpicIssueForm' // À créer si nécessaire import { projects, issueTypes } from '../../utils/TicketsConsts' import { emptyIssue } from '../../utils/emptyIssue' + const ModalCreateTicket = () => { const { isCreateTicketModalOpen } = useSelector((state) => state.ticket) const location = useLocation() const [project, setProject] = useState(projects[0].value) const [issueType, setIssueType] = useState(issueTypes[0].value) const [newIssue, setNewIssue] = useState(emptyIssue) + const [isSubmitting, setIsSubmitting] = useState(false) const dispatch = useDispatch() // Extraire le code du projet depuis le pathname @@ -52,11 +55,31 @@ const ModalCreateTicket = () => { } const handleClose = () => { + // Reset du formulaire lors de la fermeture + setNewIssue(emptyIssue) + setProject(projects[0].value) + setIssueType(issueTypes[0].value) + setIsSubmitting(false) dispatch(toggleCreateTicketModalClose()) } - const handleSubmitTicket = () => { - dispatch(addNewTicketAPI(newIssue)) + const handleSubmitTicket = async () => { + // Validation basique + if (!newIssue.fields?.summary?.trim()) { + alert('Le résumé est obligatoire') + return + } + + setIsSubmitting(true) + try { + await dispatch(addNewTicketAPI(newIssue)) + handleClose() // Fermer après succès + } catch (error) { + console.error('Erreur lors de la création du ticket:', error) + alert('Erreur lors de la création du ticket') + } finally { + setIsSubmitting(false) + } } const generateId = () => { @@ -259,13 +282,16 @@ const ModalCreateTicket = () => { {issueType === 'Bug' && } {issueType === 'Task' && } {issueType === 'Story' && } + {issueType === 'Epic' && ( +
        Formulaire Epic à implémenter (EpicIssueForm)
        + )} - + Annuler - - Créer + + {isSubmitting ? 'Création...' : 'Créer'} diff --git a/src/utils/emptyIssue.js b/src/utils/emptyIssue.js index b9a5e82e1..2be3bd2ed 100644 --- a/src/utils/emptyIssue.js +++ b/src/utils/emptyIssue.js @@ -146,7 +146,7 @@ export const emptyIssue = { }, }, id: '', - key: 'SCRUM-interne', + key: 'interne', self: '', // createdAt: { // seconds: 1746709762, diff --git a/src/views/pages/Tickets/TicketView.js b/src/views/pages/Tickets/TicketView.js index bb1620dc0..fa52968f8 100644 --- a/src/views/pages/Tickets/TicketView.js +++ b/src/views/pages/Tickets/TicketView.js @@ -1,11 +1,603 @@ -import React from 'react' +import React, { useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useParams, useNavigate } from 'react-router-dom' +import { + CBadge, + CButton, + CCard, + CCardBody, + CCardHeader, + CCol, + CContainer, + CRow, + CSpinner, + CTable, + CTableBody, + CTableDataCell, + CTableRow, +} from '@coreui/react' +import CIcon from '@coreui/icons-react' +import { cilArrowLeft, cilPencil } from '@coreui/icons' const TicketView = () => { + const { code } = useParams() + const navigate = useNavigate() + const dispatch = useDispatch() + + // Récupérer le ticket depuis le store + const { ticketList, loading } = useSelector((state) => state.ticket) + const ticket = ticketList.find((t) => t.key === code) + + useEffect(() => { + // Si le ticket n'est pas dans la liste, vous pourriez faire un appel API + if (!ticket && !loading) { + console.log('Récupération du ticket:', code) + } + }, [code, ticket, loading, dispatch]) + + const handleGoBack = () => { + navigate('/tickets') + } + + const handleEditTicket = () => { + console.log('Éditer le ticket:', ticket) + } + + if (loading) { + return ( +
        + +
        + ) + } + + if (!ticket) { + return ( + + + + + +

        Ticket non trouvé

        +

        Le ticket avec la clé "{code}" n'a pas été trouvé.

        + + + Retour à la liste + +
        +
        +
        +
        +
        + ) + } + + const formatDate = (dateString) => { + if (!dateString) return 'N/A' + return new Date(dateString).toLocaleDateString('fr-FR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } + + const getStatusColor = (status) => { + switch (status?.toLowerCase()) { + case 'done': + case 'terminé': + return 'success' + case 'in progress': + case 'en cours': + return 'warning' + case 'to do': + case 'à faire': + return 'secondary' + default: + return 'primary' + } + } + + const getPriorityColor = (priority) => { + switch (priority?.toLowerCase()) { + case 'highest': + case 'très haute': + return 'danger' + case 'high': + case 'haute': + return 'warning' + case 'medium': + case 'moyenne': + return 'info' + case 'low': + case 'basse': + return 'secondary' + default: + return 'primary' + } + } + return ( -
        -

        Ticket View

        -

        This is the ticket view page.

        -
        + + + +
        +
        + + + Retour + + / Tickets / {ticket.key} +
        + + + Éditer + +
        +
        +
        + + + + + +
        +
        {ticket.key}
        + + {ticket.configId ? 'Externe' : 'Interne'} + +
        +
        + +

        {ticket.fields?.summary || 'Pas de résumé'}

        + + {ticket.fields?.description && ( +
        +
        Description
        +

        {ticket.fields.description}

        +
        + )} + + {ticket.fields?.issuetype && ( +
        +
        Type d'issue
        +
        + {ticket.fields.issuetype.iconUrl && ( + {ticket.fields.issuetype.name} + )} + {ticket.fields.issuetype.name} + {ticket.fields.issuetype.description && ( + + - {ticket.fields.issuetype.description} + + )} +
        +
        + )} + + {/* Informations supplémentaires */} + {ticket.fields?.environment && ( +
        +
        Environnement
        +

        {ticket.fields.environment}

        +
        + )} + + {ticket.fields?.components && ticket.fields.components.length > 0 && ( +
        +
        Composants
        +
        + {ticket.fields.components.map((component, index) => ( + + {component.name} + + ))} +
        +
        + )} + + {ticket.fields?.labels && ticket.fields.labels.length > 0 && ( +
        +
        Labels
        +
        + {ticket.fields.labels.map((label, index) => ( + + {label} + + ))} +
        +
        + )} + + {ticket.fields?.fixVersions && ticket.fields.fixVersions.length > 0 && ( +
        +
        Versions de correction
        +
        + {ticket.fields.fixVersions.map((version, index) => ( + + {version.name} + + ))} +
        +
        + )} + + {ticket.fields?.affectedVersions && ticket.fields.affectedVersions.length > 0 && ( +
        +
        Versions affectées
        +
        + {ticket.fields.affectedVersions.map((version, index) => ( + + {version.name} + + ))} +
        +
        + )} +
        +
        +
        + + + + +
        Détails
        +
        + + + + + Statut + + + {ticket.fields?.status?.name || 'N/A'} + + + + + {ticket.fields?.priority && ( + + Priorité + + + {ticket.fields.priority.name} + + + + )} + + {ticket.fields?.assignee && ( + + Assigné à + +
        + {ticket.fields.assignee.avatarUrls && ( + {ticket.fields.assignee.displayName} + )} +
        +
        {ticket.fields.assignee.displayName}
        + {ticket.fields.assignee.emailAddress && ( + + {ticket.fields.assignee.emailAddress} + + )} +
        +
        +
        +
        + )} + + {ticket.fields?.reporter && ( + + Rapporteur + +
        + {ticket.fields.reporter.avatarUrls && ( + {ticket.fields.reporter.displayName} + )} +
        +
        {ticket.fields.reporter.displayName}
        + {ticket.fields.reporter.emailAddress && ( + + {ticket.fields.reporter.emailAddress} + + )} +
        +
        +
        +
        + )} + + {ticket.fields?.project && ( + + Projet + +
        + {ticket.fields.project.avatarUrls && ( + {ticket.fields.project.name} + )} +
        +
        {ticket.fields.project.name}
        + {ticket.fields.project.key} +
        +
        +
        +
        + )} + + {ticket.fields?.resolution && ( + + Résolution + + {ticket.fields.resolution.name} + {ticket.fields.resolution.description && ( +
        + + {ticket.fields.resolution.description} + +
        + )} +
        +
        + )} + + {ticket.fields?.timeestimate && ( + + Estimation + + {Math.round(ticket.fields.timeestimate / 3600)} heures + + + )} + + {ticket.fields?.timespent && ( + + Temps passé + + {Math.round(ticket.fields.timespent / 3600)} heures + + + )} + + {ticket.fields?.duedate && ( + + Date d'échéance + + + {formatDate(ticket.fields.duedate)} + + + + )} + + {ticket.fields?.resolutiondate && ( + + Date de résolution + {formatDate(ticket.fields.resolutiondate)} + + )} + + + Créé + {formatDate(ticket.fields?.created)} + + + + Mis à jour + {formatDate(ticket.fields?.updated)} + +
        +
        +
        +
        +
        +
        + + {/* Section Commentaires */} + {ticket.fields?.comment && + ticket.fields.comment.comments && + ticket.fields.comment.comments.length > 0 && ( + + + + +
        Commentaires ({ticket.fields.comment.comments.length})
        +
        + + {ticket.fields.comment.comments.map((comment, index) => ( +
        +
        + {comment.author?.avatarUrls && ( + {comment.author.displayName} + )} +
        + {comment.author?.displayName || 'Anonyme'} + {formatDate(comment.created)} +
        +
        +
        +

        {comment.body}

        + {comment.updated && comment.updated !== comment.created && ( + + Modifié le {formatDate(comment.updated)} + + )} +
        +
        + ))} +
        +
        +
        +
        + )} + + {/* Section Historique */} + {ticket.changelog && ticket.changelog.histories && ticket.changelog.histories.length > 0 && ( + + + + +
        Historique des modifications
        +
        + + {ticket.changelog.histories.slice(0, 10).map((history, index) => ( +
        +
        + {history.author?.avatarUrls && ( + {history.author.displayName} + )} +
        + {history.author?.displayName || 'Système'} + {formatDate(history.created)} +
        +
        +
        + {history.items?.map((item, itemIndex) => ( +
        + {item.field} modifié + {item.fromString && ( + + {' '} + de {item.fromString} + + )} + {item.toString && ( + + {' '} + vers {item.toString} + + )} +
        + ))} +
        +
        + ))} +
        +
        +
        +
        + )} + + {/* Section Liens et Relations */} + {ticket.fields?.issuelinks && ticket.fields.issuelinks.length > 0 && ( + + + + +
        Tickets liés
        +
        + + {ticket.fields.issuelinks.map((link, index) => ( +
        + + {link.type?.name || 'Lié'} + + {link.outwardIssue && ( + + {link.outwardIssue.key} -{' '} + {link.outwardIssue.fields?.summary} + + {link.outwardIssue.fields?.status?.name} + + + )} + {link.inwardIssue && ( + + {link.inwardIssue.key} - {link.inwardIssue.fields?.summary} + + {link.inwardIssue.fields?.status?.name} + + + )} +
        + ))} +
        +
        +
        +
        + )} + + {/* Section Sous-tâches */} + {ticket.fields?.subtasks && ticket.fields.subtasks.length > 0 && ( + + + + +
        Sous-tâches ({ticket.fields.subtasks.length})
        +
        + + + + {ticket.fields.subtasks.map((subtask, index) => ( + + + {subtask.fields?.issuetype?.name} + {subtask.key} + + {subtask.fields?.summary} + + + {subtask.fields?.status?.name} + + + + {subtask.fields?.assignee?.displayName || 'Non assigné'} + + + ))} + + + +
        +
        +
        + )} +
        ) } + export default TicketView diff --git a/src/views/pages/Tickets/TicketsHome.js b/src/views/pages/Tickets/TicketsHome.js index 6b2e57247..94f434fb6 100644 --- a/src/views/pages/Tickets/TicketsHome.js +++ b/src/views/pages/Tickets/TicketsHome.js @@ -37,8 +37,10 @@ const Tickets = () => { dispatch(toggleCreateTicketModalOpen()) } - const handleRowClick = (user) => { - console.log('Utilisateur cliqué :', user) + const handleRowClick = (ticket) => { + console.log('Ticket cliqué :', ticket) + // Rediriger vers la vue détaillée du ticket + navigate(`/ticket/${ticket.key}`) } if (loading) { From dbba53d80121998e488c33357082672f626d7fd0 Mon Sep 17 00:00:00 2001 From: Mohamed Amine DEROUICH Date: Thu, 3 Jul 2025 11:52:55 +0100 Subject: [PATCH 71/72] fix: update project description in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 25cc9cccf..a62fcca5a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "taketit", "version": "0.0.0", - "description": "CoreUI Free React Admin Template", + "description": "Projet PFE", "homepage": ".", "bugs": { "url": "https://github.com/coreui/coreui-free-react-admin-template/issues" From 590670260e6c14bc404c57ac915585984fe47f51 Mon Sep 17 00:00:00 2001 From: AMDOUNI Oumaima Date: Thu, 3 Jul 2025 17:23:57 +0200 Subject: [PATCH 72/72] commit test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a62fcca5a..941cd9ff1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "taketit", "version": "0.0.0", - "description": "Projet PFE", + "description": "Projet PFE 2025", "homepage": ".", "bugs": { "url": "https://github.com/coreui/coreui-free-react-admin-template/issues"