Skip to content

Commit 012178c

Browse files
authored
feat: support caching filter value for the report page (#449) (#451)
1 parent adbecb4 commit 012178c

File tree

27 files changed

+498
-479
lines changed

27 files changed

+498
-479
lines changed

package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@
3030
"devDependencies": {
3131
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
3232
"@babel/preset-env": "^7.26.9",
33-
"@crowdin/crowdin-api-client": "^1.41.4",
33+
"@crowdin/crowdin-api-client": "^1.42.0",
3434
"@rsdoctor/webpack-plugin": "^1.0.1",
35-
"@swc/core": "^1.11.13",
35+
"@swc/core": "^1.11.20",
3636
"@swc/jest": "^0.2.37",
37-
"@types/chrome": "0.0.313",
37+
"@types/chrome": "0.0.315",
3838
"@types/decompress": "^4.2.7",
3939
"@types/generate-json-webpack-plugin": "^0.3.7",
4040
"@types/jest": "^29.5.14",
41-
"@types/node": "^22.13.14",
41+
"@types/node": "^22.14.1",
4242
"@types/punycode": "^2.1.4",
4343
"@types/webpack": "^5.28.5",
4444
"@vue/babel-plugin-jsx": "^1.4.0",
@@ -47,7 +47,7 @@
4747
"copy-webpack-plugin": "^13.0.0",
4848
"css-loader": "^7.1.2",
4949
"decompress": "^4.2.1",
50-
"eslint": "^9.23.0",
50+
"eslint": "^9.24.0",
5151
"filemanager-webpack-plugin": "^8.0.0",
5252
"generate-json-webpack-plugin": "^2.0.0",
5353
"html-webpack-plugin": "^5.6.3",
@@ -58,23 +58,23 @@
5858
"mini-css-extract-plugin": "^2.9.2",
5959
"postcss": "^8.5.3",
6060
"postcss-loader": "^8.1.1",
61-
"postcss-rtlcss": "^5.6.0",
62-
"puppeteer": "^24.4.0",
63-
"sass": "^1.86.0",
61+
"postcss-rtlcss": "^5.7.0",
62+
"puppeteer": "^24.6.1",
63+
"sass": "^1.86.3",
6464
"sass-loader": "^16.0.5",
6565
"style-loader": "^4.0.0",
6666
"ts-loader": "^9.5.2",
6767
"ts-node": "^10.9.2",
6868
"tsconfig-paths": "^4.2.0",
69-
"typescript": "5.8.2",
69+
"typescript": "5.8.3",
7070
"url-loader": "^4.1.1",
7171
"web-ext": "^8.5.0",
72-
"webpack": "^5.98.0",
72+
"webpack": "^5.99.5",
7373
"webpack-cli": "^6.0.1"
7474
},
7575
"dependencies": {
7676
"@element-plus/icons-vue": "^2.3.1",
77-
"@vueuse/core": "^13.0.0",
77+
"@vueuse/core": "^13.1.0",
7878
"countup.js": "^2.8.0",
7979
"echarts": "^5.6.0",
8080
"element-plus": "2.9.7",

src/i18n/element.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ export const initElementLocale = async (app: App) => {
2424
app.use(ElementPlus, { locale: EL_LOCALE })
2525
}
2626

27-
export const EL_DATE_FORMAT = t(calendarMessages, { key: msg => msg.dateFormat, param: { y: 'YYYY', m: 'MM', d: 'DD' } })
27+
export const dateFormat = () => t(calendarMessages, { key: msg => msg.dateFormat, param: { y: 'YYYY', m: 'MM', d: 'DD' } })

src/i18n/message/app/option-resource.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@
350350
"authInfo": "One token with at least gist permission is required"
351351
},
352352
"obsidian_local_rest_api": {
353-
"endpointInfo": "Only HTTP is available because it is not possible to configure CORS for extensions pages"
353+
"endpointInfo": "Only HTTP is available, as CORS cannot be configured for extension pages"
354354
},
355355
"web_dav": {}
356356
},

src/pages/app/components/Analysis/components/Trend/Filter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import { t } from "@app/locale"
9-
import { EL_DATE_FORMAT } from "@i18n/element"
9+
import { dateFormat } from "@i18n/element"
1010
import { type ElementDatePickerShortcut } from "@pages/element-ui/date"
1111
import { getDatePickerIconSlots } from "@pages/element-ui/rtl"
1212
import { daysAgo } from "@util/time"
@@ -41,7 +41,7 @@ const _default = defineComponent({
4141
<ElDatePicker
4242
modelValue={dateRange.value}
4343
disabledDate={(date: Date) => date.getTime() > new Date().getTime()}
44-
format={EL_DATE_FORMAT}
44+
format={dateFormat()}
4545
type="daterange"
4646
shortcuts={SHORTCUTS}
4747
rangeSeparator="-"

src/pages/app/components/DataManage/ClearPanel/ClearFilter/DateFilter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import I18nNode from "@app/components/common/I18nNode"
88
import { t } from "@app/locale"
9-
import { EL_DATE_FORMAT } from "@i18n/element"
9+
import { dateFormat as elDateFormat } from "@i18n/element"
1010
import { type ElementDatePickerShortcut } from "@pages/element-ui/date"
1111
import { getDatePickerIconSlots } from "@pages/element-ui/rtl"
1212
import { formatTime, getBirthday, MILL_PER_DAY } from "@util/time"
@@ -54,7 +54,7 @@ const _default = defineComponent({
5454
style={{ width: "250px" } satisfies StyleValue}
5555
startPlaceholder={startPlaceholder}
5656
endPlaceholder={endPlaceholder}
57-
dateFormat={EL_DATE_FORMAT}
57+
dateFormat={elDateFormat()}
5858
type="daterange"
5959
disabledDate={(date: Date) => date.getTime() > yesterday}
6060
shortcuts={pickerShortcuts}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { type I18nKey, t } from "@app/locale"
2+
import StatDatabase from "@db/stat-database"
3+
import { DeleteFilled } from "@element-plus/icons-vue"
4+
import statService from "@service/stat-service"
5+
import { groupBy, sum } from "@util/array"
6+
import { formatTime } from "@util/time"
7+
import { ElButton, ElMessage, ElMessageBox } from "element-plus"
8+
import { defineComponent } from "vue"
9+
import { useReportComponent, useReportFilter } from "../context"
10+
import type { DisplayComponent, ReportFilterOption } from "../types"
11+
12+
const statDatabase = new StatDatabase(chrome.storage.local)
13+
14+
async function computeBatchDeleteMsg(selected: timer.stat.Row[], mergeDate: boolean, dateRange: [Date, Date] | undefined): Promise<string> {
15+
// host => total focus
16+
const hostFocus: { [host: string]: number } = groupBy(selected,
17+
a => a.siteKey?.host,
18+
grouped => grouped.map(a => a.focus).reduce((a, b) => a + b, 0)
19+
)
20+
const hosts = Object.keys(hostFocus)
21+
if (!hosts.length) {
22+
// Never happen
23+
return t(msg => msg.report.batchDelete.noSelectedMsg)
24+
}
25+
const count2Delete: number = mergeDate
26+
// All the items
27+
? sum(await Promise.all(Array.from(hosts).map(host => statService.count({ host, fullHost: true, date: dateRange }))))
28+
// The count of row
29+
: selected?.length || 0
30+
const i18nParam: Record<string, string | number | undefined> = {
31+
// count
32+
count: count2Delete,
33+
// example for hosts
34+
example: hosts[0],
35+
// Start date, if range
36+
start: undefined,
37+
// End date, if range
38+
end: undefined,
39+
// Date, if single date
40+
date: undefined,
41+
}
42+
43+
let key: I18nKey | undefined = undefined
44+
const hasDateRange = dateRange?.length === 2 && (dateRange[0] || dateRange[1])
45+
if (!hasDateRange) {
46+
// Delete all
47+
key = msg => msg.report.batchDelete.confirmMsgAll
48+
} else {
49+
const dateFormat = t(msg => msg.calendar.dateFormat)
50+
const startDate = dateRange[0]
51+
const endDate = dateRange[1]
52+
const start = formatTime(startDate, dateFormat)
53+
const end = formatTime(endDate, dateFormat)
54+
if (start === end) {
55+
// Single date
56+
key = msg => msg.report.batchDelete.confirmMsg
57+
i18nParam.date = start
58+
} else {
59+
// Date range
60+
key = msg => msg.report.batchDelete.confirmMsgRange
61+
i18nParam.start = start
62+
i18nParam.end = end
63+
}
64+
}
65+
return t(key, i18nParam)
66+
}
67+
68+
async function handleBatchDelete(displayComp: DisplayComponent | undefined, filter: ReportFilterOption) {
69+
if (!displayComp) return
70+
71+
const selected: timer.stat.Row[] = displayComp?.getSelected?.() || []
72+
if (!selected?.length) {
73+
ElMessage.info(t(msg => msg.report.batchDelete.noSelectedMsg))
74+
return
75+
}
76+
const { dateRange, mergeDate } = filter
77+
ElMessageBox({
78+
message: await computeBatchDeleteMsg(selected, mergeDate, dateRange),
79+
type: "warning",
80+
confirmButtonText: t(msg => msg.button.okey),
81+
showCancelButton: true,
82+
cancelButtonText: t(msg => msg.button.dont),
83+
// Cant close this on press ESC
84+
closeOnPressEscape: false,
85+
// Cant close this on clicking modal
86+
closeOnClickModal: false
87+
}).then(async () => {
88+
// Delete
89+
await deleteBatch(selected, mergeDate, dateRange)
90+
ElMessage.success(t(msg => msg.operation.successMsg))
91+
displayComp?.refresh?.()
92+
}).catch(() => {
93+
// Do nothing
94+
})
95+
}
96+
97+
async function deleteBatch(selected: timer.stat.Row[], mergeDate: boolean, dateRange: [Date, Date] | undefined) {
98+
if (mergeDate) {
99+
// Delete according to the date range
100+
const start = dateRange?.[0]
101+
const end = dateRange?.[1]
102+
const hosts = selected.map(d => d.siteKey?.host)
103+
await Promise.all(hosts.map(async h => h && await statDatabase.deleteByUrlBetween(h, start, end)))
104+
} else {
105+
// If not merge date, batch delete
106+
await statService.batchDelete(selected)
107+
}
108+
}
109+
110+
const BatchDelete = defineComponent(() => {
111+
const filter = useReportFilter()
112+
const comp = useReportComponent()
113+
114+
return () => (
115+
<ElButton
116+
v-show={!filter.readRemote}
117+
disabled={!!filter.siteMerge}
118+
type="primary"
119+
link
120+
icon={<DeleteFilled />}
121+
onClick={() => handleBatchDelete(comp.value, filter)}
122+
>
123+
{t(msg => msg.button.batchDelete)}
124+
</ElButton>
125+
)
126+
})
127+
128+
export default BatchDelete

src/pages/app/components/Report/ReportFilter/DownloadFile.tsx

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,51 @@
55
* https://opensource.org/licenses/MIT
66
*/
77

8+
import { useCategories } from "@app/context"
89
import { Download } from "@element-plus/icons-vue"
10+
import statService from "@service/stat-service"
911
import { ElButton, ElDropdown, ElDropdownItem, ElDropdownMenu, ElIcon } from "element-plus"
1012
import { defineComponent } from "vue"
11-
import type { FileFormat } from "../types"
13+
import { cvtOption2Param } from "../common"
14+
import { useReportFilter } from "../context"
15+
import { exportCsv, exportJson } from "../file-export"
1216
import { ICON_BTN_STYLE } from "./common"
1317

14-
const ALL_FILE_FORMATS: FileFormat[] = ["json", "csv"]
18+
const ALL_FILE_FORMATS = ["json", "csv"] as const
19+
type FileFormat = typeof ALL_FILE_FORMATS[number]
1520

16-
const _default = defineComponent({
17-
emits: {
18-
download: (_format: FileFormat) => true,
19-
},
20-
setup(_, ctx) {
21-
const handleClick = (format: FileFormat) => ctx.emit('download', format)
22-
return () => (
23-
<ElDropdown
24-
showTimeout={100}
25-
v-slots={{
26-
dropdown: () => <ElDropdownMenu>
27-
{ALL_FILE_FORMATS.map(f =>
28-
<ElDropdownItem onClick={() => handleClick(f)}>
29-
{f}
30-
</ElDropdownItem>
31-
)}
32-
</ElDropdownMenu>
33-
}}
34-
>
35-
<ElButton size="small" style={ICON_BTN_STYLE}>
36-
<ElIcon size={17} style={{ padding: "0 1px" }}>
37-
<Download />
38-
</ElIcon>
39-
</ElButton>
40-
</ElDropdown>
41-
)
21+
const DownloadFile = defineComponent(() => {
22+
const filter = useReportFilter()
23+
const { categories } = useCategories()
24+
25+
const handleDownload = async (format: FileFormat) => {
26+
const categoriesVal = categories.value
27+
const param = cvtOption2Param(filter)
28+
const rows = await statService.select(param, true)
29+
format === 'json' && exportJson(filter, rows, categoriesVal)
30+
format === 'csv' && exportCsv(filter, rows, categoriesVal)
4231
}
32+
33+
return () => (
34+
<ElDropdown
35+
showTimeout={100}
36+
v-slots={{
37+
dropdown: () => <ElDropdownMenu>
38+
{ALL_FILE_FORMATS.map(f =>
39+
<ElDropdownItem onClick={() => handleDownload(f)}>
40+
{f}
41+
</ElDropdownItem>
42+
)}
43+
</ElDropdownMenu>
44+
}}
45+
>
46+
<ElButton size="small" style={ICON_BTN_STYLE}>
47+
<ElIcon size={17} style={{ padding: "0 1px" }}>
48+
<Download />
49+
</ElIcon>
50+
</ElButton>
51+
</ElDropdown>
52+
)
4353
})
4454

45-
export default _default
55+
export default DownloadFile

0 commit comments

Comments
 (0)