Skip to content
Draft
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
9 changes: 9 additions & 0 deletions src/components/form/Lookup/Language/LanguageLookup.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,13 @@ fragment LanguageLookupItem on Language {
displayName {
value
}
ethnologue {
code {
value
}
}
registryOfLanguageVarietiesCode {
value
}

}
22 changes: 17 additions & 5 deletions src/components/form/Lookup/LookupField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { camelCase, last, uniqBy, upperFirst } from 'lodash';
import {
ComponentType,
ReactNode,
useCallback,
useEffect,
useMemo,
Expand Down Expand Up @@ -57,7 +58,10 @@
getInitialValues?: (val: string) => Partial<CreateFormValues>;
getOptionLabel: (option: T) => string | null | undefined;
createPower?: Power;
initialOptions?: { options?: readonly T[] };
/** Render the content inside each option's <li> element instead of the default label. */
renderOptionContent?: (option: T) => ReactNode;
/** Sort visible options after filtering. Already-engaged items can be pushed to the bottom. */
sortComparator?: (a: T, b: T) => number;
} & Except<
AutocompleteProps<T, Multiple, DisableClearable, false>,
| 'value'
Expand Down Expand Up @@ -95,7 +99,8 @@
variant,
createPower,
margin,
initialOptions: initial,
renderOptionContent,
sortComparator,
...props
}: LookupFieldProps<T, Multiple, DisableClearable, CreateFormValues>) {
const { powers } = useSession();
Expand Down Expand Up @@ -143,7 +148,7 @@
});
// Not just for the first load, but every network request
const searchResultsLoading = isNetworkRequestInFlight(networkStatus);
const initialOptionsLoading = initial && !initial.options;

Check failure on line 151 in src/components/form/Lookup/LookupField.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'initial'.

Check failure on line 151 in src/components/form/Lookup/LookupField.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'initial'.

Check failure on line 151 in src/components/form/Lookup/LookupField.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'initial'.

Check failure on line 151 in src/components/form/Lookup/LookupField.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'initial'.

const [createDialogState, createDialogItem, createInitialValues] =
useDialog<Partial<CreateFormValues>>();
Expand Down Expand Up @@ -171,7 +176,7 @@
// (searching for an item or have initial options).
const open =
!!meta.active &&
((input && input !== selectedText) || (!input && !!initial));

Check failure on line 179 in src/components/form/Lookup/LookupField.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'initial'.

Check failure on line 179 in src/components/form/Lookup/LookupField.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'initial'.

// Augment results with currently selected items to indicate that
// they are still valid (and to prevent MUI warning)
Expand All @@ -182,7 +187,7 @@
? [field.value as T]
: [];
const searchResults = data?.search.items;
const initialItems = initial?.options;

Check failure on line 190 in src/components/form/Lookup/LookupField.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'initial'.

Check failure on line 190 in src/components/form/Lookup/LookupField.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'initial'.

if (!searchResults?.length && !initialItems?.length) {
return selected; // optimization for no results
Expand All @@ -196,7 +201,7 @@

// Filter out duplicates caused by selected items also appearing in search results.
return uniqBy(merged, compareBy);
}, [data?.search.items, initial?.options, field.value, compareBy, multiple]);

Check failure on line 204 in src/components/form/Lookup/LookupField.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'initial'.

Check failure on line 204 in src/components/form/Lookup/LookupField.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'initial'.

const autocomplete = (
<Autocomplete<T, Multiple, DisableClearable, typeof freeSolo>
Expand Down Expand Up @@ -232,6 +237,8 @@
<li {...props}>
{typeof option === 'string'
? `Create "${option}"`
: renderOptionContent
? renderOptionContent(option)
: getOptionLabel(option)}
</li>
)}
Expand All @@ -246,19 +253,24 @@
// results that have already been selected.
const filtered = createFilterOptions<T>()(options, params);

// Apply caller-provided sort (e.g. push disabled/engaged items to bottom)
const sorted = sortComparator
? [...filtered].sort(sortComparator)
: filtered;

if (
!freeSolo ||
searchResultsLoading || // item could be returned with request in flight
params.inputValue === '' ||
filtered.map(getOptionLabel).includes(params.inputValue)
sorted.map(getOptionLabel).includes(params.inputValue)
) {
return filtered;
return sorted;
}

// If freeSolo is enabled and the input value doesn't match an existing
// or previously selected option, add it to the list. i.e. 'Add "X"'.
return [
...filtered,
...sorted,
// We want to allow strings for new options,
// which may differ from T. We handle them in renderOption.
params.inputValue as T,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { useMutation } from '@apollo/client';
import { Box, Paper, Tooltip, Typography } from '@mui/material';
import { useMemo } from 'react';
import { useFormState } from 'react-final-form';
import { makeStyles } from 'tss-react/mui';
import { Except } from 'type-fest';
import { addItemToList } from '~/api';
import { callAll } from '~/common';
Expand All @@ -12,7 +16,10 @@
LanguageField,
LanguageLookupItem,
} from '../../../../components/form/Lookup';
import { CreateLanguageEngagementDocument } from './CreateLanguageEngagement.graphql';
import {
CreateLanguageEngagementDocument,
CreateLanguageEngagementMutation,
} from './CreateLanguageEngagement.graphql';
import { invalidatePartnersEngagements } from './invalidatePartnersEngagements';
import { recalculateSensitivity } from './recalculateSensitivity';

Expand All @@ -25,30 +32,187 @@
'onSubmit'
> & {
project: ProjectIdFragment;
/** IDs of languages that already have an engagement on this project. */
engagedLanguageIds: readonly string[];
};

const useStyles = makeStyles()(({ palette, spacing }) => ({
columnHeader: {
display: 'flex',
padding: spacing(0.5, 2),
borderBottom: `1px solid ${palette.divider}`,
},
columnHeaderName: {
flex: 1,
fontSize: '0.7rem',
fontWeight: 600,
color: palette.text.secondary,
textTransform: 'uppercase',
},
columnHeaderCode: {
flexShrink: 0,
width: 52,
fontSize: '0.7rem',
fontWeight: 600,
textAlign: 'right',
color: palette.text.secondary,
textTransform: 'uppercase',
},
optionRow: {
display: 'flex',
width: '100%',
alignItems: 'center',
gap: spacing(1),
},
optionName: {
flex: 1,
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
optionCode: {
flexShrink: 0,
width: 52,
textAlign: 'right',
color: palette.text.secondary,
},
helperRow: {
display: 'flex',
alignItems: 'center',
gap: spacing(2),
},
helperKey: {
fontWeight: 600,
},
}));

/**
* Inner form content — rendered inside DialogForm's Form context so that
* useFormState can subscribe to live field values.
*/
const FormContent = ({
engagedLanguageIds,
sortComparator,
}: {
engagedLanguageIds: readonly string[];
sortComparator: (a: LanguageLookupItem, b: LanguageLookupItem) => number;
}) => {
const { classes } = useStyles();
const { values } = useFormState<CreateLanguageEngagementFormValues>({
subscription: { values: true },
});
const currentLanguage = values.engagement.languageId;

Check failure on line 105 in src/scenes/Engagement/LanguageEngagement/Create/CreateLanguageEngagement.tsx

View workflow job for this annotation

GitHub Actions / run

Property 'engagement' does not exist on type 'CreateLanguageEngagementFormValues'.

Check failure on line 105 in src/scenes/Engagement/LanguageEngagement/Create/CreateLanguageEngagement.tsx

View workflow job for this annotation

GitHub Actions / run

Property 'engagement' does not exist on type 'CreateLanguageEngagementFormValues'.

const renderOptionContent = (option: LanguageLookupItem) => {
const row = (
<span className={classes.optionRow}>
<span className={classes.optionName}>
{option.name.value ?? option.displayName.value}
</span>
<span className={classes.optionCode}>
{option.ethnologue?.code?.value ?? '-'}
</span>
<span className={classes.optionCode}>
{option.registryOfLanguageVarietiesCode?.value ?? '-'}
</span>
</span>
);

if (engagedLanguageIds.includes(option.id)) {
return (
// Tooltip on a disabled Autocomplete option requires a non-disabled wrapper
<Tooltip title="Already added to this project" placement="right">
<span style={{ display: 'block', width: '100%' }}>{row}</span>
</Tooltip>
);
}
return row;
};

return (
<>
<SubmitError />
<LanguageField
name="engagement.languageId"
label="Language"
required
PaperComponent={({ children }) => (
<Paper>
<Box className={classes.columnHeader}>
<Typography className={classes.columnHeaderName}>Name</Typography>
<Typography className={classes.columnHeaderCode}>ETH</Typography>
<Typography className={classes.columnHeaderCode}>ROLV</Typography>
</Box>
{children}
</Paper>
)}
sortComparator={sortComparator}
getOptionDisabled={(lang: LanguageLookupItem) =>
engagedLanguageIds.includes(lang.id)
}
renderOptionContent={renderOptionContent}
helperText={
currentLanguage ? (
<Box className={classes.helperRow}>
<Typography variant="caption" className={classes.helperKey}>
ETH
</Typography>
<Typography variant="caption">
{currentLanguage.ethnologue?.code?.value ?? '-'}
</Typography>
<Typography variant="caption" className={classes.helperKey}>
ROLV
</Typography>
<Typography variant="caption">
{currentLanguage.registryOfLanguageVarietiesCode?.value ?? '-'}
</Typography>
</Box>
) : undefined
}
/>
</>
);
};

export const CreateLanguageEngagement = ({
project,
engagedLanguageIds,
...props
}: CreateLanguageEngagementProps) => {
const [createEngagement] = useMutation(CreateLanguageEngagementDocument);
const submit = async ({ language }: CreateLanguageEngagementFormValues) => {

// Push already-engaged languages to the bottom of the dropdown list
const sortComparator = useMemo(
() =>
(a: LanguageLookupItem, b: LanguageLookupItem): number => {
const aEngaged = engagedLanguageIds.includes(a.id);
const bEngaged = engagedLanguageIds.includes(b.id);
if (aEngaged === bEngaged) return 0;
return aEngaged ? 1 : -1;
},
[engagedLanguageIds]
);

const submit = async ({ engagement }: CreateLanguageEngagementFormValues) => {

Check failure on line 197 in src/scenes/Engagement/LanguageEngagement/Create/CreateLanguageEngagement.tsx

View workflow job for this annotation

GitHub Actions / run

Property 'engagement' does not exist on type 'CreateLanguageEngagementFormValues'.

Check failure on line 197 in src/scenes/Engagement/LanguageEngagement/Create/CreateLanguageEngagement.tsx

View workflow job for this annotation

GitHub Actions / run

Property 'engagement' does not exist on type 'CreateLanguageEngagementFormValues'.
const languageRef = {
__typename: 'Language',
id: language.id,

Check failure on line 200 in src/scenes/Engagement/LanguageEngagement/Create/CreateLanguageEngagement.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'language'.

Check failure on line 200 in src/scenes/Engagement/LanguageEngagement/Create/CreateLanguageEngagement.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'language'.
} as const;

await createEngagement({
variables: {
input: {
project: project.id,
language: language.id,

Check failure on line 207 in src/scenes/Engagement/LanguageEngagement/Create/CreateLanguageEngagement.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'language'.

Check failure on line 207 in src/scenes/Engagement/LanguageEngagement/Create/CreateLanguageEngagement.tsx

View workflow job for this annotation

GitHub Actions / run

Cannot find name 'language'.
changeset: project.changeset?.id,
},
},
update: callAll(
addItemToList({
listId: [project, 'engagements'],
outputToItem: (res) => res.createLanguageEngagement.engagement,
outputToItem: (res: CreateLanguageEngagementMutation) =>
res.createLanguageEngagement.engagement,
}),
addItemToList({
listId: [languageRef, 'projects'],
Expand All @@ -59,15 +223,18 @@
),
});
};

return (
<DialogForm
{...props}
onSubmit={submit}
title="Create Language Engagement"
changesetAware
>
<SubmitError />
<LanguageField name="language" label="Language" required />
<FormContent
engagedLanguageIds={engagedLanguageIds}
sortComparator={sortComparator}
/>
</DialogForm>
);
};
31 changes: 25 additions & 6 deletions src/scenes/Projects/Overview/ProjectOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@mui/icons-material';
import { Chip, Grid, Skeleton, Tooltip, Typography } from '@mui/material';
import { Many } from '@seedcompany/common';
import { useMemo } from 'react';
import { useDropzone } from 'react-dropzone';
import { Helmet } from 'react-helmet-async';
import { makeStyles } from 'tss-react/mui';
Expand Down Expand Up @@ -190,9 +191,17 @@ export const ProjectOverview = () => {
0
);

const CreateEngagement = isTranslation
? CreateLanguageEngagement
: CreateInternshipEngagement;
// IDs of languages that already have an engagement on this project, used to
// disable duplicate selections in the Create Language Engagement dialog.
const engagedLanguageIds = useMemo(
() =>
engagements.data?.items.flatMap((e) =>
e.__typename === 'LanguageEngagement' && e.language.value
? [e.language.value.id]
: []
) ?? [],
[engagements.data?.items]
);

return (
<main className={classes.root}>
Expand Down Expand Up @@ -582,9 +591,19 @@ export const ProjectOverview = () => {
editFields={fieldsBeingEdited}
/>
) : null}
{project && (
<CreateEngagement project={project} {...createEngagementState} />
)}
{project &&
(isTranslation ? (
<CreateLanguageEngagement
project={project}
engagedLanguageIds={engagedLanguageIds}
{...createEngagementState}
/>
) : (
<CreateInternshipEngagement
project={project}
{...createEngagementState}
/>
))}
{project && (
<WorkflowEventsDrawer
{...workflowDrawerState}
Expand Down
Loading