Skip to content

Commit 83ffeaf

Browse files
CopilotTechQuery
andcommitted
Add FileUploader, RestForm, RestFormModal, and SearchableInput components
Co-authored-by: TechQuery <[email protected]>
1 parent 88965bc commit 83ffeaf

File tree

11 files changed

+1370
-0
lines changed

11 files changed

+1370
-0
lines changed

registry.json

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,95 @@
222222
"type": "registry:component"
223223
}
224224
]
225+
},
226+
{
227+
"name": "file-uploader",
228+
"type": "registry:component",
229+
"title": "File Uploader",
230+
"description": "A file uploader component with drag-and-drop support for managing multiple files using MobX.",
231+
"registryDependencies": ["file-picker"],
232+
"dependencies": [
233+
"mobx",
234+
"mobx-react",
235+
"mobx-react-helper",
236+
"mobx-restful"
237+
],
238+
"files": [
239+
{
240+
"path": "registry/new-york/blocks/file-uploader/file-uploader.tsx",
241+
"type": "registry:component"
242+
}
243+
]
244+
},
245+
{
246+
"name": "rest-form",
247+
"type": "registry:component",
248+
"title": "REST Form",
249+
"description": "A comprehensive form component for CRUD operations with MobX RESTful integration, supporting various field types and validation.",
250+
"registryDependencies": [
251+
"button",
252+
"label",
253+
"badge-input",
254+
"file-preview",
255+
"file-uploader",
256+
"form-field"
257+
],
258+
"dependencies": [
259+
"mobx",
260+
"mobx-i18n",
261+
"mobx-react",
262+
"mobx-react-helper",
263+
"mobx-restful",
264+
"web-utility"
265+
],
266+
"files": [
267+
{
268+
"path": "registry/new-york/blocks/rest-form/rest-form.tsx",
269+
"type": "registry:component"
270+
}
271+
]
272+
},
273+
{
274+
"name": "rest-form-modal",
275+
"type": "registry:component",
276+
"title": "REST Form Modal",
277+
"description": "A modal wrapper for REST Form component, displaying forms in a dialog for editing data.",
278+
"registryDependencies": ["dialog", "rest-form"],
279+
"dependencies": ["mobx-react", "mobx-restful", "web-utility"],
280+
"files": [
281+
{
282+
"path": "registry/new-york/blocks/rest-form-modal/rest-form-modal.tsx",
283+
"type": "registry:component"
284+
}
285+
]
286+
},
287+
{
288+
"name": "searchable-input",
289+
"type": "registry:component",
290+
"title": "Searchable Input",
291+
"description": "A searchable input component with autocomplete, supporting multiple selections and inline creation of new items.",
292+
"registryDependencies": [
293+
"button",
294+
"input",
295+
"badge-bar",
296+
"badge-input",
297+
"rest-form-modal",
298+
"scroll-list"
299+
],
300+
"dependencies": [
301+
"lodash.debounce",
302+
"mobx",
303+
"mobx-react",
304+
"mobx-react-helper",
305+
"mobx-restful",
306+
"web-utility"
307+
],
308+
"files": [
309+
{
310+
"path": "registry/new-york/blocks/searchable-input/searchable-input.tsx",
311+
"type": "registry:component"
312+
}
313+
]
225314
}
226315
]
227316
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { makeObservable } from "mobx";
5+
6+
import { FileModel, FileUploader } from "./file-uploader";
7+
8+
class ExampleFileModel extends FileModel {
9+
constructor() {
10+
super();
11+
makeObservable(this);
12+
}
13+
}
14+
15+
export const FileUploaderExample = () => {
16+
const [store] = useState(() => new ExampleFileModel());
17+
18+
return (
19+
<div className="w-full space-y-8">
20+
<div>
21+
<h3 className="text-lg font-semibold mb-4">Single File Upload</h3>
22+
<FileUploader
23+
store={store}
24+
name="single-file"
25+
accept="image/*"
26+
defaultValue={[]}
27+
/>
28+
<p className="text-sm text-muted-foreground mt-2">
29+
Upload a single image file
30+
</p>
31+
</div>
32+
33+
<div>
34+
<h3 className="text-lg font-semibold mb-4">Multiple Files Upload</h3>
35+
<FileUploader
36+
store={store}
37+
name="multiple-files"
38+
accept="image/*"
39+
multiple
40+
defaultValue={[]}
41+
/>
42+
<p className="text-sm text-muted-foreground mt-2">
43+
Upload multiple image files (drag to reorder)
44+
</p>
45+
</div>
46+
47+
<div>
48+
<h3 className="text-lg font-semibold mb-4">Uploaded Files</h3>
49+
<pre className="p-4 bg-muted rounded-md text-xs overflow-auto">
50+
{JSON.stringify(store.files, null, 2)}
51+
</pre>
52+
</div>
53+
</div>
54+
);
55+
};
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
"use client";
2+
3+
import { observable } from "mobx";
4+
import { observer } from "mobx-react";
5+
import { FormComponent, FormComponentProps, reaction } from "mobx-react-helper";
6+
import { BaseModel } from "mobx-restful";
7+
import { DragEvent } from "react";
8+
9+
import { FilePicker } from "../file-picker/file-picker";
10+
11+
export abstract class FileModel extends BaseModel {
12+
@observable
13+
accessor files: string[] = [];
14+
15+
clear() {
16+
super.clear();
17+
18+
this.files = [];
19+
}
20+
21+
/**
22+
* Override this method for Network calling,
23+
* then call `super.upload(fileURL)` to update `this.files` array.
24+
*/
25+
async upload(file: string | Blob) {
26+
if (file instanceof Blob) file = URL.createObjectURL(file);
27+
28+
const { files } = this;
29+
30+
if (!files.includes(file)) this.files = [...files, file];
31+
32+
return file;
33+
}
34+
35+
/**
36+
* Override this method for Network calling,
37+
* then call `super.delete(fileURL)` to update `this.files` array.
38+
*/
39+
async delete(file: string) {
40+
const { files } = this;
41+
const index = files.indexOf(file);
42+
43+
this.files = [...files.slice(0, index), ...files.slice(index + 1)];
44+
}
45+
46+
move(sourceIndex: number, targetIndex: number) {
47+
const { files } = this;
48+
const sourceFile = files[sourceIndex],
49+
targetFile = files[targetIndex];
50+
const frontIndex = Math.min(sourceIndex, targetIndex),
51+
backIndex = Math.max(sourceIndex, targetIndex);
52+
53+
const front = files.slice(0, frontIndex),
54+
middle = files.slice(frontIndex + 1, backIndex),
55+
back = files.slice(backIndex + 1);
56+
57+
this.files =
58+
sourceIndex < targetIndex
59+
? [...front, ...middle, targetFile, sourceFile, ...back]
60+
: [...front, sourceFile, targetFile, ...middle, ...back];
61+
}
62+
}
63+
64+
export interface FileUploaderProps extends FormComponentProps<string[]> {
65+
store: FileModel;
66+
}
67+
68+
@observer
69+
export class FileUploader extends FormComponent<FileUploaderProps> {
70+
static readonly displayName = "FileUploader";
71+
72+
@observable
73+
accessor pickIndex: number | undefined;
74+
75+
componentDidMount() {
76+
super.componentDidMount();
77+
78+
const { store } = this.props;
79+
80+
store.files = this.value || [];
81+
}
82+
83+
@reaction(({ value }) => value)
84+
protected restoreFile(value: FileUploaderProps["value"]) {
85+
const { store } = this.props;
86+
87+
store.files = value;
88+
}
89+
90+
handleDrop = (index: number) => (event: DragEvent<HTMLElement>) => {
91+
event.preventDefault();
92+
93+
const { props, pickIndex } = this;
94+
95+
if (!(pickIndex != null)) return;
96+
97+
props.store.move(pickIndex, index);
98+
99+
this.innerValue = props.store.files;
100+
};
101+
102+
handleChange =
103+
(oldURI = "") =>
104+
async (file: File) => {
105+
const { store } = this.props;
106+
107+
if (oldURI) await store.delete(oldURI);
108+
if (file) await store.upload(file);
109+
110+
this.innerValue = store.files;
111+
};
112+
113+
render() {
114+
const {
115+
className = "",
116+
style,
117+
multiple,
118+
store,
119+
value: _,
120+
defaultValue,
121+
onChange,
122+
...props
123+
} = this.props;
124+
125+
const { value } = this;
126+
127+
return (
128+
<ol
129+
className={`flex flex-wrap gap-2 list-none m-0 ${className}`}
130+
style={style}
131+
onDragOver={(event) => event.preventDefault()}
132+
>
133+
{value?.map((file, index) => (
134+
<li
135+
key={file}
136+
className="inline-block"
137+
draggable
138+
onDragStart={() => (this.pickIndex = index)}
139+
onDrop={this.handleDrop(index)}
140+
>
141+
<FilePicker
142+
{...props}
143+
value={file}
144+
onChange={this.handleChange(file)}
145+
/>
146+
</li>
147+
))}
148+
{(multiple || !value?.[0]) && (
149+
<li className="inline-block">
150+
<FilePicker
151+
{...props}
152+
name={undefined}
153+
value=""
154+
required={!value?.[0] && props.required}
155+
onChange={this.handleChange()}
156+
/>
157+
</li>
158+
)}
159+
</ol>
160+
);
161+
}
162+
}

0 commit comments

Comments
 (0)