Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
bffd7cc
Deleted translation using Weblate (Spanish)
mahvish-faridy Jul 9, 2026
280d135
Merge remote-tracking branch 'origin/main'
weblate Jul 9, 2026
892ba22
Merge pull request #4680 from chimple/weblate-main
bhanugummana Jul 14, 2026
3c1bad6
Merge branch 'dev' into guddu-main-SDE1
guddu-sutara Jul 14, 2026
7409ba2
Merge pull request #4720 from chimple/guddu-main-SDE1
bhanugummana Jul 14, 2026
d367eb7
fix/CHIM-1158-remove-duplicate-coordinator-routing-checks
Chandu-H-J Jul 14, 2026
42baf6e
fix/CHIM-1158-use-dashboard-path-constant-in-sidebar-routing
Chandu-H-J Jul 14, 2026
6acab4d
Merge pull request #4721 from chimple/fix/CHIM-1158-sidebar-routing
bhanugummana Jul 15, 2026
f56baeb
Fix campaign listing mobile browser zoom and assignment filtering
renukaj07 Jul 15, 2026
412167f
Update SqliteApi.ts
renukaj07 Jul 15, 2026
2607032
Merge pull request #4723 from chimple/renuka-main
bhanugummana Jul 15, 2026
9ea09e8
fix: Program Tab Filter Is Not Working
narayanangourav-sutara Jul 15, 2026
7f96f41
Merge pull request #4724 from chimple/main-ng-9
bhanugummana Jul 15, 2026
a50ed95
fix: Program Listing Filters Are Not Working
narayanangourav-sutara Jul 15, 2026
af2e115
Merge pull request #4725 from chimple/main-ng-9
bhanugummana Jul 15, 2026
10ca095
Restrict program creation access for program managers
renukaj07 Jul 16, 2026
ae7dc6d
Fixed campaign listing sorting
renukaj07 Jul 16, 2026
b5cdba9
Merge pull request #4726 from chimple/renuka-main
bhanugummana Jul 16, 2026
00d1ec5
fix: disable sorting for school performance column in ops school list
jagadish-commits Jul 16, 2026
7d62b1f
fix: Message Editing Remains Enabled After Campaign Cancellation.
narayanangourav-sutara Jul 16, 2026
fef7cb1
Include uniqueSubjects in campaign assignments
harshitha-kp07 Jul 16, 2026
c9dfd11
fixed the test cases
harshitha-kp07 Jul 16, 2026
d08fa7d
Merge pull request #4727 from chimple/jagadish-main-1181
vinaypanduga Jul 16, 2026
c318e6f
Merge pull request #4728 from chimple/main-ng-9
vinaypanduga Jul 16, 2026
ca8025f
Merge pull request #4729 from chimple/Hkp-main-july
vinaypanduga Jul 16, 2026
c156744
fix: Load grades dynamically based on selected schools
guddu-sutara Jul 16, 2026
f858b42
Merge pull request #4732 from chimple/guddu-main-SDE1
bhanugummana Jul 16, 2026
f4f6236
temporary fix
Chandu-H-J Jul 16, 2026
f3d87ef
fix/CHIM-1187 Prevent teacher dashboard redirect in school kids mode
Chandu-H-J Jul 16, 2026
03df74a
CHIM-1187 Preserve kids school flow from location picker
Chandu-H-J Jul 16, 2026
ac39df6
fix: Add Subjects to School button is missing for OPS user in Teacher…
narayanangourav-sutara Jul 16, 2026
d21ffd0
Disable Next while submitting on step 4
harshitha-kp07 Jul 16, 2026
127aa11
refactor: improve refresh token persistence and implement automatic s…
ALOK-TV Jul 16, 2026
aa72604
Merge pull request #4734 from chimple/main-ng-9
vinaypanduga Jul 16, 2026
52a49a0
Merge pull request #4735 from chimple/Hkp-main-july
vinaypanduga Jul 16, 2026
812b498
Merge pull request #4733 from chimple/chim-1187-selectmode-school-flow
bhanugummana Jul 16, 2026
1a5a8aa
Merge pull request #4736 from chimple/alok-main2
bhanugummana Jul 16, 2026
b99efc0
fix: fetching classes and class users separately, then aggregating st…
narayanangourav-sutara Jul 16, 2026
66665ff
CHIM-1187 Preserve school profile selection flow
Chandu-H-J Jul 16, 2026
9b13c45
Merge pull request #4737 from chimple/main-ng-9
vinaypanduga Jul 16, 2026
5f6ac91
Added API to get the schools related to USer
vinaypanduga Jul 16, 2026
a91ec87
Merge pull request #4738 from chimple/chim-1187-selectmode-school-flow
vinaypanduga Jul 16, 2026
1f52dc7
Merge pull request #4739 from chimple/vinay-main
bhanugummana Jul 16, 2026
be1d793
fix: The existing parent-phone method remains unchanged for the separ…
narayanangourav-sutara Jul 16, 2026
60f5567
Optimize campaign reach using batched school metrics queries
narayanangourav-sutara Jul 16, 2026
4616416
Merge pull request #4740 from chimple/main-ng-9
vinaypanduga Jul 16, 2026
5a051e8
fix: Campaign Message Tab Displays Incorrect Data for "Alternate Days…
narayanangourav-sutara Jul 16, 2026
ae282a5
fix: Campaign Message Tab Displays Incorrect Data for "Alternate Days…
narayanangourav-sutara Jul 17, 2026
d9df11b
Merge pull request #4741 from chimple/main-ng-9
vinaypanduga Jul 17, 2026
c77cd21
fix: Subject Dropdown Is Not Displaying All Subjects in campaign
narayanangourav-sutara Jul 17, 2026
4563d4e
Merge pull request #4742 from chimple/main-ng-9
bhanugummana Jul 17, 2026
e3a663e
fix: Once loaded, only campaign Grades and Subjects are shown.
narayanangourav-sutara Jul 17, 2026
6961a13
Merge pull request #4743 from chimple/main-ng-9
bhanugummana Jul 17, 2026
6d4a1d0
Merge branch 'main' into dev-ng-9
narayanangourav-sutara Jul 17, 2026
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
40 changes: 39 additions & 1 deletion src/components/ProfileMenu/ProfileMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import { Util } from '../../utility/util';
import { useAppSelector } from '../../redux/hooks';
import { MemoryRouter } from 'react-router-dom';
import {
CURRENT_MODE,
EVENTS,
MODES,
PAGES,
STICKER_BOOK_NOTIFICATION_DOT_ENABLED,
ENABLE_STICKER_BOOK,
} from '../../common/constants';
import { schoolUtil } from '../../utility/schoolUtil';

// --- Mocks ---

Expand All @@ -36,15 +39,22 @@ jest.mock('../../i18n', () => ({
}));

const mockPush = jest.fn();
const mockReplace = jest.fn();
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
useHistory: () => ({
push: mockPush,
replace: jest.fn(),
replace: mockReplace,
location: { pathname: '/' },
}),
}));

jest.mock('../../utility/schoolUtil', () => ({
schoolUtil: {
setCurrentClass: jest.fn(),
},
}));

// Mock useGbContext
jest.mock('../../growthbook/Growthbook', () => ({
useGbContext: () => ({ setGbUpdated: jest.fn() }),
Expand All @@ -60,10 +70,12 @@ const mockApi = {
};

const mockStudent = { id: 'student-123', name: 'Test Student' };
const mockSetCurrentClass = schoolUtil.setCurrentClass as jest.Mock;

describe('ProfileMenu Notification Logic', () => {
beforeEach(() => {
jest.clearAllMocks();
localStorage.clear();
mockApi.getUserStickerBook.mockReset();
mockApi.getUserStickerBook.mockResolvedValue([]);
mockApi.markStciekercolledasTrue.mockReset();
Expand Down Expand Up @@ -191,4 +203,30 @@ describe('ProfileMenu Notification Logic', () => {
expect.any(Object),
);
});

test('keeps school-mode switch profile on SelectMode selection flow', async () => {
localStorage.setItem(CURRENT_MODE, MODES.TEACHER_SCHOOL);

render(
<MemoryRouter>
<ProfileMenu onClose={jest.fn()} />
</MemoryRouter>,
);

await waitFor(() => expect(mockApi.getUserStickerBook).toHaveBeenCalled());

const switchProfileItem = screen
.getByText(/Switch Profile/i)
.closest('.profile-menu-item');
fireEvent.click(switchProfileItem!);

await waitFor(() => {
expect(Util.setCurrentStudent).toHaveBeenCalledWith(null);
});
expect(mockSetCurrentClass).not.toHaveBeenCalled();
expect(mockReplace).toHaveBeenCalledWith(PAGES.SELECT_MODE, {
from: '/',
fromSchoolModeSwitchProfile: true,
});
});
});
1 change: 1 addition & 0 deletions src/components/ProfileMenu/ProfileMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ const ProfileMenu = ({ onClose }: ProfileMenuProps) => {
setGbUpdated(true);
history.replace(PAGES.SELECT_MODE, {
from: history.location.pathname,
fromSchoolModeSwitchProfile: true,
});
};

Expand Down
3 changes: 2 additions & 1 deletion src/ops-console/components/DataTableBody.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
scrollbar-gutter: stable;
margin-bottom: 8px;
-webkit-overflow-scrolling: touch;
touch-action: pan-x pan-y;
overscroll-behavior: contain;
touch-action: pan-x pan-y pinch-zoom;
}

.data-tablebody-head-cell {
Expand Down
5 changes: 4 additions & 1 deletion src/ops-console/components/ProgramListTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type ProgramListTableProps = {
isExportDisabled: boolean;
isExporting: boolean;
onExport: () => void | Promise<void>;
canCreateProgram: boolean;
onNewProgram: () => void;
columns: Column<ProgramListRow>[];
rows: ProgramListRow[];
Expand Down Expand Up @@ -88,7 +89,9 @@ const ProgramListControls: React.FC<ProgramListTableProps> = (props) => (
isExporting={props.isExporting}
onClick={props.onExport}
/>
<ProgramActionsMenu onNewProgram={props.onNewProgram} />
{props.canCreateProgram ? (
<ProgramActionsMenu onNewProgram={props.onNewProgram} />
) : null}
<SchoolListDateRangeDropdown
value={props.selectedDateRange}
onChange={props.onDateRangeChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,36 @@ describe('CampaignMessages', () => {
).not.toBeInTheDocument();
});

it('revokes message editing immediately when the campaign is cancelled', async () => {
apiHandler.getCampaignMessaging.mockResolvedValue(
buildResponse([buildMessagingRow()]),
);

const { rerender } = render(
<CampaignMessages campaignId="campaign-1" isCampaignCancelled={false} />,
);

await screen.findByText('Class 1 Digital');
fireEvent.click(screen.getByLabelText('Edit global send schedule'));
expect(
screen.getByRole('button', { name: 'Save Changes' }),
).toBeInTheDocument();

rerender(<CampaignMessages campaignId="campaign-1" isCampaignCancelled />);

await waitFor(() =>
expect(
screen.queryByLabelText('Edit global send schedule'),
).not.toBeInTheDocument(),
);
await waitFor(() =>
expect(
screen.queryByRole('button', { name: 'Save Changes' }),
).not.toBeInTheDocument(),
);
expect(apiHandler.updateCampaignMessaging).not.toHaveBeenCalled();
});

it('closes the message time picker after selecting a time value', async () => {
apiHandler.getCampaignMessaging.mockResolvedValue(
buildResponse([buildMessagingRow()]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,31 @@ import {
useCampaignMessagesController,
} from './CampaignMessagesLogic';
import type { CampaignMessagesScheduleType } from './CampaignMessagesLogic';
import type { CampaignFrequency } from '../../../services/api/ServiceApi';
import './CampaignMessages.css';

interface CampaignMessagesProps {
campaignId?: string;
campaignStartDate?: string;
campaignEndDate?: string;
campaignFrequency?: CampaignFrequency;
isCampaignCancelled?: boolean;
}

const CampaignMessages: React.FC<CampaignMessagesProps> = ({
campaignId,
campaignStartDate,
campaignEndDate,
campaignFrequency,
isCampaignCancelled,
}) => {
const { t } = useTranslation();
const controller = useCampaignMessagesController({
campaignId,
campaignStartDate,
campaignEndDate,
campaignFrequency,
isCampaignCancelled,
translate: (key) => String(t(key)),
});
const timePickerWrapperRefs = useRef<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ import {
} from 'react';
import { ServiceConfig } from '../../../services/ServiceConfig';
import {
CampaignFrequency,
CampaignMessagingQueryParams,
CampaignMessagingRow,
} from '../../../services/api/ServiceApi';
import { hasCampaignWriteAccess } from '../../../services/api/campaignListingHelpers';
import { Json } from '../../../services/database';
import {
buildFrequencyTimelineDates,
DEFAULT_FREQUENCY,
} from '../campaignSetup/campaignAssignmentUtils';
import { buildCampaignDurationTimelineDates } from '../campaignSetup/campaignCommunicationUtils';
import { useAppSelector } from '../../../redux/hooks';
import { AuthState } from '../../../redux/slices/auth/authSlice';
Expand Down Expand Up @@ -119,6 +124,8 @@ interface UseCampaignMessagesControllerParams {
campaignId?: string;
campaignStartDate?: string;
campaignEndDate?: string;
campaignFrequency?: CampaignFrequency;
isCampaignCancelled?: boolean;
translate: (key: string) => string;
}

Expand Down Expand Up @@ -680,12 +687,16 @@ export const useCampaignMessagesController = ({
campaignId,
campaignStartDate,
campaignEndDate,
campaignFrequency = DEFAULT_FREQUENCY,
isCampaignCancelled = false,
translate,
}: UseCampaignMessagesControllerParams): CampaignMessagesController => {
const { roles } = useAppSelector(
(state: RootState) => state.auth as AuthState,
);
const canEdit = hasCampaignWriteAccess(roles || []);
// A cancelled campaign must become read-only even when the user's role
// normally grants campaign write access.
const canEdit = !isCampaignCancelled && hasCampaignWriteAccess(roles || []);
const [messagesData, setMessagesData] = useState<CampaignMessagesData>(
() => emptyCampaignMessagesData,
);
Expand All @@ -712,6 +723,17 @@ export const useCampaignMessagesController = ({
Record<string, boolean>
>({});
const timelineDates = useMemo(
() =>
campaignStartDate && campaignEndDate
? buildFrequencyTimelineDates(
campaignStartDate,
campaignEndDate,
campaignFrequency,
)
: [],
[campaignEndDate, campaignFrequency, campaignStartDate],
);
const campaignRangeDates = useMemo(
() =>
campaignStartDate && campaignEndDate
? buildCampaignDurationTimelineDates(campaignStartDate, campaignEndDate)
Expand Down Expand Up @@ -751,8 +773,8 @@ export const useCampaignMessagesController = ({
// Fetch the full calendar range so persisted Sunday rows cannot displace
// later non-Sunday campaign dates from the paginated response.
const pageSize =
timelineDates.length > 0
? timelineDates.length
campaignRangeDates.length > 0
? campaignRangeDates.length
: CAMPAIGN_MESSAGES_PAGE_SIZE;
const loadedMessagesData = await loadCampaignMessagesData(
campaignId,
Expand All @@ -774,7 +796,7 @@ export const useCampaignMessagesController = ({
return () => {
isMounted = false;
};
}, [campaignId, displayTimelineDates, timelineDates.length]);
}, [campaignId, campaignRangeDates.length, displayTimelineDates]);

useEffect(
() => () => {
Expand Down Expand Up @@ -1078,8 +1100,8 @@ export const useCampaignMessagesController = ({
{
page: 1,
pageSize:
timelineDates.length > 0
? timelineDates.length
campaignRangeDates.length > 0
? campaignRangeDates.length
: CAMPAIGN_MESSAGES_PAGE_SIZE,
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const CampaignCommunicationTimelineStep: React.FC<
CampaignCommunicationTimelineStepProps
> = ({
form,
frequency,
assignmentDrafts,
selectedSchoolIds,
communicationState,
Expand All @@ -31,8 +32,8 @@ const CampaignCommunicationTimelineStep: React.FC<
const { t } = useTranslation();
const timeOptions = useMemo(() => buildTimeOptions(), []);
const timelineDates = useMemo(
() => buildCommunicationTimelineDates(assignmentDrafts, form),
[assignmentDrafts, form],
() => buildCommunicationTimelineDates(assignmentDrafts, form, frequency),
[assignmentDrafts, form, frequency],
);
const { campaignReach, loadingReach } = useCampaignReach(selectedSchoolIds);

Expand Down
Loading
Loading