maglevcms 1.2.2 → 1.3.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/components/maglev/content/builder.rb +3 -1
  3. data/app/components/maglev/content/checkbox.rb +7 -1
  4. data/app/components/maglev/content/void.rb +11 -0
  5. data/app/controllers/concerns/maglev/fetchers_concern.rb +2 -1
  6. data/app/controllers/concerns/maglev/rendering_concern.rb +0 -2
  7. data/app/controllers/maglev/editor_controller.rb +4 -0
  8. data/app/frontend/editor/components/dynamic-form/dynamic-input.vue +30 -10
  9. data/app/frontend/editor/components/dynamic-form/index.vue +2 -0
  10. data/app/frontend/editor/components/kit/color-input/core-input.vue +1 -1
  11. data/app/frontend/editor/components/kit/color-input.vue +1 -1
  12. data/app/frontend/editor/components/kit/divider.vue +22 -0
  13. data/app/frontend/editor/components/kit/hint.vue +12 -0
  14. data/app/frontend/editor/components/kit/index.js +4 -0
  15. data/app/frontend/editor/components/page/form/seo.vue +1 -1
  16. data/app/frontend/editor/components/section-block-pane/setting-list.vue +4 -0
  17. data/app/frontend/editor/components/section-highlighter/top-left-actions.vue +7 -1
  18. data/app/frontend/editor/components/section-list/list-item.vue +6 -1
  19. data/app/frontend/editor/components/section-pane/index.vue +2 -1
  20. data/app/frontend/editor/components/section-pane/setting-list.vue +4 -0
  21. data/app/frontend/editor/components/style-pane/index.vue +4 -0
  22. data/app/frontend/editor/locales/editor.fr.json +8 -2
  23. data/app/frontend/editor/mixins/global.js +16 -0
  24. data/app/frontend/editor/router/index.js +1 -2
  25. data/app/frontend/editor/views/content-pane.vue +2 -2
  26. data/app/models/concerns/maglev/translatable.rb +4 -0
  27. data/app/models/maglev/page.rb +6 -0
  28. data/app/models/maglev/section/setting.rb +1 -1
  29. data/app/models/maglev/site/locales_concern.rb +7 -0
  30. data/app/models/maglev/site.rb +4 -0
  31. data/app/models/maglev/theme/style_setting.rb +1 -1
  32. data/app/services/maglev/add_site_locale.rb +64 -0
  33. data/app/services/maglev/app_container.rb +1 -0
  34. data/app/services/maglev/change_site_locales.rb +1 -1
  35. data/app/services/maglev/fetch_page.rb +7 -1
  36. data/app/services/maglev/fetch_section_screenshot_path.rb +1 -1
  37. data/app/services/maglev/get_page_fullpath.rb +5 -1
  38. data/lib/maglev/engine.rb +6 -2
  39. data/lib/maglev/preview_constraint.rb +15 -1
  40. data/lib/maglev/version.rb +1 -1
  41. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 248515cbe62cad407de20c6cffc54b333b764b997051916dcc25db54744536ae
4
- data.tar.gz: 1d7b14f11bde0e7efb1eb88b8804dc5028b3d55da858e963e538c92640377b5c
3
+ metadata.gz: 0c3b9ff3b96ced9c9f3942ea0c0f26dfc29f9d82bccdb5e6077eea9cf0538c0a
4
+ data.tar.gz: 6973bba556d750a5e8caa6f80be7b57426cf4db7a919177e510c0d356645ee7c
5
5
  SHA512:
6
- metadata.gz: b3409a4c7c5712a9e7bfa0541ca7d683e3ce17ace4a0113f9eaafeb2c3ad0d9a24a5bc07717f6ec2b5942a94539ebb9312a0cf18f3f9c9ec6bf1516e9b560dce
7
- data.tar.gz: a72a1e948ef6d9ad0003c987a745959a2da567ad78d0db890e1b207f45c4a50d3c86629537bec27bd891d6f4e48f333a0cef22b9bfa3c72168ec64b9a3a5966e
6
+ metadata.gz: b7f5ba949b5a13eb50b9d3b4fcf042fad60766decb28d8ff16ee076cf4135264eac84ec6348776913d72c3f1bbd72b751576b7445db2f88b0a86e6d5165caf30
7
+ data.tar.gz: 783b36620b0733ca8387fefd08274f63a2e6c98b65e370572dc6f49f7d7db3c6ad6219c9d0f25a34027db00ca66a684963921777e0a2553fc55af3f121388468
@@ -11,7 +11,9 @@ module Maglev
11
11
  color: Maglev::Content::Color,
12
12
  select: Maglev::Content::Select,
13
13
  collection_item: Maglev::Content::CollectionItem,
14
- icon: Maglev::Content::Icon
14
+ icon: Maglev::Content::Icon,
15
+ divider: Maglev::Content::Void,
16
+ hint: Maglev::Content::Void
15
17
  }.freeze
16
18
 
17
19
  def build(scope, content, setting)
@@ -4,7 +4,7 @@ module Maglev
4
4
  module Content
5
5
  class Checkbox < Base
6
6
  def true?
7
- !!content
7
+ !!cast_content
8
8
  end
9
9
 
10
10
  def false?
@@ -14,6 +14,12 @@ module Maglev
14
14
  def to_s
15
15
  content
16
16
  end
17
+
18
+ private
19
+
20
+ def cast_content
21
+ @cast_content ||= ActiveModel::Type::Boolean.new.cast(content)
22
+ end
17
23
  end
18
24
  end
19
25
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Content
5
+ class Void < Base
6
+ def value
7
+ ''
8
+ end
9
+ end
10
+ end
11
+ end
@@ -30,7 +30,8 @@ module Maglev
30
30
  path: maglev_page_path_from_params,
31
31
  locale: content_locale,
32
32
  default_locale: default_content_locale,
33
- fallback_to_default_locale: fallback_to_default_locale
33
+ fallback_to_default_locale: fallback_to_default_locale,
34
+ only_visible: maglev_rendering_mode == :live
34
35
  )
35
36
  end
36
37
 
@@ -11,8 +11,6 @@ module Maglev
11
11
  private
12
12
 
13
13
  def render_maglev_page
14
- raise ActionController::UnknownFormat, 'Maglev renders HTML pages only' if request.format != 'html'
15
-
16
14
  fetch_maglev_page_content
17
15
 
18
16
  verify_canonical_path and return
@@ -42,5 +42,9 @@ module Maglev
42
42
  def fallback_to_default_locale
43
43
  true
44
44
  end
45
+
46
+ def maglev_rendering_mode
47
+ :editor
48
+ end
45
49
  end
46
50
  end
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div>
3
3
  <text-input
4
- :label="setting.label"
4
+ :label="label"
5
5
  :name="setting.id"
6
6
  :isFocused="isFocused"
7
7
  @blur="$emit('blur')"
@@ -13,7 +13,7 @@
13
13
  "
14
14
  />
15
15
  <textarea-input
16
- :label="setting.label"
16
+ :label="label"
17
17
  :name="setting.id"
18
18
  :isFocused="isFocused"
19
19
  :rows="options.nbRows"
@@ -24,7 +24,7 @@
24
24
  "
25
25
  />
26
26
  <rich-text-input
27
- :label="setting.label"
27
+ :label="label"
28
28
  :name="setting.id"
29
29
  :isFocused="isFocused"
30
30
  :lineBreak="options.lineBreak"
@@ -35,27 +35,27 @@
35
35
  v-if="setting.type == 'text' && options.html"
36
36
  />
37
37
  <image-input
38
- :label="setting.label"
38
+ :label="label"
39
39
  :name="setting.id"
40
40
  :isFocused="isFocused"
41
41
  v-model="inputValue"
42
42
  v-if="setting.type == 'image'"
43
43
  />
44
44
  <icon-input
45
- :label="setting.label"
45
+ :label="label"
46
46
  :name="setting.id"
47
47
  :isFocused="isFocused"
48
48
  v-model="inputValue"
49
49
  v-if="setting.type == 'icon'"
50
50
  />
51
51
  <checkbox-input
52
- :label="setting.label"
52
+ :label="label"
53
53
  :name="setting.id"
54
54
  v-model="inputValue"
55
55
  v-if="setting.type == 'checkbox'"
56
56
  />
57
57
  <link-input
58
- :label="setting.label"
58
+ :label="label"
59
59
  :name="setting.id"
60
60
  :isFocused="isFocused"
61
61
  :withText="options.withText"
@@ -63,26 +63,37 @@
63
63
  v-if="setting.type == 'link'"
64
64
  />
65
65
  <color-input
66
- :label="setting.label"
66
+ :label="label"
67
67
  :name="setting.id"
68
68
  v-model="inputValue"
69
69
  :presets="options.presets"
70
70
  v-if="setting.type == 'color'"
71
71
  />
72
72
  <simple-select
73
- :label="setting.label"
73
+ :label="label"
74
74
  :name="setting.id"
75
75
  v-model="inputValue"
76
76
  :selectOptions="options.selectOptions"
77
77
  v-if="setting.type == 'select'"
78
78
  />
79
79
  <collection-item-input
80
- :label="setting.label"
80
+ :label="label"
81
81
  :name="setting.id"
82
82
  v-model="inputValue"
83
83
  :collection-id="options.collectionId"
84
84
  v-if="setting.type == 'collection_item'"
85
85
  />
86
+
87
+ <divider
88
+ :text="label"
89
+ :withHint="options.withHint"
90
+ v-if="setting.type == 'divider'"
91
+ />
92
+
93
+ <hint
94
+ :text="label"
95
+ v-if="setting.type == 'hint'"
96
+ />
86
97
  </div>
87
98
  </template>
88
99
 
@@ -93,8 +104,17 @@ export default {
93
104
  setting: { type: Object, default: () => ({ type: 'text' }) },
94
105
  content: { type: Array, required: true },
95
106
  isFocused: { type: Boolean, default: false },
107
+ i18nScope: { type: String, required: false }
96
108
  },
97
109
  computed: {
110
+ label() {
111
+ // i18n key examples:
112
+ // - themes.simple.style.settings.main_color
113
+ // - themes.simple.sections.navbar_01.settings.title
114
+ const i18nKey = `${this.i18nScope}.${this.setting.id}`
115
+ const translation = !this.isBlank(this.i18nScope) ? this.$st(i18nKey) : null
116
+ return translation || this.setting.label
117
+ },
98
118
  options() {
99
119
  return this.setting.options
100
120
  },
@@ -7,6 +7,7 @@
7
7
  :setting="setting"
8
8
  :content="content"
9
9
  :isFocused="focusedSetting === setting.id"
10
+ :i18nScope="i18nScope"
10
11
  @blur="$emit('blur')"
11
12
  @change="onChange"
12
13
  />
@@ -24,6 +25,7 @@ export default {
24
25
  settings: { type: Array, default: () => [] },
25
26
  content: { type: Array, default: () => [] },
26
27
  focusedSetting: { type: String, default: undefined },
28
+ i18nScope: { type: String, required: false }
27
29
  },
28
30
  methods: {
29
31
  onChange(change) {
@@ -76,7 +76,7 @@ export default {
76
76
  },
77
77
  methods: {
78
78
  updateInput(event) {
79
- var value = event.target.value
79
+ var value = event.target.value.replace('#', '')
80
80
  if (value.length > 0) value = `#${value}`
81
81
  this.$emit('input', value)
82
82
  }
@@ -5,7 +5,7 @@
5
5
  :for="name"
6
6
  v-if="showLabel"
7
7
  >
8
- <span>{{ label }}</span>
8
+ <span class="pr-2">{{ label }}</span>
9
9
  <core-input :presets="presets" v-model="updatableValue" />
10
10
  </label>
11
11
  </div>
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <div :class="{ 'pt-4': withHint, 'pt-4 pb-2': !withHint }">
3
+ <div class="relative">
4
+ <div class="absolute inset-0 flex items-center" aria-hidden="true">
5
+ <div class="w-full border-t border-gray-200"></div>
6
+ </div>
7
+ <div class="relative flex justify-center">
8
+ <span class="bg-white px-3 leading-6 text-gray-800">{{ text }}</span>
9
+ </div>
10
+ </div>
11
+ </div>
12
+ </template>
13
+
14
+ <script>
15
+ export default {
16
+ name: 'Divider',
17
+ props: {
18
+ text: { type: String, default: 'Text goes here' },
19
+ withHint: { type: Boolean, default: false }
20
+ }
21
+ }
22
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div class="text-gray-600 text-sm">{{ text }}</div>
3
+ </template>
4
+
5
+ <script>
6
+ export default {
7
+ name: 'Hint',
8
+ props: {
9
+ text: { type: String, default: 'Hint goes here' },
10
+ }
11
+ }
12
+ </script>
@@ -23,6 +23,8 @@ 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'
26
+ import Divider from './divider.vue'
27
+ import Hint from './hint.vue'
26
28
 
27
29
  Vue.component('icon', Icon)
28
30
  Vue.component('v-popoper', VPopover)
@@ -48,3 +50,5 @@ Vue.component('color-input', ColorInput)
48
50
  Vue.component('simple-select', SimpleSelect)
49
51
  Vue.component('collection-item-input', CollectionItemInput)
50
52
  Vue.component('list-item-button', ListItemButton)
53
+ Vue.component('divider', Divider)
54
+ Vue.component('hint', Hint)
@@ -86,7 +86,7 @@ export default {
86
86
  return { url: this.page.ogImageUrl }
87
87
  },
88
88
  set(ogImage) {
89
- this.$emit('on-change', { ogImageUrl: ogImage.url })
89
+ this.$emit('on-change', { ogImageUrl: ogImage?.url ?? null })
90
90
  },
91
91
  },
92
92
  },
@@ -5,6 +5,7 @@
5
5
  :settings="sectionBlockSettings"
6
6
  :content="currentSectionBlockContent"
7
7
  :focusedSetting="settingId"
8
+ :i18nScope="i18nScope"
8
9
  @blur="onBlur"
9
10
  @change="updateSectionBlockContent"
10
11
  />
@@ -31,6 +32,9 @@ export default {
31
32
  this.currentSectionBlockContent,
32
33
  )
33
34
  },
35
+ i18nScope() {
36
+ return `${this.currentSectionBlockI18nScope}.settings`
37
+ }
34
38
  },
35
39
  methods: {
36
40
  ...mapActions(['updateSectionBlockContent']),
@@ -4,7 +4,7 @@
4
4
  class="bg-editor-primary text-white py-1 px-3 rounded-l-2xl text-xs flex items-center"
5
5
  :class="{ 'rounded-r-2xl': !displayMoveArrows }"
6
6
  >
7
- <span>{{ hoveredSection.name }}</span>
7
+ <span>{{ name }}</span>
8
8
  </div>
9
9
  <button
10
10
  type="button"
@@ -34,6 +34,12 @@ export default {
34
34
  hoveredSection: { type: Object },
35
35
  },
36
36
  computed: {
37
+ name() {
38
+ return this.$st(`${this.currentI18nScope}.sections.${this.sectionType}.name`) || this.hoveredSection.name
39
+ },
40
+ sectionType() {
41
+ return this.hoveredSection.definition.id
42
+ },
37
43
  displayMoveArrows() {
38
44
  return !this.currentSection && !this.hoveredSection.definition.insertAt
39
45
  },
@@ -6,7 +6,7 @@
6
6
  :to="{ name: 'editSection', params: { sectionId: section.id } }"
7
7
  class="flex items-center"
8
8
  >
9
- <span>{{ section.name | truncate(40) }}</span>
9
+ <span>{{ name | truncate(40) }}</span>
10
10
  </router-link>
11
11
  <confirmation-button @confirm="removeSection(section.id)" v-on="$listeners">
12
12
  <button
@@ -26,6 +26,11 @@ export default {
26
26
  props: {
27
27
  section: { type: Object, required: true },
28
28
  },
29
+ computed: {
30
+ name() {
31
+ return this.$st(`${this.currentI18nScope}.sections.${this.section.type}.name`) || this.section.name
32
+ }
33
+ },
29
34
  methods: {
30
35
  ...mapActions(['removeSection']),
31
36
  },
@@ -62,6 +62,7 @@ export default {
62
62
  },
63
63
  blocksLabel() {
64
64
  return (
65
+ this.$st(`${this.currentSectionI18nScope}.blocks.label`) ||
65
66
  this.currentSectionDefinition.blocksLabel ||
66
67
  this.$t('sectionPane.tabs.blocks')
67
68
  )
@@ -70,7 +71,7 @@ export default {
70
71
  return this.currentSectionDefinition.blocksPresentation === 'tree'
71
72
  ? BlockTree
72
73
  : BlockList
73
- },
74
+ }
74
75
  },
75
76
  methods: {
76
77
  findTabIndexFromRoute() {
@@ -5,6 +5,7 @@
5
5
  :settings="sectionSettings"
6
6
  :content="currentSectionContent"
7
7
  :focusedSetting="settingId"
8
+ :i18nScope="i18nScope"
8
9
  @blur="onBlur"
9
10
  @change="updateSectionContent"
10
11
  />
@@ -31,6 +32,9 @@ export default {
31
32
  this.currentSectionContent,
32
33
  )
33
34
  },
35
+ i18nScope() {
36
+ return `${this.currentSectionI18nScope}.settings`
37
+ }
34
38
  },
35
39
  methods: {
36
40
  ...mapActions(['updateSectionContent']),
@@ -5,6 +5,7 @@
5
5
  parentKey="style"
6
6
  :settings="styleSettings"
7
7
  :content="style"
8
+ :i18nScope="i18nScope"
8
9
  @change="onChange"
9
10
  />
10
11
  </div>
@@ -28,6 +29,9 @@ export default {
28
29
  styleSettings() {
29
30
  return this.currentTheme.styleSettings
30
31
  },
32
+ i18nScope() {
33
+ return `${this.currentStyleI18nScope}.settings`
34
+ }
31
35
  },
32
36
  methods: {
33
37
  ...mapActions(['previewStyle']),
@@ -24,6 +24,7 @@
24
24
  "sidebarNav": {
25
25
  "addNewSectionTooltip": "Ajouter une nouvelle section au bas de la page",
26
26
  "managePageSectionsTooltip": "Gérer les sections de la page",
27
+ "editStyleTooltip": "Modifier le style du site",
27
28
  "openImageLibraryTooltip": "Ouvrir la galerie d'images",
28
29
  "leaveEditorTooltip": "Retour vers l'application principale"
29
30
  },
@@ -103,7 +104,7 @@
103
104
  "tabs": {
104
105
  "settings": "Contenu",
105
106
  "blocks": "Blocs",
106
- "advanced": "Paramètres avancés"
107
+ "advanced": "Paramètres"
107
108
  },
108
109
  "blockList": {
109
110
  "add": "Ajouter un nouvel élément"
@@ -112,12 +113,17 @@
112
113
  "sectionBlockPane": {
113
114
  "tabs": {
114
115
  "settings": "Contenu",
115
- "advanced": "Paramètres avancés"
116
+ "advanced": "Paramètres"
116
117
  }
117
118
  },
118
119
  "themeSectionList": {
119
120
  "emptyCategory": "Aucune section"
120
121
  },
122
+ "style": {
123
+ "edit": {
124
+ "title": "Styler site"
125
+ }
126
+ },
121
127
  "imageInput": {
122
128
  "addButton": "Ajouter une image",
123
129
  "replaceButton": "Remplacer l'image",
@@ -16,9 +16,15 @@ Vue.mixin({
16
16
  currentSite() {
17
17
  return this.$store.state.site
18
18
  },
19
+ currentI18nScope() {
20
+ return `themes.${this.currentTheme.id}`
21
+ },
19
22
  currentStyle() {
20
23
  return this.$store.state.style
21
24
  },
25
+ currentStyleI18nScope() {
26
+ return `${this.currentI18nScope}.style`
27
+ },
22
28
  currentLocale() {
23
29
  return this.$store.state.locale
24
30
  },
@@ -34,6 +40,9 @@ Vue.mixin({
34
40
  currentSection() {
35
41
  return this.$store.state.section
36
42
  },
43
+ currentSectionI18nScope() {
44
+ return `${this.currentI18nScope}.sections.${this.currentSection.type}`
45
+ },
37
46
  currentSectionList() {
38
47
  return this.$store.getters.sectionList
39
48
  },
@@ -55,6 +64,9 @@ Vue.mixin({
55
64
  currentSectionBlock() {
56
65
  return this.$store.state.sectionBlock
57
66
  },
67
+ currentSectionBlockI18nScope() {
68
+ return `${this.currentSectionI18nScope}.blocks.${this.currentSectionBlock.type}`
69
+ },
58
70
  currentSectionBlockIndex() {
59
71
  return this.$store.getters.sectionBlockIndex
60
72
  },
@@ -120,5 +132,9 @@ Vue.mixin({
120
132
  closeModal() {
121
133
  ModalBus.$emit('close')
122
134
  },
135
+ $st(key) {
136
+ // console.log('$st', key, this.$te(key) ? this.$t(key) : null)
137
+ return this.$te(key) ? this.$t(key) : null
138
+ }
123
139
  },
124
140
  })
@@ -15,7 +15,7 @@ const router = new VueRouter({
15
15
  router.beforeEach((to, from, next) => {
16
16
  // The router hasn't found a component to display so get back
17
17
  // to the screen without any UI drawer opened.
18
- if (to.matched.length === 0) {
18
+ if (to.matched.length === 0)
19
19
  return next({
20
20
  name: 'editPage',
21
21
  params: {
@@ -23,7 +23,6 @@ router.beforeEach((to, from, next) => {
23
23
  locale: store.state.locale,
24
24
  },
25
25
  })
26
- }
27
26
 
28
27
  // When an user wants to edit another page or to edit the current page
29
28
  // in a different locale, the router detects it and dispatch the new
@@ -53,10 +53,10 @@ export default {
53
53
  else return null
54
54
  },
55
55
  sectionTitle() {
56
- return this.currentSectionDefinition?.name
56
+ return this.$st(`${this.currentSectionI18nScope}.name`) || this.currentSectionDefinition?.name
57
57
  },
58
58
  sectionBlockTitle() {
59
- return (
59
+ return this.$st(`${this.currentSectionI18nScope}.blocks.label`) || (
60
60
  this.currentSectionBlockDefinition?.name +
61
61
  ' ' +
62
62
  `#${this.currentSectionBlockIndex}`
@@ -11,6 +11,10 @@ module Maglev
11
11
  public_send("#{attr}_translations")
12
12
  end
13
13
 
14
+ def translate_attr_in(attr, locale, source_locale)
15
+ translations_for(attr)[locale.to_s] ||= translations_for(attr)[source_locale.to_s]
16
+ end
17
+
14
18
  class_methods do
15
19
  def order_by_translated(attr, direction)
16
20
  order(Arel.sql("#{attr}_translations->>'#{Maglev::I18n.current_locale}'") => direction)
@@ -34,5 +34,11 @@ module Maglev
34
34
  def static?
35
35
  false
36
36
  end
37
+
38
+ def translate_in(locale, source_locale)
39
+ %i[title sections seo_title meta_description og_title og_description og_image_url].each do |attr|
40
+ translate_attr_in(attr, locale, source_locale)
41
+ end
42
+ end
37
43
  end
38
44
  end
@@ -10,7 +10,7 @@ class Maglev::Section::Setting
10
10
 
11
11
  ## validations ##
12
12
  validates :id, :label, :type, :default, 'maglev/presence': true
13
- validates :type, inclusion: { in: %w[text image checkbox link color select collection_item icon] }
13
+ validates :type, inclusion: { in: %w[text image checkbox link color select collection_item icon divider hint] }
14
14
 
15
15
  ## methods ##
16
16
 
@@ -12,6 +12,13 @@ module Maglev::Site::LocalesConcern
12
12
  validates :locales, 'maglev/collection': true, length: { minimum: 1 }
13
13
  end
14
14
 
15
+ def add_locale(locale)
16
+ return nil if locale_prefixes.include?(locale.prefix.to_sym)
17
+
18
+ locales << locale
19
+ locales
20
+ end
21
+
15
22
  def default_locale
16
23
  locales.first
17
24
  end
@@ -21,5 +21,9 @@ module Maglev
21
21
  def find_section(type)
22
22
  sections&.find { |section| section['type'] == type }
23
23
  end
24
+
25
+ def translate_in(locale, source_locale)
26
+ translate_attr_in(:sections, locale, source_locale)
27
+ end
24
28
  end
25
29
  end
@@ -10,7 +10,7 @@ class Maglev::Theme::StyleSetting
10
10
 
11
11
  ## validations ##
12
12
  validates :id, :label, :type, :default, 'maglev/presence': true
13
- validates :type, inclusion: { in: %w[text color select checkbox] }
13
+ validates :type, inclusion: { in: %w[text color select checkbox divider hint] }
14
14
 
15
15
  ## methods ##
16
16
 
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ # Add a locale (instace of Maglev::Site::Locale) to a site
5
+ class AddSiteLocale
6
+ include Injectable
7
+
8
+ argument :site
9
+ argument :locale
10
+
11
+ def call
12
+ return if locale.blank? || !site.add_locale(locale)
13
+
14
+ ActiveRecord::Base.transaction do
15
+ unsafe_call
16
+ end
17
+ end
18
+
19
+ protected
20
+
21
+ def unsafe_call
22
+ # Set a default content for site_scoped sections
23
+ apply_to_site
24
+
25
+ site.save! # persist the new locale
26
+
27
+ # add a default content in the new locale to all the pages of the site
28
+ # based on the default locale. Also take care of the path.
29
+ apply_to_pages
30
+
31
+ true
32
+ end
33
+
34
+ def apply_to_site
35
+ Maglev::I18n.with_locale(locale.prefix) do
36
+ site.translate_in(locale.prefix, site.default_locale_prefix)
37
+ end
38
+ end
39
+
40
+ def apply_to_pages
41
+ resources.find_each do |page|
42
+ # the path will be the same as in the default locale
43
+ Maglev::I18n.with_locale(locale.prefix) do
44
+ page.path = default_page_path(page)
45
+ end
46
+
47
+ # set a default content which will be the same as in the default locale
48
+ page.translate_in(locale.prefix, site.default_locale_prefix)
49
+
50
+ page.save!
51
+ end
52
+ end
53
+
54
+ def default_page_path(page)
55
+ # we can't rely on page.default_path because the service can be called outside a HTTP request
56
+ # and so we don't know for sure if Maglev::I18n.default_locale has been correctly set.
57
+ page.paths.find_by(locale: site.default_locale.prefix)&.value
58
+ end
59
+
60
+ def resources
61
+ ::Maglev::Page
62
+ end
63
+ end
64
+ end
@@ -31,6 +31,7 @@ module Maglev
31
31
  dependency :persist_section_screenshot, class: Maglev::PersistSectionScreenshot,
32
32
  depends_on: %i[fetch_theme fetch_section_screenshot_path]
33
33
 
34
+ dependency :add_site_locale, class: Maglev::AddSiteLocale
34
35
  dependency :change_site_locales, class: Maglev::ChangeSiteLocales
35
36
 
36
37
  dependency :fetch_page, class: Maglev::FetchPage, depends_on: :fetch_site
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Maglev
4
4
  # Change the locales of a site.
5
- # The new locales are an array of maps with 2 required keys: :name & :prefix.
5
+ # The new locales are an array of Maglev::Site::Locale objects
6
6
  class ChangeSiteLocales
7
7
  include Injectable
8
8
 
@@ -10,6 +10,7 @@ module Maglev
10
10
  argument :locale
11
11
  argument :default_locale
12
12
  argument :fallback_to_default_locale, default: false
13
+ argument :only_visible, default: false
13
14
 
14
15
  def call
15
16
  page = fetch_page(path, locale)
@@ -20,7 +21,12 @@ module Maglev
20
21
  protected
21
22
 
22
23
  def fetch_page(path, locale)
23
- Maglev::Page.by_path(path || 'index', locale).first
24
+ page = pages.by_path(path || 'index', locale).first
25
+ !only_visible || (page&.visible? && only_visible) ? page : nil
26
+ end
27
+
28
+ def pages
29
+ Maglev::Page
24
30
  end
25
31
  end
26
32
  end
@@ -11,7 +11,7 @@ module Maglev
11
11
 
12
12
  def call
13
13
  path = "#{fetch_sections_path.call(theme: theme)}/#{section.category}/#{section.id}.jpg"
14
- absolute ? "#{Rails.root}/public/#{path}" : "/#{path}"
14
+ absolute ? Rails.root.join("public/#{path}").to_s : "/#{path}"
15
15
  end
16
16
  end
17
17
  end
@@ -44,7 +44,7 @@ module Maglev
44
44
 
45
45
  def build_fullpath(base_url, path)
46
46
  fullpath = [base_url]
47
- fullpath.push(locale) unless same_as_default_locale?
47
+ fullpath.push(locale) if prefix_by_default_locale?
48
48
  fullpath.push(path) unless path == 'index' # for SEO purpose)
49
49
  fullpath.push(nil) if fullpath == [nil] # avoid "" as the fullpath
50
50
  fullpath.join('/')
@@ -54,6 +54,10 @@ module Maglev
54
54
  @site ||= fetch_site.call
55
55
  end
56
56
 
57
+ def prefix_by_default_locale?
58
+ !same_as_default_locale?
59
+ end
60
+
57
61
  def same_as_default_locale?
58
62
  locale.to_s == default_locale.to_s
59
63
  end
data/lib/maglev/engine.rb CHANGED
@@ -54,7 +54,9 @@ module Maglev
54
54
  urls: ["/#{vite_ruby.config.public_output_dir}"],
55
55
  root: root.join(vite_ruby.config.public_dir),
56
56
  header_rules: [
57
- [:all, { "Access-Control-Allow-Origin": '*' }]
57
+ # rubocop:disable Style/StringHashKeys
58
+ [:all, { 'Access-Control-Allow-Origin' => '*' }]
59
+ # rubocop:enable Style/StringHashKeys
58
60
  ]
59
61
  else
60
62
  # mostly when running the application in production behind NGINX or APACHE
@@ -63,7 +65,9 @@ module Maglev
63
65
  urls: ["/#{vite_ruby.config.public_output_dir}"],
64
66
  root: root.join(vite_ruby.config.public_dir),
65
67
  header_rules: [
66
- [:all, { "Access-Control-Allow-Origin": '*' }]
68
+ # rubocop:disable Style/StringHashKeys
69
+ [:all, { 'Access-Control-Allow-Origin' => '*' }]
70
+ # rubocop:enable Style/StringHashKeys
67
71
  ]
68
72
  end
69
73
  end
@@ -4,6 +4,8 @@ require 'uri'
4
4
 
5
5
  module Maglev
6
6
  class PreviewConstraint
7
+ CRAWLER_USER_AGENTS = /Googlebot|Twitterbot|facebookexternalhit/o.freeze
8
+
7
9
  attr_reader :preview_host
8
10
 
9
11
  def initialize(preview_host: nil)
@@ -11,7 +13,7 @@ module Maglev
11
13
  end
12
14
 
13
15
  def matches?(request)
14
- %i[html xml].include?(request.format.symbol) && (!preview_host || preview_host == request.host)
16
+ (accepted_format?(request) || crawler?(request)) && match_host?(request)
15
17
  end
16
18
 
17
19
  protected
@@ -21,5 +23,17 @@ module Maglev
21
23
 
22
24
  URI.parse(Maglev.config.preview_host).host # make sure we get only the host here
23
25
  end
26
+
27
+ def accepted_format?(request)
28
+ %i[html xml].include?(request.format.symbol)
29
+ end
30
+
31
+ def crawler?(request)
32
+ request.format.symbol.nil? && CRAWLER_USER_AGENTS.match?(request.user_agent)
33
+ end
34
+
35
+ def match_host?(request)
36
+ !preview_host || preview_host == request.host
37
+ end
24
38
  end
25
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Maglev
4
- VERSION = '1.2.2'
4
+ VERSION = '1.3.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maglevcms
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Didier Lafforgue
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-08 00:00:00.000000000 Z
11
+ date: 2023-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jbuilder
@@ -133,6 +133,7 @@ files:
133
133
  - app/components/maglev/content/link.rb
134
134
  - app/components/maglev/content/select.rb
135
135
  - app/components/maglev/content/text.rb
136
+ - app/components/maglev/content/void.rb
136
137
  - app/components/maglev/page_component.rb
137
138
  - app/components/maglev/section_component.rb
138
139
  - app/components/maglev/tag_helper.rb
@@ -551,7 +552,9 @@ files:
551
552
  - app/frontend/editor/components/kit/color-input/preset-button.vue
552
553
  - app/frontend/editor/components/kit/color-input/preset-dropdown.vue
553
554
  - app/frontend/editor/components/kit/confirmation-button.vue
555
+ - app/frontend/editor/components/kit/divider.vue
554
556
  - app/frontend/editor/components/kit/dropdown.vue
557
+ - app/frontend/editor/components/kit/hint.vue
555
558
  - app/frontend/editor/components/kit/icon-input.vue
556
559
  - app/frontend/editor/components/kit/icon.vue
557
560
  - app/frontend/editor/components/kit/image-input.vue
@@ -730,6 +733,7 @@ files:
730
733
  - app/services/concerns/maglev/get_page_sections/transform_collection_item_concern.rb
731
734
  - app/services/concerns/maglev/get_page_sections/transform_link_concern.rb
732
735
  - app/services/concerns/maglev/get_page_sections/transform_text_concern.rb
736
+ - app/services/maglev/add_site_locale.rb
733
737
  - app/services/maglev/app_container.rb
734
738
  - app/services/maglev/change_site_locales.rb
735
739
  - app/services/maglev/clone_page.rb
@@ -845,7 +849,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
845
849
  - !ruby/object:Gem::Version
846
850
  version: '0'
847
851
  requirements: []
848
- rubygems_version: 3.2.32
852
+ rubygems_version: 3.3.26
849
853
  signing_key:
850
854
  specification_version: 4
851
855
  summary: Page builder Ruby on Rails