mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 10:42:19 +08:00
Merge branch 'next' into develop
This commit is contained in:
commit
9020579b1f
15
CHANGELOG.md
15
CHANGELOG.md
@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
## [v1.2.36-alpha](https://github.com/nocobase/nocobase/compare/v1.2.35-alpha...v1.2.36-alpha) - 2024-08-19
|
||||
|
||||
### Merged
|
||||
|
||||
- 日本語readmeを追加する [`#4971`](https://github.com/nocobase/nocobase/pull/4971)
|
||||
- fix: mysql2 version [`#5082`](https://github.com/nocobase/nocobase/pull/5082)
|
||||
- fix: sorting of Table block data [`#5071`](https://github.com/nocobase/nocobase/pull/5071)
|
||||
- fix: the selected data in the sub table is overwritten by default values [`#5075`](https://github.com/nocobase/nocobase/pull/5075)
|
||||
|
||||
### Commits
|
||||
|
||||
- chore(versions): 😊 publish v1.2.36-alpha [`271a829`](https://github.com/nocobase/nocobase/commit/271a82944ea1fd88ff0f32ce1ff4084a614d693e)
|
||||
- chore: update changelog [`84ca0eb`](https://github.com/nocobase/nocobase/commit/84ca0eb29609d1575874e2392bbe319bad82bf7c)
|
||||
- Update README.ja-JP.md [`d5b8f1f`](https://github.com/nocobase/nocobase/commit/d5b8f1fe22fdfa5dcae556c7b4b69c7fdeb3494f)
|
||||
|
||||
## [v1.2.35-alpha](https://github.com/nocobase/nocobase/compare/v1.2.34-alpha...v1.2.35-alpha) - 2024-08-16
|
||||
|
||||
### Merged
|
||||
|
74
README.ja-JP.md
Normal file
74
README.ja-JP.md
Normal file
@ -0,0 +1,74 @@
|
||||
[English](./README.md) | [简体中文](./README.zh-CN.md) | 日本語
|
||||
|
||||
https://github.com/user-attachments/assets/b11cbb68-76bc-4e8b-a2aa-2a1feed0ab77
|
||||
|
||||
## ご協力ありがとうございます!
|
||||
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
## 最近の重要なリリース
|
||||
- [v1.0.1-alpha.1:ブロックの高さ設定をサポート - 2024/06/07](https://docs-cn.nocobase.com/welcome/changelog/20240607)
|
||||
- [v1.0.0-alpha.15:新しいプラグインの追加、「設定操作」インターフェースの改善 - 2024/05/19](https://docs-cn.nocobase.com/welcome/changelog/20240519)
|
||||
- [v1.0:新しいマイルストーン - 2024/04/28](https://docs-cn.nocobase.com/welcome/release/v1001-changelog)
|
||||
- [v0.21:ブロックのパフォーマンスの最適化 - 2024/03/29](https://docs-cn.nocobase.com/welcome/release/v0210-changelog)
|
||||
- [v0.20:複数のデータソースをサポート - 2024/03/03](https://docs-cn.nocobase.com/welcome/release/v0200-changelog)
|
||||
- [v0.19:アプリケーションフローの最適化 - 2024/01/08](https://blog-cn.nocobase.com/posts/release-v019/)
|
||||
- [v0.18:完全なテストシステムの確立 - 2023/12/21](https://blog-cn.nocobase.com/posts/release-v018/)
|
||||
- [v0.17:新しいSchemaInitializerおよびSchemaSettings - 2023/12/11](https://blog-cn.nocobase.com/posts/release-v017/)
|
||||
- [v0.16:新しいキャッシュモジュール - 2023/11/20](https://blog-cn.nocobase.com/posts/release-v016/)
|
||||
- [v0.15:新しいプラグイン設定センター - 2023/11/13](https://blog-cn.nocobase.com/posts/release-v015/)
|
||||
- [v0.14:新しいプラグインマネージャー、インターフェースを通じたプラグインの追加をサポート - 2023/09/11](https://blog-cn.nocobase.com/posts/release-v014/)
|
||||
- [v0.13: 新しいアプリケーションステートフロー - 2023/08/24](https://blog-cn.nocobase.com/posts/release-v013/)
|
||||
|
||||
## NocoBaseはなに?
|
||||
|
||||
NocoBaseは非常に拡張性の高いオープンソースのノーコード開発プラットフォームです。
|
||||
大量のお時間と資金を投入して開発する必要がなく、NocoBaseをデプロイすることですぐにでもプライベートで制御可能かつ非常に拡張性の高いノーコード開発プラットフォームを構築することができます。
|
||||
|
||||
ホームページ:
|
||||
https://www.nocobase.com/
|
||||
|
||||
オンライン体験:
|
||||
https://demo-cn.nocobase.com/new
|
||||
|
||||
ドキュメント:
|
||||
https://docs-cn.nocobase.com/
|
||||
|
||||
コミュニティ:
|
||||
https://forum.nocobase.com/
|
||||
|
||||
## 他の製品との違い
|
||||
|
||||
### 1. データモデル駆動
|
||||
|
||||
多くのノーコード製品はフォーム、表、またはプロセス駆動型であり、表に項目を追加することでフィールドを新しく作成するなど、ユーザーインターフェース上で直接データ構造を作成します。この方法のメリットは使いやすさですが、機能と柔軟性が制限されており、複雑なシナリオには対応しにくいという欠点があります。
|
||||
|
||||
NocoBaseはデータ構造とユーザーインターフェースを分離する設計理念を採用しており、データテーブルに任意の数や形態のブロック(データビュー)を作成できます。各ブロックには異なるスタイル、テキスト、操作を定義できるため、ノーコードの簡単な操作性とネイティブ開発の柔軟性を両立しています。
|
||||

|
||||
|
||||
### 2. リアルタイムエディタ
|
||||
NocoBaseは複雑で特徴的な業務システムを開発できますが、複雑で専門的な知識は必要としません。ワンクリックで設定オプションをユーザーインターフェースに表示でき、システム設定権限を持つ管理者は、見たままの操作方法でユーザーインターフェースを直接設定できます。
|
||||

|
||||
|
||||
### 3. プラグインによる高拡張性
|
||||
|
||||
NocoBaseはプラグイン化されたアーキテクチャを採用しており、新しい機能はすべてプラグインの開発とインストールによって実現できます。機能の拡張は、スマートフォンにアプリをインストールするのと同じくらい簡単です。
|
||||
|
||||

|
||||
|
||||
## インストール
|
||||
|
||||
NocoBaseは3つのインストール方法をサポートしています:
|
||||
|
||||
- <a target="_blank" href="https://docs-cn.nocobase.com/welcome/getting-started/installation/docker-compose">Dockerインストール(推奨)</a >
|
||||
|
||||
コードを書く必要がないノーコードシナリオに適しています。アップグレード時は最新のイメージをダウンロードして再起動するだけです。
|
||||
|
||||
- <a target="_blank" href="https://docs-cn.nocobase.com/welcome/getting-started/installation/create-nocobase-app">create-nocobase-appインストール</a >
|
||||
|
||||
プロジェクトのビジネスコードが完全に独立しており、ローコード開発をサポートします。
|
||||
|
||||
- <a target="_blank" href="https://docs-cn.nocobase.com/welcome/getting-started/installation/git-clone">Gitソースコードインストール</a >
|
||||
|
||||
最新の未公開バージョンを体験したい場合や、貢献したい場合、ソースコードを変更、デバッグする必要がある場合にこの方法を選択することをお勧めします。この方法は高度な開発技術が必要です。コードが更新された場合、gitフローを使用して最新のコードを取得できます。
|
@ -1,4 +1,4 @@
|
||||
English | [中文](./README.zh-CN.md)
|
||||
English | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
|
||||
|
||||
https://github.com/nocobase/nocobase/assets/1267426/1d6a3979-d1eb-4e50-b726-2f90c3f82eeb
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
[English](./README.md) | 简体中文
|
||||
[English](./README.md) | 简体中文 | [日本語](./README.ja-JP.md)
|
||||
|
||||
https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd173ade553
|
||||
|
||||
|
@ -52,7 +52,6 @@ import {
|
||||
UUIDFieldInterface,
|
||||
NanoidFieldInterface,
|
||||
UnixTimestampFieldInterface,
|
||||
DateFieldInterface,
|
||||
} from './interfaces';
|
||||
import {
|
||||
GeneralCollectionTemplate,
|
||||
@ -174,7 +173,6 @@ export class CollectionPlugin extends Plugin {
|
||||
UUIDFieldInterface,
|
||||
NanoidFieldInterface,
|
||||
UnixTimestampFieldInterface,
|
||||
DateFieldInterface,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,8 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Switch, Radio, Input } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Switch } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
export const TargetKey = () => {
|
||||
return <div>Target key</div>;
|
||||
@ -50,37 +50,3 @@ export const ForeignKey2 = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 自定义 Radio 组件
|
||||
export const CustomRadio = (props) => {
|
||||
const { options, onChange } = props;
|
||||
const [value, setValue] = useState(props.value);
|
||||
useEffect(() => {
|
||||
setValue(['server', 'client'].includes(props.value) ? props.value : 'custom');
|
||||
}, [props.value]);
|
||||
const handleRadioChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
if (e.target.value !== 'custom') {
|
||||
onChange?.(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Radio.Group onChange={handleRadioChange} value={value}>
|
||||
{options.map((option) => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
{option.value === 'custom' && value === 'custom' ? (
|
||||
<Input
|
||||
style={{ width: 200, marginLeft: 10 }}
|
||||
onChange={(e) => {
|
||||
onChange?.(e.target.value);
|
||||
}}
|
||||
value={['server', 'client', 'custom'].includes(props.value) ? null : props.value}
|
||||
/>
|
||||
) : null}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
);
|
||||
};
|
||||
|
@ -1,60 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||
import { dateTimeProps, defaultProps, operators } from './properties';
|
||||
|
||||
export class DateFieldInterface extends CollectionFieldInterface {
|
||||
name = 'date';
|
||||
type = 'object';
|
||||
group = 'datetime';
|
||||
order = 1;
|
||||
title = '{{t("Date")}}';
|
||||
sortable = true;
|
||||
default = {
|
||||
type: 'dateOnly',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
dateOnly: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
availableTypes = ['date', 'dateOnly', 'string'];
|
||||
hasDefaultValue = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
'uiSchema.x-component-props.dateFormat': {
|
||||
type: 'string',
|
||||
title: '{{t("Date format")}}',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-decorator': 'FormItem',
|
||||
default: 'YYYY-MM-DD',
|
||||
enum: [
|
||||
{
|
||||
label: '{{t("Year/Month/Day")}}',
|
||||
value: 'YYYY/MM/DD',
|
||||
},
|
||||
{
|
||||
label: '{{t("Year-Month-Day")}}',
|
||||
value: 'YYYY-MM-DD',
|
||||
},
|
||||
{
|
||||
label: '{{t("Day/Month/Year")}}',
|
||||
value: 'DD/MM/YYYY',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
filterable = {
|
||||
operators: operators.datetime,
|
||||
};
|
||||
titleUsable = true;
|
||||
}
|
@ -19,9 +19,6 @@ export class DatetimeFieldInterface extends CollectionFieldInterface {
|
||||
sortable = true;
|
||||
default = {
|
||||
type: 'date',
|
||||
defaultToCurrentTime: false,
|
||||
onUpdateToCurrentTime: false,
|
||||
timezone: 'server',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'DatePicker',
|
||||
|
@ -46,4 +46,3 @@ export * from './sort';
|
||||
export * from './uuid';
|
||||
export * from './nanoid';
|
||||
export * from './unixTimestamp';
|
||||
export * from './date';
|
||||
|
@ -10,7 +10,6 @@
|
||||
import { Field } from '@formily/core';
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { CustomRadio } from '../components';
|
||||
export * as operators from './operators';
|
||||
|
||||
export const type: ISchema = {
|
||||
@ -226,29 +225,6 @@ export const reverseFieldProperties: Record<string, ISchema> = {
|
||||
};
|
||||
|
||||
export const dateTimeProps: { [key: string]: ISchema } = {
|
||||
timezone: {
|
||||
type: 'string',
|
||||
title: '{{t("Timezone")}}',
|
||||
'x-component': CustomRadio,
|
||||
'x-decorator': 'FormItem',
|
||||
default: 'server',
|
||||
'x-component-props': {
|
||||
options: [
|
||||
{
|
||||
label: '{{t("None")}}',
|
||||
value: 'server',
|
||||
},
|
||||
{
|
||||
label: '{{t("Client\'s time zone")}}',
|
||||
value: 'client',
|
||||
},
|
||||
{
|
||||
label: '{{t("Custom")}}',
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'uiSchema.x-component-props.dateFormat': {
|
||||
type: 'string',
|
||||
title: '{{t("Date format")}}',
|
||||
@ -300,18 +276,6 @@ export const dateTimeProps: { [key: string]: ISchema } = {
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultToCurrentTime: {
|
||||
type: 'boolean',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
'x-content': '{{t("Default value to current time")}}',
|
||||
},
|
||||
onUpdateToCurrentTime: {
|
||||
type: 'boolean',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
'x-content': '{{t("Automatically update timestamp on update")}}',
|
||||
},
|
||||
};
|
||||
|
||||
export const dataSource: ISchema = {
|
||||
|
@ -8,8 +8,8 @@
|
||||
*/
|
||||
|
||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||
import { defaultProps, operators } from './properties';
|
||||
import { CustomRadio } from './components';
|
||||
import { dateTimeProps, defaultProps, operators } from './properties';
|
||||
|
||||
export class UnixTimestampFieldInterface extends CollectionFieldInterface {
|
||||
name = 'unixTimestamp';
|
||||
type = 'object';
|
||||
@ -18,47 +18,21 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface {
|
||||
title = '{{t("Unix Timestamp")}}';
|
||||
sortable = true;
|
||||
default = {
|
||||
type: 'unixTimestamp',
|
||||
accuracy: 'second',
|
||||
timezone: 'server',
|
||||
defaultToCurrentTime: false,
|
||||
onUpdateToCurrentTime: false,
|
||||
type: 'bigInt',
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
'x-component': 'UnixTimestamp',
|
||||
'x-component-props': {
|
||||
accuracy: 'second',
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
availableTypes = ['integer', 'bigInt', 'unixTimestamp'];
|
||||
hasDefaultValue = false;
|
||||
availableTypes = ['integer', 'bigInt'];
|
||||
hasDefaultValue = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
timezone: {
|
||||
type: 'string',
|
||||
title: '{{t("Timezone")}}',
|
||||
'x-component': CustomRadio,
|
||||
'x-decorator': 'FormItem',
|
||||
default: 'server',
|
||||
'x-component-props': {
|
||||
options: [
|
||||
{
|
||||
label: '{{t("None")}}',
|
||||
value: 'server',
|
||||
},
|
||||
{
|
||||
label: '{{t("Client\'s time zone")}}',
|
||||
value: 'client',
|
||||
},
|
||||
{
|
||||
label: 'custom',
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
accuracy: {
|
||||
'uiSchema.x-component-props.accuracy': {
|
||||
type: 'string',
|
||||
title: '{{t("Accuracy")}}',
|
||||
'x-component': 'Radio.Group',
|
||||
@ -69,20 +43,6 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface {
|
||||
{ value: 'second', label: '{{t("Second")}}' },
|
||||
],
|
||||
},
|
||||
defaultToCurrentTime: {
|
||||
type: 'boolean',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
'x-content': '{{t("Default value to current time")}}',
|
||||
default: true,
|
||||
},
|
||||
onUpdateToCurrentTime: {
|
||||
type: 'boolean',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
'x-content': '{{t("Automatically update timestamp on update")}}',
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
filterable = {
|
||||
operators: operators.number,
|
||||
|
@ -283,7 +283,7 @@
|
||||
"Checkbox group": "复选框",
|
||||
"China region": "中国行政区",
|
||||
"Date & Time": "日期 & 时间",
|
||||
"Datetime": "日期时间",
|
||||
"Datetime": "日期",
|
||||
"Relation": "关系类型",
|
||||
"Link to": "关联",
|
||||
"Link to description": "用于快速创建表关系,可兼容大多数普通场景。适合非开发人员使用。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,将同时在目标数据表中生成当前数据表的关联字段。",
|
||||
@ -928,7 +928,7 @@
|
||||
"Unix Timestamp": "Unix 时间戳",
|
||||
"Field value do not meet the requirements": "字符不符合要求",
|
||||
"Field value size is": "字符长度要求",
|
||||
"Style": "风格",
|
||||
"Style": "样式",
|
||||
"Unit conversion": "单位换算",
|
||||
"Separator": "分隔符",
|
||||
"Prefix": "前缀",
|
||||
@ -967,8 +967,5 @@
|
||||
"Clear default value": "清除默认值",
|
||||
"Open in new window": "新窗口打开",
|
||||
"Sorry, the page you visited does not exist.": "抱歉,你访问的页面不存在。",
|
||||
"Template engine": "模板引擎",
|
||||
"Default value to current time":"设置字段默认值为当前时间",
|
||||
"Automatically update timestamp on update":"当记录更新时自动设置字段值为当前时间",
|
||||
"Client's time zone":"客户端时区"
|
||||
"Set Template Engine": "设置模板引擎"
|
||||
}
|
||||
|
@ -78,21 +78,18 @@ export const mapDatePicker = function () {
|
||||
return (props: any) => {
|
||||
const format = getDefaultFormat(props) as any;
|
||||
const onChange = props.onChange;
|
||||
|
||||
return {
|
||||
...props,
|
||||
format: format,
|
||||
value: str2moment(props.value, props),
|
||||
onChange: (value: Dayjs | null, dateString) => {
|
||||
onChange: (value: Dayjs | null) => {
|
||||
if (onChange) {
|
||||
if (!props.showTime && value) {
|
||||
value = value.startOf('day');
|
||||
}
|
||||
if (props.dateOnly) {
|
||||
onChange(dateString);
|
||||
} else {
|
||||
onChange(moment2str(value, props));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -8,32 +8,57 @@
|
||||
*/
|
||||
|
||||
import { connect, mapReadPretty } from '@formily/react';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { DatePicker } from '../date-picker';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const toValue = (value: any, accuracy) => {
|
||||
if (value) {
|
||||
return timestampToDate(value, accuracy);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
function timestampToDate(timestamp, accuracy = 'millisecond') {
|
||||
if (accuracy === 'second') {
|
||||
timestamp *= 1000; // 如果精确度是秒级,则将时间戳乘以1000转换为毫秒级
|
||||
}
|
||||
return dayjs(timestamp);
|
||||
}
|
||||
|
||||
function getTimestamp(date, accuracy = 'millisecond') {
|
||||
if (accuracy === 'second') {
|
||||
return dayjs(date).unix();
|
||||
} else {
|
||||
return dayjs(date).valueOf(); // 默认返回毫秒级时间戳
|
||||
}
|
||||
}
|
||||
|
||||
interface UnixTimestampProps {
|
||||
value?: any;
|
||||
value?: number;
|
||||
accuracy?: 'millisecond' | 'second';
|
||||
onChange?: (value: number) => void;
|
||||
}
|
||||
|
||||
export const UnixTimestamp = connect(
|
||||
(props: UnixTimestampProps) => {
|
||||
const { value, onChange } = props;
|
||||
|
||||
const { value, onChange, accuracy = 'second' } = props;
|
||||
const v = useMemo(() => toValue(value, accuracy), [value, accuracy]);
|
||||
return (
|
||||
<DatePicker
|
||||
{...props}
|
||||
value={value}
|
||||
value={v}
|
||||
onChange={(v: any) => {
|
||||
if (onChange) {
|
||||
onChange(v);
|
||||
onChange(getTimestamp(v, accuracy));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
mapReadPretty((props) => {
|
||||
const { value } = props;
|
||||
return <DatePicker.ReadPretty {...props} value={value} />;
|
||||
const { value, accuracy = 'second' } = props;
|
||||
const v = useMemo(() => toValue(value, accuracy), [value, accuracy]);
|
||||
return <DatePicker.ReadPretty {...props} value={v} />;
|
||||
}),
|
||||
);
|
||||
|
@ -13,9 +13,11 @@ import { UnixTimestamp } from '@nocobase/client';
|
||||
describe('UnixTimestamp', () => {
|
||||
it('renders without errors', async () => {
|
||||
const { container } = await renderAppOptions({
|
||||
Component: UnixTimestamp as any,
|
||||
props: {},
|
||||
value: null,
|
||||
Component: UnixTimestamp,
|
||||
props: {
|
||||
accuracy: 'millisecond',
|
||||
},
|
||||
value: 0,
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
@ -67,10 +69,78 @@ describe('UnixTimestamp', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('millisecond', async () => {
|
||||
await renderAppOptions({
|
||||
Component: UnixTimestamp,
|
||||
value: 1712819630000,
|
||||
props: {
|
||||
accuracy: 'millisecond',
|
||||
},
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox')).toHaveValue('2024-04-11');
|
||||
});
|
||||
});
|
||||
|
||||
it('second', async () => {
|
||||
await renderAppOptions({
|
||||
Component: UnixTimestamp,
|
||||
value: 1712819630,
|
||||
props: {
|
||||
accuracy: 'second',
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox')).toHaveValue('2024-04-11');
|
||||
});
|
||||
});
|
||||
|
||||
it('string', async () => {
|
||||
await renderAppOptions({
|
||||
Component: UnixTimestamp,
|
||||
value: '2024-04-11',
|
||||
props: {
|
||||
accuracy: 'millisecond',
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox')).toHaveValue('2024-04-11');
|
||||
});
|
||||
});
|
||||
|
||||
it('change', async () => {
|
||||
const onChange = vitest.fn();
|
||||
await renderAppOptions({
|
||||
Component: UnixTimestamp,
|
||||
value: '2024-04-11',
|
||||
onChange,
|
||||
props: {
|
||||
accuracy: 'millisecond',
|
||||
},
|
||||
});
|
||||
await userEvent.click(screen.getByRole('textbox'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('td[title="2024-04-12"]'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox')).toHaveValue('2024-04-12');
|
||||
});
|
||||
expect(onChange).toBeCalledWith(1712880000000);
|
||||
});
|
||||
|
||||
it('read pretty', async () => {
|
||||
const { container } = await renderReadPrettyApp({
|
||||
Component: UnixTimestamp as any,
|
||||
Component: UnixTimestamp,
|
||||
value: '2024-04-11',
|
||||
props: {
|
||||
accuracy: 'millisecond',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText('2024-04-11')).toBeInTheDocument();
|
||||
|
@ -1,42 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Database, mockDatabase } from '@nocobase/database';
|
||||
|
||||
describe('date only', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase({
|
||||
timezone: '+08:00',
|
||||
});
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should set date field with dateOnly', async () => {
|
||||
db.collection({
|
||||
name: 'tests',
|
||||
fields: [{ name: 'date1', type: 'dateOnly' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const item = await db.getRepository('tests').create({
|
||||
values: {
|
||||
date1: '2023-03-24',
|
||||
},
|
||||
});
|
||||
|
||||
expect(item.get('date1')).toBe('2023-03-24');
|
||||
});
|
||||
});
|
@ -11,69 +11,6 @@ import { mockDatabase } from '../';
|
||||
import { Database } from '../../database';
|
||||
import { Repository } from '../../repository';
|
||||
|
||||
describe('timezone', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase({
|
||||
timezone: '+08:00',
|
||||
});
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
describe('timezone', () => {
|
||||
test('custom', async () => {
|
||||
db.collection({
|
||||
name: 'tests',
|
||||
timestamps: false,
|
||||
fields: [{ name: 'date1', type: 'date', timezone: '+06:00' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
const repository = db.getRepository('tests');
|
||||
const instance = await repository.create({ values: { date1: '2023-03-24 00:00:00' } });
|
||||
const date1 = instance.get('date1');
|
||||
expect(date1.toISOString()).toEqual('2023-03-23T18:00:00.000Z');
|
||||
});
|
||||
|
||||
test('client', async () => {
|
||||
db.collection({
|
||||
name: 'tests',
|
||||
timestamps: false,
|
||||
fields: [{ name: 'date1', type: 'date', timezone: 'client' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
const repository = db.getRepository('tests');
|
||||
const instance = await repository.create({
|
||||
values: { date1: '2023-03-24 01:00:00' },
|
||||
context: {
|
||||
timezone: '+01:00',
|
||||
},
|
||||
});
|
||||
const date1 = instance.get('date1');
|
||||
expect(date1.toISOString()).toEqual('2023-03-24T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('server', async () => {
|
||||
db.collection({
|
||||
name: 'tests',
|
||||
fields: [{ name: 'date1', type: 'date', timezone: 'server' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
const repository = db.getRepository('tests');
|
||||
const instance = await repository.create({ values: { date1: '2023-03-24 08:00:00' } });
|
||||
const date1 = instance.get('date1');
|
||||
expect(date1.toISOString()).toEqual('2023-03-24T00:00:00.000Z');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('date-field', () => {
|
||||
let db: Database;
|
||||
let repository: Repository;
|
||||
@ -93,71 +30,6 @@ describe('date-field', () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should set default to current time', async () => {
|
||||
const c1 = db.collection({
|
||||
name: 'test11',
|
||||
fields: [
|
||||
{
|
||||
name: 'date1',
|
||||
type: 'date',
|
||||
defaultToCurrentTime: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const instance = await c1.repository.create({});
|
||||
const date1 = instance.get('date1');
|
||||
expect(date1).toBeDefined();
|
||||
});
|
||||
|
||||
it('should set to current time when update', async () => {
|
||||
const c1 = db.collection({
|
||||
name: 'test11',
|
||||
fields: [
|
||||
{
|
||||
name: 'date1',
|
||||
type: 'date',
|
||||
onUpdateToCurrentTime: true,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const instance = await c1.repository.create({
|
||||
values: {
|
||||
title: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
const date1Val = instance.get('date1');
|
||||
expect(date1Val).toBeDefined();
|
||||
|
||||
console.log('update');
|
||||
await c1.repository.update({
|
||||
values: {
|
||||
title: 'test2',
|
||||
},
|
||||
filter: {
|
||||
id: instance.get('id'),
|
||||
},
|
||||
});
|
||||
|
||||
await instance.reload();
|
||||
|
||||
const date1Val2 = instance.get('date1');
|
||||
expect(date1Val2).toBeDefined();
|
||||
|
||||
expect(date1Val2.getTime()).toBeGreaterThan(date1Val.getTime());
|
||||
});
|
||||
|
||||
test('create', async () => {
|
||||
const createExpectToBe = async (key, actual, expected) => {
|
||||
const instance = await repository.create({
|
||||
values: {
|
||||
@ -167,6 +39,7 @@ describe('date-field', () => {
|
||||
return expect(instance.get(key).toISOString()).toEqual(expected);
|
||||
};
|
||||
|
||||
test('create', async () => {
|
||||
// sqlite 时区不能自定义,只有 +00:00,postgres 和 mysql 可以自定义 DB_TIMEZONE
|
||||
await createExpectToBe('date1', '2023-03-24', '2023-03-24T00:00:00.000Z');
|
||||
await createExpectToBe('date1', '2023-03-24T16:00:00.000Z', '2023-03-24T16:00:00.000Z');
|
||||
|
@ -1,86 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Database, mockDatabase } from '@nocobase/database';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('unix timestamp field', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should set default to current time', async () => {
|
||||
const c1 = db.collection({
|
||||
name: 'test11',
|
||||
fields: [
|
||||
{
|
||||
name: 'date1',
|
||||
type: 'unixTimestamp',
|
||||
defaultToCurrentTime: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const instance = await c1.repository.create({});
|
||||
const date1 = instance.get('date1');
|
||||
expect(date1).toBeDefined();
|
||||
|
||||
console.log(instance.toJSON());
|
||||
});
|
||||
|
||||
it('should set date value', async () => {
|
||||
const c1 = db.collection({
|
||||
name: 'test12',
|
||||
fields: [
|
||||
{
|
||||
name: 'date1',
|
||||
type: 'unixTimestamp',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
await c1.repository.create({
|
||||
values: {
|
||||
date1: '2021-01-01T00:00:00Z',
|
||||
},
|
||||
});
|
||||
|
||||
const item = await c1.repository.findOne();
|
||||
const val = item.get('date1');
|
||||
const date = moment(val).utc().format('YYYY-MM-DD HH:mm:ss');
|
||||
expect(date).toBe('2021-01-01 00:00:00');
|
||||
});
|
||||
|
||||
describe('timezone', () => {
|
||||
test('custom', async () => {
|
||||
db.collection({
|
||||
name: 'tests',
|
||||
timestamps: false,
|
||||
fields: [{ name: 'date1', type: 'unixTimestamp', timezone: '+06:00' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
const repository = db.getRepository('tests');
|
||||
const instance = await repository.create({ values: { date1: '2023-03-24 00:00:00' } });
|
||||
const date1 = instance.get('date1');
|
||||
expect(date1.toISOString()).toEqual('2023-03-23T18:00:00.000Z');
|
||||
});
|
||||
});
|
||||
});
|
@ -34,6 +34,7 @@ import {
|
||||
import { SequelizeStorage, Umzug } from 'umzug';
|
||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||
import { CollectionFactory } from './collection-factory';
|
||||
import { CollectionGroupManager } from './collection-group-manager';
|
||||
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
||||
import DatabaseUtils from './database-utils';
|
||||
import ReferencesMap from './features/references-map';
|
||||
@ -41,6 +42,7 @@ import { referentialIntegrityCheck } from './features/referential-integrity-chec
|
||||
import { ArrayFieldRepository } from './field-repository/array-field-repository';
|
||||
import * as FieldTypes from './fields';
|
||||
import { Field, FieldContext, RelationField } from './fields';
|
||||
import { checkDatabaseVersion } from './helpers';
|
||||
import { InheritedCollection } from './inherited-collection';
|
||||
import InheritanceMap from './inherited-map';
|
||||
import { InterfaceManager } from './interface-manager';
|
||||
@ -219,9 +221,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
opts.rawTimezone = opts.timezone;
|
||||
|
||||
if (options.dialect === 'sqlite') {
|
||||
delete opts.timezone;
|
||||
} else if (!opts.timezone) {
|
||||
@ -852,8 +851,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
* @internal
|
||||
*/
|
||||
async checkVersion() {
|
||||
return true;
|
||||
// return await checkDatabaseVersion(this);
|
||||
return await checkDatabaseVersion(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,14 +10,8 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
const datetimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
||||
|
||||
function isValidDatetime(str) {
|
||||
return datetimeRegex.test(str);
|
||||
}
|
||||
|
||||
export class DateField extends Field {
|
||||
get dataType(): any {
|
||||
get dataType() {
|
||||
return DataTypes.DATE(3);
|
||||
}
|
||||
|
||||
@ -39,59 +33,6 @@ export class DateField extends Field {
|
||||
return props.gmt;
|
||||
}
|
||||
|
||||
init() {
|
||||
const { name, defaultToCurrentTime, onUpdateToCurrentTime, timezone } = this.options;
|
||||
|
||||
this.resolveTimeZone = (context) => {
|
||||
// @ts-ignore
|
||||
const serverTimeZone = this.database.options.rawTimezone;
|
||||
if (timezone === 'server') {
|
||||
return serverTimeZone;
|
||||
}
|
||||
|
||||
if (timezone === 'client') {
|
||||
return context?.timezone || serverTimeZone;
|
||||
}
|
||||
|
||||
if (timezone) {
|
||||
return timezone;
|
||||
}
|
||||
|
||||
return serverTimeZone;
|
||||
};
|
||||
|
||||
this.beforeSave = async (instance, options) => {
|
||||
const value = instance.get(name);
|
||||
|
||||
if (!value && instance.isNewRecord && defaultToCurrentTime) {
|
||||
instance.set(name, new Date());
|
||||
return;
|
||||
}
|
||||
|
||||
if (onUpdateToCurrentTime) {
|
||||
instance.set(name, new Date());
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setter(value, options) {
|
||||
if (value === null) {
|
||||
return value;
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && isValidDatetime(value)) {
|
||||
const dateTimezone = this.resolveTimeZone(options?.context);
|
||||
const dateString = `${value} ${dateTimezone}`;
|
||||
return new Date(dateString);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
bind() {
|
||||
super.bind();
|
||||
|
||||
@ -110,13 +51,6 @@ export class DateField extends Field {
|
||||
// @ts-ignore
|
||||
model.refreshAttributes();
|
||||
}
|
||||
|
||||
this.on('beforeSave', this.beforeSave);
|
||||
}
|
||||
|
||||
unbind() {
|
||||
super.unbind();
|
||||
this.off('beforeSave', this.beforeSave);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,21 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
export class DateOnlyField extends Field {
|
||||
get dataType(): any {
|
||||
return DataTypes.DATEONLY;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DateOnlyFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'dateOnly';
|
||||
}
|
@ -56,7 +56,7 @@ export abstract class Field {
|
||||
return this.options.type;
|
||||
}
|
||||
|
||||
abstract get dataType(): any;
|
||||
abstract get dataType();
|
||||
|
||||
isRelationField() {
|
||||
return false;
|
||||
@ -171,13 +171,11 @@ export abstract class Field {
|
||||
Object.assign(opts, { type: this.database.sequelize.normalizeDataType(this.dataType) });
|
||||
}
|
||||
|
||||
Object.assign(opts, this.additionalSequelizeOptions());
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
additionalSequelizeOptions() {
|
||||
return {};
|
||||
isSqlite() {
|
||||
return this.database.sequelize.getDialect() === 'sqlite';
|
||||
}
|
||||
|
||||
typeToString() {
|
||||
|
@ -36,8 +36,6 @@ import { UUIDFieldOptions } from './uuid-field';
|
||||
import { VirtualFieldOptions } from './virtual-field';
|
||||
import { NanoidFieldOptions } from './nanoid-field';
|
||||
import { EncryptionField } from './encryption-field';
|
||||
import { UnixTimestampFieldOptions } from './unix-timestamp-field';
|
||||
import { DateOnlyFieldOptions } from './date-only-field';
|
||||
|
||||
export * from './array-field';
|
||||
export * from './belongs-to-field';
|
||||
@ -45,7 +43,6 @@ export * from './belongs-to-many-field';
|
||||
export * from './boolean-field';
|
||||
export * from './context-field';
|
||||
export * from './date-field';
|
||||
export * from './date-only-field';
|
||||
export * from './field';
|
||||
export * from './has-many-field';
|
||||
export * from './has-one-field';
|
||||
@ -64,7 +61,6 @@ export * from './uuid-field';
|
||||
export * from './virtual-field';
|
||||
export * from './nanoid-field';
|
||||
export * from './encryption-field';
|
||||
export * from './unix-timestamp-field';
|
||||
|
||||
export type FieldOptions =
|
||||
| BaseFieldOptions
|
||||
@ -85,8 +81,6 @@ export type FieldOptions =
|
||||
| SetFieldOptions
|
||||
| TimeFieldOptions
|
||||
| DateFieldOptions
|
||||
| DateOnlyFieldOptions
|
||||
| UnixTimestampFieldOptions
|
||||
| UidFieldOptions
|
||||
| UUIDFieldOptions
|
||||
| NanoidFieldOptions
|
||||
|
@ -1,60 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { DateField } from './date-field';
|
||||
import { BaseColumnFieldOptions } from './field';
|
||||
|
||||
export class UnixTimestampField extends DateField {
|
||||
get dataType() {
|
||||
return DataTypes.BIGINT;
|
||||
}
|
||||
|
||||
additionalSequelizeOptions(): {} {
|
||||
const { name } = this.options;
|
||||
let { accuracy } = this.options;
|
||||
|
||||
if (this.options?.uiSchema['x-component-props']?.accuracy) {
|
||||
accuracy = this.options?.uiSchema['x-component-props']?.accuracy;
|
||||
}
|
||||
|
||||
if (!accuracy) {
|
||||
accuracy = 'second';
|
||||
}
|
||||
|
||||
let rationalNumber = 1000;
|
||||
|
||||
if (accuracy === 'millisecond') {
|
||||
rationalNumber = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
get() {
|
||||
const value = this.getDataValue(name);
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return new Date(value * rationalNumber);
|
||||
},
|
||||
set(value) {
|
||||
if (value === null || value === undefined) {
|
||||
this.setDataValue(name, value);
|
||||
} else {
|
||||
// date to unix timestamp
|
||||
this.setDataValue(name, Math.floor(new Date(value).getTime() / rationalNumber));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface UnixTimestampFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'unix-timestamp';
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import lodash from 'lodash';
|
||||
import lodash, { isPlainObject } from 'lodash';
|
||||
import { Model as SequelizeModel, ModelStatic } from 'sequelize';
|
||||
import { Collection } from './collection';
|
||||
import { Database } from './database';
|
||||
@ -50,21 +50,6 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
|
||||
return await runner.runSync(options);
|
||||
}
|
||||
|
||||
static callSetters(values, options) {
|
||||
// map values
|
||||
const result = {};
|
||||
for (const key of Object.keys(values)) {
|
||||
const field = this.collection.getField(key);
|
||||
if (field && field.setter) {
|
||||
result[key] = field.setter.call(field, values[key], options, values, key);
|
||||
} else {
|
||||
result[key] = values[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO
|
||||
public toChangedWithAssociations() {
|
||||
// @ts-ignore
|
||||
|
@ -573,7 +573,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
underscored: this.collection.options.underscored,
|
||||
});
|
||||
|
||||
const values = (this.model as typeof Model).callSetters(guard.sanitize(options.values || {}), options);
|
||||
const values = guard.sanitize(options.values || {});
|
||||
|
||||
const instance = await this.model.create<any>(values, {
|
||||
...options,
|
||||
@ -645,7 +645,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
|
||||
const guard = UpdateGuard.fromOptions(this.model, { ...options, underscored: this.collection.options.underscored });
|
||||
|
||||
const values = (this.model as typeof Model).callSetters(guard.sanitize(options.values || {}), options);
|
||||
const values = guard.sanitize(options.values);
|
||||
|
||||
// NOTE:
|
||||
// 1. better to be moved to separated API like bulkUpdate/updateMany
|
||||
|
@ -18,8 +18,8 @@ const postgres = {
|
||||
name: 'string',
|
||||
|
||||
smallint: ['integer', 'sort'],
|
||||
integer: ['integer', 'unixTimestamp', 'sort'],
|
||||
bigint: ['bigInt', 'unixTimestamp', 'sort'],
|
||||
integer: ['integer', 'sort'],
|
||||
bigint: ['bigInt', 'sort'],
|
||||
decimal: 'decimal',
|
||||
numeric: 'float',
|
||||
real: 'float',
|
||||
@ -61,11 +61,11 @@ const mysql = {
|
||||
text: 'text',
|
||||
mediumtext: 'text',
|
||||
longtext: 'text',
|
||||
int: ['integer', 'unixTimestamp', 'sort'],
|
||||
'int unsigned': ['integer', 'unixTimestamp', 'sort'],
|
||||
integer: ['integer', 'unixTimestamp', 'sort'],
|
||||
bigint: ['bigInt', 'unixTimestamp', 'sort'],
|
||||
'bigint unsigned': ['bigInt', 'unixTimestamp', 'sort'],
|
||||
int: ['integer', 'sort'],
|
||||
'int unsigned': ['integer', 'sort'],
|
||||
integer: ['integer', 'sort'],
|
||||
bigint: ['bigInt', 'sort'],
|
||||
'bigint unsigned': ['bigInt', 'sort'],
|
||||
float: 'float',
|
||||
double: 'float',
|
||||
boolean: 'boolean',
|
||||
|
@ -224,12 +224,18 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
public requestLogger: Logger;
|
||||
protected plugins = new Map<string, Plugin>();
|
||||
protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance();
|
||||
protected _started: Date | null = null;
|
||||
private _authenticated = false;
|
||||
private _maintaining = false;
|
||||
private _maintainingCommandStatus: MaintainingCommandStatus;
|
||||
private _maintainingStatusBeforeCommand: MaintainingCommandStatus | null;
|
||||
private _actionCommand: Command;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private sqlLogger: Logger;
|
||||
protected _logger: SystemLogger;
|
||||
|
||||
constructor(public options: ApplicationOptions) {
|
||||
super();
|
||||
@ -242,8 +248,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
}
|
||||
}
|
||||
|
||||
protected _started: Date | null = null;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
@ -251,8 +255,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this._started;
|
||||
}
|
||||
|
||||
protected _logger: SystemLogger;
|
||||
|
||||
get logger() {
|
||||
return this._logger;
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { getApp } from '.';
|
||||
import { FILE_FIELD_NAME, FILE_SIZE_LIMIT_DEFAULT, STORAGE_TYPE_LOCAL } from '../../constants';
|
||||
import PluginFileManagerServer from '../server';
|
||||
|
||||
const { LOCAL_STORAGE_BASE_URL, LOCAL_STORAGE_DEST = 'storage/uploads', APP_PORT = '13000' } = process.env;
|
||||
|
||||
@ -50,6 +51,72 @@ describe('action', () => {
|
||||
|
||||
describe('create / upload', () => {
|
||||
describe('default storage', () => {
|
||||
it('should be create file record', async () => {
|
||||
const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
|
||||
const model = await Plugin.createFileRecord({
|
||||
collectionName: 'attachments',
|
||||
filePath: path.resolve(__dirname, './files/text.txt'),
|
||||
});
|
||||
const matcher = {
|
||||
title: 'text',
|
||||
extname: '.txt',
|
||||
path: '',
|
||||
// size: 13,
|
||||
meta: {},
|
||||
storageId: 1,
|
||||
};
|
||||
expect(model.toJSON()).toMatchObject(matcher);
|
||||
});
|
||||
|
||||
it('should be local2 storage', async () => {
|
||||
const storage = await StorageRepo.create({
|
||||
values: {
|
||||
name: 'local2',
|
||||
type: STORAGE_TYPE_LOCAL,
|
||||
baseUrl: DEFAULT_LOCAL_BASE_URL,
|
||||
rules: {
|
||||
size: 1024,
|
||||
},
|
||||
paranoid: true,
|
||||
},
|
||||
});
|
||||
const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
|
||||
const model = await Plugin.createFileRecord({
|
||||
collectionName: 'attachments',
|
||||
storageName: 'local2',
|
||||
filePath: path.resolve(__dirname, './files/text.txt'),
|
||||
});
|
||||
const matcher = {
|
||||
title: 'text',
|
||||
extname: '.txt',
|
||||
path: '',
|
||||
// size: 13,
|
||||
meta: {},
|
||||
storageId: storage.id,
|
||||
};
|
||||
expect(model.toJSON()).toMatchObject(matcher);
|
||||
});
|
||||
|
||||
it.only('should be custom values', async () => {
|
||||
const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
|
||||
const model = await Plugin.createFileRecord({
|
||||
collectionName: 'attachments',
|
||||
filePath: path.resolve(__dirname, './files/text.txt'),
|
||||
values: {
|
||||
size: 22,
|
||||
},
|
||||
});
|
||||
const matcher = {
|
||||
title: 'text',
|
||||
extname: '.txt',
|
||||
path: '',
|
||||
size: 22,
|
||||
meta: {},
|
||||
storageId: 1,
|
||||
};
|
||||
expect(model.toJSON()).toMatchObject(matcher);
|
||||
});
|
||||
|
||||
it('upload file should be ok', async () => {
|
||||
const { body } = await agent.resource('attachments').create({
|
||||
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
|
||||
|
@ -11,15 +11,15 @@ import { Context, Next } from '@nocobase/actions';
|
||||
import { koaMulter as multer } from '@nocobase/utils';
|
||||
import Path from 'path';
|
||||
|
||||
import Plugin from '..';
|
||||
import {
|
||||
FILE_FIELD_NAME,
|
||||
FILE_SIZE_LIMIT_DEFAULT,
|
||||
FILE_SIZE_LIMIT_MAX,
|
||||
FILE_FIELD_NAME,
|
||||
LIMIT_FILES,
|
||||
FILE_SIZE_LIMIT_MIN,
|
||||
LIMIT_FILES,
|
||||
} from '../../constants';
|
||||
import * as Rules from '../rules';
|
||||
import Plugin from '..';
|
||||
|
||||
// TODO(optimize): 需要优化错误处理,计算失败后需要抛出对应错误,以便程序处理
|
||||
function getFileFilter(storage) {
|
||||
@ -33,7 +33,7 @@ function getFileFilter(storage) {
|
||||
};
|
||||
}
|
||||
|
||||
function getFileData(ctx: Context) {
|
||||
export function getFileData(ctx: Context) {
|
||||
const { [FILE_FIELD_NAME]: file, storage } = ctx;
|
||||
if (!file) {
|
||||
return ctx.throw(400, 'file validation failed');
|
||||
|
@ -7,14 +7,17 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { Registry } from '@nocobase/utils';
|
||||
|
||||
import { basename, resolve } from 'path';
|
||||
|
||||
import { Transactionable } from '@nocobase/database';
|
||||
import fs from 'fs';
|
||||
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
|
||||
import { FileModel } from './FileModel';
|
||||
import initActions from './actions';
|
||||
import { getFileData } from './actions/attachments';
|
||||
import { AttachmentInterface } from './interfaces/attachment-interface';
|
||||
import { IStorage, StorageModel } from './storages';
|
||||
import StorageTypeAliOss from './storages/ali-oss';
|
||||
@ -26,10 +29,78 @@ export type * from './storages';
|
||||
|
||||
const DEFAULT_STORAGE_TYPE = STORAGE_TYPE_LOCAL;
|
||||
|
||||
export type FileRecordOptions = {
|
||||
collectionName: string;
|
||||
filePath: string;
|
||||
storageName?: string;
|
||||
values?: any;
|
||||
} & Transactionable;
|
||||
|
||||
export default class PluginFileManagerServer extends Plugin {
|
||||
storageTypes = new Registry<IStorage>();
|
||||
storagesCache = new Map<number, StorageModel>();
|
||||
|
||||
async createFileRecord(options: FileRecordOptions) {
|
||||
const { values, storageName, collectionName, filePath, transaction } = options;
|
||||
const collection = this.db.getCollection(collectionName);
|
||||
if (!collection) {
|
||||
throw new Error(`collection does not exist`);
|
||||
}
|
||||
const storageRepository = this.db.getRepository('storages');
|
||||
const collectionRepository = this.db.getRepository(collectionName);
|
||||
const name = storageName || collection.options.storage;
|
||||
|
||||
let storageInstance;
|
||||
if (name) {
|
||||
storageInstance = await storageRepository.findOne({
|
||||
filter: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!storageInstance) {
|
||||
storageInstance = await storageRepository.findOne({
|
||||
filter: {
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
|
||||
if (!storageInstance) {
|
||||
throw new Error('[file-manager] no linked or default storage provided');
|
||||
}
|
||||
|
||||
const storageConfig = this.storageTypes.get(storageInstance.type);
|
||||
|
||||
if (!storageConfig) {
|
||||
throw new Error(`[file-manager] storage type "${storageInstance.type}" is not defined`);
|
||||
}
|
||||
|
||||
const engine = storageConfig.make(storageInstance);
|
||||
|
||||
const file = {
|
||||
originalname: basename(filePath),
|
||||
path: filePath,
|
||||
stream: fileStream,
|
||||
} as any;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
engine._handleFile({} as any, file, (error, info) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
Object.assign(file, info);
|
||||
resolve(info);
|
||||
});
|
||||
});
|
||||
|
||||
const data = getFileData({ app: this.app, file, storage: storageInstance, request: { body: {} } } as any);
|
||||
return await collectionRepository.create({ values: { ...data, ...values }, transaction });
|
||||
}
|
||||
|
||||
async loadStorages(options?: { transaction: any }) {
|
||||
const repository = this.db.getRepository('storages');
|
||||
const storages = await repository.find({
|
||||
|
@ -7,14 +7,14 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from 'react';
|
||||
import { QRCode, Popover, Button } from 'antd';
|
||||
import { QrcodeOutlined } from '@ant-design/icons';
|
||||
import { Button, Popover, QRCode } from 'antd';
|
||||
import React, { FC, useState } from 'react';
|
||||
|
||||
import { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';
|
||||
import { css, DesignableSwitch, Icon, useApp } from '@nocobase/client';
|
||||
import { usePluginTranslation } from '../locale';
|
||||
import { useSize } from './sizeContext';
|
||||
import { css, DesignableSwitch, Icon } from '@nocobase/client';
|
||||
import { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';
|
||||
|
||||
const PadSvg = () => (
|
||||
<svg
|
||||
@ -58,6 +58,7 @@ const MobileIcon = (props: Partial<CustomIconComponentProps>) => <Icon type={''}
|
||||
|
||||
export const DesktopModeHeader: FC = () => {
|
||||
const { t } = usePluginTranslation();
|
||||
const app = useApp();
|
||||
const { setSize } = useSize();
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleQrcodeOpen = (newOpen: boolean) => {
|
||||
@ -93,7 +94,7 @@ export const DesktopModeHeader: FC = () => {
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Button style={{ color: 'white' }} href="/admin">
|
||||
<Button style={{ color: 'white' }} href={app.getRouteUrl('/admin')}>
|
||||
{t('Back')}
|
||||
</Button>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
|
@ -7,19 +7,19 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { SafeArea } from 'antd-mobile';
|
||||
import 'antd-mobile/es/components/tab-bar/tab-bar.css';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { useStyles } from './styles';
|
||||
import { useMobileRoutes } from '../../mobile-providers';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
import { getMobileTabBarItemSchema, MobileTabBarItem } from './MobileTabBar.Item';
|
||||
import { MobileTabBarPage, MobileTabBarLink } from './types';
|
||||
import { cx, DndContext, DndContextProps, SchemaComponent, useDesignable, css, useApp } from '@nocobase/client';
|
||||
import { MobileTabBarInitializer } from './initializer';
|
||||
import { css, cx, DndContext, DndContextProps, SchemaComponent, useApp, useDesignable } from '@nocobase/client';
|
||||
import { isInnerLink } from '../../utils';
|
||||
import { MobileTabBarInitializer } from './initializer';
|
||||
import { getMobileTabBarItemSchema, MobileTabBarItem } from './MobileTabBar.Item';
|
||||
import { MobileTabBarLink, MobileTabBarPage } from './types';
|
||||
|
||||
export interface MobileTabBarProps {
|
||||
/**
|
||||
@ -57,7 +57,7 @@ export const MobileTabBar: FC<MobileTabBarProps> & {
|
||||
);
|
||||
|
||||
if (!hasAuth) {
|
||||
return <Navigate to="/admin" />;
|
||||
return <Navigate to={app.getRouteUrl('/admin')} />;
|
||||
}
|
||||
|
||||
if (!enableTabBar) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user