create-ones-app 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +201 -0
  2. package/dist/index.cjs +7087 -22
  3. package/dist/index.js +7091 -26
  4. package/dist/types/actions/create/index.d.ts +3 -0
  5. package/dist/types/actions/create/index.d.ts.map +1 -0
  6. package/dist/types/actions/create/normalize.d.ts +6 -0
  7. package/dist/types/actions/create/normalize.d.ts.map +1 -0
  8. package/dist/types/actions/index.d.ts +5 -0
  9. package/dist/types/actions/index.d.ts.map +1 -0
  10. package/dist/types/cli/index.d.ts +2 -0
  11. package/dist/types/cli/index.d.ts.map +1 -0
  12. package/dist/types/command/create/index.d.ts +9 -0
  13. package/dist/types/command/create/index.d.ts.map +1 -0
  14. package/dist/types/command/index.d.ts +5 -0
  15. package/dist/types/command/index.d.ts.map +1 -0
  16. package/dist/types/common/command/index.d.ts +6 -0
  17. package/dist/types/common/command/index.d.ts.map +1 -0
  18. package/dist/types/common/command/types.d.ts +10 -0
  19. package/dist/types/common/command/types.d.ts.map +1 -0
  20. package/dist/types/common/command/utils.d.ts +8 -0
  21. package/dist/types/common/command/utils.d.ts.map +1 -0
  22. package/dist/types/common/context/index.d.ts +6 -0
  23. package/dist/types/common/context/index.d.ts.map +1 -0
  24. package/dist/types/common/context/types.d.ts +6 -0
  25. package/dist/types/common/context/types.d.ts.map +1 -0
  26. package/dist/types/common/context/utils.d.ts +4 -0
  27. package/dist/types/common/context/utils.d.ts.map +1 -0
  28. package/dist/types/common/error/enums.d.ts +7 -0
  29. package/dist/types/common/error/enums.d.ts.map +1 -0
  30. package/dist/types/common/error/index.d.ts +7 -0
  31. package/dist/types/common/error/index.d.ts.map +1 -0
  32. package/dist/types/common/error/types.d.ts +2 -0
  33. package/dist/types/common/error/types.d.ts.map +1 -0
  34. package/dist/types/common/error/utils.d.ts +2 -0
  35. package/dist/types/common/error/utils.d.ts.map +1 -0
  36. package/dist/types/common/locales/en/index.d.ts +10 -0
  37. package/dist/types/common/locales/en/index.d.ts.map +1 -0
  38. package/dist/types/common/locales/index.d.ts +6 -0
  39. package/dist/types/common/locales/index.d.ts.map +1 -0
  40. package/dist/types/common/locales/types.d.ts +4 -0
  41. package/dist/types/common/locales/types.d.ts.map +1 -0
  42. package/dist/types/common/locales/utils.d.ts +6 -0
  43. package/dist/types/common/locales/utils.d.ts.map +1 -0
  44. package/dist/types/common/package/consts.d.ts +6 -0
  45. package/dist/types/common/package/consts.d.ts.map +1 -0
  46. package/dist/types/common/package/index.d.ts +8 -0
  47. package/dist/types/common/package/index.d.ts.map +1 -0
  48. package/dist/types/common/package/schema.d.ts +345 -0
  49. package/dist/types/common/package/schema.d.ts.map +1 -0
  50. package/dist/types/common/package/types.d.ts.map +1 -0
  51. package/dist/types/common/package/utils.d.ts +5 -0
  52. package/dist/types/common/package/utils.d.ts.map +1 -0
  53. package/dist/types/common/public/consts.d.ts +5 -0
  54. package/dist/types/common/public/consts.d.ts.map +1 -0
  55. package/dist/types/common/public/index.d.ts +6 -0
  56. package/dist/types/common/public/index.d.ts.map +1 -0
  57. package/dist/types/common/public/utils.d.ts +2 -0
  58. package/dist/types/common/public/utils.d.ts.map +1 -0
  59. package/dist/types/common/template/index.d.ts +5 -0
  60. package/dist/types/common/template/index.d.ts.map +1 -0
  61. package/dist/types/common/template/utils.d.ts +4 -0
  62. package/dist/types/common/template/utils.d.ts.map +1 -0
  63. package/dist/types/index.d.ts +11 -1
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/package.json +26 -5
  66. package/public/.onesrc.json +9 -0
  67. package/public/app_opkx_schema.json +566 -0
  68. package/public/logos/logo-bulb.svg +20 -0
  69. package/public/logos/logo-check.svg +18 -0
  70. package/public/logos/logo-claude.svg +19 -0
  71. package/public/logos/logo-claw.svg +23 -0
  72. package/public/logos/logo-gear.svg +18 -0
  73. package/public/logos/logo-gemini.svg +19 -0
  74. package/public/logos/logo-haiper.svg +19 -0
  75. package/public/logos/logo-lightning.svg +19 -0
  76. package/public/logos/logo-manus.svg +21 -0
  77. package/public/logos/logo-new-api.svg +21 -0
  78. package/public/logos/logo-puzzle.svg +19 -0
  79. package/public/logos/logo-trae.svg +19 -0
  80. package/public/logos/logo-user.svg +20 -0
  81. package/public/opkx.json +21 -0
  82. package/template/example/.template.json +1 -1
  83. package/template/example/AGENTS.md +92 -103
  84. package/template/example/LICENSE +201 -0
  85. package/template/example/README.md +1 -84
  86. package/template/example/_eslint.config.mjs_ +39 -0
  87. package/template/example/_gitignore +23 -0
  88. package/template/example/_husky_pre-commit +1 -0
  89. package/template/example/_prettierignore +11 -0
  90. package/template/example/_prettierrc +6 -0
  91. package/template/example/_tsconfig.json +8 -0
  92. package/template/example/backend/app.controller.ts +85 -0
  93. package/template/example/backend/app.module.ts +34 -0
  94. package/template/example/backend/app.service.ts +46 -0
  95. package/template/example/backend/dto/issue-created-event.dto.ts +29 -0
  96. package/template/example/backend/dto/recent-work-item.dto.ts +11 -0
  97. package/template/example/backend/main.ts +10 -0
  98. package/template/example/backend/proxy.ts +46 -0
  99. package/template/example/backend/services/recent-work-items.service.ts +84 -0
  100. package/template/example/backend/utils.ts +41 -0
  101. package/template/example/nest-cli.json +8 -0
  102. package/template/example/opkx.json +106 -0
  103. package/template/example/package-lock.json +12699 -0
  104. package/template/example/package.json +84 -0
  105. package/template/example/postcss.config.js +10 -0
  106. package/template/example/public/assets/loading.png +0 -0
  107. package/template/example/public/favicon.ico +0 -0
  108. package/template/example/public/i18n/en.json +17 -0
  109. package/template/example/public/i18n/zh.json +17 -0
  110. package/template/example/public/normalize.css@8.0.1/normalize.css +349 -0
  111. package/template/example/tsconfig.backend.json +34 -0
  112. package/template/example/tsconfig.build.json +3 -0
  113. package/template/example/tsconfig.vite.json +34 -0
  114. package/template/example/tsconfig.web.json +39 -0
  115. package/template/example/vite.config.ts +286 -0
  116. package/template/example/web/pages/recent-work-items/index.css +131 -0
  117. package/template/example/web/pages/recent-work-items/index.tsx +265 -0
  118. package/template/example/web/template/index.html +65 -0
  119. package/dist/types/action.d.ts +0 -2
  120. package/dist/types/action.d.ts.map +0 -1
  121. package/dist/types/types.d.ts.map +0 -1
  122. package/dist/types/utils.d.ts +0 -7
  123. package/dist/types/utils.d.ts.map +0 -1
  124. package/template/example/netlify/functions/app_manifest.js +0 -90
  125. package/template/example/netlify/functions/app_setting_pages.js +0 -48
  126. package/template/example/netlify/functions/events.js +0 -35
  127. package/template/example/netlify/functions/install.js +0 -35
  128. package/template/example/netlify/functions/metadata.js +0 -37
  129. package/template/example/netlify.toml +0 -44
  130. package/template/example/public/index.html +0 -20
  131. package/template/example/public/logo.svg +0 -4
  132. package/template/example/public/script.js +0 -56
  133. package/template/example/public/style.css +0 -66
  134. package/template/example/types/app_manifest.d.ts +0 -115
  135. package/template/example/types/app_setting_pages.d.ts +0 -14
  136. /package/dist/types/{types.d.ts → common/package/types.d.ts} +0 -0
@@ -0,0 +1,286 @@
1
+ /* eslint-disable no-control-regex */
2
+ /* eslint-disable @typescript-eslint/no-floating-promises */
3
+ import { fileURLToPath } from 'node:url'
4
+ import { readFileSync, writeFileSync } from 'node:fs'
5
+ import { dirname, resolve, basename, extname, relative } from 'node:path'
6
+ import fse from 'fs-extra'
7
+ import { glob } from 'glob'
8
+ import { build, defineConfig, mergeConfig } from 'vite'
9
+ import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
10
+ import type { InlineConfig, UserConfig, ViteDevServer } from 'vite'
11
+
12
+ const { existsSync, readdirSync, remove, copy } = fse
13
+
14
+ const backendHostedPort = Number(process.env.ONES_HOSTED_PORT) || 8201
15
+ const webDevPort = Number(process.env.ONES_DEV_WEB_SERVER_PORT) || 8202
16
+ const NODE_ENV = process.env.NODE_ENV === 'development' ? 'development' : 'production'
17
+ const __dirname = dirname(fileURLToPath(import.meta.url))
18
+
19
+ const distDir = resolve(__dirname, 'dist')
20
+ const distWebDir = resolve(distDir, 'web')
21
+ const distPublicDir = resolve(distDir, 'public')
22
+
23
+ const webDir = resolve(__dirname, 'web')
24
+ const publicDir = resolve(__dirname, 'public')
25
+ const viteDir = resolve(__dirname, '.vite')
26
+
27
+ const sleep = (number: number) => {
28
+ return new Promise((resolve) => {
29
+ setTimeout(resolve, number)
30
+ })
31
+ }
32
+
33
+ const defaultConfig: InlineConfig = {
34
+ root: __dirname,
35
+ configFile: false,
36
+ build: {
37
+ lib: {
38
+ entry: '',
39
+ name: '',
40
+ fileName: '',
41
+ formats: ['iife'],
42
+ },
43
+ emptyOutDir: false,
44
+ copyPublicDir: false,
45
+ // Image assets under 128KB are inlined by default
46
+ // External assets must be placed in the public directory and referenced by URL
47
+ assetsInlineLimit: 128 * 1024,
48
+ },
49
+ resolve: {
50
+ alias: {
51
+ '@': webDir,
52
+ },
53
+ },
54
+ define: {
55
+ 'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
56
+ },
57
+ plugins: [cssInjectedByJsPlugin() as InlineConfig['plugins']],
58
+ }
59
+
60
+ const devConfig: UserConfig = {
61
+ build: {
62
+ outDir: viteDir,
63
+ minify: false,
64
+ sourcemap: true,
65
+ },
66
+ esbuild: {
67
+ keepNames: true,
68
+ },
69
+ }
70
+
71
+ const prodConfig: UserConfig = {
72
+ mode: 'production',
73
+ build: {
74
+ outDir: distWebDir,
75
+ },
76
+ }
77
+
78
+ const buildJS = async (filename: string) => {
79
+ const appConfig: UserConfig = {
80
+ build: {
81
+ lib: {
82
+ name: 'index',
83
+ entry: resolve(webDir, `pages/${filename}/index.tsx`),
84
+ fileName: () => `pages/${filename}.js`,
85
+ },
86
+ },
87
+ }
88
+ const envConfig = NODE_ENV === 'development' ? devConfig : prodConfig
89
+ return await build(mergeConfig(defaultConfig, mergeConfig(envConfig, appConfig)))
90
+ }
91
+
92
+ const htmlTemplate = readFileSync(resolve(__dirname, 'web/template/index.html'), {
93
+ encoding: 'utf-8',
94
+ })
95
+
96
+ const createdHTML = (...scripts: string[]) => {
97
+ return htmlTemplate.replace('<!-- SCRIPT -->', scripts.join('\n'))
98
+ }
99
+
100
+ const messagePlugin = () => {
101
+ return {
102
+ name: 'message-plugin',
103
+ configureServer(server: ViteDevServer) {
104
+ server.printUrls = () => {
105
+ const base = `http://localhost:${backendHostedPort}`
106
+ sleep(2000).then(() => {
107
+ glob(resolve(webDir, 'pages/**/index.tsx')).then((entryFiles) => {
108
+ for (const entryFile of entryFiles) {
109
+ const entryName = basename(dirname(entryFile))
110
+ console.log(`Entry "${entryName}" URL: ${base}/web/pages/${entryName}.html`)
111
+ }
112
+ })
113
+ })
114
+ }
115
+ },
116
+ }
117
+ }
118
+
119
+ const devPlugin = () => {
120
+ const codeMap: Record<string, string> = {}
121
+ const visitMap: Record<string, boolean> = {}
122
+ const counterMap: Record<string, number> = {}
123
+ const timerMap: Record<string, ReturnType<typeof setTimeout>> = {}
124
+ const htmlType = 'text/html; charset=utf-8'
125
+ const plainType = 'text/plain; charset=utf-8'
126
+ const jsType = 'application/javascript; charset=utf-8'
127
+ const jsonType = 'application/json; charset=utf-8'
128
+ const debounceTime = 300
129
+ return {
130
+ name: 'dev-plugin',
131
+ configureServer(server: ViteDevServer) {
132
+ server.watcher.on('add', (path) => {
133
+ if (!path.startsWith(webDir)) return
134
+ const rel = relative(webDir, path).replace(/\\/g, '/')
135
+ const match = rel.match(/^pages\/([^/]+)\/index\.tsx$/)
136
+ if (match) {
137
+ const entryName = match[1]
138
+ const base = `http://localhost:${backendHostedPort}`
139
+ console.log(
140
+ `Entry "${entryName}" created successfully: ${base}/web/pages/${entryName}.html`,
141
+ )
142
+ }
143
+ })
144
+ server.watcher.on('change', (path) => {
145
+ if (!path.startsWith(webDir)) return
146
+ const rel = relative(webDir, path).replace(/\\/g, '/')
147
+ const match = rel.match(/^pages\/([^/]+)\//)
148
+ if (match) {
149
+ const entryName = match[1]
150
+ const hasVisit = visitMap[entryName]
151
+ if (hasVisit) {
152
+ if (timerMap[entryName]) clearTimeout(timerMap[entryName])
153
+ timerMap[entryName] = setTimeout(() => {
154
+ const invoke = async () => {
155
+ try {
156
+ codeMap[entryName] = ''
157
+ console.log(`Entry "${entryName}" recompiling...`)
158
+ await buildJS(entryName)
159
+ console.log(`Entry "${entryName}" compiled successfully`)
160
+ const js = readFileSync(resolve(viteDir, `pages/${entryName}.js`), {
161
+ encoding: 'utf-8',
162
+ })
163
+ codeMap[entryName] = js
164
+ counterMap[entryName] = counterMap[entryName] || 0
165
+ counterMap[entryName] += 1
166
+ } catch (err) {
167
+ console.log(`Entry "${entryName}" compiled failed`)
168
+ const error = err as Error
169
+ console.log(error)
170
+ }
171
+ }
172
+ invoke()
173
+ }, debounceTime)
174
+ }
175
+ }
176
+ })
177
+ server.middlewares.use((req, res) => {
178
+ const invoke = async () => {
179
+ const pathString = req.url?.split('?')[0] ?? ''
180
+ const trimString = pathString.replace(/^\//, '')
181
+ const firstString = trimString.split('/')[0] ?? ''
182
+ const ext = extname(firstString)
183
+ if (ext === '.html' || ext === '.js') {
184
+ const filename = firstString.replace(ext, '')
185
+ const entry = resolve(webDir, `pages/${filename}/index.tsx`)
186
+ const exists = existsSync(entry)
187
+ if (exists) {
188
+ visitMap[filename] = true
189
+ if (ext === '.html') {
190
+ res.statusCode = 200
191
+ res.setHeader('Content-Type', htmlType)
192
+ res.end(createdHTML(`<script src="./${filename}.js"></script>`))
193
+ return
194
+ }
195
+ if (codeMap[filename]) {
196
+ console.log(`Entry "${filename}" using cached code`)
197
+ res.setHeader('Content-Type', jsType)
198
+ res.statusCode = 200
199
+ res.end(codeMap[filename])
200
+ return
201
+ }
202
+ try {
203
+ console.log(`Entry "${filename}" compiling...`)
204
+ await buildJS(filename)
205
+ console.log(`Entry "${filename}" compiled successfully`)
206
+ const js = readFileSync(resolve(viteDir, `pages/${filename}.js`), {
207
+ encoding: 'utf-8',
208
+ })
209
+ codeMap[filename] = js
210
+ counterMap[filename] = counterMap[filename] || 0
211
+ counterMap[filename] += 1
212
+ res.setHeader('Content-Type', jsType)
213
+ res.statusCode = 200
214
+ res.end(js)
215
+ } catch (err) {
216
+ console.log(`Entry "${filename}" compiled failed`)
217
+ const error = err as Error
218
+ console.log(error)
219
+ res.statusCode = 200
220
+ res.setHeader('Content-Type', jsType)
221
+ const string = error.stack?.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '') ?? ''
222
+ const message = '`' + string + '`'
223
+ res.end(`console.error(${message})`)
224
+ }
225
+ return
226
+ }
227
+ res.statusCode = 500
228
+ res.setHeader('Content-Type', plainType)
229
+ res.end(`Entry not found, ${entry}`)
230
+ return
231
+ }
232
+ if (/\.js\.map$/.test(firstString)) {
233
+ const filename = firstString
234
+ try {
235
+ const json = readFileSync(resolve(viteDir, `pages/${filename}`), {
236
+ encoding: 'utf-8',
237
+ })
238
+ res.statusCode = 200
239
+ res.setHeader('Content-Type', jsonType)
240
+ res.end(json)
241
+ return
242
+ } catch (error) {
243
+ String(error)
244
+ }
245
+ }
246
+ res.statusCode = 500
247
+ res.setHeader('Content-Type', plainType)
248
+ res.end('File not found')
249
+ }
250
+ invoke()
251
+ })
252
+ },
253
+ }
254
+ }
255
+
256
+ export default defineConfig(async ({ command }) => {
257
+ const isDev = command === 'serve'
258
+ if (isDev) {
259
+ return {
260
+ root: __dirname,
261
+ server: {
262
+ port: webDevPort,
263
+ },
264
+ plugins: [messagePlugin(), devPlugin()],
265
+ }
266
+ }
267
+ const sources = readdirSync(resolve(distDir))
268
+ for (const source of sources) {
269
+ if (source === 'backend') continue
270
+ await remove(resolve(distDir, source))
271
+ }
272
+ await copy(publicDir, distPublicDir)
273
+ const pages = await glob(resolve(webDir, 'pages/**/index.tsx'))
274
+ for (const page of pages) {
275
+ const filename = basename(dirname(page))
276
+ await buildJS(filename)
277
+ writeFileSync(
278
+ resolve(distWebDir, `pages/${filename}.html`),
279
+ createdHTML(`<script src="./${filename}.js"></script>`),
280
+ {
281
+ encoding: 'utf-8',
282
+ },
283
+ )
284
+ }
285
+ process.exit(0)
286
+ })
@@ -0,0 +1,131 @@
1
+ html,
2
+ body,
3
+ #root {
4
+ width: 100%;
5
+ height: 100%;
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ }
10
+ body {
11
+ font-family: "Avenir Next", "Segoe UI", Arial, sans-serif;
12
+ margin: 0;
13
+ background: #f5f8ff;
14
+ position: absolute;
15
+ top: 0;
16
+ left: 0;
17
+ }
18
+ .page {
19
+ background: linear-gradient(175deg, #2a63f6 0%, #77a2ff 55%, #f5f8ff 100%);
20
+ display: flex;
21
+ align-items: flex-start;
22
+ justify-content: center;
23
+ padding: 12px 14px 16px;
24
+ box-sizing: border-box;
25
+ min-height: 100%;
26
+ max-height: 100%;
27
+ overflow: scroll;
28
+ width: 100%;
29
+ }
30
+ .card {
31
+ background: rgba(255, 255, 255, 0.96);
32
+ border: 1px solid rgba(18, 34, 102, 0.15);
33
+ border-radius: 16px;
34
+ padding: 24px;
35
+ width: min(760px, 100%);
36
+ box-shadow: 0 14px 34px rgba(12, 26, 84, 0.28);
37
+ }
38
+ .greeting {
39
+ margin: 0;
40
+ font-size: 30px;
41
+ line-height: 1.2;
42
+ font-weight: 700;
43
+ color: #0a1f56;
44
+ }
45
+ .subtitle {
46
+ margin: 8px 0 18px;
47
+ color: #1d3a8a;
48
+ font-size: 14px;
49
+ }
50
+ .userInfo {
51
+ margin: 8px 0 0;
52
+ color: #1e3a8a;
53
+ font-size: 13px;
54
+ font-weight: 600;
55
+ }
56
+ .teamInfo {
57
+ margin: 8px 0 0;
58
+ color: #1e3a8a;
59
+ font-size: 13px;
60
+ font-weight: 600;
61
+ }
62
+ .row {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 10px;
66
+ margin-bottom: 14px;
67
+ flex-wrap: wrap;
68
+ }
69
+ label {
70
+ color: #1f2937;
71
+ font-size: 13px;
72
+ font-weight: 600;
73
+ }
74
+ select {
75
+ border: 1px solid #b6c6f8;
76
+ border-radius: 8px;
77
+ padding: 6px 10px;
78
+ color: #0f255f;
79
+ background: #ffffff;
80
+ }
81
+ .filterTag {
82
+ border: 1px solid #c2d2ff;
83
+ border-radius: 999px;
84
+ padding: 5px 10px;
85
+ font-size: 12px;
86
+ color: #173b92;
87
+ background: #eef3ff;
88
+ }
89
+ .tableWrap {
90
+ border: 1px solid #d4dfff;
91
+ border-radius: 10px;
92
+ overflow: hidden;
93
+ background: #ffffff;
94
+ }
95
+ .itemsTable {
96
+ width: 100%;
97
+ border-collapse: collapse;
98
+ table-layout: fixed;
99
+ }
100
+ .itemsTable th {
101
+ text-align: left;
102
+ font-size: 12px;
103
+ color: #26438d;
104
+ font-weight: 700;
105
+ padding: 8px 10px;
106
+ background: #edf3ff;
107
+ border-bottom: 1px solid #dbe5ff;
108
+ }
109
+ .itemsTable td {
110
+ padding: 8px 10px;
111
+ font-size: 12px;
112
+ color: #233257;
113
+ border-bottom: 1px solid #edf2ff;
114
+ white-space: nowrap;
115
+ overflow: hidden;
116
+ text-overflow: ellipsis;
117
+ }
118
+ .itemsTable tbody tr:last-child td {
119
+ border-bottom: none;
120
+ }
121
+ .itemsTable .nameCell {
122
+ font-weight: 600;
123
+ }
124
+ .itemsTable .idCell {
125
+ color: #3a4a70;
126
+ }
127
+ .empty {
128
+ margin-top: 10px;
129
+ color: #475569;
130
+ font-style: italic;
131
+ }
@@ -0,0 +1,265 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import ReactDOM from 'react-dom'
3
+ import { ONES } from '@ones-open/web-sdk'
4
+ import './index.css'
5
+
6
+ type RecentLimit = 5 | 10 | 30
7
+ type Locale = 'zh' | 'en'
8
+ type I18nText = {
9
+ greeting: string
10
+ subtitle: string
11
+ teamLabel: string
12
+ limitLabel: string
13
+ filterLabel: string
14
+ nameLabel: string
15
+ createTimeLabel: string
16
+ loading: string
17
+ empty: string
18
+ error: string
19
+ unknownTeam: string
20
+ idLabel: string
21
+ untitled: string
22
+ userLabel: string
23
+ unknownUser: string
24
+ }
25
+
26
+ interface RecentWorkItem {
27
+ workItemId: string
28
+ title: string
29
+ createdAt: number
30
+ }
31
+
32
+ interface RecentWorkItemsResponse {
33
+ ok?: boolean
34
+ items?: RecentWorkItem[]
35
+ }
36
+
37
+ interface TeamDetail {
38
+ id?: string
39
+ name?: string
40
+ }
41
+
42
+ interface TeamListResponse {
43
+ result?: string
44
+ data?: {
45
+ teams?: TeamDetail[]
46
+ }
47
+ }
48
+
49
+ const LIMIT_OPTIONS: RecentLimit[] = [5, 10, 30]
50
+
51
+ const normalizeTimestampMs = (timestamp: number): number => {
52
+ if (!Number.isFinite(timestamp)) return Number.NaN
53
+
54
+ const abs = Math.abs(timestamp)
55
+ if (abs < 1e11) return timestamp * 1000
56
+ if (abs < 1e14) return timestamp
57
+ if (abs < 1e17) return Math.floor(timestamp / 1000)
58
+ return Math.floor(timestamp / 1_000_000)
59
+ }
60
+
61
+ const formatCreatedAt = (timestamp: number) => {
62
+ const date = new Date(normalizeTimestampMs(timestamp))
63
+ if (Number.isNaN(date.getTime())) return '-'
64
+ return date.toLocaleString()
65
+ }
66
+
67
+ const isResponseLike = (value: unknown): value is { json: () => Promise<unknown> } =>
68
+ typeof value === 'object' && value !== null && 'json' in value && typeof value.json === 'function'
69
+
70
+ const parseResponse = async <T,>(value: unknown): Promise<T> => {
71
+ if (isResponseLike(value)) {
72
+ return ((await value.json()) as T) ?? ({} as T)
73
+ }
74
+
75
+ return value as T
76
+ }
77
+
78
+ const normalizeLocale = (locale: string): Locale => {
79
+ const normalized = locale.toLowerCase()
80
+ if (normalized.startsWith('zh') || normalized.includes('chinese')) {
81
+ return 'zh'
82
+ }
83
+ return 'en'
84
+ }
85
+
86
+ const fetchLocale = async (): Promise<Locale> => {
87
+ try {
88
+ return normalizeLocale(await ONES.getLocale())
89
+ } catch {
90
+ return 'en'
91
+ }
92
+ }
93
+
94
+ const fetchI18n = async (locale: Locale): Promise<Partial<I18nText>> => {
95
+ const fetchLocaleData = async (nextLocale: Locale): Promise<Partial<I18nText>> => {
96
+ const urlCandidates = [`/public/i18n/${nextLocale}.json`]
97
+
98
+ try {
99
+ for (const url of urlCandidates) {
100
+ const response = await ONES.fetchApp(url)
101
+ if (response.ok) {
102
+ return (await response.json()) as Partial<I18nText>
103
+ }
104
+ }
105
+ } catch {
106
+ console.error(`recent-work-items: error fetching i18n for locale: ${nextLocale}`)
107
+ }
108
+
109
+ return {}
110
+ }
111
+
112
+ const [en, zh] = await Promise.all([fetchLocaleData('en'), fetchLocaleData('zh')])
113
+ const preferred = locale === 'zh' ? zh : en
114
+ return { ...en, ...preferred }
115
+ }
116
+
117
+ const formatI18n = (template: string, payload: Record<string, string | number>): string =>
118
+ template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}|\{([a-zA-Z0-9_]+)\}/g, (_, a, b) => {
119
+ const key = String(a || b)
120
+ const value = payload[key]
121
+ return value === undefined ? '' : String(value)
122
+ })
123
+
124
+ const fetchTeamName = async (): Promise<string> => {
125
+ try {
126
+ const teamInfo = await ONES.getTeamInfo()
127
+ const currentTeamId = teamInfo.teamUUID || ''
128
+ if (!currentTeamId) return ''
129
+
130
+ const payload = await parseResponse<TeamListResponse>(
131
+ await ONES.fetchOpenAPI('/v2/account/teams'),
132
+ )
133
+ const teams = payload.data?.teams || []
134
+ const matched = teams.find((team) => team.id === currentTeamId)
135
+ return `${matched?.name}(${matched?.id || currentTeamId})`
136
+ } catch {
137
+ return ''
138
+ }
139
+ }
140
+
141
+ const App = () => {
142
+ useEffect(() => {
143
+ const loading = document.querySelector('.ones-app-loading')
144
+ loading?.remove()
145
+ }, [])
146
+
147
+ const [limit, setLimit] = useState<RecentLimit>(5)
148
+ const [items, setItems] = useState<RecentWorkItem[]>([])
149
+ const [text, setText] = useState<Partial<I18nText>>({})
150
+ const [teamName, setTeamName] = useState<string>('')
151
+ const [userName, setUserName] = useState<string>('')
152
+ const [loading, setLoading] = useState<boolean>(false)
153
+ const [hasError, setHasError] = useState<boolean>(false)
154
+ const t = (key: keyof I18nText) => text[key] ?? ''
155
+
156
+ const fetchItems = useCallback(async (nextLimit: RecentLimit) => {
157
+ setLoading(true)
158
+ setHasError(false)
159
+
160
+ const url = `/api/recent-work-items?limit=${nextLimit}`
161
+
162
+ try {
163
+ const payload = await parseResponse<RecentWorkItemsResponse>(await ONES.fetchApp(url))
164
+ setItems(Array.isArray(payload?.items) ? payload.items : [])
165
+ } catch {
166
+ setItems([])
167
+ setHasError(true)
168
+ } finally {
169
+ setLoading(false)
170
+ }
171
+ }, [])
172
+
173
+ useEffect(() => {
174
+ void fetchItems(limit)
175
+ }, [limit, fetchItems])
176
+
177
+ useEffect(() => {
178
+ ONES.getUserInfo()
179
+ .then((userInfo) => {
180
+ setUserName(`${userInfo.name}(${userInfo.uuid})`)
181
+ })
182
+ .catch((error) => {
183
+ console.error('ONES.getUserInfo', error)
184
+ })
185
+ }, [])
186
+
187
+ useEffect(() => {
188
+ let active = true
189
+ void (async () => {
190
+ const [nextLocale, nextTeamName] = await Promise.all([fetchLocale(), fetchTeamName()])
191
+ const nextI18n = await fetchI18n(nextLocale)
192
+ if (active) {
193
+ setTeamName(nextTeamName)
194
+ setText(nextI18n)
195
+ }
196
+ })()
197
+ return () => {
198
+ active = false
199
+ }
200
+ }, [])
201
+
202
+ const displayTeamName = teamName || t('unknownTeam')
203
+ const displayUserName = userName || t('unknownUser')
204
+
205
+ return (
206
+ <div className="page">
207
+ <div className="card">
208
+ <p className="greeting">{t('greeting')}</p>
209
+ <p className="userInfo">
210
+ {t('userLabel')}: {displayUserName}
211
+ </p>
212
+ <p className="teamInfo">
213
+ {t('teamLabel')}: {displayTeamName}
214
+ </p>
215
+ <p className="subtitle">{t('subtitle')}</p>
216
+ <div className="row">
217
+ <label htmlFor="limit">{t('limitLabel')}</label>
218
+ <select
219
+ id="limit"
220
+ value={limit}
221
+ onChange={(event) => setLimit(Number(event.target.value) as RecentLimit)}
222
+ >
223
+ {LIMIT_OPTIONS.map((option) => (
224
+ <option key={option} value={option}>
225
+ {option}
226
+ </option>
227
+ ))}
228
+ </select>
229
+ <span className="filterTag">{formatI18n(t('filterLabel'), { limit })}</span>
230
+ </div>
231
+
232
+ {loading ? <div className="empty">{t('loading')}</div> : null}
233
+
234
+ <div className="tableWrap">
235
+ <table className="itemsTable">
236
+ <thead>
237
+ <tr>
238
+ <th>{t('idLabel')}</th>
239
+ <th>{t('nameLabel')}</th>
240
+ <th>{t('createTimeLabel')}</th>
241
+ </tr>
242
+ </thead>
243
+ <tbody>
244
+ {items.map((item) => (
245
+ <tr key={`${item.workItemId}-${item.createdAt}`}>
246
+ <td className="idCell">{item.workItemId}</td>
247
+ <td className="nameCell">{item.title || t('untitled')}</td>
248
+ <td>{formatCreatedAt(item.createdAt)}</td>
249
+ </tr>
250
+ ))}
251
+ </tbody>
252
+ </table>
253
+ </div>
254
+
255
+ {!loading && hasError ? <div className="empty">{t('error')}</div> : null}
256
+ {!loading && !hasError && items.length === 0 ? (
257
+ <div className="empty">{t('empty')}</div>
258
+ ) : null}
259
+ </div>
260
+ </div>
261
+ )
262
+ }
263
+
264
+ console.log('render App component')
265
+ ReactDOM.render(<App />, document.getElementById('root'))