From 4ccff8cb2908fb28b4e294ba60c69b95b66b976b Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 2 Jan 2023 11:26:51 -0500 Subject: [PATCH] chore: Convert GroupListItem, AddGroupsToCollection, AddPeopleToCollection, Drafts to functional components --- app/components/GroupListItem.tsx | 124 +++++------ .../AddGroupsToCollection.tsx | 1 - .../AddPeopleToCollection.tsx | 193 +++++++++--------- app/scenes/CollectionPermissions/index.tsx | 10 +- app/scenes/Drafts.tsx | 177 +++++++--------- shared/i18n/locales/en_US/translation.json | 4 +- 6 files changed, 226 insertions(+), 283 deletions(-) diff --git a/app/components/GroupListItem.tsx b/app/components/GroupListItem.tsx index a4ceaf9bd..5f5fa4c5d 100644 --- a/app/components/GroupListItem.tsx +++ b/app/components/GroupListItem.tsx @@ -1,10 +1,9 @@ -import { observable } from "mobx"; import { observer } from "mobx-react"; import { GroupIcon } from "outline-icons"; import * as React from "react"; +import { useTranslation } from "react-i18next"; import styled from "styled-components"; import { MAX_AVATAR_DISPLAY } from "@shared/constants"; -import RootStore from "~/stores/RootStore"; import CollectionGroupMembership from "~/models/CollectionGroupMembership"; import Group from "~/models/Group"; import GroupMembers from "~/scenes/GroupMembers"; @@ -12,10 +11,11 @@ import Facepile from "~/components/Facepile"; import Flex from "~/components/Flex"; import ListItem from "~/components/List/Item"; import Modal from "~/components/Modal"; -import withStores from "~/components/withStores"; +import useBoolean from "~/hooks/useBoolean"; +import useStores from "~/hooks/useStores"; import NudeButton from "./NudeButton"; -type Props = RootStore & { +type Props = { group: Group; membership?: CollectionGroupMembership; showFacepile?: boolean; @@ -23,71 +23,57 @@ type Props = RootStore & { renderActions: (params: { openMembersModal: () => void }) => React.ReactNode; }; -@observer -class GroupListItem extends React.Component { - @observable - membersModalOpen = false; +function GroupListItem({ group, showFacepile, renderActions }: Props) { + const { groupMemberships } = useStores(); + const { t } = useTranslation(); + const [ + membersModalOpen, + setMembersModalOpen, + setMembersModalClosed, + ] = useBoolean(); + const memberCount = group.memberCount; + const membershipsInGroup = groupMemberships.inGroup(group.id); + const users = membershipsInGroup + .slice(0, MAX_AVATAR_DISPLAY) + .map((gm) => gm.user); + const overflow = memberCount - users.length; - handleMembersModalOpen = () => { - this.membersModalOpen = true; - }; - - handleMembersModalClose = () => { - this.membersModalOpen = false; - }; - - render() { - const { group, groupMemberships, showFacepile, renderActions } = this.props; - const memberCount = group.memberCount; - const membershipsInGroup = groupMemberships.inGroup(group.id); - const users = membershipsInGroup - .slice(0, MAX_AVATAR_DISPLAY) - .map((gm) => gm.user); - const overflow = memberCount - users.length; - - return ( - <> - - - - } - title={ - {group.name} - } - subtitle={ - <> - {memberCount} member{memberCount === 1 ? "" : "s"} - - } - actions={ - - {showFacepile && ( - - - - )} - {renderActions({ - openMembersModal: this.handleMembersModalOpen, - })} - - } - /> - - - - - ); - } + return ( + <> + + + + } + title={{group.name}} + subtitle={t("{{ count }} members", { count: memberCount })} + actions={ + + {showFacepile && ( + + + + )} + {renderActions({ + openMembersModal: setMembersModalOpen, + })} + + } + /> + + + + + ); } const Image = styled(Flex)` @@ -106,4 +92,4 @@ const Title = styled.span` } `; -export default withStores(GroupListItem); +export default observer(GroupListItem); diff --git a/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx b/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx index 69fe235bd..4ff29053f 100644 --- a/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx +++ b/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx @@ -20,7 +20,6 @@ import useStores from "~/hooks/useStores"; type Props = { collection: Collection; - onSubmit: () => void; }; function AddGroupsToCollection(props: Props) { diff --git a/app/scenes/CollectionPermissions/AddPeopleToCollection.tsx b/app/scenes/CollectionPermissions/AddPeopleToCollection.tsx index 2836b4449..3c4375a2a 100644 --- a/app/scenes/CollectionPermissions/AddPeopleToCollection.tsx +++ b/app/scenes/CollectionPermissions/AddPeopleToCollection.tsx @@ -1,9 +1,6 @@ -import { debounce } from "lodash"; -import { observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; -import { WithTranslation, withTranslation } from "react-i18next"; -import RootStore from "~/stores/RootStore"; +import { useTranslation } from "react-i18next"; import Collection from "~/models/Collection"; import User from "~/models/User"; import Invite from "~/scenes/Invite"; @@ -14,51 +11,51 @@ import Input from "~/components/Input"; import Modal from "~/components/Modal"; import PaginatedList from "~/components/PaginatedList"; import Text from "~/components/Text"; -import withStores from "~/components/withStores"; +import useBoolean from "~/hooks/useBoolean"; +import useCurrentTeam from "~/hooks/useCurrentTeam"; +import useCurrentUser from "~/hooks/useCurrentUser"; +import useDebouncedCallback from "~/hooks/useDebouncedCallback"; +import useStores from "~/hooks/useStores"; +import useToasts from "~/hooks/useToasts"; import MemberListItem from "./components/MemberListItem"; -type Props = WithTranslation & - RootStore & { - collection: Collection; - onSubmit: () => void; +type Props = { + collection: Collection; +}; + +function AddPeopleToCollection({ collection }: Props) { + const { memberships, users } = useStores(); + const { showToast } = useToasts(); + const user = useCurrentUser(); + const team = useCurrentTeam(); + const { t } = useTranslation(); + const [ + inviteModalOpen, + setInviteModalOpen, + setInviteModalClosed, + ] = useBoolean(); + const [query, setQuery] = React.useState(""); + + const handleFilter = (ev: React.ChangeEvent) => { + setQuery(ev.target.value); + debouncedFetch(ev.target.value); }; -@observer -class AddPeopleToCollection extends React.Component { - @observable - inviteModalOpen = false; - - @observable - query = ""; - - handleInviteModalOpen = () => { - this.inviteModalOpen = true; - }; - - handleInviteModalClose = () => { - this.inviteModalOpen = false; - }; - - handleFilter = (ev: React.ChangeEvent) => { - this.query = ev.target.value; - this.debouncedFetch(); - }; - - debouncedFetch = debounce(() => { - this.props.users.fetchPage({ - query: this.query, - }); - }, 250); - - handleAddUser = (user: User) => { - const { t } = this.props; + const debouncedFetch = useDebouncedCallback( + (query) => + users.fetchPage({ + query, + }), + 250 + ); + const handleAddUser = (user: User) => { try { - this.props.memberships.create({ - collectionId: this.props.collection.id, + memberships.create({ + collectionId: collection.id, userId: user.id, }); - this.props.toasts.showToast( + showToast( t("{{ userName }} was added to the collection", { userName: user.name, }), @@ -67,71 +64,63 @@ class AddPeopleToCollection extends React.Component { } ); } catch (err) { - this.props.toasts.showToast(t("Could not add user"), { + showToast(t("Could not add user"), { type: "error", }); } }; - render() { - const { users, collection, auth, t } = this.props; - const { user, team } = auth; - if (!user || !team) { - return null; - } - - return ( - - - {t("Need to add someone who’s not yet on the team yet?")}{" "} - - {t("Invite people to {{ teamName }}", { - teamName: team.name, - })} - - . - - - {t("No people matching your search")} - ) : ( - {t("No people left to add")} - ) - } - items={users - .notInCollection(collection.id, this.query) - .filter((member) => member.id !== user.id)} - fetch={this.query ? undefined : users.fetchPage} - renderItem={(item: User) => ( - this.handleAddUser(item)} - canEdit - /> - )} - /> - - - - - ); - } + return ( + + + {t("Need to add someone who’s not yet on the team yet?")}{" "} + + {t("Invite people to {{ teamName }}", { + teamName: team.name, + })} + + . + + + {t("No people matching your search")} + ) : ( + {t("No people left to add")} + ) + } + items={users + .notInCollection(collection.id, query) + .filter((member) => member.id !== user.id)} + fetch={query ? undefined : users.fetchPage} + renderItem={(item: User) => ( + handleAddUser(item)} + canEdit + /> + )} + /> + + + + + ); } -export default withTranslation()(withStores(AddPeopleToCollection)); +export default observer(AddPeopleToCollection); diff --git a/app/scenes/CollectionPermissions/index.tsx b/app/scenes/CollectionPermissions/index.tsx index 3f2c9e950..38817b190 100644 --- a/app/scenes/CollectionPermissions/index.tsx +++ b/app/scenes/CollectionPermissions/index.tsx @@ -331,10 +331,7 @@ function CollectionPermissions({ collectionId }: Props) { onRequestClose={handleAddGroupModalClose} isOpen={addGroupModalOpen} > - + - + ); diff --git a/app/scenes/Drafts.tsx b/app/scenes/Drafts.tsx index f3244c75f..767fd5562 100644 --- a/app/scenes/Drafts.tsx +++ b/app/scenes/Drafts.tsx @@ -1,14 +1,12 @@ -import { observable } from "mobx"; import { observer } from "mobx-react"; import { EditIcon } from "outline-icons"; import queryString from "query-string"; import * as React from "react"; -import { WithTranslation, withTranslation } from "react-i18next"; -import { RouteComponentProps } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useHistory, useLocation } from "react-router-dom"; import styled from "styled-components"; -import RootStore from "~/stores/RootStore"; +import { DateFilter as TDateFilter } from "@shared/types"; import CollectionFilter from "~/scenes/Search/components/CollectionFilter"; -import DateFilter from "~/scenes/Search/components/DateFilter"; import { Action } from "~/components/Actions"; import Empty from "~/components/Empty"; import Flex from "~/components/Flex"; @@ -17,34 +15,27 @@ import InputSearchPage from "~/components/InputSearchPage"; import PaginatedDocumentList from "~/components/PaginatedDocumentList"; import Scene from "~/components/Scene"; import Subheading from "~/components/Subheading"; -import withStores from "~/components/withStores"; +import useStores from "~/hooks/useStores"; import NewDocumentMenu from "~/menus/NewDocumentMenu"; +import DateFilter from "./Search/components/DateFilter"; -type Props = WithTranslation & RouteComponentProps & RootStore; +function Drafts() { + const { t } = useTranslation(); + const { documents } = useStores(); + const history = useHistory(); + const location = useLocation(); + const params = new URLSearchParams(location.search); + const collectionId = params.get("collectionId") || undefined; + const dateFilter = (params.get("dateFilter") || undefined) as TDateFilter; -@observer -class Drafts extends React.Component { - @observable - params: URLSearchParams = new URLSearchParams(this.props.location.search); - - componentDidUpdate(prevProps: Props) { - if (prevProps.location.search !== this.props.location.search) { - this.handleQueryChange(); - } - } - - handleQueryChange = () => { - this.params = new URLSearchParams(this.props.location.search); - }; - - handleFilterChange = (search: { + const handleFilterChange = (search: { dateFilter?: string | null | undefined; collectionId?: string | null | undefined; }) => { - this.props.history.replace({ - pathname: this.props.location.pathname, + history.replace({ + pathname: location.pathname, search: queryString.stringify( - { ...queryString.parse(this.props.location.search), ...search }, + { ...queryString.parse(location.search), ...search }, { skipEmptyString: true, } @@ -52,84 +43,66 @@ class Drafts extends React.Component { }); }; - get collectionId() { - const id = this.params.get("collectionId"); - return id ? id : undefined; - } + const isFiltered = collectionId || dateFilter; + const options = { + dateFilter, + collectionId, + }; - get dateFilter() { - const id = this.params.get("dateFilter"); - return (id ? id : undefined) as - | "day" - | "week" - | "month" - | "year" - | undefined; - } + return ( + } + title={t("Drafts")} + actions={ + <> + + + + + + + + } + > + {t("Drafts")} + + {t("Documents")} + + + handleFilterChange({ + collectionId, + }) + } + /> + + handleFilterChange({ + dateFilter, + }) + } + /> + + - render() { - const { t } = this.props; - const isFiltered = this.collectionId || this.dateFilter; - const options = { - dateFilter: this.dateFilter, - collectionId: this.collectionId, - }; - - return ( - } - title={t("Drafts")} - actions={ - <> - - - - - - - + + {isFiltered + ? t("No documents found for your filters.") + : t("You’ve not got any drafts at the moment.")} + } - > - {t("Drafts")} - - {t("Documents")} - - - this.handleFilterChange({ - collectionId, - }) - } - /> - - this.handleFilterChange({ - dateFilter, - }) - } - /> - - - - - {isFiltered - ? t("No documents found for your filters.") - : t("You’ve not got any drafts at the moment.")} - - } - fetch={this.props.documents.fetchDrafts} - documents={this.props.documents.drafts(options)} - options={options} - showParentDocuments - showCollection - /> - - ); - } + fetch={documents.fetchDrafts} + documents={documents.drafts(options)} + options={options} + showParentDocuments + showCollection + /> + + ); } const Filters = styled(Flex)` @@ -145,4 +118,4 @@ const Filters = styled(Flex)` } `; -export default withTranslation()(withStores(Drafts)); +export default observer(Drafts); diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index c254509bd..471b973bd 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -168,6 +168,9 @@ "You will receive an email when it's complete.": "You will receive an email when it's complete.", "A ZIP file containing the images, and documents in the Markdown format.": "A ZIP file containing the images, and documents in the Markdown format.", "A ZIP file containing the images, and documents as HTML files.": "A ZIP file containing the images, and documents as HTML files.", + "{{ count }} members": "{{ count }} members", + "{{ count }} members_plural": "{{ count }} members", + "Group members": "Group members", "Icon": "Icon", "Show menu": "Show menu", "Choose icon": "Choose icon", @@ -537,7 +540,6 @@ "Groups are for organizing your team. They work best when centered around a function or a responsibility — Support or Engineering for example.": "Groups are for organizing your team. They work best when centered around a function or a responsibility — Support or Engineering for example.", "You’ll be able to add people to the group next.": "You’ll be able to add people to the group next.", "Continue": "Continue", - "Group members": "Group members", "Recently viewed": "Recently viewed", "Created by me": "Created by me", "Weird, this shouldn’t ever be empty": "Weird, this shouldn’t ever be empty",