back
This commit is contained in:
parent
440d98beff
commit
8595a9f4e5
11
packages/ably/.npmignore
Normal file
11
packages/ably/.npmignore
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
test
|
||||||
|
jest.config.js
|
||||||
|
**/*.spec.ts
|
||||||
|
**/*.spec.tsx
|
||||||
|
**/*.test.ts
|
||||||
|
**/*.test.tsx
|
||||||
|
tsup.config.ts
|
||||||
|
tsconfig.test.json
|
||||||
|
tsconfig.declarations.json
|
93
packages/ably/CHANGELOG.md
Normal file
93
packages/ably/CHANGELOG.md
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
# @refinedev/ably
|
||||||
|
|
||||||
|
## 4.1.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5425](https://github.com/refinedev/refine/pull/5425) [`190af9fce2`](https://github.com/refinedev/refine/commit/190af9fce292bc46b169e3e121be6bf1c2a939a5) Thanks [@aliemir](https://github.com/aliemir)! - Updated `@refinedev/core` peer dependencies to latest (`^4.46.1`)
|
||||||
|
|
||||||
|
## 4.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5330](https://github.com/refinedev/refine/pull/5330) [`7c8827b43d`](https://github.com/refinedev/refine/commit/7c8827b43d9e378818be6ee23032925c97ce02d5) Thanks [@BatuhanW](https://github.com/BatuhanW)! - chore: upgrade nock library version to ^13.4.0
|
||||||
|
|
||||||
|
## 4.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5022](https://github.com/refinedev/refine/pull/5022) [`80513a4e42f`](https://github.com/refinedev/refine/commit/80513a4e42f8dda39e01157643594a9e4c32001b) Thanks [@BatuhanW](https://github.com/BatuhanW)! - chore: update README.md
|
||||||
|
|
||||||
|
- fix grammar errors.
|
||||||
|
- make all README.md files consistent.
|
||||||
|
- add code example code snippets.
|
||||||
|
|
||||||
|
## 4.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5022](https://github.com/refinedev/refine/pull/5022) [`80513a4e42f`](https://github.com/refinedev/refine/commit/80513a4e42f8dda39e01157643594a9e4c32001b) Thanks [@BatuhanW](https://github.com/BatuhanW)! - chore: update README.md
|
||||||
|
|
||||||
|
- fix grammar errors.
|
||||||
|
- make all README.md files consistent.
|
||||||
|
- add code example code snippets.
|
||||||
|
|
||||||
|
## 4.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- Thanks [@aliemir](https://github.com/aliemir), [@alicanerdurmaz](https://github.com/alicanerdurmaz), [@batuhanW](https://github.com/batuhanW), [@salihozdemir](https://github.com/salihozdemir), [@yildirayunlu](https://github.com/yildirayunlu), [@recepkutuk](https://github.com/recepkutuk)!
|
||||||
|
**Moving to the `@refinedev` scope 🎉🎉**
|
||||||
|
|
||||||
|
Moved to the `@refinedev` scope and updated our packages to use the new scope. From now on, all packages will be published under the `@refinedev` scope with their new names.
|
||||||
|
|
||||||
|
Now, we're also removing the `refine` prefix from all packages. So, the `@pankod/refine-core` package is now `@refinedev/core`, `@pankod/refine-antd` is now `@refinedev/antd`, and so on.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
## 3.31.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#3822](https://github.com/refinedev/refine/pull/3822) [`0baa99ba787`](https://github.com/refinedev/refine/commit/0baa99ba7874394d9d28d0a7b29c082c604258fb) Thanks [@BatuhanW](https://github.com/BatuhanW)! - - refine v4 release announcement added to "postinstall". - refine v4 is released 🎉 The new version is 100% backward compatible. You can upgrade to v4 with a single command! See the migration guide here: https://refine.dev/docs/migration-guide/3x-to-4x
|
||||||
|
|
||||||
|
## 3.30.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#3822](https://github.com/refinedev/refine/pull/3822) [`0baa99ba787`](https://github.com/refinedev/refine/commit/0baa99ba7874394d9d28d0a7b29c082c604258fb) Thanks [@BatuhanW](https://github.com/BatuhanW)! - - refine v4 release announcement added to "postinstall". - refine v4 is released 🎉 The new version is 100% backward compatible. You can upgrade to v4 with a single command! See the migration guide here: https://refine.dev/docs/migration-guide/3x-to-4x
|
||||||
|
|
||||||
|
## 3.29.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- Update type declaration generation with `tsc` instead of `tsup` for better navigation throughout projects source code.
|
||||||
|
|
||||||
|
## 3.28.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#2440](https://github.com/refinedev/refine/pull/2440) [`0150dcd070`](https://github.com/refinedev/refine/commit/0150dcd0700253f1c4908e7e5f2e178bb122e9af) Thanks [@aliemir](https://github.com/aliemir)! - Update type declaration generation with `tsc` instead of `tsup` for better navigation throughout projects source code.
|
||||||
|
|
||||||
|
## 3.27.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- All of the refine packages have dependencies on the `@pankod/refine-core` package. So far we have managed these dependencies with `peerDependencies` + `dependencies` but this causes issues like #2183. (having more than one @pankod/refine-core version in node_modules and creating different instances)
|
||||||
|
|
||||||
|
Managing as `peerDependencies` + `devDependencies` seems like the best way for now to avoid such issues.
|
||||||
|
|
||||||
|
## 3.26.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#2217](https://github.com/refinedev/refine/pull/2217) [`b4aae00f77`](https://github.com/refinedev/refine/commit/b4aae00f77a2476d847994db21298ae25e4cf6e5) Thanks [@omeraplak](https://github.com/omeraplak)! - All of the refine packages have dependencies on the `@pankod/refine-core` package. So far we have managed these dependencies with `peerDependencies` + `dependencies` but this causes issues like #2183. (having more than one @pankod/refine-core version in node_modules and creating different instances)
|
||||||
|
|
||||||
|
Managing as `peerDependencies` + `devDependencies` seems like the best way for now to avoid such issues.
|
||||||
|
|
||||||
|
## 3.22.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`2deb19babf`](https://github.com/refinedev/refine/commit/2deb19babfc6db5b00b111ec29aa5ece4c371bbc)]:
|
||||||
|
- @pankod/refine-core@3.23.2
|
21
packages/ably/LICENSE
Normal file
21
packages/ably/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Refine Development Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
74
packages/ably/README.md
Normal file
74
packages/ably/README.md
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<div align="center" style="margin: 30px;">
|
||||||
|
<a href="https://refine.dev">
|
||||||
|
<img alt="refine logo" src="https://refine.ams3.cdn.digitaloceanspaces.com/readme/refine-readme-banner.png">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://refine.dev">Home Page</a> |
|
||||||
|
<a href="https://discord.gg/refine">Discord</a> |
|
||||||
|
<a href="https://refine.dev/examples/">Examples</a> |
|
||||||
|
<a href="https://refine.dev/blog/">Blog</a> |
|
||||||
|
<a href="https://refine.dev/docs/">Documentation</a>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
[![Discord](https://img.shields.io/discord/837692625737613362.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/refine)
|
||||||
|
[![Twitter Follow](https://img.shields.io/twitter/follow/refine_dev?style=social)](https://twitter.com/refine_dev)
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/posts/refine-3?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-refine-3" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=362220&theme=light&period=daily" alt="refine - 100% open source React framework to build web apps 3x faster | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div align="center">refine is an open-source, headless React framework for developers building enterprise internal tools, admin panels, dashboards, B2B applications.
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
It eliminates repetitive tasks in CRUD operations and provides industry-standard solutions for critical project components like **authentication**, **access control**, **routing**, **networking**, **state management**, and **i18n**.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
# Ably integration for refine
|
||||||
|
|
||||||
|
[Ably](https://ably.com/) reliably distributes realtime data to your users using the publish/subscribe messaging pattern over WebSocket connections.
|
||||||
|
|
||||||
|
[refine](https://refine.dev/) is **headless by design**, offering unlimited styling and customization options. Moreover, refine ships with ready-made integrations for [Ant Design](https://ant.design/), [Material UI](https://mui.com/material-ui/getting-started/overview/), [Mantine](https://mantine.dev/), and [Chakra UI](https://chakra-ui.com/) for convenience.
|
||||||
|
|
||||||
|
refine has connectors for 15+ backend services, including REST API, [GraphQL](https://graphql.org/), and popular services like [Airtable](https://www.airtable.com/), [Strapi](https://strapi.io/), [Supabase](https://supabase.com/), [Firebase](https://firebase.google.com/), and [NestJS](https://nestjs.com/).
|
||||||
|
|
||||||
|
## Installation & Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install @refinedev/ably
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { liveProvider, Ably } from "@refinedev/ably";
|
||||||
|
|
||||||
|
export const ablyClient = new Ably.Realtime("YOUR_API_TOKEN");
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Refine
|
||||||
|
liveProvider={liveProvider(ablyClient)}
|
||||||
|
/* ... */
|
||||||
|
>
|
||||||
|
{/* ... */}
|
||||||
|
</Refine>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- For more detailed information and usage, refer to the [refine live provider documentation](https://refine.dev/docs/api-references/providers/live-provider/).
|
||||||
|
- Refer to refine & Ably tutorial on [official Ably docs](https://ably.com/tutorials/react-admin-panel-with-ably-and-refine).
|
||||||
|
- [Refer to documentation for more info about refine](https://refine.dev/docs/).
|
||||||
|
- [Step up to refine tutorials](https://refine.dev/docs/tutorial/introduction/index/).
|
6
packages/ably/jest.config.js
Normal file
6
packages/ably/jest.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
preset: "ts-jest",
|
||||||
|
rootDir: "./",
|
||||||
|
displayName: "ably",
|
||||||
|
testEnvironment: "jsdom",
|
||||||
|
};
|
50
packages/ably/package.json
Normal file
50
packages/ably/package.json
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"name": "@refinedev/ably",
|
||||||
|
"description": "refine ably live provider. refine is a React-based framework for building internal tools, rapidly. It ships with Ant Design System, an enterprise-level UI toolkit.",
|
||||||
|
"version": "4.1.4",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"private": false,
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsup --watch --format esm,cjs,iife --legacy-output",
|
||||||
|
"build": "tsup --format esm,cjs,iife --minify --legacy-output",
|
||||||
|
"test": "jest --passWithNoTests --runInBand",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
},
|
||||||
|
"author": "refine",
|
||||||
|
"module": "dist/esm/index.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"@refinedev/core": "^4.46.1",
|
||||||
|
"@esbuild-plugins/node-resolve": "^0.1.4",
|
||||||
|
"@types/jest": "^29.2.4",
|
||||||
|
"jest": "^29.3.1",
|
||||||
|
"jest-environment-jsdom": "^29.3.1",
|
||||||
|
"nock": "^13.4.0",
|
||||||
|
"ts-jest": "^29.0.3",
|
||||||
|
"tslib": "^2.3.1",
|
||||||
|
"tsup": "^6.7.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ably": "^1.2.15"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@refinedev/core": "^4.46.1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/refinedev/refine.git",
|
||||||
|
"directory": "packages/ably"
|
||||||
|
},
|
||||||
|
"gitHead": "829f5a516f98c06f666d6be3e6e6099c75c07719",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
57
packages/ably/src/index.ts
Normal file
57
packages/ably/src/index.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { LiveProvider, LiveEvent } from "@refinedev/core";
|
||||||
|
import Ably from "ably/promises";
|
||||||
|
import { Types } from "ably";
|
||||||
|
interface MessageType extends Types.Message {
|
||||||
|
data: LiveEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveProvider = (client: Ably.Realtime): LiveProvider => {
|
||||||
|
return {
|
||||||
|
subscribe: ({ channel, types, params, callback }) => {
|
||||||
|
const channelInstance = client.channels.get(channel);
|
||||||
|
|
||||||
|
const listener = function (message: MessageType) {
|
||||||
|
if (types.includes("*") || types.includes(message.data.type)) {
|
||||||
|
if (
|
||||||
|
message.data.type !== "created" &&
|
||||||
|
params?.ids !== undefined &&
|
||||||
|
message.data?.payload?.ids !== undefined
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
params.ids
|
||||||
|
.map(String)
|
||||||
|
.filter((value) =>
|
||||||
|
message.data.payload.ids
|
||||||
|
?.map(String)
|
||||||
|
.includes(value),
|
||||||
|
).length > 0
|
||||||
|
) {
|
||||||
|
callback(message.data as LiveEvent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(message.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
channelInstance.subscribe(listener);
|
||||||
|
|
||||||
|
return { channelInstance, listener };
|
||||||
|
},
|
||||||
|
|
||||||
|
unsubscribe: (payload: {
|
||||||
|
channelInstance: Types.RealtimeChannelPromise;
|
||||||
|
listener: () => void;
|
||||||
|
}) => {
|
||||||
|
const { channelInstance, listener } = payload;
|
||||||
|
channelInstance.unsubscribe(listener);
|
||||||
|
},
|
||||||
|
|
||||||
|
publish: (event: LiveEvent) => {
|
||||||
|
const channelInstance = client.channels.get(event.channel);
|
||||||
|
|
||||||
|
channelInstance.publish(event.type, event);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { liveProvider, Ably };
|
21
packages/ably/tsconfig.declarations.json
Normal file
21
packages/ably/tsconfig.declarations.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"test",
|
||||||
|
"../test/**/*",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.tsx"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"declarationDir": "dist",
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"declarationMap": true
|
||||||
|
}
|
||||||
|
}
|
11
packages/ably/tsconfig.json
Normal file
11
packages/ably/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"src",
|
||||||
|
"types"
|
||||||
|
],
|
||||||
|
"extends": "../../tsconfig.build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"baseUrl": ".",
|
||||||
|
}
|
||||||
|
}
|
24
packages/ably/tsup.config.ts
Normal file
24
packages/ably/tsup.config.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineConfig } from "tsup";
|
||||||
|
import { NodeResolvePlugin } from "@esbuild-plugins/node-resolve";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["src/index.ts"],
|
||||||
|
splitting: false,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: false,
|
||||||
|
platform: "browser",
|
||||||
|
esbuildPlugins: [
|
||||||
|
NodeResolvePlugin({
|
||||||
|
extensions: [".js", "ts", "tsx", "jsx"],
|
||||||
|
onResolved: (resolved) => {
|
||||||
|
if (resolved.includes("node_modules")) {
|
||||||
|
return {
|
||||||
|
external: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onSuccess: "tsc --project tsconfig.declarations.json",
|
||||||
|
});
|
11
packages/airtable/.npmignore
Normal file
11
packages/airtable/.npmignore
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
test
|
||||||
|
jest.config.js
|
||||||
|
**/*.spec.ts
|
||||||
|
**/*.spec.tsx
|
||||||
|
**/*.test.ts
|
||||||
|
**/*.test.tsx
|
||||||
|
tsup.config.ts
|
||||||
|
tsconfig.test.json
|
||||||
|
tsconfig.declarations.json
|
357
packages/airtable/CHANGELOG.md
Normal file
357
packages/airtable/CHANGELOG.md
Normal file
|
@ -0,0 +1,357 @@
|
||||||
|
# @refinedev/airtable
|
||||||
|
|
||||||
|
## 4.4.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5425](https://github.com/refinedev/refine/pull/5425) [`190af9fce2`](https://github.com/refinedev/refine/commit/190af9fce292bc46b169e3e121be6bf1c2a939a5) Thanks [@aliemir](https://github.com/aliemir)! - Updated `@refinedev/core` peer dependencies to latest (`^4.46.1`)
|
||||||
|
|
||||||
|
## 4.4.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5330](https://github.com/refinedev/refine/pull/5330) [`7c8827b43d`](https://github.com/refinedev/refine/commit/7c8827b43d9e378818be6ee23032925c97ce02d5) Thanks [@BatuhanW](https://github.com/BatuhanW)! - chore: upgrade nock library version to ^13.4.0
|
||||||
|
|
||||||
|
## 4.4.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5022](https://github.com/refinedev/refine/pull/5022) [`80513a4e42f`](https://github.com/refinedev/refine/commit/80513a4e42f8dda39e01157643594a9e4c32001b) Thanks [@BatuhanW](https://github.com/BatuhanW)! - chore: update README.md
|
||||||
|
|
||||||
|
- fix grammar errors.
|
||||||
|
- make all README.md files consistent.
|
||||||
|
- add code example code snippets.
|
||||||
|
|
||||||
|
## 4.4.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5022](https://github.com/refinedev/refine/pull/5022) [`80513a4e42f`](https://github.com/refinedev/refine/commit/80513a4e42f8dda39e01157643594a9e4c32001b) Thanks [@BatuhanW](https://github.com/BatuhanW)! - chore: update README.md
|
||||||
|
|
||||||
|
- fix grammar errors.
|
||||||
|
- make all README.md files consistent.
|
||||||
|
- add code example code snippets.
|
||||||
|
|
||||||
|
## 4.4.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#4285](https://github.com/refinedev/refine/pull/4285) [`b5cd3328504`](https://github.com/refinedev/refine/commit/b5cd332850428383e8b43f997cbb0340ac7f0dc6) Thanks [@alicanerdurmaz](https://github.com/alicanerdurmaz)! - fixed: A bug that prevented data providers from being swizzled.
|
||||||
|
|
||||||
|
## 4.4.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#4285](https://github.com/refinedev/refine/pull/4285) [`b5cd3328504`](https://github.com/refinedev/refine/commit/b5cd332850428383e8b43f997cbb0340ac7f0dc6) Thanks [@alicanerdurmaz](https://github.com/alicanerdurmaz)! - fixed: A bug that prevented data providers from being swizzled.
|
||||||
|
|
||||||
|
## 4.4.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#4276](https://github.com/refinedev/refine/pull/4276) [`740cef82e9e`](https://github.com/refinedev/refine/commit/740cef82e9e71274edb4be1d6936a2b74b73b4ec) Thanks [@alicanerdurmaz](https://github.com/alicanerdurmaz)! - feat: added swizzle to airtable
|
||||||
|
|
||||||
|
## 4.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#4276](https://github.com/refinedev/refine/pull/4276) [`740cef82e9e`](https://github.com/refinedev/refine/commit/740cef82e9e71274edb4be1d6936a2b74b73b4ec) Thanks [@alicanerdurmaz](https://github.com/alicanerdurmaz)! - feat: added swizzle to airtable
|
||||||
|
|
||||||
|
## 4.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#4276](https://github.com/refinedev/refine/pull/4276) [`740cef82e9e`](https://github.com/refinedev/refine/commit/740cef82e9e71274edb4be1d6936a2b74b73b4ec) Thanks [@alicanerdurmaz](https://github.com/alicanerdurmaz)! - feat: added swizzle to airtable
|
||||||
|
|
||||||
|
## 4.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- Thanks [@aliemir](https://github.com/aliemir), [@alicanerdurmaz](https://github.com/alicanerdurmaz), [@batuhanW](https://github.com/batuhanW), [@salihozdemir](https://github.com/salihozdemir), [@yildirayunlu](https://github.com/yildirayunlu), [@recepkutuk](https://github.com/recepkutuk)!
|
||||||
|
|
||||||
|
- `metaData` prop is now deprecated for all data provider methods. Use `meta` prop instead.
|
||||||
|
|
||||||
|
> For backward compatibility, we still support `metaData` prop with refine v4.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
create: async ({
|
||||||
|
- metaData
|
||||||
|
+ meta
|
||||||
|
}) => {
|
||||||
|
...
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- `sort`, `hasPagination`, and `metaData` parameters of `getList` method are now deprecated. Use `sorters`, `pagination`, and `meta` parameters instead.
|
||||||
|
|
||||||
|
> For backward compatibility, we still support `sort`, `hasPagination` and `metaData` props with refine v4.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
getList: async ({
|
||||||
|
- sort
|
||||||
|
+ sorters
|
||||||
|
- hasPagination
|
||||||
|
+ pagination: { mode: "off" | "server | "client" }
|
||||||
|
- metaData
|
||||||
|
+ meta
|
||||||
|
}) => {
|
||||||
|
...
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- Thanks [@aliemir](https://github.com/aliemir), [@alicanerdurmaz](https://github.com/alicanerdurmaz), [@batuhanW](https://github.com/batuhanW), [@salihozdemir](https://github.com/salihozdemir), [@yildirayunlu](https://github.com/yildirayunlu), [@recepkutuk](https://github.com/recepkutuk)!
|
||||||
|
**Moving to the `@refinedev` scope 🎉🎉**
|
||||||
|
|
||||||
|
Moved to the `@refinedev` scope and updated our packages to use the new scope. From now on, all packages will be published under the `@refinedev` scope with their new names.
|
||||||
|
|
||||||
|
Now, we're also removing the `refine` prefix from all packages. So, the `@pankod/refine-core` package is now `@refinedev/core`, `@pankod/refine-antd` is now `@refinedev/antd`, and so on.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
## 3.35.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#3822](https://github.com/refinedev/refine/pull/3822) [`0baa99ba787`](https://github.com/refinedev/refine/commit/0baa99ba7874394d9d28d0a7b29c082c604258fb) Thanks [@BatuhanW](https://github.com/BatuhanW)! - - refine v4 release announcement added to "postinstall". - refine v4 is released 🎉 The new version is 100% backward compatible. You can upgrade to v4 with a single command! See the migration guide here: https://refine.dev/docs/migration-guide/3x-to-4x
|
||||||
|
|
||||||
|
## 3.34.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#3822](https://github.com/refinedev/refine/pull/3822) [`0baa99ba787`](https://github.com/refinedev/refine/commit/0baa99ba7874394d9d28d0a7b29c082c604258fb) Thanks [@BatuhanW](https://github.com/BatuhanW)! - - refine v4 release announcement added to "postinstall". - refine v4 is released 🎉 The new version is 100% backward compatible. You can upgrade to v4 with a single command! See the migration guide here: https://refine.dev/docs/migration-guide/3x-to-4x
|
||||||
|
|
||||||
|
## 3.33.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- Only `or` was supported as a conditional filter. Now `and` and `or` can be used together and nested. 🚀
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
operator: "or",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
operator: "and",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
field: "name",
|
||||||
|
operator: "eq",
|
||||||
|
value: "John Doe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "age",
|
||||||
|
operator: "eq",
|
||||||
|
value: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: "and",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
field: "name",
|
||||||
|
operator: "eq",
|
||||||
|
value: "JR Doe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "age",
|
||||||
|
operator: "eq",
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.32.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#2751](https://github.com/refinedev/refine/pull/2751) [`addff64c77`](https://github.com/refinedev/refine/commit/addff64c777e4c9f044a1a109cb05453e6e9f762) Thanks [@yildirayunlu](https://github.com/yildirayunlu)! - Only `or` was supported as a conditional filter. Now `and` and `or` can be used together and nested. 🚀
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
operator: "or",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
operator: "and",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
field: "name",
|
||||||
|
operator: "eq",
|
||||||
|
value: "John Doe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "age",
|
||||||
|
operator: "eq",
|
||||||
|
value: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: "and",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
field: "name",
|
||||||
|
operator: "eq",
|
||||||
|
value: "JR Doe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "age",
|
||||||
|
operator: "eq",
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.31.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- Updated `dataProvider` types with `Required` utility to mark `getMany`, `createMany`, `updateMany` and `deleteMany` as implemented.
|
||||||
|
|
||||||
|
## 3.30.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#2688](https://github.com/refinedev/refine/pull/2688) [`508045ac30`](https://github.com/refinedev/refine/commit/508045ac30cd3948f68497e13fdf04f7c72ce387) Thanks [@aliemir](https://github.com/aliemir)! - Updated `dataProvider` types with `Required` utility to mark `getMany`, `createMany`, `updateMany` and `deleteMany` as implemented.
|
||||||
|
|
||||||
|
## 3.29.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- Update type declaration generation with `tsc` instead of `tsup` for better navigation throughout projects source code.
|
||||||
|
|
||||||
|
## 3.28.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#2440](https://github.com/refinedev/refine/pull/2440) [`0150dcd070`](https://github.com/refinedev/refine/commit/0150dcd0700253f1c4908e7e5f2e178bb122e9af) Thanks [@aliemir](https://github.com/aliemir)! - Update type declaration generation with `tsc` instead of `tsup` for better navigation throughout projects source code.
|
||||||
|
|
||||||
|
## 3.27.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- All of the refine packages have dependencies on the `@pankod/refine-core` package. So far we have managed these dependencies with `peerDependencies` + `dependencies` but this causes issues like #2183. (having more than one @pankod/refine-core version in node_modules and creating different instances)
|
||||||
|
|
||||||
|
Managing as `peerDependencies` + `devDependencies` seems like the best way for now to avoid such issues.
|
||||||
|
|
||||||
|
## 3.26.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#2217](https://github.com/refinedev/refine/pull/2217) [`b4aae00f77`](https://github.com/refinedev/refine/commit/b4aae00f77a2476d847994db21298ae25e4cf6e5) Thanks [@omeraplak](https://github.com/omeraplak)! - All of the refine packages have dependencies on the `@pankod/refine-core` package. So far we have managed these dependencies with `peerDependencies` + `dependencies` but this causes issues like #2183. (having more than one @pankod/refine-core version in node_modules and creating different instances)
|
||||||
|
|
||||||
|
Managing as `peerDependencies` + `devDependencies` seems like the best way for now to avoid such issues.
|
||||||
|
|
||||||
|
## 3.25.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated pagination parameters default values and added `hasPagination` property to `getList` method of the data providers.
|
||||||
|
|
||||||
|
**Implementation**
|
||||||
|
|
||||||
|
Updated the `getList` method accordingly to the changes in the `useTable` and `useList` of `@pankod/refine-core`. `hasPagination` is used to disable pagination (defaults to `true`)
|
||||||
|
|
||||||
|
**Use Cases**
|
||||||
|
|
||||||
|
For some resources, there might be no support for pagination or users might want to see all of the data without any pagination, prior to these changes this was not supported in **refine** data providers.
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @pankod/refine-core@3.36.0
|
||||||
|
|
||||||
|
## 3.25.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#2050](https://github.com/refinedev/refine/pull/2050) [`635cfe9fdb`](https://github.com/refinedev/refine/commit/635cfe9fdbfe5940b950ae99c1f0b686c78bb8e5) Thanks [@ozkalai](https://github.com/ozkalai)! - Updated pagination parameters default values and added `hasPagination` property to `getList` method of the data providers.
|
||||||
|
|
||||||
|
**Implementation**
|
||||||
|
|
||||||
|
Updated the `getList` method accordingly to the changes in the `useTable` and `useList` of `@pankod/refine-core`. `hasPagination` is used to disable pagination (defaults to `true`)
|
||||||
|
|
||||||
|
**Use Cases**
|
||||||
|
|
||||||
|
For some resources, there might be no support for pagination or users might want to see all of the data without any pagination, prior to these changes this was not supported in **refine** data providers.
|
||||||
|
|
||||||
|
- Updated dependencies [[`ecde34a9b3`](https://github.com/refinedev/refine/commit/ecde34a9b38ef5667fa863f9ebb9dcb1cfff1651), [`635cfe9fdb`](https://github.com/refinedev/refine/commit/635cfe9fdbfe5940b950ae99c1f0b686c78bb8e5)]:
|
||||||
|
- @pankod/refine-core@3.35.0
|
||||||
|
|
||||||
|
## 3.25.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated axios version (0.21.4 to 0.26.1). In this version, the way of sending headers has changed as follows.
|
||||||
|
|
||||||
|
```
|
||||||
|
// old v0.21.4
|
||||||
|
axiosInstance.defaults.headers = { Authorization: `Bearer ${data.jwt}` };
|
||||||
|
|
||||||
|
// new v0.26.1
|
||||||
|
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${data.jwt}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @pankod/refine-core@3.29.0
|
||||||
|
|
||||||
|
## 3.25.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated axios version (0.21.4 to 0.26.1). In this version, the way of sending headers has changed as follows.
|
||||||
|
|
||||||
|
```
|
||||||
|
// old v0.21.4
|
||||||
|
axiosInstance.defaults.headers = { Authorization: `Bearer ${data.jwt}` };
|
||||||
|
|
||||||
|
// new v0.26.1
|
||||||
|
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${data.jwt}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @pankod/refine-core@3.28.0
|
||||||
|
|
||||||
|
## 3.25.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated axios version (0.21.4 to 0.26.1). In this version, the way of sending headers has changed as follows.
|
||||||
|
|
||||||
|
```
|
||||||
|
// old v0.21.4
|
||||||
|
axiosInstance.defaults.headers = { Authorization: `Bearer ${data.jwt}` };
|
||||||
|
|
||||||
|
// new v0.26.1
|
||||||
|
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${data.jwt}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @pankod/refine-core@3.27.0
|
||||||
|
|
||||||
|
## 3.25.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1899](https://github.com/refinedev/refine/pull/1899) [`fbfea418a0`](https://github.com/refinedev/refine/commit/fbfea418a024a527a2b432c634f46a96d4f70d88) Thanks [@yildirayunlu](https://github.com/yildirayunlu)! - Updated axios version (0.21.4 to 0.26.1). In this version, the way of sending headers has changed as follows.
|
||||||
|
|
||||||
|
```
|
||||||
|
// old v0.21.4
|
||||||
|
axiosInstance.defaults.headers = { Authorization: `Bearer ${data.jwt}` };
|
||||||
|
|
||||||
|
// new v0.26.1
|
||||||
|
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${data.jwt}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
- Updated dependencies [[`2ba2a96fd2`](https://github.com/refinedev/refine/commit/2ba2a96fd24aa733c355ac9ef4c99b7d48115746)]:
|
||||||
|
- @pankod/refine-core@3.26.0
|
||||||
|
|
||||||
|
## 3.22.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`2deb19babf`](https://github.com/refinedev/refine/commit/2deb19babfc6db5b00b111ec29aa5ece4c371bbc)]:
|
||||||
|
- @pankod/refine-core@3.23.2
|
21
packages/airtable/LICENSE
Normal file
21
packages/airtable/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Refine Development Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
70
packages/airtable/README.md
Normal file
70
packages/airtable/README.md
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<div align="center" style="margin: 30px;">
|
||||||
|
<a href="https://refine.dev">
|
||||||
|
<img alt="refine logo" src="https://refine.ams3.cdn.digitaloceanspaces.com/readme/refine-readme-banner.png">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://refine.dev">Home Page</a> |
|
||||||
|
<a href="https://discord.gg/refine">Discord</a> |
|
||||||
|
<a href="https://refine.dev/examples/">Examples</a> |
|
||||||
|
<a href="https://refine.dev/blog/">Blog</a> |
|
||||||
|
<a href="https://refine.dev/docs/">Documentation</a>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
[![Discord](https://img.shields.io/discord/837692625737613362.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/refine)
|
||||||
|
[![Twitter Follow](https://img.shields.io/twitter/follow/refine_dev?style=social)](https://twitter.com/refine_dev)
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/posts/refine-3?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-refine-3" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=362220&theme=light&period=daily" alt="refine - 100% open source React framework to build web apps 3x faster | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div align="center">refine is an open-source, headless React framework for developers building enterprise internal tools, admin panels, dashboards, B2B applications.
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
It eliminates repetitive tasks in CRUD operations and provides industry-standard solutions for critical project components like **authentication**, **access control**, **routing**, **networking**, **state management**, and **i18n**.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# Airtable integration for refine
|
||||||
|
|
||||||
|
[Airtable](https://www.airtable.com/) is a cloud-based platform for creating and sharing relational databases.
|
||||||
|
|
||||||
|
[refine](https://refine.dev/) is **headless by design**, offering unlimited styling and customization options. Moreover, refine ships with ready-made integrations for [Ant Design](https://ant.design/), [Material UI](https://mui.com/material-ui/getting-started/overview/), [Mantine](https://mantine.dev/), and [Chakra UI](https://chakra-ui.com/) for convenience.
|
||||||
|
|
||||||
|
refine has connectors for 15+ backend services, including REST API, [GraphQL](https://graphql.org/), and popular services like [Airtable](https://www.airtable.com/), [Strapi](https://strapi.io/), [Supabase](https://supabase.com/), [Firebase](https://firebase.google.com/), and [NestJS](https://nestjs.com/).
|
||||||
|
|
||||||
|
## Installation & Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install @refinedev/airtable
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import dataProvider from "@refinedev/airtable";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Refine
|
||||||
|
dataProvider={dataProvider("API_KEY", "BASE_ID")}
|
||||||
|
/* ... */
|
||||||
|
>
|
||||||
|
{/* ... */}
|
||||||
|
</Refine>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- For more detailed information and usage, refer to the [refine data provider documentation](https://refine.dev/docs/core/providers/data-provider).
|
||||||
|
- [Refer to refine Airtable example](https://refine.dev/docs/examples/data-provider/airtable/).
|
||||||
|
- [Refer to documentation for more info about refine](https://refine.dev/docs/).
|
||||||
|
- [Step up to refine tutorials](https://refine.dev/docs/tutorial/introduction/index/).
|
7
packages/airtable/jest.config.js
Normal file
7
packages/airtable/jest.config.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
preset: "ts-jest",
|
||||||
|
rootDir: "./",
|
||||||
|
displayName: "airtable",
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/test/jest.setup.js"],
|
||||||
|
testEnvironment: "jsdom",
|
||||||
|
};
|
54
packages/airtable/package.json
Normal file
54
packages/airtable/package.json
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"name": "@refinedev/airtable",
|
||||||
|
"description": "refine Airtable data provider. refine is a React-based framework for building internal tools, rapidly. It ships with Ant Design System, an enterprise-level UI toolkit.",
|
||||||
|
"version": "4.4.6",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"private": false,
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src",
|
||||||
|
"./refine.config.js"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsup --watch --format esm,cjs,iife --legacy-output",
|
||||||
|
"build": "tsup --format esm,cjs,iife --minify --legacy-output",
|
||||||
|
"test": "jest --passWithNoTests --runInBand",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
},
|
||||||
|
"author": "refine",
|
||||||
|
"module": "dist/esm/index.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"@refinedev/core": "4.46.2",
|
||||||
|
"@esbuild-plugins/node-resolve": "^0.1.4",
|
||||||
|
"@types/jest": "^29.2.4",
|
||||||
|
"jest": "^29.3.1",
|
||||||
|
"jest-environment-jsdom": "^29.3.1",
|
||||||
|
"nock": "^13.4.0",
|
||||||
|
"ts-jest": "^29.0.3",
|
||||||
|
"tslib": "^2.3.1",
|
||||||
|
"tsup": "^6.7.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@qualifyze/airtable-formulator": "^1.0.1",
|
||||||
|
"airtable": "^0.11.1",
|
||||||
|
"asyncairtable": "^2.1.0",
|
||||||
|
"query-string": "^7.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@refinedev/core": "^4.46.1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/refinedev/refine.git",
|
||||||
|
"directory": "packages/airtable"
|
||||||
|
},
|
||||||
|
"gitHead": "829f5a516f98c06f666d6be3e6e6099c75c07719",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
80
packages/airtable/refine.config.js
Normal file
80
packages/airtable/refine.config.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/** @type {import('@refinedev/cli').RefineConfig} */
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
group: "Data Provider",
|
||||||
|
swizzle: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Data Provider",
|
||||||
|
requiredPackages: [
|
||||||
|
"@qualifyze/airtable-formulator@1.0.1",
|
||||||
|
"airtable@0.11.1",
|
||||||
|
"asyncairtable@2.1.0",
|
||||||
|
"query-string@7.1.1",
|
||||||
|
],
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/index.ts",
|
||||||
|
dest: "./providers/airtable/index.ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/dataProvider.ts",
|
||||||
|
dest: "./providers/airtable/dataProvider.ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/utils/index.ts",
|
||||||
|
dest: "./providers/airtable/utils/index.ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/utils/generateFilter.ts",
|
||||||
|
dest: "./providers/airtable/utils/generateFilter.ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/utils/generateFilterFormula.ts",
|
||||||
|
dest: "./providers/airtable/utils/generateFilterFormula.ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/utils/generateLogicalFilterFormula.ts",
|
||||||
|
dest: "./providers/airtable/utils/generateLogicalFilterFormula.ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/utils/generateLogicalFilterFormula.ts",
|
||||||
|
dest: "./providers/airtable/utils/generateLogicalFilterFormula.ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/utils/generateSort.ts",
|
||||||
|
dest: "./providers/airtable/utils/generateSort.ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/utils/isContainsOperator.ts",
|
||||||
|
dest: "./providers/airtable/utils/isContainsOperator.ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/utils/isSimpleOperator.ts",
|
||||||
|
dest: "./providers/airtable/utils/isSimpleOperator.ts",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
message: `
|
||||||
|
**\`Usage\`**
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
// title: App.tsx
|
||||||
|
import { dataProvider } from "./providers/airtable";
|
||||||
|
|
||||||
|
const API_TOKEN = "dummy-api-token";
|
||||||
|
const BASE_ID = "dummy-base-id";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Refine
|
||||||
|
dataProvider={dataProvider(API_TOKEN, BASE_ID)}
|
||||||
|
/* ... */
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
161
packages/airtable/src/dataProvider.ts
Normal file
161
packages/airtable/src/dataProvider.ts
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import { DataProvider } from "@refinedev/core";
|
||||||
|
import Airtable from "airtable";
|
||||||
|
import { AirtableBase } from "airtable/lib/airtable_base";
|
||||||
|
import { generateSort, generateFilter } from "./utils";
|
||||||
|
|
||||||
|
export const dataProvider = (
|
||||||
|
apiKey: string,
|
||||||
|
baseId: string,
|
||||||
|
airtableClient?: AirtableBase,
|
||||||
|
): Required<DataProvider> => {
|
||||||
|
const base =
|
||||||
|
airtableClient || new Airtable({ apiKey: apiKey }).base(baseId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getList: async ({ resource, pagination, sorters, filters }) => {
|
||||||
|
const {
|
||||||
|
current = 1,
|
||||||
|
pageSize = 10,
|
||||||
|
mode = "server",
|
||||||
|
} = pagination ?? {};
|
||||||
|
|
||||||
|
const generatedSort = generateSort(sorters) || [];
|
||||||
|
const queryFilters = generateFilter(filters);
|
||||||
|
|
||||||
|
const { all } = base(resource).select({
|
||||||
|
pageSize: 100,
|
||||||
|
sort: generatedSort,
|
||||||
|
...(queryFilters ? { filterByFormula: queryFilters } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await all();
|
||||||
|
const isServerPaginationEnabled = mode === "server";
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data
|
||||||
|
.slice(
|
||||||
|
isServerPaginationEnabled
|
||||||
|
? (current - 1) * pageSize
|
||||||
|
: undefined,
|
||||||
|
isServerPaginationEnabled
|
||||||
|
? current * pageSize
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
|
.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
...p.fields,
|
||||||
|
})) as any,
|
||||||
|
total: data.length,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getMany: async ({ resource, ids }) => {
|
||||||
|
const { all } = base(resource).select({
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await all();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data
|
||||||
|
.filter((p) => ids.includes(p.id))
|
||||||
|
.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
...p.fields,
|
||||||
|
})) as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async ({ resource, variables }) => {
|
||||||
|
const { id, fields } = await base(resource).create(variables);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id: id,
|
||||||
|
...fields,
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
createMany: async ({ resource, variables }) => {
|
||||||
|
const data = await base(resource).create(variables);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
...p.fields,
|
||||||
|
})) as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async ({ resource, id, variables }) => {
|
||||||
|
const { fields } = await base(resource).update(
|
||||||
|
id.toString(),
|
||||||
|
variables,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
...fields,
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMany: async ({ resource, ids, variables }) => {
|
||||||
|
const requestParams = ids.map((id) => ({
|
||||||
|
id: id.toString(),
|
||||||
|
fields: { ...variables },
|
||||||
|
}));
|
||||||
|
const data = await base(resource).update(requestParams);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
...p.fields,
|
||||||
|
})) as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getOne: async ({ resource, id }) => {
|
||||||
|
const { fields } = await base(resource).find(id.toString());
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
...fields,
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteOne: async ({ resource, id }) => {
|
||||||
|
const { fields } = await base(resource).destroy(id.toString());
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
...fields,
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteMany: async ({ resource, ids }) => {
|
||||||
|
const data = await base(resource).destroy(ids.map(String));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
...p.fields,
|
||||||
|
})) as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getApiUrl: () => {
|
||||||
|
throw Error("Not implemented on refine-airtable data provider.");
|
||||||
|
},
|
||||||
|
|
||||||
|
custom: async () => {
|
||||||
|
throw Error("Not implemented on refine-airtable data provider.");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
5
packages/airtable/src/index.ts
Normal file
5
packages/airtable/src/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { dataProvider } from "./dataProvider";
|
||||||
|
|
||||||
|
export * from "./utils";
|
||||||
|
export * from "./dataProvider";
|
||||||
|
export default dataProvider;
|
12
packages/airtable/src/utils/generateFilter.ts
Normal file
12
packages/airtable/src/utils/generateFilter.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { CrudFilters } from "@refinedev/core";
|
||||||
|
import { compile } from "@qualifyze/airtable-formulator";
|
||||||
|
import { generateFilterFormula } from "./generateFilterFormula";
|
||||||
|
|
||||||
|
export const generateFilter = (filters?: CrudFilters): string | undefined => {
|
||||||
|
if (filters) {
|
||||||
|
// Top-level array has an implicit AND as per CRUDFilter design - https://refine.dev/docs/guides-and-concepts/data-provider/handling-filters/#logicalfilters
|
||||||
|
return compile(["AND", ...generateFilterFormula(filters)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
17
packages/airtable/src/utils/generateFilterFormula.ts
Normal file
17
packages/airtable/src/utils/generateFilterFormula.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { CrudFilters, LogicalFilter } from "@refinedev/core";
|
||||||
|
import { Formula } from "@qualifyze/airtable-formulator";
|
||||||
|
import { generateLogicalFilterFormula } from "./generateLogicalFilterFormula";
|
||||||
|
|
||||||
|
export const generateFilterFormula = (filters: CrudFilters): Formula[] => {
|
||||||
|
const compound = filters.map((filter): Formula => {
|
||||||
|
const { operator, value } = filter;
|
||||||
|
|
||||||
|
if (operator === "or") {
|
||||||
|
return ["OR", ...generateFilterFormula(value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateLogicalFilterFormula(filter as LogicalFilter);
|
||||||
|
});
|
||||||
|
|
||||||
|
return compound;
|
||||||
|
};
|
50
packages/airtable/src/utils/generateLogicalFilterFormula.ts
Normal file
50
packages/airtable/src/utils/generateLogicalFilterFormula.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { LogicalFilter } from "@refinedev/core";
|
||||||
|
import { isContainsOperator, isContainssOperator } from "./isContainsOperator";
|
||||||
|
import { isSimpleOperator, simpleOperatorMapping } from "./isSimpleOperator";
|
||||||
|
import { Formula } from "@qualifyze/airtable-formulator";
|
||||||
|
|
||||||
|
export const generateLogicalFilterFormula = (
|
||||||
|
filter: LogicalFilter,
|
||||||
|
): Formula => {
|
||||||
|
const { field, operator, value } = filter;
|
||||||
|
|
||||||
|
if (isSimpleOperator(operator)) {
|
||||||
|
return [simpleOperatorMapping[operator], { field }, value];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isContainssOperator(operator)) {
|
||||||
|
const mappedOperator = {
|
||||||
|
containss: "!=",
|
||||||
|
ncontainss: "=",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return [mappedOperator[operator], ["FIND", value, { field }], 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isContainsOperator(operator)) {
|
||||||
|
const mappedOperator = {
|
||||||
|
contains: "!=",
|
||||||
|
ncontains: "=",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const find = [
|
||||||
|
"FIND",
|
||||||
|
["LOWER", value],
|
||||||
|
["LOWER", { field }],
|
||||||
|
] as Formula;
|
||||||
|
|
||||||
|
return [mappedOperator[operator], find, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operator === "null") {
|
||||||
|
return ["=", { field }, ["BLANK"]];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operator === "nnull") {
|
||||||
|
return ["!=", { field }, ["BLANK"]];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Error(
|
||||||
|
`Operator ${operator} is not supported for the Airtable data provider`,
|
||||||
|
);
|
||||||
|
};
|
8
packages/airtable/src/utils/generateSort.ts
Normal file
8
packages/airtable/src/utils/generateSort.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { CrudSorting } from "@refinedev/core";
|
||||||
|
|
||||||
|
export const generateSort = (sorters?: CrudSorting) => {
|
||||||
|
return sorters?.map((item) => ({
|
||||||
|
field: item.field,
|
||||||
|
direction: item.order,
|
||||||
|
}));
|
||||||
|
};
|
6
packages/airtable/src/utils/index.ts
Normal file
6
packages/airtable/src/utils/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export * from "./isSimpleOperator";
|
||||||
|
export * from "./isContainsOperator";
|
||||||
|
export * from "./generateLogicalFilterFormula";
|
||||||
|
export * from "./generateFilterFormula";
|
||||||
|
export * from "./generateFilter";
|
||||||
|
export * from "./generateSort";
|
9
packages/airtable/src/utils/isContainsOperator.ts
Normal file
9
packages/airtable/src/utils/isContainsOperator.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export const isContainssOperator = (
|
||||||
|
operator: any,
|
||||||
|
): operator is "containss" | "ncontainss" =>
|
||||||
|
["containss", "ncontainss"].includes(operator);
|
||||||
|
|
||||||
|
export const isContainsOperator = (
|
||||||
|
operator: any,
|
||||||
|
): operator is "contains" | "ncontains" =>
|
||||||
|
["contains", "ncontains"].includes(operator);
|
14
packages/airtable/src/utils/isSimpleOperator.ts
Normal file
14
packages/airtable/src/utils/isSimpleOperator.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
export type SimpleOperators = "eq" | "ne" | "lt" | "lte" | "gt" | "gte";
|
||||||
|
import { OperatorSymbol } from "@qualifyze/airtable-formulator";
|
||||||
|
|
||||||
|
export const simpleOperatorMapping: Record<SimpleOperators, OperatorSymbol> = {
|
||||||
|
eq: "=",
|
||||||
|
ne: "!=",
|
||||||
|
lt: "<",
|
||||||
|
lte: "<=",
|
||||||
|
gt: ">",
|
||||||
|
gte: ">=",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const isSimpleOperator = (operator: any): operator is SimpleOperators =>
|
||||||
|
Object.keys(simpleOperatorMapping).includes(operator);
|
48
packages/airtable/test/create/index.mock.ts
Normal file
48
packages/airtable/test/create/index.mock.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import nock from "nock";
|
||||||
|
|
||||||
|
nock("https://api.airtable.com:443", { encodedQueryParams: true })
|
||||||
|
.post("/v0/appKYl1H4k9g73sBT/posts/", {
|
||||||
|
fields: {
|
||||||
|
title: "foo",
|
||||||
|
content: "bar",
|
||||||
|
status: "published",
|
||||||
|
category: ["recDBRJljBDFH4rIh"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.query({})
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
"1f8b0800000000000003158c3d0f82301400ffcb9bc5b4151d3aa2821227c3e4c750e8436a2a35e53118c27ff7b15eee6e02674143c4a63c16785126dbc622aff7b082d6a1b703e809c89147b6da10980f6468640edfb1f66ee8d0326c0ce12bc41fe8fb323b64d7d2bfb3437e4ae3b983270ba127ec89b3dac4470f33a3885cd9ca7d96b9124a266297a8b4921b2d85966a2d84b8c1fc0704ab8bf4a4000000",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"*",
|
||||||
|
"content-encoding",
|
||||||
|
"gzip",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Date",
|
||||||
|
"Thu, 24 Jun 2021 13:10:12 GMT",
|
||||||
|
"Server",
|
||||||
|
"Tengine",
|
||||||
|
"Set-Cookie",
|
||||||
|
"brw=brwhuRDEqN6Ub31j3; path=/; expires=Fri, 24 Jun 2022 13:10:12 GMT; domain=.airtable.com; samesite=none; secure",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-Content-Type-Options",
|
||||||
|
"nosniff",
|
||||||
|
"X-Frame-Options",
|
||||||
|
"DENY",
|
||||||
|
"Content-Length",
|
||||||
|
"160",
|
||||||
|
"Connection",
|
||||||
|
"Close",
|
||||||
|
],
|
||||||
|
);
|
26
packages/airtable/test/create/index.spec.ts
Normal file
26
packages/airtable/test/create/index.spec.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import dataProvider from "../../src/index";
|
||||||
|
import "./index.mock";
|
||||||
|
|
||||||
|
describe("create", () => {
|
||||||
|
it("correct response", async () => {
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).create({
|
||||||
|
resource: "posts",
|
||||||
|
variables: {
|
||||||
|
title: "foo",
|
||||||
|
content: "bar",
|
||||||
|
status: "published",
|
||||||
|
category: ["recDBRJljBDFH4rIh"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
expect(data["title"]).toEqual("foo");
|
||||||
|
expect(data["status"]).toEqual("published");
|
||||||
|
expect(data["category"]).toEqual(["recDBRJljBDFH4rIh"]);
|
||||||
|
expect(data["content"]).toEqual("bar\n");
|
||||||
|
});
|
||||||
|
});
|
14
packages/airtable/test/custom/index.spec.ts
Normal file
14
packages/airtable/test/custom/index.spec.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import dataProvider from "../../src/index";
|
||||||
|
|
||||||
|
describe("custom", () => {
|
||||||
|
it("correct get response", async () => {
|
||||||
|
try {
|
||||||
|
await dataProvider("keywoytODSr6xAqfg", "appKYl1H4k9g73sBT")
|
||||||
|
.custom!({ url: "users", method: "get" });
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toEqual(
|
||||||
|
Error("Not implemented on refine-airtable data provider."),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
41
packages/airtable/test/deleteMany/index.mock.ts
Normal file
41
packages/airtable/test/deleteMany/index.mock.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import nock from "nock";
|
||||||
|
|
||||||
|
nock("https://api.airtable.com:443", { encodedQueryParams: true })
|
||||||
|
.delete("/v0/appKYl1H4k9g73sBT/posts")
|
||||||
|
.query({ "records%5B%5D": "recdgFXue7JnGD90w" })
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
"1f8b0800000000000003ab562a4a4dce2f4a2956b28aae564a49cd492d494d51b22a292a4dd551ca04b240f229e96e11a5a9e65e79ee2e9606e54ab5b1b5002d0259eb37000000",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"*",
|
||||||
|
"content-encoding",
|
||||||
|
"gzip",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Date",
|
||||||
|
"Thu, 24 Jun 2021 13:17:00 GMT",
|
||||||
|
"Server",
|
||||||
|
"Tengine",
|
||||||
|
"Set-Cookie",
|
||||||
|
"brw=brwTjdQAZbqbk0xGk; path=/; expires=Fri, 24 Jun 2022 13:17:00 GMT; domain=.airtable.com; samesite=none; secure",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-Content-Type-Options",
|
||||||
|
"nosniff",
|
||||||
|
"X-Frame-Options",
|
||||||
|
"DENY",
|
||||||
|
"Content-Length",
|
||||||
|
"71",
|
||||||
|
"Connection",
|
||||||
|
"Close",
|
||||||
|
],
|
||||||
|
);
|
15
packages/airtable/test/deleteMany/index.spec.ts
Normal file
15
packages/airtable/test/deleteMany/index.spec.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import dataProvider from "../../src/index";
|
||||||
|
import "./index.mock";
|
||||||
|
|
||||||
|
describe("deleteMany", () => {
|
||||||
|
it("correct response", async () => {
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).deleteMany!({ resource: "posts", ids: ["recdgFXue7JnGD90w"] });
|
||||||
|
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
expect(data).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
41
packages/airtable/test/deleteOne/index.mock.ts
Normal file
41
packages/airtable/test/deleteOne/index.mock.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import nock from "nock";
|
||||||
|
|
||||||
|
nock("https://api.airtable.com:443", { encodedQueryParams: true })
|
||||||
|
.delete("/v0/appKYl1H4k9g73sBT/posts/recJEGeL2aB5rGFbC")
|
||||||
|
.query({})
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
"1f8b0800000000000003ab564a49cd492d494d51b22a292a4dd551ca04b2948a5293bd5cdd537d8c129d4c8bdcdd929c956a016e8d991b29000000",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"*",
|
||||||
|
"content-encoding",
|
||||||
|
"gzip",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Date",
|
||||||
|
"Thu, 24 Jun 2021 13:19:14 GMT",
|
||||||
|
"Server",
|
||||||
|
"Tengine",
|
||||||
|
"Set-Cookie",
|
||||||
|
"brw=brwInLaPzl4WP7sXu; path=/; expires=Fri, 24 Jun 2022 13:19:13 GMT; domain=.airtable.com; samesite=none; secure",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-Content-Type-Options",
|
||||||
|
"nosniff",
|
||||||
|
"X-Frame-Options",
|
||||||
|
"DENY",
|
||||||
|
"Content-Length",
|
||||||
|
"59",
|
||||||
|
"Connection",
|
||||||
|
"Close",
|
||||||
|
],
|
||||||
|
);
|
15
packages/airtable/test/deleteOne/index.spec.ts
Normal file
15
packages/airtable/test/deleteOne/index.spec.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import dataProvider from "../../src/index";
|
||||||
|
import "./index.mock";
|
||||||
|
|
||||||
|
describe("deleteOne", () => {
|
||||||
|
it("correct response", async () => {
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).deleteOne({ resource: "posts", id: "recJEGeL2aB5rGFbC" });
|
||||||
|
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
expect(data).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
150
packages/airtable/test/getList/index.mock.ts
Normal file
150
packages/airtable/test/getList/index.mock.ts
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import nock from "nock";
|
||||||
|
import url from "url";
|
||||||
|
|
||||||
|
const commonHeaders = [
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"*",
|
||||||
|
"airtable-uncompressed-content-length",
|
||||||
|
"380",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Date",
|
||||||
|
"Thu, 24 Jun 2021 12:24:32 GMT",
|
||||||
|
"Server",
|
||||||
|
"Tengine",
|
||||||
|
"Set-Cookie",
|
||||||
|
"brw=brwHislGvzT3Ws3Yf; path=/; expires=Fri, 24 Jun 2022 12:24:32 GMT; domain=.airtable.com; samesite=none; secure",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-Content-Type-Options",
|
||||||
|
"nosniff",
|
||||||
|
"X-Frame-Options",
|
||||||
|
"DENY",
|
||||||
|
"Content-Length",
|
||||||
|
"233",
|
||||||
|
"Connection",
|
||||||
|
"Close",
|
||||||
|
];
|
||||||
|
|
||||||
|
nock("https://api.airtable.com:443", { encodedQueryParams: true })
|
||||||
|
.persist()
|
||||||
|
.get("/v0/appKYl1H4k9g73sBT/posts")
|
||||||
|
.query((query) => {
|
||||||
|
if (query.pageSize !== "100") return false;
|
||||||
|
if (query.filterByFormula === undefined) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
function () {
|
||||||
|
const parsed = new url.URL(this.req.path, "http://example.com");
|
||||||
|
const query = parsed.searchParams.get("filterByFormula");
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
offset: 0,
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
fields: {
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
commonHeaders,
|
||||||
|
);
|
||||||
|
|
||||||
|
nock("https://api.airtable.com:443", { encodedQueryParams: true })
|
||||||
|
.get("/v0/appKYl1H4k9g73sBT/posts")
|
||||||
|
.query({ pageSize: "100" })
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
"1f8b0800000000000003954fc18ac23014fc157dc7a292562bdaa3c86a570f2215c5ad87da3cd7484cd634825aecb7fb82a0c74598c3639899375382c15c1b5e40f45382e01039a23fdaaea637dee517d58925346027503a4d09565889a41aa394bab6d446f25abb4e923cb3f8abcd95825cc47030ff9687c1f06bdc31f11e360d286c66cf94017fe7ad14c51eb9736965515962b38213525555cf8b5055a94a95e7bd88ccf3520577b219a4773c1147d7256081df64dda61f26be1f056114f65a8cb13529df9ba693b9d027b14bcc6c11dffedbf4d9228307cca9cf27d536f7073da8e6fb7c010000",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"*",
|
||||||
|
"airtable-uncompressed-content-length",
|
||||||
|
"380",
|
||||||
|
"content-encoding",
|
||||||
|
"gzip",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Date",
|
||||||
|
"Thu, 24 Jun 2021 12:24:32 GMT",
|
||||||
|
"Server",
|
||||||
|
"Tengine",
|
||||||
|
"Set-Cookie",
|
||||||
|
"brw=brwHislGvzT3Ws3Yf; path=/; expires=Fri, 24 Jun 2022 12:24:32 GMT; domain=.airtable.com; samesite=none; secure",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-Content-Type-Options",
|
||||||
|
"nosniff",
|
||||||
|
"X-Frame-Options",
|
||||||
|
"DENY",
|
||||||
|
"Content-Length",
|
||||||
|
"233",
|
||||||
|
"Connection",
|
||||||
|
"Close",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
nock("https://api.airtable.com:443", { encodedQueryParams: true })
|
||||||
|
.get("/v0/appKYl1H4k9g73sBT/posts")
|
||||||
|
.query({
|
||||||
|
pageSize: "100",
|
||||||
|
"sort%5B0%5D%5Bfield%5D": "title",
|
||||||
|
"sort%5B0%5D%5Bdirection%5D": "desc",
|
||||||
|
})
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
"1f8b0800000000000003958f416bc2401085ff8ace31a86ca211cd518235d64391144b1b0f4976ac2beb6ebbbb426b30bfdd490bf59243857718debc6f785381c1521b6e217aab4070881a63f5b816fa53ec52f3f49c9ca1073b81b2c954605dee4ef62776c0d221a76d993b7cd7e69b6e34743c5b2fe56116cf172393ec61db03279c44621628a5ee6cb491bc0b17020d12ca53716cb6010bfc3e1bf7fd30f5fd2808a37032608cbd52f2566dfa50bcacce7cccbfd42891edd53e4e851476ffbf6ea5560e95232cb79c94a9bafe9d48759da94c79de9f917b5ea6a0f5a3cef0ae9fb6972ba8a00e5b7c010000",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"*",
|
||||||
|
"airtable-uncompressed-content-length",
|
||||||
|
"380",
|
||||||
|
"content-encoding",
|
||||||
|
"gzip",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Date",
|
||||||
|
"Thu, 24 Jun 2021 13:07:26 GMT",
|
||||||
|
"Server",
|
||||||
|
"Tengine",
|
||||||
|
"Set-Cookie",
|
||||||
|
"brw=brw0sykMWa9glzNWF; path=/; expires=Fri, 24 Jun 2022 13:07:25 GMT; domain=.airtable.com; samesite=none; secure",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-Content-Type-Options",
|
||||||
|
"nosniff",
|
||||||
|
"X-Frame-Options",
|
||||||
|
"DENY",
|
||||||
|
"Content-Length",
|
||||||
|
"237",
|
||||||
|
"Connection",
|
||||||
|
"Close",
|
||||||
|
],
|
||||||
|
);
|
444
packages/airtable/test/getList/index.spec.ts
Normal file
444
packages/airtable/test/getList/index.spec.ts
Normal file
|
@ -0,0 +1,444 @@
|
||||||
|
import { ConditionalFilter } from "@refinedev/core";
|
||||||
|
import dataProvider from "../../src/index";
|
||||||
|
import "./index.mock";
|
||||||
|
|
||||||
|
describe("getList", () => {
|
||||||
|
it("correct response", async () => {
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.data[0]["id"]).toBe("rec9GbXLzd6dxn4Il");
|
||||||
|
expect(response.data[0]["title"]).toBe("Hello World 3!");
|
||||||
|
expect(response.total).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct sorting response", async () => {
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
sorters: [
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
order: "desc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.data[0]["id"]).toBe("recLKRioqifTrPUIz");
|
||||||
|
expect(response.data[0]["title"]).toBe("Hello World!");
|
||||||
|
expect(response.total).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct equals filter for strings", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "eq",
|
||||||
|
field: "title",
|
||||||
|
value: "Hello World!",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must equal exactly string
|
||||||
|
expect(response.data[0]["query"]).toBe('AND({title}="Hello World!")');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct equals filter for numbers", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "eq",
|
||||||
|
field: "age",
|
||||||
|
value: 100,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must equal exactly number
|
||||||
|
expect(response.data[0]["query"]).toBe("AND({age}=100)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct not equals filter for strings", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "ne",
|
||||||
|
field: "title",
|
||||||
|
value: "Hello World!",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must not equal exactly string
|
||||||
|
expect(response.data[0]["query"]).toBe('AND({title}!="Hello World!")');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct not equals filter for numbers", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "ne",
|
||||||
|
field: "age",
|
||||||
|
value: 100,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must not equal exactly number
|
||||||
|
expect(response.data[0]["query"]).toBe("AND({age}!=100)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct less than filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "lt",
|
||||||
|
field: "age",
|
||||||
|
value: 10,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must be less than value (as number)
|
||||||
|
expect(response.data[0]["query"]).toBe("AND({age}<10)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct less than or equal filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "lte",
|
||||||
|
field: "age",
|
||||||
|
value: 10,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must be less than or equal value (as number)
|
||||||
|
expect(response.data[0]["query"]).toBe("AND({age}<=10)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct greater than filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "gt",
|
||||||
|
field: "age",
|
||||||
|
value: 10,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must be greater than value (as number)
|
||||||
|
expect(response.data[0]["query"]).toBe("AND({age}>10)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct greater than or equal filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "gte",
|
||||||
|
field: "age",
|
||||||
|
value: 10,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must be greater than or equal value (as number)
|
||||||
|
expect(response.data[0]["query"]).toBe("AND({age}>=10)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct contains filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "containss",
|
||||||
|
field: "title",
|
||||||
|
value: "Hello",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// must find string in {field} - FIND returns non-zero value
|
||||||
|
expect(response.data[0]["query"]).toBe('AND(FIND("Hello",{title})!=0)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct not contains filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "ncontainss",
|
||||||
|
field: "title",
|
||||||
|
value: "Hello",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// must not find string in {field} - FIND returns zero
|
||||||
|
expect(response.data[0]["query"]).toBe('AND(FIND("Hello",{title})=0)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct case-insensitive contains filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "contains",
|
||||||
|
field: "title",
|
||||||
|
value: "Hello",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// must find lower-cased string in lower-cased {field} - lower-casing both values makes it case-insensitive
|
||||||
|
expect(response.data[0]["query"]).toBe(
|
||||||
|
'AND(FIND(LOWER("Hello"),LOWER({title}))!=0)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct case-insensitive not contains filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "ncontains",
|
||||||
|
field: "title",
|
||||||
|
value: "Hello",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// must not find lower-cased string in lower-cased {field} - lower-casing both values makes it case-insensitive
|
||||||
|
expect(response.data[0]["query"]).toBe(
|
||||||
|
'AND(FIND(LOWER("Hello"),LOWER({title}))=0)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct truthy null filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "null",
|
||||||
|
field: "title",
|
||||||
|
value: undefined,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must be null (blank)
|
||||||
|
expect(response.data[0]["query"]).toBe("AND({title}=BLANK())");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct falsy null filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "nnull",
|
||||||
|
field: "title",
|
||||||
|
value: undefined,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must not be null (blank)
|
||||||
|
expect(response.data[0]["query"]).toBe("AND({title}!=BLANK())");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["between", "nbetween"] as const)(
|
||||||
|
"fails for %s filter",
|
||||||
|
async (operator) => {
|
||||||
|
const filter = {
|
||||||
|
operator,
|
||||||
|
field: "age",
|
||||||
|
value: [10, 15],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
await expect(() => {
|
||||||
|
return dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
}).rejects.toThrow(
|
||||||
|
`Operator ${operator} is not supported for the Airtable data provider`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(["in", "nin"] as const)("fails for %s filter", async (operator) => {
|
||||||
|
const filter = {
|
||||||
|
operator,
|
||||||
|
field: "posts",
|
||||||
|
value: ["uuid-1", "uuid-2"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
await expect(() => {
|
||||||
|
return dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
}).rejects.toThrow(
|
||||||
|
`Operator ${operator} is not supported for the Airtable data provider`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct 'or' conditional filter", async () => {
|
||||||
|
const filter = {
|
||||||
|
operator: "or",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
operator: "eq",
|
||||||
|
value: "Silver Bullet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
operator: "ne",
|
||||||
|
value: "The Mythical Man Month",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as ConditionalFilter;
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
// {field} must either be Silver Bullet or must not be Mythical Man Month
|
||||||
|
expect(response.data[0]["query"]).toBe(
|
||||||
|
'AND(OR({title}="Silver Bullet",{title}!="The Mythical Man Month"))',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct compound 'or' conditional filter", async () => {
|
||||||
|
const filters = [
|
||||||
|
{
|
||||||
|
operator: "or",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
operator: "eq",
|
||||||
|
value: "Silver Bullet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
operator: "ne",
|
||||||
|
value: "The Mythical Man Month",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: "or",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
field: "age",
|
||||||
|
operator: "gt",
|
||||||
|
value: 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "age",
|
||||||
|
operator: "lt",
|
||||||
|
value: 25,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as ConditionalFilter[];
|
||||||
|
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getList({
|
||||||
|
resource: "posts",
|
||||||
|
filters: filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.total).toBe(1);
|
||||||
|
expect(response.data[0]["query"]).toBe(
|
||||||
|
'AND(OR({title}="Silver Bullet",{title}!="The Mythical Man Month"),OR({age}>15,{age}<25))',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
43
packages/airtable/test/getMany/index.mock.ts
Normal file
43
packages/airtable/test/getMany/index.mock.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import nock from "nock";
|
||||||
|
|
||||||
|
nock("https://api.airtable.com:443", { encodedQueryParams: true })
|
||||||
|
.get("/v0/appKYl1H4k9g73sBT/posts")
|
||||||
|
.query({ pageSize: "100" })
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
"1f8b080000000000000395905d6b83301486ff4a974b69875a959acb526aed1c88d875747a114d5c53b2a48be9585bf4b72f6eeca3e0608373111e5e4e9ef79c8124a590b806f0e10c2806b0039302c55e54851b278d571118828a12d665ce4051c5884e5542685e2ba40e9a83fda160b4de12ac6129b8225c695a2099f18e20451e853cea4fbaf577c151be24cbc05fdfeed51ce48d4e48a23338a54fdd72dbb4ad91e98d6c27b5c6d0f4a133b9364d73039ae1b7a41f14f7d1097bf8953b21bb90ecd7ba94984d9325db4d67f38523c32dc87f7aa31aebc978db7ebcf4b46dc6336e185f0019c67bb7cf832c086362b01692e1c1f80afcdac97253cb82b60bdd9e4ed14d42c533ad5219afc2537f274976a4547fabd423f72fb5bc7903b0079a9c21020000",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"*",
|
||||||
|
"airtable-uncompressed-content-length",
|
||||||
|
"545",
|
||||||
|
"content-encoding",
|
||||||
|
"gzip",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Date",
|
||||||
|
"Thu, 24 Jun 2021 13:20:40 GMT",
|
||||||
|
"Server",
|
||||||
|
"Tengine",
|
||||||
|
"Set-Cookie",
|
||||||
|
"brw=brwNd7NWRhWftbS0Q; path=/; expires=Fri, 24 Jun 2022 13:20:40 GMT; domain=.airtable.com; samesite=none; secure",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-Content-Type-Options",
|
||||||
|
"nosniff",
|
||||||
|
"X-Frame-Options",
|
||||||
|
"DENY",
|
||||||
|
"Content-Length",
|
||||||
|
"294",
|
||||||
|
"Connection",
|
||||||
|
"Close",
|
||||||
|
],
|
||||||
|
);
|
20
packages/airtable/test/getMany/index.spec.ts
Normal file
20
packages/airtable/test/getMany/index.spec.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import dataProvider from "../../src/index";
|
||||||
|
import "./index.mock";
|
||||||
|
|
||||||
|
describe("getMany", () => {
|
||||||
|
it("correct response", async () => {
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getMany!({
|
||||||
|
resource: "posts",
|
||||||
|
ids: ["recLKRioqifTrPUIz", "rec9GbXLzd6dxn4Il"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
expect(data[0]["id"]).toBe("rec9GbXLzd6dxn4Il");
|
||||||
|
expect(data[1]["id"]).toBe("recLKRioqifTrPUIz");
|
||||||
|
expect(response.data.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
43
packages/airtable/test/getOne/index.mock.ts
Normal file
43
packages/airtable/test/getOne/index.mock.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import nock from "nock";
|
||||||
|
|
||||||
|
nock("https://api.airtable.com:443", { encodedQueryParams: true })
|
||||||
|
.get("/v0/appKYl1H4k9g73sBT/posts/recLKRioqifTrPUIz")
|
||||||
|
.query({})
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
"1f8b08000000000000031dcb410bc2201880e1bf52df790b958cf038c6d8aa430c23283a0cfd560e43523bd4d87fcf757edf6704a341804775d8b7c6bd4c2ffdf1d47c2183dea0d501c4082176f11dfedb802aa24e557511efce7f405c675d16edce0e4559d56bdf3ce0964134d16232355aeb1667e7ad5ec294a0c744b534cfb932c2684e3639e59252c1b8e0db1521e402d30fb10f929e9a000000",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"*",
|
||||||
|
"airtable-uncompressed-content-length",
|
||||||
|
"154",
|
||||||
|
"content-encoding",
|
||||||
|
"gzip",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Date",
|
||||||
|
"Thu, 24 Jun 2021 13:21:53 GMT",
|
||||||
|
"Server",
|
||||||
|
"Tengine",
|
||||||
|
"Set-Cookie",
|
||||||
|
"brw=brwQBAba4kMeu0MpF; path=/; expires=Fri, 24 Jun 2022 13:21:53 GMT; domain=.airtable.com; samesite=none; secure",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-Content-Type-Options",
|
||||||
|
"nosniff",
|
||||||
|
"X-Frame-Options",
|
||||||
|
"DENY",
|
||||||
|
"Content-Length",
|
||||||
|
"156",
|
||||||
|
"Connection",
|
||||||
|
"Close",
|
||||||
|
],
|
||||||
|
);
|
17
packages/airtable/test/getOne/index.spec.ts
Normal file
17
packages/airtable/test/getOne/index.spec.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import dataProvider from "../../src/index";
|
||||||
|
import "./index.mock";
|
||||||
|
|
||||||
|
describe("getOne", () => {
|
||||||
|
it("correct response", async () => {
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).getOne({ resource: "posts", id: "recLKRioqifTrPUIz" });
|
||||||
|
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
expect(data.title).toBe("Hello World!");
|
||||||
|
expect(data.status).toBe("rejected");
|
||||||
|
expect(data.category).toEqual(["recDBRJljBDFH4rIh"]);
|
||||||
|
});
|
||||||
|
});
|
11
packages/airtable/test/jest.setup.js
Normal file
11
packages/airtable/test/jest.setup.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
const nock = require("nock");
|
||||||
|
|
||||||
|
global.fetch = window.fetch = fetch;
|
||||||
|
global.Request = window.Request = fetch.Request;
|
||||||
|
global.Response = window.Response = fetch.Response;
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
nock.cleanAll();
|
||||||
|
nock.restore();
|
||||||
|
});
|
43
packages/airtable/test/update/index.mock.ts
Normal file
43
packages/airtable/test/update/index.mock.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import nock from "nock";
|
||||||
|
|
||||||
|
nock("https://api.airtable.com:443", { encodedQueryParams: true })
|
||||||
|
.patch("/v0/appKYl1H4k9g73sBT/posts/recLKRioqifTrPUIz", {
|
||||||
|
fields: { title: "Hello World!!" },
|
||||||
|
})
|
||||||
|
.query({})
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
"1f8b08000000000000031dcb410bc2201880e1bfd2bef3162a19e1718cd8aa430c23283a0cfd560e43523bd4d87fcf757edf6704a341804775d8b7c6bd4c2ffdf1d47c2187dea0d501c4082176f11dfedb802aa24e557511efce7f405c675d95edce0e65b5ad57be79c02d8768a2c5646ab4d62dcece5b9d653025e931592dcd73ce8c305a907541b9a454302ef8664908b9c0f4030447c06c9b000000",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"*",
|
||||||
|
"content-encoding",
|
||||||
|
"gzip",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Date",
|
||||||
|
"Thu, 24 Jun 2021 13:24:42 GMT",
|
||||||
|
"Server",
|
||||||
|
"Tengine",
|
||||||
|
"Set-Cookie",
|
||||||
|
"brw=brwQrvJXnX0I6wkCt; path=/; expires=Fri, 24 Jun 2022 13:24:42 GMT; domain=.airtable.com; samesite=none; secure",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-Content-Type-Options",
|
||||||
|
"nosniff",
|
||||||
|
"X-Frame-Options",
|
||||||
|
"DENY",
|
||||||
|
"Content-Length",
|
||||||
|
"157",
|
||||||
|
"Connection",
|
||||||
|
"Close",
|
||||||
|
],
|
||||||
|
);
|
21
packages/airtable/test/update/index.spec.ts
Normal file
21
packages/airtable/test/update/index.spec.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import dataProvider from "../../src/index";
|
||||||
|
import "./index.mock";
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
it("correct response", async () => {
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).update({
|
||||||
|
resource: "posts",
|
||||||
|
id: "recLKRioqifTrPUIz",
|
||||||
|
variables: {
|
||||||
|
title: "Hello World!!",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
expect(data["title"]).toBe("Hello World!!");
|
||||||
|
});
|
||||||
|
});
|
46
packages/airtable/test/updateMany/index.mock.ts
Normal file
46
packages/airtable/test/updateMany/index.mock.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import nock from "nock";
|
||||||
|
|
||||||
|
nock("https://api.airtable.com:443", { encodedQueryParams: true })
|
||||||
|
.patch("/v0/appKYl1H4k9g73sBT/posts/", {
|
||||||
|
records: [
|
||||||
|
{ id: "recLKRioqifTrPUIz", fields: { title: "Hello World!!!" } },
|
||||||
|
{ id: "rec9GbXLzd6dxn4Il", fields: { title: "Hello World!!!" } },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.query({})
|
||||||
|
.reply(
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
"1f8b0800000000000003ad8f416bc2401085ff8a996350d9042336471135ad872229169b1c6276d49575577757680de6b73b69412f3d7810e630bcf7bee14d05064b6db885f8ab02c1216e84d9db5ce8a358a7e6fd2339431bd6026593a9c0bac29dec6f6c87a5434e6e5938dc68f343371a7a349cbfcadd70349ef64cb285bc0d4e3889c44c514add5a6823b9e7797021d420c13c15fbc60f59187458bf13446910c4611447832e636c49c97bb997c9ea7376e67dfead7a89fcbfdce1b492c26e1f6b576ae55039c20acb693255d77f1b4d5d672a53be7f130adfcf143ce3a7fc720554f78d147e010000",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
|
||||||
|
"access-control-allow-methods",
|
||||||
|
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
|
||||||
|
"access-control-allow-origin",
|
||||||
|
"*",
|
||||||
|
"content-encoding",
|
||||||
|
"gzip",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Date",
|
||||||
|
"Thu, 24 Jun 2021 13:26:53 GMT",
|
||||||
|
"Server",
|
||||||
|
"Tengine",
|
||||||
|
"Set-Cookie",
|
||||||
|
"brw=brwPAe5xxAm5wIYu2; path=/; expires=Fri, 24 Jun 2022 13:26:53 GMT; domain=.airtable.com; samesite=none; secure",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-Content-Type-Options",
|
||||||
|
"nosniff",
|
||||||
|
"X-Frame-Options",
|
||||||
|
"DENY",
|
||||||
|
"Content-Length",
|
||||||
|
"235",
|
||||||
|
"Connection",
|
||||||
|
"Close",
|
||||||
|
],
|
||||||
|
);
|
22
packages/airtable/test/updateMany/index.spec.ts
Normal file
22
packages/airtable/test/updateMany/index.spec.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import dataProvider from "../../src/index";
|
||||||
|
import "./index.mock";
|
||||||
|
|
||||||
|
describe("updateMany", () => {
|
||||||
|
it("correct response", async () => {
|
||||||
|
const response = await dataProvider(
|
||||||
|
"keywoytODSr6xAqfg",
|
||||||
|
"appKYl1H4k9g73sBT",
|
||||||
|
).updateMany!({
|
||||||
|
resource: "posts",
|
||||||
|
ids: ["recLKRioqifTrPUIz", "rec9GbXLzd6dxn4Il"],
|
||||||
|
variables: {
|
||||||
|
title: "Hello World!!!",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
expect(data[0]["title"]).toBe("Hello World!!!");
|
||||||
|
expect(data[1]["title"]).toBe("Hello World!!!");
|
||||||
|
});
|
||||||
|
});
|
33
packages/airtable/test/utils/generateFilter.spec.ts
Normal file
33
packages/airtable/test/utils/generateFilter.spec.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { CrudFilters } from "@refinedev/core";
|
||||||
|
import { generateFilter } from "../../src/utils";
|
||||||
|
|
||||||
|
describe("generateFilter", () => {
|
||||||
|
it("should return undefined when no filters are provided", () => {
|
||||||
|
expect(generateFilter()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a filter formula when filters are provided", () => {
|
||||||
|
const filters: CrudFilters = [
|
||||||
|
{ field: "name", operator: "eq", value: "John" },
|
||||||
|
{ field: "age", operator: "gte", value: 30 },
|
||||||
|
];
|
||||||
|
const expected = 'AND({name}="John",{age}>=30)';
|
||||||
|
expect(generateFilter(filters)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a complex filter formula with nested filters", () => {
|
||||||
|
const filters: CrudFilters = [
|
||||||
|
{
|
||||||
|
operator: "or",
|
||||||
|
value: [
|
||||||
|
{ field: "name", operator: "eq", value: "John" },
|
||||||
|
{ field: "name", operator: "eq", value: "Jane" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ field: "age", operator: "gte", value: 30 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = 'AND(OR({name}="John",{name}="Jane"),{age}>=30)';
|
||||||
|
expect(generateFilter(filters)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
44
packages/airtable/test/utils/generateFilterFormula.spec.ts
Normal file
44
packages/airtable/test/utils/generateFilterFormula.spec.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { CrudFilters } from "@refinedev/core";
|
||||||
|
import { generateFilterFormula } from "../../src/utils";
|
||||||
|
|
||||||
|
describe("generateFilterFormula", () => {
|
||||||
|
it("should return an empty array when no filters are provided", () => {
|
||||||
|
expect(generateFilterFormula([])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a formula array when filters are provided", () => {
|
||||||
|
const filters: CrudFilters = [
|
||||||
|
{ field: "name", operator: "eq", value: "John" },
|
||||||
|
{ field: "age", operator: "gte", value: 30 },
|
||||||
|
];
|
||||||
|
const expected = [
|
||||||
|
["=", { field: "name" }, "John"],
|
||||||
|
[">=", { field: "age" }, 30],
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(generateFilterFormula(filters)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a complex formula array with nested filters", () => {
|
||||||
|
const filters: CrudFilters = [
|
||||||
|
{
|
||||||
|
operator: "or",
|
||||||
|
value: [
|
||||||
|
{ field: "name", operator: "eq", value: "John" },
|
||||||
|
{ field: "name", operator: "eq", value: "Jane" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ field: "age", operator: "gte", value: 30 },
|
||||||
|
];
|
||||||
|
const expected = [
|
||||||
|
[
|
||||||
|
"OR",
|
||||||
|
["=", { field: "name" }, "John"],
|
||||||
|
["=", { field: "name" }, "Jane"],
|
||||||
|
],
|
||||||
|
[">=", { field: "age" }, 30],
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(generateFilterFormula(filters)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { CrudFilter } from "@refinedev/core";
|
||||||
|
|
||||||
|
import { generateLogicalFilterFormula } from "../../src/utils";
|
||||||
|
import { LogicalFilter } from "@refinedev/core";
|
||||||
|
|
||||||
|
describe("generateLogicalFilterFormula", () => {
|
||||||
|
it("should generate a formula for simple operators", () => {
|
||||||
|
const filter: CrudFilter = {
|
||||||
|
field: "age",
|
||||||
|
operator: "gte",
|
||||||
|
value: 30,
|
||||||
|
};
|
||||||
|
const expected = [">=", { field: "age" }, 30];
|
||||||
|
|
||||||
|
expect(generateLogicalFilterFormula(filter)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate a formula for contains operators", () => {
|
||||||
|
const filter: CrudFilter = {
|
||||||
|
field: "name",
|
||||||
|
operator: "contains",
|
||||||
|
value: "John",
|
||||||
|
};
|
||||||
|
const expected = [
|
||||||
|
"!=",
|
||||||
|
["FIND", ["LOWER", "John"], ["LOWER", { field: "name" }]],
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(generateLogicalFilterFormula(filter)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate a formula for null operators", () => {
|
||||||
|
const filter = { field: "email", operator: "null" } as LogicalFilter;
|
||||||
|
const expected = ["=", { field: "email" }, ["BLANK"]];
|
||||||
|
|
||||||
|
expect(generateLogicalFilterFormula(filter)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate a formula for nnull operators", () => {
|
||||||
|
const filter = { field: "email", operator: "nnull" } as LogicalFilter;
|
||||||
|
const expected = ["!=", { field: "email" }, ["BLANK"]];
|
||||||
|
|
||||||
|
expect(generateLogicalFilterFormula(filter)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error for unsupported operators", () => {
|
||||||
|
const filter = {
|
||||||
|
field: "age",
|
||||||
|
operator: "unsupported",
|
||||||
|
value: 30,
|
||||||
|
} as unknown as LogicalFilter;
|
||||||
|
|
||||||
|
expect(() => generateLogicalFilterFormula(filter)).toThrowError(
|
||||||
|
"Operator unsupported is not supported for the Airtable data provider",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
22
packages/airtable/test/utils/generateSort.spec.ts
Normal file
22
packages/airtable/test/utils/generateSort.spec.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { CrudSorting } from "@refinedev/core";
|
||||||
|
import { generateSort } from "../../src/utils";
|
||||||
|
|
||||||
|
describe("generateSort", () => {
|
||||||
|
it("should return undefined if no sorters are provided", () => {
|
||||||
|
expect(generateSort(undefined)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate an array of sorting objects", () => {
|
||||||
|
const sorters: CrudSorting = [
|
||||||
|
{ field: "name", order: "asc" },
|
||||||
|
{ field: "age", order: "desc" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{ field: "name", direction: "asc" },
|
||||||
|
{ field: "age", direction: "desc" },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(generateSort(sorters)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
31
packages/airtable/test/utils/isContainsOperator.spec.ts
Normal file
31
packages/airtable/test/utils/isContainsOperator.spec.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { isContainssOperator, isContainsOperator } from "../../src/utils";
|
||||||
|
|
||||||
|
describe("Operators", () => {
|
||||||
|
describe("isContainssOperator", () => {
|
||||||
|
it("should return true if operator is containss", () => {
|
||||||
|
expect(isContainssOperator("containss")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true if operator is ncontainss", () => {
|
||||||
|
expect(isContainssOperator("ncontainss")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if operator is not containss or ncontainss", () => {
|
||||||
|
expect(isContainssOperator("contains")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isContainsOperator", () => {
|
||||||
|
it("should return true if operator is contains", () => {
|
||||||
|
expect(isContainsOperator("contains")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true if operator is ncontains", () => {
|
||||||
|
expect(isContainsOperator("ncontains")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if operator is not contains or ncontains", () => {
|
||||||
|
expect(isContainsOperator("containss")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
30
packages/airtable/test/utils/isSimpleOperator.spec.ts
Normal file
30
packages/airtable/test/utils/isSimpleOperator.spec.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { isSimpleOperator, simpleOperatorMapping } from "../../src/utils";
|
||||||
|
|
||||||
|
describe("SimpleOperators", () => {
|
||||||
|
describe("isSimpleOperator", () => {
|
||||||
|
it("should return true if operator is a simple operator", () => {
|
||||||
|
expect(isSimpleOperator("eq")).toBe(true);
|
||||||
|
expect(isSimpleOperator("ne")).toBe(true);
|
||||||
|
expect(isSimpleOperator("lt")).toBe(true);
|
||||||
|
expect(isSimpleOperator("lte")).toBe(true);
|
||||||
|
expect(isSimpleOperator("gt")).toBe(true);
|
||||||
|
expect(isSimpleOperator("gte")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if operator is not a simple operator", () => {
|
||||||
|
expect(isSimpleOperator("contains")).toBe(false);
|
||||||
|
expect(isSimpleOperator("containss")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("simpleOperatorMapping", () => {
|
||||||
|
it("should map simple operators to their corresponding Airtable symbols", () => {
|
||||||
|
expect(simpleOperatorMapping["eq"]).toBe("=");
|
||||||
|
expect(simpleOperatorMapping["ne"]).toBe("!=");
|
||||||
|
expect(simpleOperatorMapping["lt"]).toBe("<");
|
||||||
|
expect(simpleOperatorMapping["lte"]).toBe("<=");
|
||||||
|
expect(simpleOperatorMapping["gt"]).toBe(">");
|
||||||
|
expect(simpleOperatorMapping["gte"]).toBe(">=");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
21
packages/airtable/tsconfig.declarations.json
Normal file
21
packages/airtable/tsconfig.declarations.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"test",
|
||||||
|
"../test/**/*",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.tsx"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"declarationDir": "dist",
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"declarationMap": true
|
||||||
|
}
|
||||||
|
}
|
8
packages/airtable/tsconfig.json
Normal file
8
packages/airtable/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"include": ["src", "types", "refine.config.js"],
|
||||||
|
"extends": "../../tsconfig.build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"baseUrl": "."
|
||||||
|
}
|
||||||
|
}
|
24
packages/airtable/tsup.config.ts
Normal file
24
packages/airtable/tsup.config.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineConfig } from "tsup";
|
||||||
|
import { NodeResolvePlugin } from "@esbuild-plugins/node-resolve";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["src/index.ts"],
|
||||||
|
splitting: false,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: false,
|
||||||
|
platform: "browser",
|
||||||
|
esbuildPlugins: [
|
||||||
|
NodeResolvePlugin({
|
||||||
|
extensions: [".js", "ts", "tsx", "jsx"],
|
||||||
|
onResolved: (resolved) => {
|
||||||
|
if (resolved.includes("node_modules")) {
|
||||||
|
return {
|
||||||
|
external: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onSuccess: "tsc --project tsconfig.declarations.json",
|
||||||
|
});
|
11
packages/antd/.npmignore
Normal file
11
packages/antd/.npmignore
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
test
|
||||||
|
jest.config.js
|
||||||
|
**/*.spec.ts
|
||||||
|
**/*.spec.tsx
|
||||||
|
**/*.test.ts
|
||||||
|
**/*.test.tsx
|
||||||
|
tsup.config.ts
|
||||||
|
tsconfig.test.json
|
||||||
|
tsconfig.declarations.json
|
1
packages/antd/.npmrc
Normal file
1
packages/antd/.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
legacy-peer-deps=true
|
3380
packages/antd/CHANGELOG.md
Normal file
3380
packages/antd/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
71
packages/antd/README.md
Normal file
71
packages/antd/README.md
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<div align="center" style="margin: 30px;">
|
||||||
|
<a href="https://refine.dev">
|
||||||
|
<img alt="refine logo" src="https://refine.ams3.cdn.digitaloceanspaces.com/readme/refine-readme-banner.png">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://refine.dev">Home Page</a> |
|
||||||
|
<a href="https://discord.gg/refine">Discord</a> |
|
||||||
|
<a href="https://refine.dev/examples/">Examples</a> |
|
||||||
|
<a href="https://refine.dev/blog/">Blog</a> |
|
||||||
|
<a href="https://refine.dev/docs/">Documentation</a>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
[![Discord](https://img.shields.io/discord/837692625737613362.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/refine)
|
||||||
|
[![Twitter Follow](https://img.shields.io/twitter/follow/refine_dev?style=social)](https://twitter.com/refine_dev)
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/posts/refine-3?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-refine-3" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=362220&theme=light&period=daily" alt="refine - 100% open source React framework to build web apps 3x faster | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div align="center">refine is an open-source, headless React framework for developers building enterprise internal tools, admin panels, dashboards, B2B applications.
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
It eliminates repetitive tasks in CRUD operations and provides industry-standard solutions for critical project components like **authentication**, **access control**, **routing**, **networking**, **state management**, and **i18n**.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# Ant Design integration for refine
|
||||||
|
|
||||||
|
[Ant Design](https://ant.design/) is a React.js UI library that contains easy-to-use components that are useful for building interactive user interfaces.
|
||||||
|
|
||||||
|
[refine](https://refine.dev/) is **headless by design**, offering unlimited styling and customization options. Moreover, refine ships with ready-made integrations for [Ant Design](https://ant.design/), [Material UI](https://mui.com/material-ui/getting-started/overview/), [Mantine](https://mantine.dev/), and [Chakra UI](https://chakra-ui.com/) for convenience.
|
||||||
|
|
||||||
|
refine has connectors for 15+ backend services, including REST API, [GraphQL](https://graphql.org/), and popular services like [Airtable](https://www.airtable.com/), [Strapi](https://strapi.io/), [Supabase](https://supabase.com/), [Firebase](https://firebase.google.com/), and [NestJS](https://nestjs.com/).
|
||||||
|
|
||||||
|
## Installation & Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install @refinedev/antd antd
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ThemedLayoutV2 } from "@refinedev/antd";
|
||||||
|
|
||||||
|
import "@refinedev/antd/dist/reset.css";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Refine
|
||||||
|
/* ... */
|
||||||
|
>
|
||||||
|
<ThemedLayoutV2>{/* ... */}</ThemedLayoutV2>
|
||||||
|
</Refine>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- For more detailed information and usage, refer to the [refine Ant Design documentation](https://refine.dev/docs/api-reference/antd/).
|
||||||
|
- [Refer to complete refine tutorial with Ant Design](https://refine.dev/docs/tutorial/introduction/select-framework/)
|
||||||
|
- [Refer to documentation for more info about refine](https://refine.dev/docs/).
|
||||||
|
- [Step up to refine tutorials](https://refine.dev/docs/tutorial/introduction/index/).
|
31
packages/antd/jest.config.js
Normal file
31
packages/antd/jest.config.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||||
|
const { compilerOptions } = require("./tsconfig.json");
|
||||||
|
|
||||||
|
const paths = compilerOptions.paths ? compilerOptions.paths : {};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
preset: "ts-jest",
|
||||||
|
rootDir: "./",
|
||||||
|
testEnvironment: "jsdom",
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/test/jest.setup.ts"],
|
||||||
|
testPathIgnorePatterns: ["<rootDir>/node_modules/"],
|
||||||
|
moduleNameMapper: {
|
||||||
|
...pathsToModuleNameMapper(paths, { prefix: "<rootDir>/" }),
|
||||||
|
"\\.css$": "identity-obj-proxy",
|
||||||
|
"^antd/es/": "antd/lib/",
|
||||||
|
"^.+\\.tsx?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsconfig: "<rootDir>/tsconfig.test.json",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displayName: "antd",
|
||||||
|
transform: {
|
||||||
|
"^.+\\.svg$": "<rootDir>/test/svgTransform.ts",
|
||||||
|
},
|
||||||
|
coveragePathIgnorePatterns: [
|
||||||
|
"<rootDir>/src/index.ts",
|
||||||
|
"<rootDir>/src/interfaces/",
|
||||||
|
],
|
||||||
|
};
|
75
packages/antd/package.json
Normal file
75
packages/antd/package.json
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"name": "@refinedev/antd",
|
||||||
|
"version": "5.37.2",
|
||||||
|
"description": "refine is a React-based framework for building internal tools, rapidly. It ships with Ant Design System, an enterprise-level UI toolkit.",
|
||||||
|
"private": false,
|
||||||
|
"sideEffects": [
|
||||||
|
"*.css"
|
||||||
|
],
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/esm/index.js",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsup --watch --format esm,cjs,iife --legacy-output",
|
||||||
|
"build": "tsup --format esm,cjs,iife --minify --legacy-output",
|
||||||
|
"test": "jest --passWithNoTests --runInBand",
|
||||||
|
"prepare": "npm run build",
|
||||||
|
"generate-theme": "npx @emeks/antd-custom-theme-generator -w --antd ../../node_modules/antd ./src/assets/styles/custom-theme.less ./src/assets/styles/styles.min.css"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@refinedev/core": "^4.46.1",
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0",
|
||||||
|
"@types/react-dom": "^17.0.0 || ^18.0.0",
|
||||||
|
"antd": "^5.0.5",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@refinedev/cli": "^2.16.22",
|
||||||
|
"@refinedev/ui-tests": "^1.14.1",
|
||||||
|
"@refinedev/core": "^4.46.2",
|
||||||
|
"@esbuild-plugins/node-resolve": "^0.1.4",
|
||||||
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
|
"@testing-library/react": "^13.1.1",
|
||||||
|
"@testing-library/react-hooks": "^8.0.0",
|
||||||
|
"@testing-library/user-event": "^14.1.1",
|
||||||
|
"@types/jest": "^29.2.4",
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0",
|
||||||
|
"@types/testing-library__jest-dom": "^5.14.3",
|
||||||
|
"esbuild-copy-static-files": "^0.1.0",
|
||||||
|
"esbuild-plugin-inline-image": "^0.0.8",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"jest": "^29.3.1",
|
||||||
|
"jest-environment-jsdom": "^29.3.1",
|
||||||
|
"postcss": "^8.1.4",
|
||||||
|
"react-router-dom": "^6.8.1",
|
||||||
|
"ts-jest": "^29.0.3",
|
||||||
|
"tsup": "^6.7.0",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "5.0.1",
|
||||||
|
"@ant-design/pro-layout": "7.17.12",
|
||||||
|
"@refinedev/ui-types": "^1.22.4",
|
||||||
|
"@tanstack/react-query": "^4.10.1",
|
||||||
|
"antd": "^5.0.5",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
|
"react-markdown": "^6.0.1",
|
||||||
|
"remark-gfm": "^1.0.0",
|
||||||
|
"sunflower-antd": "1.0.0-beta.3",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
},
|
||||||
|
"author": "refine",
|
||||||
|
"license": "MIT",
|
||||||
|
"gitHead": "829f5a516f98c06f666d6be3e6e6099c75c07719",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/refinedev/refine.git",
|
||||||
|
"directory": "packages/antd"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
629
packages/antd/refine.config.js
Normal file
629
packages/antd/refine.config.js
Normal file
|
@ -0,0 +1,629 @@
|
||||||
|
const { dirname, join } = require("path");
|
||||||
|
const {
|
||||||
|
getImports,
|
||||||
|
appendAfterImports,
|
||||||
|
getFileContent,
|
||||||
|
} = require("@refinedev/cli");
|
||||||
|
|
||||||
|
/** @type {import('@refinedev/cli').RefineConfig} */
|
||||||
|
module.exports = {
|
||||||
|
group: "UI Framework",
|
||||||
|
swizzle: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
group: "Buttons",
|
||||||
|
label: "ShowButton",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/buttons/show/index.tsx",
|
||||||
|
dest: "./components/buttons/show.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Buttons",
|
||||||
|
label: "CreateButton",
|
||||||
|
message: `
|
||||||
|
**\`Warning:\`**
|
||||||
|
This component is used in the below component. If you want to change it, you can run the **swizzle** command for the below component or you can use props to override the default buttons.
|
||||||
|
- <List/>
|
||||||
|
`,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/buttons/create/index.tsx",
|
||||||
|
dest: "./components/buttons/create.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Buttons",
|
||||||
|
label: "CloneButton",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/buttons/clone/index.tsx",
|
||||||
|
dest: "./components/buttons/clone.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Buttons",
|
||||||
|
label: "DeleteButton",
|
||||||
|
message: `
|
||||||
|
**\`Warning:\`**
|
||||||
|
This component is used in the below components. If you want to change it, you can run the **swizzle** command for the below components or you can use props to override the default buttons.
|
||||||
|
- <Edit/>
|
||||||
|
- <List/>
|
||||||
|
`,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/buttons/delete/index.tsx",
|
||||||
|
dest: "./components/buttons/delete.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Buttons",
|
||||||
|
label: "EditButton",
|
||||||
|
message: `
|
||||||
|
**\`Warning:\`**
|
||||||
|
This component is used in the below component. If you want to change it, you can run the **swizzle** command for the below component or you can use props to override the default buttons.
|
||||||
|
- <Show/>
|
||||||
|
`,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/buttons/edit/index.tsx",
|
||||||
|
dest: "./components/buttons/edit.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Buttons",
|
||||||
|
label: "ExportButton",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/buttons/export/index.tsx",
|
||||||
|
dest: "./components/buttons/export.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Buttons",
|
||||||
|
label: "ImportButton",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/buttons/import/index.tsx",
|
||||||
|
dest: "./components/buttons/import.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Buttons",
|
||||||
|
label: "ListButton",
|
||||||
|
message: `
|
||||||
|
**\`Warning:\`**
|
||||||
|
This component is used in the below components. If you want to change it, you can run the **swizzle** command for the below components or you can use props to override the default buttons.
|
||||||
|
- <Edit/>
|
||||||
|
- <Show/>
|
||||||
|
`,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/buttons/list/index.tsx",
|
||||||
|
dest: "./components/buttons/list.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Buttons",
|
||||||
|
label: "RefreshButton",
|
||||||
|
message: `
|
||||||
|
**\`Warning:\`**
|
||||||
|
This component is used in the below components. If you want to change it, you can run the **swizzle** command for the below components or you can use props to override the default buttons.
|
||||||
|
- <Edit/>
|
||||||
|
- <Show/>
|
||||||
|
`,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/buttons/refresh/index.tsx",
|
||||||
|
dest: "./components/buttons/refresh.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Buttons",
|
||||||
|
label: "SaveButton",
|
||||||
|
message: `
|
||||||
|
**\`Warning:\`**
|
||||||
|
This component is used in the below components. If you want to change it, you can run the **swizzle** command for the below components or you can use props to override the default buttons.
|
||||||
|
- <Create/>
|
||||||
|
- <Edit/>
|
||||||
|
`,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/buttons/save/index.tsx",
|
||||||
|
dest: "./components/buttons/save.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Basic Views",
|
||||||
|
label: "Create",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/crud/create/index.tsx",
|
||||||
|
dest: "./components/crud/create.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Basic Views",
|
||||||
|
label: "List",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/crud/list/index.tsx",
|
||||||
|
dest: "./components/crud/list.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Basic Views",
|
||||||
|
label: "Show",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/crud/show/index.tsx",
|
||||||
|
dest: "./components/crud/show.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Basic Views",
|
||||||
|
label: "Edit",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/crud/edit/index.tsx",
|
||||||
|
dest: "./components/crud/edit.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Fields",
|
||||||
|
label: "BooleanField",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/fields/boolean/index.tsx",
|
||||||
|
dest: "./components/fields/boolean.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Fields",
|
||||||
|
label: "DateField",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/fields/date/index.tsx",
|
||||||
|
dest: "./components/fields/date.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Fields",
|
||||||
|
label: "EmailField",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/fields/email/index.tsx",
|
||||||
|
dest: "./components/fields/email.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Fields",
|
||||||
|
label: "FileField",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/fields/file/index.tsx",
|
||||||
|
dest: "./components/fields/file.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Fields",
|
||||||
|
label: "ImageField",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/fields/image/index.tsx",
|
||||||
|
dest: "./components/fields/image.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Fields",
|
||||||
|
label: "MarkdownField",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/fields/markdown/index.tsx",
|
||||||
|
dest: "./components/fields/markdown.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Fields",
|
||||||
|
label: "NumberField",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/fields/number/index.tsx",
|
||||||
|
dest: "./components/fields/number.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Fields",
|
||||||
|
label: "TagField",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/fields/tag/index.tsx",
|
||||||
|
dest: "./components/fields/tag.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Fields",
|
||||||
|
label: "TextField",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/fields/text/index.tsx",
|
||||||
|
dest: "./components/fields/text.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Fields",
|
||||||
|
label: "UrlField",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/fields/url/index.tsx",
|
||||||
|
dest: "./components/fields/url.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Pages",
|
||||||
|
label: "ErrorPage",
|
||||||
|
message: `
|
||||||
|
**\`Info:\`**
|
||||||
|
If you want to see an example of error page in use, you can refer to the documentation at https://refine.dev/docs/packages/documentation/routers
|
||||||
|
`,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/pages/error/index.tsx",
|
||||||
|
dest: "./components/pages/error.tsx",
|
||||||
|
transform: (content) => {
|
||||||
|
let newContent = content;
|
||||||
|
|
||||||
|
// for remove RefineErorrPageProps
|
||||||
|
const refineErrorPagePropsRegex =
|
||||||
|
/React\.FC<RefineErrorPageProps>/g;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
refineErrorPagePropsRegex,
|
||||||
|
"React.FC",
|
||||||
|
);
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Pages",
|
||||||
|
label: "AuthPage",
|
||||||
|
message: `
|
||||||
|
**\`Info:\`**
|
||||||
|
If you want to see examples of authentication pages in use, you can refer to the documentation at https://refine.dev/docs/packages/documentation/routers
|
||||||
|
`,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/pages/auth/index.tsx",
|
||||||
|
dest: "./components/pages/auth/index.tsx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/components/pages/auth/components/forgotPassword/index.tsx",
|
||||||
|
dest: "./components/pages/auth/components/forgotPassword.tsx",
|
||||||
|
transform: (content) => {
|
||||||
|
let newContent = content;
|
||||||
|
|
||||||
|
// for change style import path
|
||||||
|
const styleImportRegex = /"\.\.\/styles";/g;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
styleImportRegex,
|
||||||
|
`"./styles";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/components/pages/auth/components/login/index.tsx",
|
||||||
|
dest: "./components/pages/auth/components/login.tsx",
|
||||||
|
transform: (content) => {
|
||||||
|
let newContent = content;
|
||||||
|
|
||||||
|
// for change style import path
|
||||||
|
const styleImportRegex = /"\.\.\/styles";/g;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
styleImportRegex,
|
||||||
|
`"./styles";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/components/pages/auth/components/register/index.tsx",
|
||||||
|
dest: "./components/pages/auth/components/register.tsx",
|
||||||
|
transform: (content) => {
|
||||||
|
let newContent = content;
|
||||||
|
|
||||||
|
// for change style import path
|
||||||
|
const styleImportRegex = /"\.\.\/styles";/g;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
styleImportRegex,
|
||||||
|
`"./styles";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/components/pages/auth/components/updatePassword/index.tsx",
|
||||||
|
dest: "./components/pages/auth/components/updatePassword.tsx",
|
||||||
|
transform: (content) => {
|
||||||
|
let newContent = content;
|
||||||
|
|
||||||
|
// for change style import path
|
||||||
|
const styleImportRegex = /"\.\.\/styles";/g;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
styleImportRegex,
|
||||||
|
`"./styles";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/components/pages/auth/components/index.tsx",
|
||||||
|
dest: "./components/pages/auth/components/index.tsx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/components/pages/auth/components/styles.ts",
|
||||||
|
dest: "./components/pages/auth/components/styles.ts",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Other",
|
||||||
|
label: "Breadcrumb",
|
||||||
|
message: `
|
||||||
|
**\`Warning:\`**
|
||||||
|
This component is used in the below components. If you want to change it, you can use props to override the default breadcrumb or you can manage globally with the **options** prop to the **<Refine/>** component.
|
||||||
|
- <Edit/>
|
||||||
|
- <List/>
|
||||||
|
- <Show/>
|
||||||
|
- <Create/>
|
||||||
|
|
||||||
|
**\`Passing Breadcrumb Globally:\`**
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
// title: App.tsx
|
||||||
|
import { Breadcrumb } from "components/breadcrumb";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Refine
|
||||||
|
options={{
|
||||||
|
breadcrumb: <Breadcrumb />
|
||||||
|
/* ... */
|
||||||
|
}}
|
||||||
|
/* ... */
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/breadcrumb/index.tsx",
|
||||||
|
dest: "./components/breadcrumb.tsx",
|
||||||
|
transform: (content) => {
|
||||||
|
let newContent = content;
|
||||||
|
|
||||||
|
// for remove type export
|
||||||
|
const breadcrumbPropsExportRegex =
|
||||||
|
/export type BreadcrumbProps = RefineBreadcrumbProps<AntdBreadcrumbProps>;?/g;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
breadcrumbPropsExportRegex,
|
||||||
|
`import { BreadcrumbProps } from "@refinedev/antd";`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// change the breadcrumb import path
|
||||||
|
const breadcrumbImportRegex =
|
||||||
|
/BreadcrumbProps as AntdBreadcrumbProps,/g;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
breadcrumbImportRegex,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Other",
|
||||||
|
label: "ThemedLayoutV2",
|
||||||
|
message: `
|
||||||
|
**\`Warning:\`**
|
||||||
|
If you want to change the default layout;
|
||||||
|
You should pass layout related components to the **<ThemedLayoutV2 />** component's props.
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
// title: App.tsx
|
||||||
|
import { ThemedLayoutV2 } from "components/layout";
|
||||||
|
import { ThemedHeaderV2 } from "components/layout/header";
|
||||||
|
import { ThemedSiderV2 } from "components/layout/sider";
|
||||||
|
import { ThemedTitleV2 } from "components/layout/title";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Refine
|
||||||
|
/* ... */
|
||||||
|
>
|
||||||
|
<ThemedLayoutV2 Header={ThemedHeaderV2} Sider={ThemedSiderV2} Title={ThemedTitleV2}>
|
||||||
|
/* ... */
|
||||||
|
</ThemedLayoutV2>
|
||||||
|
</Refine>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
src: "./src/components/themedLayoutV2/sider/index.tsx",
|
||||||
|
dest: "./components/layout/sider.tsx",
|
||||||
|
transform: (content) => {
|
||||||
|
let newContent = content;
|
||||||
|
const imports = getImports(content);
|
||||||
|
|
||||||
|
imports.map((importItem) => {
|
||||||
|
// handle @components import replacement
|
||||||
|
if (
|
||||||
|
importItem.importPath === "@components" ||
|
||||||
|
importItem.importPath === "@hooks"
|
||||||
|
) {
|
||||||
|
const newStatement = `import ${importItem.namedImports} from "@refinedev/antd";`;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
importItem.statement,
|
||||||
|
newStatement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add content of ./styles.ts and remove import
|
||||||
|
if (importItem.importPath === "./styles") {
|
||||||
|
newContent = newContent.replace(
|
||||||
|
importItem.statement,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
let appending = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stylesContent = getFileContent(
|
||||||
|
join(
|
||||||
|
dirname(
|
||||||
|
"./src/components/themedLayoutV2/sider/index.tsx",
|
||||||
|
),
|
||||||
|
"/styles.ts",
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
).replace("export const", "const");
|
||||||
|
|
||||||
|
appending = stylesContent;
|
||||||
|
} catch (err) {
|
||||||
|
// console.log(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent = appendAfterImports(
|
||||||
|
newContent,
|
||||||
|
appending,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/components/themedLayoutV2/header/index.tsx",
|
||||||
|
dest: "./components/layout/header.tsx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/components/themedLayoutV2/title/index.tsx",
|
||||||
|
dest: "./components/layout/title.tsx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./src/components/themedLayoutV2/index.tsx",
|
||||||
|
dest: "./components/layout/index.tsx",
|
||||||
|
transform: (content) => {
|
||||||
|
let newContent = content;
|
||||||
|
const imports = getImports(content);
|
||||||
|
|
||||||
|
imports.map((importItem) => {
|
||||||
|
// handle @components import replacement
|
||||||
|
if (
|
||||||
|
importItem.importPath === "@components" ||
|
||||||
|
importItem.importPath === "@contexts" ||
|
||||||
|
importItem.importPath === "@hooks"
|
||||||
|
) {
|
||||||
|
const newStatement = `import ${importItem.namedImports} from "@refinedev/antd";`;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
importItem.statement,
|
||||||
|
newStatement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transform: (content) => {
|
||||||
|
let newContent = content;
|
||||||
|
const imports = getImports(content);
|
||||||
|
|
||||||
|
imports.map((importItem) => {
|
||||||
|
if (importItem.importPath === "@components") {
|
||||||
|
const newStatement = `import ${importItem.namedImports} from "@refinedev/antd";`;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
importItem.statement,
|
||||||
|
newStatement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for ui-types
|
||||||
|
if (importItem.importPath === "@refinedev/ui-types") {
|
||||||
|
newContent = newContent.replace(importItem.statement, "");
|
||||||
|
|
||||||
|
// prop is data-testid
|
||||||
|
// remove data-testid={*} from props
|
||||||
|
const testIdPropRegex = /data-testid={.*?}/g;
|
||||||
|
|
||||||
|
newContent = newContent.replace(testIdPropRegex, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// for prop types
|
||||||
|
if (
|
||||||
|
importItem.importPath === "../types" ||
|
||||||
|
importItem.importPath === "./types"
|
||||||
|
) {
|
||||||
|
const newStatement = `import type ${importItem.namedImports} from "@refinedev/antd";`;
|
||||||
|
|
||||||
|
newContent = newContent.replace(
|
||||||
|
importItem.statement,
|
||||||
|
newStatement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
254
packages/antd/src/assets/styles/reset.css
Normal file
254
packages/antd/src/assets/styles/reset.css
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
/* stylelint-disable */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
input::-ms-clear,
|
||||||
|
input::-ms-reveal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
font-family: sans-serif;
|
||||||
|
line-height: 1.15;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-ms-overflow-style: scrollbar;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
@-ms-viewport {
|
||||||
|
width: device-width;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
[tabindex="-1"]:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box;
|
||||||
|
height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
abbr[title],
|
||||||
|
abbr[data-original-title] {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
border-bottom: 0;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
address {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="number"],
|
||||||
|
textarea {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
ol ol,
|
||||||
|
ul ul,
|
||||||
|
ol ul,
|
||||||
|
ul ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
dt {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 1em;
|
||||||
|
}
|
||||||
|
dfn {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
figure {
|
||||||
|
margin: 0 0 1em;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
a,
|
||||||
|
area,
|
||||||
|
button,
|
||||||
|
[role="button"],
|
||||||
|
input:not([type="range"]),
|
||||||
|
label,
|
||||||
|
select,
|
||||||
|
summary,
|
||||||
|
textarea {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
caption {
|
||||||
|
padding-top: 0.75em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
text-align: left;
|
||||||
|
caption-side: bottom;
|
||||||
|
}
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
optgroup,
|
||||||
|
textarea {
|
||||||
|
margin: 0;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
button,
|
||||||
|
html [type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
input[type="date"],
|
||||||
|
input[type="time"],
|
||||||
|
input[type="datetime-local"],
|
||||||
|
input[type="month"] {
|
||||||
|
-webkit-appearance: listbox;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
legend {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 1.5em;
|
||||||
|
line-height: inherit;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
[type="search"] {
|
||||||
|
outline-offset: -2px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
[type="search"]::-webkit-search-cancel-button,
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
output {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
mark {
|
||||||
|
padding: 0.2em;
|
||||||
|
background-color: #feffe6;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { autoSaveIndicatorTests } from "@refinedev/ui-tests";
|
||||||
|
|
||||||
|
import { AutoSaveIndicator } from "./";
|
||||||
|
|
||||||
|
describe("AutoSaveIndicator", () => {
|
||||||
|
autoSaveIndicatorTests.bind(this)(AutoSaveIndicator);
|
||||||
|
});
|
86
packages/antd/src/components/autoSaveIndicator/index.tsx
Normal file
86
packages/antd/src/components/autoSaveIndicator/index.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
AutoSaveIndicatorProps,
|
||||||
|
useTranslate,
|
||||||
|
AutoSaveIndicator as AutoSaveIndicatorCore,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
import { Typography, theme } from "antd";
|
||||||
|
import {
|
||||||
|
EllipsisOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
|
||||||
|
export const AutoSaveIndicator: React.FC<AutoSaveIndicatorProps> = ({
|
||||||
|
status,
|
||||||
|
elements: {
|
||||||
|
success = (
|
||||||
|
<Message
|
||||||
|
key="autoSave.success"
|
||||||
|
defaultMessage="saved"
|
||||||
|
icon={<CheckCircleOutlined />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
error = (
|
||||||
|
<Message
|
||||||
|
key="autoSave.error"
|
||||||
|
defaultMessage="auto save failure"
|
||||||
|
icon={<ExclamationCircleOutlined />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
loading = (
|
||||||
|
<Message
|
||||||
|
key="autoSave.loading"
|
||||||
|
defaultMessage="saving..."
|
||||||
|
icon={<SyncOutlined />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
idle = (
|
||||||
|
<Message
|
||||||
|
key="autoSave.idle"
|
||||||
|
defaultMessage="waiting for changes"
|
||||||
|
icon={<EllipsisOutlined />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
} = {},
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<AutoSaveIndicatorCore
|
||||||
|
status={status}
|
||||||
|
elements={{
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
idle,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Message = ({
|
||||||
|
key,
|
||||||
|
defaultMessage,
|
||||||
|
icon,
|
||||||
|
}: {
|
||||||
|
key: string;
|
||||||
|
defaultMessage: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
const { useToken } = theme;
|
||||||
|
const { token } = useToken();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography.Text
|
||||||
|
style={{
|
||||||
|
marginRight: 5,
|
||||||
|
color: token.colorTextTertiary,
|
||||||
|
fontSize: ".8rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate(key, defaultMessage)}
|
||||||
|
<span style={{ marginLeft: ".2rem" }}>{icon}</span>
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
};
|
53
packages/antd/src/components/breadcrumb/index.spec.tsx
Normal file
53
packages/antd/src/components/breadcrumb/index.spec.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
import { breadcrumbTests } from "@refinedev/ui-tests";
|
||||||
|
|
||||||
|
import { render, TestWrapper, ITestWrapperProps, act } from "@test";
|
||||||
|
import { Breadcrumb } from "./";
|
||||||
|
|
||||||
|
const renderBreadcrumb = (
|
||||||
|
children: ReactNode,
|
||||||
|
wrapperProps: ITestWrapperProps = {},
|
||||||
|
) => {
|
||||||
|
return render(
|
||||||
|
<Routes>
|
||||||
|
<Route path="/:resource/:action" element={children} />
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper(wrapperProps),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DummyDashboard = () => <div>Dashboard</div>;
|
||||||
|
|
||||||
|
describe("Breadcrumb", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(console, "warn").mockImplementation(jest.fn());
|
||||||
|
});
|
||||||
|
|
||||||
|
breadcrumbTests.bind(this)(Breadcrumb);
|
||||||
|
|
||||||
|
it("should render home icon", async () => {
|
||||||
|
const { container } = renderBreadcrumb(<Breadcrumb />, {
|
||||||
|
resources: [{ name: "posts" }],
|
||||||
|
routerInitialEntries: ["/posts/create"],
|
||||||
|
DashboardPage: DummyDashboard,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelector("svg")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render home icon with 'showhHome' props", async () => {
|
||||||
|
const { container } = renderBreadcrumb(
|
||||||
|
<Breadcrumb showHome={false} />,
|
||||||
|
{
|
||||||
|
resources: [{ name: "posts" }],
|
||||||
|
routerInitialEntries: ["/posts/create"],
|
||||||
|
DashboardPage: DummyDashboard,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelector("svg")).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
87
packages/antd/src/components/breadcrumb/index.tsx
Normal file
87
packages/antd/src/components/breadcrumb/index.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
useBreadcrumb,
|
||||||
|
useLink,
|
||||||
|
useRefineContext,
|
||||||
|
useRouterContext,
|
||||||
|
useRouterType,
|
||||||
|
useResource,
|
||||||
|
matchResourceFromRoute,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
import { RefineBreadcrumbProps } from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Breadcrumb as AntdBreadcrumb,
|
||||||
|
BreadcrumbProps as AntdBreadcrumbProps,
|
||||||
|
} from "antd";
|
||||||
|
import { HomeOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
export type BreadcrumbProps = RefineBreadcrumbProps<AntdBreadcrumbProps>;
|
||||||
|
|
||||||
|
export const Breadcrumb: React.FC<BreadcrumbProps> = ({
|
||||||
|
breadcrumbProps,
|
||||||
|
showHome = true,
|
||||||
|
hideIcons = false,
|
||||||
|
meta,
|
||||||
|
}) => {
|
||||||
|
const routerType = useRouterType();
|
||||||
|
const { breadcrumbs } = useBreadcrumb({
|
||||||
|
meta,
|
||||||
|
});
|
||||||
|
const Link = useLink();
|
||||||
|
const { Link: LegacyLink } = useRouterContext();
|
||||||
|
const { hasDashboard } = useRefineContext();
|
||||||
|
|
||||||
|
const { resources } = useResource();
|
||||||
|
|
||||||
|
const rootRouteResource = matchResourceFromRoute("/", resources);
|
||||||
|
|
||||||
|
const ActiveLink = routerType === "legacy" ? LegacyLink : Link;
|
||||||
|
|
||||||
|
if (breadcrumbs.length === 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadCrumbItems = breadcrumbs.map(({ label, icon, href }) => ({
|
||||||
|
key: `breadcrumb-item-${label}`,
|
||||||
|
title: (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!hideIcons && icon}
|
||||||
|
{href ? (
|
||||||
|
<ActiveLink to={href}>{label}</ActiveLink>
|
||||||
|
) : (
|
||||||
|
<span>{label}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getBreadcrumbItems = () => {
|
||||||
|
if (showHome && (hasDashboard || rootRouteResource.found)) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "breadcrumb-item-home",
|
||||||
|
title: (
|
||||||
|
<ActiveLink to="/">
|
||||||
|
{rootRouteResource?.resource?.meta?.icon ?? (
|
||||||
|
<HomeOutlined />
|
||||||
|
)}
|
||||||
|
</ActiveLink>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...breadCrumbItems,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return breadCrumbItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AntdBreadcrumb items={getBreadcrumbItems()} {...breadcrumbProps} />;
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { buttonCloneTests } from "@refinedev/ui-tests";
|
||||||
|
|
||||||
|
import { CloneButton } from "./";
|
||||||
|
|
||||||
|
describe("Clone Button", () => {
|
||||||
|
buttonCloneTests.bind(this)(CloneButton);
|
||||||
|
});
|
117
packages/antd/src/components/buttons/clone/index.tsx
Normal file
117
packages/antd/src/components/buttons/clone/index.tsx
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { PlusSquareOutlined } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
useCan,
|
||||||
|
useNavigation,
|
||||||
|
useTranslate,
|
||||||
|
useResource,
|
||||||
|
useRouterContext,
|
||||||
|
useRouterType,
|
||||||
|
useLink,
|
||||||
|
AccessControlContext,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
import {
|
||||||
|
RefineButtonTestIds,
|
||||||
|
RefineButtonClassNames,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import { CloneButtonProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<CloneButton>` uses Ant Design's {@link https://ant.design/components/button/ `<Button> component`}.
|
||||||
|
* It uses the {@link https://refine.dev/docs/api-reference/core/hooks/navigation/useNavigation#clone `clone`} method from {@link https://refine.dev/docs/api-reference/core/hooks/navigation/useNavigation useNavigation} under the hood.
|
||||||
|
* It can be useful when redirecting the app to the create page with the record id route of resource.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/buttons/clone-button} for more details.
|
||||||
|
*/
|
||||||
|
export const CloneButton: React.FC<CloneButtonProps> = ({
|
||||||
|
resourceNameOrRouteName: propResourceNameOrRouteName,
|
||||||
|
resource: resourceNameFromProps,
|
||||||
|
recordItemId,
|
||||||
|
hideText = false,
|
||||||
|
accessControl,
|
||||||
|
meta,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const accessControlContext = useContext(AccessControlContext);
|
||||||
|
|
||||||
|
const accessControlEnabled =
|
||||||
|
accessControl?.enabled ??
|
||||||
|
accessControlContext.options.buttons.enableAccessControl;
|
||||||
|
|
||||||
|
const hideIfUnauthorized =
|
||||||
|
accessControl?.hideIfUnauthorized ??
|
||||||
|
accessControlContext.options.buttons.hideIfUnauthorized;
|
||||||
|
|
||||||
|
const { cloneUrl: generateCloneUrl } = useNavigation();
|
||||||
|
const routerType = useRouterType();
|
||||||
|
const Link = useLink();
|
||||||
|
const { Link: LegacyLink } = useRouterContext();
|
||||||
|
|
||||||
|
const ActiveLink = routerType === "legacy" ? LegacyLink : Link;
|
||||||
|
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
const { id, resource } = useResource(
|
||||||
|
resourceNameFromProps ?? propResourceNameOrRouteName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data } = useCan({
|
||||||
|
resource: resource?.name,
|
||||||
|
action: "create",
|
||||||
|
params: { id: recordItemId ?? id, resource },
|
||||||
|
queryOptions: {
|
||||||
|
enabled: accessControlEnabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createButtonDisabledTitle = () => {
|
||||||
|
if (data?.can) return "";
|
||||||
|
else if (data?.reason) return data.reason;
|
||||||
|
else
|
||||||
|
return translate(
|
||||||
|
"buttons.notAccessTitle",
|
||||||
|
"You don't have permission to access",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloneUrl =
|
||||||
|
resource && (recordItemId || id)
|
||||||
|
? generateCloneUrl(resource, recordItemId! ?? id!, meta)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (accessControlEnabled && hideIfUnauthorized && !data?.can) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActiveLink
|
||||||
|
to={cloneUrl}
|
||||||
|
replace={false}
|
||||||
|
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
|
if (data?.can === false) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onClick) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<PlusSquareOutlined />}
|
||||||
|
disabled={data?.can === false}
|
||||||
|
title={createButtonDisabledTitle()}
|
||||||
|
data-testid={RefineButtonTestIds.CloneButton}
|
||||||
|
className={RefineButtonClassNames.CloneButton}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{!hideText && (children ?? translate("buttons.clone", "Clone"))}
|
||||||
|
</Button>
|
||||||
|
</ActiveLink>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { buttonCreateTests } from "@refinedev/ui-tests";
|
||||||
|
|
||||||
|
import { CreateButton } from "./";
|
||||||
|
|
||||||
|
describe("Create Button", () => {
|
||||||
|
buttonCreateTests.bind(this)(CreateButton);
|
||||||
|
});
|
117
packages/antd/src/components/buttons/create/index.tsx
Normal file
117
packages/antd/src/components/buttons/create/index.tsx
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { PlusSquareOutlined } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
useNavigation,
|
||||||
|
useTranslate,
|
||||||
|
useCan,
|
||||||
|
useResource,
|
||||||
|
useRouterContext,
|
||||||
|
useRouterType,
|
||||||
|
useLink,
|
||||||
|
AccessControlContext,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
import {
|
||||||
|
RefineButtonClassNames,
|
||||||
|
RefineButtonTestIds,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import { CreateButtonProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <CreateButton> uses Ant Design's {@link https://ant.design/components/button/ `<Button> component`}.
|
||||||
|
* It uses the {@link https://refine.dev/docs/api-reference/core/hooks/navigation/useNavigation#create `create`} method from {@link https://refine.dev/docs/api-reference/core/hooks/navigation/useNavigation `useNavigation`} under the hood.
|
||||||
|
* It can be useful to redirect the app to the create page route of resource}.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/buttons/create-button} for more details.
|
||||||
|
*/
|
||||||
|
export const CreateButton: React.FC<CreateButtonProps> = ({
|
||||||
|
resource: resourceNameFromProps,
|
||||||
|
resourceNameOrRouteName: propResourceNameOrRouteName,
|
||||||
|
hideText = false,
|
||||||
|
accessControl,
|
||||||
|
meta,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const accessControlContext = useContext(AccessControlContext);
|
||||||
|
|
||||||
|
const accessControlEnabled =
|
||||||
|
accessControl?.enabled ??
|
||||||
|
accessControlContext.options.buttons.enableAccessControl;
|
||||||
|
|
||||||
|
const hideIfUnauthorized =
|
||||||
|
accessControl?.hideIfUnauthorized ??
|
||||||
|
accessControlContext.options.buttons.hideIfUnauthorized;
|
||||||
|
|
||||||
|
const translate = useTranslate();
|
||||||
|
const routerType = useRouterType();
|
||||||
|
const Link = useLink();
|
||||||
|
const { Link: LegacyLink } = useRouterContext();
|
||||||
|
|
||||||
|
const ActiveLink = routerType === "legacy" ? LegacyLink : Link;
|
||||||
|
|
||||||
|
const { createUrl: generateCreateUrl } = useNavigation();
|
||||||
|
|
||||||
|
const { resource } = useResource(
|
||||||
|
resourceNameFromProps ?? propResourceNameOrRouteName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data } = useCan({
|
||||||
|
resource: resource?.name,
|
||||||
|
action: "create",
|
||||||
|
queryOptions: {
|
||||||
|
enabled: accessControlEnabled,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
resource,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createButtonDisabledTitle = () => {
|
||||||
|
if (data?.can) return "";
|
||||||
|
else if (data?.reason) return data.reason;
|
||||||
|
else
|
||||||
|
return translate(
|
||||||
|
"buttons.notAccessTitle",
|
||||||
|
"You don't have permission to access",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUrl = resource ? generateCreateUrl(resource, meta) : "";
|
||||||
|
|
||||||
|
if (accessControlEnabled && hideIfUnauthorized && !data?.can) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActiveLink
|
||||||
|
to={createUrl}
|
||||||
|
replace={false}
|
||||||
|
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
|
if (data?.can === false) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onClick) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<PlusSquareOutlined />}
|
||||||
|
disabled={data?.can === false}
|
||||||
|
title={createButtonDisabledTitle()}
|
||||||
|
data-testid={RefineButtonTestIds.CreateButton}
|
||||||
|
className={RefineButtonClassNames.CreateButton}
|
||||||
|
type="primary"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{!hideText &&
|
||||||
|
(children ?? translate("buttons.create", "Create"))}
|
||||||
|
</Button>
|
||||||
|
</ActiveLink>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { buttonDeleteTests } from "@refinedev/ui-tests";
|
||||||
|
import { DeleteButton } from "./";
|
||||||
|
|
||||||
|
describe("Delete Button", () => {
|
||||||
|
buttonDeleteTests.bind(this)(DeleteButton);
|
||||||
|
});
|
150
packages/antd/src/components/buttons/delete/index.tsx
Normal file
150
packages/antd/src/components/buttons/delete/index.tsx
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import { Button, Popconfirm } from "antd";
|
||||||
|
import { DeleteOutlined } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
useDelete,
|
||||||
|
useTranslate,
|
||||||
|
useMutationMode,
|
||||||
|
useCan,
|
||||||
|
useResource,
|
||||||
|
pickNotDeprecated,
|
||||||
|
useWarnAboutChange,
|
||||||
|
AccessControlContext,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
import {
|
||||||
|
RefineButtonClassNames,
|
||||||
|
RefineButtonTestIds,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import { DeleteButtonProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<DeleteButton>` uses Ant Design's {@link https://ant.design/components/button/ `<Button>`} and {@link https://ant.design/components/button/ `<Popconfirm>`} components.
|
||||||
|
* When you try to delete something, a pop-up shows up and asks for confirmation. When confirmed it executes the `useDelete` method provided by your `dataProvider`.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/buttons/delete-button} for more details.
|
||||||
|
*/
|
||||||
|
export const DeleteButton: React.FC<DeleteButtonProps> = ({
|
||||||
|
resource: resourceNameFromProps,
|
||||||
|
resourceNameOrRouteName: propResourceNameOrRouteName,
|
||||||
|
recordItemId,
|
||||||
|
onSuccess,
|
||||||
|
mutationMode: mutationModeProp,
|
||||||
|
children,
|
||||||
|
successNotification,
|
||||||
|
errorNotification,
|
||||||
|
hideText = false,
|
||||||
|
accessControl,
|
||||||
|
metaData,
|
||||||
|
meta,
|
||||||
|
dataProviderName,
|
||||||
|
confirmTitle,
|
||||||
|
confirmOkText,
|
||||||
|
confirmCancelText,
|
||||||
|
invalidates,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const accessControlContext = useContext(AccessControlContext);
|
||||||
|
|
||||||
|
const accessControlEnabled =
|
||||||
|
accessControl?.enabled ??
|
||||||
|
accessControlContext.options.buttons.enableAccessControl;
|
||||||
|
|
||||||
|
const hideIfUnauthorized =
|
||||||
|
accessControl?.hideIfUnauthorized ??
|
||||||
|
accessControlContext.options.buttons.hideIfUnauthorized;
|
||||||
|
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
const { id, resource, identifier } = useResource(
|
||||||
|
resourceNameFromProps ?? propResourceNameOrRouteName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutationMode: mutationModeContext } = useMutationMode();
|
||||||
|
|
||||||
|
const mutationMode = mutationModeProp ?? mutationModeContext;
|
||||||
|
|
||||||
|
const { mutate, isLoading, variables } = useDelete();
|
||||||
|
|
||||||
|
const { data } = useCan({
|
||||||
|
resource: resource?.name,
|
||||||
|
action: "delete",
|
||||||
|
params: { id: recordItemId ?? id, resource },
|
||||||
|
queryOptions: {
|
||||||
|
enabled: accessControlEnabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledTitle = () => {
|
||||||
|
if (data?.can) return "";
|
||||||
|
else if (data?.reason) return data.reason;
|
||||||
|
else
|
||||||
|
return translate(
|
||||||
|
"buttons.notAccessTitle",
|
||||||
|
"You don't have permission to access",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { setWarnWhen } = useWarnAboutChange();
|
||||||
|
|
||||||
|
if (accessControlEnabled && hideIfUnauthorized && !data?.can) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popconfirm
|
||||||
|
key="delete"
|
||||||
|
okText={confirmOkText ?? translate("buttons.delete", "Delete")}
|
||||||
|
cancelText={
|
||||||
|
confirmCancelText ?? translate("buttons.cancel", "Cancel")
|
||||||
|
}
|
||||||
|
okType="danger"
|
||||||
|
title={
|
||||||
|
confirmTitle ?? translate("buttons.confirm", "Are you sure?")
|
||||||
|
}
|
||||||
|
okButtonProps={{ disabled: isLoading }}
|
||||||
|
onConfirm={(): void => {
|
||||||
|
if ((recordItemId ?? id) && identifier) {
|
||||||
|
setWarnWhen(false);
|
||||||
|
mutate(
|
||||||
|
{
|
||||||
|
id: recordItemId ?? id ?? "",
|
||||||
|
resource: identifier,
|
||||||
|
mutationMode,
|
||||||
|
successNotification,
|
||||||
|
errorNotification,
|
||||||
|
meta: pickNotDeprecated(meta, metaData),
|
||||||
|
metaData: pickNotDeprecated(meta, metaData),
|
||||||
|
dataProviderName,
|
||||||
|
invalidates,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (value) => {
|
||||||
|
onSuccess && onSuccess(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
typeof rest?.disabled !== "undefined"
|
||||||
|
? rest.disabled
|
||||||
|
: data?.can === false
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
loading={(recordItemId ?? id) === variables?.id && isLoading}
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
title={disabledTitle()}
|
||||||
|
disabled={data?.can === false}
|
||||||
|
data-testid={RefineButtonTestIds.DeleteButton}
|
||||||
|
className={RefineButtonClassNames.DeleteButton}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{!hideText &&
|
||||||
|
(children ?? translate("buttons.delete", "Delete"))}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
);
|
||||||
|
};
|
6
packages/antd/src/components/buttons/edit/index.spec.tsx
Normal file
6
packages/antd/src/components/buttons/edit/index.spec.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { buttonEditTests } from "@refinedev/ui-tests";
|
||||||
|
import { EditButton } from "./";
|
||||||
|
|
||||||
|
describe("Edit Button", () => {
|
||||||
|
buttonEditTests.bind(this)(EditButton);
|
||||||
|
});
|
118
packages/antd/src/components/buttons/edit/index.tsx
Normal file
118
packages/antd/src/components/buttons/edit/index.tsx
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { EditOutlined } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
useCan,
|
||||||
|
useNavigation,
|
||||||
|
useTranslate,
|
||||||
|
useResource,
|
||||||
|
useRouterContext,
|
||||||
|
useRouterType,
|
||||||
|
useLink,
|
||||||
|
AccessControlContext,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
import {
|
||||||
|
RefineButtonClassNames,
|
||||||
|
RefineButtonTestIds,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import { EditButtonProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<EditButton>` uses Ant Design's {@link https://ant.design/components/button/ `<Button>`} component.
|
||||||
|
* It uses the {@link https://refine.dev/docs/api-reference/core/hooks/navigation/useNavigation#edit `edit`} method from {@link https://refine.dev/docs/api-reference/core/hooks/navigation/useNavigation `useNavigation`} under the hood.
|
||||||
|
* It can be useful when redirecting the app to the edit page with the record id route of resource}.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/buttons/edit-button} for more details.
|
||||||
|
*/
|
||||||
|
export const EditButton: React.FC<EditButtonProps> = ({
|
||||||
|
resource: resourceNameFromProps,
|
||||||
|
resourceNameOrRouteName: propResourceNameOrRouteName,
|
||||||
|
recordItemId,
|
||||||
|
hideText = false,
|
||||||
|
accessControl,
|
||||||
|
meta,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const accessControlContext = useContext(AccessControlContext);
|
||||||
|
|
||||||
|
const accessControlEnabled =
|
||||||
|
accessControl?.enabled ??
|
||||||
|
accessControlContext.options.buttons.enableAccessControl;
|
||||||
|
|
||||||
|
const hideIfUnauthorized =
|
||||||
|
accessControl?.hideIfUnauthorized ??
|
||||||
|
accessControlContext.options.buttons.hideIfUnauthorized;
|
||||||
|
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
const routerType = useRouterType();
|
||||||
|
const Link = useLink();
|
||||||
|
const { Link: LegacyLink } = useRouterContext();
|
||||||
|
|
||||||
|
const ActiveLink = routerType === "legacy" ? LegacyLink : Link;
|
||||||
|
|
||||||
|
const { editUrl: generateEditUrl } = useNavigation();
|
||||||
|
|
||||||
|
const { id, resource } = useResource(
|
||||||
|
resourceNameFromProps ?? propResourceNameOrRouteName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data } = useCan({
|
||||||
|
resource: resource?.name,
|
||||||
|
action: "edit",
|
||||||
|
params: { id: recordItemId ?? id, resource },
|
||||||
|
queryOptions: {
|
||||||
|
enabled: accessControlEnabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createButtonDisabledTitle = () => {
|
||||||
|
if (data?.can) return "";
|
||||||
|
else if (data?.reason) return data.reason;
|
||||||
|
else
|
||||||
|
return translate(
|
||||||
|
"buttons.notAccessTitle",
|
||||||
|
"You don't have permission to access",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editUrl =
|
||||||
|
resource && (recordItemId ?? id)
|
||||||
|
? generateEditUrl(resource, recordItemId! ?? id!, meta)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (accessControlEnabled && hideIfUnauthorized && !data?.can) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActiveLink
|
||||||
|
to={editUrl}
|
||||||
|
replace={false}
|
||||||
|
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
|
if (data?.can === false) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onClick) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
disabled={data?.can === false}
|
||||||
|
title={createButtonDisabledTitle()}
|
||||||
|
data-testid={RefineButtonTestIds.EditButton}
|
||||||
|
className={RefineButtonClassNames.EditButton}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{!hideText && (children ?? translate("buttons.edit", "Edit"))}
|
||||||
|
</Button>
|
||||||
|
</ActiveLink>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { buttonExportTests } from "@refinedev/ui-tests";
|
||||||
|
import { ExportButton } from "./index";
|
||||||
|
|
||||||
|
describe("<ExportButton/>", () => {
|
||||||
|
buttonExportTests.bind(this)(ExportButton);
|
||||||
|
});
|
36
packages/antd/src/components/buttons/export/index.tsx
Normal file
36
packages/antd/src/components/buttons/export/index.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { ExportOutlined } from "@ant-design/icons";
|
||||||
|
import { useTranslate } from "@refinedev/core";
|
||||||
|
import {
|
||||||
|
RefineButtonClassNames,
|
||||||
|
RefineButtonTestIds,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import { ExportButtonProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<ExportButton>` is an Ant Design {@link https://ant.design/components/button/ `<Button>`} with a default export icon and a default text with "Export".
|
||||||
|
* It only has presentational value.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/buttons/export-button} for more details.
|
||||||
|
*/
|
||||||
|
export const ExportButton: React.FC<ExportButtonProps> = ({
|
||||||
|
hideText = false,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
icon={<ExportOutlined />}
|
||||||
|
data-testid={RefineButtonTestIds.ExportButton}
|
||||||
|
className={RefineButtonClassNames.ExportButton}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{!hideText && (children ?? translate("buttons.export", "Export"))}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { buttonImportTests } from "@refinedev/ui-tests";
|
||||||
|
|
||||||
|
import { ImportButton } from "./index";
|
||||||
|
|
||||||
|
describe("<ImportButton /> usage with useImport", () => {
|
||||||
|
buttonImportTests.bind(this)(ImportButton);
|
||||||
|
});
|
40
packages/antd/src/components/buttons/import/index.tsx
Normal file
40
packages/antd/src/components/buttons/import/index.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Button, Upload } from "antd";
|
||||||
|
import { ImportOutlined } from "@ant-design/icons";
|
||||||
|
import { useTranslate } from "@refinedev/core";
|
||||||
|
import {
|
||||||
|
RefineButtonClassNames,
|
||||||
|
RefineButtonTestIds,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import { ImportButtonProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<ImportButton>` is compatible with the {@link https://refine.dev/docs/api-reference/antd/hooks/import/useImport `useImport`} hook and is meant to be used as it's upload button.
|
||||||
|
* It uses Ant Design's {@link https://ant.design/components/button/ `<Button>`} and {@link https://ant.design/components/upload/ `<Upload>`} components.
|
||||||
|
* It wraps a `<Button>` component with an `<Upload>` component and accepts properties for `<Button>` and `<Upload>` components separately.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/buttons/import-button} for more details.
|
||||||
|
*/
|
||||||
|
export const ImportButton: React.FC<ImportButtonProps> = ({
|
||||||
|
uploadProps,
|
||||||
|
buttonProps,
|
||||||
|
hideText = false,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Upload {...uploadProps}>
|
||||||
|
<Button
|
||||||
|
icon={<ImportOutlined />}
|
||||||
|
data-testid={RefineButtonTestIds.ImportButton}
|
||||||
|
className={RefineButtonClassNames.ImportButton}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{!hideText &&
|
||||||
|
(children ?? translate("buttons.import", "Import"))}
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
);
|
||||||
|
};
|
11
packages/antd/src/components/buttons/index.ts
Normal file
11
packages/antd/src/components/buttons/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export { CreateButton } from "./create";
|
||||||
|
export { EditButton } from "./edit";
|
||||||
|
export { DeleteButton } from "./delete";
|
||||||
|
export { RefreshButton } from "./refresh";
|
||||||
|
export { ShowButton } from "./show";
|
||||||
|
export { ListButton } from "./list";
|
||||||
|
export { ExportButton } from "./export";
|
||||||
|
export { SaveButton } from "./save";
|
||||||
|
export { CloneButton } from "./clone";
|
||||||
|
export { ImportButton } from "./import";
|
||||||
|
export * from "./types";
|
6
packages/antd/src/components/buttons/list/index.spec.tsx
Normal file
6
packages/antd/src/components/buttons/list/index.spec.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { buttonListTests } from "@refinedev/ui-tests";
|
||||||
|
import { ListButton } from "./";
|
||||||
|
|
||||||
|
describe("List Button", () => {
|
||||||
|
buttonListTests.bind(this)(ListButton);
|
||||||
|
});
|
136
packages/antd/src/components/buttons/list/index.tsx
Normal file
136
packages/antd/src/components/buttons/list/index.tsx
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { BarsOutlined } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
useCan,
|
||||||
|
useNavigation,
|
||||||
|
useTranslate,
|
||||||
|
useUserFriendlyName,
|
||||||
|
useResource,
|
||||||
|
useRouterContext,
|
||||||
|
useRouterType,
|
||||||
|
useLink,
|
||||||
|
pickNotDeprecated,
|
||||||
|
AccessControlContext,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
import {
|
||||||
|
RefineButtonClassNames,
|
||||||
|
RefineButtonTestIds,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import { ListButtonProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<ListButton>` is using Ant Design's {@link https://ant.design/components/button/ `<Button>`} component.
|
||||||
|
* It uses the {@link https://refine.dev/docs/api-reference/core/hooks/navigation/useNavigation#list `list`} method from {@link https://refine.dev/docs/api-reference/core/hooks/navigation/useNavigation `useNavigation`} under the hood.
|
||||||
|
* It can be useful when redirecting the app to the list page route of resource}.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/buttons/list-button} for more details.
|
||||||
|
*/
|
||||||
|
export const ListButton: React.FC<ListButtonProps> = ({
|
||||||
|
resource: resourceNameFromProps,
|
||||||
|
resourceNameOrRouteName: propResourceNameOrRouteName,
|
||||||
|
hideText = false,
|
||||||
|
accessControl,
|
||||||
|
meta,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const accessControlContext = useContext(AccessControlContext);
|
||||||
|
|
||||||
|
const accessControlEnabled =
|
||||||
|
accessControl?.enabled ??
|
||||||
|
accessControlContext.options.buttons.enableAccessControl;
|
||||||
|
|
||||||
|
const hideIfUnauthorized =
|
||||||
|
accessControl?.hideIfUnauthorized ??
|
||||||
|
accessControlContext.options.buttons.hideIfUnauthorized;
|
||||||
|
|
||||||
|
const { listUrl: generateListUrl } = useNavigation();
|
||||||
|
const routerType = useRouterType();
|
||||||
|
const Link = useLink();
|
||||||
|
const { Link: LegacyLink } = useRouterContext();
|
||||||
|
const getUserFriendlyName = useUserFriendlyName();
|
||||||
|
|
||||||
|
const ActiveLink = routerType === "legacy" ? LegacyLink : Link;
|
||||||
|
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
const { resource, identifier } = useResource(
|
||||||
|
resourceNameFromProps ?? propResourceNameOrRouteName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data } = useCan({
|
||||||
|
resource: resource?.name,
|
||||||
|
action: "list",
|
||||||
|
queryOptions: {
|
||||||
|
enabled: accessControlEnabled,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
resource,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createButtonDisabledTitle = () => {
|
||||||
|
if (data?.can) return "";
|
||||||
|
else if (data?.reason) return data.reason;
|
||||||
|
else
|
||||||
|
return translate(
|
||||||
|
"buttons.notAccessTitle",
|
||||||
|
"You don't have permission to access",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listUrl = resource ? generateListUrl(resource, meta) : "";
|
||||||
|
|
||||||
|
if (accessControlEnabled && hideIfUnauthorized && !data?.can) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActiveLink
|
||||||
|
to={listUrl}
|
||||||
|
replace={false}
|
||||||
|
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
|
if (data?.can === false) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onClick) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<BarsOutlined />}
|
||||||
|
disabled={data?.can === false}
|
||||||
|
title={createButtonDisabledTitle()}
|
||||||
|
data-testid={RefineButtonTestIds.ListButton}
|
||||||
|
className={RefineButtonClassNames.ListButton}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{!hideText &&
|
||||||
|
(children ??
|
||||||
|
translate(
|
||||||
|
`${
|
||||||
|
identifier ??
|
||||||
|
resourceNameFromProps ??
|
||||||
|
propResourceNameOrRouteName
|
||||||
|
}.titles.list`,
|
||||||
|
getUserFriendlyName(
|
||||||
|
resource?.meta?.label ??
|
||||||
|
resource?.label ??
|
||||||
|
identifier ??
|
||||||
|
pickNotDeprecated(
|
||||||
|
resourceNameFromProps,
|
||||||
|
propResourceNameOrRouteName,
|
||||||
|
),
|
||||||
|
"plural",
|
||||||
|
),
|
||||||
|
))}
|
||||||
|
</Button>
|
||||||
|
</ActiveLink>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { buttonRefreshTests } from "@refinedev/ui-tests";
|
||||||
|
import { RefreshButton } from "./";
|
||||||
|
|
||||||
|
describe("Refresh Button", () => {
|
||||||
|
buttonRefreshTests.bind(this)(RefreshButton);
|
||||||
|
});
|
76
packages/antd/src/components/buttons/refresh/index.tsx
Normal file
76
packages/antd/src/components/buttons/refresh/index.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { RedoOutlined } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
useTranslate,
|
||||||
|
useResource,
|
||||||
|
useInvalidate,
|
||||||
|
queryKeys,
|
||||||
|
pickDataProvider,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
import {
|
||||||
|
RefineButtonClassNames,
|
||||||
|
RefineButtonTestIds,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import { RefreshButtonProps } from "../types";
|
||||||
|
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<RefreshButton>` uses Ant Design's {@link https://ant.design/components/button/ `<Button>`} component
|
||||||
|
* to update the data shown on the page via the {@link https://refine.dev/docs/api-reference/core/hooks/invalidate/useInvalidate `useInvalidate`} hook.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/buttons/refresh-button} for more details.
|
||||||
|
*/
|
||||||
|
export const RefreshButton: React.FC<RefreshButtonProps> = ({
|
||||||
|
resource: resourceNameFromProps,
|
||||||
|
resourceNameOrRouteName: propResourceNameOrRouteName,
|
||||||
|
recordItemId,
|
||||||
|
hideText = false,
|
||||||
|
dataProviderName,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
meta: _meta,
|
||||||
|
metaData: _metaData,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const invalidates = useInvalidate();
|
||||||
|
|
||||||
|
const { resources, identifier, id } = useResource(
|
||||||
|
resourceNameFromProps ?? propResourceNameOrRouteName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isInvalidating = !!queryClient.isFetching({
|
||||||
|
queryKey: queryKeys(
|
||||||
|
identifier,
|
||||||
|
pickDataProvider(identifier, dataProviderName, resources),
|
||||||
|
).detail(recordItemId ?? id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInvalidate = () => {
|
||||||
|
invalidates({
|
||||||
|
id: recordItemId ?? id,
|
||||||
|
invalidates: ["detail"],
|
||||||
|
dataProviderName,
|
||||||
|
resource: identifier,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
onClick ? onClick(e) : handleInvalidate();
|
||||||
|
}}
|
||||||
|
icon={<RedoOutlined spin={isInvalidating} />}
|
||||||
|
data-testid={RefineButtonTestIds.RefreshButton}
|
||||||
|
className={RefineButtonClassNames.RefreshButton}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{!hideText && (children ?? translate("buttons.refresh", "Refresh"))}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
6
packages/antd/src/components/buttons/save/index.spec.tsx
Normal file
6
packages/antd/src/components/buttons/save/index.spec.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { buttonSaveTests } from "@refinedev/ui-tests";
|
||||||
|
import { SaveButton } from "./";
|
||||||
|
|
||||||
|
describe("Save Button", () => {
|
||||||
|
buttonSaveTests.bind(this)(SaveButton);
|
||||||
|
});
|
36
packages/antd/src/components/buttons/save/index.tsx
Normal file
36
packages/antd/src/components/buttons/save/index.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { SaveOutlined } from "@ant-design/icons";
|
||||||
|
import { useTranslate } from "@refinedev/core";
|
||||||
|
import {
|
||||||
|
RefineButtonClassNames,
|
||||||
|
RefineButtonTestIds,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import { SaveButtonProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<SaveButton>` uses Ant Design's {@link https://ant.design/components/button/ `<Button>`} component.
|
||||||
|
* It uses it for presantation purposes only. Some of the hooks that refine has adds features to this button.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/buttons/save-button} for more details.
|
||||||
|
*/
|
||||||
|
export const SaveButton: React.FC<SaveButtonProps> = ({
|
||||||
|
hideText = false,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
data-testid={RefineButtonTestIds.SaveButton}
|
||||||
|
className={RefineButtonClassNames.SaveButton}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{!hideText && (children ?? translate("buttons.save", "Save"))}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
6
packages/antd/src/components/buttons/show/index.spec.tsx
Normal file
6
packages/antd/src/components/buttons/show/index.spec.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { buttonShowTests } from "@refinedev/ui-tests";
|
||||||
|
import { ShowButton } from "./";
|
||||||
|
|
||||||
|
describe("Show Button", () => {
|
||||||
|
buttonShowTests.bind(this)(ShowButton);
|
||||||
|
});
|
117
packages/antd/src/components/buttons/show/index.tsx
Normal file
117
packages/antd/src/components/buttons/show/index.tsx
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { EyeOutlined } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
useCan,
|
||||||
|
useNavigation,
|
||||||
|
useTranslate,
|
||||||
|
useResource,
|
||||||
|
useRouterContext,
|
||||||
|
useRouterType,
|
||||||
|
useLink,
|
||||||
|
AccessControlContext,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
import {
|
||||||
|
RefineButtonClassNames,
|
||||||
|
RefineButtonTestIds,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
import { ShowButtonProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<ShowButton>` uses Ant Design's {@link https://ant.design/components/button/ `<Button>`} component.
|
||||||
|
* It uses the {@link https://refine.dev/docs/api-reference/core/hooks/navigation/useNavigation#show `show`} method from {@link https://refine.dev/docs/api-reference/core/hooks/navigation/useNavigation `useNavigation`} under the hood.
|
||||||
|
* It can be useful when redirecting the app to the show page with the record id route of resource.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/buttons/show-button} for more details.
|
||||||
|
*/
|
||||||
|
export const ShowButton: React.FC<ShowButtonProps> = ({
|
||||||
|
resource: resourceNameFromProps,
|
||||||
|
resourceNameOrRouteName: propResourceNameOrRouteName,
|
||||||
|
recordItemId,
|
||||||
|
hideText = false,
|
||||||
|
accessControl,
|
||||||
|
meta,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const accessControlContext = useContext(AccessControlContext);
|
||||||
|
|
||||||
|
const accessControlEnabled =
|
||||||
|
accessControl?.enabled ??
|
||||||
|
accessControlContext.options.buttons.enableAccessControl;
|
||||||
|
|
||||||
|
const hideIfUnauthorized =
|
||||||
|
accessControl?.hideIfUnauthorized ??
|
||||||
|
accessControlContext.options.buttons.hideIfUnauthorized;
|
||||||
|
|
||||||
|
const { showUrl: generateShowUrl } = useNavigation();
|
||||||
|
const routerType = useRouterType();
|
||||||
|
const Link = useLink();
|
||||||
|
const { Link: LegacyLink } = useRouterContext();
|
||||||
|
|
||||||
|
const ActiveLink = routerType === "legacy" ? LegacyLink : Link;
|
||||||
|
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
const { id, resource } = useResource(
|
||||||
|
resourceNameFromProps ?? propResourceNameOrRouteName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data } = useCan({
|
||||||
|
resource: resource?.name,
|
||||||
|
action: "show",
|
||||||
|
params: { id: recordItemId ?? id, resource },
|
||||||
|
queryOptions: {
|
||||||
|
enabled: accessControlEnabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createButtonDisabledTitle = () => {
|
||||||
|
if (data?.can) return "";
|
||||||
|
else if (data?.reason) return data.reason;
|
||||||
|
else
|
||||||
|
return translate(
|
||||||
|
"buttons.notAccessTitle",
|
||||||
|
"You don't have permission to access",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showUrl =
|
||||||
|
resource && (recordItemId || id)
|
||||||
|
? generateShowUrl(resource, recordItemId! ?? id!, meta)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (accessControlEnabled && hideIfUnauthorized && !data?.can) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActiveLink
|
||||||
|
to={showUrl}
|
||||||
|
replace={false}
|
||||||
|
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
|
if (data?.can === false) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onClick) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
disabled={data?.can === false}
|
||||||
|
title={createButtonDisabledTitle()}
|
||||||
|
data-testid={RefineButtonTestIds.ShowButton}
|
||||||
|
className={RefineButtonClassNames.ShowButton}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{!hideText && (children ?? translate("buttons.show", "Show"))}
|
||||||
|
</Button>
|
||||||
|
</ActiveLink>
|
||||||
|
);
|
||||||
|
};
|
44
packages/antd/src/components/buttons/types.ts
Normal file
44
packages/antd/src/components/buttons/types.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { ButtonProps, UploadProps } from "antd";
|
||||||
|
import {
|
||||||
|
RefineCloneButtonProps,
|
||||||
|
RefineCreateButtonProps,
|
||||||
|
RefineDeleteButtonProps,
|
||||||
|
RefineEditButtonProps,
|
||||||
|
RefineExportButtonProps,
|
||||||
|
RefineImportButtonProps,
|
||||||
|
RefineListButtonProps,
|
||||||
|
RefineRefreshButtonProps,
|
||||||
|
RefineSaveButtonProps,
|
||||||
|
RefineShowButtonProps,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
export type ShowButtonProps = RefineShowButtonProps<ButtonProps>;
|
||||||
|
|
||||||
|
export type CloneButtonProps = RefineCloneButtonProps<ButtonProps>;
|
||||||
|
|
||||||
|
export type CreateButtonProps = RefineCreateButtonProps<ButtonProps>;
|
||||||
|
|
||||||
|
export type DeleteButtonProps = RefineDeleteButtonProps<ButtonProps>;
|
||||||
|
|
||||||
|
export type EditButtonProps = RefineEditButtonProps<ButtonProps>;
|
||||||
|
|
||||||
|
export type ExportButtonProps = RefineExportButtonProps<ButtonProps>;
|
||||||
|
|
||||||
|
export type ImportButtonProps = RefineImportButtonProps & {
|
||||||
|
/**
|
||||||
|
* Sets the button type
|
||||||
|
* @type [UploadProps](https://ant.design/components/upload/#API)
|
||||||
|
*/
|
||||||
|
uploadProps: UploadProps;
|
||||||
|
/**
|
||||||
|
* Sets props of the button
|
||||||
|
* @type [ButtonProps](https://ant.design/components/button/#API)
|
||||||
|
*/
|
||||||
|
buttonProps: ButtonProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListButtonProps = RefineListButtonProps<ButtonProps>;
|
||||||
|
|
||||||
|
export type RefreshButtonProps = RefineRefreshButtonProps<ButtonProps>;
|
||||||
|
|
||||||
|
export type SaveButtonProps = RefineSaveButtonProps<ButtonProps>;
|
45
packages/antd/src/components/crud/create/index.spec.tsx
Normal file
45
packages/antd/src/components/crud/create/index.spec.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
import { crudCreateTests } from "@refinedev/ui-tests";
|
||||||
|
import { render, TestWrapper } from "@test";
|
||||||
|
import { Create } from "./";
|
||||||
|
import { SaveButton } from "@components/buttons";
|
||||||
|
import { RefineButtonTestIds } from "@refinedev/ui-types";
|
||||||
|
|
||||||
|
const renderCreate = (create: ReactNode) => {
|
||||||
|
return render(
|
||||||
|
<Routes>
|
||||||
|
<Route path="/:resource/create" element={create} />
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
routerInitialEntries: ["/posts/create"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Create", () => {
|
||||||
|
crudCreateTests.bind(this)(Create);
|
||||||
|
|
||||||
|
it("should customize default buttons with default props", async () => {
|
||||||
|
const { queryByTestId } = renderCreate(
|
||||||
|
<Create
|
||||||
|
saveButtonProps={{ className: "customize-test" }}
|
||||||
|
footerButtons={({ saveButtonProps }) => {
|
||||||
|
expect(saveButtonProps).toBeDefined();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SaveButton {...saveButtonProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.SaveButton)).toHaveClass(
|
||||||
|
"customize-test",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
144
packages/antd/src/components/crud/create/index.tsx
Normal file
144
packages/antd/src/components/crud/create/index.tsx
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Card, Space, Spin } from "antd";
|
||||||
|
import {
|
||||||
|
useNavigation,
|
||||||
|
useTranslate,
|
||||||
|
useUserFriendlyName,
|
||||||
|
useRefineContext,
|
||||||
|
useRouterType,
|
||||||
|
useResource,
|
||||||
|
useBack,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
SaveButton,
|
||||||
|
PageHeader,
|
||||||
|
SaveButtonProps,
|
||||||
|
} from "@components";
|
||||||
|
import { CreateProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<Create>` provides us a layout to display the page.
|
||||||
|
* It does not contain any logic but adds extra functionalities like action buttons and giving titles to the page.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/ui-frameworks/antd/components/basic-views/create} for more details.
|
||||||
|
*/
|
||||||
|
export const Create: React.FC<CreateProps> = ({
|
||||||
|
title,
|
||||||
|
saveButtonProps: saveButtonPropsFromProps,
|
||||||
|
children,
|
||||||
|
resource: resourceFromProps,
|
||||||
|
isLoading = false,
|
||||||
|
breadcrumb: breadcrumbFromProps,
|
||||||
|
wrapperProps,
|
||||||
|
headerProps,
|
||||||
|
contentProps,
|
||||||
|
headerButtonProps,
|
||||||
|
headerButtons,
|
||||||
|
footerButtonProps,
|
||||||
|
footerButtons,
|
||||||
|
goBack: goBackFromProps,
|
||||||
|
}) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
const { options: { breadcrumb: globalBreadcrumb } = {} } =
|
||||||
|
useRefineContext();
|
||||||
|
|
||||||
|
const routerType = useRouterType();
|
||||||
|
const back = useBack();
|
||||||
|
const { goBack } = useNavigation();
|
||||||
|
const getUserFriendlyName = useUserFriendlyName();
|
||||||
|
|
||||||
|
const { resource, action, identifier } = useResource(resourceFromProps);
|
||||||
|
|
||||||
|
const breadcrumb =
|
||||||
|
typeof breadcrumbFromProps === "undefined"
|
||||||
|
? globalBreadcrumb
|
||||||
|
: breadcrumbFromProps;
|
||||||
|
|
||||||
|
const saveButtonProps: SaveButtonProps = {
|
||||||
|
...(isLoading ? { disabled: true } : {}),
|
||||||
|
...saveButtonPropsFromProps,
|
||||||
|
htmlType: "submit",
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultFooterButtons = (
|
||||||
|
<>
|
||||||
|
<SaveButton {...saveButtonProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...(wrapperProps ?? {})}>
|
||||||
|
<PageHeader
|
||||||
|
ghost={false}
|
||||||
|
backIcon={goBackFromProps}
|
||||||
|
onBack={
|
||||||
|
action !== "list" || typeof action !== "undefined"
|
||||||
|
? routerType === "legacy"
|
||||||
|
? goBack
|
||||||
|
: back
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
title ??
|
||||||
|
translate(
|
||||||
|
`${identifier}.titles.create`,
|
||||||
|
`Create ${getUserFriendlyName(
|
||||||
|
resource?.meta?.label ??
|
||||||
|
resource?.options?.label ??
|
||||||
|
resource?.label ??
|
||||||
|
identifier,
|
||||||
|
"singular",
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
breadcrumb={
|
||||||
|
typeof breadcrumb !== "undefined" ? (
|
||||||
|
<>{breadcrumb}</> ?? undefined
|
||||||
|
) : (
|
||||||
|
<Breadcrumb />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Space wrap {...(headerButtonProps ?? {})}>
|
||||||
|
{headerButtons
|
||||||
|
? typeof headerButtons === "function"
|
||||||
|
? headerButtons({
|
||||||
|
defaultButtons: null,
|
||||||
|
})
|
||||||
|
: headerButtons
|
||||||
|
: null}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
{...(headerProps ?? {})}
|
||||||
|
>
|
||||||
|
<Spin spinning={isLoading}>
|
||||||
|
<Card
|
||||||
|
bordered={false}
|
||||||
|
actions={[
|
||||||
|
<Space
|
||||||
|
key="action-buttons"
|
||||||
|
style={{ float: "right", marginRight: 24 }}
|
||||||
|
{...(footerButtonProps ?? {})}
|
||||||
|
>
|
||||||
|
{footerButtons
|
||||||
|
? typeof footerButtons === "function"
|
||||||
|
? footerButtons({
|
||||||
|
defaultButtons:
|
||||||
|
defaultFooterButtons,
|
||||||
|
saveButtonProps: saveButtonProps,
|
||||||
|
})
|
||||||
|
: footerButtons
|
||||||
|
: defaultFooterButtons}
|
||||||
|
</Space>,
|
||||||
|
]}
|
||||||
|
{...(contentProps ?? {})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
</Spin>
|
||||||
|
</PageHeader>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
521
packages/antd/src/components/crud/edit/index.spec.tsx
Normal file
521
packages/antd/src/components/crud/edit/index.spec.tsx
Normal file
|
@ -0,0 +1,521 @@
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
import { AccessControlProvider } from "@refinedev/core";
|
||||||
|
import { Form, Input } from "antd";
|
||||||
|
|
||||||
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
ITestWrapperProps,
|
||||||
|
render,
|
||||||
|
TestWrapper,
|
||||||
|
waitFor,
|
||||||
|
MockJSONServer,
|
||||||
|
} from "@test";
|
||||||
|
import { Edit } from "./";
|
||||||
|
import { crudEditTests } from "@refinedev/ui-tests";
|
||||||
|
import { RefineButtonTestIds } from "@refinedev/ui-types";
|
||||||
|
import {
|
||||||
|
DeleteButton,
|
||||||
|
ListButton,
|
||||||
|
RefreshButton,
|
||||||
|
SaveButton,
|
||||||
|
} from "@components/buttons";
|
||||||
|
import { useForm } from "@hooks/form";
|
||||||
|
|
||||||
|
const renderEdit = (
|
||||||
|
edit: ReactNode,
|
||||||
|
accessControlProvider?: AccessControlProvider,
|
||||||
|
wrapperOptions?: ITestWrapperProps,
|
||||||
|
) => {
|
||||||
|
return render(
|
||||||
|
<Routes>
|
||||||
|
<Route path="/:resource/edit/:id" element={edit} />
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
routerInitialEntries: ["/posts/edit/1"],
|
||||||
|
accessControlProvider,
|
||||||
|
...wrapperOptions,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Edit", () => {
|
||||||
|
crudEditTests.bind(this)(Edit);
|
||||||
|
|
||||||
|
it("should render optional mutationMode with mutationModeProp prop", async () => {
|
||||||
|
const container = renderEdit(<Edit mutationMode="undoable" />);
|
||||||
|
|
||||||
|
expect(container).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("render delete button", () => {
|
||||||
|
it("should render delete button ", async () => {
|
||||||
|
const { getByText, queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/edit/:id"
|
||||||
|
element={
|
||||||
|
<Edit
|
||||||
|
footerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: true }],
|
||||||
|
routerInitialEntries: ["/posts/edit/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.DeleteButton),
|
||||||
|
).not.toBeNull();
|
||||||
|
|
||||||
|
getByText("Edit Post");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render delete button on resource canDelete false", async () => {
|
||||||
|
const { getByText, queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/edit/:id"
|
||||||
|
element={
|
||||||
|
<Edit
|
||||||
|
footerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).toBeUndefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
></Route>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: false }],
|
||||||
|
routerInitialEntries: ["/posts/edit/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.DeleteButton)).toBeNull();
|
||||||
|
|
||||||
|
getByText("Edit Post");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render delete button on resource canDelete true & canDelete props false on component", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/edit/:id"
|
||||||
|
element={
|
||||||
|
<Edit
|
||||||
|
canDelete={false}
|
||||||
|
footerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).toBeUndefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
></Route>
|
||||||
|
</Routes>,
|
||||||
|
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: true }],
|
||||||
|
routerInitialEntries: ["/posts/edit/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.DeleteButton)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render delete button on resource canDelete false & canDelete props true on component", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/edit/:id"
|
||||||
|
element={
|
||||||
|
<Edit
|
||||||
|
canDelete={true}
|
||||||
|
footerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
></Route>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: false }],
|
||||||
|
routerInitialEntries: ["/posts/edit/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.DeleteButton),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render delete button on resource canDelete false & deleteButtonProps props not null on component", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/edit/:id"
|
||||||
|
element={
|
||||||
|
<Edit
|
||||||
|
deleteButtonProps={{ size: "large" }}
|
||||||
|
footerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
></Route>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: false }],
|
||||||
|
routerInitialEntries: ["/posts/edit/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.DeleteButton),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("accessibility of buttons by accessControlProvider", () => {
|
||||||
|
it("should render disabled list button and not disabled delete button", async () => {
|
||||||
|
const { queryByTestId } = renderEdit(
|
||||||
|
<Edit
|
||||||
|
canDelete
|
||||||
|
footerButtons={({ defaultButtons, deleteButtonProps }) => {
|
||||||
|
expect(deleteButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
can: ({ action }) => {
|
||||||
|
switch (action) {
|
||||||
|
case "list":
|
||||||
|
return Promise.resolve({ can: true });
|
||||||
|
case "delete":
|
||||||
|
default:
|
||||||
|
return Promise.resolve({ can: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.ListButton),
|
||||||
|
).not.toBeDisabled(),
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.DeleteButton),
|
||||||
|
).toBeDisabled(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render disabled list button and delete button", async () => {
|
||||||
|
const { queryByTestId } = renderEdit(
|
||||||
|
<Edit
|
||||||
|
canDelete
|
||||||
|
headerButtons={({ defaultButtons, listButtonProps }) => {
|
||||||
|
expect(listButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
footerButtons={({ defaultButtons, deleteButtonProps }) => {
|
||||||
|
expect(deleteButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
can: ({ action }) => {
|
||||||
|
switch (action) {
|
||||||
|
case "list":
|
||||||
|
case "delete":
|
||||||
|
return Promise.resolve({ can: false });
|
||||||
|
default:
|
||||||
|
return Promise.resolve({ can: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.ListButton),
|
||||||
|
).toBeDisabled(),
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.DeleteButton),
|
||||||
|
).toBeDisabled(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should customize default buttons with default props", async () => {
|
||||||
|
const { queryByTestId } = renderEdit(
|
||||||
|
<Edit
|
||||||
|
canDelete
|
||||||
|
saveButtonProps={{ className: "customize-test" }}
|
||||||
|
headerButtons={({
|
||||||
|
listButtonProps,
|
||||||
|
refreshButtonProps,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RefreshButton {...refreshButtonProps} />
|
||||||
|
<ListButton {...listButtonProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
footerButtons={({ deleteButtonProps, saveButtonProps }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteButton {...deleteButtonProps} />
|
||||||
|
<SaveButton {...saveButtonProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
can: ({ action }) => {
|
||||||
|
switch (action) {
|
||||||
|
case "list":
|
||||||
|
case "delete":
|
||||||
|
return Promise.resolve({ can: false });
|
||||||
|
default:
|
||||||
|
return Promise.resolve({ can: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.DeleteButton),
|
||||||
|
).toBeDisabled(),
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.ListButton),
|
||||||
|
).toBeDisabled(),
|
||||||
|
);
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.SaveButton)).toHaveClass(
|
||||||
|
"customize-test",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.RefreshButton),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("list button", () => {
|
||||||
|
it("should render list button", async () => {
|
||||||
|
const { queryByTestId } = renderEdit(<Edit />);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.ListButton),
|
||||||
|
).not.toBeNull(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render list button when list resource is undefined", async () => {
|
||||||
|
const { queryByTestId } = renderEdit(
|
||||||
|
<Edit
|
||||||
|
headerButtons={({ defaultButtons, listButtonProps }) => {
|
||||||
|
expect(listButtonProps).toBeUndefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
resources: [{ name: "posts", list: undefined }],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.ListButton),
|
||||||
|
).toBeNull(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render list button when has recordItemId", async () => {
|
||||||
|
const { queryByTestId } = renderEdit(
|
||||||
|
<Edit
|
||||||
|
recordItemId="1"
|
||||||
|
headerButtons={({ defaultButtons, listButtonProps }) => {
|
||||||
|
expect(listButtonProps).toBeUndefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.ListButton),
|
||||||
|
).toBeNull(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("auto save", () => {
|
||||||
|
const EditPageWithAutoSave = () => {
|
||||||
|
const { formProps, formLoading, autoSaveProps } = useForm({
|
||||||
|
action: "edit",
|
||||||
|
autoSave: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Edit autoSaveProps={autoSaveProps}>
|
||||||
|
{formLoading && <div>loading...</div>}
|
||||||
|
<Form {...formProps} layout="vertical">
|
||||||
|
<Form.Item label="Title" name="title">
|
||||||
|
<Input data-testid="title" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Edit>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("check idle,loading,success statuses", async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/edit/:id"
|
||||||
|
element={<EditPageWithAutoSave />}
|
||||||
|
></Route>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: false }],
|
||||||
|
routerInitialEntries: ["/posts/edit/1"],
|
||||||
|
dataProvider: {
|
||||||
|
...MockJSONServer,
|
||||||
|
update: () => {
|
||||||
|
return new Promise((res) => {
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
res({
|
||||||
|
data: {
|
||||||
|
id: "1",
|
||||||
|
title: "ok",
|
||||||
|
} as any,
|
||||||
|
}),
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
getByText("Edit Post");
|
||||||
|
getByText("waiting for changes");
|
||||||
|
|
||||||
|
// update title and wait
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(getByTestId("title"), {
|
||||||
|
target: { value: "test" },
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(1100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// check saving message
|
||||||
|
expect(getByText("saving...")).toBeTruthy();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// check saved message
|
||||||
|
expect(getByText("saved")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("check error status", async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/edit/:id"
|
||||||
|
element={<EditPageWithAutoSave />}
|
||||||
|
></Route>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: false }],
|
||||||
|
routerInitialEntries: ["/posts/edit/1"],
|
||||||
|
dataProvider: {
|
||||||
|
...MockJSONServer,
|
||||||
|
update: () => {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
setTimeout(() => rej("error"), 1000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
getByText("Edit Post");
|
||||||
|
getByText("waiting for changes");
|
||||||
|
|
||||||
|
// update title and wait
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(getByTestId("title"), {
|
||||||
|
target: { value: "test" },
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(1100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// check saving message
|
||||||
|
expect(getByText("saving...")).toBeTruthy();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// check saved message
|
||||||
|
expect(getByText("auto save failure")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
231
packages/antd/src/components/crud/edit/index.tsx
Normal file
231
packages/antd/src/components/crud/edit/index.tsx
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Card, Space, Spin } from "antd";
|
||||||
|
import {
|
||||||
|
useMutationMode,
|
||||||
|
useNavigation,
|
||||||
|
useTranslate,
|
||||||
|
useUserFriendlyName,
|
||||||
|
useRefineContext,
|
||||||
|
useRouterType,
|
||||||
|
useBack,
|
||||||
|
useResource,
|
||||||
|
useGo,
|
||||||
|
useToPath,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeleteButton,
|
||||||
|
RefreshButton,
|
||||||
|
ListButton,
|
||||||
|
SaveButton,
|
||||||
|
Breadcrumb,
|
||||||
|
PageHeader,
|
||||||
|
ListButtonProps,
|
||||||
|
RefreshButtonProps,
|
||||||
|
DeleteButtonProps,
|
||||||
|
SaveButtonProps,
|
||||||
|
AutoSaveIndicator,
|
||||||
|
} from "@components";
|
||||||
|
import { EditProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<Edit>` provides us a layout for displaying the page.
|
||||||
|
* It does not contain any logic but adds extra functionalities like a refresh button.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/ui-frameworks/antd/components/basic-views/edit} for more details.
|
||||||
|
*/
|
||||||
|
export const Edit: React.FC<EditProps> = ({
|
||||||
|
title,
|
||||||
|
saveButtonProps: saveButtonPropsFromProps,
|
||||||
|
mutationMode: mutationModeProp,
|
||||||
|
recordItemId,
|
||||||
|
children,
|
||||||
|
deleteButtonProps: deleteButtonPropsFromProps,
|
||||||
|
canDelete,
|
||||||
|
resource: resourceFromProps,
|
||||||
|
isLoading = false,
|
||||||
|
dataProviderName,
|
||||||
|
breadcrumb: breadcrumbFromProps,
|
||||||
|
wrapperProps,
|
||||||
|
headerProps,
|
||||||
|
contentProps,
|
||||||
|
headerButtonProps,
|
||||||
|
headerButtons,
|
||||||
|
footerButtonProps,
|
||||||
|
footerButtons,
|
||||||
|
goBack: goBackFromProps,
|
||||||
|
autoSaveProps,
|
||||||
|
}) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
const { options: { breadcrumb: globalBreadcrumb } = {} } =
|
||||||
|
useRefineContext();
|
||||||
|
const { mutationMode: mutationModeContext } = useMutationMode();
|
||||||
|
const mutationMode = mutationModeProp ?? mutationModeContext;
|
||||||
|
|
||||||
|
const routerType = useRouterType();
|
||||||
|
const back = useBack();
|
||||||
|
const go = useGo();
|
||||||
|
const { goBack, list: legacyGoList } = useNavigation();
|
||||||
|
const getUserFriendlyName = useUserFriendlyName();
|
||||||
|
|
||||||
|
const {
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
id: idFromParams,
|
||||||
|
identifier,
|
||||||
|
} = useResource(resourceFromProps);
|
||||||
|
|
||||||
|
const goListPath = useToPath({
|
||||||
|
resource,
|
||||||
|
action: "list",
|
||||||
|
});
|
||||||
|
|
||||||
|
const id = recordItemId ?? idFromParams;
|
||||||
|
|
||||||
|
const breadcrumb =
|
||||||
|
typeof breadcrumbFromProps === "undefined"
|
||||||
|
? globalBreadcrumb
|
||||||
|
: breadcrumbFromProps;
|
||||||
|
|
||||||
|
const hasList = resource?.list && !recordItemId;
|
||||||
|
const isDeleteButtonVisible =
|
||||||
|
canDelete ??
|
||||||
|
((resource?.meta?.canDelete ?? resource?.canDelete) ||
|
||||||
|
deleteButtonPropsFromProps);
|
||||||
|
|
||||||
|
const listButtonProps: ListButtonProps | undefined = hasList
|
||||||
|
? {
|
||||||
|
...(isLoading ? { disabled: true } : {}),
|
||||||
|
resource: routerType === "legacy" ? resource?.route : identifier,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const refreshButtonProps: RefreshButtonProps = {
|
||||||
|
...(isLoading ? { disabled: true } : {}),
|
||||||
|
resource: routerType === "legacy" ? resource?.route : identifier,
|
||||||
|
recordItemId: id,
|
||||||
|
dataProviderName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteButtonProps: DeleteButtonProps | undefined =
|
||||||
|
isDeleteButtonVisible
|
||||||
|
? {
|
||||||
|
...(isLoading ? { disabled: true } : {}),
|
||||||
|
resource:
|
||||||
|
routerType === "legacy" ? resource?.route : identifier,
|
||||||
|
mutationMode,
|
||||||
|
onSuccess: () => {
|
||||||
|
if (routerType === "legacy") {
|
||||||
|
legacyGoList(resource?.route ?? resource?.name ?? "");
|
||||||
|
} else {
|
||||||
|
go({ to: goListPath });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recordItemId: id,
|
||||||
|
dataProviderName,
|
||||||
|
...deleteButtonPropsFromProps,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const saveButtonProps: SaveButtonProps = {
|
||||||
|
...(isLoading ? { disabled: true } : {}),
|
||||||
|
...saveButtonPropsFromProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultHeaderButtons = (
|
||||||
|
<>
|
||||||
|
{autoSaveProps && <AutoSaveIndicator {...autoSaveProps} />}
|
||||||
|
{hasList && <ListButton {...listButtonProps} />}
|
||||||
|
<RefreshButton {...refreshButtonProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultFooterButtons = (
|
||||||
|
<>
|
||||||
|
{isDeleteButtonVisible && <DeleteButton {...deleteButtonProps} />}
|
||||||
|
<SaveButton {...saveButtonProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...(wrapperProps ?? {})}>
|
||||||
|
<PageHeader
|
||||||
|
ghost={false}
|
||||||
|
backIcon={goBackFromProps}
|
||||||
|
onBack={
|
||||||
|
action !== "list" && typeof action !== "undefined"
|
||||||
|
? routerType === "legacy"
|
||||||
|
? goBack
|
||||||
|
: back
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
title ??
|
||||||
|
translate(
|
||||||
|
`${identifier}.titles.edit`,
|
||||||
|
`Edit ${getUserFriendlyName(
|
||||||
|
resource?.meta?.label ??
|
||||||
|
resource?.options?.label ??
|
||||||
|
resource?.label ??
|
||||||
|
identifier,
|
||||||
|
"singular",
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Space wrap {...(headerButtonProps ?? {})}>
|
||||||
|
{headerButtons
|
||||||
|
? typeof headerButtons === "function"
|
||||||
|
? headerButtons({
|
||||||
|
defaultButtons: defaultHeaderButtons,
|
||||||
|
listButtonProps,
|
||||||
|
refreshButtonProps,
|
||||||
|
})
|
||||||
|
: headerButtons
|
||||||
|
: defaultHeaderButtons}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
breadcrumb={
|
||||||
|
typeof breadcrumb !== "undefined" ? (
|
||||||
|
<>{breadcrumb}</> ?? undefined
|
||||||
|
) : (
|
||||||
|
<Breadcrumb />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...(headerProps ?? {})}
|
||||||
|
>
|
||||||
|
<Spin spinning={isLoading}>
|
||||||
|
<Card
|
||||||
|
bordered={false}
|
||||||
|
actions={[
|
||||||
|
<Space
|
||||||
|
key="footer-buttons"
|
||||||
|
wrap
|
||||||
|
style={{
|
||||||
|
float: "right",
|
||||||
|
marginRight: 24,
|
||||||
|
}}
|
||||||
|
{...(footerButtonProps ?? {})}
|
||||||
|
>
|
||||||
|
{footerButtons
|
||||||
|
? typeof footerButtons === "function"
|
||||||
|
? footerButtons({
|
||||||
|
defaultButtons:
|
||||||
|
defaultFooterButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
saveButtonProps,
|
||||||
|
})
|
||||||
|
: footerButtons
|
||||||
|
: defaultFooterButtons}
|
||||||
|
</Space>,
|
||||||
|
]}
|
||||||
|
{...(contentProps ?? {})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
</Spin>
|
||||||
|
</PageHeader>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
6
packages/antd/src/components/crud/index.ts
Normal file
6
packages/antd/src/components/crud/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export { List } from "./list";
|
||||||
|
export { Create } from "./create";
|
||||||
|
export { Edit } from "./edit";
|
||||||
|
export { Show } from "./show";
|
||||||
|
|
||||||
|
export * from "./types";
|
45
packages/antd/src/components/crud/list/index.spec.tsx
Normal file
45
packages/antd/src/components/crud/list/index.spec.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { crudListTests } from "@refinedev/ui-tests";
|
||||||
|
import { RefineButtonTestIds } from "@refinedev/ui-types";
|
||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
import { CreateButton } from "@components/buttons";
|
||||||
|
import { render, TestWrapper } from "@test";
|
||||||
|
import { List } from "./index";
|
||||||
|
|
||||||
|
const renderList = (list: ReactNode) => {
|
||||||
|
return render(
|
||||||
|
<Routes>
|
||||||
|
<Route path="/:resource" element={list} />
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
routerInitialEntries: ["/posts"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("<List/>", () => {
|
||||||
|
crudListTests.bind(this)(List);
|
||||||
|
|
||||||
|
it("should customize default buttons with default props", async () => {
|
||||||
|
const { queryByTestId } = renderList(
|
||||||
|
<List
|
||||||
|
createButtonProps={{ className: "customize-test" }}
|
||||||
|
headerButtons={({ createButtonProps }) => {
|
||||||
|
expect(createButtonProps).toBeDefined();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreateButton {...createButtonProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.CreateButton)).toHaveClass(
|
||||||
|
"customize-test",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
115
packages/antd/src/components/crud/list/index.tsx
Normal file
115
packages/antd/src/components/crud/list/index.tsx
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Space } from "antd";
|
||||||
|
import {
|
||||||
|
useTranslate,
|
||||||
|
useUserFriendlyName,
|
||||||
|
useRefineContext,
|
||||||
|
useRouterType,
|
||||||
|
useResource,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
CreateButton,
|
||||||
|
CreateButtonProps,
|
||||||
|
PageHeader,
|
||||||
|
} from "@components";
|
||||||
|
import { ListProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<List>` provides us a layout for displaying the page.
|
||||||
|
* It does not contain any logic but adds extra functionalities like a refresh button.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/ui-frameworks/antd/components/basic-views/list} for more details.
|
||||||
|
*/
|
||||||
|
export const List: React.FC<ListProps> = ({
|
||||||
|
canCreate,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
createButtonProps: createButtonPropsFromProps,
|
||||||
|
resource: resourceFromProps,
|
||||||
|
wrapperProps,
|
||||||
|
contentProps,
|
||||||
|
headerProps,
|
||||||
|
breadcrumb: breadcrumbFromProps,
|
||||||
|
headerButtonProps,
|
||||||
|
headerButtons,
|
||||||
|
}) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
const { options: { breadcrumb: globalBreadcrumb } = {} } =
|
||||||
|
useRefineContext();
|
||||||
|
|
||||||
|
const routerType = useRouterType();
|
||||||
|
const getUserFriendlyName = useUserFriendlyName();
|
||||||
|
|
||||||
|
const { resource, identifier } = useResource(resourceFromProps);
|
||||||
|
|
||||||
|
const isCreateButtonVisible =
|
||||||
|
canCreate ??
|
||||||
|
((resource?.canCreate ?? !!resource?.create) ||
|
||||||
|
createButtonPropsFromProps);
|
||||||
|
|
||||||
|
const breadcrumb =
|
||||||
|
typeof breadcrumbFromProps === "undefined"
|
||||||
|
? globalBreadcrumb
|
||||||
|
: breadcrumbFromProps;
|
||||||
|
|
||||||
|
const createButtonProps: CreateButtonProps | undefined =
|
||||||
|
isCreateButtonVisible
|
||||||
|
? {
|
||||||
|
size: "middle",
|
||||||
|
resource:
|
||||||
|
routerType === "legacy" ? resource?.route : identifier,
|
||||||
|
...createButtonPropsFromProps,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const defaultExtra = isCreateButtonVisible ? (
|
||||||
|
<CreateButton {...createButtonProps} />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...(wrapperProps ?? {})}>
|
||||||
|
<PageHeader
|
||||||
|
ghost={false}
|
||||||
|
title={
|
||||||
|
title ??
|
||||||
|
translate(
|
||||||
|
`${identifier}.titles.list`,
|
||||||
|
getUserFriendlyName(
|
||||||
|
resource?.meta?.label ??
|
||||||
|
resource?.options?.label ??
|
||||||
|
resource?.label ??
|
||||||
|
identifier,
|
||||||
|
"plural",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
headerButtons ? (
|
||||||
|
<Space wrap {...headerButtonProps}>
|
||||||
|
{typeof headerButtons === "function"
|
||||||
|
? headerButtons({
|
||||||
|
defaultButtons: defaultExtra,
|
||||||
|
createButtonProps,
|
||||||
|
})
|
||||||
|
: headerButtons}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
defaultExtra
|
||||||
|
)
|
||||||
|
}
|
||||||
|
breadcrumb={
|
||||||
|
typeof breadcrumb !== "undefined" ? (
|
||||||
|
<>{breadcrumb}</> ?? undefined
|
||||||
|
) : (
|
||||||
|
<Breadcrumb />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...(headerProps ?? {})}
|
||||||
|
>
|
||||||
|
<div {...(contentProps ?? {})}>{children}</div>
|
||||||
|
</PageHeader>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
459
packages/antd/src/components/crud/show/index.spec.tsx
Normal file
459
packages/antd/src/components/crud/show/index.spec.tsx
Normal file
|
@ -0,0 +1,459 @@
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
import { RefineButtonTestIds } from "@refinedev/ui-types";
|
||||||
|
import { AccessControlProvider } from "@refinedev/core";
|
||||||
|
|
||||||
|
import { render, TestWrapper, waitFor } from "@test";
|
||||||
|
|
||||||
|
import { Show } from "./index";
|
||||||
|
import { crudShowTests } from "@refinedev/ui-tests";
|
||||||
|
import {
|
||||||
|
DeleteButton,
|
||||||
|
EditButton,
|
||||||
|
ListButton,
|
||||||
|
RefreshButton,
|
||||||
|
} from "@components/buttons";
|
||||||
|
|
||||||
|
const renderShow = (
|
||||||
|
show: ReactNode,
|
||||||
|
accessControlProvider?: AccessControlProvider,
|
||||||
|
) => {
|
||||||
|
return render(
|
||||||
|
<Routes>
|
||||||
|
<Route path="/:resource/:action/:id" element={show} />
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
accessControlProvider,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
describe("Show", () => {
|
||||||
|
crudShowTests.bind(this)(Show as any);
|
||||||
|
|
||||||
|
it("depending on the accessControlProvider it should get the buttons successfully", async () => {
|
||||||
|
const { getByText, getAllByText, queryByTestId } = renderShow(
|
||||||
|
<Show
|
||||||
|
canEdit
|
||||||
|
canDelete
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
editButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).toBeDefined();
|
||||||
|
expect(editButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
can: ({ action }) => {
|
||||||
|
switch (action) {
|
||||||
|
case "edit":
|
||||||
|
case "list":
|
||||||
|
return Promise.resolve({ can: true });
|
||||||
|
case "delete":
|
||||||
|
default:
|
||||||
|
return Promise.resolve({ can: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getByText("Edit").closest("button")).not.toBeDisabled(),
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
getAllByText("Posts")[1].closest("button"),
|
||||||
|
).not.toBeDisabled(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.DeleteButton),
|
||||||
|
).toBeDisabled(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render optional recordItemId with resource prop, not render list button", async () => {
|
||||||
|
const { getByText, queryByTestId } = renderShow(
|
||||||
|
<Show
|
||||||
|
recordItemId="1"
|
||||||
|
headerButtons={({ defaultButtons, listButtonProps }) => {
|
||||||
|
expect(listButtonProps).not.toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
getByText("Show Post");
|
||||||
|
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.ListButton)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("render edit button", () => {
|
||||||
|
it("should render edit button", async () => {
|
||||||
|
const { getByText, queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
editButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(editButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
></Route>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", edit: () => null }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.EditButton),
|
||||||
|
).not.toBeNull();
|
||||||
|
|
||||||
|
getByText("Show Post");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render edit button on resource canEdit false", async () => {
|
||||||
|
const { getByText, queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
editButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(editButtonProps).not.toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts" }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.EditButton)).toBeNull();
|
||||||
|
|
||||||
|
getByText("Show Post");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render edit button on resource canEdit true & canEdit props false on component", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
canEdit={false}
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
editButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(editButtonProps).not.toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", edit: () => null }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.EditButton)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render edit button on resource canEdit false & canEdit props true on component", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
canEdit={true}
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
editButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(editButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts" }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.EditButton),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render edit button with recordItemId prop", async () => {
|
||||||
|
const { getByText, queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
recordItemId="1"
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
editButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(editButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", edit: () => null }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.EditButton),
|
||||||
|
).not.toBeNull();
|
||||||
|
|
||||||
|
getByText("Show Post");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("render delete button", () => {
|
||||||
|
it("should render delete button", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: true }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.DeleteButton),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render delete button on resource canDelete false", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).not.toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: false }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.DeleteButton)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render delete button on resource canDelete true & canDelete props false on component", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
canDelete={false}
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).not.toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: true }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.DeleteButton)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render delete button on resource canDelete false & canDelete props true on component", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
canDelete={true}
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: false }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.DeleteButton),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render delete button with recordItemId prop", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
recordItemId="1"
|
||||||
|
headerButtons={({
|
||||||
|
defaultButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
}) => {
|
||||||
|
expect(deleteButtonProps).toBeDefined();
|
||||||
|
return <>{defaultButtons}</>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts", canDelete: true }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByTestId(RefineButtonTestIds.DeleteButton),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should customize default buttons with default props", async () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/:resource/:action/:id"
|
||||||
|
element={
|
||||||
|
<Show
|
||||||
|
canEdit
|
||||||
|
canDelete
|
||||||
|
headerButtons={({
|
||||||
|
deleteButtonProps,
|
||||||
|
editButtonProps,
|
||||||
|
listButtonProps,
|
||||||
|
refreshButtonProps,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteButton {...deleteButtonProps} />
|
||||||
|
<EditButton {...editButtonProps} />
|
||||||
|
<ListButton {...listButtonProps} />
|
||||||
|
<RefreshButton
|
||||||
|
{...refreshButtonProps}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
wrapper: TestWrapper({
|
||||||
|
resources: [{ name: "posts" }],
|
||||||
|
routerInitialEntries: ["/posts/show/1"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.DeleteButton)).not.toBeNull();
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.EditButton)).not.toBeNull();
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.ListButton)).not.toBeNull();
|
||||||
|
expect(queryByTestId(RefineButtonTestIds.RefreshButton)).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
216
packages/antd/src/components/crud/show/index.tsx
Normal file
216
packages/antd/src/components/crud/show/index.tsx
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Card, Space, Spin } from "antd";
|
||||||
|
import {
|
||||||
|
useNavigation,
|
||||||
|
useTranslate,
|
||||||
|
useUserFriendlyName,
|
||||||
|
useRefineContext,
|
||||||
|
useResource,
|
||||||
|
useToPath,
|
||||||
|
useRouterType,
|
||||||
|
useBack,
|
||||||
|
useGo,
|
||||||
|
} from "@refinedev/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EditButton,
|
||||||
|
DeleteButton,
|
||||||
|
RefreshButton,
|
||||||
|
ListButton,
|
||||||
|
Breadcrumb,
|
||||||
|
PageHeader,
|
||||||
|
ListButtonProps,
|
||||||
|
EditButtonProps,
|
||||||
|
DeleteButtonProps,
|
||||||
|
RefreshButtonProps,
|
||||||
|
} from "@components";
|
||||||
|
import { ShowProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<Show>` provides us a layout for displaying the page.
|
||||||
|
* It does not contain any logic but adds extra functionalities like a refresh button.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/ui-frameworks/antd/components/basic-views/show} for more details.
|
||||||
|
*/
|
||||||
|
export const Show: React.FC<ShowProps> = ({
|
||||||
|
title,
|
||||||
|
canEdit,
|
||||||
|
canDelete,
|
||||||
|
isLoading = false,
|
||||||
|
children,
|
||||||
|
resource: resourceFromProps,
|
||||||
|
recordItemId,
|
||||||
|
dataProviderName,
|
||||||
|
breadcrumb: breadcrumbFromProps,
|
||||||
|
contentProps,
|
||||||
|
headerProps,
|
||||||
|
wrapperProps,
|
||||||
|
headerButtons,
|
||||||
|
footerButtons,
|
||||||
|
footerButtonProps,
|
||||||
|
headerButtonProps,
|
||||||
|
goBack: goBackFromProps,
|
||||||
|
}) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
const { options: { breadcrumb: globalBreadcrumb } = {} } =
|
||||||
|
useRefineContext();
|
||||||
|
|
||||||
|
const routerType = useRouterType();
|
||||||
|
const back = useBack();
|
||||||
|
const go = useGo();
|
||||||
|
const { goBack, list: legacyGoList } = useNavigation();
|
||||||
|
const getUserFriendlyName = useUserFriendlyName();
|
||||||
|
|
||||||
|
const {
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
id: idFromParams,
|
||||||
|
identifier,
|
||||||
|
} = useResource(resourceFromProps);
|
||||||
|
|
||||||
|
const goListPath = useToPath({
|
||||||
|
resource,
|
||||||
|
action: "list",
|
||||||
|
});
|
||||||
|
|
||||||
|
const id = recordItemId ?? idFromParams;
|
||||||
|
|
||||||
|
const breadcrumb =
|
||||||
|
typeof breadcrumbFromProps === "undefined"
|
||||||
|
? globalBreadcrumb
|
||||||
|
: breadcrumbFromProps;
|
||||||
|
|
||||||
|
const hasList = resource?.list && !recordItemId;
|
||||||
|
const isDeleteButtonVisible =
|
||||||
|
canDelete ?? resource?.meta?.canDelete ?? resource?.canDelete;
|
||||||
|
const isEditButtonVisible =
|
||||||
|
canEdit ?? resource?.canEdit ?? !!resource?.edit;
|
||||||
|
|
||||||
|
const listButtonProps: ListButtonProps | undefined = hasList
|
||||||
|
? {
|
||||||
|
resource: routerType === "legacy" ? resource?.route : identifier,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
const editButtonProps: EditButtonProps | undefined = isEditButtonVisible
|
||||||
|
? {
|
||||||
|
...(isLoading ? { disabled: true } : {}),
|
||||||
|
type: "primary",
|
||||||
|
resource: routerType === "legacy" ? resource?.route : identifier,
|
||||||
|
recordItemId: id,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
const deleteButtonProps: DeleteButtonProps | undefined =
|
||||||
|
isDeleteButtonVisible
|
||||||
|
? {
|
||||||
|
...(isLoading ? { disabled: true } : {}),
|
||||||
|
resource:
|
||||||
|
routerType === "legacy" ? resource?.route : identifier,
|
||||||
|
recordItemId: id,
|
||||||
|
onSuccess: () => {
|
||||||
|
if (routerType === "legacy") {
|
||||||
|
legacyGoList(resource?.route ?? resource?.name ?? "");
|
||||||
|
} else {
|
||||||
|
go({ to: goListPath });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataProviderName,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
const refreshButtonProps: RefreshButtonProps = {
|
||||||
|
...(isLoading ? { disabled: true } : {}),
|
||||||
|
resource: routerType === "legacy" ? resource?.route : identifier,
|
||||||
|
recordItemId: id,
|
||||||
|
dataProviderName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultHeaderButtons = (
|
||||||
|
<>
|
||||||
|
{hasList && <ListButton {...listButtonProps} />}
|
||||||
|
{isEditButtonVisible && <EditButton {...editButtonProps} />}
|
||||||
|
{isDeleteButtonVisible && <DeleteButton {...deleteButtonProps} />}
|
||||||
|
<RefreshButton {...refreshButtonProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...(wrapperProps ?? {})}>
|
||||||
|
<PageHeader
|
||||||
|
ghost={false}
|
||||||
|
backIcon={goBackFromProps}
|
||||||
|
onBack={
|
||||||
|
action !== "list" && typeof action !== "undefined"
|
||||||
|
? routerType === "legacy"
|
||||||
|
? goBack
|
||||||
|
: back
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
title ??
|
||||||
|
translate(
|
||||||
|
`${identifier}.titles.show`,
|
||||||
|
`Show ${getUserFriendlyName(
|
||||||
|
resource?.meta?.label ??
|
||||||
|
resource?.options?.label ??
|
||||||
|
resource?.label ??
|
||||||
|
identifier,
|
||||||
|
"singular",
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Space
|
||||||
|
key="extra-buttons"
|
||||||
|
wrap
|
||||||
|
{...(headerButtonProps ?? {})}
|
||||||
|
>
|
||||||
|
{headerButtons
|
||||||
|
? typeof headerButtons === "function"
|
||||||
|
? headerButtons({
|
||||||
|
defaultButtons: defaultHeaderButtons,
|
||||||
|
deleteButtonProps,
|
||||||
|
editButtonProps,
|
||||||
|
listButtonProps,
|
||||||
|
refreshButtonProps,
|
||||||
|
})
|
||||||
|
: headerButtons
|
||||||
|
: defaultHeaderButtons}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
breadcrumb={
|
||||||
|
typeof breadcrumb !== "undefined" ? (
|
||||||
|
<>{breadcrumb}</> ?? undefined
|
||||||
|
) : (
|
||||||
|
<Breadcrumb />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...(headerProps ?? {})}
|
||||||
|
>
|
||||||
|
<Spin spinning={isLoading}>
|
||||||
|
<Card
|
||||||
|
bordered={false}
|
||||||
|
actions={
|
||||||
|
footerButtons
|
||||||
|
? [
|
||||||
|
<Space
|
||||||
|
key="footer-buttons"
|
||||||
|
wrap
|
||||||
|
{...footerButtonProps}
|
||||||
|
>
|
||||||
|
{typeof footerButtons === "function"
|
||||||
|
? footerButtons({
|
||||||
|
defaultButtons: null,
|
||||||
|
})
|
||||||
|
: footerButtons}
|
||||||
|
</Space>,
|
||||||
|
]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
{...(contentProps ?? {})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
</Spin>
|
||||||
|
</PageHeader>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
74
packages/antd/src/components/crud/types.ts
Normal file
74
packages/antd/src/components/crud/types.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { CardProps, SpaceProps } from "antd";
|
||||||
|
import {
|
||||||
|
CreateButtonProps,
|
||||||
|
DeleteButtonProps,
|
||||||
|
EditButtonProps,
|
||||||
|
ListButtonProps,
|
||||||
|
RefreshButtonProps,
|
||||||
|
SaveButtonProps,
|
||||||
|
} from "../buttons/types";
|
||||||
|
import {
|
||||||
|
RefineCrudCreateProps,
|
||||||
|
RefineCrudEditProps,
|
||||||
|
RefineCrudListProps,
|
||||||
|
RefineCrudShowProps,
|
||||||
|
} from "@refinedev/ui-types";
|
||||||
|
import { PageHeaderProps } from "../pageHeader";
|
||||||
|
|
||||||
|
export type CreateProps = RefineCrudCreateProps<
|
||||||
|
SaveButtonProps,
|
||||||
|
SpaceProps,
|
||||||
|
SpaceProps,
|
||||||
|
React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>,
|
||||||
|
PageHeaderProps,
|
||||||
|
CardProps
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type EditProps = RefineCrudEditProps<
|
||||||
|
SaveButtonProps,
|
||||||
|
DeleteButtonProps,
|
||||||
|
SpaceProps,
|
||||||
|
SpaceProps,
|
||||||
|
React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>,
|
||||||
|
PageHeaderProps,
|
||||||
|
CardProps,
|
||||||
|
{},
|
||||||
|
RefreshButtonProps,
|
||||||
|
ListButtonProps
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ListProps = RefineCrudListProps<
|
||||||
|
CreateButtonProps,
|
||||||
|
SpaceProps,
|
||||||
|
React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>,
|
||||||
|
PageHeaderProps,
|
||||||
|
React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ShowProps = RefineCrudShowProps<
|
||||||
|
SpaceProps,
|
||||||
|
SpaceProps,
|
||||||
|
React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>,
|
||||||
|
PageHeaderProps,
|
||||||
|
CardProps,
|
||||||
|
{},
|
||||||
|
EditButtonProps,
|
||||||
|
DeleteButtonProps,
|
||||||
|
RefreshButtonProps,
|
||||||
|
ListButtonProps
|
||||||
|
>;
|
54
packages/antd/src/components/fields/boolean/index.spec.tsx
Normal file
54
packages/antd/src/components/fields/boolean/index.spec.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import React from "react";
|
||||||
|
import { fieldBooleanTests } from "@refinedev/ui-tests";
|
||||||
|
|
||||||
|
import { render, fireEvent } from "@test";
|
||||||
|
import { BooleanField } from "./";
|
||||||
|
|
||||||
|
describe("BooleanField", () => {
|
||||||
|
fieldBooleanTests.bind(this)(BooleanField);
|
||||||
|
describe("BooleanField with default props values", () => {
|
||||||
|
const initialValues = [true, false, "true", "false", "", undefined];
|
||||||
|
|
||||||
|
const results = ["true", "false", "true", "true", "false", "false"];
|
||||||
|
|
||||||
|
const iconClass = [
|
||||||
|
"anticon-check",
|
||||||
|
"anticon-close",
|
||||||
|
"anticon-check",
|
||||||
|
"anticon-check",
|
||||||
|
"anticon-close",
|
||||||
|
"anticon-close",
|
||||||
|
];
|
||||||
|
|
||||||
|
initialValues.forEach((element, index) => {
|
||||||
|
const testName =
|
||||||
|
index === 2 || index === 3 || index === 4
|
||||||
|
? `"${initialValues[index]}"`
|
||||||
|
: initialValues[index];
|
||||||
|
|
||||||
|
it(`renders boolean field value(${testName}) with correct tooltip text and icon`, async () => {
|
||||||
|
const baseDom = render(
|
||||||
|
<div data-testid="default-field">
|
||||||
|
<BooleanField value={element} />
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.mouseOver(
|
||||||
|
baseDom.getByTestId("default-field").children[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await baseDom.findByText(results[index]),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
baseDom
|
||||||
|
.getByTestId("default-field")
|
||||||
|
.children[0].children[0].classList.contains(
|
||||||
|
iconClass[index],
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
26
packages/antd/src/components/fields/boolean/index.tsx
Normal file
26
packages/antd/src/components/fields/boolean/index.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Tooltip } from "antd";
|
||||||
|
|
||||||
|
import { CheckOutlined, CloseOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
import { BooleanFieldProps } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field is used to display boolean values. It uses the {@link https://ant.design/components/tooltip/#header `<Tooltip>`} values from Ant Design.
|
||||||
|
*
|
||||||
|
* @see {@link https://refine.dev/docs/api-reference/antd/components/fields/boolean} for more details.
|
||||||
|
*/
|
||||||
|
export const BooleanField: React.FC<BooleanFieldProps> = ({
|
||||||
|
value,
|
||||||
|
valueLabelTrue = "true",
|
||||||
|
valueLabelFalse = "false",
|
||||||
|
trueIcon = <CheckOutlined />,
|
||||||
|
falseIcon = <CloseOutlined />,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Tooltip title={value ? valueLabelTrue : valueLabelFalse} {...rest}>
|
||||||
|
{value ? <span>{trueIcon}</span> : <span>{falseIcon}</span>}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
7
packages/antd/src/components/fields/date/index.spec.tsx
Normal file
7
packages/antd/src/components/fields/date/index.spec.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { fieldDateTests } from "@refinedev/ui-tests";
|
||||||
|
|
||||||
|
import { DateField } from "./";
|
||||||
|
|
||||||
|
describe("DateField", () => {
|
||||||
|
fieldDateTests.bind(this)(DateField);
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue