diff --git a/src/apps/admin/src/ai/review-templates/AiReviewTemplatesPage.module.scss b/src/apps/admin/src/ai/review-templates/AiReviewTemplatesPage.module.scss index 7a2cb919b..286cb8362 100644 --- a/src/apps/admin/src/ai/review-templates/AiReviewTemplatesPage.module.scss +++ b/src/apps/admin/src/ai/review-templates/AiReviewTemplatesPage.module.scss @@ -224,6 +224,26 @@ border-radius: 16px; } +.toggleWrapper { + display: flex; + align-items: center; + gap: $sp-2; + cursor: pointer; + padding: 4px $sp-2; + border-radius: $sp-1; + transition: background-color 0.2s ease; + + &:hover { + background-color: rgba($black-100, 0.05); + } +} + +.toggleLabel { + font-size: 12px; + font-weight: 500; + color: $black-60; +} + .workflowsTree { padding: $sp-3 $sp-4; background-color: $tc-white; diff --git a/src/apps/admin/src/ai/review-templates/AiReviewTemplatesPage.tsx b/src/apps/admin/src/ai/review-templates/AiReviewTemplatesPage.tsx index 5159a083b..101736764 100644 --- a/src/apps/admin/src/ai/review-templates/AiReviewTemplatesPage.tsx +++ b/src/apps/admin/src/ai/review-templates/AiReviewTemplatesPage.tsx @@ -1,8 +1,10 @@ import { ChangeEvent, FC, useCallback, useEffect, useMemo, useState } from 'react' +import { toast } from 'react-toastify' +import _ from 'lodash' -import { BaseModal, Button, IconOutline, InputSelect, InputSelectOption } from '~/libs/ui' +import { BaseModal, Button, FormToggleSwitch, IconOutline, InputSelect, InputSelectOption } from '~/libs/ui' -import { PageWrapper, TableLoading, TableNoRecord } from '../../lib' +import { ConfirmModal, PageWrapper, TableLoading, TableNoRecord } from '../../lib' import { TableWrapper } from '../../lib/components/common/TableWrapper' import { ChallengeTrack, ChallengeType } from '../../lib/models' import { getChallengeTracks, getChallengeTypes } from '../../lib/services/challenge-management.service' @@ -13,6 +15,7 @@ import { deleteAiReviewTemplate, getAiReviewTemplates, TemplateWorkflowItem, + updateAiReviewTemplate, } from '../../lib/services/ai-templates.service' import { WorkflowDetailsModal } from '../review-workflows/WorkflowDetailsModal' @@ -67,6 +70,7 @@ interface TemplateItemProps { onWorkflowClick: (workflow: AiWorkflow) => void onEdit: (template: AiReviewTemplate) => void onDelete: (template: AiReviewTemplate) => void + onToggleDisabled: (template: AiReviewTemplate) => void } const TemplateItem: FC = (props: TemplateItemProps) => { @@ -87,6 +91,11 @@ const TemplateItem: FC = (props: TemplateItemProps) => { props.onDelete(props.template) }, [props]) + const handleToggleClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation() + props.onToggleDisabled(props.template) + }, [props]) + return (
= (props: TemplateItemProps) => {
- {props.template.disabled && ( - Disabled - )} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, + jsx-a11y/no-static-element-interactions */} +
+ Active + +
@@ -394,6 +445,26 @@ export const AiReviewTemplatesPage: FC = () => { {deleteModal.template?.title || deleteModal.template?.id} "? This action cannot be undone. + + +

+ Are you sure you want to + {' '} + {toggleModal.template?.disabled ? 'activate' : 'deactivate'} + {' '} + the template + {' '} + {toggleModal.template?.title || toggleModal.template?.id} + ? +

+
) } diff --git a/src/apps/admin/src/ai/review-templates/CreateTemplateModal.module.scss b/src/apps/admin/src/ai/review-templates/CreateTemplateModal.module.scss index 9b8bddf4a..dfb7fe6de 100644 --- a/src/apps/admin/src/ai/review-templates/CreateTemplateModal.module.scss +++ b/src/apps/admin/src/ai/review-templates/CreateTemplateModal.module.scss @@ -40,6 +40,7 @@ border: 1px solid $black-20; border-radius: $sp-2; padding: $sp-4; + overflow: visible; } .workflowsHeader { @@ -77,6 +78,11 @@ :global(.input-select-react__control) { min-height: 40px; } + + // Ensure dropdown menu appears above modal content + :global(.input-select-react__menu) { + z-index: 9999; + } } .weightInput { diff --git a/src/apps/admin/src/ai/review-templates/CreateTemplateModal.tsx b/src/apps/admin/src/ai/review-templates/CreateTemplateModal.tsx index 1f520c850..fe08ee4fe 100644 --- a/src/apps/admin/src/ai/review-templates/CreateTemplateModal.tsx +++ b/src/apps/admin/src/ai/review-templates/CreateTemplateModal.tsx @@ -184,18 +184,32 @@ export const CreateTemplateModal: FC = (props: Props) => { const append = fieldArrayResult.append const remove = fieldArrayResult.remove - const trackOptions: InputSelectOption[] = useMemo(() => [ - { label: 'Select track', value: '' }, - ...tracks.map(t => { + const trackOptions: InputSelectOption[] = useMemo(() => { + const seen = new Set() + const options: InputSelectOption[] = [{ label: 'Select track', value: '' }] + for (const t of tracks) { const trackValue: string = (t as ChallengeTrack & { track?: string }).track || t.name.toUpperCase() - return { label: t.name, value: trackValue } - }), - ], [tracks]) + if (!seen.has(trackValue)) { + seen.add(trackValue) + options.push({ label: t.name, value: trackValue }) + } + } + + return options + }, [tracks]) + + const typeOptions: InputSelectOption[] = useMemo(() => { + const seen = new Set() + const options: InputSelectOption[] = [{ label: 'Select type', value: '' }] + for (const t of types) { + if (!seen.has(t.name)) { + seen.add(t.name) + options.push({ label: t.name, value: t.name }) + } + } - const typeOptions: InputSelectOption[] = useMemo(() => [ - { label: 'Select type', value: '' }, - ...types.map(t => ({ label: t.name, value: t.abbreviation })), - ], [types]) + return options + }, [types]) const workflowOptions: InputSelectOption[] = useMemo(() => workflows .filter(w => !w.disabled) @@ -245,6 +259,16 @@ export const CreateTemplateModal: FC = (props: Props) => { append({ isGating: false, weightPercent: 100, workflowId: '' }) }, [append]) + const onError = useCallback((formErrors: typeof errors) => { + const workflowError = formErrors.workflows?.message + || formErrors.workflows?.root?.message + if (workflowError) { + toast.error(workflowError as string) + } else { + toast.error('Please fix the validation errors before submitting') + } + }, []) + const onSubmit = useCallback((data: FormValues) => { setIsSubmitting(true) @@ -312,7 +336,7 @@ export const CreateTemplateModal: FC = (props: Props) => { ) : (
= (props: Props) => {
- {errors.workflows && typeof errors.workflows.message === 'string' && ( -

{errors.workflows.message}

+ {(errors.workflows?.message || errors.workflows?.root?.message) && ( +

+ {(errors.workflows?.message || errors.workflows?.root?.message) as string} +

)} {fields.map((field, index) => ( diff --git a/src/apps/admin/src/ai/review-workflows/AiReviewWorkflowsPage.module.scss b/src/apps/admin/src/ai/review-workflows/AiReviewWorkflowsPage.module.scss index 56069c6c7..10418ab0e 100644 --- a/src/apps/admin/src/ai/review-workflows/AiReviewWorkflowsPage.module.scss +++ b/src/apps/admin/src/ai/review-workflows/AiReviewWorkflowsPage.module.scss @@ -15,17 +15,26 @@ tbody tr { td { - padding: $sp-2 $sp-4 !important; + padding: $sp-2 $sp-3 !important; border-radius: 0 !important; background-color: $tc-white !important; + vertical-align: middle; &:first-child { font-weight: 600; - font-size: 12px; + font-size: 11px; text-transform: uppercase; color: $black-60; white-space: nowrap; - width: 120px; + width: 100px; + min-width: 100px; + max-width: 100px; + } + + &:last-child { + word-break: break-all; + overflow-wrap: anywhere; + text-align: right; } } @@ -38,10 +47,26 @@ background-color: $black-5 !important; } } + + @media (max-width: 400px) { + tbody tr td { + padding: $sp-1 $sp-2 !important; + font-size: 13px; + + &:first-child { + font-size: 10px; + width: 85px; + min-width: 85px; + max-width: 85px; + } + } + } } .link { color: $link-blue-dark; + font-size: 14px; + font-weight: 400; text-decoration: none; cursor: pointer; @@ -55,6 +80,13 @@ text-overflow: ellipsis; white-space: nowrap; max-width: 250px; + + @media (max-width: 1050px) { + white-space: normal; + word-break: break-all; + overflow-wrap: anywhere; + max-width: none; + } } .nameCell { @@ -67,6 +99,8 @@ border: none; padding: 0; color: $link-blue-dark; + font-size: 14px; + font-weight: 400; text-decoration: none; cursor: pointer; text-align: left; @@ -76,6 +110,12 @@ &:hover { text-decoration: underline; } + + @media (max-width: 1050px) { + text-align: right; + display: block; + width: 100%; + } } .toggle { diff --git a/src/apps/admin/src/ai/review-workflows/AiReviewWorkflowsPage.tsx b/src/apps/admin/src/ai/review-workflows/AiReviewWorkflowsPage.tsx index 95ae5416b..4e50753f1 100644 --- a/src/apps/admin/src/ai/review-workflows/AiReviewWorkflowsPage.tsx +++ b/src/apps/admin/src/ai/review-workflows/AiReviewWorkflowsPage.tsx @@ -148,7 +148,9 @@ export const AiReviewWorkflowsPage: FC = () => { type: 'element', }, { + defaultSortDirection: 'asc', label: 'Scorecard', + propertyName: 'scorecard.name', renderer: (data: AiWorkflow) => { if (!data.scorecard?.id) { return {data.scorecard?.name || 'N/A'} diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.module.scss b/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.module.scss index 761a49e51..3c7200820 100644 --- a/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.module.scss +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.module.scss @@ -86,7 +86,7 @@ .sel { &:global(__menu-portal).sel:global(__menu-portal) { - z-index: 1001; + z-index: 10001; } &:global(__menu) { width: 100%; diff --git a/src/libs/ui/lib/components/table/table-functions/table.functions.ts b/src/libs/ui/lib/components/table/table-functions/table.functions.ts index 2bb6f161c..0e1e92880 100644 --- a/src/libs/ui/lib/components/table/table-functions/table.functions.ts +++ b/src/libs/ui/lib/components/table/table-functions/table.functions.ts @@ -1,3 +1,5 @@ +import _ from 'lodash' + import { Sort } from '~/apps/admin/src/platform/gamification-admin/src/game-lib' import { TableColumn } from '../table-column.model' @@ -47,24 +49,32 @@ export function getSorted( return direction === 'asc' ? a - b : b - a } + function getValue(obj: T, path: string): unknown { + return path.includes('.') ? _.get(obj, path) : obj[path] + } + if (sortColumn.type === 'money' || sortColumn.type === 'number' || sortColumn.type === 'numberElement') { return sortedData - .sort((a: T, b: T) => sortNumbers(+a[sort.fieldName], +b[sort.fieldName], sort.direction)) + .sort((a: T, b: T) => sortNumbers( + Number(getValue(a, sort.fieldName)), + Number(getValue(b, sort.fieldName)), + sort.direction, + )) } if (sortColumn.type === 'date') { return sortedData .sort((a: T, b: T) => { - const aDate = new Date(a[sort.fieldName]) - const bDate = new Date(b[sort.fieldName]) + const aDate = new Date(getValue(a, sort.fieldName) as string) + const bDate = new Date(getValue(b, sort.fieldName) as string) return sortNumbers(aDate.getTime(), bDate.getTime(), sort.direction) }) } return sortedData .sort((a: T, b: T) => { - const aField: unknown = a[sort.fieldName] - const bField: unknown = b[sort.fieldName] + const aField: unknown = getValue(a, sort.fieldName) + const bField: unknown = getValue(b, sort.fieldName) // Keep nullish values at the bottom for both sort directions. const aValue = String(aField ?? '')