Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions app/frontend/src/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export interface Proof {
code?: string;
}

export type GoogleFinish = { signedIn: true } | { challenge: string } | { connected: 'link' | 'calendar' | 'drive'; returnTo: string };
export type GoogleChoice = { choose: 'email' | 'browser'; email: string };

export type GoogleFinish = { signedIn: true } | { challenge: string } | { connected: 'link' | 'calendar' | 'drive'; returnTo: string } | GoogleChoice;

interface AuthContextValue {
user?: User;
Expand All @@ -62,6 +64,8 @@ interface AuthContextValue {
login(login: string, password: string): Promise<{ challenge: string } | undefined>;
completeTwoStep(challenge: string, code: string): Promise<void>;
finishGoogle(code: string, state: string): Promise<GoogleFinish>;
linkGoogleToExisting(login: string, password: string, code?: string): Promise<{ twoStep: true } | undefined>;
createFromGoogle(): Promise<void>;
logout(): Promise<void>;
changePassword(proof: Proof, newPassword: string): Promise<void>;
deleteAccount(proof: Proof): Promise<void>;
Expand Down Expand Up @@ -187,15 +191,27 @@ export function AuthProvider({ children }: { children: ReactNode }) {
);
},
finishGoogle: async (code, state) => {
const result = await apiRequest<Session | { twoStep: true; challenge: string } | { connected: 'link' | 'calendar' | 'drive'; returnTo: string }>('/auth/google/finish', {
const result = await apiRequest<Session | { twoStep: true; challenge: string } | { connected: 'link' | 'calendar' | 'drive'; returnTo: string } | GoogleChoice>('/auth/google/finish', {
method: 'POST',
body: JSON.stringify({ code, state }),
});
if ('challenge' in result) return { challenge: result.challenge };
if ('connected' in result) return result;
if ('connected' in result || 'choose' in result) return result;
applySession(result);
return { signedIn: true };
},
linkGoogleToExisting: async (login, password, code) => {
const result = await apiRequest<Session | { twoStep: true }>('/auth/google/existing', {
method: 'POST',
body: JSON.stringify({ login, password, ...(code ? { code } : {}) }),
});
if ('twoStep' in result) return result;
applySession(result);
return undefined;
},
createFromGoogle: async () => {
applySession(await apiRequest<Session>('/auth/google/new', { method: 'POST' }));
},
logout: async () => {
await apiRequest('/auth/logout', { method: 'POST' });
applySession(undefined);
Expand Down
10 changes: 9 additions & 1 deletion app/frontend/src/components/AssigneeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Member } from '../spaces';
import type { Task } from '../tasks';
import { useResource } from '../useResource';
import { UserAvatar } from './UserAvatar';
import { Tip } from './Tip';

export function AssigneeButton({ onAssign, task }: { onAssign?: (assignee: Task['assignee']) => void; task: Task }) {
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
Expand All @@ -21,7 +22,14 @@ export function AssigneeButton({ onAssign, task }: { onAssign?: (assignee: Task[
<Icon name="user" size={14} />
</span>
);
if (!onAssign) return task.assignee && <span title={label}>{mark}</span>;
if (!onAssign)
return (
task.assignee && (
<Tip title={label}>
<span>{mark}</span>
</Tip>
)
);
const choose = (assignee: Task['assignee']) => {
setAnchor(null);
if (assignee?.id !== task.assignee?.id) onAssign(assignee);
Expand Down
3 changes: 2 additions & 1 deletion app/frontend/src/components/BoardCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { CaptureUpdateAction, convertToExcalidrawElements, Excalidraw, exportToB
import type { RemoteExcalidrawElement } from '@excalidraw/excalidraw/data/reconcile';
import type { ExcalidrawElement, ExcalidrawTextElement, FileId } from '@excalidraw/excalidraw/element/types';
import type { AppState, Collaborator, ExcalidrawImperativeAPI, ExcalidrawProps, SocketId } from '@excalidraw/excalidraw/types';
import { Banner, FloatingToolbar, IconButton, LinesSkeleton, ToolbarButton, ToolbarSeparator, useSnackbar, type MenuItem } from '@project-graphite/ui';
import { Banner, FloatingToolbar, LinesSkeleton, ToolbarButton, ToolbarSeparator, useSnackbar, type MenuItem } from '@project-graphite/ui';
import { IconButton } from './IconButton';
import type * as Y from 'yjs';
import { useAuth } from '../auth';
import { fileSize, uploadFile } from '../files';
Expand Down
13 changes: 8 additions & 5 deletions app/frontend/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Icon, Button as UiButton, IconButton, type IconName } from '@project-graphite/ui';
import { Icon, Button as UiButton, type IconName } from '@project-graphite/ui';
import { IconButton } from './IconButton';
import { Tip } from './Tip';

const look = (large: boolean, primary: boolean) => ({ size: large ? 'medium' : 'small', variant: primary ? 'primary' : 'secondary' }) as const;

export function AddButton({ disabled, label, large = false, onClick, primary = false }: { disabled?: boolean; label: string; large?: boolean; onClick?: () => void; primary?: boolean }) {
return (
<UiButton {...look(large, primary)} aria-label={label} className="add-button" disabled={disabled} onClick={onClick} title={label} type="button">
<Icon name="plus" size={18} />
export function AddButton({ disabled, icon = 'plus', label, large = false, onClick, primary = false }: { disabled?: boolean; icon?: IconName; label: string; large?: boolean; onClick?: () => void; primary?: boolean }) {
const button = (
<UiButton {...look(large, primary)} aria-label={label} className="add-button" disabled={disabled} onClick={onClick} type="button">
<Icon name={icon} size={18} />
</UiButton>
);
return <Tip title={label}>{disabled ? <span className="inline-flex">{button}</span> : button}</Tip>;
}

export function Button({
Expand Down
3 changes: 2 additions & 1 deletion app/frontend/src/components/CategoriesEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { Banner, ConfirmDialog, IconButton, Select, type TagColor, tagColors, TextField } from '@project-graphite/ui';
import { Banner, ConfirmDialog, Select, type TagColor, tagColors, TextField } from '@project-graphite/ui';
import { IconButton } from './IconButton';
import { useAuth } from '../auth';
import { categoryName, type SpendingCategory } from '../spending';
import { useAction } from '../useAction';
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/src/components/CommentsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import { IconButton } from '@project-graphite/ui';
import { IconButton } from './IconButton';

const widthKey = 'strata-comments-width';
const minWidth = 348;
Expand Down
3 changes: 2 additions & 1 deletion app/frontend/src/components/DatePoll.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { DatePicker, formatDay, IconButton } from '@project-graphite/ui';
import { DatePicker, formatDay } from '@project-graphite/ui';
import { IconButton } from './IconButton';
import { useAction } from '../useAction';
import { clockTime, TimeField } from '../time';
import { Hint } from './Hint';
Expand Down
12 changes: 8 additions & 4 deletions app/frontend/src/components/FileBrowser.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Fragment, lazy, Suspense, useRef, useState, type HTMLAttributes, type KeyboardEvent, type MouseEvent, type PointerEvent, type ReactNode, type SyntheticEvent } from 'react';
import { Dialog, Icon, IconButton, type IconName } from '@project-graphite/ui';
import { Dialog, Icon, type IconName } from '@project-graphite/ui';
import { IconButton } from './IconButton';
import { FilePreview, fileSize, previewable } from '../files';
import { formatMoment } from '../time';
import { ActionsMenu } from './ActionsMenu';
Expand All @@ -9,6 +10,7 @@ import { DialogActions } from './DialogActions';
import { FilterMenu, type FilterGroup } from './FilterMenu';
import { Hint } from './Hint';
import { ContextMenu, NestedMenu, type NestedMenuItem } from './NestedMenu';
import { Tip } from './Tip';

const PdfViewer = lazy(() => import('./PdfViewer'));

Expand Down Expand Up @@ -281,9 +283,11 @@ export function FileBrowser<T extends BrowserEntry>({
<span className="list-row-title">
{entry.name}
{entry.shared && (
<span className="drive-shared" title="Shared by link">
<Icon name="users" size={14} />
</span>
<Tip title="Shared by link">
<span aria-label="Shared by link" className="drive-shared" role="img">
<Icon name="users" size={14} />
</span>
</Tip>
)}
</span>
{entry.detail && <span className="list-row-meta">{entry.detail}</span>}
Expand Down
10 changes: 8 additions & 2 deletions app/frontend/src/components/FileButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useRef } from 'react';
import { Button, IconButton } from '@project-graphite/ui';
import { Button } from '@project-graphite/ui';
import { AddButton } from './Button';
import { IconButton } from './IconButton';

export function FileButton({
accept,
Expand Down Expand Up @@ -28,7 +30,11 @@ export function FileButton({
return (
<>
{iconOnly ? (
<IconButton disabled={busy} icon="upload" label={label} onClick={() => input.current?.click()} size="small" />
primary ? (
<AddButton disabled={busy} icon="upload" label={label} large={large} onClick={() => input.current?.click()} primary />
) : (
<IconButton disabled={busy} icon="upload" label={label} onClick={() => input.current?.click()} size="small" />
)
) : (
<Button className="w-fit shrink-0" disabled={busy} icon="upload" onClick={() => input.current?.click()} size={large ? 'medium' : 'small'} variant={primary ? 'primary' : 'secondary'}>
{label}
Expand Down
33 changes: 18 additions & 15 deletions app/frontend/src/components/FilterMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import MuiMenu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { useState, type ReactNode } from 'react';
import { Link } from 'react-router';
import { Icon, IconButton, Button as UiButton } from '@project-graphite/ui';
import { Icon, Button as UiButton } from '@project-graphite/ui';
import { IconButton } from './IconButton';
import { Tip } from './Tip';

export interface FilterOption {
label: string;
Expand Down Expand Up @@ -49,20 +51,21 @@ export function FilterMenu({ children, compact = false, groups, label, large = f
{compact ? (
<IconButton aria-expanded={Boolean(anchor)} aria-haspopup="menu" badge={count || undefined} icon="filter" label={count ? `${label}, ${count} active: ${summary}` : label} onClick={open} />
) : (
<UiButton
aria-expanded={Boolean(anchor)}
aria-haspopup="menu"
aria-label={`${label}: ${summary}`}
className="w-fit max-w-full shrink-0"
icon="filter"
onClick={open}
size={large ? 'medium' : 'small'}
title={label}
type="button"
variant="secondary"
>
<span className="truncate">{summary || label}</span>
</UiButton>
<Tip title={label}>
<UiButton
aria-expanded={Boolean(anchor)}
aria-haspopup="menu"
aria-label={`${label}: ${summary}`}
className="w-fit max-w-full shrink-0"
icon="filter"
onClick={open}
size={large ? 'medium' : 'small'}
type="button"
variant="secondary"
>
<span className="truncate">{summary || label}</span>
</UiButton>
</Tip>
)}
{children}
<MuiMenu anchorEl={anchor} onClose={close} open={Boolean(anchor)} slotProps={{ list: { 'aria-label': drilled === undefined ? label : groups[drilled]!.label }, paper: { className: 'nested-menu min-w-56' } }}>
Expand Down
8 changes: 8 additions & 0 deletions app/frontend/src/components/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ComponentProps } from 'react';
import { IconButton as UiIconButton } from '@project-graphite/ui';
import { Tip } from './Tip';

export function IconButton({ disabled, label, ...props }: ComponentProps<typeof UiIconButton>) {
const button = <UiIconButton disabled={disabled} label={label} {...props} title={undefined} />;
return <Tip title={label}>{disabled ? <span className="inline-flex">{button}</span> : button}</Tip>;
}
12 changes: 8 additions & 4 deletions app/frontend/src/components/LinkActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import Paper from '@mui/material/Paper';
import Popper from '@mui/material/Popper';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import { IconButton, useSnackbar } from '@project-graphite/ui';
import { useSnackbar } from '@project-graphite/ui';
import { IconButton } from './IconButton';
import type { Editor } from '@tiptap/core';
import { ContextMenu } from './NestedMenu';
import { Tip } from './Tip';

interface OpenLink {
anchor: HTMLAnchorElement;
Expand Down Expand Up @@ -123,9 +125,11 @@ export function LinkActions({ editor }: { editor: Editor }) {
type="url"
/>
) : (
<a className="link-actions-address" href={link.href} onClick={(event) => { event.preventDefault(); open(); }} title={link.href}>
{link.href}
</a>
<Tip title={link.href}>
<a className="link-actions-address" href={link.href} onClick={(event) => { event.preventDefault(); open(); }}>
{link.href}
</a>
</Tip>
)}
<IconButton icon="external" label="Open link" onClick={open} size="small" />
<IconButton icon="link" label="Copy link" onClick={copy} size="small" />
Expand Down
5 changes: 3 additions & 2 deletions app/frontend/src/components/MonthStartField.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { formatDay, SegmentedControl, TextField } from '@project-graphite/ui';
import { dayKey } from '../agenda';

Expand All @@ -14,13 +14,14 @@ function currentPeriod(startDay: number, today: string) {
return [start, end].map((day) => day.toISOString().slice(0, 10)) as [string, string];
}

export function MonthStartField({ defaultValue, name }: { defaultValue: number; name: string }) {
export function MonthStartField({ defaultValue, name, onChange }: { defaultValue: number; name: string; onChange?: (startDay: number | null) => void }) {
const [mode, setMode] = useState<Mode>(defaultValue === 1 ? 'calendar' : 'payday');
const [payday, setPayday] = useState(String(defaultValue === 1 ? 25 : defaultValue));
const day = Number(payday);
const valid = /^\d{1,2}$/.test(payday.trim()) && day >= 1 && day <= 28;
const startDay = mode === 'calendar' ? 1 : valid ? day : null;
const period = startDay && currentPeriod(startDay, dayKey(new Date()));
useEffect(() => onChange?.(startDay), [onChange, startDay]);

return (
<div className="grid gap-3">
Expand Down
32 changes: 17 additions & 15 deletions app/frontend/src/components/NewTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import MuiDialog from '@mui/material/Dialog';
import MuiMenu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { lazy, Suspense, useId, useState, type ReactNode } from 'react';
import { Badge, Checkbox, ConfirmDialog, formatDay, Icon, IconButton, TagChip, type IconName } from '@project-graphite/ui';
import { Badge, Checkbox, ConfirmDialog, formatDay, Icon, TagChip, type IconName } from '@project-graphite/ui';
import { IconButton } from './IconButton';
import { useAuth } from '../auth';
import { priorityNames, quickSuggestions, withSuggestion } from '../quick-add';
import { homePath, useAllSpaces, type Member, type Tag } from '../spaces';
Expand All @@ -15,6 +16,7 @@ import { ExpandGlyph } from './glyphs';
import type { OpenTaskProps } from './OpenTask';
import { Button } from './Button';
import { UserAvatar } from './UserAvatar';
import { Tip } from './Tip';

const TaskSheet = lazy(() => import('./TaskSheet'));

Expand All @@ -31,20 +33,20 @@ export function ComposerMenu({ onChoose, value }: { onChoose: (composer: TaskCom
<IconButton aria-expanded={Boolean(anchor)} aria-haspopup="menu" icon="monitor" label="How to show a new task" onClick={(event) => setAnchor(event.currentTarget)} />
<MuiMenu anchorEl={anchor} onClose={() => setAnchor(null)} open={Boolean(anchor)} slotProps={{ list: { 'aria-label': 'How to show a new task', className: 'composer-menu' } }}>
{composers.map((composer) => (
<MenuItem
aria-checked={composer.value === value}
aria-label={composer.label}
key={composer.value}
onClick={() => {
setAnchor(null);
onChoose(composer.value);
}}
role="menuitemradio"
selected={composer.value === value}
title={composer.label}
>
{composer.icon}
</MenuItem>
<Tip key={composer.value} placement="right" title={composer.label}>
<MenuItem
aria-checked={composer.value === value}
aria-label={composer.label}
onClick={() => {
setAnchor(null);
onChoose(composer.value);
}}
role="menuitemradio"
selected={composer.value === value}
>
{composer.icon}
</MenuItem>
</Tip>
))}
</MuiMenu>
</>
Expand Down
34 changes: 18 additions & 16 deletions app/frontend/src/components/NoteComments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,21 +210,23 @@ export function NoteComments({
]
: changes;
return (
<div className="comment-entry grid gap-1" data-posted={posted === comment.id || undefined} key={comment.id}>
<p className="comment-head m-0 text-small text-muted">
<UserAvatar name={author} size="sm" userId={comment.author?.id} />
<span className="text-ink">{author}</span> · {formatMoment(comment.createdAt)}
{comment.editedAt ? ' · edited' : ''}
</p>
{comment.blockId && (
<button className="comment-block-quote cursor-pointer text-left" onClick={() => anchors.show(comment.blockId!)} type="button">
<AnchorLabel anchors={anchors} id={comment.blockId} />
</button>
)}
<p className="m-0 whitespace-pre-wrap break-words text-body text-ink">{comment.body}</p>
<div className="flex flex-wrap items-center gap-3">
{commentable && !comment.parentId && !comment.resolvedAt && <TextButton label="Reply" onClick={() => setReplying(comment.id)} />}
{actions.length > 0 && <ActionsMenu items={actions} name={`comment by ${author}`} />}
<div className="task-comment comment-entry" data-posted={posted === comment.id || undefined} key={comment.id}>
<UserAvatar name={author} size="sm" userId={comment.author?.id} />
<div className="task-comment-main">
<p className="comment-head m-0 text-small text-muted">
<span className="text-ink">{author}</span> · {formatMoment(comment.createdAt)}
{comment.editedAt ? ' · edited' : ''}
</p>
{comment.blockId && (
<button className="comment-block-quote cursor-pointer text-left" onClick={() => anchors.show(comment.blockId!)} type="button">
<AnchorLabel anchors={anchors} id={comment.blockId} />
</button>
)}
<p className="m-0 whitespace-pre-wrap break-words text-body text-ink">{comment.body}</p>
<div className="flex flex-wrap items-center gap-3">
{commentable && !comment.parentId && !comment.resolvedAt && <TextButton label="Reply" onClick={() => setReplying(comment.id)} />}
{actions.length > 0 && <ActionsMenu items={actions} name={`comment by ${author}`} />}
</div>
</div>
</div>
);
Expand All @@ -238,7 +240,7 @@ export function NoteComments({
{shown.map((thread) => (
<article className={`grid gap-3 ${thread.resolvedAt ? 'opacity-60' : ''}`} data-block-id={thread.blockId ?? undefined} key={thread.id}>
{renderComment(thread)}
<div className="grid gap-3 border-l border-line pl-4">
<div className="comment-thread-replies grid gap-3 border-l border-line pl-4">
{all.filter((comment) => comment.parentId === thread.id).map(renderComment)}
{replying === thread.id && <CommentForm label="Reply" onCancel={() => setReplying(undefined)} onSave={(body) => add(body, thread.id)} people={people} />}
</div>
Expand Down
Loading
Loading