Skip to content

Commit 3f098c4

Browse files
committed
[add] Git Pager based on HTML, JSON & Article Editor components
[add] GitHub proxy API
1 parent 65d20ad commit 3f098c4

File tree

21 files changed

+894
-71
lines changed

21 files changed

+894
-71
lines changed

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ NEXT_PUBLIC_SENTRY_DSN =
66
SENTRY_ORG =
77
SENTRY_PROJECT =
88

9-
LARK_API_HOST = https://open.larksuite.com/open-apis/
9+
LARK_API_HOST = https://open.feishu.cn/open-apis/

components/Form/HTMLEditor.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { FC } from 'react';
2+
import {
3+
AudioTool,
4+
CopyMarkdownTool,
5+
Editor,
6+
EditorProps,
7+
IFrameTool,
8+
OriginalTools,
9+
VideoTool,
10+
} from 'react-bootstrap-editor';
11+
import { Constructor } from 'web-utility';
12+
13+
const ExcludeTools = [IFrameTool, AudioTool, VideoTool];
14+
15+
const CustomTools = OriginalTools.filter(
16+
Tool => !ExcludeTools.includes(Tool as Constructor<IFrameTool>),
17+
);
18+
19+
export const HTMLEditor: FC<EditorProps> = props => (
20+
<Editor tools={[...CustomTools, CopyMarkdownTool]} {...props} />
21+
);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Icon } from 'idea-react';
2+
import { FC } from 'react';
3+
import { Button } from 'react-bootstrap';
4+
5+
const type_map = {
6+
string: { title: 'Inline text', icon: 'input-cursor' },
7+
text: { title: 'Rows text', icon: 'text-left' },
8+
object: { title: 'Key-value list', icon: 'list-ul' },
9+
array: { title: 'Ordered list', icon: 'list-ol' },
10+
};
11+
12+
export interface AddBarProps {
13+
onSelect: (type: string) => void;
14+
}
15+
16+
export const AddBar: FC<AddBarProps> = ({ onSelect }) => (
17+
<nav className="d-flex gap-1">
18+
{Object.entries(type_map).map(([key, { title, icon }]) => (
19+
<Button
20+
key={key}
21+
size="sm"
22+
variant="success"
23+
title={title}
24+
onClick={onSelect.bind(null, key)}
25+
>
26+
<Icon name={icon} />
27+
</Button>
28+
))}
29+
</nav>
30+
);
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { observable } from 'mobx';
2+
import { observer } from 'mobx-react';
3+
import { DataObject } from 'mobx-restful';
4+
import { ChangeEvent, Component, ReactNode } from 'react';
5+
import { Form } from 'react-bootstrap';
6+
7+
import { AddBar } from './AddBar';
8+
9+
export interface DataMeta {
10+
type: string;
11+
key?: string | number;
12+
value: any;
13+
// eslint-disable-next-line no-restricted-syntax
14+
children?: DataMeta[];
15+
}
16+
17+
export interface FieldProps {
18+
value: DataObject | any[] | null;
19+
onChange?: (event: FieldChangeEvent) => void;
20+
}
21+
export type FieldChangeEvent = ChangeEvent<{ value: FieldProps['value'] }>;
22+
23+
@observer
24+
export class ListField extends Component<FieldProps> {
25+
@observable
26+
accessor innerValue = {} as DataMeta;
27+
28+
componentDidMount() {
29+
this.innerValue = ListField.metaOf(this.props.value);
30+
}
31+
32+
componentDidUpdate({ value }: Readonly<FieldProps>) {
33+
if (value !== this.props.value) this.componentDidMount();
34+
}
35+
36+
static metaOf(value: any): DataMeta {
37+
if (value instanceof Array)
38+
return {
39+
type: 'array',
40+
value,
41+
children: Array.from(value, (value, key) => ({
42+
...this.metaOf(value),
43+
key,
44+
})),
45+
};
46+
47+
if (value instanceof Object)
48+
return {
49+
type: 'object',
50+
value,
51+
children: Object.entries(value).map(([key, value]) => ({
52+
...this.metaOf(value),
53+
key,
54+
})),
55+
};
56+
57+
return {
58+
type: /[\r\n]/.test(value) ? 'text' : 'string',
59+
value,
60+
};
61+
}
62+
63+
addItem = (type: string) => {
64+
let item: DataMeta = { type, value: [] };
65+
const { innerValue } = this;
66+
67+
switch (type) {
68+
case 'string':
69+
item = ListField.metaOf('');
70+
break;
71+
case 'text':
72+
item = ListField.metaOf('\n');
73+
break;
74+
case 'object':
75+
item = ListField.metaOf({});
76+
break;
77+
case 'array':
78+
item = ListField.metaOf([]);
79+
}
80+
81+
this.innerValue = {
82+
...innerValue,
83+
children: [...(innerValue.children || []), item],
84+
};
85+
};
86+
87+
protected dataChange =
88+
(method: (item: DataMeta, newKey: string) => any) =>
89+
(index: number, { currentTarget: { value: data } }: ChangeEvent<any>) => {
90+
const { children = [] } = this.innerValue;
91+
92+
const item = children[index];
93+
94+
if (!item) return;
95+
96+
method.call(this, item, data);
97+
98+
this.props.onChange?.({
99+
currentTarget: { value: this.innerValue.value },
100+
} as FieldChangeEvent);
101+
};
102+
103+
setKey = this.dataChange((item: DataMeta, newKey: string) => {
104+
const { value, children = [] } = this.innerValue;
105+
106+
item.key = newKey;
107+
108+
for (const oldKey in value)
109+
if (!children.some(({ key }) => key === oldKey)) {
110+
value[newKey] = value[oldKey];
111+
112+
delete value[oldKey];
113+
114+
return;
115+
}
116+
117+
value[newKey] = item.value;
118+
});
119+
120+
setValue = this.dataChange((item: DataMeta, newValue: any) => {
121+
const { value } = this.innerValue;
122+
123+
if (newValue instanceof Array) newValue = [...newValue];
124+
else if (typeof newValue === 'object') newValue = { ...newValue };
125+
126+
item.value = newValue;
127+
128+
if (item.key != null) value[item.key + ''] = newValue;
129+
else if (value instanceof Array) item.key = value.push(newValue) - 1;
130+
});
131+
132+
fieldOf(index: number, type: string, value: any) {
133+
switch (type) {
134+
case 'string':
135+
return (
136+
<Form.Control
137+
defaultValue={value}
138+
placeholder="Value"
139+
onBlur={this.setValue.bind(this, index)}
140+
/>
141+
);
142+
case 'text':
143+
return (
144+
<Form.Control
145+
as="textarea"
146+
defaultValue={value}
147+
placeholder="Value"
148+
onBlur={this.setValue.bind(this, index)}
149+
/>
150+
);
151+
default:
152+
return <ListField value={value} onChange={this.setValue.bind(this, index)} />;
153+
}
154+
}
155+
156+
wrapper(slot: ReactNode) {
157+
const Tag = this.innerValue.type === 'array' ? 'ol' : 'ul';
158+
159+
return <Tag className="list-unstyled d-flex flex-column gap-3">{slot}</Tag>;
160+
}
161+
162+
render() {
163+
const { type: field_type, children = [] } = this.innerValue;
164+
165+
return this.wrapper(
166+
<>
167+
<li>
168+
<AddBar onSelect={this.addItem} />
169+
</li>
170+
{children.map(({ type, key, value }, index) => (
171+
<li key={key} className="d-flex align-items-center gap-3">
172+
{field_type === 'object' && (
173+
<Form.Control
174+
defaultValue={key}
175+
required
176+
placeholder="Key"
177+
onBlur={this.setKey.bind(this, index)}
178+
/>
179+
)}
180+
{this.fieldOf(index, type, value)}
181+
</li>
182+
))}
183+
</>,
184+
);
185+
}
186+
}

0 commit comments

Comments
 (0)