panda-cms 0.7.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 +73 -0
- data/Rakefile +7 -0
- data/app/assets/builds/panda.cms.css +2808 -0
- data/app/assets/config/panda_cms_manifest.js +4 -0
- data/app/assets/stylesheets/panda/cms/application.tailwind.css +162 -0
- data/app/assets/stylesheets/panda/cms/editor.css +120 -0
- data/app/builders/panda/cms/form_builder.rb +234 -0
- data/app/components/panda/cms/admin/button_component.rb +70 -0
- data/app/components/panda/cms/admin/container_component.html.erb +13 -0
- data/app/components/panda/cms/admin/container_component.rb +13 -0
- data/app/components/panda/cms/admin/flash_message_component.html.erb +31 -0
- data/app/components/panda/cms/admin/flash_message_component.rb +47 -0
- data/app/components/panda/cms/admin/heading_component.rb +45 -0
- data/app/components/panda/cms/admin/panel_component.html.erb +7 -0
- data/app/components/panda/cms/admin/panel_component.rb +13 -0
- data/app/components/panda/cms/admin/slideover_component.html.erb +9 -0
- data/app/components/panda/cms/admin/slideover_component.rb +15 -0
- data/app/components/panda/cms/admin/statistics_component.html.erb +4 -0
- data/app/components/panda/cms/admin/statistics_component.rb +17 -0
- data/app/components/panda/cms/admin/tab_bar_component.html.erb +35 -0
- data/app/components/panda/cms/admin/tab_bar_component.rb +15 -0
- data/app/components/panda/cms/admin/table_component.html.erb +29 -0
- data/app/components/panda/cms/admin/table_component.rb +46 -0
- data/app/components/panda/cms/admin/tag_component.rb +35 -0
- data/app/components/panda/cms/admin/user_activity_component.html.erb +5 -0
- data/app/components/panda/cms/admin/user_activity_component.rb +33 -0
- data/app/components/panda/cms/admin/user_display_component.html.erb +17 -0
- data/app/components/panda/cms/admin/user_display_component.rb +21 -0
- data/app/components/panda/cms/code_component.rb +64 -0
- data/app/components/panda/cms/grid_component.html.erb +6 -0
- data/app/components/panda/cms/grid_component.rb +15 -0
- data/app/components/panda/cms/menu_component.html.erb +6 -0
- data/app/components/panda/cms/menu_component.rb +58 -0
- data/app/components/panda/cms/page_menu_component.html.erb +21 -0
- data/app/components/panda/cms/page_menu_component.rb +38 -0
- data/app/components/panda/cms/rich_text_component.html.erb +6 -0
- data/app/components/panda/cms/rich_text_component.rb +84 -0
- data/app/components/panda/cms/text_component.rb +72 -0
- data/app/constraints/panda/cms/admin_constraint.rb +18 -0
- data/app/controllers/panda/cms/admin/block_contents_controller.rb +52 -0
- data/app/controllers/panda/cms/admin/dashboard_controller.rb +20 -0
- data/app/controllers/panda/cms/admin/files_controller.rb +21 -0
- data/app/controllers/panda/cms/admin/forms_controller.rb +53 -0
- data/app/controllers/panda/cms/admin/menus_controller.rb +30 -0
- data/app/controllers/panda/cms/admin/pages_controller.rb +91 -0
- data/app/controllers/panda/cms/admin/posts_controller.rb +146 -0
- data/app/controllers/panda/cms/admin/sessions_controller.rb +94 -0
- data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +37 -0
- data/app/controllers/panda/cms/admin/settings_controller.rb +20 -0
- data/app/controllers/panda/cms/application_controller.rb +57 -0
- data/app/controllers/panda/cms/errors_controller.rb +33 -0
- data/app/controllers/panda/cms/form_submissions_controller.rb +23 -0
- data/app/controllers/panda/cms/pages_controller.rb +72 -0
- data/app/controllers/panda/cms/posts_controller.rb +13 -0
- data/app/helpers/panda/cms/admin/files_helper.rb +6 -0
- data/app/helpers/panda/cms/admin/pages_helper.rb +6 -0
- data/app/helpers/panda/cms/admin/posts_helper.rb +48 -0
- data/app/helpers/panda/cms/application_helper.rb +120 -0
- data/app/helpers/panda/cms/pages_helper.rb +6 -0
- data/app/helpers/panda/cms/theme_helper.rb +18 -0
- data/app/javascript/panda/cms/@editorjs--editorjs.js +2577 -0
- data/app/javascript/panda/cms/@hotwired--stimulus.js +4 -0
- data/app/javascript/panda/cms/@hotwired--turbo.js +160 -0
- data/app/javascript/panda/cms/@rails--actioncable--src.js +4 -0
- data/app/javascript/panda/cms/application_panda_cms.js +39 -0
- data/app/javascript/panda/cms/controllers/dashboard_controller.js +7 -0
- data/app/javascript/panda/cms/controllers/editor_form_controller.js +77 -0
- data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +320 -0
- data/app/javascript/panda/cms/controllers/index.js +48 -0
- data/app/javascript/panda/cms/controllers/slug_controller.js +87 -0
- data/app/javascript/panda/cms/editor/css_extractor.js +80 -0
- data/app/javascript/panda/cms/editor/editor_js_config.js +177 -0
- data/app/javascript/panda/cms/editor/editor_js_initializer.js +285 -0
- data/app/javascript/panda/cms/editor/plain_text_editor.js +110 -0
- data/app/javascript/panda/cms/editor/resource_loader.js +115 -0
- data/app/javascript/panda/cms/tailwindcss-stimulus-components.js +4 -0
- data/app/jobs/panda/cms/application_job.rb +6 -0
- data/app/jobs/panda/cms/record_visit_job.rb +31 -0
- data/app/mailers/panda/cms/application_mailer.rb +8 -0
- data/app/mailers/panda/cms/form_mailer.rb +21 -0
- data/app/models/action_text/rich_text_version.rb +6 -0
- data/app/models/panda/cms/application_record.rb +7 -0
- data/app/models/panda/cms/block.rb +34 -0
- data/app/models/panda/cms/block_content.rb +18 -0
- data/app/models/panda/cms/block_content_version.rb +8 -0
- data/app/models/panda/cms/breadcrumb.rb +12 -0
- data/app/models/panda/cms/current.rb +17 -0
- data/app/models/panda/cms/form.rb +9 -0
- data/app/models/panda/cms/form_submission.rb +7 -0
- data/app/models/panda/cms/menu.rb +52 -0
- data/app/models/panda/cms/menu_item.rb +58 -0
- data/app/models/panda/cms/page.rb +96 -0
- data/app/models/panda/cms/page_version.rb +8 -0
- data/app/models/panda/cms/post.rb +60 -0
- data/app/models/panda/cms/post_version.rb +8 -0
- data/app/models/panda/cms/redirect.rb +11 -0
- data/app/models/panda/cms/template.rb +124 -0
- data/app/models/panda/cms/template_version.rb +8 -0
- data/app/models/panda/cms/user.rb +31 -0
- data/app/models/panda/cms/version.rb +8 -0
- data/app/models/panda/cms/visit.rb +9 -0
- data/app/services/panda/cms/html_to_editor_js_converter.rb +200 -0
- data/app/views/active_storage/blobs/blobs/_blob.html.erb +14 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/app/views/layouts/panda/cms/application.html.erb +41 -0
- data/app/views/layouts/panda/cms/public.html.erb +3 -0
- data/app/views/panda/cms/admin/dashboard/show.html.erb +12 -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 +15 -0
- data/app/views/panda/cms/admin/forms/show.html.erb +35 -0
- data/app/views/panda/cms/admin/menus/index.html.erb +8 -0
- data/app/views/panda/cms/admin/pages/edit.html.erb +36 -0
- data/app/views/panda/cms/admin/pages/index.html.erb +22 -0
- data/app/views/panda/cms/admin/pages/new.html.erb +15 -0
- data/app/views/panda/cms/admin/pages/show.html.erb +1 -0
- data/app/views/panda/cms/admin/posts/_form.html.erb +29 -0
- data/app/views/panda/cms/admin/posts/edit.html.erb +6 -0
- data/app/views/panda/cms/admin/posts/index.html.erb +18 -0
- data/app/views/panda/cms/admin/posts/new.html.erb +6 -0
- data/app/views/panda/cms/admin/sessions/new.html.erb +17 -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 +21 -0
- data/app/views/panda/cms/admin/settings/insta.html +4 -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 +41 -0
- data/app/views/panda/cms/form_mailer/notification_email.html.erb +11 -0
- data/app/views/panda/cms/shared/_editor.html.erb +0 -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/app/views/panda/cms/shared/_importmap.html.erb +33 -0
- data/config/importmap.rb +13 -0
- data/config/initializers/inflections.rb +3 -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/paper_trail.rb +7 -0
- data/config/initializers/panda/cms.rb +10 -0
- data/config/initializers/zeitwork.rb +3 -0
- data/config/locales/en.yml +49 -0
- data/config/puma/test.rb +9 -0
- data/config/routes.rb +48 -0
- data/config/tailwind.config.js +37 -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/20240305000000_convert_html_content_to_editor_js.rb +82 -0
- data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +9 -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/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/migrate/20240904200605_create_action_text_tables.action_text.rb +24 -0
- data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +11 -0
- data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +5 -0
- data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +35 -0
- data/db/migrate/20241119214549_remove_action_text_from_posts.rb +9 -0
- data/db/migrate/20241120000419_remove_post_tag_references.rb +19 -0
- data/db/migrate/20241120110943_add_editor_js_to_posts.rb +27 -0
- data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +5 -0
- data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +5 -0
- data/db/migrate/migrate +1 -0
- data/db/seeds.rb +5 -0
- data/lib/generators/panda/cms/install_generator.rb +29 -0
- data/lib/panda/cms/bulk_editor.rb +171 -0
- data/lib/panda/cms/demo_site_generator.rb +67 -0
- data/lib/panda/cms/editor_js/blocks/alert.rb +34 -0
- data/lib/panda/cms/editor_js/blocks/base.rb +33 -0
- data/lib/panda/cms/editor_js/blocks/header.rb +15 -0
- data/lib/panda/cms/editor_js/blocks/image.rb +36 -0
- data/lib/panda/cms/editor_js/blocks/list.rb +32 -0
- data/lib/panda/cms/editor_js/blocks/paragraph.rb +15 -0
- data/lib/panda/cms/editor_js/blocks/quote.rb +41 -0
- data/lib/panda/cms/editor_js/blocks/table.rb +50 -0
- data/lib/panda/cms/editor_js/renderer.rb +124 -0
- data/lib/panda/cms/editor_js.rb +16 -0
- data/lib/panda/cms/editor_js_content.rb +21 -0
- data/lib/panda/cms/engine.rb +257 -0
- data/lib/panda/cms/exceptions_app.rb +26 -0
- data/lib/panda/cms/railtie.rb +11 -0
- data/lib/panda/cms/slug.rb +24 -0
- data/lib/panda/cms.rb +0 -0
- data/lib/panda-cms/version.rb +5 -0
- data/lib/panda-cms.rb +81 -0
- data/lib/tasks/panda_cms.rake +54 -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/panda-logo-screenprint.png +0 -0
- data/public/panda-cms-assets/panda-nav.png +0 -0
- data/public/panda-cms-assets/rich_text_editor.css +568 -0
- metadata +654 -0
@@ -0,0 +1,4 @@
|
|
1
|
+
// tailwindcss-stimulus-components@6.1.2 downloaded from https://ga.jspm.io/npm:tailwindcss-stimulus-components@6.1.2/dist/tailwindcss-stimulus-components.module.js
|
2
|
+
|
3
|
+
import{Controller as e}from"@hotwired/stimulus";var t=Object.defineProperty;var V=(e,s,a)=>s in e?t(e,s,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[s]=a;var i=(e,t,s)=>V(e,typeof t!="symbol"?t+"":t,s);async function n(e,t,s={}){t?await T(e,s):await b(e,s)}async function T(e,t={}){let{transitionClasses:s,fromClasses:a,toClasses:o,toggleClass:r}=C("Enter",e,t);return v(e,{firstFrame(){e.classList.add(...s.split(" ")),e.classList.add(...a.split(" ")),e.classList.remove(...o.split(" ")),e.classList.remove(...r.split(" "))},secondFrame(){e.classList.remove(...a.split(" ")),e.classList.add(...o.split(" "))},ending(){e.classList.remove(...s.split(" "))}})}async function b(e,t={}){let{transitionClasses:s,fromClasses:a,toClasses:o,toggleClass:r}=C("Leave",e,t);return v(e,{firstFrame(){e.classList.add(...a.split(" ")),e.classList.remove(...o.split(" ")),e.classList.add(...s.split(" "))},secondFrame(){e.classList.remove(...a.split(" ")),e.classList.add(...o.split(" "))},ending(){e.classList.remove(...s.split(" ")),e.classList.add(...r.split(" "))}})}function C(e,t,s){return{transitionClasses:t.dataset[`transition${e}`]||s[e.toLowerCase()]||e.toLowerCase(),fromClasses:t.dataset[`transition${e}From`]||s[`${e.toLowerCase()}From`]||`${e.toLowerCase()}-from`,toClasses:t.dataset[`transition${e}To`]||s[`${e.toLowerCase()}To`]||`${e.toLowerCase()}-to`,toggleClass:t.dataset.toggleClass||s.toggleClass||s.toggle||"hidden"}}function L(e){e._stimulus_transition={timeout:null,interrupted:!1}}function I(e){e._stimulus_transition&&e._stimulus_transition.interrupt&&e._stimulus_transition.interrupt()}function v(e,t){e._stimulus_transition&&I(e);let s,a,o;return L(e),e._stimulus_transition.cleanup=()=>{a||t.firstFrame(),o||t.secondFrame(),t.ending(),e._stimulus_transition=null},e._stimulus_transition.interrupt=()=>{s=!0,e._stimulus_transition.timeout&&clearTimeout(e._stimulus_transition.timeout),e._stimulus_transition.cleanup()},new Promise((r=>{s||requestAnimationFrame((()=>{s||(t.firstFrame(),a=!0,requestAnimationFrame((()=>{s||(t.secondFrame(),o=!0,e._stimulus_transition&&(e._stimulus_transition.timeout=setTimeout((()=>{s||e._stimulus_transition.cleanup(),r()}),w(e))))})))}))}))}function w(e){let t=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,s=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;return t===0&&(t=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),t+s}var s=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()}))}};i(s,"values",{dismissAfter:Number,showDelay:{type:Number,default:0}});var a=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)}};i(a,"targets",["form","status"]),i(a,"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 o=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,s=parseInt(e.substr(0,2),16),a=parseInt(e.substr(2,2),16),o=parseInt(e.substr(4,2),16);return(s*299+a*587+o*114)/1e3>=t?"#000":"#fff"}};i(o,"targets",["preview","color"]),i(o,"values",{style:{type:String,default:"backgroundColor"}});var r=class extends e{connect(){this.boundBeforeCache=this.beforeCache.bind(this),document.addEventListener("turbo:before-cache",this.boundBeforeCache)}disconnect(){document.removeEventListener("turbo:before-cache",this.boundBeforeCache)}openValueChanged(){n(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")}};i(r,"targets",["menu","button","menuItem"]),i(r,"values",{open:{type:Boolean,default:!1},closeOnEscape:{type:Boolean,default:!0},closeOnClickOutside:{type:Boolean,default:!0}}),i(r,"classes",["enter","enterFrom","enterTo","leave","leaveFrom","leaveTo","toggle"]);var l=class extends e{connect(){this.openValue&&this.open(),this.boundBeforeCache=this.beforeCache.bind(this),document.addEventListener("turbo:before-cache",this.boundBeforeCache)}disconnect(){document.removeEventListener("turbo:before-cache",this.boundBeforeCache)}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()}};i(l,"targets",["dialog"]),i(l,"values",{open:Boolean});var u=class extends e{openValueChanged(){n(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)}};i(u,"targets",["content"]),i(u,"values",{dismissAfter:Number,open:{type:Boolean,default:!1}});var h=class extends e{connect(){this.openValue&&this.open(),this.boundBeforeCache=this.beforeCache,document.addEventListener("turbo:before-cache",this.boundBeforeCache)}disconnect(){document.removeEventListener("turbo:before-cache",this.boundBeforeCache)}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()}};i(h,"targets",["dialog"]),i(h,"values",{open:Boolean});var c=class extends e{initialize(){this.updateAnchorValue&&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;typeof Turbo<"u"?Turbo.navigator.history.replace(new URL(t)):history.replaceState({},document.title,t)}}}showTab(){this.panelTargets.forEach(((e,t)=>{let s=this.tabTargets[t];t===this.indexValue?(e.classList.remove("hidden"),s.ariaSelected="true",s.dataset.active=!0,this.hasInactiveTabClass&&s?.classList?.remove(...this.inactiveTabClasses),this.hasActiveTabClass&&s?.classList?.add(...this.activeTabClasses)):(e.classList.add("hidden"),s.ariaSelected=null,delete s.dataset.active,this.hasActiveTabClass&&s?.classList?.remove(...this.activeTabClasses),this.hasInactiveTabClass&&s?.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}};i(c,"classes",["activeTab","inactiveTab"]),i(c,"targets",["tab","panel","select"]),i(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=>{n(e,this.openValue)}))}};i(d,"targets",["toggleable"]),i(d,"values",{open:{type:Boolean,default:!1}});export{s as Alert,a as Autosave,o as ColorPreview,r as Dropdown,l as Modal,u as Popover,h as Slideover,c as Tabs,d as Toggle,n as transition};
|
4
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Panda
|
2
|
+
module CMS
|
3
|
+
class RecordVisitJob < ApplicationJob
|
4
|
+
queue_as :default
|
5
|
+
|
6
|
+
def perform(
|
7
|
+
url: nil,
|
8
|
+
user_agent: nil,
|
9
|
+
referrer: nil,
|
10
|
+
ip_address: nil,
|
11
|
+
page_id: nil,
|
12
|
+
current_user_id: nil,
|
13
|
+
params: [],
|
14
|
+
visited_at: nil,
|
15
|
+
redirect_id: nil
|
16
|
+
)
|
17
|
+
Panda::CMS::Visit.create!(
|
18
|
+
url: url,
|
19
|
+
user_agent: user_agent,
|
20
|
+
referrer: referrer,
|
21
|
+
ip_address: ip_address,
|
22
|
+
page_id: page_id,
|
23
|
+
redirect_id: redirect_id,
|
24
|
+
user_id: current_user_id,
|
25
|
+
params: params,
|
26
|
+
visited_at: visited_at
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Panda
|
2
|
+
module CMS
|
3
|
+
class FormMailer < Panda::CMS::ApplicationMailer
|
4
|
+
def notification_email(form:, form_submission:)
|
5
|
+
# TODO: Handle fields named just "name", and "email" better
|
6
|
+
@submission_data = form_submission.data
|
7
|
+
@sender_name = @submission_data["first_name"].to_s + " " + @submission_data["last_name"].to_s
|
8
|
+
@sender_email = @submission_data["email"].to_s
|
9
|
+
|
10
|
+
mail(
|
11
|
+
subject: "#{form.name}: #{form_submission.created_at.strftime("%d %b %Y %H:%M")}",
|
12
|
+
to: email_address_with_name("james@otaina.co.uk", "James Inman"),
|
13
|
+
from: email_address_with_name("noreply@pandacms.io", "Panda CMS"),
|
14
|
+
reply_to: email_address_with_name(@sender_email, @sender_name),
|
15
|
+
track_opens: "true",
|
16
|
+
message_stream: "outbound"
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Panda
|
2
|
+
module CMS
|
3
|
+
class Block < ApplicationRecord
|
4
|
+
self.table_name = "panda_cms_blocks"
|
5
|
+
|
6
|
+
belongs_to :template, foreign_key: :panda_cms_template_id, class_name: "Panda::CMS::Template", inverse_of: :blocks, optional: true
|
7
|
+
has_many :block_contents, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::BlockContent", inverse_of: :block
|
8
|
+
|
9
|
+
validates :kind, presence: true
|
10
|
+
validates :name, presence: true
|
11
|
+
validates :key, presence: true, uniqueness: {scope: :panda_cms_template_id, case_sensitive: false}
|
12
|
+
|
13
|
+
# Validation for presence on template intentionally skipped to allow global elements
|
14
|
+
|
15
|
+
# NB: Commented out values are not yet implemented
|
16
|
+
enum :kind, {
|
17
|
+
plain_text: "plain_text",
|
18
|
+
rich_text: "rich_text",
|
19
|
+
iframe: "iframe",
|
20
|
+
list: "list",
|
21
|
+
code: "code"
|
22
|
+
# image: "image",
|
23
|
+
# video: "video",
|
24
|
+
# audio: "audio",
|
25
|
+
# file: "file",
|
26
|
+
# iframe: "iframe",
|
27
|
+
# quote: "quote",
|
28
|
+
# list: "list"
|
29
|
+
# table: "table",
|
30
|
+
# form: "form"
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Panda
|
2
|
+
module CMS
|
3
|
+
class BlockContent < ApplicationRecord
|
4
|
+
include EditorJsContent
|
5
|
+
|
6
|
+
self.table_name = "panda_cms_block_contents"
|
7
|
+
|
8
|
+
has_paper_trail versions: {
|
9
|
+
class_name: "Panda::CMS::BlockContentVersion"
|
10
|
+
}
|
11
|
+
|
12
|
+
belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::Page", inverse_of: :block_contents, optional: true, touch: true
|
13
|
+
belongs_to :block, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::Block", inverse_of: :block_contents, optional: false
|
14
|
+
|
15
|
+
validates :block, presence: true, uniqueness: {scope: :page}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Panda
|
2
|
+
module CMS
|
3
|
+
class Current < ActiveSupport::CurrentAttributes
|
4
|
+
attribute :root, :page
|
5
|
+
attribute :user
|
6
|
+
attribute :request_id, :user_agent, :ip_address
|
7
|
+
|
8
|
+
# resets { Time.zone = nil }
|
9
|
+
|
10
|
+
# def user=(user)
|
11
|
+
# super
|
12
|
+
# self.account = user.account
|
13
|
+
# Time.zone = user.time_zone
|
14
|
+
# end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Panda
|
2
|
+
module CMS
|
3
|
+
class Menu < ApplicationRecord
|
4
|
+
self.table_name = "panda_cms_menus"
|
5
|
+
|
6
|
+
after_save :generate_auto_menu_items, if: -> { kind == "auto" }
|
7
|
+
|
8
|
+
has_many :menu_items, -> { order(lft: :asc) }, foreign_key: :panda_cms_menu_id, class_name: "Panda::CMS::MenuItem", inverse_of: :menu
|
9
|
+
belongs_to :start_page, class_name: "Panda::CMS::Page", foreign_key: "start_page_id", inverse_of: :page_menu, optional: true
|
10
|
+
|
11
|
+
accepts_nested_attributes_for :menu_items, reject_if: :all_blank, allow_destroy: true
|
12
|
+
|
13
|
+
validates :name, presence: true, uniqueness: {case_sensitive: false}
|
14
|
+
validates :kind, presence: true, inclusion: {in: ["static", "auto"]}
|
15
|
+
validate :validate_start_page
|
16
|
+
|
17
|
+
def generate_auto_menu_items
|
18
|
+
return false if kind != "auto"
|
19
|
+
|
20
|
+
# NB: Transactions are not distributed across database connections
|
21
|
+
transaction do
|
22
|
+
menu_items.destroy_all
|
23
|
+
menu_item_root = menu_items.create(text: start_page.title, panda_cms_page_id: start_page.id)
|
24
|
+
generate_menu_items(parent_menu_item: menu_item_root, parent_page: start_page)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def generate_menu_items(parent_menu_item:, parent_page:)
|
31
|
+
parent_page.children.where(status: [:active]).each do |page|
|
32
|
+
menu_item = menu_items.create(text: page.title, panda_cms_page_id: page.id, parent: parent_menu_item)
|
33
|
+
if page.children
|
34
|
+
generate_menu_items(parent_menu_item: menu_item, parent_page: page)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Validate that the start page is set if the menu is of kind auto
|
41
|
+
#
|
42
|
+
# @return nil
|
43
|
+
# @visibility private
|
44
|
+
#
|
45
|
+
def validate_start_page
|
46
|
+
if kind == "auto" && start_page.nil?
|
47
|
+
errors.add(:start_page, "can't be blank")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "awesome_nested_set"
|
2
|
+
|
3
|
+
module Panda
|
4
|
+
module CMS
|
5
|
+
class MenuItem < ApplicationRecord
|
6
|
+
acts_as_nested_set scope: [:panda_cms_menu_id], counter_cache: :children_count
|
7
|
+
|
8
|
+
self.implicit_order_column = "lft"
|
9
|
+
self.table_name = "panda_cms_menu_items"
|
10
|
+
|
11
|
+
belongs_to :menu, foreign_key: :panda_cms_menu_id, class_name: "Panda::CMS::Menu", inverse_of: :menu_items, touch: true
|
12
|
+
belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::Page", inverse_of: :menu_items, optional: true
|
13
|
+
|
14
|
+
validates :text, presence: true, uniqueness: {scope: :panda_cms_menu_id, case_sensitive: false}
|
15
|
+
validates :page, presence: true, unless: -> { external_url.present? }
|
16
|
+
validates :external_url, presence: true, unless: -> { page.present? }
|
17
|
+
|
18
|
+
validate :validate_is_actual_link
|
19
|
+
|
20
|
+
#
|
21
|
+
# Returns the resolved link for the menu item.
|
22
|
+
#
|
23
|
+
# If the menu item is associated with a page, it returns the path of the page.
|
24
|
+
# If the menu item is associated with an external URL, it returns the external URL.
|
25
|
+
#
|
26
|
+
# @return [String] Resolved link
|
27
|
+
# @visibility public
|
28
|
+
def resolved_link
|
29
|
+
if page
|
30
|
+
page.path
|
31
|
+
elsif external_url
|
32
|
+
external_url
|
33
|
+
else
|
34
|
+
""
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
#
|
41
|
+
# Validate that the link is an actual link or a page
|
42
|
+
#
|
43
|
+
# @return nil
|
44
|
+
# @visibility private
|
45
|
+
def validate_is_actual_link
|
46
|
+
if page.nil? && external_url.nil?
|
47
|
+
errors.add(:page, "must be a valid page or external link, neither are set")
|
48
|
+
errors.add(:external_url, "must be a valid page or external link, neither are set")
|
49
|
+
end
|
50
|
+
|
51
|
+
if !page.nil? && !external_url.nil?
|
52
|
+
errors.add(:page, "must be a valid page or external link, both are set")
|
53
|
+
errors.add(:external_url, "must be a valid page or external link, both are set")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "awesome_nested_set"
|
2
|
+
|
3
|
+
module Panda
|
4
|
+
module CMS
|
5
|
+
class Page < ApplicationRecord
|
6
|
+
acts_as_nested_set counter_cache: :children_count
|
7
|
+
self.table_name = "panda_cms_pages"
|
8
|
+
self.implicit_order_column = "lft"
|
9
|
+
|
10
|
+
has_paper_trail versions: {
|
11
|
+
class_name: "Panda::CMS::PageVersion"
|
12
|
+
}
|
13
|
+
|
14
|
+
after_save :after_save
|
15
|
+
|
16
|
+
belongs_to :template, foreign_key: :panda_cms_template_id, class_name: "Panda::CMS::Template", inverse_of: :pages, optional: false, counter_cache: :pages_count
|
17
|
+
has_many :blocks, through: :template
|
18
|
+
has_many :block_contents, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::BlockContent", inverse_of: :page
|
19
|
+
has_many :menu_items, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::MenuItem", inverse_of: :page
|
20
|
+
has_many :menus, through: :menu_items
|
21
|
+
has_many :menus_of_parent, through: :parent, source: :menus
|
22
|
+
has_one :page_menu, foreign_key: :start_page_id, class_name: "Panda::CMS::Menu"
|
23
|
+
|
24
|
+
validates :title, presence: true
|
25
|
+
|
26
|
+
validates :path,
|
27
|
+
presence: true,
|
28
|
+
uniqueness: true,
|
29
|
+
format: {with: /\A\/.*\z/, message: "must start with a forward slash"}
|
30
|
+
|
31
|
+
validates :parent,
|
32
|
+
presence: true,
|
33
|
+
unless: -> { path == "/" }
|
34
|
+
|
35
|
+
validates :panda_cms_template_id,
|
36
|
+
presence: true
|
37
|
+
|
38
|
+
scope :ordered, -> { order(:lft) }
|
39
|
+
|
40
|
+
enum :status, {
|
41
|
+
active: "active",
|
42
|
+
draft: "draft",
|
43
|
+
hidden: "hidden",
|
44
|
+
archived: "archived"
|
45
|
+
}
|
46
|
+
|
47
|
+
#
|
48
|
+
# Update any menus which include this page or its parent as a menu item
|
49
|
+
#
|
50
|
+
# @return nil
|
51
|
+
# @visibility public
|
52
|
+
#
|
53
|
+
def update_auto_menus
|
54
|
+
menus.find_each(&:generate_auto_menu_items)
|
55
|
+
menus_of_parent.find_each(&:generate_auto_menu_items)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
#
|
61
|
+
# After save callbacks
|
62
|
+
#
|
63
|
+
# @return nil
|
64
|
+
# @visibility private
|
65
|
+
#
|
66
|
+
def after_save
|
67
|
+
generate_content_blocks
|
68
|
+
update_existing_menu_items
|
69
|
+
update_auto_menus
|
70
|
+
end
|
71
|
+
|
72
|
+
def generate_content_blocks
|
73
|
+
template_block_ids = template.blocks.ids
|
74
|
+
page_existing_block_ids = block_contents.map { |bc| bc.block.id }
|
75
|
+
required_block_ids = template_block_ids - page_existing_block_ids
|
76
|
+
|
77
|
+
if required_block_ids.count > 0
|
78
|
+
required_block_ids.each do |block_id|
|
79
|
+
Panda::CMS::BlockContent.find_or_create_by!(page: self, panda_cms_block_id: block_id, content: "")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Update text of existing menu items if the title differs
|
86
|
+
#
|
87
|
+
# @return nil
|
88
|
+
# @todo Only run this if the page title has changed
|
89
|
+
# @visibility private
|
90
|
+
#
|
91
|
+
def update_existing_menu_items
|
92
|
+
menu_items.where.not(text: title).update_all(text: title)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "awesome_nested_set"
|
2
|
+
|
3
|
+
module Panda
|
4
|
+
module CMS
|
5
|
+
class Post < ApplicationRecord
|
6
|
+
include ::Panda::CMS::EditorJsContent
|
7
|
+
|
8
|
+
self.table_name = "panda_cms_posts"
|
9
|
+
|
10
|
+
has_paper_trail versions: {
|
11
|
+
class_name: "Panda::CMS::PostVersion"
|
12
|
+
}
|
13
|
+
|
14
|
+
belongs_to :user, class_name: "Panda::CMS::User"
|
15
|
+
|
16
|
+
validates :title, presence: true
|
17
|
+
validates :slug,
|
18
|
+
presence: true,
|
19
|
+
uniqueness: true,
|
20
|
+
format: {
|
21
|
+
with: /\A\/[a-z0-9-]+\z/,
|
22
|
+
message: "must start with a forward slash and contain only lowercase letters, numbers, and hyphens"
|
23
|
+
}
|
24
|
+
|
25
|
+
scope :ordered, -> { order(published_at: :desc) }
|
26
|
+
scope :with_user, -> { includes(:user) }
|
27
|
+
|
28
|
+
enum :status, {
|
29
|
+
active: "active",
|
30
|
+
draft: "draft",
|
31
|
+
hidden: "hidden",
|
32
|
+
archived: "archived"
|
33
|
+
}
|
34
|
+
|
35
|
+
def to_param
|
36
|
+
slug.delete_prefix("/")
|
37
|
+
end
|
38
|
+
|
39
|
+
def admin_param
|
40
|
+
id
|
41
|
+
end
|
42
|
+
|
43
|
+
def excerpt(length = 100, squish: true)
|
44
|
+
return "" if content.blank?
|
45
|
+
|
46
|
+
text = if content.is_a?(Hash) && content["blocks"]
|
47
|
+
content["blocks"]
|
48
|
+
.select { |block| block["type"] == "paragraph" }
|
49
|
+
.map { |block| block["data"]["text"] }
|
50
|
+
.join(" ")
|
51
|
+
else
|
52
|
+
content.to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
text = text.squish if squish
|
56
|
+
text.truncate(length).html_safe
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Panda
|
2
|
+
module CMS
|
3
|
+
class Redirect < ApplicationRecord
|
4
|
+
belongs_to :origin_page, class_name: "Panda::CMS::Page", foreign_key: :origin_panda_cms_page_id
|
5
|
+
belongs_to :destination_page, class_name: "Panda::CMS::Page", foreign_key: :destination_panda_cms_page_id
|
6
|
+
|
7
|
+
validates :status_code, presence: true
|
8
|
+
validates :visits, presence: true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Panda
|
2
|
+
module CMS
|
3
|
+
# Represents a template in the Panda CMS application.
|
4
|
+
class Template < ApplicationRecord
|
5
|
+
self.table_name = "panda_cms_templates"
|
6
|
+
|
7
|
+
# Enables versioning for the Template model using the `has_paper_trail` gem.
|
8
|
+
has_paper_trail versions: {
|
9
|
+
class_name: "Panda::CMS::TemplateVersion"
|
10
|
+
}
|
11
|
+
|
12
|
+
# Associations
|
13
|
+
has_many :pages, class_name: "Panda::CMS::Page", dependent: :restrict_with_error, inverse_of: :template, foreign_key: :panda_cms_template_id
|
14
|
+
has_many :blocks, class_name: "Panda::CMS::Block", dependent: :restrict_with_error, inverse_of: :template, foreign_key: :panda_cms_template_id
|
15
|
+
has_many :block_contents, through: :blocks
|
16
|
+
|
17
|
+
# Validations
|
18
|
+
validates :name, presence: true, uniqueness: true
|
19
|
+
|
20
|
+
validates :file_path,
|
21
|
+
presence: true,
|
22
|
+
uniqueness: true,
|
23
|
+
format: {with: /\Alayouts\/.*\z/, message: "must be a valid layout file path"}
|
24
|
+
|
25
|
+
validate :validate_template_file_exists
|
26
|
+
|
27
|
+
# Scopes
|
28
|
+
scope :available, -> {
|
29
|
+
where("max_uses IS NULL OR (pages_count IS NOT NULL AND pages_count < max_uses)")
|
30
|
+
}
|
31
|
+
|
32
|
+
def self.default
|
33
|
+
find_by(file_path: "layouts/page")
|
34
|
+
end
|
35
|
+
|
36
|
+
# Generate missing blocks for all templates
|
37
|
+
# @return [void]
|
38
|
+
def self.generate_missing_blocks
|
39
|
+
# Loop through all templates in app/views/layouts/*.html.erb
|
40
|
+
Dir.glob("app/views/layouts/*.html.erb").each do |file|
|
41
|
+
# TODO: Delete all blocks which aren't in use by a template?
|
42
|
+
|
43
|
+
File.open(file).each_line do |line|
|
44
|
+
# Matches:
|
45
|
+
# Panda::CMS::RichTextComponent.new(key: :value)
|
46
|
+
# Panda::CMS::RichTextComponent.new key: :value, key: value
|
47
|
+
line.match(/Panda::CMS::([a-zA-Z]+)Component\.new[ \(]+([^\)]+)[\)]*/) do |match|
|
48
|
+
# Extract the hash values
|
49
|
+
template_path = file.gsub("app/views/", "").gsub(".html.erb", "")
|
50
|
+
template_name = template_path.gsub("layouts/", "").titleize
|
51
|
+
|
52
|
+
# Create the template if it doesn't exist
|
53
|
+
template = Panda::CMS::Template.find_or_create_by!(file_path: template_path) do |template|
|
54
|
+
template.name = template_name
|
55
|
+
end
|
56
|
+
|
57
|
+
next if match[1] == "PageMenu" # Skip PageMenu blocks
|
58
|
+
next if match[1] == "Menu" # Skip Menu blocks
|
59
|
+
|
60
|
+
# Previously used match[1].underscore but this supports more complex database
|
61
|
+
# operations, and is more secure as it'll force the usage of a class
|
62
|
+
block_kind = "Panda::CMS::#{match[1]}Component".constantize::KIND
|
63
|
+
|
64
|
+
match[2].split(",").map do |keyvar|
|
65
|
+
key, value = keyvar.split(":", 2)
|
66
|
+
next if key != "key"
|
67
|
+
|
68
|
+
block_name = value.to_s.strip.tr(":", "")
|
69
|
+
# Create the block if it doesn't exist
|
70
|
+
# TODO: +/- the output if it's created or removed
|
71
|
+
begin
|
72
|
+
block = Panda::CMS::Block.find_or_create_by!(template: template, kind: block_kind, key: block_name) do |block|
|
73
|
+
block.name = block_name.titleize
|
74
|
+
end
|
75
|
+
rescue ActiveRecord::RecordInvalid => e
|
76
|
+
raise "Error creating block '#{block_name}' on template '#{template_name}': #{e.message}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# For the given block, create the block_content for each page using the template
|
80
|
+
template.pages.each do |page|
|
81
|
+
Panda::CMS::BlockContent.find_or_create_by!(block: block, page: page) do |block_content|
|
82
|
+
block_content.content = {}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Custom validation method to check if the file_path is a valid layout file path
|
94
|
+
# NB: Currently only supports .html.erb templates, may want to expand in future?
|
95
|
+
# @return [void]
|
96
|
+
def validate_template_file_exists
|
97
|
+
# Remove any directory traversal attempts from the file_path
|
98
|
+
safe_file_path = file_path.to_s.gsub("../", "")
|
99
|
+
# Check if the file_path is an ERB template that exists in app/views
|
100
|
+
template_path = Rails.root.join("app", "views", "#{safe_file_path}.html.erb")
|
101
|
+
# NB: file? checks for files and excludes directories (unlike exist?)
|
102
|
+
errors.add(:file_path, "must be an existing layout file path") unless File.file?(template_path)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Import templates from the filesystem into the database
|
106
|
+
# @return [void]
|
107
|
+
def self.load_from_filesystem
|
108
|
+
Rails.root.glob("app/views/layouts/**/*.html.erb").each do |file|
|
109
|
+
# Extract the file path from the Rails root
|
110
|
+
file_path = file.to_s.sub("#{Rails.root}/app/views/", "").sub(".html.erb", "")
|
111
|
+
|
112
|
+
next if file_path == "layouts/application" || file_path == "layouts/mailer"
|
113
|
+
|
114
|
+
# Find or create the template based on the file path
|
115
|
+
find_or_create_by(file_path: file_path) do |t|
|
116
|
+
t.name = file_path.sub("layouts/", "").titleize
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private_class_method :load_from_filesystem
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|