flipper-plugin-sea-mammals 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BUILDING_CUSTOM_UI_GUIDE.md +164 -0
- package/IMPLEMENTATION_SUMMARY.md +155 -0
- package/__mocks__/fileMock.js +1 -0
- package/__mocks__/moment.js +2 -0
- package/babel.config.js +7 -0
- package/dist/bundle.js +155 -0
- package/jest-setup.ts +2 -0
- package/jest.config.js +38 -0
- package/package.json +51 -0
- package/src/__tests__/seamammals.spec.tsx +156 -0
- package/src/components/MammalCard.tsx +69 -0
- package/src/index.tsx +107 -0
- package/todo1 +0 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Flipper 插件自定义 UI 构建指南
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
本指南详细说明了如何将 Flipper 插件从简单的表格 UI 转换为自定义卡片式 UI,包括选择功能和侧边栏详情展示。
|
|
6
|
+
|
|
7
|
+
## 项目结构
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
flipper-plugin-sea-mammals/
|
|
11
|
+
├── src/
|
|
12
|
+
│ ├── index.tsx # 主插件文件
|
|
13
|
+
│ ├── components/ # UI组件
|
|
14
|
+
│ └── __tests__/ # 测试文件
|
|
15
|
+
├── BUILDING_CUSTOM_UI_GUIDE.md # 本指南
|
|
16
|
+
└── package.json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 步骤详解
|
|
20
|
+
|
|
21
|
+
### 步骤 1:理解插件架构
|
|
22
|
+
|
|
23
|
+
**目标**:理解 Flipper 插件的核心概念
|
|
24
|
+
|
|
25
|
+
**关键概念**:
|
|
26
|
+
|
|
27
|
+
- `plugin` 函数:插件逻辑的核心,处理数据流和状态管理
|
|
28
|
+
- `Component` 函数:UI 渲染的入口点
|
|
29
|
+
- `PluginClient`:与客户端应用通信的接口
|
|
30
|
+
- `createState`:创建可持久化的状态容器
|
|
31
|
+
|
|
32
|
+
**实现位置**:`src/index.tsx`
|
|
33
|
+
|
|
34
|
+
### 步骤 2:定义数据类型
|
|
35
|
+
|
|
36
|
+
**目标**:定义清晰的数据结构
|
|
37
|
+
|
|
38
|
+
**数据类型**:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
type Row = {
|
|
42
|
+
id: number;
|
|
43
|
+
title: string;
|
|
44
|
+
url: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type Events = {
|
|
48
|
+
newRow: Row;
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**实现位置**:`src/index.tsx` 顶部
|
|
53
|
+
|
|
54
|
+
### 步骤 3:实现插件逻辑
|
|
55
|
+
|
|
56
|
+
**目标**:创建插件核心逻辑,包括状态管理和事件处理
|
|
57
|
+
|
|
58
|
+
**功能**:
|
|
59
|
+
|
|
60
|
+
- 状态管理:使用 `createState` 管理行数据和选择状态
|
|
61
|
+
- 事件处理:监听客户端发送的 `newRow` 事件
|
|
62
|
+
- 选择功能:提供 `setSelection` 方法更新选择状态
|
|
63
|
+
|
|
64
|
+
**实现位置**:`src/index.tsx` 中的 `plugin` 函数
|
|
65
|
+
|
|
66
|
+
### 步骤 4:创建 UI 组件
|
|
67
|
+
|
|
68
|
+
**目标**:构建自定义的卡片式 UI
|
|
69
|
+
|
|
70
|
+
**组件结构**:
|
|
71
|
+
|
|
72
|
+
- `MammalCard`:单个动物卡片组件
|
|
73
|
+
- `Component`:主 UI 组件,包含卡片网格和侧边栏
|
|
74
|
+
- `renderSidebar`:侧边栏详情渲染函数
|
|
75
|
+
|
|
76
|
+
**实现位置**:
|
|
77
|
+
|
|
78
|
+
- 主 UI:`src/index.tsx` 中的 `Component` 函数
|
|
79
|
+
- 卡片组件:`src/components/MammalCard.tsx`
|
|
80
|
+
|
|
81
|
+
### 步骤 5:实现选择功能
|
|
82
|
+
|
|
83
|
+
**目标**:添加卡片选择和侧边栏详情展示
|
|
84
|
+
|
|
85
|
+
**功能**:
|
|
86
|
+
|
|
87
|
+
- 卡片点击选择
|
|
88
|
+
- 选中状态视觉反馈
|
|
89
|
+
- 侧边栏显示选中卡片详情
|
|
90
|
+
|
|
91
|
+
**实现位置**:`src/index.tsx` 中的 `Component` 函数
|
|
92
|
+
|
|
93
|
+
### 步骤 6:编写单元测试
|
|
94
|
+
|
|
95
|
+
**目标**:确保插件逻辑和 UI 的正确性
|
|
96
|
+
|
|
97
|
+
**测试内容**:
|
|
98
|
+
|
|
99
|
+
- 数据存储测试:验证行数据正确存储
|
|
100
|
+
- UI 交互测试:验证选择和渲染功能
|
|
101
|
+
- 状态持久化测试:验证状态正确导出
|
|
102
|
+
|
|
103
|
+
**实现位置**:`src/__tests__/seamammals.spec.tsx`
|
|
104
|
+
|
|
105
|
+
## 技术要点
|
|
106
|
+
|
|
107
|
+
### 状态管理
|
|
108
|
+
|
|
109
|
+
- 使用 `createState` 创建可订阅的状态
|
|
110
|
+
- 使用 `persist` 选项实现状态持久化
|
|
111
|
+
- 使用 `useValue` hook 订阅状态变化
|
|
112
|
+
|
|
113
|
+
### 事件处理
|
|
114
|
+
|
|
115
|
+
- 使用 `client.onMessage` 监听客户端事件
|
|
116
|
+
- 使用 `client.send` 发送消息到客户端
|
|
117
|
+
- 事件类型通过 `Events` 泛型参数定义
|
|
118
|
+
|
|
119
|
+
### UI 组件
|
|
120
|
+
|
|
121
|
+
- 使用 Ant Design 组件库
|
|
122
|
+
- 使用 Flipper 的 Layout 组件
|
|
123
|
+
- 使用 `DataInspector` 展示数据结构
|
|
124
|
+
|
|
125
|
+
### 测试策略
|
|
126
|
+
|
|
127
|
+
- 使用 `TestUtils.startPlugin` 测试插件逻辑
|
|
128
|
+
- 使用 `TestUtils.renderPlugin` 测试 UI 组件
|
|
129
|
+
- 使用 `sendEvent` 模拟客户端事件
|
|
130
|
+
- 使用 `exportState` 验证状态持久化
|
|
131
|
+
|
|
132
|
+
## 文件修改清单
|
|
133
|
+
|
|
134
|
+
1. **src/index.tsx**
|
|
135
|
+
|
|
136
|
+
- 添加选择状态管理
|
|
137
|
+
- 实现自定义 UI 组件
|
|
138
|
+
- 添加侧边栏功能
|
|
139
|
+
|
|
140
|
+
2. **src/components/MammalCard.tsx** (新建)
|
|
141
|
+
|
|
142
|
+
- 创建卡片组件
|
|
143
|
+
- 实现选择交互
|
|
144
|
+
|
|
145
|
+
3. **src/**tests**/seamammals.spec.tsx** (新建)
|
|
146
|
+
- 添加完整的测试用例
|
|
147
|
+
- 测试数据存储和 UI 交互
|
|
148
|
+
|
|
149
|
+
## 预期结果
|
|
150
|
+
|
|
151
|
+
完成后的插件将具备以下功能:
|
|
152
|
+
|
|
153
|
+
- 卡片式布局展示海洋哺乳动物
|
|
154
|
+
- 点击卡片显示侧边栏详情
|
|
155
|
+
- 数据持久化存储
|
|
156
|
+
- 完整的单元测试覆盖
|
|
157
|
+
- 响应式布局设计
|
|
158
|
+
|
|
159
|
+
## 下一步
|
|
160
|
+
|
|
161
|
+
1. 实现插件核心逻辑
|
|
162
|
+
2. 创建 UI 组件
|
|
163
|
+
3. 添加测试用例
|
|
164
|
+
4. 验证功能完整性
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Flipper 插件自定义 UI 实现总结
|
|
2
|
+
|
|
3
|
+
## 项目概述
|
|
4
|
+
|
|
5
|
+
本项目成功实现了 Flipper 插件的自定义 UI 功能,将简单的表格布局转换为美观的卡片式布局,并添加了选择功能和侧边栏详情展示。
|
|
6
|
+
|
|
7
|
+
## 实现的功能
|
|
8
|
+
|
|
9
|
+
### 1. 插件核心逻辑 (`src/index.tsx`)
|
|
10
|
+
|
|
11
|
+
- ✅ **状态管理**: 使用 `createState` 管理行数据和选择状态
|
|
12
|
+
- ✅ **事件处理**: 监听客户端发送的 `newRow` 事件
|
|
13
|
+
- ✅ **选择功能**: 提供 `setSelection` 方法更新选择状态
|
|
14
|
+
- ✅ **数据持久化**: 使用 `persist` 选项实现状态持久化
|
|
15
|
+
|
|
16
|
+
### 2. 自定义 UI 组件 (`src/components/MammalCard.tsx`)
|
|
17
|
+
|
|
18
|
+
- ✅ **卡片布局**: 实现美观的卡片式布局
|
|
19
|
+
- ✅ **选择交互**: 点击卡片进行选择
|
|
20
|
+
- ✅ **视觉反馈**: 选中状态的高亮显示
|
|
21
|
+
- ✅ **悬停效果**: 鼠标悬停时的动画效果
|
|
22
|
+
|
|
23
|
+
### 3. 主 UI 界面 (`src/index.tsx`)
|
|
24
|
+
|
|
25
|
+
- ✅ **响应式布局**: 使用 flexbox 实现响应式布局
|
|
26
|
+
- ✅ **侧边栏**: 显示选中卡片的详细信息
|
|
27
|
+
- ✅ **滚动容器**: 支持大量数据的滚动显示
|
|
28
|
+
- ✅ **主题集成**: 使用 Flipper 主题系统
|
|
29
|
+
|
|
30
|
+
### 4. 测试覆盖 (`src/__tests__/seamammals.spec.tsx`)
|
|
31
|
+
|
|
32
|
+
- ✅ **数据存储测试**: 验证行数据正确存储
|
|
33
|
+
- ✅ **UI 交互测试**: 验证选择和渲染功能
|
|
34
|
+
- ✅ **状态管理测试**: 验证选择状态正确管理
|
|
35
|
+
- ✅ **数据更新测试**: 验证数据更新处理
|
|
36
|
+
|
|
37
|
+
## 技术架构
|
|
38
|
+
|
|
39
|
+
### 数据流
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
客户端应用 → newRow事件 → 插件逻辑 → 状态更新 → UI重新渲染
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 组件层次
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Component (主UI)
|
|
49
|
+
├── MammalCard (卡片组件)
|
|
50
|
+
└── renderSidebar (侧边栏)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 状态管理
|
|
54
|
+
|
|
55
|
+
- `rows`: 存储所有行数据
|
|
56
|
+
- `selectedID`: 存储当前选中的 ID
|
|
57
|
+
|
|
58
|
+
## 文件结构
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
flipper-plugin-sea-mammals/
|
|
62
|
+
├── src/
|
|
63
|
+
│ ├── index.tsx # 主插件文件
|
|
64
|
+
│ ├── components/
|
|
65
|
+
│ │ └── MammalCard.tsx # 卡片组件
|
|
66
|
+
│ └── __tests__/
|
|
67
|
+
│ └── seamammals.spec.tsx # 测试文件
|
|
68
|
+
├── BUILDING_CUSTOM_UI_GUIDE.md # 构建指南
|
|
69
|
+
├── IMPLEMENTATION_SUMMARY.md # 本总结文档
|
|
70
|
+
└── package.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 关键实现细节
|
|
74
|
+
|
|
75
|
+
### 1. 插件定义
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
export function plugin(client: PluginClient<Events, {}>) {
|
|
79
|
+
const rows = createState<Record<string, Row>>({}, { persist: "rows" });
|
|
80
|
+
const selectedID = createState<string | null>(null, { persist: "selection" });
|
|
81
|
+
|
|
82
|
+
client.onMessage("newRow", (row) => {
|
|
83
|
+
rows.update((draft) => {
|
|
84
|
+
draft[row.id] = row;
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return { rows, selectedID, setSelection };
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 2. UI 组件
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
export function Component() {
|
|
96
|
+
const instance = usePlugin(plugin);
|
|
97
|
+
const rows = useValue(instance.rows);
|
|
98
|
+
const selectedID = useValue(instance.selectedID);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div style={{ display: "flex", height: "100%" }}>
|
|
102
|
+
{/* 卡片网格 */}
|
|
103
|
+
{/* 侧边栏 */}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 3. 卡片组件
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
export const MammalCard = memo<MammalCardProps>(
|
|
113
|
+
({ row, onSelect, selected }) => {
|
|
114
|
+
return (
|
|
115
|
+
<div style={cardStyle} onClick={() => onSelect(row.id)}>
|
|
116
|
+
<div style={imageStyle} />
|
|
117
|
+
<span style={titleStyle}>{row.title}</span>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 测试结果
|
|
125
|
+
|
|
126
|
+
- ✅ 4 个测试用例全部通过
|
|
127
|
+
- ✅ 数据存储功能正常
|
|
128
|
+
- ✅ UI 交互功能正常
|
|
129
|
+
- ✅ 状态管理功能正常
|
|
130
|
+
- ✅ 数据更新功能正常
|
|
131
|
+
|
|
132
|
+
## 性能优化
|
|
133
|
+
|
|
134
|
+
- ✅ 使用 `memo` 优化组件重渲染
|
|
135
|
+
- ✅ 使用 `useValue` 实现精确的状态订阅
|
|
136
|
+
- ✅ 使用 `createState` 实现高效的状态管理
|
|
137
|
+
|
|
138
|
+
## 可扩展性
|
|
139
|
+
|
|
140
|
+
- ✅ 组件化设计,易于添加新功能
|
|
141
|
+
- ✅ 类型安全,使用 TypeScript
|
|
142
|
+
- ✅ 测试覆盖,确保代码质量
|
|
143
|
+
- ✅ 文档完善,便于维护
|
|
144
|
+
|
|
145
|
+
## 下一步改进建议
|
|
146
|
+
|
|
147
|
+
1. 添加更多 UI 组件(如搜索、过滤功能)
|
|
148
|
+
2. 实现数据导出功能
|
|
149
|
+
3. 添加更多交互效果
|
|
150
|
+
4. 优化移动端适配
|
|
151
|
+
5. 添加国际化支持
|
|
152
|
+
|
|
153
|
+
## 总结
|
|
154
|
+
|
|
155
|
+
本项目成功实现了 Flipper 插件的自定义 UI 功能,展示了如何从简单的表格 UI 转换为功能丰富的卡片式 UI。通过合理的架构设计和完整的测试覆盖,确保了代码的质量和可维护性。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = "test-file-stub";
|
package/babel.config.js
ADDED
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
|
+
|
|
26
|
+
// src/index.tsx
|
|
27
|
+
var src_exports = {};
|
|
28
|
+
__export(src_exports, {
|
|
29
|
+
Component: () => Component,
|
|
30
|
+
plugin: () => plugin
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(src_exports);
|
|
33
|
+
var import_react2 = __toESM(require("react"));
|
|
34
|
+
var import_antd = require("antd");
|
|
35
|
+
var import_flipper_plugin2 = require("flipper-plugin");
|
|
36
|
+
|
|
37
|
+
// src/components/MammalCard.tsx
|
|
38
|
+
var import_react = __toESM(require("react"));
|
|
39
|
+
var import_flipper_plugin = require("flipper-plugin");
|
|
40
|
+
var MammalCard = (0, import_react.memo)(
|
|
41
|
+
({ row, onSelect, selected }) => {
|
|
42
|
+
const handleClick = () => {
|
|
43
|
+
onSelect(row.id);
|
|
44
|
+
};
|
|
45
|
+
const cardStyle = {
|
|
46
|
+
width: "200px",
|
|
47
|
+
margin: "8px",
|
|
48
|
+
cursor: "pointer",
|
|
49
|
+
border: `2px solid ${selected ? import_flipper_plugin.theme.primaryColor : "transparent"}`,
|
|
50
|
+
borderRadius: "8px",
|
|
51
|
+
background: "white",
|
|
52
|
+
transition: "all 0.2s ease"
|
|
53
|
+
};
|
|
54
|
+
const imageStyle = {
|
|
55
|
+
height: "120px",
|
|
56
|
+
backgroundImage: `url(${row.url})`,
|
|
57
|
+
backgroundSize: "cover",
|
|
58
|
+
backgroundPosition: "center",
|
|
59
|
+
backgroundRepeat: "no-repeat",
|
|
60
|
+
borderRadius: "8px 8px 0 0"
|
|
61
|
+
};
|
|
62
|
+
const titleStyle = {
|
|
63
|
+
fontWeight: 500,
|
|
64
|
+
color: import_flipper_plugin.theme.textColorPrimary,
|
|
65
|
+
display: "block",
|
|
66
|
+
padding: "12px"
|
|
67
|
+
};
|
|
68
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
69
|
+
"div",
|
|
70
|
+
{
|
|
71
|
+
style: cardStyle,
|
|
72
|
+
onClick: handleClick,
|
|
73
|
+
"data-testid": row.title,
|
|
74
|
+
onMouseEnter: (e) => {
|
|
75
|
+
e.currentTarget.style.transform = "translateY(-2px)";
|
|
76
|
+
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.15)";
|
|
77
|
+
},
|
|
78
|
+
onMouseLeave: (e) => {
|
|
79
|
+
e.currentTarget.style.transform = "translateY(0)";
|
|
80
|
+
e.currentTarget.style.boxShadow = "none";
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
/* @__PURE__ */ import_react.default.createElement("div", { style: imageStyle }),
|
|
84
|
+
/* @__PURE__ */ import_react.default.createElement("span", { style: titleStyle }, row.title)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
MammalCard.displayName = "MammalCard";
|
|
89
|
+
|
|
90
|
+
// src/index.tsx
|
|
91
|
+
function plugin(client) {
|
|
92
|
+
const rows = (0, import_flipper_plugin2.createState)({}, { persist: "rows" });
|
|
93
|
+
const selectedID = (0, import_flipper_plugin2.createState)(null, { persist: "selection" });
|
|
94
|
+
client.onMessage("newRow", (row) => {
|
|
95
|
+
rows.update((draft) => {
|
|
96
|
+
draft[row.id] = row;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
function setSelection(id) {
|
|
100
|
+
selectedID.set("" + id);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
rows,
|
|
104
|
+
selectedID,
|
|
105
|
+
setSelection
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function renderSidebar(row) {
|
|
109
|
+
return /* @__PURE__ */ import_react2.default.createElement("div", { style: { padding: "16px" } }, /* @__PURE__ */ import_react2.default.createElement(import_antd.Typography.Title, { level: 4 }, "Extras"), /* @__PURE__ */ import_react2.default.createElement(
|
|
110
|
+
"pre",
|
|
111
|
+
{
|
|
112
|
+
style: {
|
|
113
|
+
background: import_flipper_plugin2.theme.backgroundWash,
|
|
114
|
+
padding: "8px",
|
|
115
|
+
borderRadius: "4px"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
JSON.stringify(row, null, 2)
|
|
119
|
+
));
|
|
120
|
+
}
|
|
121
|
+
function Component() {
|
|
122
|
+
const instance = (0, import_flipper_plugin2.usePlugin)(plugin);
|
|
123
|
+
const rows = (0, import_flipper_plugin2.useValue)(instance.rows);
|
|
124
|
+
const selectedID = (0, import_flipper_plugin2.useValue)(instance.selectedID);
|
|
125
|
+
return /* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", height: "100%" } }, /* @__PURE__ */ import_react2.default.createElement(
|
|
126
|
+
"div",
|
|
127
|
+
{
|
|
128
|
+
style: {
|
|
129
|
+
flex: 1,
|
|
130
|
+
overflow: "auto",
|
|
131
|
+
background: import_flipper_plugin2.theme.backgroundWash,
|
|
132
|
+
padding: "16px"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
/* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" } }, Object.entries(rows).map(([id, row]) => /* @__PURE__ */ import_react2.default.createElement(
|
|
136
|
+
MammalCard,
|
|
137
|
+
{
|
|
138
|
+
row,
|
|
139
|
+
onSelect: instance.setSelection,
|
|
140
|
+
selected: id === selectedID,
|
|
141
|
+
key: id
|
|
142
|
+
}
|
|
143
|
+
)))
|
|
144
|
+
), selectedID && /* @__PURE__ */ import_react2.default.createElement(
|
|
145
|
+
"div",
|
|
146
|
+
{
|
|
147
|
+
style: {
|
|
148
|
+
width: "300px",
|
|
149
|
+
borderLeft: `1px solid ${import_flipper_plugin2.theme.borderColor}`
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
renderSidebar(rows[selectedID])
|
|
153
|
+
));
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vc3JjL2luZGV4LnRzeCIsICIuLi9zcmMvY29tcG9uZW50cy9NYW1tYWxDYXJkLnRzeCJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiaW1wb3J0IFJlYWN0LCB7IG1lbW8gfSBmcm9tIFwicmVhY3RcIjtcbmltcG9ydCB7IFR5cG9ncmFwaHkgfSBmcm9tIFwiYW50ZFwiO1xuaW1wb3J0IHtcbiAgTGF5b3V0LFxuICBQbHVnaW5DbGllbnQsXG4gIHVzZVBsdWdpbixcbiAgY3JlYXRlU3RhdGUsXG4gIHVzZVZhbHVlLFxuICB0aGVtZSxcbn0gZnJvbSBcImZsaXBwZXItcGx1Z2luXCI7XG5pbXBvcnQgeyBNYW1tYWxDYXJkIH0gZnJvbSBcIi4vY29tcG9uZW50cy9NYW1tYWxDYXJkXCI7XG5cbi8vIFx1NjU3MFx1NjM2RVx1N0M3Qlx1NTc4Qlx1NUI5QVx1NEU0OVxudHlwZSBSb3cgPSB7XG4gIGlkOiBudW1iZXI7XG4gIHRpdGxlOiBzdHJpbmc7XG4gIHVybDogc3RyaW5nO1xufTtcblxudHlwZSBFdmVudHMgPSB7XG4gIG5ld1JvdzogUm93O1xufTtcblxuLy8gXHU2M0QyXHU0RUY2XHU5MDNCXHU4RjkxXG5leHBvcnQgZnVuY3Rpb24gcGx1Z2luKGNsaWVudDogUGx1Z2luQ2xpZW50PEV2ZW50cywge30+KSB7XG4gIC8vIFx1NzJCNlx1NjAwMVx1N0JBMVx1NzQwNlxuICBjb25zdCByb3dzID0gY3JlYXRlU3RhdGU8UmVjb3JkPHN0cmluZywgUm93Pj4oe30sIHsgcGVyc2lzdDogXCJyb3dzXCIgfSk7XG4gIGNvbnN0IHNlbGVjdGVkSUQgPSBjcmVhdGVTdGF0ZTxzdHJpbmcgfCBudWxsPihudWxsLCB7IHBlcnNpc3Q6IFwic2VsZWN0aW9uXCIgfSk7XG5cbiAgLy8gXHU0RThCXHU0RUY2XHU1OTA0XHU3NDA2XG4gIGNsaWVudC5vbk1lc3NhZ2UoXCJuZXdSb3dcIiwgKHJvdykgPT4ge1xuICAgIHJvd3MudXBkYXRlKChkcmFmdCkgPT4ge1xuICAgICAgZHJhZnRbcm93LmlkXSA9IHJvdztcbiAgICB9KTtcbiAgfSk7XG5cbiAgLy8gXHU5MDA5XHU2MkU5XHU1MjlGXHU4MEZEXG4gIGZ1bmN0aW9uIHNldFNlbGVjdGlvbihpZDogbnVtYmVyKSB7XG4gICAgc2VsZWN0ZWRJRC5zZXQoXCJcIiArIGlkKTtcbiAgfVxuXG4gIC8vIFx1NjZCNFx1OTczMkFQSVxuICByZXR1cm4ge1xuICAgIHJvd3MsXG4gICAgc2VsZWN0ZWRJRCxcbiAgICBzZXRTZWxlY3Rpb24sXG4gIH07XG59XG5cbi8vIFx1NEZBN1x1OEZCOVx1NjgwRlx1NkUzMlx1NjdEM1x1NTFGRFx1NjU3MFxuZnVuY3Rpb24gcmVuZGVyU2lkZWJhcihyb3c6IFJvdykge1xuICByZXR1cm4gKFxuICAgIDxkaXYgc3R5bGU9e3sgcGFkZGluZzogXCIxNnB4XCIgfX0+XG4gICAgICA8VHlwb2dyYXBoeS5UaXRsZSBsZXZlbD17NH0+RXh0cmFzPC9UeXBvZ3JhcGh5LlRpdGxlPlxuICAgICAgPHByZVxuICAgICAgICBzdHlsZT17e1xuICAgICAgICAgIGJhY2tncm91bmQ6IHRoZW1lLmJhY2tncm91bmRXYXNoLFxuICAgICAgICAgIHBhZGRpbmc6IFwiOHB4XCIsXG4gICAgICAgICAgYm9yZGVyUmFkaXVzOiBcIjRweFwiLFxuICAgICAgICB9fVxuICAgICAgPlxuICAgICAgICB7SlNPTi5zdHJpbmdpZnkocm93LCBudWxsLCAyKX1cbiAgICAgIDwvcHJlPlxuICAgIDwvZGl2PlxuICApO1xufVxuXG4vLyBcdTRFM0JVSVx1N0VDNFx1NEVGNlxuZXhwb3J0IGZ1bmN0aW9uIENvbXBvbmVudCgpIHtcbiAgY29uc3QgaW5zdGFuY2UgPSB1c2VQbHVnaW4ocGx1Z2luKTtcbiAgY29uc3Qgcm93cyA9IHVzZVZhbHVlKGluc3RhbmNlLnJvd3MpO1xuICBjb25zdCBzZWxlY3RlZElEID0gdXNlVmFsdWUoaW5zdGFuY2Uuc2VsZWN0ZWRJRCk7XG5cbiAgcmV0dXJuIChcbiAgICA8ZGl2IHN0eWxlPXt7IGRpc3BsYXk6IFwiZmxleFwiLCBoZWlnaHQ6IFwiMTAwJVwiIH19PlxuICAgICAgPGRpdlxuICAgICAgICBzdHlsZT17e1xuICAgICAgICAgIGZsZXg6IDEsXG4gICAgICAgICAgb3ZlcmZsb3c6IFwiYXV0b1wiLFxuICAgICAgICAgIGJhY2tncm91bmQ6IHRoZW1lLmJhY2tncm91bmRXYXNoLFxuICAgICAgICAgIHBhZGRpbmc6IFwiMTZweFwiLFxuICAgICAgICB9fVxuICAgICAgPlxuICAgICAgICA8ZGl2IHN0eWxlPXt7IGRpc3BsYXk6IFwiZmxleFwiLCBmbGV4V3JhcDogXCJ3cmFwXCIsIGdhcDogXCI4cHhcIiB9fT5cbiAgICAgICAgICB7T2JqZWN0LmVudHJpZXMocm93cykubWFwKChbaWQsIHJvd10pID0+IChcbiAgICAgICAgICAgIDxNYW1tYWxDYXJkXG4gICAgICAgICAgICAgIHJvdz17cm93fVxuICAgICAgICAgICAgICBvblNlbGVjdD17aW5zdGFuY2Uuc2V0U2VsZWN0aW9ufVxuICAgICAgICAgICAgICBzZWxlY3RlZD17aWQgPT09IHNlbGVjdGVkSUR9XG4gICAgICAgICAgICAgIGtleT17aWR9XG4gICAgICAgICAgICAvPlxuICAgICAgICAgICkpfVxuICAgICAgICA8L2Rpdj5cbiAgICAgIDwvZGl2PlxuICAgICAge3NlbGVjdGVkSUQgJiYgKFxuICAgICAgICA8ZGl2XG4gICAgICAgICAgc3R5bGU9e3tcbiAgICAgICAgICAgIHdpZHRoOiBcIjMwMHB4XCIsXG4gICAgICAgICAgICBib3JkZXJMZWZ0OiBgMXB4IHNvbGlkICR7dGhlbWUuYm9yZGVyQ29sb3J9YCxcbiAgICAgICAgICB9fVxuICAgICAgICA+XG4gICAgICAgICAge3JlbmRlclNpZGViYXIocm93c1tzZWxlY3RlZElEXSl9XG4gICAgICAgIDwvZGl2PlxuICAgICAgKX1cbiAgICA8L2Rpdj5cbiAgKTtcbn1cbiIsICJpbXBvcnQgUmVhY3QsIHsgbWVtbyB9IGZyb20gXCJyZWFjdFwiO1xuaW1wb3J0IHsgdGhlbWUgfSBmcm9tIFwiZmxpcHBlci1wbHVnaW5cIjtcblxudHlwZSBSb3cgPSB7XG4gIGlkOiBudW1iZXI7XG4gIHRpdGxlOiBzdHJpbmc7XG4gIHVybDogc3RyaW5nO1xufTtcblxuaW50ZXJmYWNlIE1hbW1hbENhcmRQcm9wcyB7XG4gIHJvdzogUm93O1xuICBvblNlbGVjdDogKGlkOiBudW1iZXIpID0+IHZvaWQ7XG4gIHNlbGVjdGVkOiBib29sZWFuO1xufVxuXG5leHBvcnQgY29uc3QgTWFtbWFsQ2FyZCA9IG1lbW88TWFtbWFsQ2FyZFByb3BzPihcbiAgKHsgcm93LCBvblNlbGVjdCwgc2VsZWN0ZWQgfSkgPT4ge1xuICAgIGNvbnN0IGhhbmRsZUNsaWNrID0gKCkgPT4ge1xuICAgICAgb25TZWxlY3Qocm93LmlkKTtcbiAgICB9O1xuXG4gICAgY29uc3QgY2FyZFN0eWxlOiBSZWFjdC5DU1NQcm9wZXJ0aWVzID0ge1xuICAgICAgd2lkdGg6IFwiMjAwcHhcIixcbiAgICAgIG1hcmdpbjogXCI4cHhcIixcbiAgICAgIGN1cnNvcjogXCJwb2ludGVyXCIsXG4gICAgICBib3JkZXI6IGAycHggc29saWQgJHtzZWxlY3RlZCA/IHRoZW1lLnByaW1hcnlDb2xvciA6IFwidHJhbnNwYXJlbnRcIn1gLFxuICAgICAgYm9yZGVyUmFkaXVzOiBcIjhweFwiLFxuICAgICAgYmFja2dyb3VuZDogXCJ3aGl0ZVwiLFxuICAgICAgdHJhbnNpdGlvbjogXCJhbGwgMC4ycyBlYXNlXCIsXG4gICAgfTtcblxuICAgIGNvbnN0IGltYWdlU3R5bGU6IFJlYWN0LkNTU1Byb3BlcnRpZXMgPSB7XG4gICAgICBoZWlnaHQ6IFwiMTIwcHhcIixcbiAgICAgIGJhY2tncm91bmRJbWFnZTogYHVybCgke3Jvdy51cmx9KWAsXG4gICAgICBiYWNrZ3JvdW5kU2l6ZTogXCJjb3ZlclwiLFxuICAgICAgYmFja2dyb3VuZFBvc2l0aW9uOiBcImNlbnRlclwiLFxuICAgICAgYmFja2dyb3VuZFJlcGVhdDogXCJuby1yZXBlYXRcIixcbiAgICAgIGJvcmRlclJhZGl1czogXCI4cHggOHB4IDAgMFwiLFxuICAgIH07XG5cbiAgICBjb25zdCB0aXRsZVN0eWxlOiBSZWFjdC5DU1NQcm9wZXJ0aWVzID0ge1xuICAgICAgZm9udFdlaWdodDogNTAwLFxuICAgICAgY29sb3I6IHRoZW1lLnRleHRDb2xvclByaW1hcnksXG4gICAgICBkaXNwbGF5OiBcImJsb2NrXCIsXG4gICAgICBwYWRkaW5nOiBcIjEycHhcIixcbiAgICB9O1xuXG4gICAgcmV0dXJuIChcbiAgICAgIDxkaXZcbiAgICAgICAgc3R5bGU9e2NhcmRTdHlsZX1cbiAgICAgICAgb25DbGljaz17aGFuZGxlQ2xpY2t9XG4gICAgICAgIGRhdGEtdGVzdGlkPXtyb3cudGl0bGV9XG4gICAgICAgIG9uTW91c2VFbnRlcj17KGUpID0+IHtcbiAgICAgICAgICBlLmN1cnJlbnRUYXJnZXQuc3R5bGUudHJhbnNmb3JtID0gXCJ0cmFuc2xhdGVZKC0ycHgpXCI7XG4gICAgICAgICAgZS5jdXJyZW50VGFyZ2V0LnN0eWxlLmJveFNoYWRvdyA9IFwiMCA0cHggMTJweCByZ2JhKDAsIDAsIDAsIDAuMTUpXCI7XG4gICAgICAgIH19XG4gICAgICAgIG9uTW91c2VMZWF2ZT17KGUpID0+IHtcbiAgICAgICAgICBlLmN1cnJlbnRUYXJnZXQuc3R5bGUudHJhbnNmb3JtID0gXCJ0cmFuc2xhdGVZKDApXCI7XG4gICAgICAgICAgZS5jdXJyZW50VGFyZ2V0LnN0eWxlLmJveFNoYWRvdyA9IFwibm9uZVwiO1xuICAgICAgICB9fVxuICAgICAgPlxuICAgICAgICA8ZGl2IHN0eWxlPXtpbWFnZVN0eWxlfSAvPlxuICAgICAgICA8c3BhbiBzdHlsZT17dGl0bGVTdHlsZX0+e3Jvdy50aXRsZX08L3NwYW4+XG4gICAgICA8L2Rpdj5cbiAgICApO1xuICB9XG4pO1xuXG5NYW1tYWxDYXJkLmRpc3BsYXlOYW1lID0gXCJNYW1tYWxDYXJkXCI7XG4iXSwKICAibWFwcGluZ3MiOiAiOzs7Ozs7Ozs7Ozs7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Y0FBQyxTQUFJLE9BQU8sRUFBRSxTQUFTLFFBQVEsUUFBUSxPQUFPLEtBQzVDLDhCQUFBQSxRQUFBO0FBQUEsSUFBQztBQUFBO0FBQUEsTUFDQyxPQUFPO0FBQUEsUUFDTCxNQUFNO0FBQUEsUUFDTixVQUFVO0FBQUEsUUFDVixZQUFZLDZCQUFNO0FBQUEsUUFDbEIsU0FBUztBQUFBLE1BQ1g7QUFBQTtBQUFBLElBRUEsOEJBQUFBLFFBQUEsY0FBQyxTQUFJLE9BQU8sRUFBRSxTQUFTLFFBQVEsVUFBVSxRQUFRLEtBQUssTUFBTSxLQUN6RCxPQUFPLFFBQVEsSUFBSSxFQUFFLElBQUksQ0FBQyxDQUFDLElBQUksR0FBRyxNQUNqQyw4QkFBQUEsUUFBQTtBQUFBLE1BQUM7QUFBQTtBQUFBLFFBQ0M7QUFBQSxRQUNBLFVBQVUsU0FBUztBQUFBLFFBQ25CLFVBQVUsT0FBTztBQUFBLFFBQ2pCLEtBQUs7QUFBQTtBQUFBLElBQ1AsQ0FDRCxDQUNIO0FBQUEsRUFDRixHQUNDLGNBQ0MsOEJBQUFBLFFBQUE7QUFBQSxJQUFDO0FBQUE7QUFBQSxNQUNDLE9BQU87QUFBQSxRQUNMLE9BQU87QUFBQSxRQUNQLFlBQVksYUFBYSw2QkFBTTtBQUFBLE1BQ2pDO0FBQUE7QUFBQSxJQUVDLGNBQWMsS0FBSyxXQUFXO0FBQUEsRUFDakMsQ0FFSjtBQUVKOyIsCiAgIm5hbWVzIjogWyJpbXBvcnRfcmVhY3QiLCAiaW1wb3J0X2ZsaXBwZXJfcGx1Z2luIiwgIlJlYWN0IiwgIlJlYWN0Il0KfQo=
|
package/jest-setup.ts
ADDED
package/jest.config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: "ts-jest",
|
|
3
|
+
testEnvironment: "jsdom",
|
|
4
|
+
|
|
5
|
+
// 核心修复配置
|
|
6
|
+
moduleNameMapper: {
|
|
7
|
+
// 修复 moment 路径问题
|
|
8
|
+
"antd/node_modules/moment": "<rootDir>/node_modules/moment",
|
|
9
|
+
"^antd/node_modules/moment$": "<rootDir>/node_modules/moment",
|
|
10
|
+
|
|
11
|
+
// 确保 flipper-plugin 正确映射
|
|
12
|
+
"^flipper-plugin$": "<rootDir>/node_modules/flipper-plugin",
|
|
13
|
+
|
|
14
|
+
// 处理样式和静态文件
|
|
15
|
+
"\\.(css|less|scss)$": "identity-obj-proxy",
|
|
16
|
+
"\\.(jpg|jpeg|png|gif|svg)$": "<rootDir>/__mocks__/fileMock.js",
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// 必须包含的转换规则
|
|
20
|
+
transformIgnorePatterns: [
|
|
21
|
+
"/node_modules/(?!(antd|flipper|moment|@ant-design))",
|
|
22
|
+
],
|
|
23
|
+
|
|
24
|
+
// 测试环境配置
|
|
25
|
+
setupFilesAfterEnv: ["<rootDir>/jest-setup.ts"],
|
|
26
|
+
globals: {
|
|
27
|
+
__DEV__: true,
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// 模块解析配置
|
|
31
|
+
moduleDirectories: ["node_modules", "src"],
|
|
32
|
+
|
|
33
|
+
// 测试文件匹配
|
|
34
|
+
testMatch: [
|
|
35
|
+
"<rootDir>/src/**/__tests__/**/*.{ts,tsx}",
|
|
36
|
+
"<rootDir>/src/**/*.{test,spec}.{ts,tsx}",
|
|
37
|
+
],
|
|
38
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://fbflipper.com/schemas/plugin-package/v2.json",
|
|
3
|
+
"name": "flipper-plugin-sea-mammals",
|
|
4
|
+
"id": "sea-mammals",
|
|
5
|
+
"version": "1.0.1",
|
|
6
|
+
"pluginType": "client",
|
|
7
|
+
"main": "dist/bundle.js",
|
|
8
|
+
"flipperBundlerEntry": "src/index.tsx",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"flipper-plugin"
|
|
12
|
+
],
|
|
13
|
+
"icon": "apps",
|
|
14
|
+
"title": "Sea Mammals",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"lint": "flipper-pkg lint",
|
|
17
|
+
"prepack": "flipper-pkg lint && flipper-pkg bundle",
|
|
18
|
+
"build": "flipper-pkg bundle",
|
|
19
|
+
"watch": "flipper-pkg bundle --watch",
|
|
20
|
+
"test": "jest",
|
|
21
|
+
"test:watch": "jest --watch",
|
|
22
|
+
"test:coverage": "jest --coverage"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@emotion/styled": "latest",
|
|
26
|
+
"antd": "latest",
|
|
27
|
+
"flipper-plugin": "^0.273.0",
|
|
28
|
+
"react": "latest",
|
|
29
|
+
"react-dom": "latest"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@babel/preset-react": "latest",
|
|
33
|
+
"@babel/preset-typescript": "latest",
|
|
34
|
+
"@emotion/styled": "latest",
|
|
35
|
+
"@testing-library/react": "^16.3.0",
|
|
36
|
+
"@types/jest": "^30.0.0",
|
|
37
|
+
"@types/react": "latest",
|
|
38
|
+
"@types/react-dom": "latest",
|
|
39
|
+
"antd": "latest",
|
|
40
|
+
"create-jest": "^30.0.5",
|
|
41
|
+
"flipper-pkg": "latest",
|
|
42
|
+
"flipper-plugin": "latest",
|
|
43
|
+
"jest": "^30.0.5",
|
|
44
|
+
"jest-environment-jsdom": "^30.0.5",
|
|
45
|
+
"jest-mock-console": "latest",
|
|
46
|
+
"react": "latest",
|
|
47
|
+
"react-dom": "latest",
|
|
48
|
+
"ts-jest": "^29.4.1",
|
|
49
|
+
"typescript": "latest"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { TestUtils } from "flipper-plugin";
|
|
2
|
+
import * as MammalsPlugin from "..";
|
|
3
|
+
|
|
4
|
+
// 数据存储测试
|
|
5
|
+
test("It can store rows", () => {
|
|
6
|
+
const { instance, sendEvent } = TestUtils.startPlugin(MammalsPlugin);
|
|
7
|
+
|
|
8
|
+
expect(instance.rows.get()).toEqual({});
|
|
9
|
+
expect(instance.selectedID.get()).toBeNull();
|
|
10
|
+
|
|
11
|
+
sendEvent("newRow", {
|
|
12
|
+
id: 1,
|
|
13
|
+
title: "Dolphin",
|
|
14
|
+
url: "http://dolphin.png",
|
|
15
|
+
});
|
|
16
|
+
sendEvent("newRow", {
|
|
17
|
+
id: 2,
|
|
18
|
+
title: "Turtle",
|
|
19
|
+
url: "http://turtle.png",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(instance.rows.get()).toMatchInlineSnapshot(`
|
|
23
|
+
{
|
|
24
|
+
"1": {
|
|
25
|
+
"id": 1,
|
|
26
|
+
"title": "Dolphin",
|
|
27
|
+
"url": "http://dolphin.png",
|
|
28
|
+
},
|
|
29
|
+
"2": {
|
|
30
|
+
"id": 2,
|
|
31
|
+
"title": "Turtle",
|
|
32
|
+
"url": "http://turtle.png",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// UI交互测试
|
|
39
|
+
test("It can have selection and render details", async () => {
|
|
40
|
+
const { instance, renderer, act, sendEvent, exportState } =
|
|
41
|
+
TestUtils.renderPlugin(MammalsPlugin);
|
|
42
|
+
|
|
43
|
+
sendEvent("newRow", {
|
|
44
|
+
id: 1,
|
|
45
|
+
title: "Dolphin",
|
|
46
|
+
url: "http://dolphin.png",
|
|
47
|
+
});
|
|
48
|
+
sendEvent("newRow", {
|
|
49
|
+
id: 2,
|
|
50
|
+
title: "Turtle",
|
|
51
|
+
url: "http://turtle.png",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Dolphin卡片应该可见
|
|
55
|
+
expect(await renderer.findByTestId("Dolphin")).not.toBeNull();
|
|
56
|
+
|
|
57
|
+
// 验证Turtle卡片结构
|
|
58
|
+
expect(await renderer.findByTestId("Turtle")).toMatchInlineSnapshot(`
|
|
59
|
+
<div
|
|
60
|
+
data-testid="Turtle"
|
|
61
|
+
style="width: 200px; margin: 8px; cursor: pointer; border: 2px solid transparent; border-radius: 8px; background: white; transition: all 0.2s ease;"
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
style="height: 120px; background-image: url("http://turtle.png"); background-size: cover; background-position: center; background-repeat: no-repeat; border-radius: 8px 8px 0 0;"
|
|
65
|
+
/>
|
|
66
|
+
<span
|
|
67
|
+
style="font-weight: 500; color: var(--flipper-text-color-primary); display: block; padding: 12px;"
|
|
68
|
+
>
|
|
69
|
+
Turtle
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
`);
|
|
73
|
+
|
|
74
|
+
// 没有选择时不应该有侧边栏
|
|
75
|
+
expect(renderer.queryAllByText("Extras").length).toBe(0);
|
|
76
|
+
|
|
77
|
+
act(() => {
|
|
78
|
+
instance.setSelection(2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 现在侧边栏应该可见
|
|
82
|
+
expect(await renderer.findByText("Extras")).not.toBeNull();
|
|
83
|
+
|
|
84
|
+
// 验证导出状态
|
|
85
|
+
expect(exportState()).toEqual({
|
|
86
|
+
rows: {
|
|
87
|
+
"1": {
|
|
88
|
+
id: 1,
|
|
89
|
+
title: "Dolphin",
|
|
90
|
+
url: "http://dolphin.png",
|
|
91
|
+
},
|
|
92
|
+
"2": {
|
|
93
|
+
id: 2,
|
|
94
|
+
title: "Turtle",
|
|
95
|
+
url: "http://turtle.png",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
selection: "2",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// 选择功能测试
|
|
103
|
+
test("Selection state is properly managed", () => {
|
|
104
|
+
const { instance, sendEvent } = TestUtils.startPlugin(MammalsPlugin);
|
|
105
|
+
|
|
106
|
+
expect(instance.selectedID.get()).toBeNull();
|
|
107
|
+
|
|
108
|
+
instance.setSelection(1);
|
|
109
|
+
expect(instance.selectedID.get()).toBe("1");
|
|
110
|
+
|
|
111
|
+
instance.setSelection(2);
|
|
112
|
+
expect(instance.selectedID.get()).toBe("2");
|
|
113
|
+
|
|
114
|
+
instance.setSelection(1);
|
|
115
|
+
expect(instance.selectedID.get()).toBe("1");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// 数据更新测试
|
|
119
|
+
test("Data updates are properly handled", () => {
|
|
120
|
+
const { instance, sendEvent } = TestUtils.startPlugin(MammalsPlugin);
|
|
121
|
+
|
|
122
|
+
expect(instance.rows.get()).toEqual({});
|
|
123
|
+
|
|
124
|
+
sendEvent("newRow", {
|
|
125
|
+
id: 1,
|
|
126
|
+
title: "Dolphin",
|
|
127
|
+
url: "http://dolphin.png",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(instance.rows.get()).toEqual({
|
|
131
|
+
"1": {
|
|
132
|
+
id: 1,
|
|
133
|
+
title: "Dolphin",
|
|
134
|
+
url: "http://dolphin.png",
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
sendEvent("newRow", {
|
|
139
|
+
id: 2,
|
|
140
|
+
title: "Turtle",
|
|
141
|
+
url: "http://turtle.png",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(instance.rows.get()).toEqual({
|
|
145
|
+
"1": {
|
|
146
|
+
id: 1,
|
|
147
|
+
title: "Dolphin",
|
|
148
|
+
url: "http://dolphin.png",
|
|
149
|
+
},
|
|
150
|
+
"2": {
|
|
151
|
+
id: 2,
|
|
152
|
+
title: "Turtle",
|
|
153
|
+
url: "http://turtle.png",
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { memo } from "react";
|
|
2
|
+
import { theme } from "flipper-plugin";
|
|
3
|
+
|
|
4
|
+
type Row = {
|
|
5
|
+
id: number;
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
interface MammalCardProps {
|
|
11
|
+
row: Row;
|
|
12
|
+
onSelect: (id: number) => void;
|
|
13
|
+
selected: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const MammalCard = memo<MammalCardProps>(
|
|
17
|
+
({ row, onSelect, selected }) => {
|
|
18
|
+
const handleClick = () => {
|
|
19
|
+
onSelect(row.id);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const cardStyle: React.CSSProperties = {
|
|
23
|
+
width: "200px",
|
|
24
|
+
margin: "8px",
|
|
25
|
+
cursor: "pointer",
|
|
26
|
+
border: `2px solid ${selected ? theme.primaryColor : "transparent"}`,
|
|
27
|
+
borderRadius: "8px",
|
|
28
|
+
background: "white",
|
|
29
|
+
transition: "all 0.2s ease",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const imageStyle: React.CSSProperties = {
|
|
33
|
+
height: "120px",
|
|
34
|
+
backgroundImage: `url(${row.url})`,
|
|
35
|
+
backgroundSize: "cover",
|
|
36
|
+
backgroundPosition: "center",
|
|
37
|
+
backgroundRepeat: "no-repeat",
|
|
38
|
+
borderRadius: "8px 8px 0 0",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const titleStyle: React.CSSProperties = {
|
|
42
|
+
fontWeight: 500,
|
|
43
|
+
color: theme.textColorPrimary,
|
|
44
|
+
display: "block",
|
|
45
|
+
padding: "12px",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
style={cardStyle}
|
|
51
|
+
onClick={handleClick}
|
|
52
|
+
data-testid={row.title}
|
|
53
|
+
onMouseEnter={(e) => {
|
|
54
|
+
e.currentTarget.style.transform = "translateY(-2px)";
|
|
55
|
+
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.15)";
|
|
56
|
+
}}
|
|
57
|
+
onMouseLeave={(e) => {
|
|
58
|
+
e.currentTarget.style.transform = "translateY(0)";
|
|
59
|
+
e.currentTarget.style.boxShadow = "none";
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<div style={imageStyle} />
|
|
63
|
+
<span style={titleStyle}>{row.title}</span>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
MammalCard.displayName = "MammalCard";
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React, { memo } from "react";
|
|
2
|
+
import { Typography } from "antd";
|
|
3
|
+
import {
|
|
4
|
+
Layout,
|
|
5
|
+
PluginClient,
|
|
6
|
+
usePlugin,
|
|
7
|
+
createState,
|
|
8
|
+
useValue,
|
|
9
|
+
theme,
|
|
10
|
+
} from "flipper-plugin";
|
|
11
|
+
import { MammalCard } from "./components/MammalCard";
|
|
12
|
+
|
|
13
|
+
// 数据类型定义
|
|
14
|
+
type Row = {
|
|
15
|
+
id: number;
|
|
16
|
+
title: string;
|
|
17
|
+
url: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Events = {
|
|
21
|
+
newRow: Row;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// 插件逻辑
|
|
25
|
+
export function plugin(client: PluginClient<Events, {}>) {
|
|
26
|
+
// 状态管理
|
|
27
|
+
const rows = createState<Record<string, Row>>({}, { persist: "rows" });
|
|
28
|
+
const selectedID = createState<string | null>(null, { persist: "selection" });
|
|
29
|
+
|
|
30
|
+
// 事件处理
|
|
31
|
+
client.onMessage("newRow", (row) => {
|
|
32
|
+
rows.update((draft) => {
|
|
33
|
+
draft[row.id] = row;
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 选择功能
|
|
38
|
+
function setSelection(id: number) {
|
|
39
|
+
selectedID.set("" + id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 暴露API
|
|
43
|
+
return {
|
|
44
|
+
rows,
|
|
45
|
+
selectedID,
|
|
46
|
+
setSelection,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 侧边栏渲染函数
|
|
51
|
+
function renderSidebar(row: Row) {
|
|
52
|
+
return (
|
|
53
|
+
<div style={{ padding: "16px" }}>
|
|
54
|
+
<Typography.Title level={4}>Extras</Typography.Title>
|
|
55
|
+
<pre
|
|
56
|
+
style={{
|
|
57
|
+
background: theme.backgroundWash,
|
|
58
|
+
padding: "8px",
|
|
59
|
+
borderRadius: "4px",
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{JSON.stringify(row, null, 2)}
|
|
63
|
+
</pre>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 主UI组件
|
|
69
|
+
export function Component() {
|
|
70
|
+
const instance = usePlugin(plugin);
|
|
71
|
+
const rows = useValue(instance.rows);
|
|
72
|
+
const selectedID = useValue(instance.selectedID);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div style={{ display: "flex", height: "100%" }}>
|
|
76
|
+
<div
|
|
77
|
+
style={{
|
|
78
|
+
flex: 1,
|
|
79
|
+
overflow: "auto",
|
|
80
|
+
background: theme.backgroundWash,
|
|
81
|
+
padding: "16px",
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
|
|
85
|
+
{Object.entries(rows).map(([id, row]) => (
|
|
86
|
+
<MammalCard
|
|
87
|
+
row={row}
|
|
88
|
+
onSelect={instance.setSelection}
|
|
89
|
+
selected={id === selectedID}
|
|
90
|
+
key={id}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
{selectedID && (
|
|
96
|
+
<div
|
|
97
|
+
style={{
|
|
98
|
+
width: "300px",
|
|
99
|
+
borderLeft: `1px solid ${theme.borderColor}`,
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{renderSidebar(rows[selectedID])}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
package/todo1
ADDED
|
File without changes
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"module": "ES6",
|
|
5
|
+
"jsx": "react",
|
|
6
|
+
"sourceMap": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"moduleResolution": "node",
|
|
10
|
+
"types": ["jest"],
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true
|
|
13
|
+
},
|
|
14
|
+
"files": ["src/index.tsx"]
|
|
15
|
+
}
|