refactor: prepare saas (#1239)

Reviewed-on: daoyoucloud/tachybase#1239
This commit is contained in:
sealday 2024-06-28 14:54:51 +08:00
parent e10e627d1c
commit c12b30ffef
13 changed files with 554 additions and 12 deletions

View File

@ -331,9 +331,10 @@ export class BuiltInPlugin extends Plugin {
name: 'pinned-list',
config: {
items: {
ui: { order: 100, component: 'DesignableSwitch', pin: true, snippet: 'ui.*' },
pm: { order: 200, component: 'PluginManagerLink', pin: true, snippet: 'pm' },
sc: { order: 300, component: 'SettingsCenterDropdown', pin: true, snippet: 'pm.*' },
wf: { order: 100, component: 'WorkflowLink', pin: true, snippet: 'pm.*' },
ds: { order: 200, component: 'DatasourceLink', pin: true, snippet: 'pm.*' },
pm: { order: 300, component: 'PluginManagerLink', pin: true, snippet: 'pm' },
sc: { order: 400, component: 'SettingsCenterDropdown', pin: true, snippet: 'pm.*' },
},
},
});

View File

@ -5,7 +5,7 @@ import { Plugin } from '../../application/Plugin';
import { BlockTemplatesPane } from '../../schema-templates';
import { SystemSettingsPane } from '../system-settings';
import { PluginManager } from './PluginManager';
import { PluginManagerLink, SettingsCenterDropdown } from './PluginManagerLink';
import { DatasourceLink, PluginManagerLink, SettingsCenterDropdown, WorkflowLink } from './PluginManagerLink';
import { AdminSettingsLayout } from './PluginSetting';
export * from './PluginManager';

View File

@ -1,23 +1,67 @@
import React from 'react';
import { useDesignable } from '@tachybase/client';
import { css, useDesignable } from '@tachybase/client';
import { CalculatorOutlined, CommentOutlined, HighlightOutlined, ToolFilled, ToolOutlined } from '@ant-design/icons';
import { CalculatorOutlined, CommentOutlined, HighlightOutlined, ToolOutlined } from '@ant-design/icons';
import { FloatButton } from 'antd';
import { createPortal } from 'react-dom';
import { useContextMenu } from '../context-menu/useContextMenu';
import { CalculatorWrapper } from './calculator/Calculator';
export const AssistantProvider = ({ children }) => {
const { designable, setDesignable } = useDesignable();
const { contextMenuEnabled, setContextMenuEnable } = useContextMenu();
const ContextMenuIcon = contextMenuEnabled ? ToolFilled : ToolOutlined;
return (
<>
{children}
<FloatButton.Group trigger="hover" type="primary" style={{ right: 24, zIndex: 1250 }} icon={<ToolOutlined />}>
<FloatButton icon={<HighlightOutlined />} onClick={() => setDesignable(!designable)} />
<FloatButton icon={<CalculatorOutlined />} />
<FloatButton.Group trigger="hover" type="default" style={{ right: 24, zIndex: 1250 }} icon={<ToolOutlined />}>
<FloatButton
icon={<HighlightOutlined />}
type={designable ? 'primary' : 'default'}
onClick={() => setDesignable(!designable)}
/>
<FloatButton
icon={<CalculatorOutlined />}
onClick={() => {
createPortal(
// <div
// className={css`
// position: fixed;
// top: 0;
// left: 0;
// width: 1000px;
// height: 1000px;
// z-index: 9999;
// background-color: red;
// `}
// >
// <div />
// {/* <CalculatorWrapper /> */}
// </div>,
<div
className={css`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9000;
display: 'flex';
flex-direction: column;
`}
>
dsfasdfasfasdfadsf
</div>,
document.body,
);
alert('---');
}}
/>
<FloatButton icon={<CommentOutlined />} />
<FloatButton icon={<ContextMenuIcon onClick={() => setContextMenuEnable(!contextMenuEnabled)} />} />
<FloatButton
type={contextMenuEnabled ? 'primary' : 'default'}
icon={<ToolOutlined onClick={() => setContextMenuEnable(!contextMenuEnabled)} />}
/>
</FloatButton.Group>
</>
);

View File

@ -0,0 +1,33 @@
import React, { useEffect, useRef, useState } from 'react';
export const AutoScalingText = ({ children }) => {
const ref = useRef<HTMLDivElement>();
const [scale, setScale] = useState<number>(1);
useEffect(() => {
if (!ref.current) {
return;
}
const node = ref.current;
const parentNode = node.parentNode;
// @ts-ignore
const availableWidth = parentNode.offsetWidth;
const actualWidth = node.offsetWidth;
const actualScale = availableWidth / actualWidth;
if (scale === actualScale) return;
if (actualScale < 1) {
setScale(actualScale);
} else if (scale < 1) {
setScale(1);
}
}, [scale]);
return (
<div className="auto-scaling-text" style={{ transform: `scale(${scale},${scale})` }} ref={ref}>
{children}
</div>
);
};

View File

@ -0,0 +1,247 @@
import React from 'react';
import { CalculatorDisplay } from './CalculatorDisplay';
import { CalculatorKey } from './CalculatorKey';
import { useStyles } from './style';
const CalculatorOperations = {
'/': (prevValue, nextValue) => prevValue / nextValue,
'*': (prevValue, nextValue) => prevValue * nextValue,
'+': (prevValue, nextValue) => prevValue + nextValue,
'-': (prevValue, nextValue) => prevValue - nextValue,
'=': (prevValue, nextValue) => nextValue,
};
class Calculator extends React.Component {
state = {
value: null,
displayValue: '0',
operator: null,
waitingForOperand: false,
};
clearAll() {
this.setState({
value: null,
displayValue: '0',
operator: null,
waitingForOperand: false,
});
}
clearDisplay() {
this.setState({
displayValue: '0',
});
}
clearLastChar() {
const { displayValue } = this.state;
this.setState({
displayValue: displayValue.substring(0, displayValue.length - 1) || '0',
});
}
toggleSign() {
const { displayValue } = this.state;
const newValue = parseFloat(displayValue) * -1;
this.setState({
displayValue: String(newValue),
});
}
inputPercent() {
const { displayValue } = this.state;
const currentValue = parseFloat(displayValue);
if (currentValue === 0) return;
const fixedDigits = displayValue.replace(/^-?\d*\.?/, '');
const newValue = parseFloat(displayValue) / 100;
this.setState({
displayValue: String(newValue.toFixed(fixedDigits.length + 2)),
});
}
inputDot() {
const { displayValue } = this.state;
if (!/\./.test(displayValue)) {
this.setState({
displayValue: displayValue + '.',
waitingForOperand: false,
});
}
}
inputDigit(digit) {
const { displayValue, waitingForOperand } = this.state;
if (waitingForOperand) {
this.setState({
displayValue: String(digit),
waitingForOperand: false,
});
} else {
this.setState({
displayValue: displayValue === '0' ? String(digit) : displayValue + digit,
});
}
}
performOperation(nextOperator) {
const { value, displayValue, operator } = this.state;
const inputValue = parseFloat(displayValue);
if (value == null) {
this.setState({
value: inputValue,
});
} else if (operator) {
const currentValue = value || 0;
const newValue = CalculatorOperations[operator](currentValue, inputValue);
this.setState({
value: newValue,
displayValue: String(newValue),
});
}
this.setState({
waitingForOperand: true,
operator: nextOperator,
});
}
handleKeyDown = (event) => {
let { key } = event;
if (key === 'Enter') key = '=';
if (/\d/.test(key)) {
event.preventDefault();
this.inputDigit(parseInt(key, 10));
} else if (key in CalculatorOperations) {
event.preventDefault();
this.performOperation(key);
} else if (key === '.') {
event.preventDefault();
this.inputDot();
} else if (key === '%') {
event.preventDefault();
this.inputPercent();
} else if (key === 'Backspace') {
event.preventDefault();
this.clearLastChar();
} else if (key === 'Clear') {
event.preventDefault();
if (this.state.displayValue !== '0') {
this.clearDisplay();
} else {
this.clearAll();
}
}
};
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
}
render() {
const { displayValue } = this.state;
const clearDisplay = displayValue !== '0';
const clearText = clearDisplay ? 'C' : 'AC';
return (
<div className={this.props.className}>
<div className="calculator">
<CalculatorDisplay value={displayValue} />
<div className="calculator-keypad">
<div className="input-keys">
<div className="function-keys">
<CalculatorKey
className="key-clear"
onPress={() => (clearDisplay ? this.clearDisplay() : this.clearAll())}
>
{clearText}
</CalculatorKey>
<CalculatorKey className="key-sign" onPress={() => this.toggleSign()}>
±
</CalculatorKey>
<CalculatorKey className="key-percent" onPress={() => this.inputPercent()}>
%
</CalculatorKey>
</div>
<div className="digit-keys">
<CalculatorKey className="key-0" onPress={() => this.inputDigit(0)}>
0
</CalculatorKey>
<CalculatorKey className="key-dot" onPress={() => this.inputDot()}>
</CalculatorKey>
<CalculatorKey className="key-1" onPress={() => this.inputDigit(1)}>
1
</CalculatorKey>
<CalculatorKey className="key-2" onPress={() => this.inputDigit(2)}>
2
</CalculatorKey>
<CalculatorKey className="key-3" onPress={() => this.inputDigit(3)}>
3
</CalculatorKey>
<CalculatorKey className="key-4" onPress={() => this.inputDigit(4)}>
4
</CalculatorKey>
<CalculatorKey className="key-5" onPress={() => this.inputDigit(5)}>
5
</CalculatorKey>
<CalculatorKey className="key-6" onPress={() => this.inputDigit(6)}>
6
</CalculatorKey>
<CalculatorKey className="key-7" onPress={() => this.inputDigit(7)}>
7
</CalculatorKey>
<CalculatorKey className="key-8" onPress={() => this.inputDigit(8)}>
8
</CalculatorKey>
<CalculatorKey className="key-9" onPress={() => this.inputDigit(9)}>
9
</CalculatorKey>
</div>
</div>
<div className="operator-keys">
<CalculatorKey className="key-divide" onPress={() => this.performOperation('/')}>
÷
</CalculatorKey>
<CalculatorKey className="key-multiply" onPress={() => this.performOperation('*')}>
×
</CalculatorKey>
<CalculatorKey className="key-subtract" onPress={() => this.performOperation('-')}>
</CalculatorKey>
<CalculatorKey className="key-add" onPress={() => this.performOperation('+')}>
+
</CalculatorKey>
<CalculatorKey className="key-equals" onPress={() => this.performOperation('=')}>
=
</CalculatorKey>
</div>
</div>
</div>
</div>
);
}
}
export const CalculatorWrapper = () => {
const { styles } = useStyles();
return <Calculator className={styles.container} />;
};

View File

@ -0,0 +1,22 @@
import React from 'react';
import { AutoScalingText } from './AutoScalingText';
export const CalculatorDisplay = ({ value, ...props }) => {
const language = navigator.language || 'en-US';
let formattedValue = parseFloat(value).toLocaleString(language, {
useGrouping: true,
maximumFractionDigits: 6,
});
// Add back missing .0 in e.g. 12.0
const match = value.match(/\.\d*?(0*)$/);
if (match) formattedValue += /[1-9]/.test(match[0]) ? match[1] : match[0];
return (
<div {...props} className="calculator-display">
<AutoScalingText>{formattedValue}</AutoScalingText>
</div>
);
};

View File

@ -0,0 +1,5 @@
import React from 'react';
export const CalculatorKey = ({ onPress, className, ...props }) => {
return <button onClick={onPress} className={`calculator-key ${className}`} {...props} />;
};

View File

@ -0,0 +1,132 @@
import { createStyles } from '@tachybase/client';
export const useStyles = createStyles(({ css }) => {
return {
container: css`
button {
display: block;
background: none;
border: none;
padding: 0;
font-family: inherit;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
button:active {
box-shadow: inset 0px 0px 80px 0px rgba(0, 0, 0, 0.25);
}
#wrapper {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
#app {
width: 320px;
height: 520px;
position: relative;
}
.calculator {
width: 100%;
height: 100%;
background: black;
display: flex;
flex-direction: column;
}
#wrapper .calculator {
box-shadow: 0px 0px 20px 0px #aaa;
}
.calculator-display {
color: white;
background: #1c191c;
line-height: 130px;
font-size: 6em;
flex: 1;
}
.auto-scaling-text {
display: inline-block;
}
.calculator-display .auto-scaling-text {
padding: 0 30px;
position: absolute;
right: 0;
transform-origin: right;
}
.calculator-keypad {
height: 400px;
display: flex;
}
.calculator .input-keys {
width: 240px;
}
.calculator .function-keys {
display: flex;
}
.calculator .digit-keys {
background: #e0e0e7;
display: flex;
flex-direction: row;
flex-wrap: wrap-reverse;
}
.calculator-key {
width: 80px;
height: 80px;
border-top: 1px solid #777;
border-right: 1px solid #666;
text-align: center;
line-height: 80px;
}
.calculator .function-keys .calculator-key {
font-size: 2em;
}
.calculator .function-keys .key-multiply {
line-height: 50px;
}
.calculator .digit-keys .calculator-key {
font-size: 2.25em;
}
.calculator .digit-keys .key-0 {
width: 160px;
text-align: left;
padding-left: 32px;
}
.calculator .digit-keys .key-dot {
padding-top: 1em;
font-size: 0.75em;
}
.calculator .operator-keys .calculator-key {
color: white;
border-right: 0;
font-size: 3em;
}
.calculator .function-keys {
background: linear-gradient(to bottom, rgba(202, 202, 204, 1) 0%, rgba(196, 194, 204, 1) 100%);
}
.calculator .operator-keys {
background: linear-gradient(to bottom, rgba(252, 156, 23, 1) 0%, rgba(247, 126, 27, 1) 100%);
}
`,
};
});

View File

@ -0,0 +1,25 @@
import React from 'react';
import { useToken } from '@tachybase/client';
import { DatabaseOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from '../locale';
export const DatasourceLink = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { token } = useToken();
return (
<Tooltip title={t('Data sources')}>
<Button
icon={<DatabaseOutlined style={{ color: token.colorTextHeaderMenu }} />}
title={t('Data sources')}
onClick={() => {
navigate('/admin/settings/data-source-manager/main/collections');
}}
/>
</Tooltip>
);
};

View File

@ -4,6 +4,7 @@ import { Plugin } from '@tachybase/client';
import { BreadcumbTitle } from './component/BreadcumbTitle';
import { CollectionManagerPage } from './component/CollectionsManager';
import { DatabaseConnectionManagerPane } from './component/DatabaseConnectionManager';
import { DatasourceLink } from './component/DatasourceLink';
import { MainDataSourceManager } from './component/MainDataSourceManager';
import { DataSourcePermissionManager } from './component/PermissionManager';
import { DatabaseConnectionProvider } from './DatabaseConnectionProvider';
@ -16,6 +17,7 @@ export class PluginDataSourceManagerClient extends Plugin {
// 注册组件
this.app.addComponents({
DataSourcePermissionManager,
DatasourceLink,
});
this.app.use(DatabaseConnectionProvider);
this.app.pluginSettingsManager.add(NAMESPACE, {

View File

@ -1,7 +1,11 @@
import { i18n } from '@tachybase/client';
import { i18n, useTranslation as useT } from '@tachybase/client';
export const NAMESPACE = 'data-source-manager';
export function lang(key: string, options = {}) {
return i18n.t(key, { ...options, ns: NAMESPACE });
}
export const useTranslation = (options?) => {
return useT([NAMESPACE, 'client'], options);
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import { Plugin } from '@tachybase/client';
import { Registry } from '@tachybase/utils/client';
import { WorkflowLink } from './components/WorkflowLink';
import { ExecutionPage } from './ExecutionPage';
import { PluginActionTrigger } from './features/action-trigger';
import { PluginAggregate } from './features/aggregate';
@ -127,6 +128,7 @@ export class PluginWorkflow extends Plugin {
addComponents() {
this.app.addComponents({
WorkflowLink,
WorkflowPage,
ExecutionPage,
});

View File

@ -0,0 +1,25 @@
import React from 'react';
import { useToken } from '@tachybase/client';
import { PartitionOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import { useNavigate } from 'react-router-dom';
import { useWorkflowTranslation } from '../locale';
export const WorkflowLink = () => {
const { t } = useWorkflowTranslation();
const navigate = useNavigate();
const { token } = useToken();
return (
<Tooltip title={t('Workflow')}>
<Button
icon={<PartitionOutlined style={{ color: token.colorTextHeaderMenu }} />}
title={t('Workflow')}
onClick={() => {
navigate('/admin/settings/workflow');
}}
/>
</Tooltip>
);
};