Skip to content

Commit

Permalink
Merge pull request #2421 from zetkin/release-241209
Browse files Browse the repository at this point in the history
241209 Release
  • Loading branch information
richardolsson authored Dec 9, 2024
2 parents 135a024 + fda9002 commit a61ec4d
Show file tree
Hide file tree
Showing 31 changed files with 523 additions and 120 deletions.
91 changes: 70 additions & 21 deletions src/features/campaigns/components/ActivityList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FilterListOutlined } from '@mui/icons-material';
import { FilterListOutlined, Pending } from '@mui/icons-material';
import Fuse from 'fuse.js';
import { useMemo } from 'react';
import { Box, Card, Divider, Typography } from '@mui/material';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Box, BoxProps, Card, Divider, Typography } from '@mui/material';

import CallAssignmentListItem from './items/CallAssignmentListItem';
import EmailListItem from './items/EmailListItem';
Expand All @@ -17,12 +17,61 @@ import useClusteredActivities, {
CLUSTER_TYPE,
} from 'features/campaigns/hooks/useClusteredActivities';
import CanvassAssignmentListItem from './items/CanvassAssignmentListItem';
import ActivityListItem, { STATUS_COLORS } from './items/ActivityListItem';

interface ActivitiesProps {
activities: CampaignActivity[];
orgId: number;
}

interface LazyActivitiesBoxProps extends BoxProps {
index: number;
}

const LazyActivitiesBox = ({
index,
children,
...props
}: LazyActivitiesBoxProps) => {
const [inView, setInView] = useState(false);
const boxRef = useRef<HTMLElement>();

useEffect(() => {
if (!boxRef.current) {
return;
}

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setInView(true);
}
});
});

observer.observe(boxRef.current);
}, [boxRef]);

return inView ? (
<Box {...props}>
{index > 0 && <Divider />}
{children}
</Box>
) : (
<Box ref={boxRef}>
{index > 0 && <Divider />}
<ActivityListItem
color={STATUS_COLORS.GRAY}
endNumber={0}
href={'#'}
PrimaryIcon={Pending}
SecondaryIcon={Pending}
title={'...'}
/>
</Box>
);
};

const Activities = ({ activities, orgId }: ActivitiesProps) => {
const clustered = useClusteredActivities(activities);

Expand All @@ -31,52 +80,52 @@ const Activities = ({ activities, orgId }: ActivitiesProps) => {
{clustered.map((activity, index) => {
if (activity.kind === ACTIVITIES.CALL_ASSIGNMENT) {
return (
<Box key={`ca-${activity.data.id}`}>
{index > 0 && <Divider />}
<LazyActivitiesBox key={`ca-${activity.data.id}`} index={index}>
<CallAssignmentListItem caId={activity.data.id} orgId={orgId} />
</Box>
</LazyActivitiesBox>
);
} else if (activity.kind == ACTIVITIES.CANVASS_ASSIGNMENT) {
return (
<Box key={`canvassassignment-${activity.data.id}`}>
{index > 0 && <Divider />}
<LazyActivitiesBox
key={`canvassassignment-${activity.data.id}`}
index={index}
>
<CanvassAssignmentListItem
caId={activity.data.id}
orgId={orgId}
/>
</Box>
</LazyActivitiesBox>
);
} else if (isEventCluster(activity)) {
return (
<Box key={`event-${activity.events[0].id}`}>
{index > 0 && <Divider />}
<LazyActivitiesBox
key={`event-${activity.events[0].id}`}
index={index}
>
{activity.kind == CLUSTER_TYPE.SINGLE ? (
<EventListItem cluster={activity} />
) : (
<EventClusterListItem cluster={activity} />
)}
</Box>
</LazyActivitiesBox>
);
} else if (activity.kind === ACTIVITIES.SURVEY) {
return (
<Box key={`survey-${activity.data.id}`}>
{index > 0 && <Divider />}
<LazyActivitiesBox key={`survey-${activity.data.id}`} index={index}>
<SurveyListItem orgId={orgId} surveyId={activity.data.id} />
</Box>
</LazyActivitiesBox>
);
} else if (activity.kind === ACTIVITIES.TASK) {
return (
<Box key={`task-${activity.data.id}`}>
{index > 0 && <Divider />}
<LazyActivitiesBox key={`task-${activity.data.id}`} index={index}>
<TaskListItem orgId={orgId} taskId={activity.data.id} />
</Box>
</LazyActivitiesBox>
);
} else if (activity.kind === ACTIVITIES.EMAIL) {
return (
<Box key={`email-${activity.data.id}`}>
{index > 0 && <Divider />}
<LazyActivitiesBox key={`email-${activity.data.id}`} index={index}>
<EmailListItem emailId={activity.data.id} orgId={orgId} />
</Box>
</LazyActivitiesBox>
);
}
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
Map,
FeatureGroup as FeatureGroupType,
latLngBounds,
LatLngBounds,
LatLngTuple,
} from 'leaflet';
import { makeStyles } from '@mui/styles';
import { GpsNotFixed } from '@mui/icons-material';
Expand All @@ -28,6 +30,7 @@ import MapControls from './MapControls';
import objToLatLng from 'features/areas/utils/objToLatLng';
import CanvassAssignmentMapOverlays from './CanvassAssignmentMapOverlays';
import useAllPlaceVisits from '../hooks/useAllPlaceVisits';
import useLocalStorage from 'zui/hooks/useLocalStorage';

const useStyles = makeStyles(() => ({
'@keyframes ghostMarkerBounce': {
Expand Down Expand Up @@ -85,6 +88,21 @@ const CanvassAssignmentMap: FC<CanvassAssignmentMapProps> = ({
const crosshairRef = useRef<HTMLDivElement | null>(null);
const reactFGref = useRef<FeatureGroupType | null>(null);

const [localStorageBounds, setLocalStorageBounds] = useLocalStorage<
[LatLngTuple, LatLngTuple] | null
>(`mapBounds-${assignment.id}`, null);

const saveBounds = () => {
const bounds = map?.getBounds();

if (bounds) {
setLocalStorageBounds([
[bounds.getSouth(), bounds.getWest()],
[bounds.getNorth(), bounds.getEast()],
]);
}
};

const [zoomed, setZoomed] = useState(false);

const selectedPlace = places.find((place) => place.id == selectedPlaceId);
Expand Down Expand Up @@ -170,6 +188,10 @@ const CanvassAssignmentMap: FC<CanvassAssignmentMapProps> = ({
updateSelection();
});

map.on('moveend', saveBounds);

map.on('zoomend', () => saveBounds);

return () => {
map.off('move');
map.off('moveend');
Expand All @@ -180,7 +202,10 @@ const CanvassAssignmentMap: FC<CanvassAssignmentMapProps> = ({

useEffect(() => {
if (map && !zoomed) {
const bounds = reactFGref.current?.getBounds();
const bounds = localStorageBounds
? new LatLngBounds(localStorageBounds)
: reactFGref.current?.getBounds();

if (bounds?.isValid()) {
map.fitBounds(bounds);
setZoomed(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const AssignmentPage: FC<{ assignment: AssignmentWithAreas }> = ({
top: 0,
transition: 'left 0.3s',
width: '90vw',
zIndex: 9999,
zIndex: 99999,
}}
>
<CanvasserSidebar assignment={assignment} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ const PlaceDialog: FC<PlaceDialogProps> = ({
responses,
});
setShowSparkle(true);
back();
}}
/>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ const Place: FC<PlaceProps> = ({
<PageBase
onClose={onClose}
onEdit={onEdit}
subtitle={`${numVisitedHouseholds} / ${numHouseholds} households visited`}
title={place.title || 'Untitled place'}
>
<Box>
Expand All @@ -72,6 +71,21 @@ const Place: FC<PlaceProps> = ({
{place.description || 'Empty description'}
</Typography>
</Box>
<Box alignItems="center" display="flex" flexDirection="column" my={3}>
{!!numHouseholds && (
<>
<Typography variant="h4">
{`${numVisitedHouseholds} of ${numHouseholds}`}
</Typography>
<Typography variant="body2">households visited</Typography>
</>
)}
{!numHouseholds && (
<Typography variant="body2">
No households registered here yet
</Typography>
)}
</Box>
<Box display="flex" gap={1} justifyContent="center" m={4}>
{assignment.reporting_level == 'place' && (
<Button onClick={onVisit} variant="contained">
Expand All @@ -92,10 +106,7 @@ const Place: FC<PlaceProps> = ({
<Typography>History</Typography>
<List>
{visits.map((visit) => {
const householdsPerMetric = visit.responses.map((response) =>
response.responseCounts.reduce((sum, value) => sum + value)
);
const households = Math.max(...householdsPerMetric);
const households = estimateVisitedHouseholds(visit);
return (
<ListItem key={visit.id}>
<Box
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const PlaceVisitPage: FC<Props> = ({
}))
);
setSubmitting(false);
onClose();
}}
startIcon={
submitting ? (
Expand Down
8 changes: 4 additions & 4 deletions src/features/canvassAssignments/hooks/useSidebarStats.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { loadListIfNecessary } from 'core/caching/cacheUtils';
import { useApiClient, useAppDispatch, useAppSelector } from 'core/hooks';
import {
placesInvalidated,
placesLoad,
placesLoaded,
visitsInvalidated,
visitsLoad,
visitsLoaded,
} from '../store';
import useMembership from 'features/organizations/hooks/useMembership';
import estimateVisitedHouseholds from '../utils/estimateVisitedHouseholds';

type UseSidebarReturn = {
loading: boolean;
Expand Down Expand Up @@ -107,10 +109,7 @@ export default function useSidebarStats(

if (visitListFuture.data) {
visitListFuture.data.forEach((visit) => {
const householdsPerMetric = visit.responses.map((response) =>
response.responseCounts.reduce((sum, value) => sum + value, 0)
);
const numHouseholds = Math.max(...householdsPerMetric);
const numHouseholds = estimateVisitedHouseholds(visit);

teamPlaces.add(visit.placeId);
stats.allTime.numHouseholds += numHouseholds;
Expand All @@ -136,6 +135,7 @@ export default function useSidebarStats(
stats,
sync: () => {
dispatch(visitsInvalidated(assignmentId));
dispatch(placesInvalidated());
},
synced: visitList?.loaded || null,
};
Expand Down
4 changes: 4 additions & 0 deletions src/features/canvassAssignments/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ const canvassAssignmentSlice = createSlice({
item.data = place;
item.loaded = new Date().toISOString();
},
placesInvalidated: (state) => {
state.placeList.isStale = true;
},
placesLoad: (state) => {
state.placeList.isLoading = true;
},
Expand Down Expand Up @@ -361,6 +364,7 @@ export const {
canvassSessionsLoad,
canvassSessionsLoaded,
placeCreated,
placesInvalidated,
placesLoad,
placesLoaded,
placeUpdated,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export default function estimateVisitedHouseholds(
const householdsPerMetric = visit.responses.map((response) =>
response.responseCounts.reduce((sum, value) => sum + value)
);
return Math.max(...householdsPerMetric);
return Math.max(0, ...householdsPerMetric);
}
29 changes: 2 additions & 27 deletions src/features/emails/components/EmailEditor/utils/InlineToolBase.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,26 @@
export default class InlineToolBase {
private static _activeInstance: InlineToolBase | null = null;

private _handleSelectionChangeBound: () => void = () => undefined;

activate() {
if (InlineToolBase._activeInstance) {
InlineToolBase._activeInstance.deactivate();
}
InlineToolBase._activeInstance = this;
}

checkState() {
checkState(selection: Selection) {
this.update(selection.getRangeAt(0));
return true;
}

clear() {
this.onToolClose();
this.destroy();
}

deactivate() {
// Called by activate()
}

destroy() {
document.removeEventListener(
'selectionchange',
this._handleSelectionChangeBound
);
}

static get isInline() {
return true;
}
Expand All @@ -39,22 +30,6 @@ export default class InlineToolBase {
}

render() {
const handleSelectionChange = () => {
const selection = window.getSelection();
if (selection?.rangeCount) {
const range = window.getSelection()?.getRangeAt(0);
if (range) {
this.update(range);
}
}
};

this._handleSelectionChangeBound = handleSelectionChange.bind(this);
document.addEventListener(
'selectionchange',
this._handleSelectionChangeBound
);

return this.renderButton();
}

Expand Down
Loading

0 comments on commit a61ec4d

Please sign in to comment.