maglevcms 1.8.0 → 2.0.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 (158) hide show
  1. checksums.yaml +4 -4
  2. data/.yarn/install-state.gz +0 -0
  3. data/README.md +4 -4
  4. data/Rakefile +2 -1
  5. data/app/components/maglev/block_component.rb +1 -1
  6. data/app/components/maglev/content/base.rb +1 -1
  7. data/app/components/maglev/content/link.rb +4 -0
  8. data/app/components/maglev/section_component.rb +2 -2
  9. data/app/controllers/concerns/maglev/fetchers_concern.rb +3 -1
  10. data/app/controllers/concerns/maglev/resource_id_concern.rb +10 -0
  11. data/app/controllers/concerns/maglev/standalone_sections_concern.rb +6 -0
  12. data/app/controllers/concerns/maglev/{ui_locale_concern.rb → user_interface_locale_concern.rb} +1 -1
  13. data/app/controllers/maglev/api/collection_items_controller.rb +7 -0
  14. data/app/controllers/maglev/api/page_clones_controller.rb +1 -2
  15. data/app/controllers/maglev/api_controller.rb +1 -1
  16. data/app/controllers/maglev/application_controller.rb +1 -5
  17. data/app/controllers/maglev/assets/active_storage_proxy_controller.rb +17 -0
  18. data/app/controllers/maglev/assets/proxy_controller.rb +21 -0
  19. data/app/controllers/maglev/editor_controller.rb +1 -1
  20. data/app/controllers/maglev/page_preview_controller.rb +3 -3
  21. data/app/frontend/admin/controllers/screenshot_controller.js +29 -1
  22. data/app/frontend/editor/assets/remixicons/clipboard-line.svg +1 -0
  23. data/app/frontend/editor/assets/remixicons/ri-draggable.svg +1 -0
  24. data/app/frontend/editor/assets/remixicons/ri-folders-line.svg +1 -0
  25. data/app/frontend/editor/assets/remixicons/ri-more-fill.svg +1 -0
  26. data/app/frontend/editor/assets/remixicons/ri-shadow-line.svg +1 -0
  27. data/app/frontend/editor/components/errors/forbidden.vue +24 -0
  28. data/app/frontend/editor/components/errors/stale-record.vue +2 -2
  29. data/app/frontend/editor/components/header-nav/device-toggler.vue +6 -3
  30. data/app/frontend/editor/components/header-nav/index.vue +13 -42
  31. data/app/frontend/editor/components/header-nav/locale-toggler/index.vue +1 -1
  32. data/app/frontend/editor/components/header-nav/page-info.vue +41 -0
  33. data/app/frontend/editor/components/header-nav/preview-button.vue +6 -1
  34. data/app/frontend/editor/components/header-nav/separator.vue +1 -1
  35. data/app/frontend/editor/components/image-library/index.vue +26 -29
  36. data/app/frontend/editor/components/kit/collection-item-input.vue +9 -0
  37. data/app/frontend/editor/components/kit/copy-paste-button.vue +26 -0
  38. data/app/frontend/editor/components/kit/dropdown.vue +5 -0
  39. data/app/frontend/editor/components/kit/icon-button.vue +24 -0
  40. data/app/frontend/editor/components/kit/icon.vue +2 -3
  41. data/app/frontend/editor/components/kit/index.js +4 -2
  42. data/app/frontend/editor/components/kit/link-input.vue +1 -1
  43. data/app/frontend/editor/components/kit/modal.vue +1 -1
  44. data/app/frontend/editor/components/kit/page-icon.vue +1 -1
  45. data/app/frontend/editor/components/kit/rich-text-input/link-buttons.vue +1 -1
  46. data/app/frontend/editor/components/kit/select-input.vue +4 -2
  47. data/app/frontend/editor/components/link-picker/actions.vue +1 -1
  48. data/app/frontend/editor/components/link-picker/index.vue +1 -1
  49. data/app/frontend/editor/components/link-picker/page.vue +2 -0
  50. data/app/frontend/editor/components/page/form/main.vue +1 -1
  51. data/app/frontend/editor/components/page/list/actions-button.vue +159 -0
  52. data/app/frontend/editor/components/page/list/index.vue +15 -24
  53. data/app/frontend/editor/components/page/list/list-item.vue +10 -61
  54. data/app/frontend/editor/components/section-highlighter/index.vue +47 -46
  55. data/app/frontend/editor/components/section-list/add-button.vue +39 -0
  56. data/app/frontend/editor/components/section-list/index.vue +30 -15
  57. data/app/frontend/editor/components/section-list/list-item.vue +23 -8
  58. data/app/frontend/editor/components/section-pane/block-list/index.vue +1 -0
  59. data/app/frontend/editor/components/section-pane/block-list/list-item.vue +9 -6
  60. data/app/frontend/editor/components/section-pane/block-tree/new-nested-block-button.vue +2 -2
  61. data/app/frontend/editor/components/section-pane/block-tree/tree-node.vue +2 -2
  62. data/app/frontend/editor/components/sidebar-nav/index.vue +102 -0
  63. data/app/frontend/editor/components/sidebar-nav/link.vue +60 -0
  64. data/app/frontend/editor/components/theme-section-list/category.vue +47 -0
  65. data/app/frontend/editor/components/theme-section-list/index.vue +7 -31
  66. data/app/frontend/editor/components/theme-section-list/list-item.vue +10 -3
  67. data/app/frontend/editor/design/components/modal.scss +1 -1
  68. data/app/frontend/editor/design/components/tooltip.scss +6 -0
  69. data/app/frontend/editor/design/transitions.scss +1 -1
  70. data/app/frontend/editor/layouts/app.vue +14 -2
  71. data/app/frontend/editor/layouts/default.vue +2 -2
  72. data/app/frontend/editor/layouts/slide-pane.vue +1 -1
  73. data/app/frontend/editor/locales/editor.ar.json +265 -0
  74. data/app/frontend/editor/locales/editor.en.json +23 -15
  75. data/app/frontend/editor/locales/editor.es.json +18 -10
  76. data/app/frontend/editor/locales/editor.fr.json +28 -13
  77. data/app/frontend/editor/locales/editor.pt-BR.json +18 -10
  78. data/app/frontend/editor/locales/index.js +3 -0
  79. data/app/frontend/editor/mixins/error-modal.js +7 -2
  80. data/app/frontend/editor/mixins/preview-transformation.js +0 -1
  81. data/app/frontend/editor/plugins/i18n.js +2 -17
  82. data/app/frontend/editor/services/__tests__/section.spec.js +6 -0
  83. data/app/frontend/editor/services/collection-item.js +8 -0
  84. data/app/frontend/editor/services/live-preview.js +11 -1
  85. data/app/frontend/editor/services/page.js +1 -1
  86. data/app/frontend/editor/services/section.js +30 -0
  87. data/app/frontend/editor/services/site.js +4 -4
  88. data/app/frontend/editor/services/theme.js +1 -0
  89. data/app/frontend/editor/spec/__mocks__/page.js +3 -1
  90. data/app/frontend/editor/spec/__mocks__/services.js +1 -0
  91. data/app/frontend/editor/store/__tests__/getters.spec.js +56 -1
  92. data/app/frontend/editor/store/actions/index.js +1 -0
  93. data/app/frontend/editor/store/actions/site.js +8 -4
  94. data/app/frontend/editor/store/getters.js +11 -0
  95. data/app/frontend/editor/views/page-preview.vue +2 -1
  96. data/app/frontend/images/favicon.svg +11 -0
  97. data/app/frontend/images/logo.svg +14 -0
  98. data/app/frontend/live-preview-client/iframe-decorator.js +20 -17
  99. data/app/frontend/live-preview-client/message.js +1 -0
  100. data/app/frontend/live-preview-client/rails.js +12 -2
  101. data/app/helpers/maglev/application_helper.rb +7 -2
  102. data/app/helpers/maglev/editor_helper.rb +12 -5
  103. data/app/models/concerns/maglev/sections_concern.rb +4 -0
  104. data/app/models/maglev/asset.rb +13 -14
  105. data/app/models/maglev/page/path_concern.rb +23 -8
  106. data/app/models/maglev/page.rb +17 -18
  107. data/app/models/maglev/page_path.rb +18 -18
  108. data/app/models/maglev/section/setting.rb +1 -1
  109. data/app/models/maglev/section.rb +18 -9
  110. data/app/models/maglev/setting_types/link.rb +1 -1
  111. data/app/models/maglev/site.rb +13 -24
  112. data/app/services/maglev/app_container.rb +2 -2
  113. data/app/services/maglev/extract_locale.rb +3 -2
  114. data/app/services/maglev/fetch_page.rb +2 -0
  115. data/app/services/maglev/fetch_theme_layout.rb +2 -0
  116. data/app/services/maglev/get_page_fullpath.rb +1 -2
  117. data/app/services/maglev/remove_section_type.rb +50 -0
  118. data/app/services/maglev/rename_section_type.rb +57 -0
  119. data/app/services/maglev/search_pages.rb +2 -1
  120. data/app/views/layouts/maglev/admin/application.html.erb +1 -1
  121. data/app/views/maglev/admin/sections/previews/show.html.erb +16 -1
  122. data/app/views/maglev/api/collection_items/show.json.jbuilder +7 -0
  123. data/app/views/maglev/api/page_clones/create.json.jbuilder +3 -0
  124. data/app/views/maglev/editor/show.html.erb +6 -5
  125. data/config/locales/activerecord.ar.yml +13 -0
  126. data/config/locales/activerecord.en.yml +8 -2
  127. data/config/locales/activerecord.es.yml +4 -0
  128. data/config/locales/activerecord.fr.yml +6 -2
  129. data/config/locales/activerecord.pt-BR.yml +13 -0
  130. data/config/routes.rb +3 -4
  131. data/db/migrate/20200831101942_create_maglev_section_content.rb +10 -2
  132. data/db/migrate/20210819092740_switch_to_localized_page_fields.rb +13 -3
  133. data/db/migrate/20211008064437_add_locales_to_sites.rb +7 -1
  134. data/db/migrate/20211013210954_translate_section_content.rb +16 -2
  135. data/db/migrate/20211203224112_add_open_graph_tags_to_pages.rb +9 -3
  136. data/db/migrate/20220612092235_add_style_to_sites.rb +5 -1
  137. data/lib/commands/maglev/change_site_locales_command.rb +61 -0
  138. data/lib/commands/maglev/create_site_command.rb +28 -0
  139. data/lib/commands/maglev/sections/remove_command.rb +48 -0
  140. data/lib/commands/maglev/sections/rename_command.rb +49 -0
  141. data/lib/generators/maglev/templates/install/config/initializers/maglev.rb +4 -1
  142. data/lib/generators/maglev/templates/theme/app/views/theme/layout.html.erb.tt +8 -2
  143. data/lib/maglev/errors.rb +1 -0
  144. data/lib/maglev/preview_constraint.rb +5 -1
  145. data/lib/maglev/theme_filesystem_loader.rb +7 -0
  146. data/lib/maglev/version.rb +1 -1
  147. data/lib/maglev.rb +13 -3
  148. data/lib/tasks/maglev/icons.rake +123 -0
  149. data/lib/tasks/maglev/vite.rake +62 -0
  150. data/lib/tasks/maglev_tasks.rake +9 -107
  151. data/tailwind.config.js +1 -0
  152. metadata +48 -33
  153. data/app/controllers/maglev/assets_controller.rb +0 -10
  154. data/app/frontend/editor/components/kit/list-item-button.vue +0 -16
  155. data/app/frontend/editor/components/sidebar-nav.vue +0 -125
  156. data/app/frontend/editor/plugins/maglev_dummy.js +0 -2
  157. data/app/frontend/images/favicon.png +0 -0
  158. data/app/frontend/images/logo.png +0 -0
@@ -2,7 +2,7 @@
2
2
  "headerNav": {
3
3
  "pages": "Pages :",
4
4
  "pageSettings": "Paramètres page",
5
- "previewSite": "Prévisualiser site",
5
+ "previewSite": "Prévisualiser",
6
6
  "previewToggler": {
7
7
  "button": "Voir site",
8
8
  "draftLink": "Version brouillon",
@@ -22,7 +22,7 @@
22
22
  }
23
23
  },
24
24
  "sidebarNav": {
25
- "addNewSectionTooltip": "Ajouter une nouvelle section au bas de la page",
25
+ "listPagesTooltip": "Gérer les pages de votre site",
26
26
  "managePageSectionsTooltip": "Gérer les sections de la page",
27
27
  "editStyleTooltip": "Modifier le style du site",
28
28
  "openImageLibraryTooltip": "Ouvrir la galerie d'images",
@@ -43,14 +43,16 @@
43
43
  "title": "Liste des pages",
44
44
  "subTitle": "Gérer les pages de votre site",
45
45
  "newButton": "Nouvelle page",
46
- "searchPlaceholder": "Tapez un titre de page...",
47
- "item": {
48
- "edit": "Éditer les paramètres",
49
- "clone": "Dupliquer",
50
- "hide": "Cacher",
51
- "show": "Montrer",
52
- "delete": "Supprimer"
53
- }
46
+ "searchPlaceholder": "Tapez votre recherche ici..."
47
+ },
48
+ "actions": {
49
+ "edit": "Modifier les paramètres",
50
+ "clone": "Dupliquer",
51
+ "hide": "Masquer",
52
+ "show": "Afficher",
53
+ "delete": "Supprimer",
54
+ "copyUrlToClipboard": "Copier l'URL",
55
+ "copyUrlToClipboardSuccess": "Copié !"
54
56
  },
55
57
  "new": {
56
58
  "title": "Créer une nouvelle page",
@@ -97,7 +99,8 @@
97
99
  },
98
100
  "listPane": {
99
101
  "title": "Organiser les sections",
100
- "empty": "Vous n'avez aucune section."
102
+ "empty": "Vous n'avez aucune section.",
103
+ "addButton": "Ajouter une nouvelle section"
101
104
  }
102
105
  },
103
106
  "sectionPane": {
@@ -113,6 +116,7 @@
113
116
  "sectionBlockPane": {
114
117
  "tabs": {
115
118
  "settings": "Contenu",
119
+ "blocks": "Blocs",
116
120
  "advanced": "Paramètres"
117
121
  }
118
122
  },
@@ -121,7 +125,13 @@
121
125
  },
122
126
  "style": {
123
127
  "edit": {
124
- "title": "Styler site"
128
+ "title": "Styler votre site",
129
+ "submitButton": {
130
+ "default": "Mettre à jour le style",
131
+ "inProgress": "Mise à jour...",
132
+ "success": "Style mis à jour !",
133
+ "fail": "Échec !"
134
+ }
125
135
  }
126
136
  },
127
137
  "imageInput": {
@@ -228,7 +238,12 @@
228
238
  "staleRecord": {
229
239
  "title": "Nous sommes désolés 🙇",
230
240
  "message": "Nous n'avons pu sauvegarder vos modifications car de plus récentes viennent d'être effectuées par une autre personne.",
231
- "button": "Recharger la page"
241
+ "button": "Veuillez recharger votre page"
242
+ },
243
+ "forbidden": {
244
+ "title": "Nous sommes désolés 🙇",
245
+ "message": "Vous n'êtes pas autorisé à effectuer cette action. Veuillez contacter votre administrateur.",
246
+ "button": "Fermer"
232
247
  }
233
248
  },
234
249
  "support": {
@@ -22,7 +22,7 @@
22
22
  }
23
23
  },
24
24
  "sidebarNav": {
25
- "addNewSectionTooltip": "Adicionar uma nova seção na parte inferior da página",
25
+ "listPagesTooltip": "Gerenciar as páginas do seu site",
26
26
  "managePageSectionsTooltip": "Reordenar / excluir as seções da página",
27
27
  "editStyleTooltip": "Alterar o estilo do seu site",
28
28
  "openImageLibraryTooltip": "Abrir a galeria de imagens",
@@ -43,14 +43,16 @@
43
43
  "title": "Lista de páginas",
44
44
  "subTitle": "Gerenciar as páginas do seu site",
45
45
  "newButton": "Nova página",
46
- "searchPlaceholder": "Digite sua pesquisa aqui...",
47
- "item": {
48
- "edit": "Editar configurações da página",
49
- "clone": "Clonar página",
50
- "hide": "Ocultar página",
51
- "show": "Mostrar página",
52
- "delete": "Excluir página"
53
- }
46
+ "searchPlaceholder": "Digite sua pesquisa aqui..."
47
+ },
48
+ "actions": {
49
+ "edit": "Editar configurações",
50
+ "clone": "Clonar",
51
+ "hide": "Ocultar",
52
+ "show": "Mostrar",
53
+ "delete": "Excluir",
54
+ "copyUrlToClipboard": "Copiar URL",
55
+ "copyUrlToClipboardSuccess": "Copiado!"
54
56
  },
55
57
  "new": {
56
58
  "title": "Criar uma nova página",
@@ -97,7 +99,8 @@
97
99
  },
98
100
  "listPane": {
99
101
  "title": "Organizar seções",
100
- "empty": "Não há seções."
102
+ "empty": "Não há seções.",
103
+ "addButton": "Adicionar uma nova seção"
101
104
  }
102
105
  },
103
106
  "sectionPane": {
@@ -236,6 +239,11 @@
236
239
  "title": "Pedimos desculpas 🙇",
237
240
  "message": "Não conseguimos salvar suas alterações porque outra pessoa atualizou o conteúdo no intervalo de tempo.",
238
241
  "button": "Por favor, recarregue a página"
242
+ },
243
+ "forbidden": {
244
+ "title": "Pedimos desculpas 🙇",
245
+ "message": "Você não tem permissão para realizar esta ação. Consulte seu administrador.",
246
+ "button": "Fechar"
239
247
  }
240
248
  },
241
249
  "support": {
@@ -2,9 +2,11 @@ import EditorEN from './editor.en.json'
2
2
  import EditorES from './editor.es.json'
3
3
  import EditorFR from './editor.fr.json'
4
4
  import EditorPTBR from './editor.pt-BR.json'
5
+ import EditorAR from './editor.ar.json'
5
6
  import { deepMerge } from '@/misc/utils'
6
7
 
7
8
  const overriddenEN = window.customTranslations?.en ?? {}
9
+ const overriddenAR = window.customTranslations?.ar ?? {}
8
10
  const overriddenES = window.customTranslations?.es ?? {}
9
11
  const overriddenFR = window.customTranslations?.fr ?? {}
10
12
  const overriddenPTBR =
@@ -17,4 +19,5 @@ export default {
17
19
  es: deepMerge(EditorES, overriddenES),
18
20
  fr: deepMerge(EditorFR, overriddenFR),
19
21
  'pt-BR': deepMerge(EditorPTBR, overriddenPTBR),
22
+ ar: deepMerge(EditorAR, overriddenAR),
20
23
  }
@@ -1,4 +1,5 @@
1
- import StaleRecord from '@/components/errors/stale-record.vue'
1
+ import StaleRecordModal from '@/components/errors/stale-record.vue'
2
+ import ForbiddenModal from '@/components/errors/forbidden.vue'
2
3
 
3
4
  export default {
4
5
  methods: {
@@ -7,9 +8,13 @@ export default {
7
8
 
8
9
  switch (errorType) {
9
10
  case 'staleRecord':
10
- ModalComponent = StaleRecord
11
+ ModalComponent = StaleRecordModal
12
+ break
13
+ case 'forbidden':
14
+ ModalComponent = ForbiddenModal
11
15
  break
12
16
  default:
17
+ console.warn("Unknown errorType:", errorType)
13
18
  return // unknown error type
14
19
  }
15
20
 
@@ -9,7 +9,6 @@ export default {
9
9
  previewPaneMaxWidth() {
10
10
  const sectionPaneWidth =
11
11
  document.querySelector('.slide-pane')?.offsetWidth || 0
12
- console.log('previewPaneMaxWidth', this.windowWidth, sectionPaneWidth)
13
12
  return this.windowWidth - sectionPaneWidth
14
13
  },
15
14
  previewScaleRatio() {
@@ -4,24 +4,9 @@ import messages from '@/locales'
4
4
 
5
5
  Vue.use(VueI18n)
6
6
 
7
- const AVAILABLE_LOCALES = ['en', 'fr']
8
- var locale = 'en'
9
-
10
- if (document.documentElement.lang) {
11
- // fetch the local from the HTML tag
12
- locale = document.documentElement.lang
13
- } else {
14
- // try to fetch the browser locale
15
- const language = navigator.languages[0]
16
- if (language) {
17
- locale = language.split('-')[0]
18
- if (AVAILABLE_LOCALES.indexOf(locale) === -1) locale = null
19
- }
20
- }
21
-
22
7
  const i18n = new VueI18n({
23
- locale,
24
- fallbackLocale: AVAILABLE_LOCALES[0],
8
+ locale: window.uiLocale,
9
+ fallbackLocale: 'en',
25
10
  messages,
26
11
  })
27
12
 
@@ -30,4 +30,10 @@ describe('SectionService', () => {
30
30
  ])
31
31
  })
32
32
  })
33
+ describe('#getSectionLabel', () => {
34
+ it('returns the label of the section', () => {
35
+ const label = service.getSectionLabel(simpleContentSection, theme.sections[0])
36
+ expect(label).toEqual('preTitle')
37
+ })
38
+ })
33
39
  })
@@ -9,4 +9,12 @@ export default (api) => ({
9
9
  .get(`/collections/${collectionId}`, options)
10
10
  .then(({ data }) => data)
11
11
  },
12
+ findOne: (collectionId, id) => {
13
+ console.log(
14
+ `[CollectionItem] Fetching the item ${id} of ${collectionId}`,
15
+ )
16
+ return api
17
+ .get(`/collections/${collectionId}/${id}`)
18
+ .then(({ data }) => data)
19
+ },
12
20
  })
@@ -26,6 +26,12 @@ export const removeSection = (sectionId) => {
26
26
  postMessage('section:remove', { sectionId })
27
27
  }
28
28
 
29
+ export const pingSection = (sectionId) => {
30
+ if (!sectionId) return
31
+ postMessage('section:ping', { sectionId })
32
+ }
33
+
34
+
29
35
  // === Block related actions ===
30
36
  export const addBlock = (content, section, sectionBlock) => {
31
37
  postMessage('block:add', { content, section, sectionBlock })
@@ -43,6 +49,10 @@ export const removeBlock = (content, section, sectionBlockId) => {
43
49
  postMessage('block:remove', { content, section, sectionBlockId })
44
50
  }
45
51
 
52
+ export const simulateFakeScroll = () => {
53
+ notifyScrolling(null)
54
+ }
55
+
46
56
  // === Other actions ===
47
57
 
48
58
  export const updateStyle = (content, style) => {
@@ -113,7 +123,7 @@ const openSettingPane = (name, params) => {
113
123
  })
114
124
  }
115
125
 
116
- const notifyScrolling = (boundingRect) => {
126
+ export const notifyScrolling = (boundingRect) => {
117
127
  const event = new CustomEvent('maglev:preview:scroll', {
118
128
  detail: { boundingRect },
119
129
  })
@@ -89,7 +89,7 @@ export default (api) => ({
89
89
 
90
90
  clone: (id) => {
91
91
  console.log('[PageService] Cloning page #', id)
92
- return api.post(`/pages/${id}/clones`, {})
92
+ return api.post(`/pages/${id}/clones`, {}).then(({ data }) => data)
93
93
  },
94
94
 
95
95
  destroy: (id) => {
@@ -146,6 +146,36 @@ const buildDefaultBlocks = (definition) => {
146
146
  return blocks
147
147
  }
148
148
 
149
+ export const getSectionLabel = (section, definition) => {
150
+ let label = null
151
+
152
+ definition.settings.some((setting) => {
153
+ const value = section.settings.find(
154
+ (contentSetting) => contentSetting.id === setting.id,
155
+ )?.value
156
+
157
+ if (value === undefined) return false
158
+
159
+ switch (setting.type) {
160
+ case 'text':
161
+ const doc = new DOMParser().parseFromString(value.replace(/<br\/?>/g, ' '), 'text/html')
162
+ label = doc.body.textContent
163
+ break
164
+ case 'link':
165
+ if (!isBlank(value?.text)) label = value.text
166
+ break
167
+ case 'collection_item':
168
+ if (!isBlank(value?.label)) label = value.label
169
+ break
170
+ default:
171
+ break
172
+ }
173
+
174
+ return !!label
175
+ })
176
+ return label
177
+ }
178
+
149
179
  export const getBlockLabel = (block, definition, index) => {
150
180
  let label, image
151
181
  definition.settings.forEach((setting) => {
@@ -12,10 +12,10 @@ export default (api) => ({
12
12
  return response.headers['lock-version']
13
13
  })
14
14
  },
15
- publish() {
16
- return api.post(`/publication`).then(({ data }) => data)
15
+ publish({ pageId }) {
16
+ return api.post(`/publication`, { page_id: pageId }).then(({ data }) => data)
17
17
  },
18
- getLastPublication() {
19
- return api.get(`/publication`).then(({ data }) => data)
18
+ getLastPublication({ pageId }) {
19
+ return api.get('/publication', { params: { page_id: pageId }}).then(({ data }) => data)
20
20
  },
21
21
  })
@@ -1,5 +1,6 @@
1
1
  export const buildCategories = (theme) => {
2
2
  return theme.sectionCategories.map((category) => ({
3
+ id: category.id,
3
4
  name: category.name,
4
5
  children: theme.sections
5
6
  .filter((section) => section.category == category.id)
@@ -204,6 +204,7 @@ export const page = {
204
204
  ogDescription: null,
205
205
  ogImageUrl: null,
206
206
  previewUrl: '/maglev/preview',
207
+ liveUrl: '/',
207
208
  sectionNames: [
208
209
  { id: 'GrYZW-VP', name: 'Navbar 01' },
209
210
  { id: '8hKSujtd', name: 'Content #1' },
@@ -418,13 +419,14 @@ export const normalizedPage = {
418
419
  ogDescription: null,
419
420
  ogImageUrl: null,
420
421
  previewUrl: '/maglev/preview',
422
+ liveUrl: '/',
421
423
  sectionNames: [
422
424
  { id: 'GrYZW-VP', name: 'Navbar 01' },
423
425
  { id: '8hKSujtd', name: 'Content #1' },
424
426
  { id: 'xM6f-kyh', name: 'List #1' },
425
427
  ],
426
428
  lockVersion: 1,
427
- translated: true,
429
+ translated: true,
428
430
  },
429
431
  },
430
432
  },
@@ -39,6 +39,7 @@ const sectionService = {
39
39
  build: vi.fn(),
40
40
  getSettings: vi.fn(),
41
41
  buildDefaultBlock: vi.fn(),
42
+ getSectionLabel: vi.fn(),
42
43
  }
43
44
 
44
45
  const blockService = {
@@ -1,5 +1,5 @@
1
1
  import Vuex from 'vuex'
2
- import { vi } from 'vitest'
2
+ import { describe, vi } from 'vitest'
3
3
  import { createLocalVue } from '@vue/test-utils'
4
4
  import defaultState from '@/store/default-state'
5
5
  import buildGetters from '@/store/getters'
@@ -29,6 +29,58 @@ describe('Getters', () => {
29
29
  store.commit('SET_SITE', site)
30
30
  })
31
31
 
32
+ describe('#currentPagePath', () => {
33
+ let freshNormalizedPage = null
34
+ beforeEach(() => {
35
+ freshNormalizedPage = structuredClone(normalizedPage)
36
+ mockedServices.page.normalize = vi.fn(() => freshNormalizedPage)
37
+ })
38
+ describe('Given this is the home page', () => {
39
+ it('returns the path of the page', () => {
40
+ store.commit('SET_PAGE', page)
41
+ expect(store.getters.currentPagePath).toStrictEqual('/index')
42
+ })
43
+ })
44
+ describe('Given this is a random page', () => {
45
+ it('returns the path of the page', () => {
46
+ freshNormalizedPage.entities.page['1'].path = '/bonjour'
47
+ freshNormalizedPage.entities.page['1'].liveUrl = '/fr/bonjour'
48
+ store.commit('SET_PAGE', page)
49
+ expect(store.getters.currentPagePath).toStrictEqual('/fr/bonjour')
50
+ })
51
+ })
52
+ describe('Given the liveUrl contains the domain name', () => {
53
+ it('returns the path of the page', () => {
54
+ freshNormalizedPage.entities.page['1'].liveUrl = 'https://example.com:8080/fr'
55
+ freshNormalizedPage.entities.page['1'].path = 'index'
56
+ store.commit('SET_PAGE', page)
57
+ expect(store.getters.currentPagePath).toStrictEqual('/fr/index')
58
+ })
59
+ })
60
+ })
61
+
62
+ describe('#currentPageUrl', () => {
63
+ let freshNormalizedPage = null
64
+ beforeEach(() => {
65
+ freshNormalizedPage = structuredClone(normalizedPage)
66
+ mockedServices.page.normalize = vi.fn(() => freshNormalizedPage)
67
+ })
68
+ describe('Given the page live URL is not prefixed with the base URL', () => {
69
+ it('returns the url of the page', () => {
70
+ freshNormalizedPage.entities.page['1'].liveUrl = '/hello-world'
71
+ store.commit('SET_PAGE', page)
72
+ expect(store.getters.currentPageUrl).toStrictEqual('http://localhost:3000/hello-world')
73
+ })
74
+ })
75
+ describe('Given the page live URL is prefixed with the base URL', () => {
76
+ it('returns the url of the page', () => {
77
+ freshNormalizedPage.entities.page['1'].liveUrl = 'https://example.com:8080/fr'
78
+ store.commit('SET_PAGE', page)
79
+ expect(store.getters.currentPageUrl).toStrictEqual('https://example.com:8080/fr')
80
+ })
81
+ })
82
+ })
83
+
32
84
  describe('#content', () => {
33
85
  it('returns the content of the sections for the page', () => {
34
86
  mockedServices.page.denormalize = vi.fn(() => page)
@@ -83,18 +135,21 @@ describe('Getters', () => {
83
135
  id: 'GrYZW-VP',
84
136
  type: 'navbar_01',
85
137
  name: 'Navbar 01',
138
+ label: undefined,
86
139
  viewportFixedPosition: false,
87
140
  },
88
141
  {
89
142
  id: '8hKSujtd',
90
143
  type: 'content_01',
91
144
  name: 'Content #1',
145
+ label: undefined,
92
146
  viewportFixedPosition: false,
93
147
  },
94
148
  {
95
149
  id: 'xM6f-kyh',
96
150
  type: 'list_01',
97
151
  name: 'List #1',
152
+ label: undefined,
98
153
  viewportFixedPosition: false,
99
154
  },
100
155
  ])
@@ -6,6 +6,7 @@ import buildSectionBlockActions from './section-block'
6
6
  export default (services) => ({
7
7
  setDevice({ commit }, value) {
8
8
  commit('SET_DEVICE', value)
9
+ commit('SET_HOVERED_SECTION', null)
9
10
  },
10
11
  setTheme({ commit }, theme) {
11
12
  commit('SET_THEME', theme)
@@ -7,15 +7,19 @@ export default (services) => ({
7
7
  commit('SET_STYLE', style)
8
8
  })
9
9
  },
10
- loadPublishButtonState({ commit }) {
10
+ loadPublishButtonState({ state, commit }) {
11
11
  services.site
12
- .getLastPublication()
12
+ .getLastPublication({ pageId: state.page.id })
13
13
  .then((data) => commit('SET_PUBLISH_BUTTON_STATE', data))
14
14
  },
15
- async publishSite({ commit }) {
15
+ async publishSite({ state, commit }) {
16
16
  services.site
17
- .publish()
17
+ .publish({ pageId: state.page.id })
18
18
  .then((data) => commit('SET_PUBLISH_BUTTON_STATE', data))
19
+ .catch(({ response: { status } }) => {
20
+ console.log('[Maglev] could not publish the page', status)
21
+ if (status === 403) commit('OPEN_ERROR_MODAL', 'forbidden')
22
+ })
19
23
  },
20
24
  pollLastPublication({ dispatch }) {
21
25
  dispatch('loadPublishButtonState')
@@ -1,4 +1,14 @@
1
+ import { isBlank } from '@/misc/utils'
2
+
1
3
  export default (services) => ({
4
+ currentPagePath: ({ page }) => {
5
+ const nakedPath = page.liveUrl.startsWith('http') ? new URL(page.liveUrl).pathname : page.liveUrl
6
+ return page.path === 'index' ? `${nakedPath}/index`.replace('//', '/') : nakedPath
7
+ },
8
+ currentPageUrl: ({ page }) => {
9
+ if (page.liveUrl.startsWith('http')) return page.liveUrl
10
+ return new URL(page.liveUrl, location.origin).toString()
11
+ },
2
12
  sectionList: (
3
13
  { page, sections, sectionBlocks },
4
14
  { sectionDefinition: getSectiondefinition },
@@ -14,6 +24,7 @@ export default (services) => ({
14
24
  id: sectionContent.id,
15
25
  type: sectionContent['type'],
16
26
  name: sectionDefinition.name,
27
+ label: services.section.getSectionLabel(sectionContent, sectionDefinition),
17
28
  viewportFixedPosition: !!sectionDefinition.viewportFixedPosition,
18
29
  }
19
30
  })
@@ -19,6 +19,7 @@
19
19
  v-if="currentPage"
20
20
  >
21
21
  <div
22
+ id="iframe-wrapper"
22
23
  class="transition-all duration-100 ease-in-out"
23
24
  :class="{ [deviceClass]: true, hidden: isPageEmpty }"
24
25
  :style="{ opacity: previewReady ? 1 : 0 }"
@@ -64,7 +65,7 @@
64
65
  >
65
66
  <template v-slot:icon>
66
67
  <uikit-icon
67
- name="add-box-line"
68
+ name="ri-stack-line"
68
69
  size="1.5rem"
69
70
  class="mx-1 text-black"
70
71
  />
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <ns0:svg height="314px" version="1.1" viewBox="0 0 464 314" width="464px" xmlns:ns0="http://www.w3.org/2000/svg">
3
+ <ns0:defs>
4
+
5
+ </ns0:defs>
6
+ <ns0:g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1">
7
+ <ns0:g fill="#000000" id="maglev-3">
8
+ <ns0:path d="M454.3,164.08 L419.43,88.31 C394.87,34.93 341.49,0.74 282.74,0.74 L103.98,0.74 C92.95,0.74 84.01,9.68 84.01,20.71 C84.01,31.74 92.95,40.68 103.98,40.68 L229.29,40.68 C229.29,40.68 219.11,64.98 219.11,64.98 C212.3,81.22 196.41,91.79 178.8,91.79 L20.74,91.79 C9.71,91.79 0.77,100.73 0.77,111.76 C0.77,122.79 9.71,131.73 20.74,131.73 L191.14,131.73 C191.14,131.73 180.96,156.04 180.96,156.04 C174.15,172.28 158.26,182.85 140.65,182.85 L52.82,182.85 C41.79,182.85 32.85,191.79 32.85,202.82 C32.85,213.85 41.79,222.79 52.82,222.79 L414.89,222.79 C418.73,222.79 421.53,226.58 420.25,230.19 C417.74,237.29 413.97,244.05 408.92,250.15 C396.11,265.63 376.56,273.93 356.46,273.93 L126,273.93 C114.97,273.93 106.03,282.87 106.03,293.9 C106.03,304.93 114.97,313.87 126,313.87 L355.88,313.87 C390.23,313.87 423.3,298.4 443.55,270.65 C466.4,239.34 470.31,198.94 454.28,164.09 L454.3,164.08 Z" id="Path"/>
9
+ </ns0:g>
10
+ </ns0:g>
11
+ </ns0:svg>
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="464px" height="314px" viewBox="0 0 464 314" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3
+ <defs>
4
+ <linearGradient x1="0.00215870369%" y1="49.9984032%" x2="100.004107%" y2="49.9984032%" id="linearGradient-1">
5
+ <stop stop-color="#0061FF" offset="0%"></stop>
6
+ <stop stop-color="#EC1FDA" offset="100%"></stop>
7
+ </linearGradient>
8
+ </defs>
9
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
10
+ <g id="maglev-3" fill="url(#linearGradient-1)">
11
+ <path d="M454.3,164.08 L419.43,88.31 C394.87,34.93 341.49,0.74 282.74,0.74 L103.98,0.74 C92.95,0.74 84.01,9.68 84.01,20.71 C84.01,31.74 92.95,40.68 103.98,40.68 L229.29,40.68 C229.29,40.68 219.11,64.98 219.11,64.98 C212.3,81.22 196.41,91.79 178.8,91.79 L20.74,91.79 C9.71,91.79 0.77,100.73 0.77,111.76 C0.77,122.79 9.71,131.73 20.74,131.73 L191.14,131.73 C191.14,131.73 180.96,156.04 180.96,156.04 C174.15,172.28 158.26,182.85 140.65,182.85 L52.82,182.85 C41.79,182.85 32.85,191.79 32.85,202.82 C32.85,213.85 41.79,222.79 52.82,222.79 L414.89,222.79 C418.73,222.79 421.53,226.58 420.25,230.19 C417.74,237.29 413.97,244.05 408.92,250.15 C396.11,265.63 376.56,273.93 356.46,273.93 L126,273.93 C114.97,273.93 106.03,282.87 106.03,293.9 C106.03,304.93 114.97,313.87 126,313.87 L355.88,313.87 C390.23,313.87 423.3,298.4 443.55,270.65 C466.4,239.34 470.31,198.94 454.28,164.09 L454.3,164.08 Z" id="Path"></path>
12
+ </g>
13
+ </g>
14
+ </svg>
@@ -53,6 +53,19 @@ export const start = (config) => {
53
53
 
54
54
  // click on links
55
55
  disableLinks(previewDocument)
56
+
57
+ // Only works on Google Chrome
58
+ selectHoveredSectionAtStartup(previewDocument, config.stickySectionIds)
59
+ }
60
+
61
+ const selectHoveredSectionAtStartup = (previewDocument, stickySectionIds) => {
62
+ setTimeout(() => {
63
+ const section = previewDocument.querySelector('[data-maglev-section-id]:hover')
64
+
65
+ if (section) {
66
+ onSectionHovered(previewDocument, section, stickySectionIds, true)
67
+ }
68
+ }, 200)
56
69
  }
57
70
 
58
71
  const listen = (previewDocument, eventType, handler) => {
@@ -96,28 +109,18 @@ const listen = (previewDocument, eventType, handler) => {
96
109
  }
97
110
 
98
111
  const listenScrolling = (previewDocument) => {
99
- let mouseX = 0
100
- let mouseY = 0
101
-
102
- addEventListener(previewDocument, 'mousemove', (e) => {
103
- mouseX = e.clientX
104
- mouseY = e.clientY
105
- })
106
-
107
- const debouncedScrollNotifier = debounce(() => {
108
- const el = previewDocument
109
- .elementFromPoint(mouseX, mouseY)
110
- ?.closest('[data-maglev-section-id]')
111
-
112
+ const scrollNotifier = () => {
113
+ const el = previewDocument.querySelector('[data-maglev-section-id]:hover')
112
114
  if (el) postMessage('scroll', { boundingRect: el.getBoundingClientRect() })
113
- }, 10)
115
+ }
114
116
 
115
- addEventListener(previewDocument, 'scroll', debouncedScrollNotifier)
117
+ addEventListener(previewDocument, 'scroll', scrollNotifier)
116
118
  }
117
119
 
118
- const onSectionHovered = (previewDocument, el, stickySectionIds) => {
120
+ const onSectionHovered = (previewDocument, el, stickySectionIds, force = false) => {
119
121
  const sectionId = el.dataset.maglevSectionId
120
- if (hoveredSectionId !== sectionId) {
122
+
123
+ if (hoveredSectionId !== sectionId || force) {
121
124
  postMessage('section:hover', {
122
125
  sectionId,
123
126
  sectionRect: el.getBoundingClientRect(),
@@ -21,6 +21,7 @@ export const listenMessages = () => {
21
21
  case 'section:move':
22
22
  case 'section:update':
23
23
  case 'section:remove':
24
+ case 'section:ping':
24
25
  case 'block:add':
25
26
  case 'block:move':
26
27
  case 'block:update':