From 312cb37453733b5e7b575e604d96dc2e700ae37a Mon Sep 17 00:00:00 2001 From: Shintaro Morikawa Date: Tue, 26 May 2026 12:08:21 +0900 Subject: [PATCH 1/4] Show query ID on the left of the date in QueryList items Co-Authored-By: Claude Opus 4.7 (1M context) --- src/renderer/components/QueryList/QueryList.css | 16 +++++++++++++--- src/renderer/components/QueryList/QueryList.tsx | 5 ++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/renderer/components/QueryList/QueryList.css b/src/renderer/components/QueryList/QueryList.css index 4c6f4c9..61e6580 100644 --- a/src/renderer/components/QueryList/QueryList.css +++ b/src/renderer/components/QueryList/QueryList.css @@ -106,10 +106,20 @@ margin-top: 2px; } +.QueryList-item-meta { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10px; + margin-bottom: 5px; +} + +.QueryList-item-id { + font-size: 11px; + color: var(--color-text-muted); +} + .QueryList-item-time { font-size: 11px; color: var(--color-text-muted); - align-self: flex-end; - margin-top: 10px; - margin-bottom: 5px; } diff --git a/src/renderer/components/QueryList/QueryList.tsx b/src/renderer/components/QueryList/QueryList.tsx index 09cbc06..c85f126 100644 --- a/src/renderer/components/QueryList/QueryList.tsx +++ b/src/renderer/components/QueryList/QueryList.tsx @@ -95,7 +95,10 @@ const QueryList: React.FC = ({
{query.title}
{query.body.replace(/\s{2,}/g, " ").substring(0, 50)}
-
{query.createdAt.format("YYYY-MM-DD")}
+
+
ID: {query.id}
+
{query.createdAt.format("YYYY-MM-DD")}
+
))} From f2f3201da2e34fb60ee4cdafb8959d7d58191916 Mon Sep 17 00:00:00 2001 From: Shintaro Morikawa Date: Tue, 26 May 2026 12:29:27 +0900 Subject: [PATCH 2/4] Add update notification badge for queries updated while window is inactive Track window blur timestamp and mark queries whose updatedAt is newer than that timestamp when the window regains focus. The mark is cleared when the query is selected, or when the previously selected query is moved away from. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/Database/Query.ts | 9 +++- .../components/QueryList/QueryList.css | 11 +++++ .../components/QueryList/QueryList.tsx | 9 +++- src/renderer/pages/Query/Query.tsx | 6 +++ src/renderer/pages/Query/QueryAction.ts | 4 ++ src/renderer/pages/Query/QueryStore.ts | 45 +++++++++++++++++-- test/unit/lib/Database/Query.test.ts | 2 + 7 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/lib/Database/Query.ts b/src/lib/Database/Query.ts index cc8d9ee..500989e 100644 --- a/src/lib/Database/Query.ts +++ b/src/lib/Database/Query.ts @@ -30,12 +30,17 @@ export type DatabaseQueryType = Omit[]> { - const results = await connection.all("select id, title, body, createdAt from queries order by createdAt desc"); + static async getAll(): Promise[]> { + const results = await connection.all( + "select id, title, body, createdAt, updatedAt from queries order by createdAt desc" + ); return results.map((query) => { if (query.createdAt) { query.createdAt = moment.utc(query.createdAt, "YYYY-MM-DD HH:mm:ss", true).local(); } + if (query.updatedAt) { + query.updatedAt = moment.utc(query.updatedAt, "YYYY-MM-DD HH:mm:ss", true).local(); + } return query; }); } diff --git a/src/renderer/components/QueryList/QueryList.css b/src/renderer/components/QueryList/QueryList.css index 61e6580..4d74c11 100644 --- a/src/renderer/components/QueryList/QueryList.css +++ b/src/renderer/components/QueryList/QueryList.css @@ -117,6 +117,17 @@ .QueryList-item-id { font-size: 11px; color: var(--color-text-muted); + display: flex; + align-items: center; + gap: 6px; +} + +.QueryList-item-update-mark { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--color-accent); } .QueryList-item-time { diff --git a/src/renderer/components/QueryList/QueryList.tsx b/src/renderer/components/QueryList/QueryList.tsx index c85f126..719ca96 100644 --- a/src/renderer/components/QueryList/QueryList.tsx +++ b/src/renderer/components/QueryList/QueryList.tsx @@ -9,6 +9,7 @@ const MENU_ID = "QUERY_LIST_MENU"; type Props = { readonly queries: QueryType[]; readonly selectedQueryId: number | null; + readonly markedQueryIds: number[]; readonly onAddQuery: () => void; readonly onRefresh: () => void; readonly onSelectQuery: (queryId: number) => void; @@ -19,6 +20,7 @@ type Props = { const QueryList: React.FC = ({ queries, selectedQueryId, + markedQueryIds, onAddQuery, onRefresh, onSelectQuery, @@ -96,7 +98,12 @@ const QueryList: React.FC = ({
{query.title}
{query.body.replace(/\s{2,}/g, " ").substring(0, 50)}
-
ID: {query.id}
+
+ ID: {query.id} + {markedQueryIds.includes(query.id) && ( + + )} +
{query.createdAt.format("YYYY-MM-DD")}
diff --git a/src/renderer/pages/Query/Query.tsx b/src/renderer/pages/Query/Query.tsx index 0318de2..1676fc5 100644 --- a/src/renderer/pages/Query/Query.tsx +++ b/src/renderer/pages/Query/Query.tsx @@ -17,16 +17,22 @@ class Query extends React.Component { override componentDidMount(): void { Action.initialize(); window.addEventListener("focus", this.handleWindowFocus); + window.addEventListener("blur", this.handleWindowBlur); } override componentWillUnmount(): void { window.removeEventListener("focus", this.handleWindowFocus); + window.removeEventListener("blur", this.handleWindowBlur); } private handleWindowFocus = (): void => { Action.initialize(); }; + private handleWindowBlur = (): void => { + Action.markWindowBlurred(); + }; + handleAddQuery(): void { const defaultDataSourceId = this.state.setting.defaultDataSourceId; const ds = defaultDataSourceId !== null ? this.findDataSourceById(defaultDataSourceId) : this.state.dataSources[0]; diff --git a/src/renderer/pages/Query/QueryAction.ts b/src/renderer/pages/Query/QueryAction.ts index 657065f..8c5dc6d 100644 --- a/src/renderer/pages/Query/QueryAction.ts +++ b/src/renderer/pages/Query/QueryAction.ts @@ -156,6 +156,10 @@ const QueryAction = { dispatch("updateEditor", params); }, + markWindowBlurred(): void { + dispatch("windowBlurred", { timestamp: Date.now() }); + }, + async selectResultTab(query: QueryType, name: string): Promise { dispatch("selectResultTab", { id: query.id, name }); diff --git a/src/renderer/pages/Query/QueryStore.ts b/src/renderer/pages/Query/QueryStore.ts index 1ea15f4..14a4fb4 100644 --- a/src/renderer/pages/Query/QueryStore.ts +++ b/src/renderer/pages/Query/QueryStore.ts @@ -10,6 +10,8 @@ export interface QueryState { dataSources: DataSourceType[]; charts: ChartType[]; selectedQueryId: number | null; + markedQueryIds: number[]; + inactiveSinceAt: number | null; editor: { height: number | null; line: number | null; @@ -25,6 +27,8 @@ export default class QueryStore extends Store { dataSources: [], charts: [], selectedQueryId: null, + markedQueryIds: [], + inactiveSinceAt: null, editor: { height: null, line: null, @@ -35,10 +39,29 @@ export default class QueryStore extends Store { override reduce(type: string, payload: any): StateBuilder { switch (type) { case "initialize": { + const inactiveSinceAt = this.state.inactiveSinceAt; + const existingMarkedIds = this.state.markedQueryIds; + const existingIds = new Set(payload.queries.map((q: QueryType) => q.id)); + let newMarkedIds = existingMarkedIds.filter((id) => existingIds.has(id)); + if (inactiveSinceAt !== null) { + for (const q of payload.queries as QueryType[]) { + if (q.updatedAt && q.updatedAt.valueOf() > inactiveSinceAt && !newMarkedIds.includes(q.id)) { + newMarkedIds = [...newMarkedIds, q.id]; + } + } + } return this.merge("setting", payload.setting) .mergeList("queries", payload.queries) .mergeList("charts", payload.charts) - .mergeList("dataSources", payload.dataSources); + .mergeList("dataSources", payload.dataSources) + .set("markedQueryIds", newMarkedIds) + .set("inactiveSinceAt", null); + } + case "windowBlurred": { + if (this.state.inactiveSinceAt !== null) { + return this.set("inactiveSinceAt", this.state.inactiveSinceAt); + } + return this.set("inactiveSinceAt", payload.timestamp); } case "selectQuery": { const idx = this.findQueryIndex(payload.id); @@ -49,7 +72,17 @@ export default class QueryStore extends Store { status: currentQuery.status || payload.query.status, executor: currentQuery.executor, }; - return this.set("selectedQueryId", payload.id).set("editor.line", null).merge(`queries.${idx}`, updatedQuery); + const idsToClear = new Set([payload.id]); + if (this.state.selectedQueryId !== null) { + idsToClear.add(this.state.selectedQueryId); + } + return this.set("selectedQueryId", payload.id) + .set("editor.line", null) + .merge(`queries.${idx}`, updatedQuery) + .set( + "markedQueryIds", + this.state.markedQueryIds.filter((id) => !idsToClear.has(id)) + ); } case "addNewQuery": { return this.set("selectedQueryId", payload.query.id).set("editor.line", null).prepend("queries", payload.query); @@ -60,7 +93,13 @@ export default class QueryStore extends Store { } case "deleteQuery": { const idx = this.findQueryIndex(payload.id); - return this.set("selectedQueryId", null).set("editor.line", null).del(`queries.${idx}`); + return this.set("selectedQueryId", null) + .set("editor.line", null) + .del(`queries.${idx}`) + .set( + "markedQueryIds", + this.state.markedQueryIds.filter((id) => id !== payload.id) + ); } case "updateEditor": { return this.merge("editor", payload); diff --git a/test/unit/lib/Database/Query.test.ts b/test/unit/lib/Database/Query.test.ts index b4790a8..f0f421d 100644 --- a/test/unit/lib/Database/Query.test.ts +++ b/test/unit/lib/Database/Query.test.ts @@ -21,12 +21,14 @@ suite("Database/Query", () => { id: 1, title: "title 1", createdAt: moment.utc("2017-01-01 00:00:00", "YYYY-MM-DD HH:mm:ss", true).local(), + updatedAt: moment.utc("2017-01-02 00:00:00", "YYYY-MM-DD HH:mm:ss", true).local(), body: "select 1;", }, { id: 2, title: "title 2", createdAt: moment.utc("2017-01-01 00:00:00", "YYYY-MM-DD HH:mm:ss", true).local(), + updatedAt: moment.utc("2017-01-02 00:00:00", "YYYY-MM-DD HH:mm:ss", true).local(), body: "select 1;", }, ]); From d02866caf1bbc0503394d3fd7b8c75ed0529e49c Mon Sep 17 00:00:00 2001 From: Shintaro Morikawa Date: Tue, 26 May 2026 12:38:57 +0900 Subject: [PATCH 3/4] Also mark queries when execution finishes while a different query is selected Previously the mark was only added on window focus based on updatedAt. With this change, a query whose execution completes while it is not the currently selected query is also marked, so the user notices the result when switching back to it. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/renderer/pages/Query/QueryAction.ts | 2 ++ src/renderer/pages/Query/QueryStore.ts | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/renderer/pages/Query/QueryAction.ts b/src/renderer/pages/Query/QueryAction.ts index 8c5dc6d..8364831 100644 --- a/src/renderer/pages/Query/QueryAction.ts +++ b/src/renderer/pages/Query/QueryAction.ts @@ -105,6 +105,7 @@ const QueryAction = { id, params: Object.assign({ executor: null }, params), }); + dispatch("markQueryAfterExecution", { id }); Database.Query.update(id, params); ipcRenderer.send("queryCompleted", { @@ -129,6 +130,7 @@ const QueryAction = { id, params: Object.assign({ executor: null }, params), }); + dispatch("markQueryAfterExecution", { id }); Database.Query.update( id, Object.assign(params, { diff --git a/src/renderer/pages/Query/QueryStore.ts b/src/renderer/pages/Query/QueryStore.ts index 14a4fb4..5707b6d 100644 --- a/src/renderer/pages/Query/QueryStore.ts +++ b/src/renderer/pages/Query/QueryStore.ts @@ -63,6 +63,15 @@ export default class QueryStore extends Store { } return this.set("inactiveSinceAt", payload.timestamp); } + case "markQueryAfterExecution": { + if (this.state.selectedQueryId === payload.id) { + return this.set("markedQueryIds", this.state.markedQueryIds); + } + if (this.state.markedQueryIds.includes(payload.id)) { + return this.set("markedQueryIds", this.state.markedQueryIds); + } + return this.set("markedQueryIds", [...this.state.markedQueryIds, payload.id]); + } case "selectQuery": { const idx = this.findQueryIndex(payload.id); const currentQuery = this.state.queries[idx]; From f177d0c84c9f02dda6e9fc8e349c7f695dcf5263 Mon Sep 17 00:00:00 2001 From: Shintaro Morikawa Date: Thu, 28 May 2026 15:05:58 +0900 Subject: [PATCH 4/4] Simplify mark computation in initialize reducer Use a Set for the marked ids so duplicates are deduped automatically, and fold the inactive-period guard into the filter predicate, dropping the explicit includes check and the surrounding if block. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/renderer/pages/Query/QueryStore.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/renderer/pages/Query/QueryStore.ts b/src/renderer/pages/Query/QueryStore.ts index 5707b6d..58c45ee 100644 --- a/src/renderer/pages/Query/QueryStore.ts +++ b/src/renderer/pages/Query/QueryStore.ts @@ -39,22 +39,17 @@ export default class QueryStore extends Store { override reduce(type: string, payload: any): StateBuilder { switch (type) { case "initialize": { - const inactiveSinceAt = this.state.inactiveSinceAt; - const existingMarkedIds = this.state.markedQueryIds; + const { inactiveSinceAt } = this.state; const existingIds = new Set(payload.queries.map((q: QueryType) => q.id)); - let newMarkedIds = existingMarkedIds.filter((id) => existingIds.has(id)); - if (inactiveSinceAt !== null) { - for (const q of payload.queries as QueryType[]) { - if (q.updatedAt && q.updatedAt.valueOf() > inactiveSinceAt && !newMarkedIds.includes(q.id)) { - newMarkedIds = [...newMarkedIds, q.id]; - } - } - } + const markedIds = new Set(this.state.markedQueryIds.filter((id) => existingIds.has(id))); + (payload.queries as QueryType[]) + .filter((q) => inactiveSinceAt !== null && q.updatedAt && q.updatedAt.valueOf() > inactiveSinceAt) + .forEach((q) => markedIds.add(q.id)); return this.merge("setting", payload.setting) .mergeList("queries", payload.queries) .mergeList("charts", payload.charts) .mergeList("dataSources", payload.dataSources) - .set("markedQueryIds", newMarkedIds) + .set("markedQueryIds", [...markedIds]) .set("inactiveSinceAt", null); } case "windowBlurred": {