Skip to content

Commit

Permalink
[add] Git Pager components & page
Browse files Browse the repository at this point in the history
[add] GitHub OAuth 2.0 API
[optimize] upgrade to KoAJAX 1, PNPM 9, Sentry 8 & other latest Upstream packages
  • Loading branch information
TechQuery committed Jun 1, 2024
1 parent ab48062 commit c837a6c
Show file tree
Hide file tree
Showing 16 changed files with 7,549 additions and 4,518 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

- uses: pnpm/action-setup@v2
with:
version: 8
version: 9

- uses: actions/setup-node@v3
if: ${{ !env.VERCEL_TOKEN || !env.VERCEL_ORG_ID || !env.VERCEL_PROJECT_ID }}
Expand Down
96 changes: 96 additions & 0 deletions components/Form/CascadeSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { debounce } from 'lodash';
import { Component } from 'react';
import { Form } from 'react-bootstrap';
import { uniqueID } from 'web-utility';

export interface CascadeProps {
required: boolean;
}

interface LevelItem {
label?: string;
list: string[];
}

export abstract class CascadeSelect<
P extends CascadeProps,
> extends Component<P> {
UID = uniqueID();

innerPath: string[] = [];

list: LevelItem[] = [];

get path() {
return this.innerPath.filter(Boolean).slice(0, -1).join('/');
}

get name() {
return this.innerPath.slice(-1)[0];
}

get pathName() {
return this.innerPath.filter(Boolean).join('/');
}

reset() {
const { innerPath, list } = this;

this.innerPath = [innerPath[0]];
this.list = [list[0]];
}

componentDidMount() {
this.changeLevel(-1, '');
}

abstract getNextLevel(): Promise<LevelItem | undefined>;

changeLevel = debounce(async (index: number, value: string) => {
const { innerPath, list } = this;

innerPath.splice(index, Infinity, value);

const level = await this.getNextLevel();

if (level != null) list.splice(++index, Infinity, level);
else list.length = ++index;

this.list = list;
});

render() {
const { UID, list } = this,
{ required } = this.props;

return (
<>
{list.map(({ label, list }, index) => {
const IID = `input-${UID}-${index}`,
LID = `list-${UID}-${index}`;

return (
<span key={IID} className="form-inline d-inline-flex">
<Form.Control
id={IID}
list={LID}
onChange={({ target: { value } }) =>
(value = value.trim()) && this.changeLevel(index, value)
}
required={!index && required}
/>
<datalist id={LID}>
{list.map(item => (
<option value={item} key={item} />
))}
</datalist>
<label htmlFor={IID} className="pl-2 pr-2">
{label}
</label>
</span>
);
})}
</>
);
}
}
26 changes: 26 additions & 0 deletions components/Form/JSONEditor/AddBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FC } from 'react';

const type_map = {
string: { title: 'Inline text', icon: 'grip-lines' },
text: { title: 'Rows text', icon: 'align-left' },
object: { title: 'Key-value list', icon: 'list-ul' },
array: { title: 'Ordered list', icon: 'list-ol' },
};

export interface AddBarProps {
onSelect: (type: string) => void;
}

export const AddBar: FC<AddBarProps> = ({ onSelect }) => (
<nav>
{Object.entries(type_map).map(([key, { title, icon }]) => (
<button
key={key}
type="button"
className={'btn btn-sm btn-success m-1 fas fa-' + icon}
title={title}
onClick={onSelect.bind(null, key)}
/>
))}
</nav>
);
179 changes: 179 additions & 0 deletions components/Form/JSONEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import { ChangeEvent, Component, ReactNode, SyntheticEvent } from 'react';
import { Form } from 'react-bootstrap';

import { AddBar } from './AddBar';

export interface DataMeta {
type: string;
key?: string | number;
value: any;
children?: DataMeta[];
}

export interface FieldProps {
value: object | any[] | null;
onChange?: (event: ChangeEvent) => void;
}

@observer
export class ListField extends Component<FieldProps> {
@observable
accessor innerValue = ListField.metaOf(this.props.value);

static metaOf(value: any): DataMeta {
if (value instanceof Array)
return {
type: 'array',
value,
children: Array.from(value, (value, key) => ({
...this.metaOf(value),
key,
})),
};

if (value instanceof Object)
return {
type: 'object',
value,
children: Object.entries(value).map(([key, value]) => ({
...this.metaOf(value),
key,
})),
};

return {
type: /[\r\n]/.test(value) ? 'text' : 'string',
value,
};
}

addItem = (type: string) => {
var item: DataMeta = { type, value: [] },
{ innerValue } = this;

switch (type) {
case 'string':
item = ListField.metaOf('');
break;
case 'text':
item = ListField.metaOf('\n');
break;
case 'object':
item = ListField.metaOf({});
break;
case 'array':
item = ListField.metaOf([]);
}

this.innerValue = {
...innerValue,
children: [...(innerValue.children || []), item],
};
};

protected dataChange =
(method: (item: DataMeta, newKey: string) => any) =>
(
index: number,
{ currentTarget: { value: data } }: SyntheticEvent<any>,
) => {
const { children = [] } = this.innerValue;

const item = children[index];

if (!item) return;

method.call(this, item, data);

this.props.onChange?.({
target: { value: this.innerValue.value },
} as unknown as ChangeEvent);
};

setKey = this.dataChange((item: DataMeta, newKey: string) => {
const { value, children = [] } = this.innerValue;

item.key = newKey;

for (let oldKey in value)
if (!children.some(({ key }) => key === oldKey)) {
value[newKey] = value[oldKey];

delete value[oldKey];
return;
}

value[newKey] = item.value;
});

setValue = this.dataChange((item: DataMeta, newValue: any) => {
const { value } = this.innerValue;

if (newValue instanceof Array) newValue = [...newValue];
else if (typeof newValue === 'object') newValue = { ...newValue };

item.value = newValue;

if (item.key != null) value[item.key + ''] = newValue;
else if (value instanceof Array) item.key = value.push(newValue) - 1;
});

fieldOf(index: number, type: string, value: any) {
switch (type) {
case 'string':
return (
<Form.Control
defaultValue={value}
placeholder="Value"
onBlur={this.setValue.bind(this, index)}
/>
);
case 'text':
return (
<Form.Control
as="textarea"
defaultValue={value}
placeholder="Value"
onBlur={this.setValue.bind(this, index)}
/>
);
default:
return (
<ListField value={value} onChange={this.setValue.bind(this, index)} />
);
}
}

wrapper(slot: ReactNode) {
const Tag = this.innerValue.type === 'array' ? 'ol' : 'ul';

return <Tag className="inline-form">{slot}</Tag>;
}

render() {
const { type: field_type, children = [] } = this.innerValue;

return this.wrapper(
<>
<li className="form-group">
<AddBar onSelect={this.addItem} />
</li>
{children.map(({ type, key, value }, index) => (
<li className="input-group input-group-sm" key={key}>
{field_type === 'object' && (
<Form.Control
defaultValue={key}
required
placeholder="Key"
onBlur={this.setKey.bind(this, index)}
/>
)}
{this.fieldOf(index, type, value)}
</li>
))}
</>,
);
}
}
34 changes: 34 additions & 0 deletions components/Form/MarkdownEditor/TurnDown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import TurnDown from 'turndown';
// @ts-ignore
import { gfm } from 'turndown-plugin-gfm';

const Empty_HREF = /^(#|javascript:\s*void\(0\);?\s*)$/;

type TurnDownGFM = (td: TurnDown) => void;

export default class extends TurnDown {
constructor(options?: any) {
super({
headingStyle: 'atx',
hr: '---',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
linkStyle: 'referenced',
...options,
});

this.use(gfm as TurnDownGFM)
.addRule('non_url', {
filter: node =>
['a', 'area'].includes(node.nodeName.toLowerCase()) &&
Empty_HREF.test(node.getAttribute('href') || ''),
replacement: (content, node) =>
content.trim() ||
(node instanceof HTMLElement ? node.title.trim() : ''),
})
.addRule('asset_code', {
filter: ['style', 'script'],
replacement: () => '',
});
}
}
20 changes: 20 additions & 0 deletions components/Form/MarkdownEditor/index.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.editor {
position: relative;
min-height: 2.35rem;
&::before {
position: absolute;
right: 1px;
top: 1px;
padding: 0.3rem 0.5rem;
background: white;
content: 'count: ' attr(data-count) !important;
}
&:focus::before {
content: none;
}
img {
display: block;
margin: 1rem auto;
max-height: 70vh;
}
}
Loading

0 comments on commit c837a6c

Please sign in to comment.