Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/atoms/Icons/MapIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Base, BaseProps } from './Base'

export function MapIndicator(props: BaseProps): JSX.Element {
return (
<Base {...props} w="16px" h="16px" viewBox="0 0 16 16" title="mapIndicator">
<path
fill="currentColor"
fillRule="nonzero"
d="M7 0C3.1 0 0 3.1 0 7c0 1.9.7 3.7 2.1 5 .1.1 4.1 3.7 4.2 3.8.4.3 1 .3 1.3 0 .1-.1 4.2-3.7 4.2-3.8 1.4-1.3 2.1-3.1 2.1-5 .1-3.9-3-7-6.9-7zm0 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"
/>
</Base>
)
}

MapIndicator.displayName = 'MapIndicator'
3 changes: 2 additions & 1 deletion src/atoms/Icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ export * from './CircularInformation'
export * from './Download'
export * from './GoAhead'
export * from './GoBack'
export * from './Multimedia'
export * from './Loader'
export * from './MapIndicator'
export * from './Multimedia'
export * from './Profile'
export * from './Remote'
export * from './Schedule'
Expand Down
24 changes: 14 additions & 10 deletions src/documentation/pages/Organisms/CalendarDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ interface ICalendarDropdown {

<MyTitle>onClickEvent</MyTitle>
<MyText>
El prop <strong>onClickEvent</strong> es opcional. Si se envía, se ejecuta al presionar
cualquiera de los eventos del dropdown y entrega hacia arriba la información completa del
evento seleccionado.
El prop <strong>onClickEvent</strong> es opcional. Si se envía, se ejecuta al presionar un
evento del dropdown siempre que ese evento también tenga <strong>url</strong>, y entrega
hacia arriba la información completa del evento seleccionado. Los eventos sin URL se
muestran como no disponibles y no ejecutan el callback.
</MyText>
<Code
text={`
Expand Down Expand Up @@ -100,6 +101,8 @@ interface ICalendarDropdown {
start: '2025-05-28T04:01:00.000Z',
end: '2025-05-28T05:01:00.000Z',
type: 'work-release',
url: '/demo',
headquarters_address: null,
translatedTitle: 'Inicio del plazo para realizar "Caso 2: Evaluado (con grupo de notas y sin rúbrica)"',
associated_resource: {
id: 1,
Expand Down Expand Up @@ -193,7 +196,7 @@ const events = [
name: 'Inicio del plazo para realizar "Caso 2: Evaluado (con grupo de notas y sin rúbrica)"',
description: 'Unidad 2',
},
url: null,
url: '/demo',
course: {
id: 40711,
name: '[Pruebas TI] - Herramientas para la Gestión Estratégica de Procesoss',
Expand Down Expand Up @@ -250,7 +253,7 @@ const events = [
name: 'CalendarEventDeadlineAnswer "Control 3"',
description: 'Unidad 3',
},
url: null,
url: '/demo',
course: {
id: 40711,
name: '[Pruebas TI] - Herramientas para la Gestión Estratégica de Procesoss',
Expand All @@ -271,14 +274,15 @@ const events = [
start: '2027-01-01T02:59:00.000Z',
end: '2027-01-01T03:59:00.000Z',
duration_in_minutes: 60,
type: 'cv-events',
type: 'cpr',
id_resource: 853909,
associated_resource: {
id: 1,
name: 'CalendarEventDeadlineAnswer "Con intentos y tiempo"',
description: 'Clase con Pruebas de Prueba',
},
url: null,
url: '/demo',
headquarters_address: 'Av. Apoquindo 12345, Santiago',
course: {
id: 22568,
name: 'Pruebas contenido v8',
Expand Down Expand Up @@ -306,7 +310,7 @@ const events = [
name: 'CalendarEventDeadlineAnswer "Pregunta tipo archivo toma 2"',
description: 'Clase con Pruebas de Prueba',
},
url: null,
url: '/demo',
course: {
id: 22568,
name: 'Pruebas contenido v8',
Expand Down Expand Up @@ -334,7 +338,7 @@ const events = [
name: 'CalendarEventDeadlineAnswer "Prueba pregunta text con tiempo"',
description: 'Unit 1',
},
url: null,
url: '/demo',
course: {
id: 22568,
name: 'Pruebas contenido v8',
Expand Down Expand Up @@ -362,7 +366,7 @@ const events = [
name: 'CalendarEventDeadlineAnswer "Control refactor de la alerta"',
description: 'Clase con Pruebas de Prueba',
},
url: null,
url: '/demo',
course: {
id: 22568,
name: 'Pruebas contenido v8',
Expand Down
35 changes: 31 additions & 4 deletions src/documentation/pages/Organisms/EventsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export const EventsListPage = (): JSX.Element => {
Dentro del menú dropdown y las modales que se abren en la vista calendario, se muestra el
detalle del nombre del curso al que pertenece el evento.
<br />
Los eventos de tipo <strong>cpr</strong> muestran la dirección de la sede recibida en{' '}
<strong>headquartersAddress</strong>. Si este dato no llega, no se muestra ni el texto de
sede ni el ícono de ubicación.
<br />
Para esto, el componente debe recibir los props{' '}
<ul style={{ listStylePosition: 'inside' }}>
<li>
Expand All @@ -68,6 +72,12 @@ export const EventsListPage = (): JSX.Element => {
</ul>
</MyText>

<MyText>
El evento solo se comporta como clickeable cuando recibe tanto <strong>onClick</strong> como{' '}
<strong>url</strong>. Si <strong>url</strong> llega vacío o nulo, se muestra el estado{' '}
<strong>Aún no disponible</strong> y se deshabilita la interacción.
</MyText>

<Box border={`1px solid ${vars('colors-neutral-platinum')}`} borderTop="none">
<EventsList
color="#00857A"
Expand All @@ -82,6 +92,20 @@ export const EventsListPage = (): JSX.Element => {
courseName="[Pruebas TI] - Herramientas para la Gestión Estratégica de Procesos"
duration={40}
/>
<EventsList
color="#00857A"
key="2"
name='Clase presencial "Taller de caso"'
day="sábado"
date="01 jul"
time="10:00 hrs."
text="Curso"
type="cpr"
showCourse
courseName="[Pruebas TI] - Herramientas para la Gestión Estratégica de Procesos"
duration={120}
headquartersAddress="Sede Apoquindo"
/>
</Box>

<MyTitle>3. Eventos en vista curso</MyTitle>
Expand Down Expand Up @@ -121,20 +145,23 @@ export const EventsListPage = (): JSX.Element => {
<Code
text={`
interface IEventList {
color?: string // Color del curso asociado
day: string // Día de la semana
date: string // Fecha
duration_in_minutes?: number // Duración del evento en minutos
color?: string // Color del curso asociado
day: string // Día de la semana
date: string // Fecha
duration?: number // Duración del evento en minutos
time: string // Hora
name: string // Nombre del evento
hasNotification?: boolean // Indica si el evento tiene notificación
headquartersAddress?: string | null // Dirección de la sede para eventos CPR
onClick?: () => void // Permite usar el item como elemento clickeable
showCourse?: boolean // Indica si se muestra el curso
showUnit?: boolean // Indica si se muestra la unidad
courseName?: string // Nombre del curso
unitName?: string // Nombre de la unidad
text: string // "Curso"
type: string // Identificador del tipo de evento
unavailableLabel?: string // Texto del estado no disponible
url?: string | null // URL requerida junto a onClick para habilitar interacción
}
`}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { ChakraProvider, Menu, MenuList } from '@chakra-ui/react'
import { render, RenderResult, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

import { Event } from '../../types'
import { EventsGroup } from './EventsGroup'

const baseEvent: Event = {
id: 1,
associated_resource: {
id: 1,
name: 'Evaluación 2',
},
course: {
id: 1,
name: 'Economía',
},
course_id: 1,
end: '2026-07-27T03:59:00.000Z',
start: '2026-07-27T02:59:00.000Z',
formatedDate: {
day: 'viernes',
date: '26 julio',
time: '23:59 hrs.',
},
translatedTitle: 'Se habilita para responder “Evaluación 2”',
type: 'evaluation-release',
url: null,
}

const renderComponent = (event: Event, onClickEvent = jest.fn()): RenderResult =>
render(
<ChakraProvider>
<Menu isOpen>
<MenuList>
<EventsGroup events={[event]} onClickEvent={onClickEvent} text="Curso" title="Próximos" />
</MenuList>
</Menu>
</ChakraProvider>
)

describe('EventsGroup', () => {
it('renders unavailable event label and does not call onClickEvent when url is empty', async () => {
const user = userEvent.setup()
const onClickEvent = jest.fn()

renderComponent(baseEvent, onClickEvent)

expect(screen.getByText('Aún no disponible')).toBeInTheDocument()

await user.click(screen.getByText('Se habilita para responder “Evaluación 2”'))

expect(onClickEvent).not.toHaveBeenCalled()
})

it('calls onClickEvent when event has url', async () => {
const user = userEvent.setup()
const onClickEvent = jest.fn()
const event = { ...baseEvent, url: 'https://example.com/evaluation-2' }

renderComponent(event, onClickEvent)

await user.click(screen.getByText('Se habilita para responder “Evaluación 2”'))

expect(onClickEvent).toHaveBeenCalledWith(event)
})

it('renders headquarters address for cpr events', () => {
const event = {
...baseEvent,
duration_in_minutes: 40,
headquarters_address: 'Sede Apoquindo',
type: 'cpr',
url: 'https://example.com/cpr',
}

renderComponent(event)

expect(screen.getByText('Sede Apoquindo')).toBeInTheDocument()
expect(screen.queryByText('Link clase online')).not.toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,20 @@ export const EventsGroup = ({
>
<MenuGroup title={title}>
{events.map((event: Event) => {
const hasUrl = Boolean(event.url)
const eventOnClick = onClickEvent && hasUrl ? () => onClickEvent(event) : undefined

return (
// Una vez que el evento se comporte como link, se debe cambiar Box a MenuItem y aplicar el efecto de focus
<Box
bg={vars('colors-neutral-white') ?? '#FFFFFF'}
border="none"
cursor={onClickEvent ? 'pointer' : 'default'}
cursor={eventOnClick ? 'pointer' : 'default'}
padding="0"
key={event.id}
_hover={{
boxShadow: 'none !important',
cursor: onClickEvent ? 'pointer !important' : 'default !important',
cursor: eventOnClick ? 'pointer !important' : 'default !important',
bg: 'none !important',
}}
_focus={{
Expand All @@ -79,8 +82,10 @@ export const EventsGroup = ({
text={text}
type={event.type}
hasNotification={event.isNew}
onClick={onClickEvent ? () => onClickEvent(event) : undefined}
headquartersAddress={event.headquarters_address}
onClick={eventOnClick}
showCourse
url={event.url ?? ''}
/>
</Box>
)
Expand Down
2 changes: 2 additions & 0 deletions src/organisms/Calendar/Dropdown/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ export interface Event {
end: string
start: string
formatedDate: FormattedDate
headquarters_address?: string | null
id: number
isNew?: boolean
translatedTitle: string
type: string
url?: string | null
}

export type Events = Event[]
Expand Down
49 changes: 48 additions & 1 deletion src/organisms/Calendar/EventsList/EventsList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,73 @@ describe('EventsList', () => {
const onClick = jest.fn()
const user = userEvent.setup()

renderComponent(onClick)
renderComponent(onClick, { url: '/demo' })

await user.click(screen.getByText('Evento demo'))

expect(onClick).toHaveBeenCalledTimes(1)
})

it('does not call onClick when url is not provided', async () => {
const onClick = jest.fn()
const user = userEvent.setup()

renderComponent(onClick)

await user.click(screen.getByText('Evento demo'))

expect(onClick).not.toHaveBeenCalled()
})

it('renders the item when onClick is not provided', () => {
renderComponent()

expect(screen.getByText('Evento demo')).toBeInTheDocument()
})

it('renders unavailable label and does not call onClick when url is empty', async () => {
const onClick = jest.fn()
const user = userEvent.setup()

renderComponent(onClick, { url: '' })

expect(screen.getByText('Aún no disponible')).toBeInTheDocument()

await user.click(screen.getByText('Evento demo'))

expect(onClick).not.toHaveBeenCalled()
})

it('renders duration for online or in-person events with positive minutes', () => {
const { container } = renderComponent(undefined, { duration: 40 })

expect(screen.getByText('Link clase online')).toBeInTheDocument()
expect(container).toHaveTextContent(/40\s*min/)
})

it('renders headquarters address instead of online class link for cpr events', () => {
const { container } = renderComponent(undefined, {
duration: 40,
headquartersAddress: 'Sede Apoquindo',
type: 'cpr',
})

expect(screen.getByText('Sede Apoquindo')).toBeInTheDocument()
expect(screen.queryByText('Link clase online')).not.toBeInTheDocument()
expect(container).toHaveTextContent(/40\s*min/)
})

it('does not render location text when cpr event has no headquarters address', () => {
const { container } = renderComponent(undefined, {
duration: 40,
headquartersAddress: null,
type: 'cpr',
})

expect(screen.queryByText('Link clase online')).not.toBeInTheDocument()
expect(container).toHaveTextContent(/40\s*min/)
})

it('does not render duration when minutes are zero', () => {
renderComponent(undefined, { duration: 0 })

Expand Down
Loading
Loading