Skip to content
Merged
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
89 changes: 53 additions & 36 deletions app/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ import { RsvpPage, SharePage } from './pages/RsvpPages';
import { SavePage } from './pages/SavePage';
import { DataSettings } from './pages/settings/DataSettings';
import { DeveloperSettings } from './pages/settings/DeveloperSettings';
import { FeaturesSettings } from './pages/settings/FeaturesSettings';
import { AppsSettings } from './pages/settings/AppsSettings';
import { ProfileSettings } from './pages/settings/ProfileSettings';
import { SecuritySettings } from './pages/settings/SecuritySettings';
import { SettingsLayout } from './pages/settings/SettingsLayout';
import { TrashSettings } from './pages/settings/TrashSettings';
import { SpaceActivity } from './pages/spaces/SpaceActivity';
import { SpaceItems } from './pages/spaces/SpaceItems';
import { SpaceLayout } from './pages/spaces/SpaceLayout';
Expand All @@ -55,7 +56,6 @@ import { SpaceTags } from './pages/spaces/SpaceTags';
import { SpaceTasks } from './pages/spaces/SpaceTasks';
import { TasksPage } from './pages/TasksPage';
import { TidyPage } from './pages/TidyPage';
import { TrashPage } from './pages/TrashPage';
import { VerifyPage } from './pages/VerifyPage';

function Protected() {
Expand All @@ -67,10 +67,10 @@ function Protected() {
return auth.user ? <Outlet /> : <Navigate replace state={{ from: location }} to="/login" />;
}

function FeatureRoute({ feature }: { feature: string }) {
function AppRoute({ app }: { app: string }) {
const auth = useAuth();
const location = useLocation();
return auth.user?.features.includes(feature) ? <Outlet /> : <Navigate replace state={{ feature, from: location }} to="/settings/features" />;
return auth.user?.features.includes(app) ? <Outlet /> : <Navigate replace state={{ app, from: location }} to="/settings/apps" />;
}

function SignedOutLogin() {
Expand All @@ -96,6 +96,7 @@ const legacyRoutes = [
['review', '/journal'],
['invitations', '/spaces/invitations'],
['menu', '/more'],
['trash', '/settings/trash'],
] as const;

function RouterLink({ href, ...props }: UiLinkProps) {
Expand All @@ -122,35 +123,51 @@ export function App() {
<Route element={<Moved to={to} />} key={path} path={path} />
))}
<Route element={<Protected />}>
<Route path="tasks" element={<TasksPage />} />
<Route path="agenda">
<Route index element={<AgendaPage key="today" view="today" />} />
<Route path="list" element={<AgendaPage key="list" view="list" />} />
<Route path="day" element={<AgendaPage key="day" view="day" />} />
<Route path="week" element={<AgendaPage key="week" view="week" />} />
<Route path="month" element={<AgendaPage key="month" view="month" />} />
<Route path="calendars" element={<CalendarsPage />} />
<Route element={<AppRoute app="tasks" />}>
<Route path="tasks" element={<TasksPage />} />
</Route>
<Route element={<AppRoute app="agenda" />}>
<Route path="agenda">
<Route index element={<AgendaPage key="today" view="today" />} />
<Route path="list" element={<AgendaPage key="list" view="list" />} />
<Route path="day" element={<AgendaPage key="day" view="day" />} />
<Route path="week" element={<AgendaPage key="week" view="week" />} />
<Route path="month" element={<AgendaPage key="month" view="month" />} />
<Route path="calendars" element={<CalendarsPage />} />
</Route>
</Route>
<Route path="events/:id" element={<EventPage />} />
<Route path="pages">
<Route index element={<PagesIndex key="pages" section="pages" />} />
<Route path="quick-notes" element={<PagesIndex key="quick-notes" section="quick-notes" />} />
<Route path="journal" element={<PagesIndex key="journal" section="journal" />} />
<Route element={<AppRoute app="pages" />}>
<Route path="pages">
<Route index element={<PagesIndex key="pages" section="pages" />} />
<Route path="quick-notes" element={<PagesIndex key="quick-notes" section="quick-notes" />} />
<Route path="journal" element={<PagesIndex key="journal" section="journal" />} />
</Route>
</Route>
<Route element={<AppRoute app="boards" />}>
<Route path="boards" element={<PagesIndex key="boards" section="boards" />} />
</Route>
<Route path="boards" element={<PagesIndex key="boards" section="boards" />} />
<Route path="notes/:id" element={<NotePage />} />
<Route path="bookmarks" element={<BookmarksPage />} />
<Route path="bookmarks/:id" element={<BookmarkPage />} />
<Route path="journal" element={<JournalPage />} />
<Route path="files" element={<FilesPage />} />
<Route path="spending">
<Route index element={<SpendingPage key="overview" tab="overview" />} />
<Route path="transactions" element={<SpendingPage key="transactions" tab="transactions" />} />
<Route path="accounts" element={<SpendingPage key="accounts" tab="accounts" />} />
<Route path="budgets" element={<SpendingPage key="budgets" tab="budgets" />} />
<Route path="recurring" element={<RecurringPage />} />
<Route path="insights" element={<SpendingInsightsPage />} />
<Route path="net-worth" element={<NetWorthPage />} />
<Route element={<AppRoute app="bookmarks" />}>
<Route path="bookmarks" element={<BookmarksPage />} />
<Route path="bookmarks/:id" element={<BookmarkPage />} />
</Route>
<Route element={<AppRoute app="journal" />}>
<Route path="journal" element={<JournalPage />} />
</Route>
<Route element={<AppRoute app="files" />}>
<Route path="files" element={<FilesPage />} />
</Route>
<Route element={<AppRoute app="spending" />}>
<Route path="spending">
<Route index element={<SpendingPage key="overview" tab="overview" />} />
<Route path="transactions" element={<SpendingPage key="transactions" tab="transactions" />} />
<Route path="accounts" element={<SpendingPage key="accounts" tab="accounts" />} />
<Route path="budgets" element={<SpendingPage key="budgets" tab="budgets" />} />
<Route path="recurring" element={<RecurringPage />} />
<Route path="insights" element={<SpendingInsightsPage />} />
<Route path="net-worth" element={<NetWorthPage />} />
</Route>
</Route>
<Route path="spaces">
<Route index element={<SpacesPage />} />
Expand All @@ -166,39 +183,39 @@ export function App() {
<Route path="activity" element={<SpaceActivity />} />
</Route>
</Route>
<Route element={<FeatureRoute feature="goals" />}>
<Route element={<AppRoute app="goals" />}>
<Route path="goals" element={<GoalsPage />} />
<Route path="goals/:id" element={<GoalPage />} />
</Route>
<Route element={<FeatureRoute feature="people" />}>
<Route element={<AppRoute app="people" />}>
<Route path="people" element={<PeoplePage />} />
<Route path="people/:id" element={<PersonPage />} />
</Route>
<Route element={<FeatureRoute feature="recipes" />}>
<Route element={<AppRoute app="recipes" />}>
<Route path="recipes" element={<RecipesPage />} />
<Route path="recipes/plan" element={<MealPlanPage />} />
<Route path="recipes/:id" element={<RecipePage />} />
</Route>
<Route element={<FeatureRoute feature="tidy" />}>
<Route element={<AppRoute app="tidy" />}>
<Route path="tidy" element={<TidyPage key="suggestions" section="suggestions" />} />
<Route path="tidy/folder" element={<TidyPage key="folder" section="folder" />} />
<Route path="tidy/history" element={<TidyPage key="history" section="history" />} />
</Route>
<Route element={<FeatureRoute feature="trips" />}>
<Route element={<AppRoute app="trips" />}>
<Route path="trips" element={<TripsPage />} />
<Route path="trips/:id" element={<TripPage />} />
</Route>
<Route path="inbox" element={<InboxPage />} />
<Route path="trash" element={<TrashPage />} />
<Route path="more" element={<MorePage />} />
<Route path="save" element={<SavePage />} />
<Route path="admin" element={<AdminPage />} />
<Route path="settings" element={<SettingsLayout />}>
<Route index element={<ProfileSettings />} />
<Route path="features" element={<FeaturesSettings />} />
<Route path="apps" element={<AppsSettings />} />
<Route path="security" element={<SecuritySettings />} />
<Route path="developer" element={<DeveloperSettings />} />
<Route path="data" element={<DataSettings />} />
<Route path="trash" element={<TrashSettings />} />
</Route>
</Route>
<Route path="*" element={<NotFoundPage />} />
Expand Down
112 changes: 112 additions & 0 deletions app/frontend/src/apps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { IconName } from '@project-graphite/ui';

export interface AppEntry {
key: string;
label: string;
description: string;
icon: IconName;
href: string;
within?: string[];
phoneTab?: boolean;
}

export const appCatalogue: AppEntry[] = [
{
key: 'tasks',
label: 'Tasks',
description: 'Lists of things to do, with due dates, repeats and habits, on their own or shared in a space.',
icon: 'check-mark',
href: '/tasks',
},
{
key: 'agenda',
label: 'Agenda',
description: 'Your day, week and month in one place, with events, connected calendars and what is due.',
icon: 'calendar',
href: '/agenda',
within: ['/events'],
phoneTab: true,
},
{
key: 'pages',
label: 'Pages',
description: 'Notes and documents you can nest, link and share, plus quick notes for anything in passing.',
icon: 'file',
href: '/pages',
within: ['/notes'],
},
{
key: 'boards',
label: 'Boards',
description: 'Canvases for sketching ideas, arranging cards and laying out plans.',
icon: 'board',
href: '/boards',
},
{
key: 'bookmarks',
label: 'Bookmarks',
description: 'Links you save from anywhere, with a preview, tags and a place to read later.',
icon: 'bookmark',
href: '/bookmarks',
},
{
key: 'journal',
label: 'Journal',
description: 'A daily entry and a weekly review of what you did and what comes next.',
icon: 'pencil',
href: '/journal',
},
{
key: 'files',
label: 'Files',
description: 'Uploads from across your spaces, in one library you can search and sort.',
icon: 'library',
href: '/files',
},
{
key: 'spending',
label: 'Spending',
description: 'Accounts, transactions, budgets, recurring payments and your net worth over time.',
icon: 'wallet',
href: '/spending',
phoneTab: true,
},
{
key: 'goals',
label: 'Goals',
description: 'Goals for the year, quarter or month, with progress from a number or from linked tasks and habits.',
icon: 'target',
href: '/goals',
},
{
key: 'people',
label: 'People',
description: 'Birthdays on your agenda with a reminder the day before, gift ideas, and the events you shared.',
icon: 'users',
href: '/people',
},
{
key: 'recipes',
label: 'Recipes and meals',
description: 'Recipes you can paste from a link, a weekly meal plan, and a shopping list made from it.',
icon: 'utensils',
href: '/recipes',
},
{
key: 'tidy',
label: 'Tidy',
description: 'Suggestions and rules that file, tag and clear out items across your spaces, with an optional weekly summary.',
icon: 'sparkles',
href: '/tidy',
},
{
key: 'trips',
label: 'Trips',
description: 'A place for each trip: itinerary, packing list, documents, pages and what it costs, shared with whoever comes along.',
icon: 'plane',
href: '/trips',
},
];

export const enabledApps = (keys: string[] | undefined) =>
(keys ?? []).flatMap((key) => appCatalogue.filter((app) => app.key === key));
2 changes: 1 addition & 1 deletion app/frontend/src/components/CategoriesEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function CategoriesEditor({ categories, onClose, onSaved, spaceId }: { ca
</li>
))}
</ul>
<Button disabled={rows.length >= 40} label="Add a category" onClick={() => setRows((current) => [...current, { key: Math.max(...current.map((row) => row.key)) + 1, name: '', colour: 'gray' }])} />
<Button disabled={rows.length >= 40} label="Add a category" large onClick={() => setRows((current) => [...current, { key: Math.max(...current.map((row) => row.key)) + 1, name: '', colour: 'gray' }])} />
{error && (
<Alert className="m-0" severity="error">
{error}
Expand Down
1 change: 1 addition & 0 deletions app/frontend/src/components/ItemBoards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function ItemBoards({ editable, itemId, spaceId, title }: { editable: boo
disabled={adding.busy}
icon="plus"
label="New board"
large
onClick={() =>
void adding.run(async () => {
const board = await auth.request<{ id: string }>(`/spaces/${spaceId}/notes`, { method: 'POST', body: JSON.stringify({ board: true, title: `${title} board` }) });
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/src/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function Page({
);
}

export function DetailState({ back, resource, thing }: { back: Back; resource: { error: string; status?: number; reload: () => void }; thing: string }) {
export function DetailState({ back, resource, thing }: { back?: Back; resource: { error: string; status?: number; reload: () => void }; thing: string }) {
if (resource.status === 404) {
return (
<Page back={back} title={`${thing} not found`}>
Expand Down
6 changes: 3 additions & 3 deletions app/frontend/src/components/PageTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function PageTree() {
const navigate = useNavigate();
const { pathname } = useLocation();
const spaces = useSpaces();
const listed = useResource<Note[]>(collapsed ? null : '/me/notes', true);
const listed = useResource<Note[]>('/me/notes', true);
const action = useAction();
const [open, setOpen] = useState(readOpen);
const [dragged, setDragged] = useState<Note>();
Expand All @@ -49,7 +49,7 @@ export function PageTree() {

useEffect(() => onPagesChanged(reload), [reload]);

if (collapsed || !listed.data || !spaces.data) return null;
if (!listed.data || !spaces.data) return null;
const pages = listed.data.filter((note) => !note.template);
const childrenOf = (spaceId: string, parentId: string | null) =>
nestedUnder(pages, parentId)
Expand Down Expand Up @@ -160,7 +160,7 @@ export function PageTree() {
}

return (
<section aria-label="Page tree" className="page-tree">
<section aria-label="Page tree" className="page-tree" hidden={collapsed}>
<p className="sidebar-section-label">Pages</p>
{spaces.data.map((space) => {
const key = `space:${space.id}`;
Expand Down
4 changes: 2 additions & 2 deletions app/frontend/src/components/QuickAddTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function QuickAddTask({ listId, onAdded, spaceId }: { listId?: string; on
}, 'Could not add the task');
}}
>
<div className="flex items-center gap-2">
<div className="flex items-stretch gap-2">
<TextField
aria-describedby={understood ? hintId : undefined}
className="min-w-0 flex-1"
Expand All @@ -45,7 +45,7 @@ export function QuickAddTask({ listId, onAdded, spaceId }: { listId?: string; on
onChange={(event) => setText(event.currentTarget.value)}
value={text}
/>
<Button disabled={!quick.title || adding.busy} label="Add" submit />
<Button disabled={!quick.title || adding.busy} label="Add" large submit />
</div>
{understood && (
<p className="mono-sm m-0 text-faint" id={hintId} role="status">
Expand Down
4 changes: 2 additions & 2 deletions app/frontend/src/components/SearchPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import { CommandPalette, errorMessage, isAbortError, type CommandItem } from '@project-graphite/ui';
import { isAdmin, useAuth } from '../auth';
import { enabledFeatures } from '../features';
import { enabledApps } from '../apps';
import { searchableAreas } from '../navigation';
import { itemHref, itemKindName, useSpaces, type Tag } from '../spaces';
import { useAction } from '../useAction';
Expand Down Expand Up @@ -78,7 +78,7 @@ export function SearchPalette({ onClose }: { onClose: () => void }) {
...(spaces.data ?? [])
.filter((space) => matches(space.name))
.map((space) => ({ group: 'Spaces', href: `/spaces/${space.id}`, id: `space-${space.id}`, label: space.name })),
...searchableAreas(enabledFeatures(user?.features), isAdmin(user))
...searchableAreas(enabledApps(user?.features), isAdmin(user))
.filter((area) => matches(area.label) || (area.hint && matches(area.hint)))
.map((area) => ({ group: 'Go to', hint: area.hint, href: area.href, id: `page-${area.href}`, label: area.label })),
];
Expand Down
Loading
Loading