Browse Source

初回

develop
sosuke.iwabuchi 2 years ago
commit
b39fe150f5
24 changed files with 14647 additions and 0 deletions
  1. +6
    -0
      .env.example
  2. +54
    -0
      .eslintrc.js
  3. +18
    -0
      .gitignore
  4. +10
    -0
      bin/makeDefinitions.sh
  5. +9
    -0
      bin/upload.sh
  6. +19
    -0
      dev-uploader.js
  7. +7832
    -0
      package-lock.json
  8. +50
    -0
      package.json
  9. +12
    -0
      src/apps/定期予約選考/customize-manifest.json
  10. +97
    -0
      src/apps/定期予約選考/index.ts
  11. +6
    -0
      src/common/appids.ts
  12. +39
    -0
      src/common/header-button.ts
  13. +20
    -0
      src/common/rest-api-client.ts
  14. +5
    -0
      src/mypage/index.ts
  15. +34
    -0
      src/mypage/lib.ts
  16. +24
    -0
      src/mypage/メール.ts
  17. +9
    -0
      src/mypage/定期予約選考.ts
  18. +10
    -0
      src/types/index.ts
  19. +66
    -0
      src/types/定期予約選考.ts
  20. +59
    -0
      src/types/定期申込予約.ts
  21. +29
    -0
      tsconfig.json
  22. +19
    -0
      uploader.js
  23. +108
    -0
      webpack.config.js
  24. +6112
    -0
      yarn.lock

+ 6
- 0
.env.example View File

@@ -0,0 +1,6 @@
KINTONE_BASE_URL=
KINTONE_USERNAME=
KINTONE_PASSWORD=

MYPAGE_BASE_URL=
MYPAGE_TOKEN=

+ 54
- 0
.eslintrc.js View File

@@ -0,0 +1,54 @@
module.exports = {
// --------------------------------
// 呼び出したいルール(パッケージ)
// --------------------------------
// ES5 & kintone の場合
// extends: "@cybozu/eslint-config/presets/kintone-customize-es5",

// ES6以上 & kintone の場合
extends: ["@cybozu", "@cybozu/eslint-config/globals/kintone"],

// node & kintone の場合
// extends: ["@cybozu/eslint-config/presets/node", "@cybozu/eslint-config/globals/kintone"],

// --------------------------------
// グローバル変数の定義
// --------------------------------
globals: {
garoon: "readonly",
},

// --------------------------------
// ルール
// --------------------------------
rules: {
quotes: ["error", "single"],
"require-atomic-updates": "off",
},

// --------------------------------
// TypeScript用の設定
// --------------------------------
overrides: [
{
files: ["*.ts", "*.tsx"],
extends: [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"@cybozu",
"@cybozu/eslint-config/globals/kintone",
],
plugins: ["@typescript-eslint"],
parser: "@typescript-eslint/parser",
parserOptions: {
sourceType: "module",
},
rules: {
quotes: ["error", "single"],
"require-atomic-updates": "off",
"no-undef": "off",
},
},
],
};

+ 18
- 0
.gitignore View File

@@ -0,0 +1,18 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Dependency directories
node_modules/

# webpack build directory
dist/


# secret
.env

.vscode

+ 10
- 0
bin/makeDefinitions.sh View File

@@ -0,0 +1,10 @@
#!/bin/bash

cd $(dirname ${0})
cd ..

# 環境変数の読み込み
# export $(cat .env.development | grep -v ^# | sed -e s/^DEV_//g | xargs)

echo "env-cmd kintone-dts-gen --app-id ${1} -o ./types/fields.d.ts"
npx env-cmd kintone-dts-gen --app-id ${1} -o ./types/fields.d.ts

+ 9
- 0
bin/upload.sh View File

@@ -0,0 +1,9 @@
#!/bin/bash

cd $(dirname ${0})
cd ..

# 環境変数の読み込み
export $(cat .env.development | grep -v ^# | sed -e s/^DEV_//g | xargs)

yarn upload

+ 19
- 0
dev-uploader.js View File

@@ -0,0 +1,19 @@
import { execSync } from 'child_process';

const file =
process.argv[2]

if (file !== undefined && file.length === 0) {
throw new Error("引数不正")
}
if (!file.endsWith('customize-manifest.json')) {
throw new Error("ファイル名不正 customize-manifest.jsonを指定してください")
}


console.log('\nuploading... ', file);
const command = `npx kintone-customize-uploader `;
const result = execSync(command + file);
console.log('\n' + result);



+ 7832
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 50
- 0
package.json View File

@@ -0,0 +1,50 @@
{
"name": "kt-kintone",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode production",
"dev": "webpack --mode development",
"watch": "webpack -w --mode development",
"lint": "eslint src/**/*.js",
"fix": "eslint src/**/*.js --fix",
"upload": "cross-env NODE_ENV=development babel-node --presets @babel/env -r dotenv/config uploader.js",
"dev-upload": "cross-env NODE_ENV=development webpack && babel-node --presets @babel/env -r dotenv/config dev-uploader.js"
},
"author": "sosuke iwabuchi",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/node": "^7.8.7",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@cybozu/eslint-config": "^8.0.0",
"@kintone/customize-uploader": "^3.0.1",
"@kintone/dts-gen": "^5.0.1",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"@typescript-eslint/eslint-plugin": "^4.2.0",
"@typescript-eslint/parser": "^4.2.0",
"babel-loader": "^8.1.0",
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"dotenv-webpack": "^8.0.1",
"env-cmd": "^10.1.0",
"eslint": "^6.8.0",
"fork-ts-checker-webpack-plugin": "^5.2.0",
"glob": "^7.1.6",
"ts-loader": "^8.0.17",
"tsconfig-paths-webpack-plugin": "^4.1.0",
"typescript": "^4.1.2",
"webpack": "^5.51.2",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.1.0"
},
"dependencies": {
"@kintone/rest-api-client": "^2.0.17",
"core-js": "^3.6.4"
}
}

+ 12
- 0
src/apps/定期予約選考/customize-manifest.json View File

@@ -0,0 +1,12 @@
{
"app": "301",
"scope": "ALL",
"desktop": {
"js": ["dist/定期予約選考.js"],
"css":[]
},
"mobile": {
"js": [],
"css":[]
}
}

+ 97
- 0
src/apps/定期予約選考/index.ts View File

@@ -0,0 +1,97 @@
import { AppID } from "@/common/appids";
import { setHeaderButton } from "@/common/header-button";
import { makeRecordData } from "@/common/rest-api-client";
import { noticeToCandidates } from "@/mypage/定期予約選考";
import {
定期予約選考,
定期予約選考フィールド名,
選考ステータスDropdown,
} from "@/types/定期予約選考";
import { 定期申込予約フィールド名, 状態Dropdown } from "@/types/定期申込予約";
import { KintoneRestAPIClient } from "@kintone/rest-api-client";

const getCallBackHandleNoticeTocandidates = (currentRecord: 定期予約選考) => {
return async () => {
const confirm = window.confirm("対象者へメール送信しますか?");
if (!confirm || !currentRecord) return;

const result = await noticeToCandidates(
Number(currentRecord.レコード番号.value)
);
if (result) {
window.alert("通知しました");
location.reload();
} else {
window.alert("失敗しました");
}
};
};

const getCallBackHandleFinish = (currentRecord: 定期予約選考) => {
return async () => {
const confirm = window.confirm("確定しますか");
if (!confirm || !currentRecord) return;

const client = new KintoneRestAPIClient();

// 選考のステータスを終了にする
const param = {
app: AppID.定期予約選考,
id: kintone.app.record.getId() ?? "",
record: makeRecordData({
[定期予約選考フィールド名.選考ステータス]: 選考ステータスDropdown.終了,
}),
};

// 申込のステータスを選考当選にする
const entries = {
app: AppID.定期申込予約,
records: currentRecord.選考結果一覧.value.map((row) => {
return {
id: row.value.選考結果一覧_申込レコード番号.value,
record: makeRecordData({
[定期申込予約フィールド名.状態]: 状態Dropdown.選考当選,
}),
};
}),
};

try {
await client.record.updateRecord(param);
await client.record.updateRecords(entries);
} catch (e) {
// 失敗
window.alert("失敗しました");
throw e;
}

window.alert("成功しました。当選者の承認処理を行ってください");
location.reload();
};
};

(() => {
const events = ["app.record.create.show", "app.record.edit.show"];
kintone.events.on(events, (event) => {
const record = event.record as 定期予約選考;
});

kintone.events.on("app.record.detail.show", (event) => {
const currentRecord: 定期予約選考 = event.record;

// 各種ボタンの設置
if (
currentRecord.選考ステータス.value === 選考ステータスDropdown.通知者選択中
) {
setHeaderButton(
"通知メール送信",
getCallBackHandleNoticeTocandidates(currentRecord)
);
}
if (
currentRecord.選考ステータス.value === 選考ステータスDropdown.候補者仮決定
) {
setHeaderButton("当選者確定", getCallBackHandleFinish(currentRecord));
}
});
})();

+ 6
- 0
src/common/appids.ts View File

@@ -0,0 +1,6 @@
export const AppID = {
定期申込予約: 271,
定期予約選考: 301,
問い合わせ: 291,
} as const;
export type AppID = (typeof AppID)[keyof typeof AppID];

+ 39
- 0
src/common/header-button.ts View File

@@ -0,0 +1,39 @@
export const setHeaderButton = (title: string, onClick: VoidFunction) => {
const interval = setInterval(() => {
const wrapper = document.getElementsByClassName(
"gaia-app-statusbar-actionmenu-wrapper"
)[0];

if (wrapper) {
// メニューが存在しなければ作成する
let menu = wrapper.querySelector(".gaia-app-statusbar-actionmenu");
if (!menu) {
menu = document.createElement("div");
menu.classList.add(".gaia-app-statusbar-actionmenu");
wrapper.appendChild(menu);
}

// ステータスバーの表示が無効になっていれば解除する
const statusbar = document.querySelector<HTMLDivElement>(
".gaia-app-statusbar"
);
if (statusbar) {
statusbar.style.display = "unset";
}

const button = document.createElement("span");
button.classList.add("gaia-app-statusbar-action");
const label = document.createElement("span");
label.setAttribute("title", title);
label.innerText = title;
label.classList.add("gaia-app-statusbar-action-label");

button.onclick = onClick;

button.appendChild(label);
menu.appendChild(button);

clearInterval(interval);
}
}, 50);
};

+ 20
- 0
src/common/rest-api-client.ts View File

@@ -0,0 +1,20 @@
export const setFieldValue = (fieldCode: string, value: string | object) => {
return {
[fieldCode]: {
value,
},
};
};

export const makeRecordData = (data: {
[fieldCode: string]: string | object;
}) => {
let ret = {};
Object.keys(data).forEach((fieldCode) => {
ret = {
...ret,
...setFieldValue(fieldCode, data[fieldCode]),
};
});
return ret;
};

+ 5
- 0
src/mypage/index.ts View File

@@ -0,0 +1,5 @@
export const MyPageApiID = {
メール送信依頼: "email/send",
定期選考一斉通知: "season-ticket-contract-selection/notice-to-candidates",
} as const;
export type MyPageApiID = (typeof MyPageApiID)[keyof typeof MyPageApiID];

+ 34
- 0
src/mypage/lib.ts View File

@@ -0,0 +1,34 @@
import { MyPageApiID } from ".";

type MyPageApiResponse = {
result: "SUCCESS" | "FAILED";
};

export const getUrl = (apiId: MyPageApiID) => {
return [process.env.MYPAGE_BASE_URL ?? "", "api-from-kintone", apiId].join(
"/"
);
};

export const send = async (apiId: MyPageApiID, data: object) => {
const url = getUrl(apiId);
const sendData = {
...data,
token: process.env.MYPAGE_TOKEN ?? "",
};
console.info("MyPageAPICall", url, data);

const res: MyPageApiResponse = await kintone.proxy(
url,
"POST",
{ "Content-Type": "application/json" },
sendData
);

if (res?.result === "SUCCESS") {
return true;
} else {
console.error(res);
return false;
}
};

+ 24
- 0
src/mypage/メール.ts View File

@@ -0,0 +1,24 @@
import { MyPageApiID } from ".";
import { send } from "./lib";

export const EmailID = {
定期選考_一斉通知: "定期選考_一斉通知",
} as const;
export type EmailID = (typeof EmailID)[keyof typeof EmailID];

export const sendEmail = (
id: EmailID,
key: {
season_ticket_contract_entry_record_no?: string;
application_no?: string;
},
data: object
) => {
const sendData = {
email_id: id,
...data,
...key,
token: process.env.MYPAGE_TOKEN ?? "",
};
return send(MyPageApiID.メール送信依頼, sendData);
};

+ 9
- 0
src/mypage/定期予約選考.ts View File

@@ -0,0 +1,9 @@
import { MyPageApiID } from ".";
import { send } from "./lib";

export const noticeToCandidates = (recordNo: number) => {
const sendData = {
record_no: recordNo,
};
return send(MyPageApiID.定期選考一斉通知, sendData);
};

+ 10
- 0
src/types/index.ts View File

@@ -0,0 +1,10 @@
import { KintoneRecordField } from "@kintone/rest-api-client";

export type AppRecord = {
$id: KintoneRecordField.ID;
$revision: KintoneRecordField.Revision;
作成者: KintoneRecordField.Creator;
レコード番号: KintoneRecordField.RecordNumber;
更新日時: KintoneRecordField.UpdatedTime;
作成日時: KintoneRecordField.CreatedTime;
};

+ 66
- 0
src/types/定期予約選考.ts View File

@@ -0,0 +1,66 @@
import { KintoneRecordField } from "@kintone/rest-api-client";
import { AppRecord } from ".";

const F = {
選考ステータス: "選考ステータス",
} as const;

export const 選考ステータスDropdown = {
起票: "起票",
通知者選択中: "通知者選択中",
契約希望者受付中: "契約希望者受付中",
候補者仮決定: "候補者仮決定",
選考不調: "選考不調",
終了: "終了",
} as const;
export type 選考ステータスDropdown =
(typeof 選考ステータスDropdown)[keyof typeof 選考ステータスDropdown];

export const 定期予約選考フィールド名 = F;
export type 定期予約選考 = AppRecord & {
利用開始日: KintoneRecordField.Date;
検索用_契約希望者_レコード番号: KintoneRecordField.SingleLineText;
自動選考メッセージ: KintoneRecordField.MultiLineText;
[F.選考ステータス]: KintoneRecordField.Dropdown;
選考締切日: KintoneRecordField.Date;
検索用_申込一覧_レコード番号: KintoneRecordField.SingleLineText;
駐車場名: KintoneRecordField.SingleLineText;
検索用_選考結果_レコード番号: KintoneRecordField.SingleLineText;
申込者一覧: KintoneRecordField.Subtable<{
申込者一覧_申込レコード番号: KintoneRecordField.Number;
申込者一覧_メールアドレス: KintoneRecordField.SingleLineText;
申込者一覧_台数: KintoneRecordField.SingleLineText;
申込者一覧_氏名: KintoneRecordField.SingleLineText;
申込者一覧_受付日時: KintoneRecordField.DateTime;
申込者一覧_プラン: KintoneRecordField.SingleLineText;
申込者一覧_申込番号: KintoneRecordField.SingleLineText;
申込者一覧_利用開始希望日: KintoneRecordField.Date;
申込者一覧_状態: KintoneRecordField.SingleLineText;
申込者一覧_通知対象: KintoneRecordField.CheckBox;
}>;
対象車室一覧: KintoneRecordField.Subtable<{
対象車室一覧_車室タイプ: KintoneRecordField.SingleLineText;
対象車室一覧_車室番号: KintoneRecordField.SingleLineText;
対象車室一覧_車室レコード番号: KintoneRecordField.Number;
}>;
選考結果一覧: KintoneRecordField.Subtable<{
選考結果一覧_氏名: KintoneRecordField.SingleLineText;
選考結果一覧_申込レコード番号: KintoneRecordField.Number;
選考結果一覧_申込番号: KintoneRecordField.SingleLineText;
選考結果一覧_車室レコード番号: KintoneRecordField.Number;
選考結果一覧_プラン: KintoneRecordField.SingleLineText;
選考結果一覧_車室タイプ: KintoneRecordField.SingleLineText;
選考結果一覧_状態: KintoneRecordField.SingleLineText;
選考結果一覧_車室番号: KintoneRecordField.SingleLineText;
}>;
契約希望者一覧: KintoneRecordField.Subtable<{
契約希望者一覧_利用開始希望日: KintoneRecordField.Date;
契約希望者一覧_申込番号: KintoneRecordField.SingleLineText;
契約希望者一覧_申込レコード番号: KintoneRecordField.Number;
契約希望者一覧_台数: KintoneRecordField.SingleLineText;
契約希望者一覧_プラン: KintoneRecordField.SingleLineText;
契約希望者一覧_状態: KintoneRecordField.SingleLineText;
契約希望者一覧_氏名: KintoneRecordField.SingleLineText;
契約希望者一覧_受付日時: KintoneRecordField.DateTime;
}>;
};

+ 59
- 0
src/types/定期申込予約.ts View File

@@ -0,0 +1,59 @@
import { KintoneRecordField } from "@kintone/rest-api-client";
import { AppRecord } from ".";

const F = {
状態: "status",
} as const;

export const 状態Dropdown = {
新規: "新規",
承認_自動承認: "承認(自動承認)",
承認_手動: "承認(手動)",
契約中: "契約中",
予約: "予約",
選考当選: "選考当選",
入金待ち: "入金待ち",
空き待ち: "空き待ち",
キャンセル: "キャンセル",
} as const;
export type 状態Dropdown = (typeof 状態Dropdown)[keyof typeof 状態Dropdown];

export const 定期申込予約フィールド名 = F;

export type 定期申込予約 = AppRecord & {
備考: KintoneRecordField.MultiLineText;
受付順: KintoneRecordField.Number;
申込番号: KintoneRecordField.SingleLineText;
利用方法: KintoneRecordField.MultiLineText;
定期駐車場プラン: KintoneRecordField.SingleLineText;
台数: KintoneRecordField.SingleLineText;
防犯登録番号: KintoneRecordField.SingleLineText;
請求対象分_月: KintoneRecordField.Number;
初回振り込み合計額: KintoneRecordField.Number;
メールアドレス: KintoneRecordField.SingleLineText;
受付日時: KintoneRecordField.DateTime;
学生手帳: KintoneRecordField.SingleLineText;
氏名: KintoneRecordField.SingleLineText;
定期駐車料金: KintoneRecordField.Number;
学校名: KintoneRecordField.SingleLineText;
住所: KintoneRecordField.SingleLineText;
電話番号: KintoneRecordField.SingleLineText;
受付日: KintoneRecordField.Date;
ParkingNavi駐車場コード: KintoneRecordField.SingleLineText;
フリガナ: KintoneRecordField.SingleLineText;
利用開始希望日: KintoneRecordField.Date;
メモ: KintoneRecordField.MultiLineText;
車室番号: KintoneRecordField.SingleLineText;
ParkingNavi駐車場: KintoneRecordField.SingleLineText;
駐車場: KintoneRecordField.SingleLineText;
振込期日: KintoneRecordField.Date;
日割り分_金額: KintoneRecordField.Number;
自動承認メモ: KintoneRecordField.MultiLineText;
車両番号: KintoneRecordField.SingleLineText;
請求対象分_金額: KintoneRecordField.Number;
ParkingNaviプラン: KintoneRecordField.SingleLineText;
日割り分_月: KintoneRecordField.Number;
支払方法: KintoneRecordField.Dropdown;
auto_confirm_status: KintoneRecordField.SingleLineText;
[F.状態]: KintoneRecordField.Dropdown;
};

+ 29
- 0
tsconfig.json View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"sourceMap": true,
// TSはECMAScript 5に変換
"target": "ES2015",
// TSのモジュールはES Modulesとして出力
"module": "ES2015",
// 厳密モードとして設定
"strict": true,
"allowJs": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
},
},
"files": [
"./node_modules/@kintone/dts-gen/kintone.d.ts",
],
"include": [
"src/**/*"
],
"exclude": [
"dist",
"node_modules"
]
}

+ 19
- 0
uploader.js View File

@@ -0,0 +1,19 @@
const { execSync } = require('child_process');
const glob = require('glob');

// const command = `npx kintone-customize-uploader --base-url ${process.env.KINTONE_BASE_URL} --username ${process.env.KINTONE_USER} --password ${process.env.KINTONE_PASSWORD} `;
// npx kintone-customize-uploader --base-url https://5c8zkdujl0pn.cybozu.co --username sosuke.iwabuchi@satellite-tech.co.jp --password .1Satellite
// KINTONE_BASE_URL=https://5c8zkdujl0pn.cybozu.com
// KINTONE_USERNAME=sosuke.iwabuchi@satellite-tech.co.jp
// KINTONE_PASSWORD=.1Satellite
const command = `npx kintone-customize-uploader `;
const entries =
process.argv.slice(2).length > 0
? process.argv.slice(2)
: glob.sync('src/apps/**/customize-manifest.json');
entries.forEach(file => {
console.log('\nuploading... ', file);
const result = execSync(command + file);
console.log('\n' + result);
});


+ 108
- 0
webpack.config.js View File

@@ -0,0 +1,108 @@
const webpack = require('webpack');
const path = require('path');
const glob = require('glob');
const { exec } = require('child_process');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const Dotenv = require('dotenv-webpack');

const basePath = path.resolve('src', 'apps');

// basePath配下の各ディレクトリを複数のentryとする
const entries = glob.sync('**/index.+(js|ts|tsx)', { cwd: basePath }).reduce(
(prev, file) => ({
...prev,
[path.dirname(file)]: path.resolve(basePath, file),
}),
{}
);

module.exports = {
entry: entries,
cache: false,
resolve: {
// 拡張子を配列で指定
extensions: [
'.ts', '.js',
],
plugins: [
new TsconfigPathsPlugin({
configFile: './tsconfig.json',
}),
]
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3,
},
],
],
},
},
},
{
test: /\.tsx?$/,
use: 'ts-loader',
}
],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
plugins: [
// new Dotenv({ systemvars: true }),
new ForkTsCheckerWebpackPlugin(),
{
// watchモードのとき再ビルドされたものをアップロードする
apply: (compiler) => {
compiler.hooks.afterEmit.tapPromise(
'upload javascript files',
(compilation) => {
if (!compiler.options.watch) return Promise.resolve();

const emittedFiles = Object.keys(compilation.assets)
.filter((file) => {
const source = compilation.assets[file];
return source.emitted && source.existsAt;
})
.map((file) => file.replace('.js', ''));

const processes = glob
.sync(`@(${emittedFiles.join('|')})/customize-manifest.json`, {
cwd: basePath,
})
.map((file) => {
console.log('\nuploading... ', file);
return exec(
`yarn upload ${path.resolve(basePath, file)}`,
(err, stdout, stderr) => {
if (stdout) process.stdout.write(stdout);
if (stderr) process.stderr.write(stderr);
}
);
});
return Promise.all(processes);
}
);
},
},
// new webpack.DefinePlugin({
// 'process.env': {
// "MYPAGE_BASE_URL": JSON.stringify(process.env.MYPAGE_BASE_URL),
// "MYPAGE_TOKEN": JSON.stringify(process.env.MYPAGE_TOKEN),
// }
// }),
],
};

+ 6112
- 0
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save