Skip to content

Commit

Permalink
Merge branch 'main' into MPDX-8351-constants-localized
Browse files Browse the repository at this point in the history
  • Loading branch information
caleballdrin committed Oct 21, 2024
2 parents 84f4f4e + 86d678b commit fab0007
Show file tree
Hide file tree
Showing 89 changed files with 2,624 additions and 1,407 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Note: there is a test account you can use. Get this from another developer if yo
- `ONESKY_API_KEY` - Public key for uploading/downloading translations from OneSky
- `ONESKY_API_SECRET` - Secret key for uploading/downloading translations from OneSky
- `ONESKY_PROJECT_ID` - Project id for uploading/downloading translations from OneSky
- `GOOGLE_MAPS_API_KEY` - Google Maps API key configured to have access to the `places` API
- `GOOGLE_TAG_MANAGER_CONTAINER_ID` - Optional Google Tag Manager container ID
- `NEXT_PUBLIC_MEDIA_FAVICON` - Application favicon image url
- `NEXT_PUBLIC_MEDIA_LOGO` - Application logo image url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from 'src/components/Shared/Filters/FilterPanel.mocks';
import { ContactFilterStatusEnum } from 'src/graphql/types.generated';
import theme from 'src/theme';
import { ContactsWrapper } from './ContactsWrapper';
import { ContactsWrapper, extractContactId } from './ContactsWrapper';

const onSelectedFiltersChanged = jest.fn();
const onClose = jest.fn();
Expand Down Expand Up @@ -165,15 +165,19 @@ describe('ContactsWrapper', () => {
userEvent.click(getByRole('button', { name: 'Any' }));
expect(routeReplace).toHaveBeenLastCalledWith({
pathname: '/contacts',
query: { accountListId: 'account-list-1' },
query: {
accountListId: 'account-list-1',
contactId: [],
},
});

userEvent.click(getByRole('button', { name: 'Tag 1' }));
expect(routeReplace).toHaveBeenLastCalledWith({
pathname: '/contacts',
query: {
accountListId: 'account-list-1',
filters: '%7B%22tags%22:%5B%22Tag%201%22%5D,%22anyTags%22:true%7D',
filters: encodeURIComponent('{"tags":["Tag 1"],"anyTags":true}'),
contactId: [],
},
});

Expand All @@ -182,8 +186,35 @@ describe('ContactsWrapper', () => {
pathname: '/contacts',
query: {
accountListId: 'account-list-1',
filters: '%7B%22tags%22:%5B%22Tag%201%22%5D%7D',
filters: encodeURIComponent('{"tags":["Tag 1"]}'),
contactId: [],
},
});
});
});

describe('extractContactId', () => {
it('returns the last item in the contactId query param', () => {
expect(
extractContactId({
contactId: ['flows', 'contact-1'],
}),
).toBe('contact-1');
});

it('returns undefined when the last item in the contactId query param is the view mode', () => {
expect(
extractContactId({
contactId: ['flows'],
}),
).toBeUndefined();
});

it('returns undefined when the last item in the contactId query param is empty', () => {
expect(
extractContactId({
contactId: [],
}),
).toBeUndefined();
});
});
118 changes: 92 additions & 26 deletions pages/accountLists/[accountListId]/contacts/ContactsWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,126 @@
import { ParsedUrlQuery } from 'node:querystring';
import { useRouter } from 'next/router';
import React, { useEffect, useMemo, useState } from 'react';
import { ContactsProvider } from 'src/components/Contacts/ContactsContext/ContactsContext';
import { ContactFilterSetInput } from 'src/graphql/types.generated';
import { TableViewModeEnum } from 'src/components/Shared/Header/ListHeader';
import {
ContactFilterSetInput,
TaskFilterSetInput,
} from 'src/graphql/types.generated';
import { sanitizeFilters } from 'src/lib/sanitizeFilters';
import { getQueryParam } from 'src/utils/queryParam';

interface Props {
children?: React.ReactNode;
addViewMode?: boolean;
}

export const ContactsWrapper: React.FC<Props> = ({ children }) => {
const router = useRouter();
const { query, replace, pathname, isReady } = router;
/*
* Extract the contact id from the contactId query param, which is an array that may also contain
* the view mode.
*/
export const extractContactId = (query: ParsedUrlQuery): string | undefined => {
const contactId = query.contactId?.at(-1);
if (
!contactId ||
(Object.values(TableViewModeEnum) as string[]).includes(contactId)
) {
return undefined;
} else {
return contactId;
}
};

const urlFilters =
query?.filters && JSON.parse(decodeURI(query.filters as string));
export const ContactsWrapper: React.FC<Props> = ({
children,
addViewMode = false,
}) => {
const router = useRouter();
const { query, replace, pathname } = router;

const [activeFilters, setActiveFilters] = useState<ContactFilterSetInput>(
urlFilters ?? {},
// Extract the initial contact id from the URL
const [contactId, setContactId] = useState<string | undefined>(() =>
extractContactId(query),
);
const [starredFilter, setStarredFilter] = useState<ContactFilterSetInput>({});
const [filterPanelOpen, setFilterPanelOpen] = useState<boolean>(true);
const sanitizedFilters = useMemo(
() => sanitizeFilters(activeFilters),
[activeFilters],
// Update the contact id when the URL changes
useEffect(() => {
setContactId(extractContactId(query));
}, [query]);

// Extract the initial view mode from the URL
const [viewMode, setViewMode] = useState(() => {
const viewMode = query.contactId?.[0];
if (
viewMode &&
(Object.values(TableViewModeEnum) as string[]).includes(viewMode)
) {
return viewMode as TableViewModeEnum;
} else {
return TableViewModeEnum.List;
}
});

const [activeFilters, setActiveFilters] = useState<
ContactFilterSetInput & TaskFilterSetInput
>(JSON.parse(decodeURIComponent(getQueryParam(query, 'filters') ?? '{}')));
const [starredFilter, setStarredFilter] = useState<
ContactFilterSetInput & TaskFilterSetInput
>({});
const [searchTerm, setSearchTerm] = useState(
getQueryParam(query, 'searchTerm') ?? '',
);
const [filterPanelOpen, setFilterPanelOpen] = useState(true);

const { contactId, searchTerm } = query;
const urlQuery = useMemo(() => {
// Omit the filters and searchTerm from the previous query because we don't want them in the URL
// if they are empty and Next.js will still add them to the URL query even if they are undefined.
// i.e. { filters: undefined, searchTerm: '' } results in a querystring of ?filters=&searchTerm
const { filters: _filters, searchTerm: _searchTerm, ...newQuery } = query;

useEffect(() => {
if (!isReady) {
return;
const queryContactId: string[] = [];
if (addViewMode && viewMode !== TableViewModeEnum.List) {
queryContactId.push(viewMode);
}
if (contactId) {
queryContactId.push(contactId);
}
newQuery.contactId = queryContactId;

const sanitizedFilters = sanitizeFilters(activeFilters);
if (
viewMode !== TableViewModeEnum.Map &&
Object.keys(sanitizedFilters).length
) {
newQuery.filters = encodeURIComponent(JSON.stringify(sanitizedFilters));
}

const { filters: _, ...oldQuery } = query;
if (searchTerm) {
newQuery.searchTerm = encodeURIComponent(searchTerm);
}
return newQuery;
}, [contactId, viewMode, activeFilters, searchTerm]);

useEffect(() => {
replace({
pathname,
query: {
...oldQuery,
...(Object.keys(sanitizedFilters).length
? { filters: encodeURI(JSON.stringify(sanitizedFilters)) }
: undefined),
},
query: urlQuery,
});
}, [sanitizedFilters, isReady]);
}, [urlQuery]);

return (
<ContactsProvider
urlFilters={urlFilters}
activeFilters={activeFilters}
setActiveFilters={setActiveFilters}
starredFilter={starredFilter}
setStarredFilter={setStarredFilter}
filterPanelOpen={filterPanelOpen}
setFilterPanelOpen={setFilterPanelOpen}
contactId={contactId}
setContactId={setContactId}
viewMode={viewMode}
setViewMode={setViewMode}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
>
{children}
</ContactsProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,7 @@ const Contacts: React.FC = ({}) => {
mainContent={<ContactsMainPanel />}
rightPanel={
<DynamicContactsRightPanel
onClose={() =>
setContactFocus(
undefined,
true,
viewMode === TableViewModeEnum.Flows,
viewMode === TableViewModeEnum.Map,
)
}
onClose={() => setContactFocus(undefined)}
/>
}
rightOpen={contactDetailsOpen}
Expand All @@ -71,7 +64,7 @@ const Contacts: React.FC = ({}) => {
};

const ContactsPage: React.FC = () => (
<ContactsWrapper>
<ContactsWrapper addViewMode>
<Contacts />
</ContactsWrapper>
);
Expand Down
102 changes: 30 additions & 72 deletions pages/accountLists/[accountListId]/contacts/flows/setup.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,11 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import { loadSession } from 'pages/api/utils/pagePropsHelpers';
import {
ContactFlowOption,
colorMap,
} from 'src/components/Contacts/ContactFlow/ContactFlow';
import { colorMap } from 'src/components/Contacts/ContactFlow/ContactFlow';
import { ContactFlowSetupColumn } from 'src/components/Contacts/ContactFlow/ContactFlowSetup/Column/ContactFlowSetupColumn';
import { UnusedStatusesColumn } from 'src/components/Contacts/ContactFlow/ContactFlowSetup/Column/UnusedStatusesColumn';
import { ContactFlowSetupDragLayer } from 'src/components/Contacts/ContactFlow/ContactFlowSetup/DragLayer/ContactFlowSetupDragLayer';
import { ContactFlowSetupHeader } from 'src/components/Contacts/ContactFlow/ContactFlowSetup/Header/ContactFlowSetupHeader';
import { useUpdateUserOptionsMutation } from 'src/components/Contacts/ContactFlow/ContactFlowSetup/UpdateUserOptions.generated';
import {
GetUserOptionsDocument,
GetUserOptionsQuery,
} from 'src/components/Contacts/ContactFlow/GetUserOptions.generated';
import { getDefaultFlowOptions } from 'src/components/Contacts/ContactFlow/contactFlowDefaultOptions';
import {
FlowOption,
Expand Down Expand Up @@ -52,7 +44,7 @@ const ContactFlowSetupPage: React.FC = () => {
const resetColumnsMessage = t(
'Since all columns have been removed, resetting columns to their default values',
);
const { options: userOptions, loading } = useFlowOptions();
const [userOptions, updateOptions, { loading }] = useFlowOptions();

useEffect(() => {
if (!userOptions.length) {
Expand All @@ -62,7 +54,6 @@ const ContactFlowSetupPage: React.FC = () => {
}
}, [userOptions]);

const [updateUserOptions] = useUpdateUserOptionsMutation();
const { appName } = useGetAppSettings();

const allUsedStatuses = flowOptions
Expand All @@ -72,49 +63,8 @@ const ContactFlowSetupPage: React.FC = () => {
(status) => !allUsedStatuses.includes(status),
);

const updateOptions = useCallback(
async (options: ContactFlowOption[]): Promise<void> => {
const stringified = JSON.stringify(options);
await updateUserOptions({
variables: {
key: 'flows',
value: stringified,
},
update: (cache, { data: updatedUserOption }) => {
const query = {
query: GetUserOptionsDocument,
};
const dataFromCache = cache.readQuery<GetUserOptionsQuery>(query);

if (dataFromCache) {
const filteredOld = dataFromCache.userOptions.filter(
(option) => option.key !== 'flows',
);

const data = {
userOptions: [
...filteredOld,
{
__typename: 'Option',
id: updatedUserOption?.createOrUpdateUserOption?.option.id,
key: 'flows',
value: stringified,
},
],
};
cache.writeQuery({ ...query, data });
}
enqueueSnackbar(t('User options updated!'), {
variant: 'success',
});
},
});
},
[],
);

const addColumn = (): Promise<void> => {
return updateOptions([
const addColumn = (): void => {
updateOptions([
...flowOptions,
{
name: 'Untitled',
Expand All @@ -139,27 +89,35 @@ const ContactFlowSetupPage: React.FC = () => {

const changeColor = (index: number, color: string): void => {
const temp = [...flowOptions];
temp[index].color = color;
temp[index] = { ...temp[index], color };
updateOptions(temp);
};

const moveStatus = (
originIndex: number,
destinationIndex: number,
draggedStatus: StatusEnum,
): void => {
const temp = [...flowOptions];
if (originIndex > -1) {
temp[originIndex].statuses = temp[originIndex].statuses.filter(
(status) => status !== draggedStatus,
);
}
if (destinationIndex > -1) {
temp[destinationIndex].statuses.push(draggedStatus);
}
updateOptions(temp);
setFlowOptions(temp);
};
const moveStatus = useCallback(
(
originIndex: number,
destinationIndex: number,
draggedStatus: StatusEnum,
): void => {
const temp = [...flowOptions];
if (originIndex > -1) {
temp[originIndex] = {
...temp[originIndex],
statuses: temp[originIndex].statuses.filter(
(status) => status !== draggedStatus,
),
};
}
if (destinationIndex > -1) {
temp[destinationIndex] = {
...temp[destinationIndex],
statuses: [...temp[destinationIndex].statuses, draggedStatus],
};
}
updateOptions(temp);
},
[flowOptions],
);

const changeTitle = (
event: React.ChangeEvent<HTMLInputElement>,
Expand Down
Loading

0 comments on commit fab0007

Please sign in to comment.