pages_core 3.7.0 → 3.8.0

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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/app/assets/builds/pages_core/admin-dist.js +55 -0
  4. data/app/assets/stylesheets/pages/admin/components/image_editor.scss +1 -0
  5. data/app/assets/stylesheets/pages/admin/components/image_grid.scss +33 -5
  6. data/app/assets/stylesheets/pages/admin/components/layout.scss +2 -1
  7. data/app/assets/stylesheets/pages/admin/components/login.scss +6 -0
  8. data/app/assets/stylesheets/pages/admin/components/tabs.scss +5 -0
  9. data/app/assets/stylesheets/pages/admin/components/tag_editor.scss +13 -7
  10. data/app/assets/stylesheets/pages/admin/controllers/pages.scss +13 -5
  11. data/app/assets/stylesheets/pages/admin.scss +0 -1
  12. data/app/controller_dummies/admin/admin_controller.rb +1 -1
  13. data/app/controller_dummies/application_controller.rb +1 -1
  14. data/app/controller_dummies/attachments_controller.rb +1 -1
  15. data/app/controller_dummies/frontend_controller.rb +1 -1
  16. data/app/controller_dummies/images_controller.rb +1 -1
  17. data/app/controller_dummies/page_files_controller.rb +1 -1
  18. data/app/controller_dummies/pages_controller.rb +1 -1
  19. data/app/controller_dummies/sitemaps_controller.rb +1 -1
  20. data/app/controllers/admin/attachments_controller.rb +1 -1
  21. data/app/controllers/admin/images_controller.rb +10 -7
  22. data/app/controllers/concerns/pages_core/authentication.rb +9 -4
  23. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +5 -0
  24. data/app/controllers/pages_core/frontend/pages_controller.rb +5 -2
  25. data/app/controllers/sessions_controller.rb +1 -1
  26. data/app/formatters/pages_core/link_renderer.rb +2 -2
  27. data/app/helpers/application_helper.rb +1 -1
  28. data/app/helpers/frontend_helper.rb +1 -1
  29. data/app/helpers/pages_core/admin/content_tabs_helper.rb +5 -2
  30. data/app/helpers/pages_core/admin/image_uploads_helper.rb +2 -3
  31. data/app/helpers/pages_core/admin/tag_editor_helper.rb +9 -39
  32. data/app/helpers/pages_core/head_tags_helper.rb +11 -20
  33. data/app/helpers/pages_core/open_graph_tags_helper.rb +1 -1
  34. data/app/javascript/admin-dist.js +2 -0
  35. data/app/javascript/components/Attachments/Attachment.jsx +121 -0
  36. data/app/javascript/components/Attachments/AttachmentEditor.jsx +116 -0
  37. data/app/javascript/components/Attachments/Placeholder.jsx +10 -0
  38. data/app/javascript/components/Attachments.jsx +165 -0
  39. data/app/{assets/javascripts/pages/admin/components/date_range_select.jsx → javascript/components/DateRangeSelect.jsx} +16 -5
  40. data/app/javascript/components/EditableImage.jsx +61 -0
  41. data/app/{assets/javascripts/pages/admin/components/file_upload_button.jsx → javascript/components/FileUploadButton.jsx} +11 -1
  42. data/app/{assets/javascripts/pages/admin/components/focal_point.jsx → javascript/components/ImageCropper/FocalPoint.jsx} +12 -1
  43. data/app/javascript/components/ImageCropper/Image.jsx +65 -0
  44. data/app/javascript/components/ImageCropper/Toolbar.jsx +73 -0
  45. data/app/javascript/components/ImageCropper/useCrop.js +199 -0
  46. data/app/javascript/components/ImageCropper.jsx +90 -0
  47. data/app/javascript/components/ImageEditor/Form.jsx +98 -0
  48. data/app/javascript/components/ImageEditor.jsx +62 -0
  49. data/app/javascript/components/ImageGrid/DragElement.jsx +30 -0
  50. data/app/javascript/components/ImageGrid/FilePlaceholder.jsx +9 -0
  51. data/app/javascript/components/ImageGrid/GridImage.jsx +103 -0
  52. data/app/javascript/components/ImageGrid/Placeholder.jsx +23 -0
  53. data/app/javascript/components/ImageGrid.jsx +257 -0
  54. data/app/javascript/components/ImageUploader.jsx +171 -0
  55. data/app/{assets/javascripts/pages/admin/components/modal.jsx → javascript/components/Modal.jsx} +13 -2
  56. data/app/javascript/components/ModalStore.jsx +12 -0
  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/ToastStore.jsx +14 -0
  70. data/app/javascript/components/drag/draggedOrder.js +51 -0
  71. data/app/javascript/components/drag/useDragCollection.js +84 -0
  72. data/app/javascript/components/drag/useDragUploader.js +112 -0
  73. data/app/javascript/components/drag/useDraggable.js +17 -0
  74. data/app/javascript/components/drag.js +6 -0
  75. data/app/javascript/components.js +14 -0
  76. data/app/javascript/controllers/EditPageController.js +20 -0
  77. data/app/javascript/controllers/LoginController.js +29 -0
  78. data/app/javascript/controllers/MainController.js +65 -0
  79. data/app/javascript/controllers/PageOptionsController.js +62 -0
  80. data/app/javascript/features/RichText.jsx +34 -0
  81. data/app/javascript/hooks.js +2 -0
  82. data/app/javascript/index.js +33 -0
  83. data/app/{assets/javascripts/pages/admin/lib/tree.jsx → javascript/lib/Tree.js} +55 -54
  84. data/app/javascript/lib/copyToClipboard.js +13 -0
  85. data/app/javascript/lib/readyHandler.js +22 -0
  86. data/app/javascript/lib/request.js +36 -0
  87. data/app/models/concerns/pages_core/page_model/images.rb +3 -1
  88. data/app/models/concerns/pages_core/page_model/searchable.rb +19 -0
  89. data/app/models/concerns/pages_core/searchable_document.rb +71 -0
  90. data/app/models/concerns/pages_core/taggable.rb +27 -12
  91. data/app/models/page.rb +2 -0
  92. data/app/models/page_exporter.rb +2 -2
  93. data/app/models/page_image.rb +0 -2
  94. data/app/models/role.rb +1 -1
  95. data/app/models/search_document.rb +72 -0
  96. data/app/models/tag.rb +1 -0
  97. data/app/models/user.rb +1 -1
  98. data/app/{serializers/admin/attachment_serializer.rb → resources/admin/attachment_resource.rb} +6 -5
  99. data/app/{serializers/admin/image_serializer.rb → resources/admin/image_resource.rb} +9 -9
  100. data/app/resources/admin/page_file_resource.rb +10 -0
  101. data/app/{serializers/admin/page_image_serializer.rb → resources/admin/page_image_resource.rb} +4 -2
  102. data/app/resources/export/attachment_resource.rb +10 -0
  103. data/app/resources/export/page_image_resource.rb +45 -0
  104. data/app/resources/export/page_resource.rb +42 -0
  105. data/app/{serializers/page_image_serializer.rb → resources/page_image_resource.rb} +8 -16
  106. data/app/resources/page_resource.rb +33 -0
  107. data/app/services/pages_core/destroy_invite_service.rb +2 -2
  108. data/app/services/pages_core/invite_service.rb +2 -2
  109. data/app/views/admin/pages/_edit_content.html.erb +1 -1
  110. data/app/views/admin/pages/_edit_files.html.erb +1 -5
  111. data/app/views/admin/pages/_edit_images.html.erb +1 -5
  112. data/app/views/admin/pages/_edit_options.html.erb +74 -55
  113. data/app/views/admin/pages/_form.html.erb +19 -0
  114. data/app/views/admin/pages/edit.html.erb +35 -61
  115. data/app/views/admin/pages/index.html.erb +0 -1
  116. data/app/views/admin/pages/new.html.erb +32 -32
  117. data/app/views/admin/users/_access_control.html.erb +5 -1
  118. data/app/views/admin/users/login.html.erb +12 -4
  119. data/app/views/feeds/pages.rss.builder +1 -2
  120. data/app/views/layouts/admin/_header.html.erb +1 -1
  121. data/app/views/layouts/admin/_page_header.html.erb +33 -0
  122. data/app/views/layouts/admin.html.erb +23 -42
  123. data/app/views/pages_core/_google_analytics.html.erb +8 -0
  124. data/db/migrate/20180625154059_enable_search_extensions.rb +10 -0
  125. data/db/migrate/20210209151400_create_search_configurations.rb +35 -0
  126. data/db/migrate/20210210235200_create_search_documents.rb +74 -0
  127. data/lib/pages_core/engine.rb +1 -5
  128. data/lib/pages_core/templates/block_configuration.rb +1 -1
  129. data/lib/pages_core/templates/configuration_handler.rb +1 -1
  130. data/lib/pages_core/version.rb +1 -1
  131. data/lib/pages_core.rb +3 -5
  132. data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +0 -7
  133. data/lib/rails/generators/pages_core/install/templates/page_templates_initializer.rb +2 -2
  134. metadata +101 -115
  135. data/app/assets/javascripts/pages/admin/components/attachment.jsx +0 -130
  136. data/app/assets/javascripts/pages/admin/components/attachment_editor.jsx +0 -131
  137. data/app/assets/javascripts/pages/admin/components/attachments.jsx +0 -211
  138. data/app/assets/javascripts/pages/admin/components/drag_uploader.jsx +0 -174
  139. data/app/assets/javascripts/pages/admin/components/editable_image.jsx +0 -57
  140. data/app/assets/javascripts/pages/admin/components/grid_image.jsx +0 -124
  141. data/app/assets/javascripts/pages/admin/components/image_editor.jsx +0 -496
  142. data/app/assets/javascripts/pages/admin/components/image_grid.jsx +0 -306
  143. data/app/assets/javascripts/pages/admin/components/image_uploader.jsx +0 -176
  144. data/app/assets/javascripts/pages/admin/components/modal_store.jsx +0 -20
  145. data/app/assets/javascripts/pages/admin/components/rich_text_area.jsx +0 -64
  146. data/app/assets/javascripts/pages/admin/components/rich_text_toolbar.jsx +0 -91
  147. data/app/assets/javascripts/pages/admin/components/toast.jsx +0 -34
  148. data/app/assets/javascripts/pages/admin/components/toast_store.jsx +0 -52
  149. data/app/assets/javascripts/pages/admin/components.jsx +0 -2
  150. data/app/assets/javascripts/pages/admin/features/content_tabs.jsx +0 -72
  151. data/app/assets/javascripts/pages/admin/features/edit_page.jsx +0 -97
  152. data/app/assets/javascripts/pages/admin/features/rich_text.jsx +0 -14
  153. data/app/assets/javascripts/pages/admin/features/tag_editor.jsx +0 -160
  154. data/app/assets/javascripts/pages/admin.jsx +0 -17
  155. data/app/assets/javascripts/pages/login_form.jsx +0 -21
  156. data/app/serializers/admin/page_file_serializer.rb +0 -8
  157. data/app/serializers/page_export_serializer.rb +0 -32
  158. data/app/serializers/page_file_export_serializer.rb +0 -6
  159. data/app/serializers/page_image_export_serializer.rb +0 -42
  160. data/app/serializers/page_serializer.rb +0 -23
  161. data/app/views/layouts/admin/_analytics.html.erb +0 -16
  162. data/lib/rails/generators/pages_core/frontend/templates/application.js.erb +0 -15
  163. data/vendor/assets/javascripts/ReactCrop.min.js +0 -1
  164. 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 "../ModalStore";
6
+ import ToastStore from "../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 "../ModalStore";
5
+ import ToastStore from "../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 "./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
+ };
@@ -1,4 +1,7 @@
1
- class FileUploadButton extends React.Component {
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ export default class FileUploadButton extends React.Component {
2
5
  constructor(props) {
3
6
  super(props);
4
7
  this.inputRef = React.createRef();
@@ -42,3 +45,10 @@ class FileUploadButton extends React.Component {
42
45
  this.inputRef.current.click();
43
46
  }
44
47
  }
48
+
49
+ FileUploadButton.propTypes = {
50
+ callback: PropTypes.func,
51
+ type: PropTypes.string,
52
+ multiple: PropTypes.bool,
53
+ multiline: PropTypes.bool
54
+ };
@@ -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
+ };
@@ -0,0 +1,73 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ export default function Toolbar(props) {
5
+ const { cropping } = props.cropState;
6
+
7
+ const aspectRatios = [
8
+ ["Free", null], ["1:1", 1], ["3:2", 3/2], ["2:3", 2/3],
9
+ ["4:3", 4/3], ["3:4", 3/4], ["5:4", 5/4], ["4:5", 4/5],
10
+ ["16:9", 16/9]
11
+ ];
12
+
13
+ const updateAspect = (ratio) => (evt) => {
14
+ evt.preventDefault();
15
+ props.setAspect(ratio);
16
+ };
17
+
18
+ const width = Math.ceil(props.cropState.crop_width);
19
+ const height = Math.ceil(props.cropState.crop_height);
20
+ const format = props.image.content_type.split("/")[1].toUpperCase();
21
+
22
+ return (
23
+ <div className="toolbars">
24
+ <div className="toolbar">
25
+ <div className="info">
26
+ <span className="format">
27
+ {width}x{height} {format}
28
+ </span>
29
+ </div>
30
+ <button title="Crop image"
31
+ onClick={props.toggleCrop}
32
+ className={cropping ? "active" : ""}>
33
+ <i className="fa fa-crop" />
34
+ </button>
35
+ <button disabled={cropping}
36
+ title="Toggle focal point"
37
+ onClick={props.toggleFocal}>
38
+ <i className="fa fa-bullseye" />
39
+ </button>
40
+ <a href={props.image.original_url}
41
+ className="button"
42
+ title="Download original image"
43
+ disabled={cropping}
44
+ download={props.image.filename}
45
+ onClick={evt => cropping && evt.preventDefault()}>
46
+ <i className="fa fa-download" />
47
+ </a>
48
+ </div>
49
+ {cropping && (
50
+ <div className="aspect-ratios toolbar">
51
+ <div className="label">
52
+ Lock aspect ratio:
53
+ </div>
54
+ {aspectRatios.map(ratio => (
55
+ <button key={"ratio-" + ratio[1]}
56
+ className={(ratio[1] == props.cropState.aspect) ? "active" : ""}
57
+ onClick={updateAspect(ratio[1])}>
58
+ {ratio[0]}
59
+ </button>
60
+ ))}
61
+ </div>
62
+ )}
63
+ </div>
64
+ );
65
+ }
66
+
67
+ Toolbar.propTypes = {
68
+ cropState: PropTypes.object,
69
+ image: PropTypes.object,
70
+ setAspect: PropTypes.func,
71
+ toggleCrop: PropTypes.func,
72
+ toggleFocal: PropTypes.func
73
+ };