maglevcms 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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