diff --git a/.all-contributorsrc b/.all-contributorsrc index 778abd0e19..a3241e045d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -215,10 +215,20 @@ "contributions": [ "code" ] + }, + { + "login": "NoHara42", + "name": "Ned O'Hara", + "avatar_url": "https://avatars.githubusercontent.com/u/43496778?v=4", + "profile": "https://github.com/NoHara42", + "contributions": [ + "code" + ] } ], "projectName": "community-platform", "projectOwner": "ONEARMY", "repoType": "github", - "repoHost": "https://github.com" + "repoHost": "https://github.com", + "commitConvention": "angular" } diff --git a/.circleci/config.yml b/.circleci/config.yml index 004688b64f..76acb42d06 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,6 +17,8 @@ orbs: browser-tools: circleci/browser-tools@1.1.3 # used to enable slack integration (required api key set in environment) slack: circleci/slack@4.4.2 + # used to track coverage + codecov: codecov/codecov@3.2.4 ###################################################################################################### # Aliases - code snippets that can be included inline in any other markup @@ -284,6 +286,12 @@ jobs: - run: # NOTE - run-in-band to try reduce memory leaks (https://github.com/facebook/jest/issues/7874) command: yarn run test:unit:ci && yarn run test:components + - store_artifacts: + path: coverage + - store_artifacts: + path: packages/components/coverage + - codecov/upload + test_circular_dependency: docker: *docker steps: @@ -295,6 +303,7 @@ jobs: command: yarn workspaces focus one-army-community-platform - run: command: yarn run test:madge + build: <<: *docker_matrix environment: diff --git a/README.md b/README.md index a11e8dd910..1d54fd19c2 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Thanks go to these wonderful people ([emoji key](https://allcontributors.org/doc Devtato
Devtato

💻 + Ned O'Hara
Ned O'Hara

💻 diff --git a/functions/src/emailNotifications/createEmail.ts b/functions/src/emailNotifications/createEmail.ts index 883d1b5cfb..e659178597 100644 --- a/functions/src/emailNotifications/createEmail.ts +++ b/functions/src/emailNotifications/createEmail.ts @@ -13,6 +13,7 @@ const getResourceLabelFromNotificationType = (type: NotificationType) => { return 'research' case 'howto_useful': case 'howto_mention': + case 'new_comment': return 'how-to' } } diff --git a/functions/src/emailNotifications/index.ts b/functions/src/emailNotifications/index.ts index 29b2848196..fe487b4a4f 100644 --- a/functions/src/emailNotifications/index.ts +++ b/functions/src/emailNotifications/index.ts @@ -1,8 +1,41 @@ import * as functions from 'firebase-functions' import { createNotificationEmails } from './createEmail' +import { db } from '../Firebase/firestoreDB' +import { DB_ENDPOINTS, IUserDB } from '../models' /** Trigger daily process to send any pending email notifications */ exports.sendDaily = functions.pubsub // Trigger daily at 5pm (https://crontab.guru/#0_17_*_*_*) .schedule('0 17 * * *') .onRun(async () => createNotificationEmails()) + +exports.sendOnce = functions.https.onCall(async (_, context) => { + if (!context.auth) { + throw new functions.https.HttpsError( + 'failed-precondition', + 'The function must be called while authenticated.', + ) + } + // Validate user exists and has admin status before triggering function. + const { uid } = context.auth + const user = await db.collection(DB_ENDPOINTS.users).doc(uid).get() + if (user.exists) { + const { userRoles } = user.data() as IUserDB + if (userRoles?.some((role) => ['admin', 'super-admin'].includes(role))) { + try { + await createNotificationEmails() + return 'OK' + } catch (error) { + console.error(error) + throw new functions.https.HttpsError( + 'internal', + 'There was an error creating emails.', + ) + } + } + } + throw new functions.https.HttpsError( + 'permission-denied', + 'Emails can be triggered by admins only.', + ) +}) diff --git a/package.json b/package.json index 818cbc2bbf..091685cad9 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "test": "yarn workspace oa-cypress start", "test:components": "yarn workspace oa-components test", "test:unit": "env-cmd -e cra craco test --env=jsdom", - "test:unit:ci": "yarn build:themes && yarn build:components && env-cmd -e cra craco test --env=jsdom --runInBand --logHeapUsage", + "test:unit:ci": "yarn build:themes && yarn build:components && env-cmd -e cra craco test --env=jsdom --runInBand --logHeapUsage --coverage", "test:madge": "npx madge --circular --extensions ts,tsx ./ --exclude src/stores", "storybook": "yarn workspace oa-components start", "storybook:build": "yarn build:themes && yarn workspace oa-components build:sb", diff --git a/packages/components/.gitignore b/packages/components/.gitignore index 7fa505ce04..390cc2fb1c 100644 --- a/packages/components/.gitignore +++ b/packages/components/.gitignore @@ -1,3 +1,4 @@ node_modules storybook-static -dist \ No newline at end of file +dist +coverage \ No newline at end of file diff --git a/packages/components/package.json b/packages/components/package.json index 4040b5c3fe..5fd002cdc3 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -14,7 +14,7 @@ "dev": "tsc --watch", "lint": "eslint . --ext .js,.jsx,.ts,.tsx src --color", "new-component": "ts-node scripts/newComponent.ts", - "test": "vitest" + "test": "vitest --coverage" }, "dependencies": { "@emotion/react": "^11.10.6", @@ -36,7 +36,7 @@ "storybook": "^7.0.4", "theme-ui": "^0.15.7", "use-debounce": "^8.0.4", - "webpack": "^5.75.0", + "webpack": "5.76.0", "yup": "^1.1.1" }, "peerDependencies": { @@ -56,6 +56,7 @@ "@types/react-flag-icon-css": "^1.0.5", "@types/react-portal": "^4.0.4", "@vitejs/plugin-react": "^3.1.0", + "@vitest/coverage-c8": "^0.30.1", "babel-loader": "8.1.0", "eslint": "^7.32.0", "eslint-plugin-import": "^2.25.4", @@ -67,6 +68,6 @@ "react-dom": "^17.0.2", "ts-node": "^10.7.0", "typescript": "^4.5.5", - "vitest": "^0.29.5" + "vitest": "^0.30.1" } } diff --git a/packages/components/src/Button/Button.tsx b/packages/components/src/Button/Button.tsx index 950d5a0a60..5c15f65ccf 100644 --- a/packages/components/src/Button/Button.tsx +++ b/packages/components/src/Button/Button.tsx @@ -11,6 +11,7 @@ export interface IBtnProps extends React.ButtonHTMLAttributes { small?: boolean large?: boolean showIconOnly?: boolean + iconColor?: string } type ToArray = [Type] extends [any] ? Type[] : never @@ -112,7 +113,7 @@ export const Button = (props: BtnProps) => { pointerEvents: 'none', }} > - + )} ` min-width: ${(props) => (props.size ? `${props.size}px` : '32px')}; min-height: ${(props) => (props.size ? `${props.size}px` : '32px')}; position: relative; - color: ${(props) => (props.color ? `${props.color}` : 'inherit')}; - ${verticalAlign} - ${space} - - ${(props) => + color: ${(props) => props.color || 'inherit'}; + ${verticalAlign} ${space} + ${(props) => props.onClick && ` cursor: pointer; - `} + `}; ` const sizeMap = { diff --git a/packages/components/src/UserStatistics/__snapshots__/UserStatistics.test.tsx.snap b/packages/components/src/UserStatistics/__snapshots__/UserStatistics.test.tsx.snap index 87c4ecb63b..5c2121c4bf 100644 --- a/packages/components/src/UserStatistics/__snapshots__/UserStatistics.test.tsx.snap +++ b/packages/components/src/UserStatistics/__snapshots__/UserStatistics.test.tsx.snap @@ -12,7 +12,7 @@ exports[`UserStatistics > renders correctly 1`] = ` class="css-i1ihic-Box" >
renders correctly 1`] = ` class="css-i1ihic-Box" >
renders correctly 1`] = ` class="css-i1ihic-Box" >
@@ -33,6 +34,9 @@ const NotificationListContainer = styled(Box)` const AdminNotifictions = observer(() => { const { db } = useDB() const [emailsPending, setEmailsPending] = useState([]) + const [triggerEmailState, setTriggerEmailState] = useState( + 'Click the button to trigger emails.', + ) // Load list of pending approvals on mount only, dependencies empty to avoid reloading useEffect(() => { @@ -55,6 +59,16 @@ const AdminNotifictions = observer(() => { }) }, []) + const triggerEmails = async () => { + setTriggerEmailState('Sending...') + try { + await functions.httpsCallable('emailNotifications-sendOnce')() + setTriggerEmailState('Emails sent successfully.') + } catch (error) { + setTriggerEmailState(`Error sending emails: \n ${error}`) + } + } + /** Function applied to render each table row cell */ const RenderContent: React.FC = ( props: ICellRenderProps, @@ -82,6 +96,8 @@ const AdminNotifictions = observer(() => { <>

Admin Notifictions

Pending Emails

+ + {triggerEmailState &&

{triggerEmailState}

} ({ @@ -28,7 +31,9 @@ jest.mock('src/index', () => ({ jest.mock('src/stores/Research/research.store') -import ResearchArticle from './ResearchArticle' +const activeUser = FactoryUser({ + userRoles: ['beta-tester'], +}) describe('Research Article', () => { const mockResearchStore = { @@ -75,6 +80,49 @@ describe('Research Article', () => { expect(wrapper.getAllByText('another-example-username')).toHaveLength(1) }) + it('displays "Follow" button text and color if not subscribed', async () => { + // Arrange + ;(useResearchStore as jest.Mock).mockReturnValue({ + ...mockResearchStore, + }) + + // Act + const wrapper = getWrapper() + const followingButton = wrapper.getByTestId('following-button') + + // Assert + expect(wrapper.getAllByText('Follow').length).toBeGreaterThan(0) + expect(followingButton).toBeInTheDocument() + expect(followingButton).toHaveAttribute( + 'iconcolor', + Theme.colors.notSubscribed, + ) + }) + + it('displays "Following" button text and color if user is subscribed', async () => { + // Arrange + ;(useResearchStore as jest.Mock).mockReturnValue({ + ...mockResearchStore, + activeResearchItem: FactoryResearchItem({ + subscribers: [activeUser.userName], + }), + activeUser, + }) + + // Act + const wrapper = getWrapper() + const followingButton = wrapper.getByTestId('following-button') + + // Assert + expect(wrapper.getAllByText('Following').length).toBeGreaterThan(0) + + expect(followingButton).toBeInTheDocument() + expect(followingButton).toHaveAttribute( + 'iconcolor', + Theme.colors.subscribed, + ) + }) + describe('Research Update', () => { it('displays contributors', async () => { // Arrange @@ -147,7 +195,11 @@ describe('Research Article', () => { const getWrapper = () => { return render( - + { )}