Skip to content

Commit

Permalink
feat: teams page with crew integration
Browse files Browse the repository at this point in the history
  • Loading branch information
asabotovich committed Feb 15, 2024
1 parent 486e9cd commit f857edd
Show file tree
Hide file tree
Showing 25 changed files with 448 additions and 14 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ S3_BUCKET=
S3_PATH_STYLE=
S3_TLS=

NEXT_PUBLIC_CREW_URL=
NEXT_PUBLIC_CREW_API_TOKEN=

# in case you don't have access to unpkg CDN you can include all scripts with framework to main bundle
INCLUDE_SCRIPTS_TO_MAIN_BUNDLE=

Expand Down
24 changes: 24 additions & 0 deletions cypress/fixtures/langs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,10 @@
"Settings": {
"ru": "Настройки",
"en": "Settings"
},
"Team": {
"ru": "Команда",
"en": "Team"
}
},
"ProjectParticipants": {
Expand Down Expand Up @@ -1151,6 +1155,20 @@
"en": "Ok, got it"
}
},
"ProjectTeamPage": {
"title": {
"ru": "Taskany — {project} — Настройки",
"en": "Taskany — {project} — Settings"
},
"Add team": {
"ru": "Добавить команду",
"en": "Add team"
},
"enter the title": {
"ru": "",
"en": ""
}
},
"Reactions": {
"and {count} more": {
"ru": " и еще {count}",
Expand Down Expand Up @@ -1231,6 +1249,12 @@
"en": "Starred"
}
},
"TeamListItem": {
"delete": {
"ru": "удалить",
"en": "delete"
}
},
"UserEditableList": {
"Type user name or email": {
"ru": "Введите имя или email",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "teams" TEXT[];
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ model Project {
personal Boolean? @default(false)
archived Boolean? @default(false)
sharedGoals Goal[] @relation("partnershipProjects")
teams String[]
createdAt DateTime @default(dbgenerated("timezone('utc'::text, now())")) @db.Timestamp()
updatedAt DateTime @default(dbgenerated("timezone('utc'::text, now())")) @updatedAt
Expand Down
3 changes: 2 additions & 1 deletion src/components/ProjectPageTabs/ProjectPageTabs.i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"Goals": "Goals",
"Settings": "Settings"
"Settings": "Settings",
"Team": "Team"
}
3 changes: 2 additions & 1 deletion src/components/ProjectPageTabs/ProjectPageTabs.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"Goals": "Цели",
"Settings": "Настройки"
"Settings": "Настройки",
"Team": "Команда"
}
1 change: 1 addition & 0 deletions src/components/ProjectPageTabs/ProjectPageTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const ProjectPageTabs: FC<{ id: string; editable?: boolean }> = ({ id, ed
() => [
[tr('Goals'), routes.project(id), true],
[tr('Settings'), routes.projectSettings(id), true],
[tr('Team'), routes.projectTeam(id), true],
],
[id],
);
Expand Down
5 changes: 5 additions & 0 deletions src/components/ProjectTeamPage/ProjectTeamPage.i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Taskany — {project} — Settings",
"Add team": "Add team",
"enter the title": "enter the title"
}
17 changes: 17 additions & 0 deletions src/components/ProjectTeamPage/ProjectTeamPage.i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable */
// Do not edit, use generator to update
import { i18n, fmt, I18nLangSet } from 'easy-typed-intl';
import getLang from '../../../utils/getLang';

import ru from './ru.json';
import en from './en.json';

export type I18nKey = keyof typeof ru & keyof typeof en;
type I18nLang = 'ru' | 'en';

const keyset: I18nLangSet<I18nKey> = {};

keyset['ru'] = ru;
keyset['en'] = en;

export const tr = i18n<I18nLang, I18nKey>(keyset, fmt, getLang);
5 changes: 5 additions & 0 deletions src/components/ProjectTeamPage/ProjectTeamPage.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Taskany — {project} — Настройки",
"Add team": "Добавить команду",
"enter the title": "введите название команды"
}
4 changes: 4 additions & 0 deletions src/components/ProjectTeamPage/ProjectTeamPage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.ProjectTeamPageActions {
display: inline-block;
margin-bottom: var(--gap-s);
}
106 changes: 106 additions & 0 deletions src/components/ProjectTeamPage/ProjectTeamPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { nullable } from '@taskany/bricks';
import { Link, TreeView, TreeViewElement, TreeViewNode } from '@taskany/bricks/harmony';
import { useCallback } from 'react';

import { ExternalPageProps } from '../../utils/declareSsrProps';
import { routes } from '../../hooks/router';
import { useProjectResource } from '../../hooks/useProjectResource';
import { trpc } from '../../utils/trpcClient';
import { pageHeader } from '../../utils/domObjects';
import { PageContent } from '../PageContent/PageContent';
import { Page } from '../Page/Page';
import { ProjectPageTabs } from '../ProjectPageTabs/ProjectPageTabs';
import { TeamListItem } from '../TeamListItem/TeamListItem';
import { TeamComboBox } from '../TeamComboBox';
import { CommonHeader } from '../CommonHeader';
import { PageSep } from '../PageSep';
import { TextList, TextListItem } from '../TextList';
import { UserBadge } from '../UserBadge';
import { Team } from '../../types/crew';

import { tr } from './ProjectTeamPage.i18n';
import s from './ProjectTeamPage.module.css';

export const ProjectTeamPage = ({ user, ssrTime, params: { id } }: ExternalPageProps) => {
const { data: project } = trpc.project.getById.useQuery({ id });
const { updateProject } = useProjectResource(id);

const ids = project?.teams ?? [];

const { data: teams } = trpc.crew.getTeamByIds.useQuery(
{ ids },
{
keepPreviousData: true,
enabled: Boolean(ids.length),
},
);

const onRemove = useCallback(
({ id }: Team) => {
if (project) {
updateProject()({
...project,
teams: project.teams.filter((teamId) => teamId !== id),
});
}
},
[project, updateProject],
);

if (!project) return null;

const pageTitle = tr
.raw('title', {
project: project.title,
})
.join('');

return (
<Page user={user} ssrTime={ssrTime} title={pageTitle}>
<CommonHeader title={project.title} description={project.description} {...pageHeader.attr}>
<ProjectPageTabs id={id} editable />
</CommonHeader>

<PageSep />

<PageContent>
<div className={s.ProjectTeamPageActions}>
<TeamComboBox project={project} text={tr('Add team')} placeholder={tr('enter the title')} />
</div>
{nullable(teams, (ts) => (
<TreeView>
{ts.map((t) => (
<TreeViewNode
title={
<TeamListItem
name={t.name}
href={routes.crewTeam(t.id)}
onRemoveClick={() => onRemove(t)}
/>
}
key={t.id}
visible
>
<TextList listStyle="none">
{t.memberships.map(({ user }) => (
<TreeViewElement key={user.id}>
<TextListItem>
<Link target="_blank" href={routes.crewUser(user.id)}>
<UserBadge
image={user.image}
name={user.name ?? user.email}
email={user.email}
/>
</Link>
</TextListItem>
</TreeViewElement>
))}
</TextList>
</TreeViewNode>
))}
</TreeView>
))}
</PageContent>
</Page>
);
};
104 changes: 104 additions & 0 deletions src/components/TeamComboBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useCallback, useState, ChangeEvent } from 'react';
import { ComboBox } from '@taskany/bricks';
import { Button, Input } from '@taskany/bricks/harmony';
import { IconPlusCircleOutline } from '@taskany/icons';

import { trpc } from '../utils/trpcClient';
import { Team } from '../types/crew';
import { useProjectResource } from '../hooks/useProjectResource';
import { ProjectByIdReturnType } from '../../trpc/inferredTypes';

import { ProjectMenuItem } from './ProjectMenuItem';

interface TeamComboBoxProps {
text?: React.ComponentProps<typeof Button>['text'];
placeholder?: string;
disabled?: boolean;
project: NonNullable<ProjectByIdReturnType>;
}

export const TeamComboBox = React.forwardRef<HTMLDivElement, TeamComboBoxProps & React.HTMLAttributes<HTMLDivElement>>(
({ text, project, disabled, placeholder, ...attrs }, ref) => {
const { updateProject } = useProjectResource(project.id);

const [search, setSearch] = useState('');
const [visible, setVisible] = useState(false);

const { data } = trpc.crew.teamSuggetions.useQuery(
{
search,
take: 3,
},
{
enabled: search.length >= 1,
},
);

const onClickOutside = useCallback((cb: () => void) => cb(), []);

const onChange = useCallback(
(value?: Team) => {
setSearch('');

if (value) {
updateProject()({
...project,
teams: [...project.teams, value.id],
});
}
},
[updateProject, project],
);

const resetSearch = useCallback(() => {
setSearch('');
}, []);

return (
<ComboBox
ref={ref}
text={text}
value={search}
items={data}
onChange={onChange}
visible={visible}
disabled={disabled}
onClickOutside={onClickOutside}
onClose={resetSearch}
renderTrigger={(props) => (
<Button
view="ghost"
text={props.text}
disabled={props.disabled}
onClick={props.onClick}
iconLeft={<IconPlusCircleOutline size="s" />}
/>
)}
renderInput={(props) => (
<Input
outline
autoFocus
disabled={props.disabled}
placeholder={placeholder}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.currentTarget.value);
setVisible(true);
}}
{...props}
/>
)}
renderItem={(props) => {
return (
<ProjectMenuItem
key={props.item.id}
focused={props.cursor === props.index}
onClick={props.onClick}
title={props.item.name}
/>
);
}}
{...attrs}
/>
);
},
);
3 changes: 3 additions & 0 deletions src/components/TeamListItem/TeamListItem.i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"delete": "delete"
}
17 changes: 17 additions & 0 deletions src/components/TeamListItem/TeamListItem.i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable */
// Do not edit, use generator to update
import { i18n, fmt, I18nLangSet } from 'easy-typed-intl';
import getLang from '../../../utils/getLang';

import ru from './ru.json';
import en from './en.json';

export type I18nKey = keyof typeof ru & keyof typeof en;
type I18nLang = 'ru' | 'en';

const keyset: I18nLangSet<I18nKey> = {};

keyset['ru'] = ru;
keyset['en'] = en;

export const tr = i18n<I18nLang, I18nKey>(keyset, fmt, getLang);
3 changes: 3 additions & 0 deletions src/components/TeamListItem/TeamListItem.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"delete": "удалить"
}
14 changes: 14 additions & 0 deletions src/components/TeamListItem/TeamListItem.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.TeamListItemLink {
width: 100%;
display: flex;
justify-content: space-between;
cursor: pointer;
background-color: var(--gray4);
padding: var(--gap-s);
border-radius: var(--radius-m);
margin: var(--gap-xs) 0;
}

.TeamListItemLink:hover {
background-color: var(--gray6);
}
Loading

0 comments on commit f857edd

Please sign in to comment.