pages_core 3.7.0 → 3.8.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -2
- data/Rakefile +37 -0
- data/app/assets/builds/pages_core/admin-dist.js +55 -0
- data/app/assets/stylesheets/pages/admin/components/image_editor.scss +1 -0
- data/app/assets/stylesheets/pages/admin/components/image_grid.scss +33 -5
- data/app/assets/stylesheets/pages/admin/components/layout.scss +2 -1
- data/app/assets/stylesheets/pages/admin/components/login.scss +6 -0
- data/app/assets/stylesheets/pages/admin/components/tabs.scss +5 -0
- data/app/assets/stylesheets/pages/admin/components/tag_editor.scss +13 -7
- data/app/assets/stylesheets/pages/admin/controllers/pages.scss +13 -5
- data/app/assets/stylesheets/pages/admin.scss +0 -1
- data/app/controller_dummies/admin/admin_controller.rb +1 -1
- data/app/controller_dummies/application_controller.rb +1 -1
- data/app/controller_dummies/attachments_controller.rb +1 -1
- data/app/controller_dummies/frontend_controller.rb +1 -1
- data/app/controller_dummies/images_controller.rb +1 -1
- data/app/controller_dummies/page_files_controller.rb +1 -1
- data/app/controller_dummies/pages_controller.rb +1 -1
- data/app/controller_dummies/sitemaps_controller.rb +1 -1
- data/app/controllers/admin/attachments_controller.rb +1 -1
- data/app/controllers/admin/images_controller.rb +11 -8
- data/app/controllers/concerns/pages_core/authentication.rb +9 -4
- data/app/controllers/concerns/pages_core/preview_pages_controller.rb +5 -0
- data/app/controllers/pages_core/frontend/pages_controller.rb +5 -2
- data/app/controllers/sessions_controller.rb +1 -1
- data/app/formatters/pages_core/link_renderer.rb +2 -2
- data/app/helpers/application_helper.rb +1 -1
- data/app/helpers/frontend_helper.rb +1 -1
- data/app/helpers/pages_core/admin/content_tabs_helper.rb +5 -2
- data/app/helpers/pages_core/admin/image_uploads_helper.rb +2 -3
- data/app/helpers/pages_core/admin/tag_editor_helper.rb +9 -39
- data/app/helpers/pages_core/head_tags_helper.rb +11 -20
- data/app/helpers/pages_core/open_graph_tags_helper.rb +1 -1
- data/app/javascript/admin-dist.js +2 -0
- data/app/javascript/components/Attachments/Attachment.jsx +121 -0
- data/app/javascript/components/Attachments/AttachmentEditor.jsx +116 -0
- data/app/javascript/components/Attachments/Placeholder.jsx +10 -0
- data/app/javascript/components/Attachments.jsx +165 -0
- data/app/{assets/javascripts/pages/admin/components/date_range_select.jsx → javascript/components/DateRangeSelect.jsx} +16 -5
- data/app/javascript/components/EditableImage.jsx +61 -0
- data/app/javascript/components/FileUploadButton.jsx +47 -0
- data/app/{assets/javascripts/pages/admin/components/focal_point.jsx → javascript/components/ImageCropper/FocalPoint.jsx} +12 -1
- data/app/javascript/components/ImageCropper/Image.jsx +65 -0
- data/app/javascript/components/ImageCropper/Toolbar.jsx +73 -0
- data/app/javascript/components/ImageCropper/useCrop.js +199 -0
- data/app/javascript/components/ImageCropper.jsx +90 -0
- data/app/javascript/components/ImageEditor/Form.jsx +98 -0
- data/app/javascript/components/ImageEditor.jsx +62 -0
- data/app/javascript/components/ImageGrid/DragElement.jsx +30 -0
- data/app/javascript/components/ImageGrid/FilePlaceholder.jsx +9 -0
- data/app/javascript/components/ImageGrid/GridImage.jsx +103 -0
- data/app/javascript/components/ImageGrid/Placeholder.jsx +23 -0
- data/app/javascript/components/ImageGrid.jsx +257 -0
- data/app/javascript/components/ImageUploader.jsx +171 -0
- data/app/{assets/javascripts/pages/admin/components/modal.jsx → javascript/components/Modal.jsx} +13 -2
- data/app/{assets/javascripts/pages/admin/components/page_dates.jsx → javascript/components/PageDates.jsx} +11 -1
- data/app/{assets/javascripts/pages/admin/components/page_files.jsx → javascript/components/PageFiles.jsx} +11 -2
- data/app/{assets/javascripts/pages/admin/components/page_images.jsx → javascript/components/PageImages.jsx} +11 -2
- data/app/{assets/javascripts/pages/admin/components/page_tree_store.jsx → javascript/components/PageTree.jsx} +127 -137
- data/app/{assets/javascripts/pages/admin/components/page_tree.jsx → javascript/components/PageTreeDraggable.jsx} +35 -29
- data/app/{assets/javascripts/pages/admin/components/page_tree_node.jsx → javascript/components/PageTreeNode.jsx} +35 -20
- data/app/javascript/components/RichTextArea.jsx +213 -0
- data/app/javascript/components/RichTextToolbarButton.jsx +20 -0
- data/app/javascript/components/TagEditor/AddTagForm.jsx +42 -0
- data/app/javascript/components/TagEditor/Tag.jsx +32 -0
- data/app/javascript/components/TagEditor.jsx +61 -0
- data/app/javascript/components/Toast.jsx +72 -0
- data/app/javascript/components/drag/draggedOrder.js +51 -0
- data/app/javascript/components/drag/useDragCollection.js +84 -0
- data/app/javascript/components/drag/useDragUploader.js +112 -0
- data/app/javascript/components/drag/useDraggable.js +17 -0
- data/app/javascript/components/drag.js +6 -0
- data/app/javascript/components.js +15 -0
- data/app/javascript/controllers/EditPageController.js +20 -0
- data/app/javascript/controllers/LoginController.js +29 -0
- data/app/javascript/controllers/MainController.js +65 -0
- data/app/javascript/controllers/PageOptionsController.js +62 -0
- data/app/javascript/features/RichText.jsx +34 -0
- data/app/javascript/hooks.js +2 -0
- data/app/javascript/index.js +38 -0
- data/app/{assets/javascripts/pages/admin/lib/tree.jsx → javascript/lib/Tree.js} +55 -54
- data/app/javascript/lib/copyToClipboard.js +13 -0
- data/app/javascript/lib/readyHandler.js +22 -0
- data/app/javascript/lib/request.js +36 -0
- data/app/javascript/stores/ModalStore.jsx +12 -0
- data/app/javascript/stores/ToastStore.jsx +14 -0
- data/app/javascript/stores.js +2 -0
- data/app/models/concerns/pages_core/page_model/images.rb +3 -1
- data/app/models/concerns/pages_core/page_model/searchable.rb +19 -0
- data/app/models/concerns/pages_core/searchable_document.rb +71 -0
- data/app/models/concerns/pages_core/taggable.rb +27 -12
- data/app/models/page.rb +2 -0
- data/app/models/page_exporter.rb +2 -2
- data/app/models/page_image.rb +0 -2
- data/app/models/role.rb +1 -1
- data/app/models/search_document.rb +72 -0
- data/app/models/tag.rb +1 -0
- data/app/models/user.rb +1 -1
- data/app/{serializers/admin/attachment_serializer.rb → resources/admin/attachment_resource.rb} +6 -5
- data/app/{serializers/admin/image_serializer.rb → resources/admin/image_resource.rb} +9 -9
- data/app/resources/admin/page_file_resource.rb +10 -0
- data/app/{serializers/admin/page_image_serializer.rb → resources/admin/page_image_resource.rb} +4 -2
- data/app/resources/export/attachment_resource.rb +10 -0
- data/app/resources/export/page_image_resource.rb +45 -0
- data/app/resources/export/page_resource.rb +42 -0
- data/app/{serializers/page_image_serializer.rb → resources/page_image_resource.rb} +8 -16
- data/app/resources/page_resource.rb +33 -0
- data/app/services/pages_core/destroy_invite_service.rb +2 -2
- data/app/services/pages_core/invite_service.rb +2 -2
- data/app/views/admin/pages/_edit_content.html.erb +1 -1
- data/app/views/admin/pages/_edit_files.html.erb +1 -5
- data/app/views/admin/pages/_edit_images.html.erb +1 -5
- data/app/views/admin/pages/_edit_options.html.erb +74 -55
- data/app/views/admin/pages/_form.html.erb +19 -0
- data/app/views/admin/pages/edit.html.erb +35 -61
- data/app/views/admin/pages/index.html.erb +0 -1
- data/app/views/admin/pages/new.html.erb +32 -32
- data/app/views/admin/users/_access_control.html.erb +5 -1
- data/app/views/admin/users/login.html.erb +12 -4
- data/app/views/feeds/pages.rss.builder +1 -2
- data/app/views/layouts/admin/_header.html.erb +1 -1
- data/app/views/layouts/admin/_page_header.html.erb +33 -0
- data/app/views/layouts/admin.html.erb +23 -42
- data/app/views/pages_core/_google_analytics.html.erb +8 -0
- data/db/migrate/20180625154059_enable_search_extensions.rb +10 -0
- data/db/migrate/20210209151400_create_search_configurations.rb +35 -0
- data/db/migrate/20210210235200_create_search_documents.rb +74 -0
- data/lib/pages_core/engine.rb +1 -5
- data/lib/pages_core/templates/block_configuration.rb +1 -1
- data/lib/pages_core/templates/configuration_handler.rb +1 -1
- data/lib/pages_core/version.rb +3 -1
- data/lib/pages_core.rb +3 -5
- data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +0 -7
- data/lib/rails/generators/pages_core/install/templates/page_templates_initializer.rb +2 -2
- metadata +116 -115
- data/app/assets/javascripts/pages/admin/components/attachment.jsx +0 -130
- data/app/assets/javascripts/pages/admin/components/attachment_editor.jsx +0 -131
- data/app/assets/javascripts/pages/admin/components/attachments.jsx +0 -211
- data/app/assets/javascripts/pages/admin/components/drag_uploader.jsx +0 -174
- data/app/assets/javascripts/pages/admin/components/editable_image.jsx +0 -57
- data/app/assets/javascripts/pages/admin/components/file_upload_button.jsx +0 -44
- data/app/assets/javascripts/pages/admin/components/grid_image.jsx +0 -124
- data/app/assets/javascripts/pages/admin/components/image_editor.jsx +0 -496
- data/app/assets/javascripts/pages/admin/components/image_grid.jsx +0 -306
- data/app/assets/javascripts/pages/admin/components/image_uploader.jsx +0 -176
- data/app/assets/javascripts/pages/admin/components/modal_store.jsx +0 -20
- data/app/assets/javascripts/pages/admin/components/rich_text_area.jsx +0 -64
- data/app/assets/javascripts/pages/admin/components/rich_text_toolbar.jsx +0 -91
- data/app/assets/javascripts/pages/admin/components/toast.jsx +0 -34
- data/app/assets/javascripts/pages/admin/components/toast_store.jsx +0 -52
- data/app/assets/javascripts/pages/admin/components.jsx +0 -2
- data/app/assets/javascripts/pages/admin/features/content_tabs.jsx +0 -72
- data/app/assets/javascripts/pages/admin/features/edit_page.jsx +0 -97
- data/app/assets/javascripts/pages/admin/features/rich_text.jsx +0 -14
- data/app/assets/javascripts/pages/admin/features/tag_editor.jsx +0 -160
- data/app/assets/javascripts/pages/admin.jsx +0 -17
- data/app/assets/javascripts/pages/login_form.jsx +0 -21
- data/app/serializers/admin/page_file_serializer.rb +0 -8
- data/app/serializers/page_export_serializer.rb +0 -32
- data/app/serializers/page_file_export_serializer.rb +0 -6
- data/app/serializers/page_image_export_serializer.rb +0 -42
- data/app/serializers/page_serializer.rb +0 -23
- data/app/views/layouts/admin/_analytics.html.erb +0 -16
- data/lib/rails/generators/pages_core/frontend/templates/application.js.erb +0 -15
- data/vendor/assets/javascripts/ReactCrop.min.js +0 -1
- data/vendor/assets/javascripts/reflux.min.js +0 -1
@@ -0,0 +1,121 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import PropTypes from "prop-types";
|
3
|
+
import copyToClipboard from "../../lib/copyToClipboard";
|
4
|
+
import AttachmentEditor from "./AttachmentEditor";
|
5
|
+
import ModalStore from "../../stores/ModalStore";
|
6
|
+
import ToastStore from "../../stores/ToastStore";
|
7
|
+
|
8
|
+
import { useDraggable } from "../drag";
|
9
|
+
|
10
|
+
export default function Attachment(props) {
|
11
|
+
const { attributeName, draggable } = props;
|
12
|
+
const { record } = draggable;
|
13
|
+
const { attachment, uploading } = record;
|
14
|
+
|
15
|
+
const listeners = useDraggable(draggable, props.startDrag);
|
16
|
+
|
17
|
+
const copyEmbed = (evt) => {
|
18
|
+
evt.preventDefault();
|
19
|
+
copyToClipboard(`[attachment:${attachment.id}]`);
|
20
|
+
ToastStore.dispatch({
|
21
|
+
type: "NOTICE", message: "Embed code copied to clipboard"
|
22
|
+
});
|
23
|
+
};
|
24
|
+
|
25
|
+
const deleteRecord = (evt) => {
|
26
|
+
evt.preventDefault();
|
27
|
+
if (props.deleteRecord) {
|
28
|
+
props.deleteRecord();
|
29
|
+
}
|
30
|
+
};
|
31
|
+
|
32
|
+
const description = () => {
|
33
|
+
if (attachment.description && attachment.description[props.locale]) {
|
34
|
+
return attachment.description[props.locale];
|
35
|
+
}
|
36
|
+
return null;
|
37
|
+
};
|
38
|
+
|
39
|
+
const name = () => {
|
40
|
+
if (attachment.name && attachment.name[props.locale]) {
|
41
|
+
return attachment.name[props.locale];
|
42
|
+
}
|
43
|
+
return null;
|
44
|
+
};
|
45
|
+
|
46
|
+
const editAttachment = (evt) => {
|
47
|
+
evt.preventDefault();
|
48
|
+
ModalStore.dispatch({
|
49
|
+
type: "OPEN",
|
50
|
+
payload: <AttachmentEditor attachment={attachment}
|
51
|
+
locale={props.locale}
|
52
|
+
locales={props.locales}
|
53
|
+
onUpdate={props.onUpdate} />
|
54
|
+
});
|
55
|
+
};
|
56
|
+
|
57
|
+
const classes = ["attachment"];
|
58
|
+
if (props.placeholder) {
|
59
|
+
classes.push("placeholder");
|
60
|
+
}
|
61
|
+
if (record.uploading) {
|
62
|
+
classes.push("uploading");
|
63
|
+
}
|
64
|
+
|
65
|
+
const icon = uploading ? "cloud-upload" : "paperclip";
|
66
|
+
|
67
|
+
return (
|
68
|
+
<div className={classes.join(" ")}
|
69
|
+
{...listeners}>
|
70
|
+
<input name={`${attributeName}[id]`}
|
71
|
+
type="hidden" value={record.id || ""} />
|
72
|
+
<input name={`${attributeName}[attachment_id]`}
|
73
|
+
type="hidden" value={(attachment && attachment.id) || ""} />
|
74
|
+
<input name={`${attributeName}[position]`}
|
75
|
+
type="hidden" value={props.position} />
|
76
|
+
{!uploading &&
|
77
|
+
<div className="actions">
|
78
|
+
<button onClick={editAttachment}>
|
79
|
+
Edit
|
80
|
+
</button>
|
81
|
+
{props.showEmbed && (
|
82
|
+
<button onClick={copyEmbed}>
|
83
|
+
Embed
|
84
|
+
</button>
|
85
|
+
)}
|
86
|
+
{props.deleteRecord && (
|
87
|
+
<button onClick={deleteRecord}>
|
88
|
+
Remove
|
89
|
+
</button>
|
90
|
+
)}
|
91
|
+
</div>
|
92
|
+
}
|
93
|
+
{attachment &&
|
94
|
+
<div className="attachment-info">
|
95
|
+
<h3>
|
96
|
+
<i className={`fa fa-${icon} icon`} />
|
97
|
+
{name() || <em>Untitled</em>}<br />
|
98
|
+
</h3>
|
99
|
+
{!uploading &&
|
100
|
+
<a href={attachment.url}
|
101
|
+
rel="noreferrer"
|
102
|
+
target="_blank">{attachment.filename}</a>}
|
103
|
+
{!uploading && description() && <p>{description()}</p>}
|
104
|
+
</div>}
|
105
|
+
</div>
|
106
|
+
);
|
107
|
+
}
|
108
|
+
|
109
|
+
Attachment.propTypes = {
|
110
|
+
locale: PropTypes.string,
|
111
|
+
locales: PropTypes.object,
|
112
|
+
draggable: PropTypes.object,
|
113
|
+
deleteRecord: PropTypes.func,
|
114
|
+
startDrag: PropTypes.func,
|
115
|
+
showEmbed: PropTypes.bool,
|
116
|
+
onUpdate: PropTypes.func,
|
117
|
+
attributeName: PropTypes.string,
|
118
|
+
placeholder: PropTypes.bool,
|
119
|
+
position: PropTypes.number,
|
120
|
+
ref: PropTypes.object
|
121
|
+
};
|
@@ -0,0 +1,116 @@
|
|
1
|
+
import React, { useState } from "react";
|
2
|
+
import PropTypes from "prop-types";
|
3
|
+
import copyToClipboard, { copySupported } from "../../lib/copyToClipboard";
|
4
|
+
import ModalStore from "../../stores/ModalStore";
|
5
|
+
import ToastStore from "../../stores/ToastStore";
|
6
|
+
import { putJson } from "../../lib/request";
|
7
|
+
|
8
|
+
export default function AttachmentEditor(props) {
|
9
|
+
const { attachment } = props;
|
10
|
+
|
11
|
+
const [locale, setLocale] = useState(props.locale);
|
12
|
+
const [localizations, setLocalizations] = useState({
|
13
|
+
name: attachment.name || {},
|
14
|
+
description: attachment.description || {},
|
15
|
+
});
|
16
|
+
|
17
|
+
const updateLocalization = (name) => (evt) => {
|
18
|
+
setLocalizations({
|
19
|
+
...localizations,
|
20
|
+
[name]: { ...localizations[name],
|
21
|
+
[locale]: evt.target.value }
|
22
|
+
});
|
23
|
+
};
|
24
|
+
|
25
|
+
const copyEmbedCode = (evt) => {
|
26
|
+
evt.preventDefault();
|
27
|
+
copyToClipboard(`[attachment:${attachment.id}]`);
|
28
|
+
ToastStore.dispatch({
|
29
|
+
type: "NOTICE", message: "Embed code copied to clipboard"
|
30
|
+
});
|
31
|
+
};
|
32
|
+
|
33
|
+
const save = (evt) => {
|
34
|
+
evt.preventDefault();
|
35
|
+
evt.stopPropagation();
|
36
|
+
|
37
|
+
let data = { ...localizations };
|
38
|
+
|
39
|
+
putJson(`/admin/attachments/${attachment.id}`,
|
40
|
+
{ attachment: data });
|
41
|
+
|
42
|
+
if (props.onUpdate) {
|
43
|
+
props.onUpdate(data);
|
44
|
+
}
|
45
|
+
ModalStore.dispatch({ type: "CLOSE" });
|
46
|
+
};
|
47
|
+
|
48
|
+
return (
|
49
|
+
<div className="attachment-editor">
|
50
|
+
<form>
|
51
|
+
{props.locales && Object.keys(props.locales).length > 1 && (
|
52
|
+
<div className="field">
|
53
|
+
<label>
|
54
|
+
Locale
|
55
|
+
</label>
|
56
|
+
<select name="locale"
|
57
|
+
onChange={e => setLocale(e.target.value)}>
|
58
|
+
{Object.keys(props.locales).map(key => (
|
59
|
+
<option key={`locale-${key}`} value={key}>
|
60
|
+
{props.locales[key]}
|
61
|
+
</option>
|
62
|
+
))}
|
63
|
+
</select>
|
64
|
+
</div>
|
65
|
+
)}
|
66
|
+
<div className="field">
|
67
|
+
<label>Name</label>
|
68
|
+
<input type="text"
|
69
|
+
className="name"
|
70
|
+
value={localizations.name[locale] || ""}
|
71
|
+
onChange={updateLocalization("name")} />
|
72
|
+
</div>
|
73
|
+
<div className="field">
|
74
|
+
<label>Description</label>
|
75
|
+
<textarea className="description"
|
76
|
+
value={localizations.description[locale] || ""}
|
77
|
+
onChange={updateLocalization("description")} />
|
78
|
+
</div>
|
79
|
+
<div className="field embed-code">
|
80
|
+
<label>
|
81
|
+
Embed code
|
82
|
+
</label>
|
83
|
+
<input type="text"
|
84
|
+
value={`[attachment:${attachment.id}]`}
|
85
|
+
disabled={true} />
|
86
|
+
{copySupported() && (
|
87
|
+
<button onClick={copyEmbedCode}>
|
88
|
+
Copy
|
89
|
+
</button>
|
90
|
+
)}
|
91
|
+
</div>
|
92
|
+
<div className="field">
|
93
|
+
<label>File</label>
|
94
|
+
<a href={attachment.url}
|
95
|
+
rel="noreferrer"
|
96
|
+
target="_blank">{attachment.filename}</a>
|
97
|
+
</div>
|
98
|
+
<div className="buttons">
|
99
|
+
<button onClick={save}>
|
100
|
+
Save
|
101
|
+
</button>
|
102
|
+
<button onClick={() => ModalStore.dispatch({ type: "CLOSE" })}>
|
103
|
+
Cancel
|
104
|
+
</button>
|
105
|
+
</div>
|
106
|
+
</form>
|
107
|
+
</div>
|
108
|
+
);
|
109
|
+
}
|
110
|
+
|
111
|
+
AttachmentEditor.propTypes = {
|
112
|
+
attachment: PropTypes.object,
|
113
|
+
locale: PropTypes.string,
|
114
|
+
locales: PropTypes.object,
|
115
|
+
onUpdate: PropTypes.func
|
116
|
+
};
|
@@ -0,0 +1,165 @@
|
|
1
|
+
import React, { useState } from "react";
|
2
|
+
import PropTypes from "prop-types";
|
3
|
+
import Attachment from "./Attachments/Attachment";
|
4
|
+
import Placeholder from "./Attachments/Placeholder";
|
5
|
+
import FileUploadButton from "./FileUploadButton";
|
6
|
+
import { post } from "../lib/request";
|
7
|
+
|
8
|
+
import { createDraggable,
|
9
|
+
draggedOrder,
|
10
|
+
useDragCollection,
|
11
|
+
useDragUploader } from "./drag";
|
12
|
+
|
13
|
+
function filenameToName(str) {
|
14
|
+
return str.replace(/\.[\w\d]+$/, "").replace(/_/g, " ");
|
15
|
+
}
|
16
|
+
|
17
|
+
export default function Attachments(props) {
|
18
|
+
const collection = useDragCollection(props.records);
|
19
|
+
const locales = props.locales ? Object.keys(props.locales) : [props.locale];
|
20
|
+
const [deleted, setDeleted] = useState([]);
|
21
|
+
|
22
|
+
const uploadAttachment = (file) => {
|
23
|
+
let name = {};
|
24
|
+
locales.forEach((l) => name[l] = file.name);
|
25
|
+
|
26
|
+
const draggable = createDraggable(
|
27
|
+
{ attachment: { filename: file.name, name: name },
|
28
|
+
uploading: true }
|
29
|
+
);
|
30
|
+
|
31
|
+
let data = new FormData();
|
32
|
+
|
33
|
+
data.append("attachment[file]", file);
|
34
|
+
locales.forEach((l) => {
|
35
|
+
data.append(`attachment[name][${l}]`, filenameToName(file.name));
|
36
|
+
});
|
37
|
+
|
38
|
+
post("/admin/attachments.json", data)
|
39
|
+
.then(json => {
|
40
|
+
collection.dispatch({
|
41
|
+
type: "update",
|
42
|
+
payload: { ...draggable,
|
43
|
+
record: { attachment: json, uploading: false } }
|
44
|
+
});
|
45
|
+
});
|
46
|
+
|
47
|
+
return draggable;
|
48
|
+
};
|
49
|
+
|
50
|
+
const receiveFiles = (files) => {
|
51
|
+
collection.dispatch({
|
52
|
+
type: "append",
|
53
|
+
payload: files.map(f => uploadAttachment(f))
|
54
|
+
});
|
55
|
+
};
|
56
|
+
|
57
|
+
const dragEnd = (dragState, files) => {
|
58
|
+
collection.dispatch({
|
59
|
+
type: "reorder",
|
60
|
+
payload: draggedOrder(collection, dragState)
|
61
|
+
});
|
62
|
+
collection.dispatch({
|
63
|
+
type: "insertFiles",
|
64
|
+
payload: files.map(f => uploadAttachment(f))
|
65
|
+
});
|
66
|
+
};
|
67
|
+
|
68
|
+
const [dragState,
|
69
|
+
dragStart,
|
70
|
+
listeners] = useDragUploader([collection], dragEnd);
|
71
|
+
|
72
|
+
const position = (record) => {
|
73
|
+
return [...collection.draggables.map(d => d.record),
|
74
|
+
...deleted].indexOf(record) + 1;
|
75
|
+
};
|
76
|
+
|
77
|
+
const attrName = (record) => {
|
78
|
+
return `${props.attribute}[${position(record)}]`;
|
79
|
+
};
|
80
|
+
|
81
|
+
const update = (draggable) => (attachment) => {
|
82
|
+
const { record } = draggable;
|
83
|
+
const updated = {
|
84
|
+
...draggable,
|
85
|
+
record: {
|
86
|
+
...record,
|
87
|
+
attachment: { ...record.attachment, ...attachment }
|
88
|
+
}
|
89
|
+
};
|
90
|
+
collection.dispatch({ type: "update", payload: updated });
|
91
|
+
};
|
92
|
+
|
93
|
+
const remove = (draggable) => () => {
|
94
|
+
collection.dispatch({ type: "remove", payload: draggable });
|
95
|
+
if (draggable.record.id) {
|
96
|
+
setDeleted([...deleted, draggable.record]);
|
97
|
+
}
|
98
|
+
};
|
99
|
+
|
100
|
+
const attachment = (draggable) => {
|
101
|
+
const { dragging } = dragState;
|
102
|
+
|
103
|
+
if (draggable === "Files") {
|
104
|
+
return (<Placeholder key="placeholder" />);
|
105
|
+
}
|
106
|
+
|
107
|
+
return (
|
108
|
+
<Attachment key={draggable.handle}
|
109
|
+
draggable={draggable}
|
110
|
+
locale={props.locale}
|
111
|
+
locales={props.locales}
|
112
|
+
showEmbed={props.showEmbed}
|
113
|
+
startDrag={dragStart}
|
114
|
+
position={position(draggable.record)}
|
115
|
+
onUpdate={update(draggable)}
|
116
|
+
deleteRecord={remove(draggable)}
|
117
|
+
attributeName={attrName(draggable.record)}
|
118
|
+
placeholder={dragging && dragging == draggable} />
|
119
|
+
);
|
120
|
+
};
|
121
|
+
|
122
|
+
const dragOrder = draggedOrder(collection, dragState);
|
123
|
+
|
124
|
+
const classes = ["attachments"];
|
125
|
+
if (dragState.dragging) {
|
126
|
+
classes.push("dragover");
|
127
|
+
}
|
128
|
+
|
129
|
+
return (
|
130
|
+
<div className={classes.join(" ")}
|
131
|
+
ref={collection.ref}
|
132
|
+
{...listeners}>
|
133
|
+
<div className="files">
|
134
|
+
{dragOrder.map(d => attachment(d))}
|
135
|
+
</div>
|
136
|
+
<div className="deleted">
|
137
|
+
{deleted.map(r =>
|
138
|
+
<span className="deleted-attachment" key={r.id}>
|
139
|
+
<input name={`${attrName(r)}[id]`}
|
140
|
+
type="hidden"
|
141
|
+
value={r.id} />
|
142
|
+
<input name={`${attrName(r)}[attachment_id]`}
|
143
|
+
type="hidden"
|
144
|
+
value={(r.attachment && r.attachment.id) || ""} />
|
145
|
+
<input name={`${attrName(r)}[_destroy]`}
|
146
|
+
type="hidden"
|
147
|
+
value={true} />
|
148
|
+
</span>)}
|
149
|
+
</div>
|
150
|
+
<div className="drop-target">
|
151
|
+
<FileUploadButton multiple={true}
|
152
|
+
multiline={true}
|
153
|
+
callback={receiveFiles} />
|
154
|
+
</div>
|
155
|
+
</div>
|
156
|
+
);
|
157
|
+
}
|
158
|
+
|
159
|
+
Attachments.propTypes = {
|
160
|
+
attribute: PropTypes.string,
|
161
|
+
locale: PropTypes.string,
|
162
|
+
locales: PropTypes.object,
|
163
|
+
records: PropTypes.array,
|
164
|
+
showEmbed: PropTypes.bool
|
165
|
+
};
|
@@ -1,4 +1,7 @@
|
|
1
|
-
|
1
|
+
import React from "react";
|
2
|
+
import PropTypes from "prop-types";
|
3
|
+
|
4
|
+
export default class DateRangeSelect extends React.Component {
|
2
5
|
constructor(props) {
|
3
6
|
super(props);
|
4
7
|
this.state = {
|
@@ -37,16 +40,16 @@ class DateRangeSelect extends React.Component {
|
|
37
40
|
|
38
41
|
modifyDate(original, options = {}) {
|
39
42
|
var newDate = new Date(original);
|
40
|
-
if (
|
43
|
+
if (Object.prototype.hasOwnProperty.call(options, "year")) {
|
41
44
|
newDate.setFullYear(options.year);
|
42
45
|
}
|
43
|
-
if (
|
46
|
+
if (Object.prototype.hasOwnProperty.call(options, "month")) {
|
44
47
|
newDate.setMonth(options.month);
|
45
48
|
}
|
46
|
-
if (
|
49
|
+
if (Object.prototype.hasOwnProperty.call(options, "date")) {
|
47
50
|
newDate.setDate(options.date);
|
48
51
|
}
|
49
|
-
if (
|
52
|
+
if (Object.prototype.hasOwnProperty.call(options, "time") &&
|
50
53
|
options.time.match(/^[\d]{1,2}(:[\d]{1,2})?$/)) {
|
51
54
|
newDate.setHours(options.time.split(":")[0]);
|
52
55
|
newDate.setMinutes(options.time.split(":")[1] || 0);
|
@@ -172,3 +175,11 @@ class DateRangeSelect extends React.Component {
|
|
172
175
|
return Array.apply(null, Array(31)).map((_, i) => i + 1);
|
173
176
|
}
|
174
177
|
}
|
178
|
+
|
179
|
+
DateRangeSelect.propTypes = {
|
180
|
+
startsAt: PropTypes.string,
|
181
|
+
endsAt: PropTypes.string,
|
182
|
+
disabled: PropTypes.bool,
|
183
|
+
disableTime: PropTypes.bool,
|
184
|
+
objectName: PropTypes.string
|
185
|
+
};
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import React, { useState } from "react";
|
2
|
+
import PropTypes from "prop-types";
|
3
|
+
import ImageEditor from "./ImageEditor";
|
4
|
+
import ModalStore from "../stores/ModalStore";
|
5
|
+
|
6
|
+
export default function EditableImage(props) {
|
7
|
+
const [image, setImage] = useState(props.image);
|
8
|
+
const [src, setSrc] = useState(props.src);
|
9
|
+
|
10
|
+
const height = () => {
|
11
|
+
const width = image.crop_width || image.real_width;
|
12
|
+
const height = image.crop_height || image.real_height;
|
13
|
+
return Math.round((height / width) * props.width);
|
14
|
+
};
|
15
|
+
|
16
|
+
const updateImage = (updatedImage, src) => {
|
17
|
+
let newImage = { ...image, ...updatedImage };
|
18
|
+
setSrc(src);
|
19
|
+
setImage(newImage);
|
20
|
+
if (props.onUpdate) {
|
21
|
+
props.onUpdate(newImage, src);
|
22
|
+
}
|
23
|
+
};
|
24
|
+
|
25
|
+
const handleClick = (evt) => {
|
26
|
+
evt.preventDefault();
|
27
|
+
ModalStore.dispatch({
|
28
|
+
type: "OPEN",
|
29
|
+
payload: <ImageEditor image={image}
|
30
|
+
caption={props.caption}
|
31
|
+
locale={props.locale}
|
32
|
+
locales={props.locales}
|
33
|
+
onUpdate={updateImage} />
|
34
|
+
});
|
35
|
+
};
|
36
|
+
|
37
|
+
const altWarning = !image.alternative[props.locale];
|
38
|
+
|
39
|
+
return (
|
40
|
+
<div className="editable-image">
|
41
|
+
{altWarning &&
|
42
|
+
<span className="alt-warning" title="Alternative text is missing">
|
43
|
+
<i className="fa fa-exclamation-triangle icon" />
|
44
|
+
</span>}
|
45
|
+
<img src={src}
|
46
|
+
width={props.width}
|
47
|
+
height={height()}
|
48
|
+
onClick={handleClick} />
|
49
|
+
</div>
|
50
|
+
);
|
51
|
+
}
|
52
|
+
|
53
|
+
EditableImage.propTypes = {
|
54
|
+
image: PropTypes.object,
|
55
|
+
src: PropTypes.string,
|
56
|
+
caption: PropTypes.bool,
|
57
|
+
locale: PropTypes.string,
|
58
|
+
locales: PropTypes.object,
|
59
|
+
width: PropTypes.number,
|
60
|
+
onUpdate: PropTypes.func
|
61
|
+
};
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import React, { useRef } from "react";
|
2
|
+
import PropTypes from "prop-types";
|
3
|
+
|
4
|
+
export default function FileUploadButton(props) {
|
5
|
+
const inputRef = useRef();
|
6
|
+
|
7
|
+
const handleChange = (evt) => {
|
8
|
+
let fileList = evt.target.files;
|
9
|
+
let files = [];
|
10
|
+
for (var i = 0; i < fileList.length; i++) {
|
11
|
+
files.push(fileList[i]);
|
12
|
+
}
|
13
|
+
if (files.length > 0) {
|
14
|
+
props.callback(files);
|
15
|
+
}
|
16
|
+
};
|
17
|
+
|
18
|
+
const triggerDialog = (evt) => {
|
19
|
+
evt.preventDefault();
|
20
|
+
inputRef.current.click();
|
21
|
+
};
|
22
|
+
|
23
|
+
return (
|
24
|
+
<div className="upload-button">
|
25
|
+
<span>
|
26
|
+
Drag and drop {props.type || "file"}
|
27
|
+
{props.multiple && "s"} here, or
|
28
|
+
{props.multiline && <br />}
|
29
|
+
<button onClick={triggerDialog}>
|
30
|
+
choose a file
|
31
|
+
</button>
|
32
|
+
</span>
|
33
|
+
<input type="file"
|
34
|
+
onChange={handleChange}
|
35
|
+
ref={inputRef}
|
36
|
+
style={{ display: "none" }}
|
37
|
+
multiple={props.multiple || false} />
|
38
|
+
</div>
|
39
|
+
);
|
40
|
+
}
|
41
|
+
|
42
|
+
FileUploadButton.propTypes = {
|
43
|
+
callback: PropTypes.func,
|
44
|
+
type: PropTypes.string,
|
45
|
+
multiple: PropTypes.bool,
|
46
|
+
multiline: PropTypes.bool
|
47
|
+
};
|
@@ -1,4 +1,7 @@
|
|
1
|
-
|
1
|
+
import React from "react";
|
2
|
+
import PropTypes from "prop-types";
|
3
|
+
|
4
|
+
export default class FocalPoint extends React.Component {
|
2
5
|
constructor(props) {
|
3
6
|
super(props);
|
4
7
|
this.state = {
|
@@ -80,3 +83,11 @@ class FocalPoint extends React.Component {
|
|
80
83
|
);
|
81
84
|
}
|
82
85
|
}
|
86
|
+
|
87
|
+
FocalPoint.propTypes = {
|
88
|
+
x: PropTypes.number,
|
89
|
+
y: PropTypes.number,
|
90
|
+
onChange: PropTypes.func,
|
91
|
+
width: PropTypes.number,
|
92
|
+
height: PropTypes.number
|
93
|
+
};
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import PropTypes from "prop-types";
|
3
|
+
import ReactCrop from "react-image-crop";
|
4
|
+
|
5
|
+
import { cropSize } from "./useCrop";
|
6
|
+
import FocalPoint from "./FocalPoint";
|
7
|
+
|
8
|
+
export default function Image(props) {
|
9
|
+
const imageSize = () => {
|
10
|
+
const { image, cropping, crop_width, crop_height } = props.cropState;
|
11
|
+
if (cropping) {
|
12
|
+
return { width: image.real_width, height: image.real_height };
|
13
|
+
} else {
|
14
|
+
return { width: crop_width, height: crop_height };
|
15
|
+
}
|
16
|
+
};
|
17
|
+
|
18
|
+
const maxWidth = props.containerSize.width;
|
19
|
+
const maxHeight = props.containerSize.height;
|
20
|
+
const aspect = imageSize().width / imageSize().height;
|
21
|
+
|
22
|
+
var width = maxWidth;
|
23
|
+
var height = maxWidth / aspect;
|
24
|
+
|
25
|
+
if (height > maxHeight) {
|
26
|
+
height = maxHeight;
|
27
|
+
width = maxHeight * aspect;
|
28
|
+
}
|
29
|
+
|
30
|
+
const style = { width: `${width}px`, height: `${height}px` };
|
31
|
+
|
32
|
+
if (props.cropState.cropping) {
|
33
|
+
return (
|
34
|
+
<div className="image-wrapper" style={style}>
|
35
|
+
<ReactCrop src={props.cropState.image.uncropped_url}
|
36
|
+
crop={cropSize(props.cropState)}
|
37
|
+
minWidth={10}
|
38
|
+
minHeight={10}
|
39
|
+
onChange={props.setCrop} />
|
40
|
+
</div>
|
41
|
+
);
|
42
|
+
} else {
|
43
|
+
return (
|
44
|
+
<div className="image-wrapper" style={style}>
|
45
|
+
{props.focalPoint && (
|
46
|
+
<FocalPoint width={width} height={height}
|
47
|
+
x={props.focalPoint.x}
|
48
|
+
y={props.focalPoint.y}
|
49
|
+
onChange={props.setFocal} />
|
50
|
+
)}
|
51
|
+
<img src={props.croppedImage} />
|
52
|
+
</div>
|
53
|
+
);
|
54
|
+
}
|
55
|
+
|
56
|
+
}
|
57
|
+
|
58
|
+
Image.propTypes = {
|
59
|
+
containerSize: PropTypes.object,
|
60
|
+
croppedImage: PropTypes.string,
|
61
|
+
cropState: PropTypes.object,
|
62
|
+
focalPoint: PropTypes.object,
|
63
|
+
setCrop: PropTypes.func,
|
64
|
+
setFocal: PropTypes.func
|
65
|
+
};
|