Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

Backlog APIを叩いて、指定したプロジェクトのデータをバックアップします。
バックアップしたデータは簡易ビューアで閲覧できます。
静的ファイル配信に対応、FTPアップロードするだけで動作可能。

![](https://i.imgur.com/CWX1wbL.png)
![](https://i.imgur.com/ylTYYPW.png)
Expand All @@ -21,6 +22,10 @@ Backlog APIを叩いて、指定したプロジェクトのデータをバック
- `npm run build`
- `dist` ディレクトリに、バックアップも含めてアセット一式が保存されます

## バックアップのクリーニング
- `npm run clean`


## ライセンス

MIT License
Binary file added bun.lockb
Binary file not shown.
8,721 changes: 2,844 additions & 5,877 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 5 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"clean": "rimraf scripts/backlog/dist dist",
"backup": "run-s backup:issue backup:textsearch",
"backup:issue": "tsx scripts/backlog/index.mts",
"backup:textsearch": "tsx scripts/textsearch/index.mts",
"build": "vite build"
"build": "vite build",
"viewer": "vite preview"
},
"author": "COMMON CREATION, Co., Ltd.",
"license": "MIT",
"devDependencies": {
"@types/flexsearch": "^0.7.3",
"@types/isomorphic-fetch": "^0.0.36",
"@types/isomorphic-form-data": "^2.0.1",
"@types/node": "^20.3.2",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@types/react-syntax-highlighter": "^15.5.7",
"dotenv": "^16.3.1",
"npm-run-all": "^4.1.5",
"rimraf": "^6.0.1",
"rome": "^12.1.3",
"tsx": "^3.12.7",
"typescript": "^5.1.5",
Expand All @@ -38,8 +39,6 @@
"backlog-js": "^0.13.0",
"dayjs": "^1.11.8",
"flexsearch": "0.7.21",
"isomorphic-fetch": "^3.0.0",
"isomorphic-form-data": "^2.0.0",
"kuromojin": "^3.0.0",
"mobx": "^6.9.0",
"mobx-react-lite": "^3.4.3",
Expand All @@ -50,11 +49,10 @@
"react-syntax-highlighter": "^15.5.0",
"remark": "^14.0.3",
"remark-gfm": "^3.0.1",
"remark-markdown": "^0.0.1",
"remark-markdown": "^0.0.0",
"sanitize.css": "^13.0.0",
"strip-markdown": "^5.0.1",
"typestyle": "^2.4.0",
"vite-plugin-node-stdlib-browser": "^0.2.1",
"vite-tsconfig-paths": "^4.2.0"
},
"private": true
Expand Down
4 changes: 1 addition & 3 deletions scripts/backlog/index.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import "isomorphic-form-data";
import "isomorphic-fetch";
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
import { mkdir, rm, writeFile } from "fs/promises";
Expand All @@ -20,7 +18,7 @@ try {
force: true,
recursive: true,
});
} catch (e) {}
} catch (e) { }
await mkdir(distConfigs, {
recursive: true,
});
Expand Down
12 changes: 6 additions & 6 deletions scripts/textsearch/index.mts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
import { writeFile } from "fs/promises";
import { writeFile, readFile } from "fs/promises";
import { config } from "dotenv";
import { remark } from "remark";
import strip from "strip-markdown";
import { tokenize } from "kuromojin";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const dist = resolve(__dirname, "../backlog", "dist", "assets");
const dist = resolve(__dirname, "..", "backlog", "dist", "assets");
const distConfigs = resolve(dist, "configs");
const distIssuePages = resolve(dist, "pages");
const distIssues = resolve(dist, "issues");
Expand All @@ -22,14 +22,14 @@ const stripMarkdown = async (str: string) => (await remarkCache.process(str)).va

const documents = [];

const { start, end } = await import(resolve(distConfigs, "pages.json"));
const { start, end } = JSON.parse(await readFile(resolve(distConfigs, "pages.json"), 'utf-8'));
for (let i = start; i <= end; i++) {
console.log("search index:", i + 1, "/", end + 1);

const { default: page } = await import(resolve(distIssuePages, `${i}.json`));
const page = JSON.parse(await readFile(resolve(distIssuePages, `${i}.json`), 'utf-8'));
for (const id of page.map((p) => p.id)) {
const { default: issue } = await import(resolve(distIssues, `${id}`, "issue.json"));
const { default: comments } = await import(resolve(distIssues, `${id}`, "comments.json"));
const issue = JSON.parse(await readFile(resolve(distIssues, `${id}`, "issue.json"), 'utf-8'));
const comments = JSON.parse(await readFile(resolve(distIssues, `${id}`, "comments.json"), 'utf-8'));

const target = [issue.summary, issue.description, ...comments.map((c) => c.content)].join("\n\n");
const plain = await stripMarkdown(target);
Expand Down
26 changes: 13 additions & 13 deletions viewer/components/notificationUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ export const NotificationUser: React.FC<Props> = (props: Props) => {
return (
<Box ml={1}>
<Tooltip title={props.user?.name} placement="top">
<Badge
overlap="circular"
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
invisible={!props.alreadyRead}
color="success"
variant="dot"
>
<Avatar
alt={props.user?.name}
src={`/assets/users/${props.user?.id}/icon`}
sx={{ width: 24, height: 24, fontSize: 12, opacity: props.alreadyRead ? 1 : 0.6 }}
/>
</Badge>
<Badge
overlap="circular"
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
invisible={!props.alreadyRead}
color="success"
variant="dot"
>
<Avatar
alt={props.user?.name}
src={`./users/${props.user?.id}/icon`}
sx={{ width: 24, height: 24, fontSize: 12, opacity: props.alreadyRead ? 1 : 0.6 }}
/>
</Badge>
</Tooltip>
</Box>
);
Expand Down
2 changes: 1 addition & 1 deletion viewer/components/userHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const UserHeader: React.FC<Props> = (props: Props) => {
return (
<Box display={"flex"} alignItems={"center"}>
<Box>
<Avatar alt={props.user?.name} src={`/assets/users/${props.user?.id}/icon`} />
<Avatar alt={props.user?.name} src={`./users/${props.user?.id}/icon`} />
</Box>
<Box ml={1.5}>
<Box>
Expand Down
18 changes: 9 additions & 9 deletions viewer/containers/issue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const notificationType = (type: string) => {
export const Issue: React.FC = observer((props) => {
const { pageStore, issueStore } = useStore();
const { id: issueId } = useParams();
const [ showMoreinfo, setShowMoreInfo ] = useState(false);
const [showMoreinfo, setShowMoreInfo] = useState(false);

useDidMount(() => {
pageStore.fetch();
Expand All @@ -52,7 +52,7 @@ export const Issue: React.FC = observer((props) => {
<Box display={"inline-block"}>開始日</Box>
<Box display={"inline-block"} ml={1}>{issueStore.issue.startDate ? dayjs(issueStore.issue.startDate).format("YYYY/MM/DD") : "-"}</Box>
</Box>
<Box display={"inline-block"} ml={3} style={ (issueStore.issue.dueDate && dayjs(issueStore.issue.dueDate).isBefore(dayjs())) ? { color: "#f42858" } : undefined}>
<Box display={"inline-block"} ml={3} style={(issueStore.issue.dueDate && dayjs(issueStore.issue.dueDate).isBefore(dayjs())) ? { color: "#f42858" } : undefined}>
<Box display={"inline-block"}>期限日</Box>
<Box display={"inline-block"} ml={1}>{issueStore.issue.dueDate ? dayjs(issueStore.issue.dueDate).format("YYYY/MM/DD") : "-"} {dayjs(issueStore.issue.dueDate).isBefore(dayjs()) ? "🔥" : ""}</Box>
</Box>
Expand All @@ -77,7 +77,7 @@ export const Issue: React.FC = observer((props) => {
className="markdown-body"
remarkPlugins={[[remarkGfm, { singleTilde: false, }]]}
components={{
code({node, inline, className, children, ...props}) {
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return inline ? (
<code {...props} className={className}>
Expand All @@ -98,7 +98,7 @@ export const Issue: React.FC = observer((props) => {
>
{issueStore.issue.description?.replace(/!\[image\]\[(.*?)\]/g, (all, match1) => {
const targetAttachmentId = issueStore.issue.attachments?.slice().find((attachment) => attachment.name === match1)?.id;
return `![image](/assets/issues/${issueId}/attachments/${targetAttachmentId})`;
return `![image](./issues/${issueId}/attachments/${targetAttachmentId})`;
}).replaceAll("\n", " \n")}
</ReactMarkdown>
</Box>
Expand Down Expand Up @@ -171,7 +171,7 @@ export const Issue: React.FC = observer((props) => {
<Box>
<Avatar
alt={issueStore.issue.assignee?.name}
src={`/assets/users/${issueStore.issue.assignee?.id}/icon`}
src={`./users/${issueStore.issue.assignee?.id}/icon`}
sx={{ width: 24, height: 24, fontSize: 12, m: 0 }}
/>
</Box>
Expand Down Expand Up @@ -217,7 +217,7 @@ export const Issue: React.FC = observer((props) => {
</Box>
<Box mt={2}>
<Button variant="text" disableElevation fullWidth onClick={() => setShowMoreInfo(!showMoreinfo)}>
{showMoreinfo ? <ArrowUpward /> : <ArrowDownward /> }
{showMoreinfo ? <ArrowUpward /> : <ArrowDownward />}
</Button>
</Box>
</CardContent>
Expand Down Expand Up @@ -289,7 +289,7 @@ export const Issue: React.FC = observer((props) => {
case "status":
return <Typography variant="body2" key={index}>◎ 状態: {changeLog.originalValue || "未設定"} ➡️ {changeLog.newValue || "未設定"}</Typography>;
case "attachment":
return <Typography variant="body2" key={index}>◎ 添付ファイル: {changeLog.originalValue || "未設定"} ➡️ {changeLog.newValue || "削除"} {changeLog.attachmentInfo && <a href={`/assets/issues/${issueId}/attachments/${changeLog.attachmentInfo.id}`} download={changeLog.attachmentInfo?.name}>ダウンロード</a>}</Typography>;
return <Typography variant="body2" key={index}>◎ 添付ファイル: {changeLog.originalValue || "未設定"} ➡️ {changeLog.newValue || "削除"} {changeLog.attachmentInfo && <a href={`./issues/${issueId}/attachments/${changeLog.attachmentInfo.id}`} download={changeLog.attachmentInfo?.name}>ダウンロード</a>}</Typography>;
case "summary":
return <Typography variant="body2" key={index}>◎ 件名: {changeLog.originalValue || "未設定"} ➡️ {changeLog.newValue || "未設定"}</Typography>;
default:
Expand All @@ -300,7 +300,7 @@ export const Issue: React.FC = observer((props) => {
className="markdown-body"
remarkPlugins={[[remarkGfm, { singleTilde: false, }]]}
components={{
code({node, inline, className, children, ...props}) {
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return inline ? (
<code {...props} className={className}>
Expand All @@ -321,7 +321,7 @@ export const Issue: React.FC = observer((props) => {
>
{comment.content?.replace(/!\[image\]\[(.*?)\]/g, (all, match1) => {
const targetAttachmentId = comment.changeLog?.slice().find((attachment) => attachment.attachmentInfo?.name === match1)?.attachmentInfo.id;
return `![image](/assets/issues/${issueId}/attachments/${targetAttachmentId})`;
return `![image](./issues/${issueId}/attachments/${targetAttachmentId})`;
}).replaceAll("\n", " \n")}
</ReactMarkdown>
{comment.created !== comment.updated && (
Expand Down
22 changes: 16 additions & 6 deletions viewer/containers/issues.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import { useStore } from "../stores";
import { useDidMount } from "@better-hooks/lifecycle";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
Expand All @@ -15,6 +15,16 @@ export const Issues: React.FC = observer((props) => {
pageStore.generateIndex();
});

useEffect(() => {
const loadData = async () => {
// 静的ファイルとして配置されたJSONを fetch で取得
const response = await fetch('./configs/pages.json');
const data = await response.json();
pageStore.setPages(data);
};
loadData();
}, []);

const columns: GridColDef[] = [
{
field: "種別",
Expand Down Expand Up @@ -46,12 +56,12 @@ export const Issues: React.FC = observer((props) => {
<Box display={"flex"} alignItems={"center"}>
<Avatar
alt={params.row?.assignee?.name}
src={`/assets/users/${params.row?.assignee?.id}/icon`}
src={`./users/${params.row?.assignee?.id}/icon`}
sx={{ width: 24, height: 24, fontSize: 12, mr: 0.5 }}
/>
{params.row?.assignee?.name}
{params.row?.assignee?.name}
</Box>
) : params.row?.assignee?.name
) : params.row?.assignee?.name
},
{
field: "状態",
Expand All @@ -73,7 +83,7 @@ export const Issues: React.FC = observer((props) => {
width: 72,
valueGetter: (params) => params.row?.priority?.name,
renderCell: (params) => {
switch(params.row?.priority?.name) {
switch (params.row?.priority?.name) {
case "高":
return <span style={{ color: "#f42858" }}>⬆</span>;
case "中":
Expand Down Expand Up @@ -104,7 +114,7 @@ export const Issues: React.FC = observer((props) => {
variant="outlined"
size="small"
fullWidth={true}
label={pageStore.loadingIndexes ? "キーワード検索 辞書取得中..." : "キーワード検索" }
label={pageStore.loadingIndexes ? "キーワード検索 辞書取得中..." : "キーワード検索"}
placeholder="mecab-ipadicによる形態素解析結果を使用してキーワード検索"
disabled={pageStore.loadingIndexes}
value={pageStore.keyword}
Expand Down
16 changes: 8 additions & 8 deletions viewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { configure } from "mobx";
import { observer } from "mobx-react-lite";
import React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { HashRouter, Route, Routes } from "react-router-dom";
import { style } from "typestyle";
import CssBaseline from "@mui/material/CssBaseline";
import { Index } from "./containers";
Expand Down Expand Up @@ -55,12 +55,12 @@ const styles = {
createRoot(document.querySelector("#app")!).render(
<div className={styles.root}>
<CssBaseline />
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/issues" element={<Issues />} />
<Route path="/issues/:id" element={<Issue />} />
</Routes>
</BrowserRouter>
<HashRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/issues" element={<Issues />} />
<Route path="/issues/:id" element={<Issue />} />
</Routes>
</HashRouter>
</div>
);
4 changes: 2 additions & 2 deletions viewer/stores/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export class IssueStore {
}

const [issue, comments] = await Promise.all([
fetch(`/assets/issues/${issueId}/issue.json`),
fetch(`/assets/issues/${issueId}/comments.json`)
fetch(`./issues/${issueId}/issue.json`),
fetch(`./issues/${issueId}/comments.json`)
]);
this.issue = await issue.json();
this.comments = await comments.json();
Expand Down
8 changes: 4 additions & 4 deletions viewer/stores/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class PageStore {
public currentDownloading = 0;
public page = 0;
public pageSize = 20;
public issueKeyIndex: {[issueKey: string]: string} = {};
public issueKeyIndex: { [issueKey: string]: string } = {};
public searchIndex: FlexSearchDocument<unknown, false> | undefined;
public keyword = "";

Expand All @@ -31,15 +31,15 @@ export class PageStore {
this.loadingPages = true;

try {
const pageInfo = await fetch("/assets/configs/pages.json");
const pageInfo = await fetch("./configs/pages.json");
const { start, end } = await pageInfo.json();
this.totalPage = end;

const pages = [];
for (let i = start; i <= end; i++) {
this.currentDownloading = i;

const res = await fetch(`/assets/pages/${i}.json`);
const res = await fetch(`./pages/${i}.json`);
const page: backlog.Entity.Issue.Issue[] = await res.json();
pages.push(...page);

Expand All @@ -63,7 +63,7 @@ export class PageStore {
this.loadingIndexes = true;

try {
const res = await fetch("/assets/configs/search-index.json");
const res = await fetch("./configs/search-index.json");
const searchIndexes = await res.json();

this.searchIndex = new FlexSearchDocument({
Expand Down
Loading