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.
- package/LICENSE +201 -0
- package/dist/index.cjs +7087 -22
- package/dist/index.js +7091 -26
- package/dist/types/actions/create/index.d.ts +3 -0
- package/dist/types/actions/create/index.d.ts.map +1 -0
- package/dist/types/actions/create/normalize.d.ts +6 -0
- package/dist/types/actions/create/normalize.d.ts.map +1 -0
- package/dist/types/actions/index.d.ts +5 -0
- package/dist/types/actions/index.d.ts.map +1 -0
- package/dist/types/cli/index.d.ts +2 -0
- package/dist/types/cli/index.d.ts.map +1 -0
- package/dist/types/command/create/index.d.ts +9 -0
- package/dist/types/command/create/index.d.ts.map +1 -0
- package/dist/types/command/index.d.ts +5 -0
- package/dist/types/command/index.d.ts.map +1 -0
- package/dist/types/common/command/index.d.ts +6 -0
- package/dist/types/common/command/index.d.ts.map +1 -0
- package/dist/types/common/command/types.d.ts +10 -0
- package/dist/types/common/command/types.d.ts.map +1 -0
- package/dist/types/common/command/utils.d.ts +8 -0
- package/dist/types/common/command/utils.d.ts.map +1 -0
- package/dist/types/common/context/index.d.ts +6 -0
- package/dist/types/common/context/index.d.ts.map +1 -0
- package/dist/types/common/context/types.d.ts +6 -0
- package/dist/types/common/context/types.d.ts.map +1 -0
- package/dist/types/common/context/utils.d.ts +4 -0
- package/dist/types/common/context/utils.d.ts.map +1 -0
- package/dist/types/common/error/enums.d.ts +7 -0
- package/dist/types/common/error/enums.d.ts.map +1 -0
- package/dist/types/common/error/index.d.ts +7 -0
- package/dist/types/common/error/index.d.ts.map +1 -0
- package/dist/types/common/error/types.d.ts +2 -0
- package/dist/types/common/error/types.d.ts.map +1 -0
- package/dist/types/common/error/utils.d.ts +2 -0
- package/dist/types/common/error/utils.d.ts.map +1 -0
- package/dist/types/common/locales/en/index.d.ts +10 -0
- package/dist/types/common/locales/en/index.d.ts.map +1 -0
- package/dist/types/common/locales/index.d.ts +6 -0
- package/dist/types/common/locales/index.d.ts.map +1 -0
- package/dist/types/common/locales/types.d.ts +4 -0
- package/dist/types/common/locales/types.d.ts.map +1 -0
- package/dist/types/common/locales/utils.d.ts +6 -0
- package/dist/types/common/locales/utils.d.ts.map +1 -0
- package/dist/types/common/package/consts.d.ts +6 -0
- package/dist/types/common/package/consts.d.ts.map +1 -0
- package/dist/types/common/package/index.d.ts +8 -0
- package/dist/types/common/package/index.d.ts.map +1 -0
- package/dist/types/common/package/schema.d.ts +345 -0
- package/dist/types/common/package/schema.d.ts.map +1 -0
- package/dist/types/common/package/types.d.ts.map +1 -0
- package/dist/types/common/package/utils.d.ts +5 -0
- package/dist/types/common/package/utils.d.ts.map +1 -0
- package/dist/types/common/public/consts.d.ts +5 -0
- package/dist/types/common/public/consts.d.ts.map +1 -0
- package/dist/types/common/public/index.d.ts +6 -0
- package/dist/types/common/public/index.d.ts.map +1 -0
- package/dist/types/common/public/utils.d.ts +2 -0
- package/dist/types/common/public/utils.d.ts.map +1 -0
- package/dist/types/common/template/index.d.ts +5 -0
- package/dist/types/common/template/index.d.ts.map +1 -0
- package/dist/types/common/template/utils.d.ts +4 -0
- package/dist/types/common/template/utils.d.ts.map +1 -0
- package/dist/types/index.d.ts +11 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +26 -5
- package/public/.onesrc.json +9 -0
- package/public/app_opkx_schema.json +566 -0
- package/public/logos/logo-bulb.svg +20 -0
- package/public/logos/logo-check.svg +18 -0
- package/public/logos/logo-claude.svg +19 -0
- package/public/logos/logo-claw.svg +23 -0
- package/public/logos/logo-gear.svg +18 -0
- package/public/logos/logo-gemini.svg +19 -0
- package/public/logos/logo-haiper.svg +19 -0
- package/public/logos/logo-lightning.svg +19 -0
- package/public/logos/logo-manus.svg +21 -0
- package/public/logos/logo-new-api.svg +21 -0
- package/public/logos/logo-puzzle.svg +19 -0
- package/public/logos/logo-trae.svg +19 -0
- package/public/logos/logo-user.svg +20 -0
- package/public/opkx.json +21 -0
- package/template/example/.template.json +1 -1
- package/template/example/AGENTS.md +92 -103
- package/template/example/LICENSE +201 -0
- package/template/example/README.md +1 -84
- package/template/example/_eslint.config.mjs_ +39 -0
- package/template/example/_gitignore +23 -0
- package/template/example/_husky_pre-commit +1 -0
- package/template/example/_prettierignore +11 -0
- package/template/example/_prettierrc +6 -0
- package/template/example/_tsconfig.json +8 -0
- package/template/example/backend/app.controller.ts +85 -0
- package/template/example/backend/app.module.ts +34 -0
- package/template/example/backend/app.service.ts +46 -0
- package/template/example/backend/dto/issue-created-event.dto.ts +29 -0
- package/template/example/backend/dto/recent-work-item.dto.ts +11 -0
- package/template/example/backend/main.ts +10 -0
- package/template/example/backend/proxy.ts +46 -0
- package/template/example/backend/services/recent-work-items.service.ts +84 -0
- package/template/example/backend/utils.ts +41 -0
- package/template/example/nest-cli.json +8 -0
- package/template/example/opkx.json +106 -0
- package/template/example/package-lock.json +12699 -0
- package/template/example/package.json +84 -0
- package/template/example/postcss.config.js +10 -0
- package/template/example/public/assets/loading.png +0 -0
- package/template/example/public/favicon.ico +0 -0
- package/template/example/public/i18n/en.json +17 -0
- package/template/example/public/i18n/zh.json +17 -0
- package/template/example/public/normalize.css@8.0.1/normalize.css +349 -0
- package/template/example/tsconfig.backend.json +34 -0
- package/template/example/tsconfig.build.json +3 -0
- package/template/example/tsconfig.vite.json +34 -0
- package/template/example/tsconfig.web.json +39 -0
- package/template/example/vite.config.ts +286 -0
- package/template/example/web/pages/recent-work-items/index.css +131 -0
- package/template/example/web/pages/recent-work-items/index.tsx +265 -0
- package/template/example/web/template/index.html +65 -0
- package/dist/types/action.d.ts +0 -2
- package/dist/types/action.d.ts.map +0 -1
- package/dist/types/types.d.ts.map +0 -1
- package/dist/types/utils.d.ts +0 -7
- package/dist/types/utils.d.ts.map +0 -1
- package/template/example/netlify/functions/app_manifest.js +0 -90
- package/template/example/netlify/functions/app_setting_pages.js +0 -48
- package/template/example/netlify/functions/events.js +0 -35
- package/template/example/netlify/functions/install.js +0 -35
- package/template/example/netlify/functions/metadata.js +0 -37
- package/template/example/netlify.toml +0 -44
- package/template/example/public/index.html +0 -20
- package/template/example/public/logo.svg +0 -4
- package/template/example/public/script.js +0 -56
- package/template/example/public/style.css +0 -66
- package/template/example/types/app_manifest.d.ts +0 -115
- package/template/example/types/app_setting_pages.d.ts +0 -14
- /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'))
|