diff --git a/api/hasura/auth.ts b/api/hasura/auth.ts index 507ccea5d8..58c3f11ca2 100644 --- a/api/hasura/auth.ts +++ b/api/hasura/auth.ts @@ -30,7 +30,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { 'X-Hasura-User-Id': tokenRow.tokenable_id.toString(), 'X-Hasura-Role': profile.admin_view ? 'superadmin' : 'user', }); - } catch (e) { + } catch (e: any) { res.status(401).json({ error: '401', message: (e.message as string) || 'Unexpected error', diff --git a/scripts/setup-hardhat.sh b/scripts/setup-hardhat.sh index 10fff6662d..d015e72389 100755 --- a/scripts/setup-hardhat.sh +++ b/scripts/setup-hardhat.sh @@ -3,7 +3,7 @@ set -e FIND_DEV_PID="lsof -t -i:8545 -sTCP:LISTEN" -git submodule update --init --recursive +# git submodule update --init --recursive yarn hardhat:install --frozen-lockfile yarn hardhat:compile diff --git a/src/components/ProfileCard/ProfileCard.tsx b/src/components/ProfileCard/ProfileCard.tsx index f53c978acb..e53db72322 100644 --- a/src/components/ProfileCard/ProfileCard.tsx +++ b/src/components/ProfileCard/ProfileCard.tsx @@ -13,11 +13,13 @@ import { } from 'components'; import { USER_ROLE_ADMIN, USER_ROLE_COORDINAPE } from 'config/constants'; import { useNavigation } from 'hooks'; +import { useProfileTasks } from 'recoilState'; import { useSetEditProfileOpen } from 'recoilState/ui'; import { EXTERNAL_URL_FEEDBACK } from 'routes/paths'; import { CardInfoText } from './CardInfoText'; import { GiftInput } from './GiftInput'; +import { TasksSummary } from './TasksSummary'; import { IUser } from 'types'; @@ -28,7 +30,7 @@ const useStyles = makeStyles(theme => ({ alignItems: 'center', justifyContent: 'space-between', width: 330, - height: 452, + minHeight: 452, margin: theme.spacing(1), padding: theme.spacing(1.3, 1.3, 2), background: theme.colors.background, @@ -91,6 +93,7 @@ const useStyles = makeStyles(theme => ({ textAlign: 'center', WebkitLineClamp: 4, wordBreak: 'break-word', + width: '100%', }, editButton: { margin: theme.spacing(7, 0, 2), @@ -133,6 +136,8 @@ export const ProfileCard = ({ const { getToMap, getToProfile } = useNavigation(); const setEditProfileOpen = useSetEditProfileOpen(); + const tasks = useProfileTasks(user.address); + const userBioTextLength = user?.bio?.length ?? 0; const skillsLength = user?.profile?.skills?.length ?? 0; @@ -201,6 +206,8 @@ export const ProfileCard = ({ ) : ( {user.bio} )} + + {!!tasks && } {!disabled && updateGift && ( diff --git a/src/components/ProfileCard/TasksSummary.tsx b/src/components/ProfileCard/TasksSummary.tsx new file mode 100644 index 0000000000..90ada7e920 --- /dev/null +++ b/src/components/ProfileCard/TasksSummary.tsx @@ -0,0 +1,69 @@ +import { FC } from 'react'; + +import { makeStyles, Typography } from '@material-ui/core'; + +import { RightArrowIcon } from 'icons'; + +export interface Task { + id: string; + name: string; + permalink: string; +} + +interface Props { + tasks: Task[]; +} + +const useStyles = makeStyles(theme => ({ + root: { + textAlign: 'left', + margin: theme.spacing(1, 0), + padding: theme.spacing(1.5), + backgroundColor: theme.colors.white, + borderRadius: 8, + }, + title: { + textTransform: 'uppercase', + color: theme.colors.lightText, + fontWeight: 600, + }, + taskItem: { + display: 'flex', + alignItems: 'center', + color: theme.colors.mediumGray, + transition: 'color 200ms ease-out', + '&:hover': { + color: theme.colors.text, + }, + }, + taskName: { + flex: 1, + fontWeight: 600, + }, +})); + +export const TasksSummary: FC = ({ tasks }) => { + const classes = useStyles(); + if (!tasks.length) return null; + return ( +
+ + Completed Tasks + + {tasks.map(task => ( + + + {task.name} + + + + ))} +
+ ); +}; diff --git a/src/config/env.ts b/src/config/env.ts index 393ce82caa..8a3803e47c 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -23,6 +23,14 @@ export const API_URL = getEnvValue( 'REACT_APP_API_BASE_URL', 'https://missing-laravel-url.edu' ); +export const DEWORK_API_URL = getEnvValue( + 'REACT_APP_DEWORK_API_URL', + 'https://api.dework.xyz' +); +export const DEWORK_APP_INSTALL_URL = getEnvValue( + 'REACT_APP_DEWORK_APP_INSTALL_URL', + 'https://dework.xyz/apps/install/coordinape' +); // The test key always returns: 10000000-aaaa-bbbb-cccc-000000000001 export const CAPTCHA_SITE_KEY = IN_PRODUCTION ? getEnvValue('REACT_APP_H_CAPTCHA', 'missing-captcha-site-key') diff --git a/src/icons/DeworkIcon.tsx b/src/icons/DeworkIcon.tsx new file mode 100644 index 0000000000..e1206344ed --- /dev/null +++ b/src/icons/DeworkIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import { SvgIcon } from '@material-ui/core'; + +export const DeworkIcon = (props: any) => ( + + + + +); diff --git a/src/icons/RightArrow.tsx b/src/icons/RightArrow.tsx new file mode 100644 index 0000000000..59e59f229b --- /dev/null +++ b/src/icons/RightArrow.tsx @@ -0,0 +1,36 @@ +/* eslint-disable react/display-name */ +import * as React from 'react'; + +import { styled, SvgIconConfig } from 'stitches.config'; + +import { IconProps } from 'types'; + +export const RightArrowIcon = styled( + React.forwardRef( + ({ color = 'currentColor', ...props }, forwardedRef) => { + return ( + + + + ); + } + ), + SvgIconConfig +); + +export default RightArrowIcon; diff --git a/src/icons/index.ts b/src/icons/index.ts index 8f5811e747..aef58767b9 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -15,6 +15,7 @@ export * from './DocsIcon'; export * from './FilterIcon'; export * from './MediumIcon'; export * from './TwitterIcon'; +export * from './DeworkIcon'; export * from './DiscordIcon'; export * from './BalanceIcon'; export * from './DeleteIcon'; @@ -25,6 +26,7 @@ export * from './UploadIcon'; export * from './MinusCircleIcon'; export * from './InfoIcon'; export * from './DownArrow'; +export * from './RightArrow'; export * from './GithubIcon'; export * from './TelegramIcon'; export * from './LinkIcon'; diff --git a/src/pages/AdminPage/AdminCircleModal.tsx b/src/pages/AdminPage/AdminCircleModal.tsx index 0542594721..94f750a0c7 100644 --- a/src/pages/AdminPage/AdminCircleModal.tsx +++ b/src/pages/AdminPage/AdminCircleModal.tsx @@ -5,10 +5,19 @@ import { transparentize } from 'polished'; import { makeStyles, Button } from '@material-ui/core'; -import { ApeAvatar, FormModal, ApeTextField, ApeToggle } from 'components'; +import { + ApeAvatar, + FormModal, + ApeTextField, + ApeToggle, + ApeInfoTooltip, +} from 'components'; +import { DEWORK_APP_INSTALL_URL } from 'config/env'; import { useApiAdminCircle } from 'hooks'; import { UploadIcon, EditIcon } from 'icons'; +import { DeworkIcon } from 'icons/DeworkIcon'; import { useSelectedCircle } from 'recoilState/app'; +import { getDeworkCallbackPath } from 'routes/paths'; import { getCircleAvatar } from 'utils/domain'; import { ICircle } from 'types'; @@ -132,6 +141,11 @@ const useStyles = makeStyles(theme => ({ textAlign: 'center', color: theme.colors.linkBlue, }, + completedWorkContainer: { + display: 'grid', + placeItems: 'center', + marginBottom: theme.spacing(2), + }, })); const YesNoTooltip = ({ yes = '', no = '', href = '', anchorText = '' }) => { @@ -192,6 +206,9 @@ export const AdminCircleModal = ({ const [vouchingText, setVouchingText] = useState(circle.vouchingText); const [onlyGiverVouch, setOnlyGiverVouch] = useState(circle.only_giver_vouch); const [autoOptOut, setAutoOptOut] = useState(circle.auto_opt_out); + const [deworkOrganizationId, setDeworkOrganizationId] = useState( + circle.dework_organization_id + ); // onChange Logo const onChangeLogo = (e: React.ChangeEvent) => { @@ -251,7 +268,8 @@ export const AdminCircleModal = ({ vouchingText !== circle.vouchingText || onlyGiverVouch !== circle.only_giver_vouch || teamSelection !== circle.team_selection || - autoOptOut !== circle.auto_opt_out + autoOptOut !== circle.auto_opt_out || + deworkOrganizationId !== circle.dework_organization_id ) { await updateCircle({ name: circleName, @@ -268,6 +286,7 @@ export const AdminCircleModal = ({ only_giver_vouch: onlyGiverVouch, team_selection: teamSelection, auto_opt_out: autoOptOut, + dework_organization_id: deworkOrganizationId, }).then(() => { onClose(); }); @@ -292,7 +311,8 @@ export const AdminCircleModal = ({ vouchingText !== circle.vouchingText || onlyGiverVouch !== circle.only_giver_vouch || teamSelection !== circle.team_selection || - autoOptOut !== circle.auto_opt_out; + autoOptOut !== circle.auto_opt_out || + deworkOrganizationId !== circle.dework_organization_id; return ( +
+
+

+ Show List of Completed Work + + Connect to Dework to show a list of tasks each contributor has + done during an epoch + +

+
+ {deworkOrganizationId ? ( + + ) : ( + + )} +

Discord Webhook

{allowEdit && ( diff --git a/src/pages/DeworkCallbackPage/DeworkCallbackPage.tsx b/src/pages/DeworkCallbackPage/DeworkCallbackPage.tsx new file mode 100644 index 0000000000..8e39c4dd49 --- /dev/null +++ b/src/pages/DeworkCallbackPage/DeworkCallbackPage.tsx @@ -0,0 +1,37 @@ +import { FC, useCallback, useEffect, useMemo } from 'react'; + +import { useHistory, useLocation } from 'react-router-dom'; + +import { useApiAdminCircle } from 'hooks'; +import { useCircle, useSelectedCircle } from 'recoilState'; +import { getCirclesPath } from 'routes/paths'; + +export const DeworkCallbackPage: FC = () => { + const history = useHistory(); + const { search } = useLocation(); + const deworkOrganizationId = useMemo( + () => new URLSearchParams(search).get('dework_organization_id'), + [search] + ); + + const { circleId } = useSelectedCircle(); + const { circle } = useCircle(circleId); + const { updateCircle } = useApiAdminCircle(circleId); + + const updateDeworkOrganizationId = useCallback(async () => { + if (deworkOrganizationId) { + await updateCircle({ + ...circle, + token_name: circle.tokenName, + update_webhook: 0, + dework_organization_id: deworkOrganizationId, + }); + } + + history.replace(getCirclesPath()); + }, []); + useEffect(() => { + updateDeworkOrganizationId(); + }, []); + return null; +}; diff --git a/src/recoilState/app.ts b/src/recoilState/app.ts index 8670b1aaa9..eae2271443 100644 --- a/src/recoilState/app.ts +++ b/src/recoilState/app.ts @@ -10,6 +10,7 @@ import { useRecoilValueLoadable, } from 'recoil'; +import { DEWORK_API_URL } from 'config/env'; import { getApiService } from 'services/api'; import { extraProfile } from 'utils/modelExtenders'; import { getSelfIdProfile } from 'utils/selfIdHelpers'; @@ -398,3 +399,74 @@ export const useProfile = (address: string) => export const useEpochsStatus = (circleId: number) => useRecoilValue(rCircleEpochsStatus(circleId)); + +interface ICircleTask { + id: string; + name: string; + permalink: string; + assignees: { + id: string; + threepids: { + source: string; + threepid: string; + }[]; + }[]; +} + +export const rSelectedCircleTasks = selector({ + key: 'rSelectedCircleTasks', + get: async ({ get }) => { + const circle = get(rSelectedCircle); + if (!circle.circle.dework_organization_id) return []; + return fetch(`${DEWORK_API_URL}/graphql`, { + // return fetch(`https://api.demo.dework.xyz/graphql`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + query: ` + query CoordinapeDeworkTasksQuery($organizationId:UUID!) { + tasks: getTasks(input:{ + organizationIds: [$organizationId] + statuses: [DONE] + }) { + id + name + permalink + assignees { + id + threepids { + source + threepid + } + } + } + } + `, + variables: { organizationId: circle.circle.dework_organization_id }, + }), + }) + .then(res => res.json()) + .then(json => json.data.tasks); + }, +}); + +export const rProfileTasks = selectorFamily({ + key: 'rProfileTasks', + get: + (address: string) => + ({ get }) => { + const allTasks = get(rSelectedCircleTasks); + return allTasks.filter(task => + task.assignees.some(user => + user.threepids.some( + t => + t.source === 'metamask' && + t.threepid.toLowerCase() === address.toLowerCase() + ) + ) + ); + }, +}); + +export const useProfileTasks = (address: string) => + useRecoilValue(rProfileTasks(address)); diff --git a/src/routes/paths.ts b/src/routes/paths.ts index 5d997b082b..dbf7e106a2 100644 --- a/src/routes/paths.ts +++ b/src/routes/paths.ts @@ -50,6 +50,7 @@ export const getCirclesPath = () => '/admin/circles'; export const getCreateCirclePath = () => APP_PATH_CREATE_CIRCLE; export const getProfilePath = ({ address }: { address: string }) => `/profile/${address}`; +export const getDeworkCallbackPath = () => `/dework-callback`; interface INavItem { label: string; diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx index 994c07a00b..6702ff5a52 100644 --- a/src/routes/routes.tsx +++ b/src/routes/routes.tsx @@ -6,6 +6,7 @@ import AdminPage from 'pages/AdminPage'; import AllocationPage from 'pages/AllocationPage'; import CreateCirclePage from 'pages/CreateCirclePage'; import DefaultPage from 'pages/DefaultPage'; +import { DeworkCallbackPage } from 'pages/DeworkCallbackPage/DeworkCallbackPage'; import HistoryPage from 'pages/HistoryPage'; import OverviewPage from 'pages/OverviewPage'; import ProfilePage from 'pages/ProfilePage'; @@ -126,6 +127,12 @@ const LoggedInRoutes = () => { path={paths.getCirclesPath()} component={AdminPage} /> + diff --git a/src/types/api.circle.d.ts b/src/types/api.circle.d.ts index ef0150881b..ff40fb52e0 100644 --- a/src/types/api.circle.d.ts +++ b/src/types/api.circle.d.ts @@ -28,6 +28,7 @@ export interface IApiCircle { protocol_id: number; protocol: IProtocol; auto_opt_out: boolean; + dework_organization_id: string | null; } export interface ICircle extends IApiCircle { diff --git a/src/types/api.d.ts b/src/types/api.d.ts index 3c95afb41e..d391f1e48e 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -60,6 +60,7 @@ export interface PutCirclesParam { only_giver_vouch: boolean; team_selection: boolean; auto_opt_out: boolean; + dework_organization_id: string | null; } export interface CreateCircleParam {