maglevcms 1.1.6 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/components/maglev/content/image.rb +2 -0
- data/app/controllers/concerns/maglev/fetchers_concern.rb +1 -0
- data/app/controllers/concerns/maglev/standalone_sections_concern.rb +6 -0
- data/app/controllers/maglev/sitemap_controller.rb +31 -0
- data/app/frontend/editor/assets/remixicons/ri-palette-line.svg +1 -0
- data/app/frontend/editor/components/dynamic-form/dynamic-input.vue +2 -1
- data/app/frontend/editor/components/header-nav/publish-button.vue +1 -1
- data/app/frontend/editor/components/kit/collection-item-input.vue +5 -2
- data/app/frontend/editor/components/kit/color-input/core-input.vue +85 -0
- data/app/frontend/editor/components/kit/color-input/preset-button.vue +57 -0
- data/app/frontend/editor/components/kit/color-input/preset-dropdown.vue +46 -0
- data/app/frontend/editor/components/kit/color-input.vue +43 -0
- data/app/frontend/editor/components/kit/dropdown.vue +1 -0
- data/app/frontend/editor/components/kit/index.js +2 -2
- data/app/frontend/editor/components/kit/submit-button.vue +3 -3
- data/app/frontend/editor/components/kit/tabs.vue +2 -1
- data/app/frontend/editor/components/kit/text-input.vue +2 -2
- data/app/frontend/editor/misc/utils.js +32 -2
- data/app/frontend/editor/services/api.js +2 -2
- data/app/frontend/editor/spec/__mocks__/page.js +36 -36
- data/app/frontend/editor/spec/__mocks__/section.js +29 -29
- data/app/helpers/maglev/editor_helper.rb +1 -1
- data/app/helpers/maglev/page_preview_helper.rb +5 -1
- data/app/helpers/maglev/sitemap_helper.rb +13 -0
- data/app/models/concerns/maglev/sections_concern.rb +10 -0
- data/app/models/maglev/page/path_concern.rb +8 -1
- data/app/models/maglev/section/setting.rb +5 -0
- data/app/models/maglev/site/locales_concern.rb +8 -0
- data/app/services/concerns/maglev/get_page_sections/transform_collection_item_concern.rb +8 -1
- data/app/services/maglev/app_container.rb +2 -0
- data/app/services/maglev/fetch_collection_items.rb +6 -2
- data/app/services/maglev/persist_page.rb +23 -4
- data/app/services/maglev/setup_pages.rb +41 -5
- data/app/views/maglev/api/pages/_show.json.jbuilder +1 -2
- data/app/views/maglev/sitemap/index.xml.builder +21 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20210830085101_create_maglev_page_paths.rb +1 -1
- data/db/migrate/20210906102712_add_canonical_to_pages.rb +1 -1
- data/db/migrate/20211008064437_add_locales_to_sites.rb +1 -1
- data/db/migrate/20211013210954_translate_section_content.rb +1 -1
- data/db/migrate/20211101205001_add_lock_version_to_maglev_pages.rb +1 -1
- data/db/migrate/20211116161121_better_page_path_canonical_indices.rb +1 -1
- data/db/migrate/20211124101005_fix_page_path_indices.rb +1 -1
- data/db/migrate/20211203224112_add_open_graph_tags_to_pages.rb +1 -1
- data/db/migrate/20220612092235_add_style_to_sites.rb +1 -1
- data/lib/generators/maglev/install_generator.rb +3 -2
- data/lib/generators/maglev/section_generator.rb +2 -1
- data/lib/generators/maglev/templates/section/app/theme/sections/%category%/%file_name%.yml.tt +13 -9
- data/lib/maglev/engine.rb +8 -2
- data/lib/maglev/version.rb +1 -1
- data/package.json +3 -3
- data/yarn.lock +799 -565
- metadata +14 -7
- data/app/frontend/editor/components/kit/color-picker.vue +0 -81
- /data/app/frontend/editor/assets/remixicons/{check-line.svg → ri-check-line.svg} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b23017be4bbbf4d06d99cf0f94ede1136e0e0ea343e0fbbef94edb6dc4d112d4
|
4
|
+
data.tar.gz: b6e5d107a2ea7a18091ffdd084a3fed631d0726fe845e0ac0f7100f1fc3e2b60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b579c11945ca3c28bb48f59d7efc0ea70a6d1608b7275cd507c3044cf30446bf3a342163ad9c468f0f3cf690386be137401e34f6426bbc016b4306af2d701ff
|
7
|
+
data.tar.gz: d4257dfeb8ef1a210fc9147325e4cae5517c4a5f676b7c27637353573ee9c4ba315283edc4b0c97253d06b6327ba48b4fbf25199b4247f21913482b17f53777a
|
@@ -15,6 +15,8 @@ module Maglev
|
|
15
15
|
private
|
16
16
|
|
17
17
|
def fetch_maglev_site_scoped_sections
|
18
|
+
return if within_maglev_engine?
|
19
|
+
|
18
20
|
fetch_maglev_site
|
19
21
|
fetch_maglev_theme
|
20
22
|
fetch_maglev_dummy_page
|
@@ -24,5 +26,9 @@ module Maglev
|
|
24
26
|
def fetch_maglev_dummy_page
|
25
27
|
@fetch_maglev_page = ::Maglev::Page.new(title: 'DummyPage', sections: fetch_maglev_site.sections)
|
26
28
|
end
|
29
|
+
|
30
|
+
def within_maglev_engine?
|
31
|
+
controller_path.starts_with?('maglev/')
|
32
|
+
end
|
27
33
|
end
|
28
34
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Maglev
|
4
|
+
class SitemapController < ApplicationController
|
5
|
+
include Maglev::FetchersConcern
|
6
|
+
include Maglev::ServicesConcern
|
7
|
+
include Maglev::ContentLocaleConcern
|
8
|
+
|
9
|
+
before_action :verify_request_format!
|
10
|
+
before_action :fetch_maglev_site
|
11
|
+
|
12
|
+
def index
|
13
|
+
@host = request.protocol + fetch_host
|
14
|
+
@pages = fetch_pages
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def fetch_host
|
20
|
+
request.headers['HTTP_X_MAGLEV_HOST'] || request.host
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_pages
|
24
|
+
Maglev::Page.all.visible
|
25
|
+
end
|
26
|
+
|
27
|
+
def verify_request_format!
|
28
|
+
raise ActionController::UnknownFormat, 'Sitemap is only rendered as XML' if request.format != 'xml'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C17.5222 2 22 5.97778 22 10.8889C22 13.9556 19.5111 16.4444 16.4444 16.4444H14.4778C13.5556 16.4444 12.8111 17.1889 12.8111 18.1111C12.8111 18.5333 12.9778 18.9222 13.2333 19.2111C13.5 19.5111 13.6667 19.9 13.6667 20.3333C13.6667 21.2556 12.9 22 12 22C6.47778 22 2 17.5222 2 12C2 6.47778 6.47778 2 12 2ZM10.8111 18.1111C10.8111 16.0843 12.451 14.4444 14.4778 14.4444H16.4444C18.4065 14.4444 20 12.851 20 10.8889C20 7.1392 16.4677 4 12 4C7.58235 4 4 7.58235 4 12C4 16.19 7.2226 19.6285 11.324 19.9718C10.9948 19.4168 10.8111 18.7761 10.8111 18.1111ZM7.5 12C6.67157 12 6 11.3284 6 10.5C6 9.67157 6.67157 9 7.5 9C8.32843 9 9 9.67157 9 10.5C9 11.3284 8.32843 12 7.5 12ZM16.5 12C15.6716 12 15 11.3284 15 10.5C15 9.67157 15.6716 9 16.5 9C17.3284 9 18 9.67157 18 10.5C18 11.3284 17.3284 12 16.5 12ZM12 9C11.1716 9 10.5 8.32843 10.5 7.5C10.5 6.67157 11.1716 6 12 6C12.8284 6 13.5 6.67157 13.5 7.5C13.5 8.32843 12.8284 9 12 9Z"></path></svg>
|
@@ -62,7 +62,7 @@
|
|
62
62
|
v-model="inputValue"
|
63
63
|
v-if="setting.type == 'link'"
|
64
64
|
/>
|
65
|
-
<color-
|
65
|
+
<color-input
|
66
66
|
:label="setting.label"
|
67
67
|
:name="setting.id"
|
68
68
|
v-model="inputValue"
|
@@ -80,6 +80,7 @@
|
|
80
80
|
:label="setting.label"
|
81
81
|
:name="setting.id"
|
82
82
|
v-model="inputValue"
|
83
|
+
:collection-id="options.collectionId"
|
83
84
|
v-if="setting.type == 'collection_item'"
|
84
85
|
/>
|
85
86
|
</div>
|
@@ -10,7 +10,7 @@
|
|
10
10
|
:searchEnabled="true"
|
11
11
|
:searchPlaceholder="$t(`collectionItemInput.select.searchPlaceholder`)"
|
12
12
|
:emptyLabel="$t(`collectionItemInput.select.emptyLabel`)"
|
13
|
-
:fetchList="(q) => services.collectionItem.findAll(
|
13
|
+
:fetchList="(q) => services.collectionItem.findAll(collectionId, { q })"
|
14
14
|
:clearEnabled="true"
|
15
15
|
buttonClass="h-10"
|
16
16
|
v-model="selectedCollectionItem"
|
@@ -42,18 +42,21 @@
|
|
42
42
|
</template>
|
43
43
|
|
44
44
|
<script>
|
45
|
+
import { camelizeKeys } from '@/misc/utils'
|
46
|
+
|
45
47
|
export default {
|
46
48
|
name: 'CollectionItemInput',
|
47
49
|
props: {
|
48
50
|
label: { type: String, default: 'Label' },
|
49
51
|
name: { type: String, default: 'image' },
|
50
52
|
value: { default: () => null },
|
53
|
+
collectionId: { type: String },
|
51
54
|
isFocused: { type: Boolean, default: false },
|
52
55
|
},
|
53
56
|
computed: {
|
54
57
|
selectedCollectionItem: {
|
55
58
|
get() {
|
56
|
-
return this.value
|
59
|
+
return this.value === 'any' ? null : camelizeKeys(this.value)
|
57
60
|
},
|
58
61
|
set(collectionItem) {
|
59
62
|
this.$emit('input', collectionItem ? { ...collectionItem } : null)
|
@@ -0,0 +1,85 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="relative">
|
3
|
+
<div
|
4
|
+
class="absolute left-3 top-2 w-6 h-6 rounded-sm border border-gray-200"
|
5
|
+
:class="{ 'bg-checkerboard': isTransparent }"
|
6
|
+
:style="{ 'background-color': styleRgbaColor }"></div>
|
7
|
+
|
8
|
+
<span class="absolute left-11 top-1.5 font-bold text-gray-900 text-lg">#</span>
|
9
|
+
|
10
|
+
<input
|
11
|
+
type="text"
|
12
|
+
:value="inputColor"
|
13
|
+
@input="updateInput"
|
14
|
+
class="py-2 pl-14 rounded bg-gray-100 text-gray-800 focus:outline-none focus:ring placeholder-gray-500 font-normal"
|
15
|
+
:class="{
|
16
|
+
'pr-8': hasPresets,
|
17
|
+
'pr-2': !hasPresets
|
18
|
+
}"
|
19
|
+
autocomplete="off"
|
20
|
+
minlength="4"
|
21
|
+
maxlength="8"
|
22
|
+
size="7"
|
23
|
+
/>
|
24
|
+
|
25
|
+
<preset-dropdown
|
26
|
+
v-model="updatableValue"
|
27
|
+
:presets="presets"
|
28
|
+
v-if="hasPresets"
|
29
|
+
/>
|
30
|
+
</div>
|
31
|
+
</template>
|
32
|
+
|
33
|
+
<script>
|
34
|
+
import PresetDropdown from './preset-dropdown.vue'
|
35
|
+
|
36
|
+
import { colorVariableToHex, colorVariableToRgb } from '@/misc/utils'
|
37
|
+
|
38
|
+
export default {
|
39
|
+
name: 'CoreInput',
|
40
|
+
components: { PresetDropdown },
|
41
|
+
props: {
|
42
|
+
value: { type: String },
|
43
|
+
presets: {
|
44
|
+
type: Array,
|
45
|
+
default: () => [],
|
46
|
+
},
|
47
|
+
},
|
48
|
+
computed: {
|
49
|
+
inputColor() {
|
50
|
+
return this.hexColor?.replaceAll('#', '')
|
51
|
+
},
|
52
|
+
hexColor() {
|
53
|
+
return colorVariableToHex(this.value)
|
54
|
+
},
|
55
|
+
rgbColor() {
|
56
|
+
return colorVariableToRgb(this.value)
|
57
|
+
},
|
58
|
+
styleRgbaColor() {
|
59
|
+
const color = this.isTransparent ? { r: 255, g: 255, b: 255 } : (this.rgbColor || { r: 0, g: 0, b: 0 })
|
60
|
+
return `rgba(${color.r}, ${color.g}, ${color.b}, 1)`
|
61
|
+
},
|
62
|
+
isTransparent() {
|
63
|
+
return this.value === ''
|
64
|
+
},
|
65
|
+
hasPresets() {
|
66
|
+
return this.presets && this.presets.length > 0
|
67
|
+
},
|
68
|
+
updatableValue: {
|
69
|
+
get() {
|
70
|
+
return this.value
|
71
|
+
},
|
72
|
+
set(color) {
|
73
|
+
this.$emit('input', color)
|
74
|
+
},
|
75
|
+
},
|
76
|
+
},
|
77
|
+
methods: {
|
78
|
+
updateInput(event) {
|
79
|
+
var value = event.target.value
|
80
|
+
if (value.length > 0) value = `#${value}`
|
81
|
+
this.$emit('input', value)
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
</script>
|
@@ -0,0 +1,57 @@
|
|
1
|
+
<template>
|
2
|
+
<div
|
3
|
+
class="self-center cursor-pointer p-0.5 rounded-sm"
|
4
|
+
@click="selectPreset"
|
5
|
+
>
|
6
|
+
<div
|
7
|
+
class="flex items-center justify-center w-6 h-6 rounded-sm transition transform duration-200 ease-in-out hover:scale-110 select-none text-white"
|
8
|
+
:class="{
|
9
|
+
'border border-gray-300 text-gray-800': isWhite,
|
10
|
+
'bg-checkerboard': isTransparent,
|
11
|
+
}"
|
12
|
+
:style="{ 'background-color': hexColor }"
|
13
|
+
>
|
14
|
+
<icon name="ri-check-line" size="1rem" v-if="selected" />
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
</template>
|
18
|
+
|
19
|
+
<script>
|
20
|
+
import { hexToRgb, colorVariableToHex, colorVariableToRgb } from '@/misc/utils'
|
21
|
+
|
22
|
+
export default {
|
23
|
+
name: 'PresetButton',
|
24
|
+
props: {
|
25
|
+
preset: { type: String },
|
26
|
+
value: { type: String },
|
27
|
+
},
|
28
|
+
computed: {
|
29
|
+
selected() {
|
30
|
+
return !!this.value && this.value.trim().toLowerCase() === this.hexColor
|
31
|
+
},
|
32
|
+
hexColor() {
|
33
|
+
return colorVariableToHex(this.preset)
|
34
|
+
},
|
35
|
+
rgbColor() {
|
36
|
+
return colorVariableToRgb(this.preset)
|
37
|
+
},
|
38
|
+
isWhite() {
|
39
|
+
if (!this.rgbColor) return true
|
40
|
+
return this.rgbColor.r === 255 && this.rgbColor.g === 255 && this.rgbColor.b === 255
|
41
|
+
},
|
42
|
+
isTransparent() {
|
43
|
+
return this.hexColor === ''
|
44
|
+
},
|
45
|
+
borderColor() {
|
46
|
+
if (!this.selected) return 'transparent'
|
47
|
+
let color = this.isWhite ? { r: 0, g: 0, b: 0 } : this.rgbColor
|
48
|
+
return `rgba(${color.r}, ${color.g}, ${color.b}, 0.40)`
|
49
|
+
}
|
50
|
+
},
|
51
|
+
methods: {
|
52
|
+
selectPreset() {
|
53
|
+
this.$emit('input', this.hexColor)
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
57
|
+
</script>
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<template>
|
2
|
+
<dropdown v-on="$listeners" class="absolute top-1.5 right-1">
|
3
|
+
<template v-slot:button>
|
4
|
+
<button
|
5
|
+
class="px-1 py-1 hover:text-gray-900 focus:outline-none transition-colors duration-200"
|
6
|
+
>
|
7
|
+
<icon name="ri-palette-line" size="1.25rem" />
|
8
|
+
</button>
|
9
|
+
</template>
|
10
|
+
<template v-slot:content>
|
11
|
+
<div class="flex flex-wrap gap-1">
|
12
|
+
<preset-button
|
13
|
+
v-for="preset in presets"
|
14
|
+
v-model="updatableValue"
|
15
|
+
:preset="preset"
|
16
|
+
:key="preset" />
|
17
|
+
</div>
|
18
|
+
</template>
|
19
|
+
</dropdown>
|
20
|
+
</template>
|
21
|
+
|
22
|
+
<script>
|
23
|
+
import PresetButton from './preset-button.vue'
|
24
|
+
|
25
|
+
export default {
|
26
|
+
name: 'PresetDropdown',
|
27
|
+
components: { PresetButton },
|
28
|
+
props: {
|
29
|
+
value: { type: String },
|
30
|
+
presets: {
|
31
|
+
type: Array,
|
32
|
+
default: () => [],
|
33
|
+
},
|
34
|
+
},
|
35
|
+
computed: {
|
36
|
+
updatableValue: {
|
37
|
+
get() {
|
38
|
+
return this.value
|
39
|
+
},
|
40
|
+
set(color) {
|
41
|
+
this.$emit('input', color)
|
42
|
+
},
|
43
|
+
},
|
44
|
+
}
|
45
|
+
}
|
46
|
+
</script>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="pt-1">
|
3
|
+
<label
|
4
|
+
class="flex items-center justify-between font-semibold text-gray-800"
|
5
|
+
:for="name"
|
6
|
+
v-if="showLabel"
|
7
|
+
>
|
8
|
+
<span>{{ label }}</span>
|
9
|
+
<core-input :presets="presets" v-model="updatableValue" />
|
10
|
+
</label>
|
11
|
+
</div>
|
12
|
+
</template>
|
13
|
+
|
14
|
+
<script>
|
15
|
+
import { hexToRgb } from '@/misc/utils'
|
16
|
+
|
17
|
+
import CoreInput from '@/components/kit/color-input/core-input.vue'
|
18
|
+
|
19
|
+
export default {
|
20
|
+
name: 'ColorInput',
|
21
|
+
components: { CoreInput },
|
22
|
+
props: {
|
23
|
+
label: { type: String, default: 'Label' },
|
24
|
+
name: { type: String, default: 'color' },
|
25
|
+
presets: {
|
26
|
+
type: Array,
|
27
|
+
default: () => [],
|
28
|
+
},
|
29
|
+
value: { type: String },
|
30
|
+
showLabel: { type: Boolean, default: true }
|
31
|
+
},
|
32
|
+
computed: {
|
33
|
+
updatableValue: {
|
34
|
+
get() {
|
35
|
+
return this.value
|
36
|
+
},
|
37
|
+
set(color) {
|
38
|
+
this.$emit('input', color)
|
39
|
+
},
|
40
|
+
},
|
41
|
+
},
|
42
|
+
}
|
43
|
+
</script>
|
@@ -19,7 +19,7 @@ import CheckboxInput from './checkbox-input.vue'
|
|
19
19
|
import SearchInput from './search-input.vue'
|
20
20
|
import Pagination from './pagination/index.vue'
|
21
21
|
import PageIcon from './page-icon.vue'
|
22
|
-
import
|
22
|
+
import ColorInput from './color-input.vue'
|
23
23
|
import SimpleSelect from './simple-select.vue'
|
24
24
|
import CollectionItemInput from './collection-item-input.vue'
|
25
25
|
import ListItemButton from './list-item-button.vue'
|
@@ -44,7 +44,7 @@ Vue.component('checkbox-input', CheckboxInput)
|
|
44
44
|
Vue.component('search-input', SearchInput)
|
45
45
|
Vue.component('pagination', Pagination)
|
46
46
|
Vue.component('page-icon', PageIcon)
|
47
|
-
Vue.component('color-
|
47
|
+
Vue.component('color-input', ColorInput)
|
48
48
|
Vue.component('simple-select', SimpleSelect)
|
49
49
|
Vue.component('collection-item-input', CollectionItemInput)
|
50
50
|
Vue.component('list-item-button', ListItemButton)
|
@@ -8,17 +8,17 @@
|
|
8
8
|
<span v-if="isDefaultState" data-button-label>{{ defaultLabel }}</span>
|
9
9
|
|
10
10
|
<span v-if="isInProgressState" class="flex items-center justify-center">
|
11
|
-
<icon
|
11
|
+
<icon name="ri-loader-4-line" spin color="#fff" key="progress" />
|
12
12
|
<span class="ml-2" data-button-label>{{ inProgressLabel }}</span>
|
13
13
|
</span>
|
14
14
|
|
15
15
|
<span v-if="isSuccessState" class="flex items-center justify-center">
|
16
|
-
<icon
|
16
|
+
<icon name="ri-check-line" color="#fff" key="success" />
|
17
17
|
<span class="ml-2" data-button-label>{{ successLabel }}</span>
|
18
18
|
</span>
|
19
19
|
|
20
20
|
<span v-if="isFailState" class="flex items-center justify-center">
|
21
|
-
<icon
|
21
|
+
<icon name="ri-alert-line" color="#fff" key="fail" />
|
22
22
|
<span class="ml-2" data-button-label>{{ failLabel }}</span>
|
23
23
|
</span>
|
24
24
|
</button>
|
@@ -8,8 +8,9 @@
|
|
8
8
|
v-for="(tab, index) in tabs"
|
9
9
|
:key="`tab-${index}`"
|
10
10
|
type="button"
|
11
|
-
class="
|
11
|
+
class=" py-1 pb-0 px-4 block hover:text-editor-primary focus:outline-none border-b-2 z-10"
|
12
12
|
:class="{
|
13
|
+
'text-gray-500 border-transparent': index !== currentIndex,
|
13
14
|
'text-editor-primary font-medium border-editor-primary':
|
14
15
|
index === currentIndex,
|
15
16
|
}"
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<template>
|
2
|
-
<div>
|
2
|
+
<div class="space-y-1">
|
3
3
|
<label
|
4
4
|
class="block font-semibold text-gray-800"
|
5
5
|
:for="name"
|
@@ -20,7 +20,7 @@
|
|
20
20
|
:placeholder="placeholder"
|
21
21
|
@blur="blur()"
|
22
22
|
@input="updateInput"
|
23
|
-
class="block w-full
|
23
|
+
class="block w-full py-2 px-3 rounded bg-gray-100 text-gray-800 focus:outline-none focus:ring placeholder-gray-500"
|
24
24
|
autocomplete="off"
|
25
25
|
ref="input"
|
26
26
|
/>
|
@@ -8,11 +8,28 @@ export const isBlank = (object) => {
|
|
8
8
|
}
|
9
9
|
|
10
10
|
export const camelize = (str) => {
|
11
|
+
if (!str.includes('_')) return str
|
11
12
|
return str
|
12
13
|
.toLowerCase()
|
13
14
|
.replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase())
|
14
15
|
}
|
15
16
|
|
17
|
+
export const camelizeKeys = (obj) => {
|
18
|
+
if (Array.isArray(obj)) {
|
19
|
+
return obj.map(v => camelizeKeys(v));
|
20
|
+
} else if (obj != null && obj.constructor === Object) {
|
21
|
+
return Object.keys(obj).reduce(
|
22
|
+
(result, key) => ({
|
23
|
+
...result,
|
24
|
+
[camelize(key)]: camelizeKeys(obj[key]),
|
25
|
+
}),
|
26
|
+
{},
|
27
|
+
);
|
28
|
+
}
|
29
|
+
return obj;
|
30
|
+
}
|
31
|
+
|
32
|
+
|
16
33
|
export const numberToHumanSize = (size, i18n) => {
|
17
34
|
if (isBlank(size)) return null
|
18
35
|
|
@@ -94,16 +111,29 @@ export const omitEmpty = (obj) =>
|
|
94
111
|
Object.keys(obj).forEach((key) => obj[key] === undefined && delete obj[key])
|
95
112
|
|
96
113
|
export const hexToRgb = (hex) => {
|
97
|
-
|
114
|
+
if (!hex) return null
|
115
|
+
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex.trim())
|
98
116
|
return result
|
99
117
|
? {
|
100
118
|
r: parseInt(result[1], 16),
|
101
119
|
g: parseInt(result[2], 16),
|
102
|
-
b: parseInt(result[3], 16),
|
120
|
+
b: result[3] ? parseInt(result[3], 16) : 0,
|
103
121
|
}
|
104
122
|
: null
|
105
123
|
}
|
106
124
|
|
125
|
+
export const colorVariableToHex = (variable) => {
|
126
|
+
if (!variable) return null
|
127
|
+
const color = (variable.startsWith('--')
|
128
|
+
? getComputedStyle(document.body).getPropertyValue(variable)
|
129
|
+
: variable).trim().toLowerCase()
|
130
|
+
return color === 'transparent' ? '' : color
|
131
|
+
}
|
132
|
+
|
133
|
+
export const colorVariableToRgb = (variable) => {
|
134
|
+
return hexToRgb(colorVariableToHex(variable))
|
135
|
+
}
|
136
|
+
|
107
137
|
// Static pages have absolute path ("/something") but regular pages have no leading slash
|
108
138
|
export const formatPath = (path) => {
|
109
139
|
return path[0] === '/' ? path : `/${path}`
|
@@ -18,10 +18,10 @@ const api = axios.create({
|
|
18
18
|
Accept: 'application/json',
|
19
19
|
},
|
20
20
|
transformResponse(data) {
|
21
|
-
//
|
21
|
+
// for "security" (even if the Rails API already camelcases the JSON),
|
22
|
+
// we also camelcase the response in the Editor.
|
22
23
|
// console.log('[DEBUG] API response', data)
|
23
24
|
return data ? camelcaseObjectDeep(JSON.parse(data)) : {}
|
24
|
-
// return data ? JSON.parse(data) : {} // LEGACY
|
25
25
|
},
|
26
26
|
})
|
27
27
|
|