Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9057f6d56 | ||
|
|
44b59edcb3 |
@@ -14,7 +14,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
init_tracing();
|
init_tracing();
|
||||||
|
|
||||||
let config = AppConfig::from_env()?;
|
let config = AppConfig::from_env()?;
|
||||||
let pool = db::init_pool(&config.database_url)?;
|
let pool = db::init_pool_with_size(&config.database_url, 1)?;
|
||||||
let s3_client = build_client(&config).await?;
|
let s3_client = build_client(&config).await?;
|
||||||
let storage = Arc::new(S3Storage::new(s3_client, config.s3_bucket.clone()));
|
let storage = Arc::new(S3Storage::new(s3_client, config.s3_bucket.clone()));
|
||||||
let jwt = JwtService::from_config(&config)?;
|
let jwt = JwtService::from_config(&config)?;
|
||||||
|
|||||||
+8
-1
@@ -5,10 +5,17 @@ use diesel::r2d2::{ConnectionManager, Pool};
|
|||||||
|
|
||||||
pub type PgPool = Pool<ConnectionManager<PgConnection>>;
|
pub type PgPool = Pool<ConnectionManager<PgConnection>>;
|
||||||
|
|
||||||
|
const DEFAULT_MAX_POOL_SIZE: u32 = 2;
|
||||||
|
|
||||||
pub fn init_pool(database_url: &str) -> anyhow::Result<PgPool> {
|
pub fn init_pool(database_url: &str) -> anyhow::Result<PgPool> {
|
||||||
|
init_pool_with_size(database_url, DEFAULT_MAX_POOL_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_pool_with_size(database_url: &str, max_size: u32) -> anyhow::Result<PgPool> {
|
||||||
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||||
|
let pool_size = max_size.max(1);
|
||||||
let pool = Pool::builder()
|
let pool = Pool::builder()
|
||||||
.max_size(16)
|
.max_size(pool_size)
|
||||||
.connection_timeout(Duration::from_secs(10))
|
.connection_timeout(Duration::from_secs(10))
|
||||||
.build(manager)?;
|
.build(manager)?;
|
||||||
Ok(pool)
|
Ok(pool)
|
||||||
|
|||||||
+44
-13
@@ -17,6 +17,7 @@ import {
|
|||||||
Outlet,
|
Outlet,
|
||||||
useLocation,
|
useLocation,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
|
useMatch,
|
||||||
matchPath,
|
matchPath,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
@@ -323,8 +324,8 @@ const AppLayout = () => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const appState = useAppState();
|
const appState = useAppState();
|
||||||
const appDispatch = useAppDispatch();
|
const appDispatch = useAppDispatch();
|
||||||
const folderMatch = matchPath('/folders/:folderId', location.pathname);
|
const folderMatch = matchPath('/documents/folder/:folderId', location.pathname);
|
||||||
const nestedDocMatch = matchPath('/folders/:folderId/documents/:documentId', location.pathname);
|
const nestedDocMatch = matchPath('/documents/folder/:folderId/documents/:documentId', location.pathname);
|
||||||
const docMatch = nestedDocMatch || matchPath('/documents/:documentId', location.pathname);
|
const docMatch = nestedDocMatch || matchPath('/documents/:documentId', location.pathname);
|
||||||
const routeFolderId =
|
const routeFolderId =
|
||||||
folderMatch?.params?.folderId || nestedDocMatch?.params?.folderId || null;
|
folderMatch?.params?.folderId || nestedDocMatch?.params?.folderId || null;
|
||||||
@@ -397,6 +398,13 @@ const AppLayout = () => {
|
|||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [activeTagFilters, setActiveTagFilters] = useState([]);
|
const [activeTagFilters, setActiveTagFilters] = useState([]);
|
||||||
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState([]);
|
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState([]);
|
||||||
|
const documentsRouteMatch = useMatch('/documents');
|
||||||
|
const documentsFolderRouteMatch = useMatch('/documents/folder/:folderId');
|
||||||
|
const tagsRouteMatch = useMatch('/tags');
|
||||||
|
const correspondentsRouteMatch = useMatch('/correspondents');
|
||||||
|
const isDocumentsRoute = Boolean(documentsRouteMatch || documentsFolderRouteMatch);
|
||||||
|
const isTagsRoute = Boolean(tagsRouteMatch);
|
||||||
|
const isCorrespondentsRoute = Boolean(correspondentsRouteMatch);
|
||||||
const toggleTagFilter = useCallback((tagId) => {
|
const toggleTagFilter = useCallback((tagId) => {
|
||||||
if (!tagId) return;
|
if (!tagId) return;
|
||||||
setActiveTagFilters((previous) =>
|
setActiveTagFilters((previous) =>
|
||||||
@@ -1741,7 +1749,7 @@ const AppLayout = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = targetId === 'root' ? '/folders' : `/folders/${targetId}`;
|
const path = targetId === 'root' ? '/documents' : `/documents/folder/${targetId}`;
|
||||||
navigate(path, { replace });
|
navigate(path, { replace });
|
||||||
},
|
},
|
||||||
[loadFolder, navigate],
|
[loadFolder, navigate],
|
||||||
@@ -3155,7 +3163,7 @@ const AppLayout = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = targetId === 'root' ? '/folders' : `/folders/${targetId}`;
|
const path = targetId === 'root' ? '/documents' : `/documents/folder/${targetId}`;
|
||||||
navigate(path, { replace: false });
|
navigate(path, { replace: false });
|
||||||
},
|
},
|
||||||
[navigate, selectedFolder, loadFolder],
|
[navigate, selectedFolder, loadFolder],
|
||||||
@@ -4239,11 +4247,9 @@ const AppLayout = () => {
|
|||||||
onFolderDragStart: handleFolderDragStart,
|
onFolderDragStart: handleFolderDragStart,
|
||||||
onFolderDragEnd: handleFolderDragEnd,
|
onFolderDragEnd: handleFolderDragEnd,
|
||||||
draggedFolderId,
|
draggedFolderId,
|
||||||
onShowTags: () => navigate('/tags'),
|
|
||||||
tags,
|
tags,
|
||||||
activeTagIds: activeTagFilters,
|
activeTagIds: activeTagFilters,
|
||||||
onToggleTagFilter: toggleTagFilter,
|
onToggleTagFilter: toggleTagFilter,
|
||||||
onShowCorrespondents: () => navigate('/correspondents'),
|
|
||||||
correspondents,
|
correspondents,
|
||||||
activeCorrespondentIds: activeCorrespondentFilters,
|
activeCorrespondentIds: activeCorrespondentFilters,
|
||||||
onToggleCorrespondentFilter: toggleCorrespondentFilter,
|
onToggleCorrespondentFilter: toggleCorrespondentFilter,
|
||||||
@@ -4489,6 +4495,31 @@ const AppLayout = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="app-bar__right">
|
<div className="app-bar__right">
|
||||||
|
<div className="app-bar__links">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`app-bar__link${isDocumentsRoute ? ' active' : ''}`}
|
||||||
|
onClick={() => navigate('/documents')}
|
||||||
|
>
|
||||||
|
Documents
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`app-bar__link${isTagsRoute ? ' active' : ''}`}
|
||||||
|
onClick={() => navigate('/tags')}
|
||||||
|
>
|
||||||
|
Tags
|
||||||
|
<span className="app-bar__link-count">{tags.length}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`app-bar__link${isCorrespondentsRoute ? ' active' : ''}`}
|
||||||
|
onClick={() => navigate('/correspondents')}
|
||||||
|
>
|
||||||
|
Correspondents
|
||||||
|
<span className="app-bar__link-count">{correspondents.length}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{status && (
|
{status && (
|
||||||
<div className="app-bar__status">
|
<div className="app-bar__status">
|
||||||
<StatusBanner status={status} />
|
<StatusBanner status={status} />
|
||||||
@@ -4669,7 +4700,7 @@ const LoginRoute = () => {
|
|||||||
if (typeof target === 'string' && target.startsWith('/')) {
|
if (typeof target === 'string' && target.startsWith('/')) {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
return '/folders';
|
return '/documents';
|
||||||
}, [location.state]);
|
}, [location.state]);
|
||||||
|
|
||||||
if (appStatus !== 'logged-out' && appStatus !== 'authenticating') {
|
if (appStatus !== 'logged-out' && appStatus !== 'authenticating') {
|
||||||
@@ -4734,17 +4765,17 @@ const AppRouter = () => (
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/account/login" element={<LoginRoute />} />
|
<Route path="/account/login" element={<LoginRoute />} />
|
||||||
<Route element={<AppLayout />}>
|
<Route element={<AppLayout />}>
|
||||||
<Route path="/" element={<Navigate to="/folders" replace />} />
|
<Route path="/" element={<Navigate to="/documents" replace />} />
|
||||||
<Route path="/folders" element={<DocumentsRoute />} />
|
<Route path="/documents" element={<DocumentsRoute />} />
|
||||||
<Route path="/folders/:folderId" element={<DocumentsRoute />} />
|
<Route path="/documents/folder/:folderId" element={<DocumentsRoute />} />
|
||||||
<Route path="/documents/:documentId" element={<DocumentsRoute />} />
|
|
||||||
<Route
|
<Route
|
||||||
path="/folders/:folderId/documents/:documentId"
|
path="/documents/folder/:folderId/documents/:documentId"
|
||||||
element={<DocumentsRoute />}
|
element={<DocumentsRoute />}
|
||||||
/>
|
/>
|
||||||
|
<Route path="/documents/:documentId" element={<DocumentsRoute />} />
|
||||||
<Route path="/tags" element={<TagsRoute />} />
|
<Route path="/tags" element={<TagsRoute />} />
|
||||||
<Route path="/correspondents" element={<CorrespondentsRoute />} />
|
<Route path="/correspondents" element={<CorrespondentsRoute />} />
|
||||||
<Route path="*" element={<Navigate to="/folders" replace />} />
|
<Route path="*" element={<Navigate to="/documents" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useMatch } from 'react-router-dom';
|
import { ChevronIcon, FolderIcon, TrashIcon, EditIcon } from '../ui/icons';
|
||||||
import {
|
|
||||||
ChevronIcon,
|
|
||||||
FolderIcon,
|
|
||||||
TagIcon,
|
|
||||||
TrashIcon,
|
|
||||||
CorrespondentIcon,
|
|
||||||
EditIcon,
|
|
||||||
} from '../ui/icons';
|
|
||||||
import { getTagColorStyle } from '../utils/colors';
|
import { getTagColorStyle } from '../utils/colors';
|
||||||
|
|
||||||
const FolderNode = ({
|
const FolderNode = ({
|
||||||
@@ -137,21 +129,13 @@ const Sidebar = ({
|
|||||||
onFolderDragStart,
|
onFolderDragStart,
|
||||||
onFolderDragEnd,
|
onFolderDragEnd,
|
||||||
draggedFolderId,
|
draggedFolderId,
|
||||||
onShowTags,
|
|
||||||
tags = [],
|
tags = [],
|
||||||
activeTagIds = [],
|
activeTagIds = [],
|
||||||
onToggleTagFilter,
|
onToggleTagFilter,
|
||||||
onShowCorrespondents,
|
|
||||||
correspondents = [],
|
correspondents = [],
|
||||||
activeCorrespondentIds = [],
|
activeCorrespondentIds = [],
|
||||||
onToggleCorrespondentFilter,
|
onToggleCorrespondentFilter,
|
||||||
}) => {
|
}) => {
|
||||||
const handleShowTags = onShowTags || (() => {});
|
|
||||||
const tagsRouteMatch = useMatch('/tags');
|
|
||||||
const isTagsRoute = Boolean(tagsRouteMatch);
|
|
||||||
const handleShowCorrespondents = onShowCorrespondents || (() => {});
|
|
||||||
const correspondentsRouteMatch = useMatch('/correspondents');
|
|
||||||
const isCorrespondentsRoute = Boolean(correspondentsRouteMatch);
|
|
||||||
const sortedCorrespondents = useMemo(
|
const sortedCorrespondents = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[...correspondents].sort((a, b) =>
|
[...correspondents].sort((a, b) =>
|
||||||
@@ -224,21 +208,6 @@ const Sidebar = ({
|
|||||||
<h3>Tags</h3>
|
<h3>Tags</h3>
|
||||||
<span className="meta">{tags.length}</span>
|
<span className="meta">{tags.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
className={`sidebar-item${isTagsRoute ? ' active' : ''}`}
|
|
||||||
onClick={handleShowTags}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
|
||||||
event.preventDefault();
|
|
||||||
handleShowTags();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TagIcon className="sidebar-item__icon" />
|
|
||||||
<span>All tags</span>
|
|
||||||
</span>
|
|
||||||
<div className="sidebar-tag-cloud" role="list">
|
<div className="sidebar-tag-cloud" role="list">
|
||||||
{tags.length ? (
|
{tags.length ? (
|
||||||
tags.map((tag) => {
|
tags.map((tag) => {
|
||||||
@@ -269,25 +238,6 @@ const Sidebar = ({
|
|||||||
<h3>Correspondents</h3>
|
<h3>Correspondents</h3>
|
||||||
<span className="meta">{correspondents.length}</span>
|
<span className="meta">{correspondents.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
className={`sidebar-item${isCorrespondentsRoute ? ' active' : ''}`}
|
|
||||||
onClick={() => {
|
|
||||||
onToggleCorrespondentFilter?.(null);
|
|
||||||
handleShowCorrespondents();
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
|
||||||
event.preventDefault();
|
|
||||||
onToggleCorrespondentFilter?.(null);
|
|
||||||
handleShowCorrespondents();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CorrespondentIcon className="sidebar-item__icon" />
|
|
||||||
<span>All correspondents</span>
|
|
||||||
</span>
|
|
||||||
<ul className="sidebar-correspondent-list">
|
<ul className="sidebar-correspondent-list">
|
||||||
{sortedCorrespondents.length ? (
|
{sortedCorrespondents.length ? (
|
||||||
sortedCorrespondents.map((correspondent) => {
|
sortedCorrespondents.map((correspondent) => {
|
||||||
|
|||||||
+43
-1
@@ -306,6 +306,47 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-bar__links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bar__link {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bar__link:hover {
|
||||||
|
background: var(--sidebar-hover-bg);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bar__link:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bar__link.active {
|
||||||
|
background: var(--sidebar-active-bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bar__link-count {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.app-main {
|
.app-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -782,6 +823,7 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
|
|
||||||
.folder-row:hover {
|
.folder-row:hover {
|
||||||
background: var(--sidebar-hover-bg);
|
background: var(--sidebar-hover-bg);
|
||||||
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-row.active {
|
.folder-row.active {
|
||||||
@@ -859,7 +901,7 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
gap: 0.28rem;
|
gap: 0.28rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
color: var(--sidebar-fg, #6f7683);
|
color: var(--sidebar-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section:first-of-type,
|
.sidebar-section:first-of-type,
|
||||||
|
|||||||
Reference in New Issue
Block a user