Search function added
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
17. **API Authentication Enforcement:** Applied JWT Bearer token validation to all sensitive routes.
|
||||
18. **Persistent Workspace Settings:** Added a `Workspace` database model to persist global dashboard settings like Name and Timezone.
|
||||
19. **Dynamic UI Integration:** Completely refactored the navigation and boards to build columns and tabs dynamically from the live database user list.
|
||||
20. **Functional Search:** Implemented a real-time task search feature. Clicking the search icon now reveals an inline search bar that filters tasks by title, description, or tags across all views.
|
||||
|
||||
### Phase 3: Advanced Features
|
||||
- **Real-time Notifications:** Explore WebSockets for task assignments.
|
||||
|
||||
@@ -59,6 +59,36 @@ function App() {
|
||||
|
||||
const { tasks, users: dbUsers, audit, workspace, deletedTasks, loading } = useApiData(authed);
|
||||
const [tab, setTab] = React.useState('overview');
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [showSearch, setShowSearch] = React.useState(false);
|
||||
|
||||
// Map API fields to frontend component expectations
|
||||
const frontendTasks = React.useMemo(() => tasks.map(t => ({
|
||||
...t,
|
||||
assignee: t.assignee_id,
|
||||
addedBy: t.added_by,
|
||||
addedAt: t.added_at,
|
||||
tags: t.tags.map(tagObj => tagObj.tag)
|
||||
})), [tasks]);
|
||||
|
||||
const filteredTasks = React.useMemo(() => {
|
||||
if (!searchQuery.trim()) return frontendTasks;
|
||||
const q = searchQuery.toLowerCase();
|
||||
return frontendTasks.filter(t =>
|
||||
t.title.toLowerCase().includes(q) ||
|
||||
(t.description && t.description.toLowerCase().includes(q)) ||
|
||||
t.tags.some(tag => tag.toLowerCase().includes(q))
|
||||
);
|
||||
}, [frontendTasks, searchQuery]);
|
||||
|
||||
const frontendAudit = React.useMemo(() => audit.map(a => ({
|
||||
...a,
|
||||
actor: a.actor,
|
||||
action: a.action,
|
||||
summary: a.summary,
|
||||
target: a.target,
|
||||
at: a.at
|
||||
})), [audit]);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.dbUsers = dbUsers;
|
||||
@@ -193,24 +223,6 @@ function App() {
|
||||
const dismissHU = (id) => setHeadsUp(h => h.filter(x => x.id !== id));
|
||||
const openTaskFromAnywhere = (id) => { setOpenTaskId(id); setShowLogs(false); };
|
||||
|
||||
// Map API fields to frontend component expectations
|
||||
const frontendTasks = tasks.map(t => ({
|
||||
...t,
|
||||
assignee: t.assignee_id,
|
||||
addedBy: t.added_by,
|
||||
addedAt: t.added_at,
|
||||
tags: t.tags.map(tagObj => tagObj.tag)
|
||||
}));
|
||||
|
||||
const frontendAudit = audit.map(a => ({
|
||||
...a,
|
||||
actor: a.actor,
|
||||
action: a.action,
|
||||
summary: a.summary,
|
||||
target: a.target,
|
||||
at: a.at
|
||||
}));
|
||||
|
||||
const mappedOpenTask = frontendTasks.find(x => x.id === openTaskId);
|
||||
|
||||
return (
|
||||
@@ -225,6 +237,10 @@ function App() {
|
||||
onLogs={() => setShowLogs(true)}
|
||||
onProfile={() => setShowSettings(true)}
|
||||
workspace={workspace}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
showSearch={showSearch}
|
||||
onToggleSearch={() => { setShowSearch(!showSearch); if (showSearch) setSearchQuery(''); }}
|
||||
/>
|
||||
|
||||
<HeadsUp items={headsUp} onDismiss={dismissHU} onOpenTask={openTaskFromAnywhere} />
|
||||
@@ -232,7 +248,7 @@ function App() {
|
||||
<main className="main">
|
||||
{tab === 'overview' && (
|
||||
<OverviewScreen
|
||||
tasks={frontendTasks} density={t.density}
|
||||
tasks={filteredTasks} density={t.density}
|
||||
dbUsers={dbUsers}
|
||||
onOpen={(task) => setOpenTaskId(task.id)}
|
||||
onAddFor={(uid) => setAdding(uid)}
|
||||
@@ -253,7 +269,7 @@ function App() {
|
||||
)}
|
||||
{tab !== 'overview' && tab !== 'deleted' && (
|
||||
<UserScreen
|
||||
user={merge(tab)} tasks={frontendTasks} density={t.density}
|
||||
user={merge(tab)} tasks={filteredTasks} density={t.density}
|
||||
onOpen={(task) => setOpenTaskId(task.id)}
|
||||
onAddFor={(uid) => setAdding(uid)}
|
||||
/>
|
||||
|
||||
+26
-2
@@ -106,15 +106,34 @@ function BrandMark({ size = 22 }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TopBar({ me, dbUsers = [], isAdmin, tab, setTab, onAdd, onLogs, onLogout, onProfile, workspace }) {
|
||||
function TopBar({ me, dbUsers = [], isAdmin, tab, setTab, onAdd, onLogs, onLogout, onProfile, workspace, searchQuery, setSearchQuery, showSearch, onToggleSearch }) {
|
||||
return (
|
||||
<header className="topbar">
|
||||
<div className="topbar__left">
|
||||
{showSearch ? (
|
||||
<div className="topbar__search">
|
||||
<Icon.Search />
|
||||
<input
|
||||
className="topbar__search-input"
|
||||
autoFocus
|
||||
placeholder="Search tasks, descriptions, tags…"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<IconBtn label="Close search" onClick={onToggleSearch}>
|
||||
<Icon.Close />
|
||||
</IconBtn>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="topbar__brand"><BrandMark /><span>Dashy</span></span>
|
||||
<span className="topbar__divider" />
|
||||
<span className="topbar__workspace">{workspace ? workspace.name : 'loading…'}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!showSearch && (
|
||||
<nav className="tabs" role="tablist">
|
||||
<Tab id="overview" label="Overview" tab={tab} setTab={setTab} />
|
||||
{dbUsers.map(u => (
|
||||
@@ -122,14 +141,19 @@ function TopBar({ me, dbUsers = [], isAdmin, tab, setTab, onAdd, onLogs, onLogou
|
||||
))}
|
||||
{isAdmin && <Tab id="deleted" label="Deleted" tab={tab} setTab={setTab} />}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
<div className="topbar__right">
|
||||
{!showSearch && (
|
||||
<>
|
||||
<button className="btn btn--soft" onClick={onAdd}>
|
||||
<Icon.Plus /> New task
|
||||
</button>
|
||||
<IconBtn label="Search">
|
||||
<IconBtn label="Search" onClick={onToggleSearch}>
|
||||
<Icon.Search />
|
||||
</IconBtn>
|
||||
</>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<IconBtn label="Audit log" onClick={onLogs}>
|
||||
<Icon.Logs />
|
||||
|
||||
+30
@@ -264,6 +264,36 @@ input, textarea { font: inherit; color: inherit; }
|
||||
.topbar__me-name { font-size: 12.5px; font-weight: 600; }
|
||||
.topbar__me-role { font-size: 10.5px; color: var(--fg-soft); }
|
||||
|
||||
/* === SEARCH === */
|
||||
.topbar__search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--bg-sunken);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 0 4px 0 10px;
|
||||
height: 32px;
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
animation: search-slide 180ms ease;
|
||||
}
|
||||
.topbar__search-input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
color: var(--fg);
|
||||
padding: 0;
|
||||
}
|
||||
.topbar__search-input::placeholder { color: var(--fg-faint); }
|
||||
|
||||
@keyframes search-slide {
|
||||
from { opacity: 0; transform: translateX(-10px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
/* === MAIN === */
|
||||
.main { flex: 1; overflow: auto; padding: 20px; min-height: 0; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user