diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index 8182969d5..181e81743 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -116,3 +116,9 @@ export const APPLICATION_LANGUAGES_CODE = [ 'ru', 'es' ]; + +export enum IssuesView { + CARDS = 'CARDS', + TABLE = 'TABLE', + BLOCKS = 'BLOCKS' +} diff --git a/apps/web/components/ui/data-table.tsx b/apps/web/components/ui/data-table.tsx new file mode 100644 index 000000000..cae448f7f --- /dev/null +++ b/apps/web/components/ui/data-table.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" + +import { + Table, + TableHeader, + TableRow, + TableHead, + TableCell, + TableBody, + TableFooter, +} from './table'; + +interface DataTableProps { + columns: ColumnDef[] + data: TData[], + footerRows?: React.ReactNode[], + isError?: boolean; + noResultsMessage?: { + heading: string; + content: string; + } +} + +function DataTable({ + columns, + data, + footerRows, +}: DataTableProps) { + + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [sorting, setSorting] = React.useState([]) + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + + { + footerRows && footerRows?.length > 0 && ( + + {footerRows.map((row, index) => ( + {row} + ))} + + ) + } + +
+ ); +} + +export default DataTable; diff --git a/apps/web/components/ui/table.tsx b/apps/web/components/ui/table.tsx new file mode 100644 index 000000000..e59990b4f --- /dev/null +++ b/apps/web/components/ui/table.tsx @@ -0,0 +1,115 @@ +import * as React from "react" + +import { clsxm } from '@app/utils'; + + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/apps/web/lib/features/team-member-cell.tsx b/apps/web/lib/features/team-member-cell.tsx new file mode 100644 index 000000000..84a60ee98 --- /dev/null +++ b/apps/web/lib/features/team-member-cell.tsx @@ -0,0 +1,107 @@ +import { useTeamMemberCard, useTMCardTaskEdit, useCollaborative } from "@app/hooks"; +import { OT_Member } from "@app/interfaces"; +import { clsxm } from "@app/utils"; +import { InputField } from "lib/components"; +import { TaskTimes } from "./task/task-times"; +import { TaskEstimateInfo } from "./team/user-team-card/task-estimate"; +import { TaskInfo } from "./team/user-team-card/task-info"; +import { UserInfo } from "./team/user-team-card/user-info"; +import { UserTeamCardMenu } from "./team/user-team-card/user-team-card-menu"; +import React from "react"; +import get from "lodash/get"; + + +export function TaskCell({ row }: { row: any }) { + const member = row.original as OT_Member; + const memberInfo = useTeamMemberCard(member); + const taskEdition = useTMCardTaskEdit(memberInfo?.memberTask); + const publicTeam = false; + + return ( + + ); +} + +export function UserInfoCell({ cell }: { cell: any}) { + const row = get(cell, 'row', {}); + const member = row.original as OT_Member; + const publicTeam = get(cell, 'column.columnDef.meta.publicTeam', false); + const memberInfo = useTeamMemberCard(member); + + return ( + + ); +} + +export function WorkedOnTaskCell({ row }: { row: any }) { + const member = row.original as OT_Member; + const memberInfo = useTeamMemberCard(member); + + return ( + + ); +} + +export function TaskEstimateInfoCell({ row }: { row: any }) { + const member = row.original as OT_Member; + const memberInfo = useTeamMemberCard(member); + const taskEdition = useTMCardTaskEdit(memberInfo?.memberTask); + + return ( + + ); +} + +export function ActionMenuCell ({ cell }: { cell: any}) { + const row = get(cell, 'row', {}); + const member = row.original as OT_Member; + const active = get(cell, 'column.columnDef.meta.active', false); + const memberInfo = useTeamMemberCard(member); + + const { collaborativeSelect, user_selected, onUserSelect } = useCollaborative( + memberInfo.memberUser + ); + const taskEdition = useTMCardTaskEdit(memberInfo.memberTask); + + return ( + <> + {(!collaborativeSelect || active) && ( + + )} + + {collaborativeSelect && !active && ( + + )} + + ); +} diff --git a/apps/web/lib/features/team-members-card-view.tsx b/apps/web/lib/features/team-members-card-view.tsx new file mode 100644 index 000000000..55b8911df --- /dev/null +++ b/apps/web/lib/features/team-members-card-view.tsx @@ -0,0 +1,131 @@ +import { + useAuthenticateUser, + useModal, + useOrganizationTeams, + useTeamInvitations +} from '@app/hooks'; +import { Transition } from '@headlessui/react'; +import { InviteFormModal } from './team/invite/invite-form-modal'; +import { + InvitedCard, + InviteUserTeamCard +} from './team/invite/user-invite-card'; +import { InviteUserTeamSkeleton, UserTeamCard, UserTeamCardSkeleton } from '.'; +import { OT_Member } from '@app/interfaces'; + +interface Props { + teamMembers: OT_Member[]; + publicTeam: boolean; +} + +const TeamMembersCardView: React.FC = ({ teamMembers, publicTeam=false }) => { + const { isTeamManager, user } = useAuthenticateUser(); + const currentUser = teamMembers.find((m) => { + return m.employee.userId === user?.id; + }); + const { activeTeam, teamsFetching } = useOrganizationTeams(); + const members = activeTeam?.members || []; + + const { teamInvitations } = useTeamInvitations(); + const $teamsFetching = teamsFetching && members.length === 0; + return( +
    + {/* Current authenticated user members */} + +
  • + +
  • +
    + + {/* Team members list */} + {members.map((member) => { + return ( + +
  • + +
  • +
    + ); + })} + + {members.length > 0 && + teamInvitations.map((invitation) => ( +
  • + +
  • + ))} + + {/* Loader skeleton */} + + {[1, 2].map((_, i) => { + return ( +
  • + +
  • + ); + })} +
  • + +
  • +
    + + {/* Invite button */} + +
  • + +
  • +
    +
+ ) + +} + +function Invite() { + const { user } = useAuthenticateUser(); + const { openModal, isOpen, closeModal } = useModal(); + + return ( + <> + + + + ); +} + +export default TeamMembersCardView; diff --git a/apps/web/lib/features/team-members-table-view.tsx b/apps/web/lib/features/team-members-table-view.tsx new file mode 100644 index 000000000..eabac557d --- /dev/null +++ b/apps/web/lib/features/team-members-table-view.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import DataTable from '@components/ui/data-table'; +import { Column, ColumnDef } from '@tanstack/react-table'; +import { OT_Member } from '@app/interfaces'; +import { UserInfoCell, TaskCell, WorkedOnTaskCell, TaskEstimateInfoCell, ActionMenuCell } from './team-member-cell'; +import { useAuthenticateUser, useModal } from '@app/hooks'; +import { InviteUserTeamCard } from './team/invite/user-invite-card'; +import { InviteFormModal } from './team/invite/invite-form-modal'; + +const TeamMembersTableView = ({ + teamMembers, + publicTeam = false, + active = false +}: { + teamMembers: OT_Member[]; + publicTeam?: boolean; + active?: boolean; +}) => { + const columns = React.useMemo[]>( + () => [ + { + id: 'name', + header: 'Name', + cell: UserInfoCell, + meta: { + publicTeam + } + }, + { + id: 'task', + header: 'Task', + cell: TaskCell + }, + { + id: 'workedOnTask', + header: 'Worked on task', + cell: WorkedOnTaskCell + }, + { + id: 'estimate', + header: 'Estimate', + cell: TaskEstimateInfoCell + }, + { + id: 'action', + header: 'Action', + cell: ActionMenuCell, + meta: { + active + } + } + ], + [] + ); + + const footerRows = React.useMemo(() => [], []); + + return ( + []} + data={teamMembers} + footerRows={footerRows} + noResultsMessage={{ + heading: 'No team members found', + content: 'Try adjusting your search or filter to find what you’re looking for.' + }} + /> + ); +}; + +function Invite() { + const { user } = useAuthenticateUser(); + const { openModal, isOpen, closeModal } = useModal(); + + return ( + <> + + + + ); +} + +export default TeamMembersTableView; diff --git a/apps/web/lib/features/team-members.tsx b/apps/web/lib/features/team-members.tsx index d96c025e4..be2763ac3 100644 --- a/apps/web/lib/features/team-members.tsx +++ b/apps/web/lib/features/team-members.tsx @@ -1,132 +1,65 @@ -import { useAuthenticateUser, useModal, useOrganizationTeams, useTeamInvitations } from '@app/hooks'; +import { useAuthenticateUser, useOrganizationTeams } from '@app/hooks'; import { Transition } from '@headlessui/react'; -import { InviteFormModal } from './team/invite/invite-form-modal'; -import { InvitedCard, InviteUserTeamCard } from './team/invite/user-invite-card'; -import { InviteUserTeamSkeleton, UserTeamCard, UserTeamCardSkeleton } from '.'; import UserTeamCardSkeletonCard from '@components/shared/skeleton/UserTeamCardSkeleton'; import InviteUserTeamCardSkeleton from '@components/shared/skeleton/InviteTeamCardSkeleton'; import { UserCard } from '@components/shared/skeleton/TeamPageSkeleton'; -// import { useEffect } from 'react'; -export function TeamMembers({ publicTeam = false }: { publicTeam?: boolean }) { - const { isTeamManager, user } = useAuthenticateUser(); - const { activeTeam, teamsFetching } = useOrganizationTeams(); - const { teamInvitations } = useTeamInvitations(); +import TeamMembersTableView from './team-members-table-view'; +import TeamMembersCardView from './team-members-card-view'; +import { IssuesView } from '@app/constants'; - const members = activeTeam?.members || []; - const $teamsFetching = teamsFetching && members.length === 0; +type TeamMembersProps = { + publicTeam?: boolean; + kabanView?: IssuesView; +}; - const currentUser = members.find((m) => { - return m.employee.userId === user?.id; - }); +export function TeamMembers({ publicTeam = false, kabanView = IssuesView.CARDS }: TeamMembersProps) { + const { user } = useAuthenticateUser(); + const { activeTeam } = useOrganizationTeams(); - const $members = members.filter((m) => { - return m.employee.user?.id !== user?.id; - }); + const members = activeTeam?.members || []; + const currentUser = members.find((m) => m.employee.userId === user?.id); - return members.length === 0 ? ( -
-
- - -
-
- - -
-
- ) : ( -
    - {/* Current authenticated user members */} - -
  • - -
  • -
    +const $members = members.filter((m) => m.employee.user?.id !== user?.id); - {/* Team members list */} - {$members.map((member) => { - return ( - -
  • - -
  • -
    - ); - })} - {members.length > 0 && - teamInvitations.map((invitation) => ( -
  • - -
  • - ))} +let teamMembersView; - {/* Loader skeleton */} - - {[1, 2].map((_, i) => { - return ( -
  • - -
  • - ); - })} -
  • - -
  • -
    - - {/* Invite button */} - -
  • - -
  • -
    -
- ); +switch (true) { + case members.length === 0: + teamMembersView = ( +
+
+ + +
+
+ + +
+
+ ); + break; + case kabanView === IssuesView.CARDS: + teamMembersView = ; + break; + case kabanView === IssuesView.TABLE: + teamMembersView = ( + + + + ); + break; + default: + teamMembersView = ; } - -function Invite() { - const { user } = useAuthenticateUser(); - const { openModal, isOpen, closeModal } = useModal(); - - return ( - <> - - - - ); +return teamMembersView; } diff --git a/apps/web/lib/features/team/invite/user-invite-card.tsx b/apps/web/lib/features/team/invite/user-invite-card.tsx index 2a81b7398..887c22643 100644 --- a/apps/web/lib/features/team/invite/user-invite-card.tsx +++ b/apps/web/lib/features/team/invite/user-invite-card.tsx @@ -225,9 +225,6 @@ export function InviteUserTeamCard({ -
- -
{/* Show user name, email and image */}
@@ -248,59 +245,6 @@ export function InviteUserTeamCard({
- - - {/* Task information */} - - {t('common.TASK_TITTLE')} - - - - {/* TaskTime */} -
- {t('common.TODAY')}: - 00h : 00m -
- - - {/* TaskEstimateInfo */} -
- - : - -
-
- - - {/* Card menu */} -
- 0h : 0m -
- - -
- -
-
-
- -
- {t('common.TASK_TITTLE')} -
- {t('common.TODAY')}: - 0h : 0m -
-
-
- - : - -
-
-
- 0h : 0m -
-
); diff --git a/apps/web/pages/index.tsx b/apps/web/pages/index.tsx index 5f0ee7e8b..d6cc077a7 100644 --- a/apps/web/pages/index.tsx +++ b/apps/web/pages/index.tsx @@ -14,11 +14,15 @@ import { } from 'lib/features'; import { MainHeader, MainLayout } from 'lib/layout'; import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; +import { IssuesView } from '@app/constants'; +import { TableCellsIcon, QueueListIcon } from '@heroicons/react/24/solid'; function MainPage() { const { t } = useTranslation(); const { isTeamMember, isTrackingEnabled, activeTeam } = useOrganizationTeams(); const breadcrumb = [...t('pages.home.BREADCRUMB', { returnObjects: true }), activeTeam?.name || '']; + const [view, setView] = useState(IssuesView.CARDS); return ( @@ -30,24 +34,52 @@ function MainPage() {
{/* */} +
+ + +
-
+
{isTeamMember ? : null} {/* Header user card list */} - {isTeamMember ? : null} + {view === IssuesView.CARDS && isTeamMember ? : null} {/* Divider */}
- {isTeamMember ? : } + {isTeamMember ? : } ); } diff --git a/yarn.lock b/yarn.lock index b153eeb8a..977478325 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5095,7 +5095,7 @@ lodash.merge "^4.6.2" postcss-selector-parser "6.0.10" -"@tanstack/react-table@^8.10.7": +"@tanstack/react-table@8.10.7": version "8.10.7" resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.10.7.tgz#733f4bee8cf5aa19582f944dd0fd3224b21e8c94" integrity sha512-bXhjA7xsTcsW8JPTTYlUg/FuBpn8MNjiEPhkNhIGCUR6iRQM2+WEco4OBpvDeVcR9SE+bmWLzdfiY7bCbCSVuA==