Skip to content

Commit

Permalink
Merge pull request #84 from dapetcu21/incompatible-scopes
Browse files Browse the repository at this point in the history
Selectively disable scopes in picker
  • Loading branch information
RaduCStefanescu authored Oct 9, 2020
2 parents 49989a8 + ba6ead8 commit 905d56a
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 15 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@code4ro/reusable-components",
"version": "0.1.43",
"version": "0.1.44",
"description": "Component library for code4ro",
"keywords": [
"code4ro",
Expand Down
28 changes: 22 additions & 6 deletions src/components/ElectionScopePicker/ElectionScopePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo } from "react";
import Select from "react-select";
import { ElectionScope, ElectionScopeIncomplete } from "../../types/Election";
import { ElectionCompatibleScopes, ElectionScope, ElectionScopeIncomplete } from "../../types/Election";
import { APIRequestState } from "../../util/api";
import { ElectionScopeAPI, OptionWithID } from "../../util/electionApi";
import { themable, useTheme } from "../../hooks/theme";
Expand All @@ -12,6 +12,7 @@ type Props = {
apiData: ElectionScopePickerAPIData;
value: ElectionScopeIncomplete;
onChange: (scope: ElectionScopeIncomplete) => unknown;
compatibleScopes?: ElectionCompatibleScopes;
};

export const electionScopePickerUpdateType = (
Expand Down Expand Up @@ -90,12 +91,15 @@ export type ElectionScopePickerSelectOnChange<K = number> = (
value: OptionWithID<K> | ReadonlyArray<OptionWithID<K>> | null | undefined,
) => void;

export type ElectionScopePickerIsOptionDisabled<K = number> = (value: OptionWithID<K>) => boolean;

export type ElectionScopePickerSelectProps<K = number> = {
label: string;
selectProps: {
value: OptionWithID<K> | null;
onChange: ElectionScopePickerSelectOnChange<K>;
options: OptionWithID<K>[];
isOptionDisabled?: ElectionScopePickerIsOptionDisabled<K>;
isLoading: boolean;
isDisabled: boolean;
placeholder?: string;
Expand Down Expand Up @@ -126,6 +130,7 @@ export const useElectionScopePickerGetSelectProps = (
apiData: ElectionScopePickerAPIData,
scope: ElectionScopeIncomplete,
onChangeScope: (newScope: ElectionScopeIncomplete) => unknown,
compatibleScopes?: ElectionCompatibleScopes,
): ElectionScopePickerSelectProps[] => {
const countyMap = useMemo(() => buildMap(apiData.countyData.data), [apiData.countyData.data]);
const localityMap = useMemo(() => buildMap(apiData.localityData.data), [apiData.localityData.data]);
Expand Down Expand Up @@ -202,7 +207,10 @@ export const useElectionScopePickerGetSelectProps = (
});
}

if (scope.type === "diaspora" || scope.type === "diaspora_country") {
if (
(scope.type === "diaspora" && compatibleScopes?.diaspora_country !== false) ||
scope.type === "diaspora_country"
) {
selects.push({
label: "Țară",
selectProps: {
Expand Down Expand Up @@ -237,12 +245,13 @@ const typeOptions: OptionWithID<ElectionScope["type"]>[] = [
{ id: "national", name: typeNames.national },
{ id: "county", name: typeNames.county },
{ id: "locality", name: typeNames.locality },
// { id: "diaspora", name: typeNames.diaspora },
{ id: "diaspora", name: typeNames.diaspora },
];

export const useElectionScopePickerGetTypeSelectProps = (
scope: ElectionScopeIncomplete,
onChangeScope: (newScope: ElectionScopeIncomplete) => unknown,
compatibleScopes?: ElectionCompatibleScopes,
): ElectionScopePickerSelectProps<ElectionScope["type"]> => {
const onTypeChange = useCallback<ElectionScopePickerSelectOnChange<ElectionScope["type"]>>(
(value) => {
Expand All @@ -256,13 +265,19 @@ export const useElectionScopePickerGetTypeSelectProps = (
[scope, onChangeScope],
);

const isOptionDisabled = useMemo<ElectionScopePickerIsOptionDisabled<ElectionScope["type"]> | undefined>(
() => (compatibleScopes ? (option) => compatibleScopes[option.id] === false : undefined),
[compatibleScopes],
);

const value = scope.type === "diaspora_country" ? "diaspora" : scope.type;
return {
label: "Diviziune",
selectProps: {
value: { id: value, name: typeNames[value] },
onChange: onTypeChange,
options: typeOptions,
isOptionDisabled,
isLoading: false,
isDisabled: false,
},
Expand Down Expand Up @@ -294,9 +309,9 @@ const typeSelectStyles = {
export const ElectionScopePicker = themable<Props>(
"ElectionScopePicker",
cssClasses,
)(({ classes, apiData, value, onChange }) => {
const typeSelect = useElectionScopePickerGetTypeSelectProps(value, onChange);
const selects = useElectionScopePickerGetSelectProps(apiData, value, onChange);
)(({ classes, apiData, value, onChange, compatibleScopes }) => {
const typeSelect = useElectionScopePickerGetTypeSelectProps(value, onChange, compatibleScopes);
const selects = useElectionScopePickerGetSelectProps(apiData, value, onChange, compatibleScopes);
const theme = useTheme();

const selectTheme = useMemo(
Expand Down Expand Up @@ -324,6 +339,7 @@ export const ElectionScopePicker = themable<Props>(
theme={selectTheme}
className={classes.typeSelect}
styles={typeSelectStyles}
key={JSON.stringify(compatibleScopes)} // Workaround: The menu list in the Select component doesn't want to re-render on isOptionDisabled change
/>
<div className={classes.selects}>
{selects.map(({ label, selectProps }, index) => (
Expand Down
24 changes: 17 additions & 7 deletions src/stories/APIIntegration.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ElectionResultsProcess } from "../components/ElectionResultsProcess/Ele
import { ElectionResultsSeats } from "../components/ElectionResultsSeats/ElectionResultsSeats";
import { ElectionResultsTableSection } from "../components/ElectionResultsTableSection/ElectionResultsTableSection";
import { ElectionTimeline } from "../components/ElectionTimeline/ElectionTimeline";
import { ElectionScopeIncomplete } from "../types/Election";
import { ElectionCompatibleScopes, ElectionScopeIncomplete } from "../types/Election";
import { ElectionScopePicker, useElectionScopePickerApi } from "../components/ElectionScopePicker/ElectionScopePicker";
import { useBallotData } from "../hooks/electionApiHooks";
import { ElectionNewsSection } from "../components/ElectionNewsSection/ElectionNewsSection";
Expand Down Expand Up @@ -97,16 +97,26 @@ export const ElectionTimelineComponent = (args: { api: string; apiUrl: string })
);
};

export const ElectionScopeComponent = (args: { api: string; apiUrl: string; ballotId: number }) => {
export const ElectionScopeComponent = (args: {
api: string;
apiUrl: string;
ballotId: number;
compatibleScopes?: ElectionCompatibleScopes;
}) => {
const [scope, setScope] = useState<ElectionScopeIncomplete>({ type: "national" });
const electionApi: ElectionAPI = useApi(args.api, args.apiUrl);
const apiData = useElectionScopePickerApi(electionApi, scope, args.ballotId);
return <ElectionScopePicker apiData={apiData} value={scope} onChange={setScope} />;
return (
<ElectionScopePicker apiData={apiData} value={scope} onChange={setScope} compatibleScopes={args.compatibleScopes} />
);
};

ElectionScopeComponent.args = {
ballotId: 1,
compatibleScopes: {},
};

ElectionScopeComponent.argTypes = {
ballotId: {
defaultValue: 1,
control: "number",
},
ballotId: { control: "number" },
compatibleScopes: { control: "object" },
};
51 changes: 50 additions & 1 deletion src/types/Election.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ export type ElectionType =
| "house"
| "local_council"
| "county_council"
| "county_council_president"
| "mayor"
| "european_parliament"
| string;

export const electionTypeInvolvesDiaspora = (electionType: ElectionType): boolean =>
electionType !== "local_council" && electionType !== "county_council" && electionType !== "mayor";
electionTypeCompatibleScopes(electionType).diaspora !== false;

export const electionTypeHasSeats = (electionType: ElectionType): boolean =>
electionType === "senate" ||
Expand All @@ -60,6 +61,54 @@ export const electionHasSeats = (electionType: ElectionType, results: ElectionRe
electionTypeHasSeats(electionType) &&
results.candidates.reduce<boolean>((acc, cand) => acc || cand.seats != null, false);

export type ElectionCompatibleScopes = Partial<Record<ElectionScope["type"], boolean>>;

const allCompatibleScopes: ElectionCompatibleScopes = {};
const countyCompatibleScopes: ElectionCompatibleScopes = { locality: false, diaspora: false, diaspora_country: false };
const localCompatibleScopes: ElectionCompatibleScopes = { diaspora: false, diaspora_country: false };

const compatibleScopesByType: Partial<Record<ElectionType, ElectionCompatibleScopes>> = {
local_council: localCompatibleScopes,
mayor: localCompatibleScopes,
county_council: countyCompatibleScopes,
county_council_president: countyCompatibleScopes,
};

export const electionTypeCompatibleScopes = (electionType: ElectionType): ElectionCompatibleScopes =>
compatibleScopesByType[electionType] ?? allCompatibleScopes;

const fallbackOrder: ElectionScope["type"][] = ["national", "diaspora", "county", "locality", "diaspora_country"];
const emptyScopes: Record<ElectionScope["type"], ElectionScopeIncomplete> = {
national: { type: "national" },
county: { type: "county", countyId: null },
locality: { type: "locality", countyId: null, localityId: null },
diaspora: { type: "diaspora" },
diaspora_country: { type: "diaspora_country", countryId: null },
};

export const electionScopeCoerceToCompatible = (
scope: ElectionScopeIncomplete,
compatibleScopes: ElectionCompatibleScopes,
): ElectionScopeIncomplete => {
if (compatibleScopes[scope.type] !== false) return scope;

if (scope.type === "locality") {
return electionScopeCoerceToCompatible({ type: "county", countyId: scope.countyId }, compatibleScopes);
}

if (scope.type === "diaspora_country") {
return electionScopeCoerceToCompatible({ type: "diaspora" }, compatibleScopes);
}

for (const fallbackType of fallbackOrder) {
if (compatibleScopes[fallbackType] !== false) {
return emptyScopes[fallbackType];
}
}

return emptyScopes.national;
};

export type ElectionBallotMeta = {
// The app should work with any specified "type" in here, including values unknown yet to the frontend
// This is just for extra visual customisation like splitting local council results in two tables,
Expand Down

1 comment on commit 905d56a

@vercel
Copy link

@vercel vercel bot commented on 905d56a Oct 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.