Skip to content

Commit 68aadbb

Browse files
committed
Close #30: Add support for selecting already uploaded files
1 parent 1bff6f4 commit 68aadbb

File tree

9 files changed

+346
-4
lines changed

9 files changed

+346
-4
lines changed

dev/vendor/react-modal.min.js

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/form.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ export class FormFileInput extends React.Component {
465465
<FormInput {...props} inputRef={this.inputRef} />
466466
</div>
467467
}
468-
</div>
468+
</div>
469469
</div>
470470
);
471471
}

src/components/icons.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ export default function Icon(props) {
88
case 'chevron-down':
99
icon = <ChevronDown />;
1010
break;
11+
case 'arrow-down':
12+
icon = <ArrowDown />;
13+
break;
14+
case 'x-lg':
15+
icon = <XLg />;
16+
break;
1117
}
1218

1319
return (
@@ -28,3 +34,15 @@ function ChevronDown(props) {
2834
<path fillRule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
2935
);
3036
}
37+
38+
function ArrowDown(props) {
39+
return (
40+
<path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/>
41+
);
42+
}
43+
44+
function XLg(props) {
45+
return (
46+
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
47+
);
48+
}

src/components/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {FormInput, FormCheckInput, FormRadioInput, FormSelectInput, FormFileInpu
44
import {FormRow, FormGroup, FormRowControls, GroupTitle} from './containers';
55
import Loader from './loaders';
66
import Icon from './icons';
7+
import FileUploader from './uploader';
78

89
export {
910
Button,
@@ -12,4 +13,5 @@ export {
1213
FormRow, FormGroup, FormRowControls, GroupTitle,
1314
Loader,
1415
Icon,
16+
FileUploader,
1517
};

src/components/loaders.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export default function Loader (props) {
2-
return <div className="rjf-loader"></div>;
2+
let className = 'rjf-loader';
3+
if (props.className)
4+
className = className + ' ' + props.className;
5+
6+
return <div className={className}></div>;
37
}

src/components/uploader.js

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import Button from './buttons';
2+
import Loader from './loaders';
3+
import {EditorContext, capitalize} from '../util';
4+
import {FormFileInput} from './form.js';
5+
import Icon from './icons';
6+
7+
export default class FileUploader extends React.Component {
8+
static contextType = EditorContext;
9+
10+
constructor(props) {
11+
super(props);
12+
13+
this.state = {
14+
value: props.value,
15+
//fileName: this.getFileName(),
16+
loading: false,
17+
open: false,
18+
pane: 'upload'
19+
};
20+
21+
this.inputRef = React.createRef();
22+
}
23+
24+
openModal = (e) => {
25+
this.setState({open: true});
26+
}
27+
28+
closeModal = (e) => {
29+
this.setState({open: false, pane: 'upload'});
30+
}
31+
32+
togglePane = (name) => {
33+
this.setState({pane: name});
34+
}
35+
36+
handleFileSelect = (value) => {
37+
// we create a fake event
38+
let event = {
39+
target: {
40+
type: 'text',
41+
value: value,
42+
name: this.props.name
43+
}
44+
};
45+
46+
this.props.onChange(event);
47+
48+
this.closeModal();
49+
}
50+
51+
handleFileUpload = (e) => {
52+
this.props.onChange(e);
53+
this.closeModal();
54+
}
55+
56+
clearFile = () => {
57+
if (window.confirm('Do you want to remove this file?')) {
58+
let event = {
59+
target: {
60+
type: 'text',
61+
value: '',
62+
name: this.props.name
63+
}
64+
};
65+
66+
this.props.onChange(event);
67+
}
68+
}
69+
70+
render() {
71+
if (!this.context.fileListEndpoint) {
72+
return <FormFileInput {...this.props} />;
73+
}
74+
75+
return (
76+
<div>
77+
{this.props.label && <label>{this.props.label}</label>}
78+
<div className="rjf-file-field">
79+
{this.props.value &&
80+
<div className="rjf-current-file-name">
81+
Current file: <span>{this.props.value}</span> {' '}
82+
<Button className="remove-file" onClick={this.clearFile}>Clear</Button>
83+
</div>
84+
}
85+
<Button onClick={this.openModal} className="upload-modal__open">
86+
{this.props.value ? 'Change file' : 'Select file'}
87+
</Button>
88+
</div>
89+
90+
<ReactModal
91+
isOpen={this.state.open}
92+
onRequestClose={this.closeModal}
93+
contentLabel="Select file"
94+
portalClassName="rjf-modal-portal"
95+
overlayClassName="rjf-modal__overlay"
96+
className="rjf-modal__dialog"
97+
bodyOpenClassName="rjf-modal__main-body--open"
98+
>
99+
<div className="rjf-modal__content">
100+
<div className="rjf-modal__header">
101+
<TabButton
102+
onClick={this.togglePane}
103+
tabName="upload"
104+
active={this.state.pane === "upload"}
105+
>
106+
Upload new
107+
</TabButton>{' '}
108+
<TabButton
109+
onClick={this.togglePane}
110+
tabName="library"
111+
active={this.state.pane === "library"}
112+
>
113+
Choose from library
114+
</TabButton>
115+
116+
<Button className="modal__close" onClick={this.closeModal} title="Close (Esc)">
117+
<Icon name="x-lg" />
118+
</Button>
119+
</div>
120+
<div className="rjf-modal__body">
121+
122+
{this.state.pane === 'upload' &&
123+
<UploadPane
124+
{...this.props}
125+
onChange={this.handleFileUpload}
126+
label=''
127+
value=''
128+
help_text=''
129+
/>
130+
}
131+
{this.state.pane === 'library' &&
132+
<LibraryPane
133+
fileListEndpoint={this.context.fileListEndpoint}
134+
onFileSelect={this.handleFileSelect}
135+
/>
136+
}
137+
138+
</div>
139+
<div className="rjf-modal__footer">
140+
<Button className="modal__footer-close" onClick={this.closeModal}>Cancel</Button>
141+
</div>
142+
</div>
143+
</ReactModal>
144+
</div>
145+
);
146+
}
147+
}
148+
149+
150+
function TabButton(props) {
151+
let className = 'rjf-upload-modal__tab-button';
152+
if (props.active)
153+
className += ' rjf-upload-modal__tab-button--active';
154+
155+
return (
156+
<button
157+
onClick={() => props.onClick(props.tabName)}
158+
className={className}
159+
>
160+
{props.children}
161+
</button>
162+
);
163+
}
164+
165+
166+
function UploadPane(props) {
167+
return (
168+
<div class="rjf-upload-modal__pane">
169+
<h3>Upload new</h3>
170+
<br/>
171+
<FormFileInput {...props} />
172+
</div>
173+
);
174+
}
175+
176+
177+
class LibraryPane extends React.Component {
178+
constructor(props) {
179+
super(props);
180+
181+
this.state = {
182+
loading: true,
183+
files: [],
184+
page: 0, // current page
185+
hasMore: true,
186+
};
187+
}
188+
189+
componentDidMount() {
190+
//setTimeout(() => this.setState({loading: false}), 1000);
191+
this.fetchList();
192+
}
193+
194+
fetchList = () => {
195+
let endpoint = this.props.fileListEndpoint;
196+
197+
if (!endpoint) {
198+
console.error(
199+
"Error: fileListEndpoint option need to be passed "
200+
+ "while initializing editor for enabling file listing.");
201+
this.setState({loading: false, hasMore: false});
202+
return;
203+
}
204+
205+
fetch(endpoint + '?page=' + (this.state.page + 1), {method: 'GET'})
206+
.then((response) => response.json())
207+
.then((result) => {
208+
this.setState((state) => ({
209+
loading: false,
210+
files: [...state.files, ...result.file_list],
211+
page: result.file_list.length > 0 ? state.page + 1 : state.page,
212+
hasMore: result.file_list.length > 0,
213+
})
214+
);
215+
})
216+
.catch((error) => {
217+
alert('Something went wrong while uploading file');
218+
console.error('Error:', error);
219+
this.setState({loading: false});
220+
});
221+
}
222+
223+
onLoadMore = (e) => {
224+
this.setState({loading: true}, this.fetchList);
225+
}
226+
227+
render() {
228+
return (
229+
<div className="rjf-upload-modal__pane">
230+
<h3>Media library</h3>
231+
232+
<div className="rjf-upload-modal__media-container">
233+
{this.state.files.map((i) => {
234+
return <MediaTile {...i} onClick={this.props.onFileSelect} />
235+
})}
236+
</div>
237+
238+
{this.state.loading && <Loader className="rjf-upload-modal__media-loader" />}
239+
240+
{!this.state.loading && this.state.hasMore &&
241+
<div>
242+
<Button onClick={this.onLoadMore} className="upload-modal__media-load">
243+
<Icon name="arrow-down" /> View more
244+
</Button>
245+
</div>
246+
}
247+
{!this.state.hasMore &&
248+
<div className="rjf-upload-modal__media-end-message">
249+
{this.state.files.length ? 'End of list' : 'No files found'}
250+
</div>
251+
}
252+
</div>
253+
);
254+
}
255+
}
256+
257+
258+
const DEFAULT_THUBNAIL = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='%23999999' class='bi bi-file-earmark' viewBox='0 0 16 16'%3E%3Cpath d='M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z'/%3E%3C/svg%3E";
259+
260+
261+
function MediaTile(props) {
262+
return (
263+
<div className="rjf-upload-modal__media-tile">
264+
<div className="rjf-upload-modal__media-tile-inner" tabIndex="0" onClick={() => props.onClick(props.value)}>
265+
<img src={props.thumbnail ? props.thumbnail : DEFAULT_THUBNAIL} />
266+
<div className="rjf-upload-modal__media-tile-info">
267+
<span>{props.name}</span>
268+
<span>{props.date_created}</span>
269+
<span>{props.size}</span>
270+
</div>
271+
</div>
272+
</div>
273+
);
274+
}

src/form.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export default class Form extends React.Component {
184184
<EditorContext.Provider
185185
value={{
186186
fileUploadEndpoint: this.props.fileUploadEndpoint,
187+
fileListEndpoint: this.props.fileListEndpoint,
187188
fieldName: this.props.fieldName,
188189
modelName: this.props.modelName,
189190
}}

src/renderer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default function JSONForm(config) {
77
this.schema = config.schema;
88
this.data = config.data;
99
this.fileUploadEndpoint = config.fileUploadEndpoint;
10+
this.fileListEndpoint = config.fileListEndpoint;
1011
this.fieldName = config.fieldName;
1112
this.modelName = config.modelName;
1213

@@ -17,6 +18,7 @@ export default function JSONForm(config) {
1718
dataInputId={this.dataInputId}
1819
data={this.data}
1920
fileUploadEndpoint={this.fileUploadEndpoint}
21+
fileListEndpoint={this.fileListEndpoint}
2022
fieldName={this.fieldName}
2123
modelName={this.modelName}
2224
/>,

src/ui.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {getBlankData} from './data';
22
import {Button, FormInput, FormCheckInput, FormRadioInput, FormSelectInput,
33
FormFileInput, FormRow, FormGroup, GroupTitle, FormRowControls, FormTextareaInput,
4-
FormDateTimeInput, FormMultiSelectInput} from './components';
4+
FormDateTimeInput, FormMultiSelectInput, FileUploader} from './components';
55
import {getVerboseName} from './util';
66

77

@@ -64,8 +64,10 @@ function FormField(props) {
6464
InputField = FormInput;
6565

6666
if (props.schema.format) {
67-
if (props.schema.format === 'data-url' || props.schema.format === 'file-url') {
67+
if (props.schema.format === 'data-url') {
6868
InputField = FormFileInput;
69+
} else if (props.schema.format === 'file-url') {
70+
InputField = FileUploader;
6971
} else if (props.schema.format === 'datetime') {
7072
InputField = FormDateTimeInput;
7173
}

0 commit comments

Comments
 (0)