pages_core 3.7.0 → 3.8.3

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.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/Rakefile +37 -0
  4. data/app/assets/builds/pages_core/admin-dist.js +55 -0
  5. data/app/assets/stylesheets/pages/admin/components/image_editor.scss +1 -0
  6. data/app/assets/stylesheets/pages/admin/components/image_grid.scss +33 -5
  7. data/app/assets/stylesheets/pages/admin/components/layout.scss +2 -1
  8. data/app/assets/stylesheets/pages/admin/components/login.scss +6 -0
  9. data/app/assets/stylesheets/pages/admin/components/tabs.scss +5 -0
  10. data/app/assets/stylesheets/pages/admin/components/tag_editor.scss +13 -7
  11. data/app/assets/stylesheets/pages/admin/controllers/pages.scss +13 -5
  12. data/app/assets/stylesheets/pages/admin.scss +0 -1
  13. data/app/controller_dummies/admin/admin_controller.rb +1 -1
  14. data/app/controller_dummies/application_controller.rb +1 -1
  15. data/app/controller_dummies/attachments_controller.rb +1 -1
  16. data/app/controller_dummies/frontend_controller.rb +1 -1
  17. data/app/controller_dummies/images_controller.rb +1 -1
  18. data/app/controller_dummies/page_files_controller.rb +1 -1
  19. data/app/controller_dummies/pages_controller.rb +1 -1
  20. data/app/controller_dummies/sitemaps_controller.rb +1 -1
  21. data/app/controllers/admin/attachments_controller.rb +1 -1
  22. data/app/controllers/admin/images_controller.rb +11 -8
  23. data/app/controllers/concerns/pages_core/authentication.rb +9 -4
  24. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +5 -0
  25. data/app/controllers/pages_core/frontend/pages_controller.rb +5 -2
  26. data/app/controllers/sessions_controller.rb +1 -1
  27. data/app/formatters/pages_core/link_renderer.rb +2 -2
  28. data/app/helpers/application_helper.rb +1 -1
  29. data/app/helpers/frontend_helper.rb +1 -1
  30. data/app/helpers/pages_core/admin/content_tabs_helper.rb +5 -2
  31. data/app/helpers/pages_core/admin/image_uploads_helper.rb +2 -3
  32. data/app/helpers/pages_core/admin/tag_editor_helper.rb +9 -39
  33. data/app/helpers/pages_core/head_tags_helper.rb +11 -20
  34. data/app/helpers/pages_core/open_graph_tags_helper.rb +1 -1
  35. data/app/javascript/admin-dist.js +2 -0
  36. data/app/javascript/components/Attachments/Attachment.jsx +121 -0
  37. data/app/javascript/components/Attachments/AttachmentEditor.jsx +116 -0
  38. data/app/javascript/components/Attachments/Placeholder.jsx +10 -0
  39. data/app/javascript/components/Attachments.jsx +165 -0
  40. data/app/{assets/javascripts/pages/admin/components/date_range_select.jsx → javascript/components/DateRangeSelect.jsx} +16 -5
  41. data/app/javascript/components/EditableImage.jsx +61 -0
  42. data/app/javascript/components/FileUploadButton.jsx +47 -0
  43. data/app/{assets/javascripts/pages/admin/components/focal_point.jsx → javascript/components/ImageCropper/FocalPoint.jsx} +12 -1
  44. data/app/javascript/components/ImageCropper/Image.jsx +65 -0
  45. data/app/javascript/components/ImageCropper/Toolbar.jsx +73 -0
  46. data/app/javascript/components/ImageCropper/useCrop.js +199 -0
  47. data/app/javascript/components/ImageCropper.jsx +90 -0
  48. data/app/javascript/components/ImageEditor/Form.jsx +98 -0
  49. data/app/javascript/components/ImageEditor.jsx +62 -0
  50. data/app/javascript/components/ImageGrid/DragElement.jsx +30 -0
  51. data/app/javascript/components/ImageGrid/FilePlaceholder.jsx +9 -0
  52. data/app/javascript/components/ImageGrid/GridImage.jsx +103 -0
  53. data/app/javascript/components/ImageGrid/Placeholder.jsx +23 -0
  54. data/app/javascript/components/ImageGrid.jsx +257 -0
  55. data/app/javascript/components/ImageUploader.jsx +171 -0
  56. data/app/{assets/javascripts/pages/admin/components/modal.jsx → javascript/components/Modal.jsx} +13 -2
  57. data/app/{assets/javascripts/pages/admin/components/page_dates.jsx → javascript/components/PageDates.jsx} +11 -1
  58. data/app/{assets/javascripts/pages/admin/components/page_files.jsx → javascript/components/PageFiles.jsx} +11 -2
  59. data/app/{assets/javascripts/pages/admin/components/page_images.jsx → javascript/components/PageImages.jsx} +11 -2
  60. data/app/{assets/javascripts/pages/admin/components/page_tree_store.jsx → javascript/components/PageTree.jsx} +127 -137
  61. data/app/{assets/javascripts/pages/admin/components/page_tree.jsx → javascript/components/PageTreeDraggable.jsx} +35 -29
  62. data/app/{assets/javascripts/pages/admin/components/page_tree_node.jsx → javascript/components/PageTreeNode.jsx} +35 -20
  63. data/app/javascript/components/RichTextArea.jsx +213 -0
  64. data/app/javascript/components/RichTextToolbarButton.jsx +20 -0
  65. data/app/javascript/components/TagEditor/AddTagForm.jsx +42 -0
  66. data/app/javascript/components/TagEditor/Tag.jsx +32 -0
  67. data/app/javascript/components/TagEditor.jsx +61 -0
  68. data/app/javascript/components/Toast.jsx +72 -0
  69. data/app/javascript/components/drag/draggedOrder.js +51 -0
  70. data/app/javascript/components/drag/useDragCollection.js +84 -0
  71. data/app/javascript/components/drag/useDragUploader.js +112 -0
  72. data/app/javascript/components/drag/useDraggable.js +17 -0
  73. data/app/javascript/components/drag.js +6 -0
  74. data/app/javascript/components.js +15 -0
  75. data/app/javascript/controllers/EditPageController.js +20 -0
  76. data/app/javascript/controllers/LoginController.js +29 -0
  77. data/app/javascript/controllers/MainController.js +65 -0
  78. data/app/javascript/controllers/PageOptionsController.js +62 -0
  79. data/app/javascript/features/RichText.jsx +34 -0
  80. data/app/javascript/hooks.js +2 -0
  81. data/app/javascript/index.js +38 -0
  82. data/app/{assets/javascripts/pages/admin/lib/tree.jsx → javascript/lib/Tree.js} +55 -54
  83. data/app/javascript/lib/copyToClipboard.js +13 -0
  84. data/app/javascript/lib/readyHandler.js +22 -0
  85. data/app/javascript/lib/request.js +36 -0
  86. data/app/javascript/stores/ModalStore.jsx +12 -0
  87. data/app/javascript/stores/ToastStore.jsx +14 -0
  88. data/app/javascript/stores.js +2 -0
  89. data/app/models/concerns/pages_core/page_model/images.rb +3 -1
  90. data/app/models/concerns/pages_core/page_model/searchable.rb +19 -0
  91. data/app/models/concerns/pages_core/searchable_document.rb +71 -0
  92. data/app/models/concerns/pages_core/taggable.rb +27 -12
  93. data/app/models/page.rb +2 -0
  94. data/app/models/page_exporter.rb +2 -2
  95. data/app/models/page_image.rb +0 -2
  96. data/app/models/role.rb +1 -1
  97. data/app/models/search_document.rb +72 -0
  98. data/app/models/tag.rb +1 -0
  99. data/app/models/user.rb +1 -1
  100. data/app/{serializers/admin/attachment_serializer.rb → resources/admin/attachment_resource.rb} +6 -5
  101. data/app/{serializers/admin/image_serializer.rb → resources/admin/image_resource.rb} +9 -9
  102. data/app/resources/admin/page_file_resource.rb +10 -0
  103. data/app/{serializers/admin/page_image_serializer.rb → resources/admin/page_image_resource.rb} +4 -2
  104. data/app/resources/export/attachment_resource.rb +10 -0
  105. data/app/resources/export/page_image_resource.rb +45 -0
  106. data/app/resources/export/page_resource.rb +42 -0
  107. data/app/{serializers/page_image_serializer.rb → resources/page_image_resource.rb} +8 -16
  108. data/app/resources/page_resource.rb +33 -0
  109. data/app/services/pages_core/destroy_invite_service.rb +2 -2
  110. data/app/services/pages_core/invite_service.rb +2 -2
  111. data/app/views/admin/pages/_edit_content.html.erb +1 -1
  112. data/app/views/admin/pages/_edit_files.html.erb +1 -5
  113. data/app/views/admin/pages/_edit_images.html.erb +1 -5
  114. data/app/views/admin/pages/_edit_options.html.erb +74 -55
  115. data/app/views/admin/pages/_form.html.erb +19 -0
  116. data/app/views/admin/pages/edit.html.erb +35 -61
  117. data/app/views/admin/pages/index.html.erb +0 -1
  118. data/app/views/admin/pages/new.html.erb +32 -32
  119. data/app/views/admin/users/_access_control.html.erb +5 -1
  120. data/app/views/admin/users/login.html.erb +12 -4
  121. data/app/views/feeds/pages.rss.builder +1 -2
  122. data/app/views/layouts/admin/_header.html.erb +1 -1
  123. data/app/views/layouts/admin/_page_header.html.erb +33 -0
  124. data/app/views/layouts/admin.html.erb +23 -42
  125. data/app/views/pages_core/_google_analytics.html.erb +8 -0
  126. data/db/migrate/20180625154059_enable_search_extensions.rb +10 -0
  127. data/db/migrate/20210209151400_create_search_configurations.rb +35 -0
  128. data/db/migrate/20210210235200_create_search_documents.rb +74 -0
  129. data/lib/pages_core/engine.rb +1 -5
  130. data/lib/pages_core/templates/block_configuration.rb +1 -1
  131. data/lib/pages_core/templates/configuration_handler.rb +1 -1
  132. data/lib/pages_core/version.rb +3 -1
  133. data/lib/pages_core.rb +3 -5
  134. data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +0 -7
  135. data/lib/rails/generators/pages_core/install/templates/page_templates_initializer.rb +2 -2
  136. metadata +116 -115
  137. data/app/assets/javascripts/pages/admin/components/attachment.jsx +0 -130
  138. data/app/assets/javascripts/pages/admin/components/attachment_editor.jsx +0 -131
  139. data/app/assets/javascripts/pages/admin/components/attachments.jsx +0 -211
  140. data/app/assets/javascripts/pages/admin/components/drag_uploader.jsx +0 -174
  141. data/app/assets/javascripts/pages/admin/components/editable_image.jsx +0 -57
  142. data/app/assets/javascripts/pages/admin/components/file_upload_button.jsx +0 -44
  143. data/app/assets/javascripts/pages/admin/components/grid_image.jsx +0 -124
  144. data/app/assets/javascripts/pages/admin/components/image_editor.jsx +0 -496
  145. data/app/assets/javascripts/pages/admin/components/image_grid.jsx +0 -306
  146. data/app/assets/javascripts/pages/admin/components/image_uploader.jsx +0 -176
  147. data/app/assets/javascripts/pages/admin/components/modal_store.jsx +0 -20
  148. data/app/assets/javascripts/pages/admin/components/rich_text_area.jsx +0 -64
  149. data/app/assets/javascripts/pages/admin/components/rich_text_toolbar.jsx +0 -91
  150. data/app/assets/javascripts/pages/admin/components/toast.jsx +0 -34
  151. data/app/assets/javascripts/pages/admin/components/toast_store.jsx +0 -52
  152. data/app/assets/javascripts/pages/admin/components.jsx +0 -2
  153. data/app/assets/javascripts/pages/admin/features/content_tabs.jsx +0 -72
  154. data/app/assets/javascripts/pages/admin/features/edit_page.jsx +0 -97
  155. data/app/assets/javascripts/pages/admin/features/rich_text.jsx +0 -14
  156. data/app/assets/javascripts/pages/admin/features/tag_editor.jsx +0 -160
  157. data/app/assets/javascripts/pages/admin.jsx +0 -17
  158. data/app/assets/javascripts/pages/login_form.jsx +0 -21
  159. data/app/serializers/admin/page_file_serializer.rb +0 -8
  160. data/app/serializers/page_export_serializer.rb +0 -32
  161. data/app/serializers/page_file_export_serializer.rb +0 -6
  162. data/app/serializers/page_image_export_serializer.rb +0 -42
  163. data/app/serializers/page_serializer.rb +0 -23
  164. data/app/views/layouts/admin/_analytics.html.erb +0 -16
  165. data/lib/rails/generators/pages_core/frontend/templates/application.js.erb +0 -15
  166. data/vendor/assets/javascripts/ReactCrop.min.js +0 -1
  167. 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,10 @@
1
+ import React from "react";
2
+
3
+ export default function Placeholder() {
4
+ return (
5
+ <div className="attachment drop-placeholder"
6
+ key="file-placeholder">
7
+ Upload files here
8
+ </div>
9
+ );
10
+ }
@@ -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
- class DateRangeSelect extends React.Component {
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 (options.hasOwnProperty("year")) {
43
+ if (Object.prototype.hasOwnProperty.call(options, "year")) {
41
44
  newDate.setFullYear(options.year);
42
45
  }
43
- if (options.hasOwnProperty("month")) {
46
+ if (Object.prototype.hasOwnProperty.call(options, "month")) {
44
47
  newDate.setMonth(options.month);
45
48
  }
46
- if (options.hasOwnProperty("date")) {
49
+ if (Object.prototype.hasOwnProperty.call(options, "date")) {
47
50
  newDate.setDate(options.date);
48
51
  }
49
- if (options.hasOwnProperty("time") &&
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
- class FocalPoint extends React.Component {
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
+ };