keystone_ui-pages 0.2.0 → 0.4.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.
- checksums.yaml +4 -4
- data/app/assets/builds/keystone_ui_pages/editor.js +35 -22
- data/app/controllers/keystone_ui/pages/manage/elements_controller.rb +4 -1
- data/app/controllers/keystone_ui/pages/manage/pages_controller.rb +11 -0
- data/app/controllers/keystone_ui/pages/manage/titles_controller.rb +15 -0
- data/app/helpers/keystone_ui/pages/drawing_helper.rb +30 -2
- data/app/helpers/keystone_ui/pages/editor_helper.rb +1 -0
- data/app/javascript/keystone_ui_pages/ElementFields.jsx +4 -1
- data/app/javascript/keystone_ui_pages/LayoutPanel.jsx +1 -1
- data/app/javascript/keystone_ui_pages/PageEditor.jsx +4 -2
- data/app/javascript/keystone_ui_pages/PageTitle.jsx +16 -0
- data/app/javascript/keystone_ui_pages/RichTextField.jsx +100 -0
- data/app/javascript/keystone_ui_pages/operations.js +2 -0
- data/app/models/keystone_ui/pages/page.rb +1 -0
- data/app/models/keystone_ui/pages/version.rb +4 -0
- data/app/views/keystone_ui/pages/manage/pages/edit.html.erb +22 -0
- data/app/views/keystone_ui/pages/manage/pages/index.html.erb +1 -0
- data/app/views/keystone_ui/pages/pages/show.html.erb +1 -0
- data/config/routes.rb +2 -1
- data/lib/keystone_ui/pages/keystone_elements.rb +3 -3
- data/lib/keystone_ui/pages/operations.rb +6 -2
- data/lib/keystone_ui/pages/version.rb +1 -1
- data/the_local/agents/keystone_ui-pages-develop.md +32 -4
- metadata +5 -1
|
@@ -22,7 +22,10 @@ module KeystoneUi
|
|
|
22
22
|
|
|
23
23
|
def field_value
|
|
24
24
|
value = params.to_unsafe_h["value"]
|
|
25
|
-
|
|
25
|
+
return value.map { |entry| entry.to_h.stringify_keys } if value.is_a?(Array)
|
|
26
|
+
return { "html" => value["html"].to_s } if value.is_a?(Hash) && value.key?("html")
|
|
27
|
+
|
|
28
|
+
value.to_s
|
|
26
29
|
end
|
|
27
30
|
end
|
|
28
31
|
end
|
|
@@ -25,6 +25,17 @@ module KeystoneUi
|
|
|
25
25
|
(session[:keystone_ui_pages_return_to] ||= {})[@page.id.to_s] = params[:return_to] if local_address?(params[:return_to])
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def edit
|
|
29
|
+
@page = owned_pages.find(params[:id])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update
|
|
33
|
+
@page = owned_pages.find(params[:id])
|
|
34
|
+
return render(:edit, status: :unprocessable_entity) unless @page.update(params.require(:page).permit(:name, :slug, :place))
|
|
35
|
+
|
|
36
|
+
redirect_to manage_pages_path
|
|
37
|
+
end
|
|
38
|
+
|
|
28
39
|
def publish
|
|
29
40
|
page = owned_pages.find(params[:id])
|
|
30
41
|
page.publish
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KeystoneUi
|
|
4
|
+
module Pages
|
|
5
|
+
module Manage
|
|
6
|
+
class TitlesController < BaseController
|
|
7
|
+
include EditsPages
|
|
8
|
+
|
|
9
|
+
def update
|
|
10
|
+
changed(Operations.title(page.document, params[:title]))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -9,6 +9,8 @@ module KeystoneUi
|
|
|
9
9
|
WIDE_SPANS = { "1/1" => "@sm:col-span-12", "3/4" => "@sm:col-span-9", "2/3" => "@sm:col-span-8", "1/2" => "@sm:col-span-6", "1/3" => "@sm:col-span-4", "1/4" => "@sm:col-span-3" }.freeze
|
|
10
10
|
ELEMENT_ALIGNMENTS = { "stretch" => "flex flex-col", "start" => "flex justify-start", "center" => "flex justify-center", "end" => "flex justify-end" }.freeze
|
|
11
11
|
COLUMN_ALIGNMENTS = { "top" => "justify-start", "center" => "justify-center", "bottom" => "justify-end" }.freeze
|
|
12
|
+
SIZED_SECTION_PADDING = "py-[calc(var(--ks-spacing)*16)] lg:py-[calc(var(--ks-spacing)*24)]"
|
|
13
|
+
SHADED_CLASSES = "bg-(color:--ks-color-fill-muted) dark:bg-(color:--ks-color-fill-muted-dark)"
|
|
12
14
|
PLAIN_FIGURE_DIVIDER = "@sm:[.ks-pages-column:has(.ks-metric-plain)+&:has(.ks-metric-plain)]:border-l-(length:--ks-border-width) " \
|
|
13
15
|
"@sm:[.ks-pages-column:has(.ks-metric-plain)+&:has(.ks-metric-plain)]:border-(color:--ks-color-border) " \
|
|
14
16
|
"dark:@sm:[.ks-pages-column:has(.ks-metric-plain)+&:has(.ks-metric-plain)]:border-(color:--ks-color-border-dark)"
|
|
@@ -18,6 +20,12 @@ module KeystoneUi
|
|
|
18
20
|
Rails.cache.fetch(key) { drawn_document(version.document, values: values).to_str }.html_safe
|
|
19
21
|
end
|
|
20
22
|
|
|
23
|
+
def pages_paragraph(text:)
|
|
24
|
+
return ui_paragraph(text: text) unless text.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
rich_paragraphs(text["html"].to_s)
|
|
27
|
+
end
|
|
28
|
+
|
|
21
29
|
def drawn_document(document, editing: false, values: {})
|
|
22
30
|
@drawn_values = values.to_h.stringify_keys
|
|
23
31
|
safe_join(document["sections"].map { |section| drawn_section(section, editing) })
|
|
@@ -25,12 +33,31 @@ module KeystoneUi
|
|
|
25
33
|
|
|
26
34
|
private
|
|
27
35
|
|
|
36
|
+
def rich_paragraphs(html)
|
|
37
|
+
stored = Nokogiri::HTML5.fragment(html)
|
|
38
|
+
stored.css("script, style").each(&:remove)
|
|
39
|
+
fragment = Nokogiri::HTML5.fragment(Rails::HTML5::SafeListSanitizer.new.sanitize(stored.to_html, tags: %w[p br a], attributes: %w[href]))
|
|
40
|
+
fragment.css("a:not([href])").each { |link| link.replace(link.children) }
|
|
41
|
+
fragment.css("p").each { |paragraph| paragraph["class"] = "ks-paragraph" }
|
|
42
|
+
fragment.css("a[href^='http://'], a[href^='https://']").each { |link| link["target"], link["rel"] = "_blank", "noopener noreferrer" }
|
|
43
|
+
fragment.to_html.html_safe
|
|
44
|
+
end
|
|
45
|
+
|
|
28
46
|
def drawn_section(section, editing)
|
|
29
47
|
return if hidden?(section) && !editing
|
|
30
48
|
|
|
31
49
|
spacing = Keystone::Ui::SectionComponent::SPACING_CLASSES.fetch((section["spacing"] || Operations::DEFAULTS[:section]["spacing"]).to_sym)
|
|
32
50
|
data = editing ? { section_id: section["id"], hidden: hidden?(section) || nil }.compact : {}
|
|
33
|
-
|
|
51
|
+
shading = SHADED_CLASSES if section["background"] == "shaded"
|
|
52
|
+
tag.section(section_content(section, editing), class: [ "ks-pages-section flex flex-col gap-6 print:break-inside-avoid #{spacing}", shading ].compact.join(" "), data: data)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def section_content(section, editing)
|
|
56
|
+
rows = safe_join(section["rows"].map { |row| drawn_row(row, editing) })
|
|
57
|
+
width = section["width"]
|
|
58
|
+
return rows if width.nil? || width == "none"
|
|
59
|
+
|
|
60
|
+
tag.div(rows, class: "ks-page #{Keystone::Ui::PageComponent::MAX_WIDTH_CLASSES.fetch(width.to_sym)} w-full flex flex-col gap-6 #{SIZED_SECTION_PADDING}")
|
|
34
61
|
end
|
|
35
62
|
|
|
36
63
|
def drawn_row(row, editing)
|
|
@@ -75,6 +102,7 @@ module KeystoneUi
|
|
|
75
102
|
return [] if (field[:kind] == :list || (field[:kind] == :data && field[:default].is_a?(Array))) && value.blank?
|
|
76
103
|
return nil if field[:kind] == :data && value.blank?
|
|
77
104
|
return value if value.blank?
|
|
105
|
+
return ERB::Util.html_escape(value) if field[:as] == :escaped_text
|
|
78
106
|
return value.to_sym if field[:as] == :key
|
|
79
107
|
return value.to_s == "true" if field[:kind] == :yes_or_no
|
|
80
108
|
return value.to_sym if field[:kind] == :one_of
|
|
@@ -91,7 +119,7 @@ module KeystoneUi
|
|
|
91
119
|
end
|
|
92
120
|
|
|
93
121
|
def filled(stored)
|
|
94
|
-
return stored.presence unless stored.is_a?(Hash)
|
|
122
|
+
return stored.presence unless stored.is_a?(Hash) && stored.key?("value")
|
|
95
123
|
|
|
96
124
|
(@drawn_values || {})[stored["value"]].presence || stored["fallback"].presence || ""
|
|
97
125
|
end
|
|
@@ -23,6 +23,7 @@ module KeystoneUi
|
|
|
23
23
|
samples: Values.samples_for(page),
|
|
24
24
|
sample: chosen_sample(page),
|
|
25
25
|
samples_url: manage_page_sample_path(page),
|
|
26
|
+
title_url: manage_page_title_path(page),
|
|
26
27
|
access_exceptions: KeystoneUi::Pages.access_exceptions.invert.to_a,
|
|
27
28
|
access_exception: page.access_exception,
|
|
28
29
|
access_exception_url: manage_page_access_exception_path(page),
|
|
@@ -6,6 +6,7 @@ import Checkbox from "keystone_ui-react/src/Checkbox.jsx"
|
|
|
6
6
|
import Button from "keystone_ui-react/src/Button.jsx"
|
|
7
7
|
import { Label } from "keystone_ui-react/src/FieldText.jsx"
|
|
8
8
|
import ListField from "./ListField"
|
|
9
|
+
import RichTextField from "./RichTextField"
|
|
9
10
|
|
|
10
11
|
const FieldInput = ({ field, value, onChange }) => {
|
|
11
12
|
const shown = value ?? field.default ?? ""
|
|
@@ -59,7 +60,9 @@ export default function ElementFields({ element, definition, alignments, valueNa
|
|
|
59
60
|
{field.kind === "list" && !isBound(element.fields[field.key]) &&
|
|
60
61
|
<ListField field={field} entries={element.fields[field.key] ?? field.default ?? []} onChange={onChange} />}
|
|
61
62
|
{field.kind === "data" && <p className="ks-hint">Filled with data the host supplies on a template page</p>}
|
|
62
|
-
{field.kind
|
|
63
|
+
{field.kind === "rich_text" &&
|
|
64
|
+
<RichTextField key={JSON.stringify(element.fields[field.key] ?? null)} field={field} value={element.fields[field.key]} onChange={onChange} />}
|
|
65
|
+
{field.kind !== "list" && field.kind !== "data" && field.kind !== "rich_text" && <FieldInput key={JSON.stringify(element.fields[field.key] ?? null)} field={field}
|
|
63
66
|
value={isBound(element.fields[field.key]) ? element.fields[field.key].fallback ?? "" : element.fields[field.key]}
|
|
64
67
|
onChange={(key, text) => (isBound(element.fields[key]) ? onBind(key, element.fields[key].value, text) : onChange(key, text))} />}
|
|
65
68
|
{isBound(element.fields[field.key]) &&
|
|
@@ -5,7 +5,7 @@ import Alert from "keystone_ui-react/src/Alert.jsx"
|
|
|
5
5
|
import { Label } from "keystone_ui-react/src/FieldText.jsx"
|
|
6
6
|
|
|
7
7
|
const LABELS = {
|
|
8
|
-
section: { spacing: "Section spacing" },
|
|
8
|
+
section: { spacing: "Section spacing", background: "Section background", width: "Section width" },
|
|
9
9
|
row: { gap: "Row gap" },
|
|
10
10
|
column: { wide: "Width on wide screens", narrow: "Width on narrow screens", align: "Vertical alignment" }
|
|
11
11
|
}
|
|
@@ -2,13 +2,14 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react"
|
|
|
2
2
|
import { DragDropManager, Draggable, Droppable, KeyboardSensor, PointerSensor } from "@dnd-kit/dom"
|
|
3
3
|
import { Sortable } from "@dnd-kit/dom/sortable"
|
|
4
4
|
import Button from "keystone_ui-react/src/Button.jsx"
|
|
5
|
-
import { addColumn, addElement, addRow, addSection, arrange, bindField, chooseAccessException, chooseSample, duplicateNode, moveNode, removeNode, setField, showWhen, stepHistory } from "./operations"
|
|
5
|
+
import { addColumn, addElement, addRow, addSection, arrange, bindField, chooseAccessException, chooseSample, duplicateNode, moveNode, removeNode, setField, setTitle, showWhen, stepHistory } from "./operations"
|
|
6
6
|
import LayoutPanel from "./LayoutPanel"
|
|
7
7
|
import DeviceWidth, { DEVICES } from "./DeviceWidth"
|
|
8
8
|
import SelectionToolbar from "./SelectionToolbar"
|
|
9
9
|
import ElementPalette from "./ElementPalette"
|
|
10
10
|
import SelectionPath from "./SelectionPath"
|
|
11
11
|
import SampleChoice from "./SampleChoice"
|
|
12
|
+
import PageTitle from "./PageTitle"
|
|
12
13
|
import AccessExceptionChoice from "./AccessExceptionChoice"
|
|
13
14
|
import ShowWhen from "./ShowWhen"
|
|
14
15
|
import ThemeMode from "./ThemeMode"
|
|
@@ -29,7 +30,7 @@ const innermost = (target) => NODES.map(([selector, kind]) => [target.closest(se
|
|
|
29
30
|
|
|
30
31
|
const PLACED_SENSORS = [PointerSensor.configure({ preventActivation: () => false }), KeyboardSensor]
|
|
31
32
|
|
|
32
|
-
export default function PageEditor({ name, publish_url, unpublish_url, elements, canvas: drawn, document: saved, elements_url, sections_url, columns_url, rows_url, nodes_url, arrangements, defaults, shares, value_names, looks = [], samples = [], sample, samples_url, access_exceptions = [], access_exception, access_exception_url, history_url, live_url, back_url, undo_url, redo_url, undoable: couldUndo, redoable: couldRedo, token }) {
|
|
33
|
+
export default function PageEditor({ name, publish_url, unpublish_url, elements, canvas: drawn, document: saved, elements_url, sections_url, columns_url, rows_url, nodes_url, arrangements, defaults, shares, value_names, looks = [], samples = [], sample, samples_url, title_url, access_exceptions = [], access_exception, access_exception_url, history_url, live_url, back_url, undo_url, redo_url, undoable: couldUndo, redoable: couldRedo, token }) {
|
|
33
34
|
const [canvas, setCanvas] = useState(drawn)
|
|
34
35
|
const [undoable, setUndoable] = useState(couldUndo)
|
|
35
36
|
const [redoable, setRedoable] = useState(couldRedo)
|
|
@@ -245,6 +246,7 @@ export default function PageEditor({ name, publish_url, unpublish_url, elements,
|
|
|
245
246
|
<aside data-settings-panel className="w-72 shrink-0 overflow-y-auto border-l border-(--ks-color-border) bg-(--ks-color-surface) p-4 dark:border-(--ks-color-border-dark) dark:bg-(--ks-color-surface-dark)">
|
|
246
247
|
<SelectionPath path={selectionPath} onChoose={choosePart} />
|
|
247
248
|
{!node && !definition && <p className="ks-hint">Select something on the page to change it.</p>}
|
|
249
|
+
{!node && !definition && <PageTitle key={document.title ?? ""} title={document.title} onChange={(title) => setTitle(title_url, token, title).then(shown, refused)} />}
|
|
248
250
|
{node && <LayoutPanel node={node} values={inUse(node)} arrangements={arrangements}
|
|
249
251
|
onArrange={(id, setting, value) => arrange(nodes_url, token, id, setting, value).then(shown, refused)}
|
|
250
252
|
refusal={refusal}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import Input from "keystone_ui-react/src/Input.jsx"
|
|
3
|
+
import { Label } from "keystone_ui-react/src/FieldText.jsx"
|
|
4
|
+
|
|
5
|
+
export default function PageTitle({ title, onChange }) {
|
|
6
|
+
const shown = title ?? ""
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex flex-col gap-1">
|
|
10
|
+
<Label htmlFor="ks-pages-page-title">Page title</Label>
|
|
11
|
+
<Input id="ks-pages-page-title" type="text" defaultValue={shown}
|
|
12
|
+
onBlur={(event) => event.target.value !== shown && onChange(event.target.value)} />
|
|
13
|
+
<p className="ks-hint">Shown in the browser tab once the page is published</p>
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react"
|
|
2
|
+
import Input from "keystone_ui-react/src/Input.jsx"
|
|
3
|
+
import Button from "keystone_ui-react/src/Button.jsx"
|
|
4
|
+
import { Label } from "keystone_ui-react/src/FieldText.jsx"
|
|
5
|
+
import { LexicalComposer } from "@lexical/react/LexicalComposer"
|
|
6
|
+
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
|
|
7
|
+
import { ContentEditable } from "@lexical/react/LexicalContentEditable"
|
|
8
|
+
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
|
|
9
|
+
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
|
|
10
|
+
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"
|
|
11
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
|
|
12
|
+
import { LinkNode, TOGGLE_LINK_COMMAND, $isLinkNode } from "@lexical/link"
|
|
13
|
+
import { $generateHtmlFromNodes, $generateNodesFromDOM } from "@lexical/html"
|
|
14
|
+
import { $getRoot, $getSelection, $isRangeSelection, $createParagraphNode, $createTextNode, TextNode } from "lexical"
|
|
15
|
+
|
|
16
|
+
const filled = (stored) => (editor) => {
|
|
17
|
+
const root = $getRoot()
|
|
18
|
+
if (stored && typeof stored === "object" && "html" in stored) {
|
|
19
|
+
const dom = new DOMParser().parseFromString(stored.html, "text/html")
|
|
20
|
+
root.append(...$generateNodesFromDOM(editor, dom))
|
|
21
|
+
} else {
|
|
22
|
+
root.append($createParagraphNode().append($createTextNode(stored ?? "")))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const selectedLink = () => {
|
|
27
|
+
const selection = $getSelection()
|
|
28
|
+
if (!$isRangeSelection(selection)) return null
|
|
29
|
+
const node = selection.anchor.getNode()
|
|
30
|
+
return $isLinkNode(node) ? node : $isLinkNode(node.getParent()) ? node.getParent() : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PlainWords = () => {
|
|
34
|
+
const [editor] = useLexicalComposerContext()
|
|
35
|
+
useEffect(() => editor.registerNodeTransform(TextNode, (node) => {
|
|
36
|
+
if (node.getFormat() !== 0) node.setFormat(0)
|
|
37
|
+
if (node.getStyle() !== "") node.setStyle("")
|
|
38
|
+
}), [editor])
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const LinkControls = () => {
|
|
43
|
+
const [editor] = useLexicalComposerContext()
|
|
44
|
+
const [address, setAddress] = useState(null)
|
|
45
|
+
|
|
46
|
+
const opened = () => editor.getEditorState().read(() => setAddress(selectedLink()?.getURL() ?? ""))
|
|
47
|
+
const saved = () => {
|
|
48
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, address || null)
|
|
49
|
+
setAddress(null)
|
|
50
|
+
}
|
|
51
|
+
const removed = () => {
|
|
52
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
|
53
|
+
setAddress(null)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (address === null) return <Button type="button" variant="secondary" onClick={opened}>Link</Button>
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex flex-col gap-2">
|
|
60
|
+
<Label htmlFor="ks-pages-link-address">Link address</Label>
|
|
61
|
+
<Input id="ks-pages-link-address" type="url" value={address} onChange={(event) => setAddress(event.target.value)} />
|
|
62
|
+
<div className="flex gap-2">
|
|
63
|
+
<Button type="button" onClick={saved}>Save link</Button>
|
|
64
|
+
<Button type="button" variant="secondary" onClick={removed}>Remove link</Button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const SavedOnLeave = ({ fieldKey, onChange, children }) => {
|
|
71
|
+
const [editor] = useLexicalComposerContext()
|
|
72
|
+
const left = (event) => {
|
|
73
|
+
if (event.currentTarget.contains(event.relatedTarget)) return
|
|
74
|
+
editor.getEditorState().read(() => onChange(fieldKey, { html: $generateHtmlFromNodes(editor, null) }))
|
|
75
|
+
}
|
|
76
|
+
return <div data-rich-text-field={fieldKey} onBlur={left} className="flex flex-col gap-2">{children}</div>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function RichTextField({ field, value, onChange }) {
|
|
80
|
+
const config = {
|
|
81
|
+
namespace: `ks-pages-${field.key}`,
|
|
82
|
+
nodes: [LinkNode],
|
|
83
|
+
editorState: filled(value ?? field.default),
|
|
84
|
+
onError: (error) => { throw error }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<LexicalComposer initialConfig={config}>
|
|
89
|
+
<SavedOnLeave fieldKey={field.key} onChange={onChange}>
|
|
90
|
+
<RichTextPlugin
|
|
91
|
+
contentEditable={<ContentEditable id={`ks-pages-field-${field.key}`} className="ks-input min-h-24" />}
|
|
92
|
+
ErrorBoundary={LexicalErrorBoundary} />
|
|
93
|
+
<HistoryPlugin />
|
|
94
|
+
<LinkPlugin />
|
|
95
|
+
<PlainWords />
|
|
96
|
+
<LinkControls />
|
|
97
|
+
</SavedOnLeave>
|
|
98
|
+
</LexicalComposer>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -7,6 +7,8 @@ const sent = (url, token, method, body = {}) =>
|
|
|
7
7
|
|
|
8
8
|
export const addElement = (url, token, key, columnId) => sent(url, token, "POST", { key, column_id: columnId })
|
|
9
9
|
|
|
10
|
+
export const setTitle = (url, token, title) => sent(url, token, "POST", { title })
|
|
11
|
+
|
|
10
12
|
export const setField = (url, token, elementId, field, value) => sent(`${url}/${elementId}`, token, "PATCH", { field, value })
|
|
11
13
|
|
|
12
14
|
export const addSection = (url, token) => sent(url, token, "POST", {})
|
|
@@ -18,6 +18,7 @@ module KeystoneUi
|
|
|
18
18
|
scope :current, -> { where(archived_at: nil) }
|
|
19
19
|
|
|
20
20
|
validates :name, :slug, presence: true
|
|
21
|
+
validates :slug, uniqueness: { scope: %i[owner_type owner_id] }
|
|
21
22
|
validates :place, inclusion: { in: -> { KeystoneUi::Pages.places.keys } }, allow_nil: true, if: -> { KeystoneUi::Pages.places.any? }
|
|
22
23
|
validates :access_exception, inclusion: { in: -> { KeystoneUi::Pages.access_exceptions.keys } }, allow_nil: true, if: -> { KeystoneUi::Pages.access_exceptions.any? }
|
|
23
24
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<%= ui_page(max_width: :xl) do %>
|
|
2
|
+
<%= ui_page_header(title: "Settings") %>
|
|
3
|
+
|
|
4
|
+
<% if @page.errors.any? %>
|
|
5
|
+
<%= ui_alert(message: @page.errors.full_messages.to_sentence, type: :error) %>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<%= ui_form(action: manage_page_path(@page), method: :patch) do %>
|
|
9
|
+
<label class="ks-label mb-1">Name</label>
|
|
10
|
+
<%= ui_input(name: "page[name]", value: @page.name) %>
|
|
11
|
+
|
|
12
|
+
<label class="ks-label mb-1">Address</label>
|
|
13
|
+
<%= ui_input(name: "page[slug]", value: @page.slug) %>
|
|
14
|
+
|
|
15
|
+
<% if KeystoneUi::Pages.places.any? %>
|
|
16
|
+
<label class="ks-label mb-1">Place</label>
|
|
17
|
+
<%= ui_select(name: "page[place]", options: KeystoneUi::Pages.places.invert.to_a, selected: @page.place) %>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
<%= ui_button(label: "Save", type: :submit) %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
<% end %>
|
|
39
39
|
<td class="ks-table-cell-middle text-sm whitespace-nowrap text-right">
|
|
40
40
|
<%= link_to "Edit", manage_page_path(page) %>
|
|
41
|
+
<%= link_to "Settings", edit_manage_page_path(page), class: "ml-4" %>
|
|
41
42
|
<% if live %>
|
|
42
43
|
<%= link_to "View live", manage_page_live_path(page), class: "ml-4" %>
|
|
43
44
|
<% if @printable %>
|
data/config/routes.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
KeystoneUi::Pages::Engine.routes.draw do
|
|
4
4
|
namespace :manage do
|
|
5
|
-
resources :pages, only: %i[index new create show] do
|
|
5
|
+
resources :pages, only: %i[index new create show edit update] do
|
|
6
6
|
post :publish, on: :member
|
|
7
7
|
post :unpublish, on: :member
|
|
8
8
|
post :archive, on: :member
|
|
@@ -20,6 +20,7 @@ KeystoneUi::Pages::Engine.routes.draw do
|
|
|
20
20
|
end
|
|
21
21
|
resource :document, only: %i[update]
|
|
22
22
|
post :sample, to: "samples#update"
|
|
23
|
+
post :title, to: "titles#update"
|
|
23
24
|
post :access_exception, to: "access_exceptions#update"
|
|
24
25
|
post :undo, to: "edit_history#undo"
|
|
25
26
|
post :redo, to: "edit_history#redo"
|
|
@@ -23,8 +23,8 @@ module KeystoneUi
|
|
|
23
23
|
{ key: :note, label: "Note", kind: :text } ])
|
|
24
24
|
catalog.register(key: :label, name: "Label", component: :ui_caption, icon: :tag, group: "Text",
|
|
25
25
|
fields: [ { key: :text, label: "Text", kind: :text, default: "Label" } ])
|
|
26
|
-
catalog.register(key: :paragraph, name: "Paragraph", component: :
|
|
27
|
-
fields: [ { key: :text, label: "Text", kind: :
|
|
26
|
+
catalog.register(key: :paragraph, name: "Paragraph", component: :pages_paragraph, icon: :text, group: "Text",
|
|
27
|
+
fields: [ { key: :text, label: "Text", kind: :rich_text, default: "Write the text of this paragraph here." } ])
|
|
28
28
|
catalog.register(key: :hero, name: "Hero", component: :ui_hero, icon: :heading, group: "Text",
|
|
29
29
|
fields: [ { key: :title, label: "Title", kind: :text, default: "Headline" },
|
|
30
30
|
{ key: :subtitle, label: "Subtitle", kind: :long_text },
|
|
@@ -66,7 +66,7 @@ module KeystoneUi
|
|
|
66
66
|
{ key: :subtitle, label: "Subtitle", kind: :text },
|
|
67
67
|
{ key: :features, label: "Features", kind: :list,
|
|
68
68
|
entry: [ { key: :title, label: "Title", kind: :text }, { key: :description, label: "Description", kind: :long_text },
|
|
69
|
-
{ key: :icon, label: "Icon", kind: :text,
|
|
69
|
+
{ key: :icon, label: "Icon", kind: :text, as: :escaped_text,
|
|
70
70
|
default: '<svg viewBox="0 0 24 24" class="size-6 fill-none stroke-current stroke-2"><path d="M5 12l5 5L20 7"/></svg>' } ],
|
|
71
71
|
default: [ { "title" => "Fast", "description" => "Pages load quickly." },
|
|
72
72
|
{ "title" => "Clear", "description" => "Every page reads plainly." },
|
|
@@ -14,13 +14,13 @@ module KeystoneUi
|
|
|
14
14
|
module Operations
|
|
15
15
|
WIDTHS = %w[1/1 3/4 2/3 1/2 1/3 1/4].freeze
|
|
16
16
|
ARRANGEMENTS = {
|
|
17
|
-
section: { "spacing" => %w[sm md lg] },
|
|
17
|
+
section: { "spacing" => %w[sm md lg], "background" => %w[plain shaded], "width" => %w[none sm md lg xl] },
|
|
18
18
|
row: { "gap" => %w[sm md lg xl] },
|
|
19
19
|
column: { "wide" => WIDTHS, "narrow" => WIDTHS, "align" => %w[top center bottom] },
|
|
20
20
|
element: { "align" => %w[stretch start center end] }
|
|
21
21
|
}.freeze
|
|
22
22
|
DEFAULTS = {
|
|
23
|
-
section: { "spacing" => "md" },
|
|
23
|
+
section: { "spacing" => "md", "background" => "plain", "width" => "none" },
|
|
24
24
|
row: { "gap" => "md" },
|
|
25
25
|
column: { "narrow" => "1/1", "align" => "top" },
|
|
26
26
|
element: { "align" => "stretch" }
|
|
@@ -36,6 +36,10 @@ module KeystoneUi
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def title(document, text)
|
|
40
|
+
copied(document).tap { |changed| changed["title"] = text.to_s.strip.presence }
|
|
41
|
+
end
|
|
42
|
+
|
|
39
43
|
def add_section(document)
|
|
40
44
|
copied(document).tap { |changed| changed["sections"] << Document.section }
|
|
41
45
|
end
|
|
@@ -274,10 +274,38 @@ The catalog offers these Keystone UI components by default: `button`,
|
|
|
274
274
|
turns symbol keys into strings, so either works, but the names must match.
|
|
275
275
|
- **The editor's page list** at `/pages/manage/pages` shows each page's name,
|
|
276
276
|
its state, and when it was last published, plus its place and access
|
|
277
|
-
exception when the host names any. Each row links to Edit and
|
|
278
|
-
button, and a published page also links to View live and, when
|
|
279
|
-
Chromium is found, to its PDF. Show archived lists archived pages
|
|
280
|
-
each with a Restore button.
|
|
277
|
+
exception when the host names any. Each row links to Edit and Settings and has
|
|
278
|
+
an Archive button, and a published page also links to View live and, when
|
|
279
|
+
Chrome or Chromium is found, to its PDF. Show archived lists archived pages
|
|
280
|
+
instead, each with a Restore button.
|
|
281
|
+
- **A page's title** is set in the builder's settings panel when nothing is
|
|
282
|
+
selected, and reaches visitors when the page is published. The engine's
|
|
283
|
+
public address sets it with `content_for(:title)`, so a host layout that
|
|
284
|
+
shows `content_for(:title)` in its `<title>` gets it. A host drawing a page
|
|
285
|
+
in its own view reads `page.live_version&.title`, which is nil when the page
|
|
286
|
+
has none.
|
|
287
|
+
- **A paragraph's text is rich text.** Editors type it in the builder and
|
|
288
|
+
link selected words with the Link button, and pasted text keeps only its
|
|
289
|
+
words and links. It is stored as `{ "html" => "..." }` and cleaned each time
|
|
290
|
+
it is drawn, keeping paragraphs, line breaks and links with safe addresses.
|
|
291
|
+
A link to an `http` or `https` address opens in a new tab. A paragraph saved
|
|
292
|
+
earlier as a plain string still draws as escaped text.
|
|
293
|
+
- **A feature grid's icons** are typed per feature in the builder, usually as
|
|
294
|
+
an emoji. A typed icon is drawn as text, so markup or a script in it is shown
|
|
295
|
+
and never run. A feature left without an icon shows the check mark.
|
|
296
|
+
- **A section's background and width** are set in the builder's layout panel.
|
|
297
|
+
A shaded section fills the full width of the page with keystone_ui's
|
|
298
|
+
`--ks-color-fill-muted` colour, and its dark value in a dark theme. A section
|
|
299
|
+
with a width of `sm`, `md`, `lg` or `xl` holds its rows at keystone_ui's
|
|
300
|
+
page width of that name with its page side padding, padded above and below
|
|
301
|
+
from `--ks-spacing`. The page builder has no setting of its own for these:
|
|
302
|
+
a host changes them through its keystone_ui theme values. A section saved
|
|
303
|
+
without either draws as it did before.
|
|
304
|
+
- **A page's settings** at `/pages/manage/pages/:id/edit` change its name, slug
|
|
305
|
+
and place after it is created. A slug another page of the same owner uses, or
|
|
306
|
+
a place the host does not name, is refused with a message and changes nothing.
|
|
307
|
+
A changed slug moves the published page to its new address, and the old
|
|
308
|
+
address is not redirected.
|
|
281
309
|
- **A page's state** is Draft before its first publish, Published while it has
|
|
282
310
|
a live version, Unpublished once that version is taken down, and Archived.
|
|
283
311
|
The builder's Unpublish button, shown next to Publish on a published page,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keystone_ui-pages
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tyler Schneider
|
|
@@ -90,6 +90,7 @@ files:
|
|
|
90
90
|
- app/controllers/keystone_ui/pages/manage/rows_controller.rb
|
|
91
91
|
- app/controllers/keystone_ui/pages/manage/samples_controller.rb
|
|
92
92
|
- app/controllers/keystone_ui/pages/manage/sections_controller.rb
|
|
93
|
+
- app/controllers/keystone_ui/pages/manage/titles_controller.rb
|
|
93
94
|
- app/controllers/keystone_ui/pages/manage/versions_controller.rb
|
|
94
95
|
- app/controllers/keystone_ui/pages/pages_controller.rb
|
|
95
96
|
- app/helpers/keystone_ui/pages/drawing_helper.rb
|
|
@@ -103,6 +104,8 @@ files:
|
|
|
103
104
|
- app/javascript/keystone_ui_pages/ListField.jsx
|
|
104
105
|
- app/javascript/keystone_ui_pages/LookChoice.jsx
|
|
105
106
|
- app/javascript/keystone_ui_pages/PageEditor.jsx
|
|
107
|
+
- app/javascript/keystone_ui_pages/PageTitle.jsx
|
|
108
|
+
- app/javascript/keystone_ui_pages/RichTextField.jsx
|
|
106
109
|
- app/javascript/keystone_ui_pages/SampleChoice.jsx
|
|
107
110
|
- app/javascript/keystone_ui_pages/SelectionPath.jsx
|
|
108
111
|
- app/javascript/keystone_ui_pages/SelectionToolbar.jsx
|
|
@@ -115,6 +118,7 @@ files:
|
|
|
115
118
|
- app/models/keystone_ui/pages/page.rb
|
|
116
119
|
- app/models/keystone_ui/pages/pdf.rb
|
|
117
120
|
- app/models/keystone_ui/pages/version.rb
|
|
121
|
+
- app/views/keystone_ui/pages/manage/pages/edit.html.erb
|
|
118
122
|
- app/views/keystone_ui/pages/manage/pages/index.html.erb
|
|
119
123
|
- app/views/keystone_ui/pages/manage/pages/new.html.erb
|
|
120
124
|
- app/views/keystone_ui/pages/manage/pages/show.html.erb
|