Skip to content

Commit 273f555

Browse files
authored
fix: not working to import data from WATT (#637)
1 parent f23f180 commit 273f555

File tree

6 files changed

+138
-109
lines changed

6 files changed

+138
-109
lines changed

src/pages/app/components/DataManage/Migration/ImportOtherButton/Sop.tsx

Lines changed: 51 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,63 +15,59 @@ import { defineComponent, ref } from "vue"
1515
import Step1 from "./Step1"
1616
import Step2 from "./Step2"
1717

18-
const _default = defineComponent({
19-
emits: {
20-
cancel: () => true,
21-
import: () => true,
22-
},
23-
setup(_, ctx) {
24-
const step = ref<0 | 1>(0)
25-
const step1 = ref<SopStepInstance<timer.imported.Data>>()
26-
const step2 = ref<SopStepInstance<timer.imported.ConflictResolution>>()
18+
type Props = {
19+
onCancel: NoArgCallback
20+
onImport: NoArgCallback
21+
}
2722

28-
const { data, refresh: handleNext, loading: parsing } = useManualRequest(() => step1.value!.parseData(), {
29-
defaultValue: { rows: [] },
30-
onSuccess: () => step.value = 1,
31-
onError: e => ElMessage.error((e as Error)?.message ?? 'Unknown Error')
32-
})
23+
const _default = defineComponent<Props>((props) => {
24+
const step = ref<0 | 1>(0)
25+
const step1 = ref<SopStepInstance<timer.imported.Data>>()
26+
const step2 = ref<SopStepInstance<timer.imported.ConflictResolution>>()
3327

34-
const { loading: importing, refresh: doImport } = useManualRequest(
35-
async () => {
36-
const resolution = await step2.value?.parseData?.()
37-
if (!resolution) throw new Error(t(msg => msg.dataManage.importOther.conflictNotSelected))
38-
await processImportedData(data.value, resolution)
39-
},
40-
{
41-
onSuccess: () => {
42-
ElMessage.success(t(msg => msg.operation.successMsg))
43-
ctx.emit('import')
44-
},
45-
onError: e => ElMessage.warning((e as Error)?.message ?? 'Unknown error'),
46-
}
47-
)
28+
const { data, refresh: handleNext, loading: parsing } = useManualRequest(() => step1.value!.parseData(), {
29+
defaultValue: { rows: [] },
30+
onSuccess: () => step.value = 1,
31+
onError: e => ElMessage.error((e as Error)?.message ?? 'Unknown Error')
32+
})
4833

49-
return () => (
50-
<DialogSop
51-
first={step.value === 0}
52-
last={step.value === 1}
53-
onCancel={() => ctx.emit('cancel')}
54-
onBack={() => step.value = 0}
55-
onNext={handleNext}
56-
onFinish={doImport}
57-
nextLoading={parsing.value}
58-
finishLoading={importing.value}
59-
v-slots={{
60-
steps: () => (
61-
<ElSteps space={200} finishStatus="success" active={step.value} alignCenter>
62-
<ElStep title={t(msg => msg.dataManage.importOther.step1)} />
63-
<ElStep title={t(msg => msg.dataManage.importOther.step2)} />
64-
</ElSteps>
65-
),
66-
content: () => (
67-
<Flex width="100%" justify="center">
68-
{step.value === 0 ? <Step1 ref={step1} /> : <Step2 ref={step2} data={data.value} />}
69-
</Flex>
70-
),
71-
}}
72-
/>
73-
)
74-
}
75-
})
34+
const { loading: importing, refresh: doImport } = useManualRequest(async () => {
35+
const resolution = await step2.value?.parseData?.()
36+
if (!resolution) throw new Error(t(msg => msg.dataManage.importOther.conflictNotSelected))
37+
await processImportedData(data.value, resolution)
38+
}, {
39+
onSuccess: () => {
40+
ElMessage.success(t(msg => msg.operation.successMsg))
41+
props.onImport?.()
42+
},
43+
onError: e => ElMessage.warning((e as Error)?.message ?? 'Unknown error'),
44+
})
45+
46+
return () => (
47+
<DialogSop
48+
first={step.value === 0}
49+
last={step.value === 1}
50+
onCancel={props.onCancel}
51+
onBack={() => step.value = 0}
52+
onNext={handleNext}
53+
onFinish={doImport}
54+
nextLoading={parsing.value}
55+
finishLoading={importing.value}
56+
v-slots={{
57+
steps: () => (
58+
<ElSteps space={200} finishStatus="success" active={step.value} alignCenter>
59+
<ElStep title={t(msg => msg.dataManage.importOther.step1)} />
60+
<ElStep title={t(msg => msg.dataManage.importOther.step2)} />
61+
</ElSteps>
62+
),
63+
content: () => (
64+
<Flex width="100%" justify="center">
65+
{step.value === 0 ? <Step1 ref={step1} /> : <Step2 ref={step2} data={data.value} />}
66+
</Flex>
67+
),
68+
}}
69+
/>
70+
)
71+
}, { props: ['onCancel', 'onImport'] })
7672

7773
export default _default

src/pages/app/components/DataManage/Migration/ImportOtherButton/Step1.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { t } from "@app/locale"
1010
import { Document } from "@element-plus/icons-vue"
1111
import { useState } from "@hooks"
1212
import Flex from "@pages/components/Flex"
13-
import { ElButton, ElForm, ElFormItem, ElOption, ElSelect } from "element-plus"
13+
import { ElButton, ElForm, ElFormItem, ElSelect } from "element-plus"
1414
import { defineComponent, ref } from "vue"
1515
import { type OtherExtension, parseFile } from "./processor"
1616

@@ -22,13 +22,13 @@ const OTHER_NAMES: { [ext in OtherExtension]: string } = {
2222

2323
const OTHER_FILE_FORMAT: { [ext in OtherExtension]: string } = {
2424
webtime_tracker: '.csv,.json',
25-
web_activity_time_tracker: '.csv',
25+
web_activity_time_tracker: '.csv,.json',
2626
history_trends_unlimited: '.tsv',
2727
}
2828

2929
const ALL_TYPES: OtherExtension[] = Object.keys(OTHER_NAMES) as OtherExtension[]
3030

31-
const _default = defineComponent((_, ctx) => {
31+
const _default = defineComponent<{}>((_, ctx) => {
3232
const [type, setType] = useState<OtherExtension>('webtime_tracker')
3333
const [selectedFile, setSelectedFile] = useState<File>()
3434
const fileInput = ref<HTMLInputElement>()
@@ -48,11 +48,10 @@ const _default = defineComponent((_, ctx) => {
4848
return () => (
4949
<ElForm labelWidth={100} labelPosition="left" style={{ width: '500px' }}>
5050
<ElFormItem label={t(msg => msg.dataManage.importOther.dataSource)} required>
51-
<ElSelect modelValue={type.value} onChange={setType}>
52-
{
53-
ALL_TYPES.map(type => <ElOption value={type} label={OTHER_NAMES[type]} />)
54-
}
55-
</ElSelect>
51+
<ElSelect
52+
modelValue={type.value} onChange={setType}
53+
options={ALL_TYPES.map(value => ({ value, label: OTHER_NAMES[value] }))}
54+
/>
5655
</ElFormItem>
5756
<ElFormItem label={t(msg => msg.dataManage.importOther.file)} required>
5857
<Flex gap={10}>

src/pages/app/components/DataManage/Migration/ImportOtherButton/Step2.tsx

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,25 @@
88
import { type SopStepInstance } from "@app/components/common/DialogSop"
99
import CompareTable from "@app/components/common/imported/CompareTable"
1010
import ResolutionRadio from "@app/components/common/imported/ResolutionRadio"
11-
import { t } from "@app/locale"
1211
import { useState } from "@hooks"
1312
import Flex from "@pages/components/Flex"
14-
import { defineComponent, type PropType } from "vue"
13+
import { defineComponent } from "vue"
1514

16-
const _default = defineComponent({
17-
props: {
18-
data: {
19-
type: Object as PropType<timer.imported.Data>,
20-
required: true,
21-
},
22-
},
23-
setup(props, ctx) {
24-
const [resolution, setResolution] = useState<timer.imported.ConflictResolution>()
15+
const _default = defineComponent<{ data: timer.imported.Data }>((props, ctx) => {
16+
const [resolution, setResolution] = useState<timer.imported.ConflictResolution>()
2517

26-
ctx.expose({
27-
parseData: () => resolution.value
28-
} satisfies SopStepInstance<timer.imported.ConflictResolution | undefined>)
18+
ctx.expose({
19+
parseData: () => resolution.value
20+
} satisfies SopStepInstance<timer.imported.ConflictResolution | undefined>)
2921

30-
return () => (
31-
<Flex column width="100%" gap={20}>
32-
<CompareTable
33-
data={props.data}
34-
comparedColName={t(msg => msg.dataManage.importOther.imported)}
35-
/>
36-
<Flex width="100%" justify="center">
37-
<ResolutionRadio modelValue={resolution.value} onChange={setResolution} />
38-
</Flex>
22+
return () => (
23+
<Flex column width="100%" gap={20}>
24+
<CompareTable data={props.data} comparedCol={msg => msg.dataManage.importOther.imported} />
25+
<Flex width="100%" justify="center">
26+
<ResolutionRadio modelValue={resolution.value} onChange={setResolution} />
3927
</Flex>
40-
)
41-
}
42-
})
28+
</Flex>
29+
)
30+
}, { props: ['data'] })
4331

4432
export default _default

src/pages/app/components/DataManage/Migration/ImportOtherButton/processor.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function parseFile(ext: OtherExtension, file: File): Promise<timer.
3131
let focus = false
3232
let time = false
3333
if (ext === 'web_activity_time_tracker') {
34-
rows = await parseWebActivityTimeTracker(file)
34+
[rows, time] = await parseWebActivityTimeTracker(file)
3535
focus = true
3636
} else if (ext === 'webtime_tracker') {
3737
rows = await parseWebtimeTracker(file)
@@ -44,16 +44,62 @@ export async function parseFile(ext: OtherExtension, file: File): Promise<timer.
4444
return { rows, focus, time }
4545
}
4646

47-
async function parseWebActivityTimeTracker(file: File): Promise<timer.imported.Row[]> {
47+
async function parseWebActivityTimeTracker(file: File): Promise<[timer.imported.Row[], time: boolean]> {
4848
const text = await file.text()
49-
const lines = text.split('\n').map(line => line.trim()).filter(line => !!line).splice(1)
50-
const rows = lines.map(line => {
51-
const [host, date, seconds] = line.split(',').map(cell => cell.trim())
52-
!host || !date || (!seconds && seconds !== '0') && throwError()
53-
const [year, month, day] = date.split('/')
54-
!year || !month || !day && throwError()
55-
const realDate = `${year}${month.length == 2 ? month : '0' + month}${day.length == 2 ? day : '0' + day}`
56-
return { host, date: realDate, focus: parseInt(seconds) * MILL_PER_SECOND, time: 0 } satisfies timer.imported.Row
49+
if (isCsvFile(file)) {
50+
const lines = text.split('\n').map(line => line.trim()).filter(line => !!line).splice(1)
51+
const rows = lines.map(line => {
52+
const [host, date, seconds] = line.split(',').map(cell => cell.trim())
53+
!host || !date || (!seconds && seconds !== '0') && throwError()
54+
const [year, month, day] = date.split('/')
55+
!year || !month || !day && throwError()
56+
const realDate = `${year}${month.length == 2 ? month : '0' + month}${day.length == 2 ? day : '0' + day}`
57+
return { host, date: realDate, focus: parseInt(seconds) * MILL_PER_SECOND, time: 0 } satisfies timer.imported.Row
58+
})
59+
return [rows, false]
60+
} else if (isJsonFile(file)) {
61+
return [parseWattJsonFile(text), true]
62+
} else {
63+
throw new Error("Invalid file format")
64+
}
65+
}
66+
67+
type WattJsonItem = {
68+
url?: string
69+
favicon?: string
70+
summaryTime?: number
71+
counter?: number
72+
days?: {
73+
// new Date().toLocaleDateString("en-US") => "7/22/2023"
74+
// @see https://github.com/Stigmatoz/web-activity-time-tracker/blob/master/src/utils/date.ts#L6
75+
date?: string
76+
// seconds
77+
summary?: number
78+
// visit count
79+
counter?: number
80+
}[]
81+
}
82+
83+
const parseWattJsonFile = (fileContent: string) => {
84+
const rows: timer.imported.Row[] = []
85+
const data = JSON.parse(fileContent) as WattJsonItem[]
86+
data.forEach(({ url: host, days }) => {
87+
if (!host) throw new Error("Invalid item without url")
88+
if (!days) throw new Error("Invalid item without days")
89+
days.forEach(({ date, summary, counter: time }) => {
90+
if (!date) throw new Error("Invalid day without date")
91+
if (!summary && !time) throw new Error("Invalid day without summary and counter")
92+
const [month, day, year] = date.split('/')
93+
if (!year || !month || !day) throw new Error("Invalid date format: " + date)
94+
const dateObj = new Date(parseInt(year), parseInt(month) - 1, parseInt(day))
95+
const realDate = formatTimeYMD(dateObj)
96+
rows.push({
97+
host,
98+
date: realDate,
99+
focus: (summary ?? 0) * MILL_PER_SECOND,
100+
time: time ?? 0,
101+
})
102+
})
57103
})
58104
return rows
59105
}

src/pages/app/components/Option/components/BackupOption/Download/Step2.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const _default = defineComponent<Props>((props, ctx) => {
3434
})
3535
}
3636
</ElAlert>
37-
<CompareTable data={props.data} comparedColName={t(msg => msg.option.backup.download.willDownload)} />
37+
<CompareTable data={props.data} comparedCol={msg => msg.option.backup.download.willDownload} />
3838
<Flex justify="center">
3939
<ResolutionRadio modelValue={resolution.value} onChange={setResolution} />
4040
</Flex>

src/pages/app/components/common/imported/CompareTable.tsx

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

88
import HostAlert from "@app/components/common/HostAlert"
9-
import { t } from "@app/locale"
9+
import { type I18nKey, t } from "@app/locale"
1010
import { cvt2LocaleTime, periodFormatter } from "@app/util/time"
1111
import { useState } from "@hooks"
1212
import Box from "@pages/components/Box"
@@ -34,11 +34,11 @@ function computeList(sort: SortInfo, originRows: timer.imported.Row[]): timer.im
3434
return originRows.sort(comparator)
3535
}
3636

37-
const focusCol = (comparedColName: string): Column[] => [
37+
const focusCol = (comparedCol: I18nKey): Column[] => [
3838
{
3939
width: 150,
4040
align: 'center',
41-
title: comparedColName,
41+
title: t(comparedCol),
4242
cellRenderer: ({ rowData }) => <span>{periodFormatter((rowData as timer.imported.Row).focus)}</span>,
4343
}, {
4444
width: 150,
@@ -48,11 +48,11 @@ const focusCol = (comparedColName: string): Column[] => [
4848
}
4949
]
5050

51-
const timeCol = (comparedColName: string): Column[] => [
51+
const timeCol = (comparedCol: I18nKey): Column[] => [
5252
{
5353
width: 150,
5454
align: 'center',
55-
title: comparedColName,
55+
title: t(comparedCol),
5656
cellRenderer: ({ rowData }) => <span>{(rowData as timer.imported.Row).time ?? 0}</span>,
5757
}, {
5858
width: 150,
@@ -64,7 +64,7 @@ const timeCol = (comparedColName: string): Column[] => [
6464

6565
type Props = {
6666
data: timer.imported.Data
67-
comparedColName: string
67+
comparedCol: I18nKey
6868
}
6969

7070
const BASE_COLUMNS: Column[] = [
@@ -99,8 +99,8 @@ const _default = defineComponent<Props>((props) => {
9999
const columns = computed(() => {
100100
const value = [...BASE_COLUMNS]
101101
const { focus, time } = data.value
102-
focus && value.push(...focusCol(props.comparedColName))
103-
time && value.push(...timeCol(props.comparedColName))
102+
focus && value.push(...focusCol(props.comparedCol))
103+
time && value.push(...timeCol(props.comparedCol))
104104
return value
105105
})
106106

@@ -120,6 +120,6 @@ const _default = defineComponent<Props>((props) => {
120120
} />
121121
</Box>
122122
)
123-
}, { props: ['data', 'comparedColName'] })
123+
}, { props: ['data', 'comparedCol'] })
124124

125125
export default _default

0 commit comments

Comments
 (0)