Search function added

This commit is contained in:
NPS Agent
2026-05-12 10:15:34 +09:30
parent 1edde60317
commit 5968294081
4 changed files with 108 additions and 37 deletions
+1
View File
@@ -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.
+36 -20
View File
@@ -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)}
/>
+41 -17
View File
@@ -106,30 +106,54 @@ 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">
<span className="topbar__brand"><BrandMark /><span>Dashy</span></span>
<span className="topbar__divider" />
<span className="topbar__workspace">{workspace ? workspace.name : 'loading…'}</span>
{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>
<nav className="tabs" role="tablist">
<Tab id="overview" label="Overview" tab={tab} setTab={setTab} />
{dbUsers.map(u => (
<Tab key={u.id} id={u.id} label={u.name} tab={tab} setTab={setTab} user={u} />
))}
{isAdmin && <Tab id="deleted" label="Deleted" tab={tab} setTab={setTab} />}
</nav>
{!showSearch && (
<nav className="tabs" role="tablist">
<Tab id="overview" label="Overview" tab={tab} setTab={setTab} />
{dbUsers.map(u => (
<Tab key={u.id} id={u.id} label={u.name} tab={tab} setTab={setTab} user={u} />
))}
{isAdmin && <Tab id="deleted" label="Deleted" tab={tab} setTab={setTab} />}
</nav>
)}
<div className="topbar__right">
<button className="btn btn--soft" onClick={onAdd}>
<Icon.Plus /> New task
</button>
<IconBtn label="Search">
<Icon.Search />
</IconBtn>
{!showSearch && (
<>
<button className="btn btn--soft" onClick={onAdd}>
<Icon.Plus /> New task
</button>
<IconBtn label="Search" onClick={onToggleSearch}>
<Icon.Search />
</IconBtn>
</>
)}
{isAdmin && (
<IconBtn label="Audit log" onClick={onLogs}>
<Icon.Logs />
+30
View File
@@ -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; }