panda_cms 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +71 -0
- data/Rakefile +8 -0
- data/app/assets/builds/panda_cms.css +1 -0
- data/app/assets/config/panda_cms_manifest.js +1 -0
- data/app/assets/stylesheets/panda_cms/application.tailwind.css +61 -0
- data/app/builders/panda_cms/form_builder.rb +118 -0
- data/app/components/panda_cms/admin/button_component.rb +65 -0
- data/app/components/panda_cms/admin/container_component.html.erb +13 -0
- data/app/components/panda_cms/admin/container_component.rb +11 -0
- data/app/components/panda_cms/admin/flash_message_component.html.erb +30 -0
- data/app/components/panda_cms/admin/flash_message_component.rb +44 -0
- data/app/components/panda_cms/admin/heading_component.rb +43 -0
- data/app/components/panda_cms/admin/panel_component.html.erb +7 -0
- data/app/components/panda_cms/admin/panel_component.rb +11 -0
- data/app/components/panda_cms/admin/slideover_component.html.erb +9 -0
- data/app/components/panda_cms/admin/slideover_component.rb +13 -0
- data/app/components/panda_cms/admin/statistics_component.html.erb +4 -0
- data/app/components/panda_cms/admin/statistics_component.rb +15 -0
- data/app/components/panda_cms/admin/tab_bar_component.html.erb +35 -0
- data/app/components/panda_cms/admin/tab_bar_component.rb +13 -0
- data/app/components/panda_cms/admin/table_component.html.erb +21 -0
- data/app/components/panda_cms/admin/table_component.rb +43 -0
- data/app/components/panda_cms/admin/tag_component.rb +33 -0
- data/app/components/panda_cms/admin/user_activity_component.html.erb +5 -0
- data/app/components/panda_cms/admin/user_activity_component.rb +19 -0
- data/app/components/panda_cms/admin/user_display_component.html.erb +11 -0
- data/app/components/panda_cms/admin/user_display_component.rb +19 -0
- data/app/components/panda_cms/grid_component.html.erb +6 -0
- data/app/components/panda_cms/grid_component.rb +13 -0
- data/app/components/panda_cms/menu_component.html.erb +3 -0
- data/app/components/panda_cms/menu_component.rb +18 -0
- data/app/components/panda_cms/page_menu_component.html.erb +24 -0
- data/app/components/panda_cms/page_menu_component.rb +24 -0
- data/app/components/panda_cms/rich_text_component.html.erb +40 -0
- data/app/components/panda_cms/rich_text_component.rb +35 -0
- data/app/components/panda_cms/text_component.rb +67 -0
- data/app/constraints/panda_cms/admin_constraint.rb +16 -0
- data/app/controllers/panda_cms/admin/block_contents_controller.rb +44 -0
- data/app/controllers/panda_cms/admin/dashboard_controller.rb +31 -0
- data/app/controllers/panda_cms/admin/files_controller.rb +19 -0
- data/app/controllers/panda_cms/admin/forms_controller.rb +51 -0
- data/app/controllers/panda_cms/admin/menus_controller.rb +81 -0
- data/app/controllers/panda_cms/admin/pages_controller.rb +88 -0
- data/app/controllers/panda_cms/admin/posts_controller.rb +34 -0
- data/app/controllers/panda_cms/admin/sessions_controller.rb +83 -0
- data/app/controllers/panda_cms/admin/settings/bulk_editor_controller.rb +35 -0
- data/app/controllers/panda_cms/admin/settings_controller.rb +18 -0
- data/app/controllers/panda_cms/application_controller.rb +55 -0
- data/app/controllers/panda_cms/errors_controller.rb +31 -0
- data/app/controllers/panda_cms/form_submissions_controller.rb +21 -0
- data/app/controllers/panda_cms/pages_controller.rb +56 -0
- data/app/controllers/panda_cms/posts_controller.rb +17 -0
- data/app/helpers/panda_cms/admin/files_helper.rb +4 -0
- data/app/helpers/panda_cms/admin/pages_helper.rb +4 -0
- data/app/helpers/panda_cms/application_helper.rb +96 -0
- data/app/helpers/panda_cms/pages_helper.rb +4 -0
- data/app/helpers/panda_cms/theme_helper.rb +16 -0
- data/app/javascript/base.js +37 -0
- data/app/javascript/controllers/menu_controller.js +19 -0
- data/app/javascript/controllers/text_controller.js +78 -0
- data/app/javascript/controllers/text_field_update_controller.js +23 -0
- data/app/javascript/vendor/stimulus-components-rails-nested-form.js +2 -0
- data/app/javascript/vendor/tailwindcss-stimulus-components.js +2 -0
- data/app/jobs/panda_cms/application_job.rb +4 -0
- data/app/jobs/panda_cms/record_visit_job.rb +29 -0
- data/app/lib/panda_cms/bulk_editor.rb +169 -0
- data/app/lib/panda_cms/demo_site_generator.rb +70 -0
- data/app/lib/panda_cms/slug.rb +22 -0
- data/app/mailers/panda_cms/application_mailer.rb +6 -0
- data/app/mailers/panda_cms/form_mailer.rb +19 -0
- data/app/models/panda_cms/application_record.rb +5 -0
- data/app/models/panda_cms/block.rb +32 -0
- data/app/models/panda_cms/block_content.rb +16 -0
- data/app/models/panda_cms/block_content_version.rb +6 -0
- data/app/models/panda_cms/breadcrumb.rb +10 -0
- data/app/models/panda_cms/current.rb +15 -0
- data/app/models/panda_cms/form.rb +7 -0
- data/app/models/panda_cms/form_submission.rb +5 -0
- data/app/models/panda_cms/menu.rb +50 -0
- data/app/models/panda_cms/menu_item.rb +56 -0
- data/app/models/panda_cms/page.rb +81 -0
- data/app/models/panda_cms/page_version.rb +6 -0
- data/app/models/panda_cms/post.rb +25 -0
- data/app/models/panda_cms/post_version.rb +6 -0
- data/app/models/panda_cms/redirect.rb +9 -0
- data/app/models/panda_cms/template.rb +117 -0
- data/app/models/panda_cms/template_version.rb +6 -0
- data/app/models/panda_cms/user.rb +15 -0
- data/app/models/panda_cms/version.rb +6 -0
- data/app/models/panda_cms/visit.rb +7 -0
- data/app/views/layouts/panda_cms/application.html.erb +44 -0
- data/app/views/layouts/panda_cms/public.html.erb +3 -0
- data/app/views/panda_cms/admin/dashboard/show.html.erb +11 -0
- data/app/views/panda_cms/admin/files/index.html.erb +124 -0
- data/app/views/panda_cms/admin/files/show.html.erb +2 -0
- data/app/views/panda_cms/admin/forms/edit.html.erb +0 -0
- data/app/views/panda_cms/admin/forms/index.html.erb +13 -0
- data/app/views/panda_cms/admin/forms/new.html.erb +16 -0
- data/app/views/panda_cms/admin/forms/show.html.erb +35 -0
- data/app/views/panda_cms/admin/menus/_form.html.erb +21 -0
- data/app/views/panda_cms/admin/menus/_menu_item_fields.html.erb +7 -0
- data/app/views/panda_cms/admin/menus/edit.html.erb +58 -0
- data/app/views/panda_cms/admin/menus/index.html.erb +10 -0
- data/app/views/panda_cms/admin/menus/new.html.erb +5 -0
- data/app/views/panda_cms/admin/pages/edit.html.erb +26 -0
- data/app/views/panda_cms/admin/pages/index.html.erb +16 -0
- data/app/views/panda_cms/admin/pages/new.html.erb +16 -0
- data/app/views/panda_cms/admin/pages/show.html.erb +1 -0
- data/app/views/panda_cms/admin/posts/index.html.erb +16 -0
- data/app/views/panda_cms/admin/sessions/new.html.erb +18 -0
- data/app/views/panda_cms/admin/settings/bulk_editor/new.html.erb +68 -0
- data/app/views/panda_cms/admin/settings/index.html.erb +19 -0
- data/app/views/panda_cms/admin/shared/_breadcrumbs.html.erb +28 -0
- data/app/views/panda_cms/admin/shared/_flash.html.erb +5 -0
- data/app/views/panda_cms/admin/shared/_sidebar.html.erb +45 -0
- data/app/views/panda_cms/form_mailer/notification_email.html.erb +11 -0
- data/app/views/panda_cms/shared/_favicons.html.erb +9 -0
- data/app/views/panda_cms/shared/_footer.html.erb +2 -0
- data/app/views/panda_cms/shared/_header.html.erb +15 -0
- data/config/importmap.rb +9 -0
- data/config/initializers/panda_cms/form_errors.rb +38 -0
- data/config/initializers/panda_cms/healthcheck_log_silencer.rb +11 -0
- data/config/initializers/panda_cms.rb +52 -0
- data/config/locales/en.yml +29 -0
- data/config/routes.rb +43 -0
- data/config/tailwind.config.js +35 -0
- data/config/tailwind.embed.config.js +20 -0
- data/db/migrate/20240205223709_create_panda_cms_pages.rb +9 -0
- data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +14 -0
- data/db/migrate/20240303002805_create_panda_cms_templates.rb +11 -0
- data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +14 -0
- data/db/migrate/20240303022441_create_panda_cms_blocks.rb +13 -0
- data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +10 -0
- data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +14 -0
- data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +10 -0
- data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +12 -0
- data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +5 -0
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +16 -0
- data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +6 -0
- data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +5 -0
- data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +24 -0
- data/db/migrate/20240317010532_create_panda_cms_users.rb +12 -0
- data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +7 -0
- data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +5 -0
- data/db/migrate/20240317214827_create_panda_cms_redirects.rb +14 -0
- data/db/migrate/20240317230622_create_panda_cms_visits.rb +13 -0
- data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +58 -0
- data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +5 -0
- data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
- data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +28 -0
- data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +8 -0
- data/db/migrate/20240804110225_add_status_to_panda_cms_pages.rb +7 -0
- data/db/migrate/20240804235210_create_panda_cms_forms.rb +11 -0
- data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +9 -0
- data/db/migrate/20240805121123_create_panda_cms_posts.rb +27 -0
- data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +14 -0
- data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +13 -0
- data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +5 -0
- data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +5 -0
- data/db/seeds.rb +4 -0
- data/lib/generators/panda_cms/install_generator.rb +24 -0
- data/lib/panda_cms/engine.rb +167 -0
- data/lib/panda_cms/exceptions_app.rb +24 -0
- data/lib/panda_cms/version.rb +3 -0
- data/lib/panda_cms.rb +15 -0
- data/lib/tasks/panda_cms.rake +92 -0
- data/lib/templates/erb/scaffold/_form.html.erb.tt +43 -0
- data/lib/templates/erb/scaffold/edit.html.erb.tt +8 -0
- data/lib/templates/erb/scaffold/index.html.erb.tt +14 -0
- data/lib/templates/erb/scaffold/new.html.erb.tt +7 -0
- data/lib/templates/erb/scaffold/partial.html.erb.tt +22 -0
- data/lib/templates/erb/scaffold/show.html.erb.tt +15 -0
- data/public/panda-cms-assets/favicons/android-chrome-192x192.png +0 -0
- data/public/panda-cms-assets/favicons/android-chrome-512x512.png +0 -0
- data/public/panda-cms-assets/favicons/apple-touch-icon.png +0 -0
- data/public/panda-cms-assets/favicons/browserconfig.xml +9 -0
- data/public/panda-cms-assets/favicons/favicon-16x16.png +0 -0
- data/public/panda-cms-assets/favicons/favicon-32x32.png +0 -0
- data/public/panda-cms-assets/favicons/favicon.ico +0 -0
- data/public/panda-cms-assets/favicons/mstile-150x150.png +0 -0
- data/public/panda-cms-assets/favicons/safari-pinned-tab.svg +61 -0
- data/public/panda-cms-assets/favicons/site.webmanifest +14 -0
- data/public/panda-cms-assets/javascripts/base.js +37 -0
- data/public/panda-cms-assets/javascripts/controllers/menu_controller.js +19 -0
- data/public/panda-cms-assets/javascripts/controllers/text_field_update_controller.js +23 -0
- data/public/panda-cms-assets/javascripts/embed/editable.js +308 -0
- data/public/panda-cms-assets/javascripts/vendor/stimulus-components-rails-nested-form.js +2 -0
- data/public/panda-cms-assets/javascripts/vendor/stimulus-loading.js +113 -0
- data/public/panda-cms-assets/javascripts/vendor/tailwindcss-stimulus-components.js +2 -0
- data/public/panda-cms-assets/panda-logo-screenprint.png +0 -0
- data/public/panda-cms-assets/panda-nav.png +0 -0
- metadata +1034 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
import { Controller as PandaCmsController } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends PandaCmsController {
|
4
|
+
static targets = ["source"];
|
5
|
+
static values = { page: String, blockcontent: String };
|
6
|
+
static classes = ["initial", "success", "error"];
|
7
|
+
|
8
|
+
connect() {
|
9
|
+
this.sourceTarget.classList.add(...this.initialClasses);
|
10
|
+
}
|
11
|
+
|
12
|
+
save() {
|
13
|
+
this.cleanedContent = this.clean(this.sourceTarget.innerHTML);
|
14
|
+
|
15
|
+
fetch(
|
16
|
+
`/admin/pages/${this.pageValue}/block_contents/${this.blockcontentValue}`,
|
17
|
+
{
|
18
|
+
method: "PATCH",
|
19
|
+
headers: {
|
20
|
+
"Content-Type": "application/json",
|
21
|
+
"X-CSRF-Token": document
|
22
|
+
.querySelector('meta[name="csrf-token"]')
|
23
|
+
.getAttribute("content"),
|
24
|
+
},
|
25
|
+
body: JSON.stringify({ content: this.cleanedContent }),
|
26
|
+
}
|
27
|
+
)
|
28
|
+
.then((response) => response.json())
|
29
|
+
.then((data) => {
|
30
|
+
this.setSuccessClasses();
|
31
|
+
})
|
32
|
+
.catch((error) => {
|
33
|
+
alert("Error:", error);
|
34
|
+
console.log(error);
|
35
|
+
this.element.classList.remove(
|
36
|
+
...this.initialClasses,
|
37
|
+
...this.successClasses
|
38
|
+
);
|
39
|
+
this.element.classList.add(...this.errorClasses);
|
40
|
+
});
|
41
|
+
}
|
42
|
+
|
43
|
+
setInitialClasses() {
|
44
|
+
this.element.classList.remove(...this.successClasses, ...this.errorClasses);
|
45
|
+
this.element.classList.add(...this.initialClasses);
|
46
|
+
}
|
47
|
+
|
48
|
+
setSuccessClasses() {
|
49
|
+
this.element.classList.remove(...this.initialClasses, ...this.errorClasses);
|
50
|
+
this.element.classList.add(...this.successClasses);
|
51
|
+
this.resetInitialClassesTimer();
|
52
|
+
}
|
53
|
+
|
54
|
+
setErrorClasses() {
|
55
|
+
this.element.classList.remove(
|
56
|
+
...this.initialClasses,
|
57
|
+
...this.successClasses
|
58
|
+
);
|
59
|
+
this.element.classList.add(...this.errorClasses);
|
60
|
+
this.resetInitialClassesTimer();
|
61
|
+
}
|
62
|
+
|
63
|
+
resetInitialClassesTimer() {
|
64
|
+
setTimeout(
|
65
|
+
function () {
|
66
|
+
this.setInitialClasses();
|
67
|
+
}.bind(this),
|
68
|
+
1000
|
69
|
+
);
|
70
|
+
}
|
71
|
+
|
72
|
+
clean(content) {
|
73
|
+
// Replace horrible bullet points
|
74
|
+
content = content.replace(/• /g, "* ");
|
75
|
+
// TODO: More formatting
|
76
|
+
return content;
|
77
|
+
}
|
78
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { Controller as PandaCmsController } from "@hotwired/stimulus"
|
2
|
+
export default class extends PandaCmsController {
|
3
|
+
static targets = [ "existing_root", "input_select", "input_text", "output_text" ]
|
4
|
+
|
5
|
+
connect() {
|
6
|
+
}
|
7
|
+
|
8
|
+
generatePath() {
|
9
|
+
this.output_textTarget.value = "/" + this.createSlug(this.input_textTarget.value);
|
10
|
+
}
|
11
|
+
|
12
|
+
setPrePath() {
|
13
|
+
this.parent_slugs = this.input_selectTarget.options[this.input_selectTarget.selectedIndex].text.match(/.*\((.*)\)$/)[1];
|
14
|
+
this.output_textTarget.previousSibling.innerHTML = (this.existing_rootTarget.value + this.parent_slugs).replace(/\/$/, '');;
|
15
|
+
}
|
16
|
+
|
17
|
+
createSlug(input) {
|
18
|
+
return input.toLowerCase().trim()
|
19
|
+
.replace(/[^\w\s-]/g, "-")
|
20
|
+
.replace(/&/, "and")
|
21
|
+
.replace(/[\s_-]+/g, "-");
|
22
|
+
}
|
23
|
+
}
|
@@ -0,0 +1,2 @@
|
|
1
|
+
import{Controller as e}from"@hotwired/stimulus";const t=class _RailsNestedForm extends e{add(e){e.preventDefault();const t=this.templateTarget.innerHTML.replace(/NEW_RECORD/g,(new Date).getTime().toString());this.targetTarget.insertAdjacentHTML("beforebegin",t);const r=new CustomEvent("rails-nested-form:add",{bubbles:!0});this.element.dispatchEvent(r)}remove(e){e.preventDefault();const t=e.target.closest(this.wrapperSelectorValue);if(t.dataset.newRecord==="true")t.remove();else{t.style.display="none";const e=t.querySelector("input[name*='_destroy']");e.value="1"}const r=new CustomEvent("rails-nested-form:remove",{bubbles:!0});this.element.dispatchEvent(r)}};t.targets=["target","template"],t.values={wrapperSelector:{type:String,default:".nested-form-wrapper"}};let r=t;export{r as default};
|
2
|
+
|
@@ -0,0 +1,2 @@
|
|
1
|
+
import{Controller as e}from"@hotwired/stimulus";var t=Object.defineProperty;var V=(e,a,i)=>a in e?t(e,a,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[a]=i;var s=(e,t,a)=>(V(e,typeof t!="symbol"?t+"":t,a),a);async function r(e,t,a={}){t?T(e,a):b(e,a)}async function T(e,t={}){let a=e.dataset.transitionEnter||t.enter||"enter",i=e.dataset.transitionEnterFrom||t.enterFrom||"enter-from",n=e.dataset.transitionEnterTo||t.enterTo||"enter-to",o=e.dataset.toggleClass||t.toggleClass||"hidden";e.classList.add(...a.split(" ")),e.classList.add(...i.split(" ")),e.classList.remove(...n.split(" ")),e.classList.remove(...o.split(" ")),await v(),e.classList.remove(...i.split(" ")),e.classList.add(...n.split(" "));try{await x(e)}finally{e.classList.remove(...a.split(" "))}}async function b(e,t={}){let a=e.dataset.transitionLeave||t.leave||"leave",i=e.dataset.transitionLeaveFrom||t.leaveFrom||"leave-from",n=e.dataset.transitionLeaveTo||t.leaveTo||"leave-to",o=e.dataset.toggleClass||t.toggle||"hidden";e.classList.add(...a.split(" ")),e.classList.add(...i.split(" ")),e.classList.remove(...n.split(" ")),await v(),e.classList.remove(...i.split(" ")),e.classList.add(...n.split(" "));try{await x(e)}finally{e.classList.remove(...a.split(" ")),e.classList.add(...o.split(" "))}}function v(){return new Promise((e=>{requestAnimationFrame((()=>{requestAnimationFrame(e)}))}))}function x(e){return Promise.all(e.getAnimations().map((e=>e.finished)))}var a=class extends e{connect(){setTimeout((()=>{T(this.element)}),this.showDelayValue),this.hasDismissAfterValue&&setTimeout((()=>{this.close()}),this.dismissAfterValue)}close(){b(this.element).then((()=>{this.element.remove()}))}};s(a,"values",{dismissAfter:Number,showDelay:{type:Number,default:0}});var i=class extends e{connect(){this.timeout=null}save(){clearTimeout(this.timeout),this.timeout=setTimeout((()=>{this.statusTarget.textContent=this.submittingTextValue,this.formTarget.requestSubmit()}),this.submitDurationValue)}success(){this.setStatus(this.successTextValue)}error(){this.setStatus(this.errorTextValue)}setStatus(e){this.statusTarget.textContent=e,this.timeout=setTimeout((()=>{this.statusTarget.textContent=""}),this.statusDurationValue)}};s(i,"targets",["form","status"]),s(i,"values",{submitDuration:{type:Number,default:1e3},statusDuration:{type:Number,default:2e3},submittingText:{type:String,default:"Saving..."},successText:{type:String,default:"Saved!"},errorText:{type:String,default:"Unable to save."}});var n=class extends e{update(){this.preview=this.colorTarget.value}set preview(e){this.previewTarget.style[this.styleValue]=e;let t=this._getContrastYIQ(e);this.styleValue==="color"?this.previewTarget.style.backgroundColor=t:this.previewTarget.style.color=t}_getContrastYIQ(e){e=e.replace("#","");let t=128,a=parseInt(e.substr(0,2),16),i=parseInt(e.substr(2,2),16),n=parseInt(e.substr(4,2),16);return(a*299+i*587+n*114)/1e3>=t?"#000":"#fff"}};s(n,"targets",["preview","color"]),s(n,"values",{style:{type:String,default:"backgroundColor"}});var o=class extends e{connect(){document.addEventListener("turbo:before-cache",this.beforeCache.bind(this))}disconnect(){document.removeEventListener("turbo:before-cache",this.beforeCache.bind(this))}openValueChanged(){r(this.menuTarget,this.openValue,this.transitionOptions),this.openValue===!0&&this.hasMenuItemTarget&&this.menuItemTargets[0].focus()}show(){this.openValue=!0}close(){this.openValue=!1}hide(e){this.closeOnClickOutsideValue&&e.target.nodeType&&this.element.contains(e.target)===!1&&this.openValue&&(this.openValue=!1),this.closeOnEscapeValue&&e.key==="Escape"&&this.openValue&&(this.openValue=!1)}toggle(){this.openValue=!this.openValue}nextItem(e){e.preventDefault(),this.menuItemTargets[this.nextIndex].focus()}previousItem(e){e.preventDefault(),this.menuItemTargets[this.previousIndex].focus()}get currentItemIndex(){return this.menuItemTargets.indexOf(document.activeElement)}get nextIndex(){return Math.min(this.currentItemIndex+1,this.menuItemTargets.length-1)}get previousIndex(){return Math.max(this.currentItemIndex-1,0)}get transitionOptions(){return{enter:this.hasEnterClass?this.enterClass:"transition ease-out duration-100",enterFrom:this.hasEnterFromClass?this.enterFromClass:"transform opacity-0 scale-95",enterTo:this.hasEnterToClass?this.enterToClass:"transform opacity-100 scale-100",leave:this.hasLeaveClass?this.leaveClass:"transition ease-in duration-75",leaveFrom:this.hasLeaveFromClass?this.leaveFromClass:"transform opacity-100 scale-100",leaveTo:this.hasLeaveToClass?this.leaveToClass:"transform opacity-0 scale-95",toggleClass:this.hasToggleClass?this.toggleClass:"hidden"}}beforeCache(){this.openValue=!1,this.menuTarget.classList.add("hidden")}};s(o,"targets",["menu","button","menuItem"]),s(o,"values",{open:{type:Boolean,default:!1},closeOnEscape:{type:Boolean,default:!0},closeOnClickOutside:{type:Boolean,default:!0}}),s(o,"classes",["enter","enterFrom","enterTo","leave","leaveFrom","leaveTo","toggle"]);var l=class extends e{connect(){this.openValue&&this.open(),document.addEventListener("turbo:before-cache",this.beforeCache.bind(this))}disconnect(){document.removeEventListener("turbo:before-cache",this.beforeCache.bind(this))}open(){this.dialogTarget.showModal()}close(){this.dialogTarget.setAttribute("closing",""),Promise.all(this.dialogTarget.getAnimations().map((e=>e.finished))).then((()=>{this.dialogTarget.removeAttribute("closing"),this.dialogTarget.close()}))}backdropClose(e){e.target.nodeName=="DIALOG"&&this.close()}show(){this.dialogTarget.show()}hide(){this.close()}beforeCache(){this.close()}};s(l,"targets",["dialog"]),s(l,"values",{open:Boolean});var h=class extends e{openValueChanged(){r(this.contentTarget,this.openValue),this.shouldAutoDismiss&&this.scheduleDismissal()}show(e){this.shouldAutoDismiss&&this.scheduleDismissal(),this.openValue=!0}hide(){this.openValue=!1}toggle(){this.openValue=!this.openValue}get shouldAutoDismiss(){return this.openValue&&this.hasDismissAfterValue}scheduleDismissal(){this.hasDismissAfterValue&&(this.cancelDismissal(),this.timeoutId=setTimeout((()=>{this.hide(),this.timeoutId=void 0}),this.dismissAfterValue))}cancelDismissal(){typeof this.timeoutId=="number"&&(clearTimeout(this.timeoutId),this.timeoutId=void 0)}};s(h,"targets",["content"]),s(h,"values",{dismissAfter:Number,open:{type:Boolean,default:!1}});var u=class extends e{connect(){this.openValue&&this.open(),document.addEventListener("turbo:before-cache",this.beforeCache.bind(this))}disconnect(){document.removeEventListener("turbo:before-cache",this.beforeCache.bind(this))}open(){this.dialogTarget.showModal()}close(){this.dialogTarget.setAttribute("closing",""),Promise.all(this.dialogTarget.getAnimations().map((e=>e.finished))).then((()=>{this.dialogTarget.removeAttribute("closing"),this.dialogTarget.close()}))}backdropClose(e){e.target.nodeName=="DIALOG"&&this.close()}show(){this.open()}hide(){this.close()}beforeCache(){this.close()}};s(u,"targets",["dialog"]),s(u,"values",{open:Boolean});var c=class extends e{initialize(){this.anchor&&(this.indexValue=this.tabTargets.findIndex((e=>e.id===this.anchor)))}connect(){this.showTab()}change(e){e.currentTarget.tagName==="SELECT"?this.indexValue=e.currentTarget.selectedIndex:e.currentTarget.dataset.index?this.indexValue=e.currentTarget.dataset.index:e.currentTarget.dataset.id?this.indexValue=this.tabTargets.findIndex((t=>t.id==e.currentTarget.dataset.id)):this.indexValue=this.tabTargets.indexOf(e.currentTarget)}nextTab(){this.indexValue=Math.min(this.indexValue+1,this.tabsCount-1)}previousTab(){this.indexValue=Math.max(this.indexValue-1,0)}firstTab(){this.indexValue=0}lastTab(){this.indexValue=this.tabsCount-1}indexValueChanged(){if(this.showTab(),this.dispatch("tab-change",{target:this.tabTargets[this.indexValue],detail:{activeIndex:this.indexValue}}),this.updateAnchorValue){let e=this.tabTargets[this.indexValue].id;if(this.scrollToAnchorValue)location.hash=e;else{let t=window.location.href.split("#")[0]+"#"+e;history.replaceState({},document.title,t)}}}showTab(){this.panelTargets.forEach(((e,t)=>{let a=this.tabTargets[t];t===this.indexValue?(e.classList.remove("hidden"),a.ariaSelected="true",a.dataset.active=!0,this.hasInactiveTabClass&&a?.classList?.remove(...this.inactiveTabClasses),this.hasActiveTabClass&&a?.classList?.add(...this.activeTabClasses)):(e.classList.add("hidden"),a.ariaSelected=null,delete a.dataset.active,this.hasActiveTabClass&&a?.classList?.remove(...this.activeTabClasses),this.hasInactiveTabClass&&a?.classList?.add(...this.inactiveTabClasses))})),this.hasSelectTarget&&(this.selectTarget.selectedIndex=this.indexValue),this.scrollActiveTabIntoViewValue&&this.scrollToActiveTab()}scrollToActiveTab(){let e=this.element.querySelector("[aria-selected]");e&&e.scrollIntoView({inline:"center"})}get tabsCount(){return this.tabTargets.length}get anchor(){return document.URL.split("#").length>1?document.URL.split("#")[1]:null}};s(c,"classes",["activeTab","inactiveTab"]),s(c,"targets",["tab","panel","select"]),s(c,"values",{index:0,updateAnchor:Boolean,scrollToAnchor:Boolean,scrollActiveTabIntoView:Boolean});var d=class extends e{toggle(e){this.openValue=!this.openValue,this.animate()}toggleInput(e){this.openValue=e.target.checked,this.animate()}hide(){this.openValue=!1,this.animate()}show(){this.openValue=!0,this.animate()}animate(){this.toggleableTargets.forEach((e=>{r(e,this.openValue)}))}};s(d,"targets",["toggleable"]),s(d,"values",{open:{type:Boolean,default:!1}});export{a as Alert,i as Autosave,n as ColorPreview,o as Dropdown,l as Modal,h as Popover,u as Slideover,c as Tabs,d as Toggle};
|
2
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module PandaCms
|
2
|
+
class RecordVisitJob < ApplicationJob
|
3
|
+
queue_as :default
|
4
|
+
|
5
|
+
def perform(
|
6
|
+
url: nil,
|
7
|
+
user_agent: nil,
|
8
|
+
referrer: nil,
|
9
|
+
ip_address: nil,
|
10
|
+
page_id: nil,
|
11
|
+
current_user_id: nil,
|
12
|
+
params: [],
|
13
|
+
visited_at: nil,
|
14
|
+
redirect_id: nil
|
15
|
+
)
|
16
|
+
PandaCms::Visit.create!(
|
17
|
+
url: url,
|
18
|
+
user_agent: user_agent,
|
19
|
+
referrer: referrer,
|
20
|
+
ip_address: ip_address,
|
21
|
+
page_id: page_id,
|
22
|
+
redirect_id: redirect_id,
|
23
|
+
user_id: current_user_id,
|
24
|
+
params: params,
|
25
|
+
visited_at: visited_at
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require "htmlentities"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module PandaCms
|
5
|
+
#
|
6
|
+
# Bulk editor for site content in JSON format
|
7
|
+
#
|
8
|
+
class BulkEditor
|
9
|
+
#
|
10
|
+
# Export all site content to a JSON string
|
11
|
+
#
|
12
|
+
# @return [String] The JSON data
|
13
|
+
#
|
14
|
+
def self.export
|
15
|
+
data = extract_current_data
|
16
|
+
JSON.pretty_generate(data)
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Import site content from a JSON string
|
21
|
+
#
|
22
|
+
# @param json_data [String] The JSON data to import
|
23
|
+
# @return [Hash] A hash of debug information
|
24
|
+
#
|
25
|
+
def self.import(json_data)
|
26
|
+
# See if we can parse the JSON
|
27
|
+
new_data = JSON.parse(json_data)
|
28
|
+
current_data = extract_current_data
|
29
|
+
|
30
|
+
debug = {
|
31
|
+
success: [],
|
32
|
+
error: [],
|
33
|
+
warning: []
|
34
|
+
}
|
35
|
+
|
36
|
+
# Make sure templates are up to date
|
37
|
+
PandaCms::Template.generate_missing_blocks
|
38
|
+
|
39
|
+
# Run through the new data and compare it to the current data
|
40
|
+
new_data["pages"].each do |path, page_data|
|
41
|
+
if current_data["pages"][path].nil?
|
42
|
+
begin
|
43
|
+
page = PandaCms::Page.create!(
|
44
|
+
path: path,
|
45
|
+
title: page_data["title"],
|
46
|
+
template: PandaCms::Template.find_by(name: page_data["template"]),
|
47
|
+
parent: PandaCms::Page.find_by(path: page_data["parent"])
|
48
|
+
)
|
49
|
+
rescue => e
|
50
|
+
debug[:error] << "Failed to create page '#{path}': #{e.message}"
|
51
|
+
next
|
52
|
+
end
|
53
|
+
|
54
|
+
if !page
|
55
|
+
debug[:error] << "Unhandled: page '#{path}' does not exist in the current data and cannot be created"
|
56
|
+
next
|
57
|
+
else
|
58
|
+
debug[:success] << "Created page '#{path}' with title '#{page_data["title"]}'"
|
59
|
+
end
|
60
|
+
else
|
61
|
+
page = PandaCms::Page.find_by(path: path)
|
62
|
+
|
63
|
+
if page_data["title"] != current_data["pages"][path]["title"]
|
64
|
+
page.update(title: page_data["title"])
|
65
|
+
debug[:success] << "Updated: page '#{path}' title from '#{current_data["pages"][path]["title"]}' to '#{page_data["title"]}'"
|
66
|
+
end
|
67
|
+
|
68
|
+
if page_data["template"] != current_data["pages"][path]["template"]
|
69
|
+
# TODO: Handle page template changes
|
70
|
+
debug[:error] << "Page '#{path}' template is '#{current_data["pages"][path]["template"]}' and cannot be changed to '#{page_data["template"]}' without manual intervention"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
page_data["contents"].each do |key, block_data|
|
75
|
+
content = block_data["content"]
|
76
|
+
|
77
|
+
if current_data.dig("pages", path, "contents", key).nil?
|
78
|
+
raise "Unknown page 1" if page.nil?
|
79
|
+
block = PandaCms::Block.find_or_create_by(key: key, template: page.template) do |block_meta|
|
80
|
+
block_meta.name = key.titleize
|
81
|
+
end
|
82
|
+
|
83
|
+
if !block
|
84
|
+
debug[:error] << "Error creating block '#{key.titleize}' on page '#{page.title}'"
|
85
|
+
next
|
86
|
+
end
|
87
|
+
|
88
|
+
block_content = PandaCms::BlockContent.find_or_create_by(block: block, page: page)
|
89
|
+
# block_content.content = HTMLEntities.new.encode(content, :named)
|
90
|
+
block_content.content = content
|
91
|
+
|
92
|
+
begin
|
93
|
+
block_content.save!
|
94
|
+
|
95
|
+
if block_content.content != content
|
96
|
+
debug[:error] << "Failed to save content for '#{block.name}' on page '#{page.title}'"
|
97
|
+
else
|
98
|
+
debug[:success] << "Created '#{block.name}' content on page '#{page.title}'"
|
99
|
+
end
|
100
|
+
rescue => e
|
101
|
+
debug[:error] << "Failed to create '#{block.name}' content on page '#{page.title}': #{e.message}"
|
102
|
+
end
|
103
|
+
elsif content != current_data["pages"][path]["contents"][key]["content"]
|
104
|
+
# Content has changed
|
105
|
+
raise "Unknown page 2" if page.nil?
|
106
|
+
block = PandaCms::Block.find_by(key: key, template: page.template)
|
107
|
+
if PandaCms::BlockContent.find_by(page: page, block: block)&.update(content: content)
|
108
|
+
debug[:success] << "Updated '#{key.titleize}' content on page '#{page.title}'"
|
109
|
+
else
|
110
|
+
debug[:error] << "Failed to update '#{key.titleize}' content on page '#{page.title}'"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
new_data["menus"].each do |menu_data|
|
117
|
+
end
|
118
|
+
|
119
|
+
new_data["templates"].each do |template_data|
|
120
|
+
end
|
121
|
+
|
122
|
+
debug
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Extract the current data from the database into a standardised format
|
127
|
+
#
|
128
|
+
# Used both as the export format, and to compare imported data with for changes
|
129
|
+
#
|
130
|
+
# @visibility private
|
131
|
+
def self.extract_current_data
|
132
|
+
data = {
|
133
|
+
"pages" => {},
|
134
|
+
"menus" => {},
|
135
|
+
"templates" => {},
|
136
|
+
"settings" => {}
|
137
|
+
}
|
138
|
+
|
139
|
+
# Pages
|
140
|
+
PandaCms::Page.includes(:template).order("lft ASC").each do |page|
|
141
|
+
data["pages"][page.path] ||= {}
|
142
|
+
end
|
143
|
+
|
144
|
+
# TODO: Eventually set the position of the block in the template, and then order from there rather than the name?
|
145
|
+
PandaCms::BlockContent.includes(:block, page: [:template]).order("panda_cms_pages.lft ASC, panda_cms_blocks.key ASC").each do |block_content|
|
146
|
+
item = data["pages"][block_content.page.path] ||= {}
|
147
|
+
item["title"] = block_content.page.title
|
148
|
+
item["template"] = block_content.page.template.name
|
149
|
+
item["parent"] = block_content.page.parent&.path
|
150
|
+
item["contents"] ||= {}
|
151
|
+
item["contents"][block_content.block.key] = {
|
152
|
+
kind: block_content.block.kind, # We need the kind to recreate the block
|
153
|
+
content: block_content.content
|
154
|
+
}
|
155
|
+
data["pages"][block_content.page.path] = item
|
156
|
+
end
|
157
|
+
|
158
|
+
# Menus
|
159
|
+
# item = data["menus"][] ||= {}
|
160
|
+
|
161
|
+
# Templates
|
162
|
+
# item = data["templates"][] ||= {}
|
163
|
+
|
164
|
+
data["settings"] = {}
|
165
|
+
|
166
|
+
data.with_indifferent_access
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module PandaCms
|
2
|
+
class DemoSiteGenerator
|
3
|
+
attr_accessor :menus, :pages, :templates
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@menus = {}
|
7
|
+
@pages = {}
|
8
|
+
@templates = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
#
|
12
|
+
# Creates initial templates and empty blocks
|
13
|
+
#
|
14
|
+
# @return [Hash] A hash containing the created templates
|
15
|
+
def create_templates
|
16
|
+
# Templates
|
17
|
+
initial_templates = [
|
18
|
+
{name: "Homepage", file_path: "layouts/homepage", max_uses: 1},
|
19
|
+
{name: "Page", file_path: "layouts/page"}
|
20
|
+
]
|
21
|
+
|
22
|
+
initial_templates.each do |template|
|
23
|
+
@templates[template[:name].downcase.to_sym] = PandaCms::Template.find_or_create_by!(template)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Blocks
|
27
|
+
initial_blocks = [
|
28
|
+
{kind: "rich_text", name: "Introduction Text", key: "introduction_text", template: @templates[:homepage]},
|
29
|
+
{kind: "rich_text", name: "Main Content", key: "main_content", template: @templates[:homepage]},
|
30
|
+
{kind: "rich_text", name: "Main Content", key: "main_content", template: @templates[:page]}
|
31
|
+
]
|
32
|
+
|
33
|
+
initial_blocks.each do |block_data|
|
34
|
+
PandaCms::Block.find_or_create_by!(block_data)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Empty block contents
|
38
|
+
PandaCms::Block.find_each do |block|
|
39
|
+
block.template.pages.each do |page|
|
40
|
+
PandaCms::BlockContent.find_or_create_by!(page: page, block: block, content: "")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Creates initial pages
|
47
|
+
#
|
48
|
+
# @return [Hash] A hash containing the created pages
|
49
|
+
def create_pages
|
50
|
+
@pages[:home] = PandaCms::Page.find_or_create_by!({path: "/", title: "Home", template: @templates[:homepage]})
|
51
|
+
@pages[:about] = PandaCms::Page.find_or_create_by!({path: "/about", title: "About", template: @templates[:page], parent: @pages[:home]})
|
52
|
+
@pages[:terms] = PandaCms::Page.find_or_create_by!({path: "/terms-and-conditions", title: "Terms & Conditions", template: @templates[:page], parent: @pages[:home]})
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Creates initial menus
|
57
|
+
#
|
58
|
+
# @return [Hash] A hash containing the created menus
|
59
|
+
#
|
60
|
+
def create_menus
|
61
|
+
@menus = {}
|
62
|
+
@menus[:main] = PandaCms::Menu.find_or_create_by!(name: "Main Menu")
|
63
|
+
@menus[:footer] = PandaCms::Menu.find_or_create_by!(name: "Footer Menu")
|
64
|
+
|
65
|
+
# Automatically create main menu from homepage
|
66
|
+
@menus[:main].update(kind: :auto, start_page: @pages[:home])
|
67
|
+
@menus[:main].generate_auto_menu_items
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module PandaCms
|
2
|
+
class Slug
|
3
|
+
#
|
4
|
+
# Generates a slug from a provided string
|
5
|
+
#
|
6
|
+
# @param string [String] The provided string to turn into a slug
|
7
|
+
# @return string Generated slug
|
8
|
+
# @see text_field_update_controller.js should also implement this logic
|
9
|
+
def self.generate(string)
|
10
|
+
# Trim whitespace and downcase the string
|
11
|
+
string = string.to_s.strip.downcase
|
12
|
+
# Replace & with "and"
|
13
|
+
string = string.gsub("&", "and")
|
14
|
+
# Remove special characters
|
15
|
+
string = string.gsub(/[\!\@\£\$\%\^\&\*\(\)\+\=\{\}\[\]\:\;\"\'\|\\\`\<\>\?\,\.\/]+/, "")
|
16
|
+
# Replace any whitespace character with -
|
17
|
+
string = string.gsub(/[^\w\s-]/, "-")
|
18
|
+
# Replace multiple occurences of _ and - with -
|
19
|
+
string.gsub(/[\s_-]+/, "-")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module PandaCms
|
2
|
+
class FormMailer < PandaCms::ApplicationMailer
|
3
|
+
def notification_email(form:, form_submission:)
|
4
|
+
# TODO: Handle fields named just "name", and "email" better
|
5
|
+
@submission_data = form_submission.data
|
6
|
+
@sender_name = @submission_data["first_name"].to_s + " " + @submission_data["last_name"].to_s
|
7
|
+
@sender_email = @submission_data["email"].to_s
|
8
|
+
|
9
|
+
mail(
|
10
|
+
subject: "#{form.name}: #{form_submission.created_at.strftime("%d %b %Y %H:%M")}",
|
11
|
+
to: email_address_with_name("james@otaina.co.uk", "James Inman"),
|
12
|
+
from: email_address_with_name("noreply@pandacms.io", "Panda CMS"),
|
13
|
+
reply_to: email_address_with_name(@sender_email, @sender_name),
|
14
|
+
track_opens: "true",
|
15
|
+
message_stream: "outbound"
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PandaCms
|
2
|
+
class Block < ApplicationRecord
|
3
|
+
self.table_name = "panda_cms_blocks"
|
4
|
+
|
5
|
+
belongs_to :template, foreign_key: :panda_cms_template_id, class_name: "PandaCms::Template", inverse_of: :blocks, optional: true
|
6
|
+
has_many :block_contents, foreign_key: :panda_cms_block_id, class_name: "PandaCms::BlockContent", inverse_of: :block
|
7
|
+
|
8
|
+
validates :kind, presence: true
|
9
|
+
validates :name, presence: true
|
10
|
+
validates :key, presence: true, uniqueness: {scope: :panda_cms_template_id, case_sensitive: false}
|
11
|
+
|
12
|
+
# Validation for presence on template intentionally skipped to allow global elements
|
13
|
+
|
14
|
+
# NB: Commented out values are not yet implemented
|
15
|
+
enum :kind, {
|
16
|
+
plain_text: "plain_text",
|
17
|
+
rich_text: "rich_text",
|
18
|
+
iframe: "iframe",
|
19
|
+
list: "list"
|
20
|
+
# image: "image",
|
21
|
+
# video: "video",
|
22
|
+
# audio: "audio",
|
23
|
+
# file: "file",
|
24
|
+
# code: "code",
|
25
|
+
# iframe: "iframe",
|
26
|
+
# quote: "quote",
|
27
|
+
# list: "list"
|
28
|
+
# table: "table",
|
29
|
+
# form: "form"
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "redcarpet"
|
2
|
+
|
3
|
+
module PandaCms
|
4
|
+
class BlockContent < ApplicationRecord
|
5
|
+
self.table_name = "panda_cms_block_contents"
|
6
|
+
|
7
|
+
has_paper_trail versions: {
|
8
|
+
class_name: "PandaCms::BlockContentVersion"
|
9
|
+
}
|
10
|
+
|
11
|
+
belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "PandaCms::Page", inverse_of: :block_contents, optional: true, touch: true
|
12
|
+
belongs_to :block, foreign_key: :panda_cms_block_id, class_name: "PandaCms::Block", inverse_of: :block_contents, optional: false
|
13
|
+
|
14
|
+
validates :block, presence: true, uniqueness: {scope: :page}
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module PandaCms
|
2
|
+
class Current < ActiveSupport::CurrentAttributes
|
3
|
+
attribute :root, :page
|
4
|
+
attribute :user
|
5
|
+
attribute :request_id, :user_agent, :ip_address
|
6
|
+
|
7
|
+
# resets { Time.zone = nil }
|
8
|
+
|
9
|
+
# def user=(user)
|
10
|
+
# super
|
11
|
+
# self.account = user.account
|
12
|
+
# Time.zone = user.time_zone
|
13
|
+
# end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module PandaCms
|
2
|
+
class Menu < ApplicationRecord
|
3
|
+
self.table_name = "panda_cms_menus"
|
4
|
+
|
5
|
+
after_save :generate_auto_menu_items, if: -> { kind == "auto" }
|
6
|
+
|
7
|
+
has_many :menu_items, -> { order(lft: :asc) }, foreign_key: :panda_cms_menu_id, class_name: "PandaCms::MenuItem", inverse_of: :menu
|
8
|
+
belongs_to :start_page, class_name: "PandaCms::Page", foreign_key: "start_page_id", inverse_of: :page_menu, optional: true
|
9
|
+
|
10
|
+
accepts_nested_attributes_for :menu_items, reject_if: :all_blank, allow_destroy: true
|
11
|
+
|
12
|
+
validates :name, presence: true, uniqueness: {case_sensitive: false}
|
13
|
+
validates :kind, presence: true, inclusion: {in: ["static", "auto"]}
|
14
|
+
validate :validate_start_page
|
15
|
+
|
16
|
+
def generate_auto_menu_items
|
17
|
+
return false if kind != "auto"
|
18
|
+
|
19
|
+
# NB: Transactions are not distributed across database connections
|
20
|
+
transaction do
|
21
|
+
menu_items.destroy_all
|
22
|
+
menu_item_root = menu_items.create(text: start_page.title, panda_cms_page_id: start_page.id)
|
23
|
+
generate_menu_items(parent_menu_item: menu_item_root, parent_page: start_page)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def generate_menu_items(parent_menu_item:, parent_page:)
|
30
|
+
parent_page.children.where(status: [:active]).each do |page|
|
31
|
+
menu_item = menu_items.create(text: page.title, panda_cms_page_id: page.id, parent: parent_menu_item)
|
32
|
+
if page.children
|
33
|
+
generate_menu_items(parent_menu_item: menu_item, parent_page: page)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Validate that the start page is set if the menu is of kind auto
|
40
|
+
#
|
41
|
+
# @return nil
|
42
|
+
# @visibility private
|
43
|
+
#
|
44
|
+
def validate_start_page
|
45
|
+
if kind == "auto" && start_page.nil?
|
46
|
+
errors.add(:start_page, "can't be blank")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|