diff --git a/backend/src/routes/ai_use_cases.js b/backend/src/routes/ai_use_cases.js index 4316772..b1a72b4 100644 --- a/backend/src/routes/ai_use_cases.js +++ b/backend/src/routes/ai_use_cases.js @@ -91,8 +91,8 @@ router.use(checkCrudPermissions('ai_use_cases')); router.post('/', wrapAsync(async (req, res) => { const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); - await Ai_use_casesService.create(req.body.data, req.currentUser, true, link.host); - const payload = true; + const createdAiUseCase = await Ai_use_casesService.create(req.body.data, req.currentUser, true, link.host); + const payload = createdAiUseCase?.get ? createdAiUseCase.get({ plain: true }) : createdAiUseCase; res.status(200).send(payload); })); @@ -193,6 +193,11 @@ router.put('/:id', wrapAsync(async (req, res) => { res.status(200).send(payload); })); +router.put('/:id/submit', wrapAsync(async (req, res) => { + const payload = await Ai_use_casesService.submitForReview(req.params.id, req.currentUser); + res.status(200).send(payload); +})); + /** * @swagger * /api/ai_use_cases/{id}: diff --git a/backend/src/routes/approval_steps.js b/backend/src/routes/approval_steps.js index 1367780..1d2c6ee 100644 --- a/backend/src/routes/approval_steps.js +++ b/backend/src/routes/approval_steps.js @@ -184,6 +184,11 @@ router.put('/:id', wrapAsync(async (req, res) => { res.status(200).send(payload); })); +router.put('/:id/decision', wrapAsync(async (req, res) => { + const payload = await Approval_stepsService.recordDecision(req.params.id, req.body, req.currentUser); + res.status(200).send(payload); +})); + /** * @swagger * /api/approval_steps/{id}: diff --git a/backend/src/services/ai_use_cases.js b/backend/src/services/ai_use_cases.js index ab68f55..d13b474 100644 --- a/backend/src/services/ai_use_cases.js +++ b/backend/src/services/ai_use_cases.js @@ -1,21 +1,69 @@ const db = require('../db/models'); const Ai_use_casesDBApi = require('../db/api/ai_use_cases'); -const processFile = require("../middlewares/upload"); +const processFile = require('../middlewares/upload'); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); const stream = require('stream'); +const APPROVAL_SEQUENCE = [ + { + step_type: 'partner', + step_order: 1, + reviewerRoles: ['Governance Lead', 'Administrator'], + }, + { + step_type: 'general_counsel', + step_order: 2, + reviewerRoles: ['General Counsel Reviewer', 'Administrator'], + }, + { + step_type: 'it_security', + step_order: 3, + reviewerRoles: ['IT Security Reviewer', 'Administrator'], + }, + { + step_type: 'ethics_risk', + step_order: 4, + reviewerRoles: ['Ethics and Risk Reviewer', 'Administrator'], + }, +]; +function buildHttpError(message, code = 400) { + const error = new Error(message); + error.code = code; + return error; +} +async function findReviewerId(reviewerRoles, transaction) { + for (const roleName of reviewerRoles) { + const reviewer = await db.users.findOne({ + include: [ + { + model: db.roles, + as: 'app_role', + required: true, + where: { name: roleName }, + }, + ], + order: [['createdAt', 'ASC']], + transaction, + }); + + if (reviewer?.id) { + return reviewer.id; + } + } + + return null; +} module.exports = class Ai_use_casesService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); + try { - await Ai_use_casesDBApi.create( + const createdAiUseCase = await Ai_use_casesDBApi.create( data, { currentUser, @@ -24,13 +72,14 @@ module.exports = class Ai_use_casesService { ); await transaction.commit(); + return createdAiUseCase; } catch (error) { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { @@ -38,7 +87,7 @@ module.exports = class Ai_use_casesService { const bufferStream = new stream.PassThrough(); const results = []; - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); await new Promise((resolve, reject) => { bufferStream @@ -49,13 +98,13 @@ module.exports = class Ai_use_casesService { resolve(); }) .on('error', (error) => reject(error)); - }) + }); await Ai_use_casesDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, }); await transaction.commit(); @@ -67,16 +116,15 @@ module.exports = class Ai_use_casesService { static async update(data, id, currentUser) { const transaction = await db.sequelize.transaction(); + try { - let ai_use_cases = await Ai_use_casesDBApi.findBy( - {id}, - {transaction}, + const ai_use_cases = await Ai_use_casesDBApi.findBy( + { id }, + { transaction }, ); if (!ai_use_cases) { - throw new ValidationError( - 'ai_use_casesNotFound', - ); + throw new ValidationError('ai_use_casesNotFound'); } const updatedAi_use_cases = await Ai_use_casesDBApi.update( @@ -90,12 +138,79 @@ module.exports = class Ai_use_casesService { await transaction.commit(); return updatedAi_use_cases; - } catch (error) { await transaction.rollback(); throw error; } - }; + } + + static async submitForReview(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + const aiUseCase = await db.ai_use_cases.findByPk(id, { transaction }); + + if (!aiUseCase) { + throw buildHttpError('AI use case not found.', 404); + } + + const currentStatus = aiUseCase.status || 'draft'; + + if (currentStatus !== 'draft') { + throw buildHttpError('Only draft AI use cases can be submitted for review.'); + } + + if (!aiUseCase.title || !aiUseCase.business_goal || !aiUseCase.data_classificationId || !aiUseCase.intended_toolId) { + throw buildHttpError( + 'Add a title, business goal, data classification, and intended AI tool before submitting for review.', + ); + } + + const existingApprovalSteps = await db.approval_steps.count({ + where: { use_caseId: id }, + transaction, + }); + + if (existingApprovalSteps > 0) { + throw buildHttpError('This AI use case already has an approval workflow in progress.'); + } + + for (const approvalStep of APPROVAL_SEQUENCE) { + const assignedReviewerId = await findReviewerId(approvalStep.reviewerRoles, transaction); + + await db.approval_steps.create( + { + step_type: approvalStep.step_type, + decision: 'pending', + comments: null, + assigned_at: new Date(), + decided_at: null, + step_order: approvalStep.step_order, + use_caseId: aiUseCase.id, + assigned_reviewerId: assignedReviewerId, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + } + + await aiUseCase.update( + { + status: 'submitted', + submitted_at: aiUseCase.submitted_at || new Date(), + approved_at: null, + }, + { transaction }, + ); + + await transaction.commit(); + return Ai_use_casesDBApi.findBy({ id }); + } catch (error) { + await transaction.rollback(); + throw error; + } + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); @@ -131,8 +246,4 @@ module.exports = class Ai_use_casesService { throw error; } } - - }; - - diff --git a/backend/src/services/approval_steps.js b/backend/src/services/approval_steps.js index 62c72aa..9db1c32 100644 --- a/backend/src/services/approval_steps.js +++ b/backend/src/services/approval_steps.js @@ -1,21 +1,29 @@ const db = require('../db/models'); const Approval_stepsDBApi = require('../db/api/approval_steps'); -const processFile = require("../middlewares/upload"); +const processFile = require('../middlewares/upload'); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); const stream = require('stream'); +const NEXT_STATUS_BY_STEP = { + partner: 'risk_review', + general_counsel: 'security_review', + it_security: 'ethics_review', + ethics_risk: 'approved', +}; - - +function buildHttpError(message, code = 400) { + const error = new Error(message); + error.code = code; + return error; +} module.exports = class Approval_stepsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); + try { - await Approval_stepsDBApi.create( + const createdApprovalStep = await Approval_stepsDBApi.create( data, { currentUser, @@ -24,13 +32,14 @@ module.exports = class Approval_stepsService { ); await transaction.commit(); + return createdApprovalStep; } catch (error) { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { @@ -38,7 +47,7 @@ module.exports = class Approval_stepsService { const bufferStream = new stream.PassThrough(); const results = []; - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); await new Promise((resolve, reject) => { bufferStream @@ -49,13 +58,13 @@ module.exports = class Approval_stepsService { resolve(); }) .on('error', (error) => reject(error)); - }) + }); await Approval_stepsDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, }); await transaction.commit(); @@ -67,16 +76,15 @@ module.exports = class Approval_stepsService { static async update(data, id, currentUser) { const transaction = await db.sequelize.transaction(); + try { - let approval_steps = await Approval_stepsDBApi.findBy( - {id}, - {transaction}, + const approval_steps = await Approval_stepsDBApi.findBy( + { id }, + { transaction }, ); if (!approval_steps) { - throw new ValidationError( - 'approval_stepsNotFound', - ); + throw new ValidationError('approval_stepsNotFound'); } const updatedApproval_steps = await Approval_stepsDBApi.update( @@ -90,12 +98,107 @@ module.exports = class Approval_stepsService { await transaction.commit(); return updatedApproval_steps; - } catch (error) { await transaction.rollback(); throw error; } - }; + } + + static async recordDecision(id, data, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + const approvalStep = await db.approval_steps.findByPk(id, { transaction }); + + if (!approvalStep) { + throw buildHttpError('Approval step not found.', 404); + } + + if (approvalStep.decision !== 'pending') { + throw buildHttpError('This approval step has already been decided.'); + } + + if (!['approved', 'rejected', 'needs_changes'].includes(data?.decision)) { + throw buildHttpError('Select a valid decision: approved, rejected, or needs_changes.'); + } + + const aiUseCase = await db.ai_use_cases.findByPk(approvalStep.use_caseId, { transaction }); + + if (!aiUseCase) { + throw buildHttpError('The related AI use case could not be found.', 404); + } + + const activePendingStep = await db.approval_steps.findOne({ + where: { + use_caseId: approvalStep.use_caseId, + decision: 'pending', + }, + order: [['step_order', 'ASC']], + transaction, + }); + + if (!activePendingStep || activePendingStep.id !== approvalStep.id) { + throw buildHttpError('Only the current approval step can be decided.'); + } + + await approvalStep.update( + { + decision: data.decision, + comments: data?.comments?.trim() || null, + decided_at: new Date(), + updatedById: currentUser.id, + }, + { transaction }, + ); + + if (data.decision === 'approved') { + const nextStatus = NEXT_STATUS_BY_STEP[approvalStep.step_type] || 'approved'; + + await aiUseCase.update( + nextStatus === 'approved' + ? { + status: 'approved', + approved_at: new Date(), + updatedById: currentUser.id, + } + : { + status: nextStatus, + approved_at: null, + updatedById: currentUser.id, + }, + { transaction }, + ); + } + + if (data.decision === 'rejected') { + await aiUseCase.update( + { + status: 'rejected', + approved_at: null, + updatedById: currentUser.id, + }, + { transaction }, + ); + } + + if (data.decision === 'needs_changes') { + await aiUseCase.update( + { + status: 'needs_changes', + approved_at: null, + updatedById: currentUser.id, + }, + { transaction }, + ); + } + + await transaction.commit(); + return Approval_stepsDBApi.findBy({ id }); + } catch (error) { + await transaction.rollback(); + throw error; + } + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); @@ -131,8 +234,4 @@ module.exports = class Approval_stepsService { throw error; } } - - }; - - diff --git a/frontend/src/colors.ts b/frontend/src/colors.ts index 71e116a..681cbe0 100644 --- a/frontend/src/colors.ts +++ b/frontend/src/colors.ts @@ -11,10 +11,10 @@ export const colorsBgLight = { white: 'bg-white text-black', light: ' bg-white text-black text-black dark:bg-dark-900 dark:text-white', contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black', - success: 'bg-emerald-500 border-emerald-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white', + success: 'bg-emerald-500 border-emerald-500 dark:bg-emerald-500 dark:border-emerald-500 text-white', danger: 'bg-red-500 border-red-500 text-white', warning: 'bg-yellow-500 border-yellow-500 text-white', - info: 'bg-blue-500 border-blue-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white', + info: 'bg-[#0E1A2B] border-[#0E1A2B] dark:bg-[#D8B75E] dark:border-[#D8B75E] text-white dark:text-[#08111F]', } export const colorsText = { @@ -24,7 +24,7 @@ export const colorsText = { success: 'text-emerald-500', danger: 'text-red-500', warning: 'text-yellow-500', - info: 'text-blue-500', + info: 'text-[#0E1A2B]', }; export const colorsOutline = { @@ -34,7 +34,7 @@ export const colorsOutline = { success: [colorsText.success, 'border-emerald-500'].join(' '), danger: [colorsText.danger, 'border-red-500'].join(' '), warning: [colorsText.warning, 'border-yellow-500'].join(' '), - info: [colorsText.info, 'border-blue-500'].join(' '), + info: [colorsText.info, 'border-[#0E1A2B]'].join(' '), }; export const getButtonColor = ( @@ -53,30 +53,30 @@ export const getButtonColor = ( whiteDark: 'ring-gray-200 dark:ring-dark-500', lightDark: 'ring-gray-200 dark:ring-gray-500', contrast: 'ring-gray-300 dark:ring-gray-400', - success: 'ring-emerald-300 dark:ring-pavitra-blue', + success: 'ring-emerald-300 dark:ring-emerald-500', danger: 'ring-red-300 dark:ring-red-700', warning: 'ring-yellow-300 dark:ring-yellow-700', - info: "ring-blue-300 dark:ring-pavitra-blue", + info: "ring-[#D8B75E]/35 dark:ring-[#D8B75E]/35", }, active: { white: 'bg-gray-100', whiteDark: 'bg-gray-100 dark:bg-dark-800', lightDark: 'bg-gray-200 dark:bg-slate-700', contrast: 'bg-gray-700 dark:bg-slate-100', - success: 'bg-emerald-700 dark:bg-pavitra-blue', + success: 'bg-emerald-700 dark:bg-emerald-500', danger: 'bg-red-700 dark:bg-red-600', warning: 'bg-yellow-700 dark:bg-yellow-600', - info: 'bg-blue-700 dark:bg-pavitra-blue', + info: 'bg-[#162742] dark:bg-[#F2C95C]', }, bg: { white: 'bg-white text-black', whiteDark: 'bg-white text-black dark:bg-dark-900 dark:text-white', lightDark: 'bg-gray-100 text-black dark:bg-slate-800 dark:text-white', contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black', - success: 'bg-emerald-600 dark:bg-pavitra-blue text-white', + success: 'bg-emerald-600 dark:bg-emerald-500 text-white', danger: 'bg-red-600 text-white dark:bg-red-500 ', warning: 'bg-yellow-600 dark:bg-yellow-500 text-white', - info: " bg-blue-600 dark:bg-pavitra-blue text-white ", + info: " bg-[#0E1A2B] dark:bg-[#D8B75E] text-white dark:text-[#08111F] ", }, bgHover: { white: 'hover:bg-gray-100', @@ -84,39 +84,39 @@ export const getButtonColor = ( lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700', contrast: 'hover:bg-gray-700 hover:dark:bg-slate-100', success: - 'hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-pavitra-blue hover:dark:border-pavitra-blue', + 'hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-emerald-600 hover:dark:border-emerald-600', danger: 'hover:bg-red-700 hover:border-red-700 hover:dark:bg-red-600 hover:dark:border-red-600', warning: 'hover:bg-yellow-700 hover:border-yellow-700 hover:dark:bg-yellow-600 hover:dark:border-yellow-600', - info: "hover:bg-blue-700 hover:border-blue-700 hover:dark:bg-pavitra-blue/80 hover:dark:border-pavitra-blue/80", + info: "hover:bg-[#162742] hover:border-[#162742] hover:dark:bg-[#F2C95C] hover:dark:border-[#F2C95C]", }, borders: { white: 'border-white', whiteDark: 'border-white dark:border-dark-900', lightDark: 'border-gray-100 dark:border-slate-800', contrast: 'border-gray-800 dark:border-white', - success: 'border-emerald-600 dark:border-pavitra-blue', + success: 'border-emerald-600 dark:border-emerald-500', danger: 'border-red-600 dark:border-red-500', warning: 'border-yellow-600 dark:border-yellow-500', - info: "border-blue-600 border-blue-600 dark:border-pavitra-blue", + info: "border-[#0E1A2B] dark:border-[#D8B75E]", }, text: { contrast: 'dark:text-slate-100', - success: 'text-emerald-600 dark:text-pavitra-blue', + success: 'text-emerald-600 dark:text-emerald-400', danger: 'text-red-600 dark:text-red-500', warning: 'text-yellow-600 dark:text-yellow-500', - info: 'text-blue-600 dark:text-pavitra-blue', + info: 'text-[#0E1A2B] dark:text-[#D8B75E]', }, outlineHover: { contrast: 'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black', - success: 'hover:bg-emerald-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue', + success: 'hover:bg-emerald-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-emerald-500', danger: 'hover:bg-red-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-red-600', warning: 'hover:bg-yellow-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-yellow-600', - info: "hover:bg-blue-600 hover:bg-blue-600 hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue", + info: "hover:bg-[#0E1A2B] hover:text-white hover:dark:bg-[#D8B75E] hover:dark:text-[#08111F] hover:dark:border-[#D8B75E]", }, } diff --git a/frontend/src/components/Ai_tools/TableAi_tools.tsx b/frontend/src/components/Ai_tools/TableAi_tools.tsx index 1564ca3..b7e1d38 100644 --- a/frontend/src/components/Ai_tools/TableAi_tools.tsx +++ b/frontend/src/components/Ai_tools/TableAi_tools.tsx @@ -42,9 +42,6 @@ const TableSampleAi_tools = ({ filterItems, setFilterItems, filters, showGrid }) const { ai_tools, loading, count, notify: ai_toolsNotify, refetch } = useAppSelector((state) => state.ai_tools) const { currentUser } = useAppSelector((state) => state.auth); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const corners = useAppSelector((state) => state.style.corners); const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); for (let i = 0; i < numPages; i++) { pagesList.push(i); @@ -202,9 +199,7 @@ const TableSampleAi_tools = ({ filterItems, setFilterItems, filters, showGrid }) }; const controlClasses = - 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + - ` ${bgColor} ${focusRing} ${corners} ` + - 'dark:bg-slate-800 border'; + 'w-full my-1.5 rounded-lg border border-[#D8D0C2] bg-white px-3 py-2 text-sm text-[#0E1A2B] shadow-sm outline-none placeholder:text-[#9A8F7F] focus:border-[#D8B75E] focus:ring-2 focus:ring-[#D8B75E]/25 dark:border-dark-700 dark:bg-slate-800 dark:placeholder-gray-400'; const dataGrid = ( @@ -262,7 +257,7 @@ const TableSampleAi_tools = ({ filterItems, setFilterItems, filters, showGrid }) return ( <> {filterItems && Array.isArray( filterItems ) && filterItems.length ? - +
<> +
+
+

Active filters

+

Narrow the register without leaving the governance workspace.

+
+ Review scope +
{filterItems && filterItems.map((filterItem) => { return ( -
-
-
Filter
+
+
+
Filter
filter.title === filterItem?.fields?.selectedField )?.type === 'enum' ? ( -
-
+
+
Value
filter.title === filterItem?.fields?.selectedField )?.number ? ( -
-
-
From
+
+
+
From
-
To
+
To
-
-
+
+
+
From
-
To
+
To
) : ( -
-
Contains
+
+
Contains
)}
-
Action
+
Action
) })} -
+
diff --git a/frontend/src/components/Ai_tools/configureAi_toolsCols.tsx b/frontend/src/components/Ai_tools/configureAi_toolsCols.tsx index 26e2d59..9f467b5 100644 --- a/frontend/src/components/Ai_tools/configureAi_toolsCols.tsx +++ b/frontend/src/components/Ai_tools/configureAi_toolsCols.tsx @@ -43,7 +43,7 @@ export const loadColumns = async ( { field: 'name', - headerName: 'ToolName', + headerName: 'Tool Name', flex: 1, minWidth: 120, filterable: false, @@ -58,7 +58,7 @@ export const loadColumns = async ( { field: 'tool_type', - headerName: 'ToolType', + headerName: 'Tool Type', flex: 1, minWidth: 120, filterable: false, @@ -95,7 +95,7 @@ export const loadColumns = async ( { field: 'deployment_model', - headerName: 'DeploymentModel', + headerName: 'Deployment Model', flex: 1, minWidth: 120, filterable: false, @@ -110,7 +110,7 @@ export const loadColumns = async ( { field: 'approval_status', - headerName: 'ApprovalStatus', + headerName: 'Approval Status', flex: 1, minWidth: 120, filterable: false, @@ -125,7 +125,7 @@ export const loadColumns = async ( { field: 'supports_sso', - headerName: 'SupportsSSO', + headerName: 'Supports SSO', flex: 1, minWidth: 120, filterable: false, @@ -141,7 +141,7 @@ export const loadColumns = async ( { field: 'supports_audit_logs', - headerName: 'SupportsAuditLogs', + headerName: 'Supports Audit Logs', flex: 1, minWidth: 120, filterable: false, @@ -157,7 +157,7 @@ export const loadColumns = async ( { field: 'soc2_status', - headerName: 'SOC2Status', + headerName: 'SOC2 Status', flex: 1, minWidth: 120, filterable: false, @@ -172,7 +172,7 @@ export const loadColumns = async ( { field: 'data_retention_policy', - headerName: 'DataRetentionPolicy', + headerName: 'Data Retention Policy', flex: 1, minWidth: 120, filterable: false, @@ -187,7 +187,7 @@ export const loadColumns = async ( { field: 'training_on_client_data_policy', - headerName: 'TrainingOnClientDataPolicy', + headerName: 'Training On Client Data Policy', flex: 1, minWidth: 120, filterable: false, @@ -202,7 +202,7 @@ export const loadColumns = async ( { field: 'data_residency', - headerName: 'DataResidency', + headerName: 'Data Residency', flex: 1, minWidth: 120, filterable: false, @@ -217,7 +217,7 @@ export const loadColumns = async ( { field: 'deletion_policy', - headerName: 'DeletionPolicy', + headerName: 'Deletion Policy', flex: 1, minWidth: 120, filterable: false, @@ -232,7 +232,7 @@ export const loadColumns = async ( { field: 'security_posture_summary', - headerName: 'SecurityPostureSummary', + headerName: 'Security Posture Summary', flex: 1, minWidth: 120, filterable: false, @@ -247,7 +247,7 @@ export const loadColumns = async ( { field: 'security_documents', - headerName: 'SecurityDocuments', + headerName: 'Security Documents', flex: 1, minWidth: 120, filterable: false, @@ -273,7 +273,7 @@ export const loadColumns = async ( { field: 'monthly_cost', - headerName: 'MonthlyCost', + headerName: 'Monthly Cost', flex: 1, minWidth: 120, filterable: false, diff --git a/frontend/src/components/Ai_use_cases/TableAi_use_cases.tsx b/frontend/src/components/Ai_use_cases/TableAi_use_cases.tsx index fef56b0..81171d7 100644 --- a/frontend/src/components/Ai_use_cases/TableAi_use_cases.tsx +++ b/frontend/src/components/Ai_use_cases/TableAi_use_cases.tsx @@ -48,9 +48,6 @@ const TableSampleAi_use_cases = ({ filterItems, setFilterItems, filters, showGri const { ai_use_cases, loading, count, notify: ai_use_casesNotify, refetch } = useAppSelector((state) => state.ai_use_cases) const { currentUser } = useAppSelector((state) => state.auth); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const corners = useAppSelector((state) => state.style.corners); const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); for (let i = 0; i < numPages; i++) { pagesList.push(i); @@ -98,23 +95,23 @@ const TableSampleAi_use_cases = ({ filterItems, setFilterItems, filters, showGri setKanbanColumns([ - { id: "draft", label: "draft" }, + { id: "draft", label: "Draft" }, - { id: "submitted", label: "submitted" }, + { id: "submitted", label: "Submitted" }, - { id: "risk_review", label: "risk_review" }, + { id: "risk_review", label: "Risk review" }, - { id: "security_review", label: "security_review" }, + { id: "security_review", label: "Security review" }, - { id: "ethics_review", label: "ethics_review" }, + { id: "ethics_review", label: "Ethics review" }, - { id: "approved", label: "approved" }, + { id: "approved", label: "Approved" }, - { id: "rejected", label: "rejected" }, + { id: "rejected", label: "Rejected" }, - { id: "needs_changes", label: "needs_changes" }, + { id: "needs_changes", label: "Needs changes" }, - { id: "retired", label: "retired" }, + { id: "retired", label: "Retired" }, ]); @@ -243,9 +240,7 @@ const TableSampleAi_use_cases = ({ filterItems, setFilterItems, filters, showGri }; const controlClasses = - 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + - ` ${bgColor} ${focusRing} ${corners} ` + - 'dark:bg-slate-800 border'; + 'w-full my-1.5 rounded-lg border border-[#D8D0C2] bg-white px-3 py-2 text-sm text-[#0E1A2B] shadow-sm outline-none placeholder:text-[#9A8F7F] focus:border-[#D8B75E] focus:ring-2 focus:ring-[#D8B75E]/25 dark:border-dark-700 dark:bg-slate-800 dark:placeholder-gray-400'; const dataGrid = ( @@ -303,7 +298,7 @@ const TableSampleAi_use_cases = ({ filterItems, setFilterItems, filters, showGri return ( <> {filterItems && Array.isArray( filterItems ) && filterItems.length ? - + <> +
+
+

Active filters

+

Narrow the register without leaving the governance workspace.

+
+ Review scope +
{filterItems && filterItems.map((filterItem) => { return ( -
-
-
Filter
+
+
+
Filter
filter.title === filterItem?.fields?.selectedField )?.type === 'enum' ? ( -
-
+
+
Value
filter.title === filterItem?.fields?.selectedField )?.number ? ( -
-
-
From
+
+
+
From
-
To
+
To
-
-
+
+
+
From
-
To
+
To
) : ( -
-
Contains
+
+
Contains
)}
-
Action
+
Action
) })} -
+
diff --git a/frontend/src/components/Ai_use_cases/configureAi_use_casesCols.tsx b/frontend/src/components/Ai_use_cases/configureAi_use_casesCols.tsx index 537bbc8..37527fd 100644 --- a/frontend/src/components/Ai_use_cases/configureAi_use_casesCols.tsx +++ b/frontend/src/components/Ai_use_cases/configureAi_use_casesCols.tsx @@ -14,6 +14,7 @@ import DataGridMultiSelect from "../DataGridMultiSelect"; import ListActionsPopover from '../ListActionsPopover'; import {hasPermission} from "../../helpers/userPermissions"; +import { humanize } from '../../helpers/humanize'; type Params = (id: string) => void; @@ -43,7 +44,7 @@ export const loadColumns = async ( { field: 'title', - headerName: 'UseCaseTitle', + headerName: 'Title', flex: 1, minWidth: 120, filterable: false, @@ -95,7 +96,7 @@ export const loadColumns = async ( { field: 'practice_group', - headerName: 'PracticeGroup', + headerName: 'Practice group', flex: 1, minWidth: 120, filterable: false, @@ -117,7 +118,7 @@ export const loadColumns = async ( { field: 'matter_type', - headerName: 'MatterType', + headerName: 'Matter type', flex: 1, minWidth: 120, filterable: false, @@ -139,7 +140,7 @@ export const loadColumns = async ( { field: 'data_classification', - headerName: 'DataClassification', + headerName: 'Data classification', flex: 1, minWidth: 120, filterable: false, @@ -161,7 +162,7 @@ export const loadColumns = async ( { field: 'intended_tool', - headerName: 'IntendedAITool', + headerName: 'Intended AI tool', flex: 1, minWidth: 120, filterable: false, @@ -183,7 +184,7 @@ export const loadColumns = async ( { field: 'business_goal', - headerName: 'BusinessGoal', + headerName: 'Business goal', flex: 1, minWidth: 120, filterable: false, @@ -207,13 +208,13 @@ export const loadColumns = async ( editable: hasUpdatePermission, - + valueFormatter: ({ value }) => value ? humanize(value) : 'Not set', }, { field: 'risk_level', - headerName: 'RiskLevel', + headerName: 'Risk level', flex: 1, minWidth: 120, filterable: false, @@ -222,13 +223,13 @@ export const loadColumns = async ( editable: hasUpdatePermission, - + valueFormatter: ({ value }) => value ? humanize(value) : 'Not set', }, { field: 'expected_hours_saved', - headerName: 'ExpectedHoursSaved', + headerName: 'Expected hours saved', flex: 1, minWidth: 120, filterable: false, @@ -244,7 +245,7 @@ export const loadColumns = async ( { field: 'review_notes', - headerName: 'ReviewNotes', + headerName: 'Review notes', flex: 1, minWidth: 120, filterable: false, @@ -259,7 +260,7 @@ export const loadColumns = async ( { field: 'submitted_at', - headerName: 'SubmittedAt', + headerName: 'Submitted at', flex: 1, minWidth: 120, filterable: false, @@ -277,7 +278,7 @@ export const loadColumns = async ( { field: 'approved_at', - headerName: 'ApprovedAt', + headerName: 'Approved at', flex: 1, minWidth: 120, filterable: false, diff --git a/frontend/src/components/Approval_steps/TableApproval_steps.tsx b/frontend/src/components/Approval_steps/TableApproval_steps.tsx index 09793df..2eae8e1 100644 --- a/frontend/src/components/Approval_steps/TableApproval_steps.tsx +++ b/frontend/src/components/Approval_steps/TableApproval_steps.tsx @@ -42,9 +42,6 @@ const TableSampleApproval_steps = ({ filterItems, setFilterItems, filters, showG const { approval_steps, loading, count, notify: approval_stepsNotify, refetch } = useAppSelector((state) => state.approval_steps) const { currentUser } = useAppSelector((state) => state.auth); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const corners = useAppSelector((state) => state.style.corners); const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); for (let i = 0; i < numPages; i++) { pagesList.push(i); @@ -202,9 +199,7 @@ const TableSampleApproval_steps = ({ filterItems, setFilterItems, filters, showG }; const controlClasses = - 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + - ` ${bgColor} ${focusRing} ${corners} ` + - 'dark:bg-slate-800 border'; + 'w-full my-1.5 rounded-lg border border-[#D8D0C2] bg-white px-3 py-2 text-sm text-[#0E1A2B] shadow-sm outline-none placeholder:text-[#9A8F7F] focus:border-[#D8B75E] focus:ring-2 focus:ring-[#D8B75E]/25 dark:border-dark-700 dark:bg-slate-800 dark:placeholder-gray-400'; const dataGrid = ( @@ -262,7 +257,7 @@ const TableSampleApproval_steps = ({ filterItems, setFilterItems, filters, showG return ( <> {filterItems && Array.isArray( filterItems ) && filterItems.length ? - + <> +
+
+

Active filters

+

Narrow the register without leaving the governance workspace.

+
+ Review scope +
{filterItems && filterItems.map((filterItem) => { return ( -
-
-
Filter
+
+
+
Filter
filter.title === filterItem?.fields?.selectedField )?.type === 'enum' ? ( -
-
+
+
Value
filter.title === filterItem?.fields?.selectedField )?.number ? ( -
-
-
From
+
+
+
From
-
To
+
To
-
-
+
+
+
From
-
To
+
To
) : ( -
-
Contains
+
+
Contains
)}
-
Action
+
Action
) })} -
+
diff --git a/frontend/src/components/Approval_steps/configureApproval_stepsCols.tsx b/frontend/src/components/Approval_steps/configureApproval_stepsCols.tsx index 836689b..20f222d 100644 --- a/frontend/src/components/Approval_steps/configureApproval_stepsCols.tsx +++ b/frontend/src/components/Approval_steps/configureApproval_stepsCols.tsx @@ -43,7 +43,7 @@ export const loadColumns = async ( { field: 'use_case', - headerName: 'AIUseCase', + headerName: 'AI Use Case', flex: 1, minWidth: 120, filterable: false, @@ -65,7 +65,7 @@ export const loadColumns = async ( { field: 'step_type', - headerName: 'StepType', + headerName: 'Step Type', flex: 1, minWidth: 120, filterable: false, @@ -80,7 +80,7 @@ export const loadColumns = async ( { field: 'assigned_reviewer', - headerName: 'AssignedReviewer', + headerName: 'Assigned Reviewer', flex: 1, minWidth: 120, filterable: false, @@ -132,7 +132,7 @@ export const loadColumns = async ( { field: 'assigned_at', - headerName: 'AssignedAt', + headerName: 'Assigned At', flex: 1, minWidth: 120, filterable: false, @@ -150,7 +150,7 @@ export const loadColumns = async ( { field: 'decided_at', - headerName: 'DecidedAt', + headerName: 'Decided At', flex: 1, minWidth: 120, filterable: false, @@ -168,7 +168,7 @@ export const loadColumns = async ( { field: 'step_order', - headerName: 'StepOrder', + headerName: 'Step Order', flex: 1, minWidth: 120, filterable: false, diff --git a/frontend/src/components/AsideMenu.tsx b/frontend/src/components/AsideMenu.tsx index 442dfac..d6e9595 100644 --- a/frontend/src/components/AsideMenu.tsx +++ b/frontend/src/components/AsideMenu.tsx @@ -19,7 +19,7 @@ export default function AsideMenu({ <> { const [isLinkActive, setIsLinkActive] = useState(false) - const [isDropdownActive, setIsDropdownActive] = useState(false) + const [isDropdownActive, setIsDropdownActive] = useState(Boolean(item.isOpenByDefault)) const asideMenuItemStyle = useAppSelector((state) => state.style.asideMenuItemStyle) - const asideMenuDropdownStyle = useAppSelector((state) => state.style.asideMenuDropdownStyle) const asideMenuItemActiveStyle = useAppSelector((state) => state.style.asideMenuItemActiveStyle) const borders = useAppSelector((state) => state.style.borders); const activeLinkColor = useAppSelector( @@ -38,17 +37,33 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { setIsLinkActive(linkPathNameView === activeView); } + + if (item.menu && isReady) { + const activePathname = new URL(asPath, location.href).pathname + const activeView = activePathname.split('/')[1]; + const hasActiveChild = item.menu.some((menuItem) => { + if (!menuItem.href) { + return false; + } + + const childPathName = new URL(menuItem.href, location.href).pathname + '/'; + const childView = childPathName.split('/')[1]; + return childView === activeView; + }); + + if (hasActiveChild) { + setIsDropdownActive(true); + } + } }, [item.href, isReady, asPath]) const asideMenuItemInnerContents = ( <> {item.icon && ( - + )} {item.label} @@ -56,25 +71,25 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { )} ) const componentClass = [ - 'flex cursor-pointer py-1.5 ', - isDropdownList ? 'px-6 text-sm' : '', + 'flex min-w-0 cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 transition-colors', + isDropdownList ? 'text-sm' : 'text-[15px] font-medium', item.color ? getButtonColor(item.color, false, true) : `${asideMenuItemStyle}`, isLinkActive - ? `text-black ${activeLinkColor} dark:text-white dark:bg-dark-800` + ? `text-[#0E1A2B] ${activeLinkColor} dark:text-white dark:bg-dark-800` : '', ].join(' '); return ( -
  • +
  • {item.withDevider &&
    } {item.href && ( @@ -89,8 +104,8 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { {item.menu && ( diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index c78bf73..9988818 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -29,19 +29,21 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props return (
  • - ))} - {!loading && integrations.length === 0 && ( -
    -

    No data to display

    -
    - )} +
    + + ); + })} -
    + + {!loading && integrations.length === 0 && ( +
    +

    No integrations to display

    +
    + )} + +
    diff --git a/frontend/src/components/KanbanBoard/KanbanCard.tsx b/frontend/src/components/KanbanBoard/KanbanCard.tsx index 7655572..6474260 100644 --- a/frontend/src/components/KanbanBoard/KanbanCard.tsx +++ b/frontend/src/components/KanbanBoard/KanbanCard.tsx @@ -12,6 +12,8 @@ type Props = { setItemIdToDelete: (id: string) => void; }; +const formatValue = (value: string) => value.replace(/_/g, ' '); + const KanbanCard = ({ item, entityName, @@ -30,23 +32,46 @@ const KanbanCard = ({ [item], ); + const statusValue = + item.risk_level || + item.approval_status || + item.assessment_status || + item.vendor_status || + item.decision; + const summary = + item.business_goal || + item.security_posture_summary || + item.findings || + item.comments || + item.notes; + return (
    {item[showFieldName] ?? 'No data'} + {statusValue && ( + + {formatValue(statusValue)} + + )}
    + {summary && ( +

    + {summary} +

    + )}
    -

    {moment(item.createdAt).format('MMM DD hh:mm a')}

    +

    {moment(item.createdAt).format('MMM DD hh:mm a')}

    { + const displayLabel = column.label + .replace(/_/g, ' ') + .replace(/\b\w/g, (letter) => letter.toUpperCase()); const [currentPage, setCurrentPage] = useState(0); const [count, setCount] = useState(0); const [data, setData] = useState(null); @@ -161,19 +164,24 @@ const KanbanColumn = ({ -
    -

    {column.label}

    -

    {count}

    +
    +
    +

    Review lane

    +

    {displayLabel}

    +
    +

    + {count} +

    { drop(node); listInnerRef.current = node; }} - className={'p-3 space-y-3 flex-1 overflow-y-auto max-h-[400px]'} + className={'flex-1 space-y-3 overflow-y-auto p-3 max-h-[560px]'} onScroll={onScroll} > {data?.map((item) => ( @@ -188,7 +196,7 @@ const KanbanColumn = ({
    ))} {!data?.length && ( -

    No data

    +

    No records in this lane

    )}
    diff --git a/frontend/src/components/LegalOpsPageIntro.tsx b/frontend/src/components/LegalOpsPageIntro.tsx new file mode 100644 index 0000000..0b1c3cc --- /dev/null +++ b/frontend/src/components/LegalOpsPageIntro.tsx @@ -0,0 +1,48 @@ +import React, { ReactNode } from 'react' + +type PageMetric = { + label: string + value: string + helper: string +} + +type Props = { + eyebrow: string + title: string + description: string + metrics: PageMetric[] + children?: ReactNode +} + +export default function LegalOpsPageIntro({ + eyebrow, + title, + description, + metrics, + children, +}: Props) { + return ( +
    +
    +
    +

    {eyebrow}

    +

    {title}

    +

    {description}

    +
    + {children &&
    {children}
    } +
    +
    + {metrics.map((metric) => ( +
    +

    {metric.label}

    +

    {metric.value}

    +

    {metric.helper}

    +
    + ))} +
    +
    + ) +} diff --git a/frontend/src/components/Matter_types/CardMatter_types.tsx b/frontend/src/components/Matter_types/CardMatter_types.tsx index 2556978..eb223ad 100644 --- a/frontend/src/components/Matter_types/CardMatter_types.tsx +++ b/frontend/src/components/Matter_types/CardMatter_types.tsx @@ -1,135 +1,3 @@ -import React from 'react'; -import ImageField from '../ImageField'; -import ListActionsPopover from '../ListActionsPopover'; -import { useAppSelector } from '../../stores/hooks'; -import dataFormatter from '../../helpers/dataFormatter'; -import { Pagination } from '../Pagination'; -import {saveFile} from "../../helpers/fileSaver"; -import LoadingSpinner from "../LoadingSpinner"; -import Link from 'next/link'; +import ListMatter_types from './ListMatter_types'; -import {hasPermission} from "../../helpers/userPermissions"; - - -type Props = { - matter_types: any[]; - loading: boolean; - onDelete: (id: string) => void; - currentPage: number; - numPages: number; - onPageChange: (page: number) => void; -}; - -const CardMatter_types = ({ - matter_types, - loading, - onDelete, - currentPage, - numPages, - onPageChange, -}: Props) => { - const asideScrollbarsStyle = useAppSelector( - (state) => state.style.asideScrollbarsStyle, - ); - const bgColor = useAppSelector((state) => state.style.cardsColor); - const darkMode = useAppSelector((state) => state.style.darkMode); - const corners = useAppSelector((state) => state.style.corners); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - - const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_MATTER_TYPES') - - - return ( -
    - {loading && } -
      - {!loading && matter_types.map((item, index) => ( -
    • - -
      - - - {item.name} - - - -
      - -
      -
      -
      - - -
      -
      MatterType
      -
      -
      - { item.name } -
      -
      -
      - - - - -
      -
      Description
      -
      -
      - { item.description } -
      -
      -
      - - - - -
      -
      Active
      -
      -
      - { dataFormatter.booleanFormatter(item.is_active) } -
      -
      -
      - - - -
      -
    • - ))} - {!loading && matter_types.length === 0 && ( -
      -

      No data to display

      -
      - )} -
    -
    - -
    -
    - ); -}; - -export default CardMatter_types; +export default ListMatter_types; diff --git a/frontend/src/components/Matter_types/ListMatter_types.tsx b/frontend/src/components/Matter_types/ListMatter_types.tsx index 2f09826..3d316e6 100644 --- a/frontend/src/components/Matter_types/ListMatter_types.tsx +++ b/frontend/src/components/Matter_types/ListMatter_types.tsx @@ -1,16 +1,10 @@ import React from 'react'; -import CardBox from '../CardBox'; -import ImageField from '../ImageField'; -import dataFormatter from '../../helpers/dataFormatter'; -import {saveFile} from "../../helpers/fileSaver"; -import ListActionsPopover from "../ListActionsPopover"; -import {useAppSelector} from "../../stores/hooks"; -import {Pagination} from "../Pagination"; -import LoadingSpinner from "../LoadingSpinner"; import Link from 'next/link'; - -import {hasPermission} from "../../helpers/userPermissions"; - +import ListActionsPopover from "../ListActionsPopover"; +import { useAppSelector } from "../../stores/hooks"; +import { Pagination } from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import { hasPermission } from "../../helpers/userPermissions"; type Props = { matter_types: any[]; @@ -21,84 +15,111 @@ type Props = { onPageChange: (page: number) => void; }; -const ListMatter_types = ({ matter_types, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { - - const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_MATTER_TYPES') - - const corners = useAppSelector((state) => state.style.corners); - const bgColor = useAppSelector((state) => state.style.cardsColor); +const getInitials = (name?: string) => { + if (!name) { + return 'MT'; + } + return name + .split(' ') + .map((part) => part[0]) + .join('') + .slice(0, 2) + .toUpperCase(); +}; + +const ListMatter_types = ({ matter_types, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_MATTER_TYPES'); return ( <> -
    - {loading && } +
    + {loading && ( +
    + +
    + )} + {!loading && matter_types.map((item) => ( -
    - -
    - - dark:divide-dark-700 overflow-x-auto' - } - > - - -
    -

    MatterType

    -

    { item.name }

    +
    +
    +
    +
    + {getInitials(item.name)} +
    +
    +

    + Matter type +

    + + {item.name || 'Unnamed matter type'} + +
    - - - - -
    -

    Description

    -

    { item.description }

    -
    - - - - -
    -

    Active

    -

    { dataFormatter.booleanFormatter(item.is_active) }

    -
    - - - -
    - -
    + +
    +
    + + {item.is_active ? 'Active' : 'Inactive'} + + + Intake context + +
    + +

    + {item.description || 'No matter-type description yet.'} +

    + +
    +

    + Governance use +

    +

    + Helps classify AI use cases by legal matter context before risk review and approval. +

    +
    +
    +
    ))} + {!loading && matter_types.length === 0 && ( -
    -

    No data to display

    -
    +
    +

    No matter types to display

    +
    )}
    -
    + +
    ) }; -export default ListMatter_types \ No newline at end of file +export default ListMatter_types diff --git a/frontend/src/components/Matter_types/TableMatter_types.tsx b/frontend/src/components/Matter_types/TableMatter_types.tsx index 55447dd..bb9d441 100644 --- a/frontend/src/components/Matter_types/TableMatter_types.tsx +++ b/frontend/src/components/Matter_types/TableMatter_types.tsx @@ -44,9 +44,6 @@ const TableSampleMatter_types = ({ filterItems, setFilterItems, filters, showGri const { matter_types, loading, count, notify: matter_typesNotify, refetch } = useAppSelector((state) => state.matter_types) const { currentUser } = useAppSelector((state) => state.auth); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const corners = useAppSelector((state) => state.style.corners); const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); for (let i = 0; i < numPages; i++) { pagesList.push(i); @@ -204,9 +201,7 @@ const TableSampleMatter_types = ({ filterItems, setFilterItems, filters, showGri }; const controlClasses = - 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + - ` ${bgColor} ${focusRing} ${corners} ` + - 'dark:bg-slate-800 border'; + 'w-full my-1.5 rounded-lg border border-[#D8D0C2] bg-white px-3 py-2 text-sm text-[#0E1A2B] shadow-sm outline-none placeholder:text-[#9A8F7F] focus:border-[#D8B75E] focus:ring-2 focus:ring-[#D8B75E]/25 dark:border-dark-700 dark:bg-slate-800 dark:placeholder-gray-400'; const dataGrid = ( @@ -264,7 +259,7 @@ const TableSampleMatter_types = ({ filterItems, setFilterItems, filters, showGri return ( <> {filterItems && Array.isArray( filterItems ) && filterItems.length ? - + <> +
    +
    +

    Active filters

    +

    Narrow matter types by name or description.

    +
    + Matter scope +
    {filterItems && filterItems.map((filterItem) => { return ( -
    -
    -
    Filter
    +
    +
    +
    Filter
    filter.title === filterItem?.fields?.selectedField )?.type === 'enum' ? ( -
    -
    +
    +
    Value
    filter.title === filterItem?.fields?.selectedField )?.number ? ( -
    -
    -
    From
    +
    +
    +
    From
    -
    To
    +
    To
    -
    -
    +
    +
    +
    From
    -
    To
    +
    To
    ) : ( -
    -
    Contains
    +
    +
    Contains
    )}
    -
    Action
    +
    Action
    ) })} -
    +
    diff --git a/frontend/src/components/Matter_types/configureMatter_typesCols.tsx b/frontend/src/components/Matter_types/configureMatter_typesCols.tsx index f97aad6..fb0cff1 100644 --- a/frontend/src/components/Matter_types/configureMatter_typesCols.tsx +++ b/frontend/src/components/Matter_types/configureMatter_typesCols.tsx @@ -43,7 +43,7 @@ export const loadColumns = async ( { field: 'name', - headerName: 'MatterType', + headerName: 'Matter type', flex: 1, minWidth: 120, filterable: false, diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index c270ae0..89b204e 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -5,7 +5,6 @@ import BaseIcon from './BaseIcon' import NavBarItemPlain from './NavBarItemPlain' import NavBarMenuList from './NavBarMenuList' import { MenuNavBarItem } from '../interfaces' -import { useAppSelector } from '../stores/hooks'; type Props = { menu: MenuNavBarItem[] @@ -16,7 +15,6 @@ type Props = { export default function NavBar({ menu, className = '', children }: Props) { const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false) const [isScrolled, setIsScrolled] = useState(false); - const bgColor = useAppSelector((state) => state.style.bgLayoutColor); useEffect(() => { const handleScroll = () => { @@ -35,9 +33,9 @@ export default function NavBar({ menu, className = '', children }: Props) { return (