This commit is contained in:
Tony Air 2021-06-19 21:30:03 +02:00
parent dca97bb394
commit 3c0d9263c5
87 changed files with 4409 additions and 0 deletions

8
.idea/cms-niceties.iml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cms-niceties.iml" filepath="$PROJECT_DIR$/.idea/cms-niceties.iml" />
</modules>
</component>
</project>

39
_config.php Executable file
View File

@ -0,0 +1,39 @@
<?php
use SilverStripe\Forms\HTMLEditor\HtmlEditorConfig;
use SilverStripe\Core\Manifest\ModuleResourceLoader;
use SilverStripe\ORM\Search\FulltextSearchable;
/*use SilverStripe\View\Parsers\ShortcodeParser;
use A2nt\CMSNiceties\Extensions\EmbedShortcodeProvider;*/
// setup TinyMCE editor
$config = HtmlEditorConfig::get('cms');
$config->enablePlugins([
'template',
'fullscreen',
'hr',
'contextmenu',
'charmap',
'visualblocks',
'lists',
'charcount' => ModuleResourceLoader::resourceURL(
'drmartingonzo/ss-tinymce-charcount:client/dist/js/bundle.js'
),
]);
$config->addButtonsToLine(2, 'hr');
$config->setOption('block_formats', 'Paragraph=p;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Address=address;Pre=pre');
$config->setOption('invalid_elements', 'h1');
$config->setOption(
'table_class_list',
[
['title' => 'Transparent Table', 'value' => 'table-none'],
['title' => 'Shaded rows', 'value' => 'table table-striped table-bordered'],
]
);
FulltextSearchable::enable();
// replace embed parser
/*$parser = ShortcodeParser::get('default');
$parser->unregister('embed');
$parser->register('embed', [EmbedShortcodeProvider::class, 'handle_shortcode']);*/

28
_config/base-config.yml Executable file
View File

@ -0,0 +1,28 @@
---
Name: webapp-base-config
---
SilverStripe\Core\Manifest\ModuleManifest:
project: app
Page:
default_container_class: 'container'
#SilverStripe\Admin\LeftAndMain:
# extra_requirements_javascript:
# - 'colymba/gridfield-bulk-editing-tools:client/dist/js/main.js'
# - 'colymba/gridfield-bulk-editing-tools:client/lang/en.js'
# extra_requirements_css:
# - 'colymba/gridfield-bulk-editing-tools:client/dist/styles/main.css'
SilverStripe\Admin\LeftAndMain:
extra_requirements_javascript:
- 'app/client/dist/js/app_cms.js'
extra_requirements_css:
- 'app/client/dist/css/app_cms.css'
SilverStripe\Forms\HTMLEditor\TinyMCEConfig:
editor_css:
- 'app/client/dist/css/app_editor.css'
SilverStripe\Control\Email\Email:
send_all_emails_from: noreply@twma.pro

47
_config/base-extensions.yml Executable file
View File

@ -0,0 +1,47 @@
---
Name: webapp-base-extensions
---
# Basic extensions
SilverStripe\Admin\LeftAndMain:
extensions:
- A2nt\CMSNiceties\Extensions\LeftAndMainExtension
SilverStripe\SiteConfig\SiteConfig:
extensions:
- A2nt\CMSNiceties\Extensions\SocialExtension
- A2nt\CMSNiceties\Extensions\SiteConfigExtension
- A2nt\CMSNiceties\Extensions\NotificationsExtension
SilverStripe\CMS\Model\SiteTree:
extensions:
- A2nt\CMSNiceties\Extensions\SiteTreeExtension
Sheadawson\Linkable\Models\EmbeddedObject:
extensions:
- A2nt\CMSNiceties\Extensions\EmbeddedObjectExtension
SilverStripe\Assets:
extensions:
- A2nt\CMSNiceties\Extensions\ImageExtension
Dynamic\FlexSlider\Model\SlideImage:
extensions:
- A2nt\CMSNiceties\Extensions\SlideImageExtension
SilverStripe\Core\Injector\Injector:
#SilverStripe\UserForms\Model\UserDefinedForm:
# class: A2nt\CMSNiceties\Extensions\UserDefinedForm_HiddenClass
Sheadawson\Linkable\Forms\EmbeddedObjectField:
class: A2nt\CMSNiceties\Extensions\EmbedObjectField
SilverStripe\Forms\CompositeField:
class: A2nt\CMSNiceties\Extensions\CompositeFieldExtension
SilverStripe\UserForms\Form\UserForm:
extensions:
- A2nt\CMSNiceties\Extensions\PlaceholderFormExtension
Page:
searchable_objects:
- A2nt\CMSNiceties\Models\TeamMember
extensions:
- DNADesign\Elemental\Extensions\ElementalPageExtension

81
_config/base-files.yml Executable file
View File

@ -0,0 +1,81 @@
---
Name: webapp-base-files
---
SilverStripe\Blog\Model\BlogPost:
featured_images_directory: 'blog-posts'
SilverStripe\Assets\Upload_Validator:
allowedExtensions:
- 'stl'
SilverStripe\Assets\File:
allowed_extensions:
- 'ace'
- 'arc'
- 'arj'
- 'asf'
- 'au'
- 'avi'
- 'bmp'
- 'bz2'
- 'cab'
- 'cda'
- 'csv'
- 'dmg'
- 'doc'
- 'docx'
- 'dotx'
- 'flv'
- 'gif'
- 'gpx'
- 'gz'
- 'hqx'
- 'ico'
- 'jpeg'
- 'jpg'
- 'kml'
- 'm4a'
- 'm4v'
- 'mid'
- 'midi'
- 'mkv'
- 'mov'
- 'mp3'
- 'mp4'
- 'mpa'
- 'mpeg'
- 'mpg'
- 'ogg'
- 'ogv'
- 'pages'
- 'pcx'
- 'pdf'
- 'png'
- 'pps'
- 'ppt'
- 'pptx'
- 'potx'
- 'ra'
- 'ram'
- 'rm'
- 'rtf'
- 'sit'
- 'sitx'
- 'tar'
- 'tgz'
- 'tif'
- 'tiff'
- 'txt'
- 'wav'
- 'webm'
- 'wma'
- 'wmv'
- 'xls'
- 'xlsx'
- 'xltx'
- 'zip'
- 'zipx'
- 'stl'
app_categories:
document:
- 'stl'

38
_config/base-graphql.yml Normal file
View File

@ -0,0 +1,38 @@
---
Name: webapp-base-graphql
After: graphqlconfig
---
SilverStripe\Control\Director:
rules:
'graphql': '%$SilverStripe\GraphQL\Controller.default'
SilverStripe\GraphQL\Controller:
cors:
Enabled: true
Allow-Origin: '*'
Allow-Headers: 'Authorization, Content-Type, Content-Language, apikey'
Allow-Methods: 'GET, PUT, DELETE, OPTIONS, POST'
#Allow-Credentials: 'true'
Max-Age: 600 # 600 seconds = 10 minutes.
SilverStripe\GraphQL\Auth\Handler:
authenticators:
- class: A2nt\CMSNiceties\GraphQL\APIKeyAuthenticator
priority: 30
SilverStripe\GraphQL\Manager.default:
properties:
Middlewares:
APIKeyMiddleware: A2nt\CMSNiceties\GraphQL\APIKeyMiddleware
SilverStripe\GraphQL\Manager:
schemas:
default:
types:
member: 'A2nt\CMSNiceties\GraphQL\MemberTypeCreator'
page: 'A2nt\CMSNiceties\GraphQL\PageTypeCreator'
element: 'A2nt\CMSNiceties\GraphQL\ElementTypeCreator'
queries:
readPages: 'A2nt\CMSNiceties\GraphQL\PaginatedReadPagesQueryCreator'
readMembers: 'A2nt\CMSNiceties\GraphQL\ReadMembersQueryCreator'
paginatedReadMembers: 'A2nt\CMSNiceties\GraphQL\PaginatedReadMembersQueryCreator'

60
_config/base-logs.yml_ Executable file
View File

@ -0,0 +1,60 @@
---
Name: webapp-base-logs-dev
Only:
environment: dev
---
SilverStripe\Core\Injector\Injector:
Psr\Log\LoggerInterface.errorhandler:
calls:
pushMyDisplayErrorHandler: [pushHandler, ['%$DisplayErrorHandler']]
DisplayErrorHandler:
class: SilverStripe\Logging\HTTPOutputHandler
constructor:
- 'notice'
properties:
Formatter: '%$SilverStripe\Logging\DetailedErrorFormatter'
CLIFormatter: '%$SilverStripe\Logging\DetailedErrorFormatter'
---
Name: webapp-base-logs-live
Except:
environment: dev
---
SilverStripe\Core\Injector\Injector:
# Default logger implementation for general purpose use
Psr\Log\LoggerInterface:
calls:
# Save system logs to file
pushFileLogHandler: [pushHandler, ['%$LogFileHandler']]
# Core error handler for system use
Psr\Log\LoggerInterface.errorhandler:
calls:
# Save errors to file
pushFileLogHandler: [pushHandler, ['%$LogFileHandler']]
# Format and display errors in the browser/CLI
pushMyDisplayErrorHandler: [pushHandler, ['%$DisplayErrorHandler']]
# Custom handler to log to a file
LogFileHandler:
class: Monolog\Handler\StreamHandler
constructor:
- '../silverstripe.log'
- 'notice'
properties:
Formatter: '%$Monolog\Formatter\HtmlFormatter'
ContentType: text/html
# Handler for displaying errors in the browser or CLI
DisplayErrorHandler:
class: SilverStripe\Logging\HTTPOutputHandler
constructor:
- 'error'
properties:
Formatter: '%$SilverStripe\Logging\DebugViewFriendlyErrorFormatter'
# Configuration for the "friendly" error formatter
SilverStripe\Logging\DebugViewFriendlyErrorFormatter:
class: SilverStripe\Logging\DebugViewFriendlyErrorFormatter
properties:
Title: 'There has been an error'
Body: 'The website server has not been able to respond to your request'

View File

@ -0,0 +1,9 @@
---
Name: webapp-base-mimeuploadvalidator
After: '#mimeuploadvalidator'
Only:
moduleexists: 'silverstripe/mimevalidator'
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Assets\Upload_Validator:
class: SilverStripe\MimeValidator\MimeUploadValidator

105
_config/base-security.yml Executable file
View File

@ -0,0 +1,105 @@
---
Name: 'webapp-base-security'
After: 'framework/*, cms/*, security_baseline'
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Security\MemberAuthenticator\LostPasswordHandler:
class: A2nt\CMSNiceties\Extensions\LostPasswordHandlerExtension
SilverStripe\Security\MemberAuthenticator\MemberLoginForm:
class: A2nt\CMSNiceties\Extensions\SiteMemberLoginForm
---
Except:
environment: dev
---
# Secure cookies
SilverStripe\Control\Session:
cookie_secure: true
strict_user_agent_check: false
timeout: 604800
SilverStripe\Forms\PasswordField:
autocompleate: false
SilverStripe\Security\Member:
lock_out_after_incorrect_logins: 5
lock_out_delay_mins: 5
# Password expiry should only happen when the password is leaked (optionally expire automatically if PCI/NIST compliance is required)
# password_expiry_days: 90
# instead of password change, we send out a notice on change of password OR Email (notify_account_security_change)
notify_password_change: false
#######################
# Security Headers
#######################
#Controller:
# security_headers:
# # # Values may contain :security_reporting_base_url: placeholders, will be replaced with the URL to SecurityBaselineController endpoint
# # Header-Directive: "value; another value;"
# # X-Version-Alias-Of-Same-Header: "x:Header-Directive" # 'x-alias' headers may be aliased to the standard by a value starting with "x:Standard"
# # X-Another-Alias-Version-Of-Same: "different; value syntax as well;"
#
# A useful base from guttmann/silverstripe-security-headers - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Security:
#
# # Content-Security-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
# # Specifies approved sources of content that the browser may load from your website
# # Useful: upgrade-insecure-requests; (Instructs browser to treat a site's insecure URLs as if they are HTTPS (eg for legacy sites)
# # Example: Allow everything but only from the same origin:
# Content-Security-Policy: "default-src 'self';"
# # Example: Allow Google Analytics, Google AJAX CDN and Same Origin
# Content-Security-Policy: "script-src 'self' www.google-analytics.com ajax.googleapis.com;"
# # Example: Starter Policy - allows images, scripts, AJAX, form actions, and CSS from the same origin, and does not allow any
# # other resources to load (eg object, frame, media, etc). It is a good starting point for many sites.
# Content-Security-Policy: "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';base-uri 'self';form-action 'self'"
# # Content-Security-Policy-Report-Only - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# # Allows web developers to experiment with policies by monitoring (but not enforcing) their effects
# # Browsers capable of enforcing CSP will send a violation report as a POST request to report-uri
# Content-Security-Policy-Report-Only: default-src https:; report-uri /security-reporting-endpoint/csp/
# Content-Security-Policy-Report-Only: "default-src https:; script-src 'self' https: 'unsafe-inline' 'unsafe-eval'; connect-src 'self'; img-src 'self' https: data:; style-src 'self' 'unsafe-inline'; base-uri 'self'; form-action 'self'; report-uri /security-reporting-endpoint/csp/;"
# # Strict-Transport-Security - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
# # Tells the browser to ONLY interact with the site using HTTPS and never HTTP
# Strict-Transport-Security: "max-age=31536000" # time in seconds (one year=31536000) to remember that the site is only accessible over HTTPS
# # Frame-Options - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
# # Disallowes pages to render within a frame - protects against clickjacking attacks
# Frame-Options: "deny"
# # XSS-Protection - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
# # protect against Cross-site Scripting attacks (value 1=sanitize (default in most browsers), set to "1; mode=block" to prevent rendering if attack is detected)
# # Deprecated: if you do not need to support legacy browsers, it is recommended that you use Content-Security-Policy without allowing unsafe-inline scripts instead
# X-XSS-Protection: "1; mode=block"
# # X-Content-Type-Options - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
# # Indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed
# # NOTE: Opting out of MIME sniffing can cause HTML web pages to be downloaded instead of rendered when they are
# # served with a MIME type other than text/html. Make sure to set both headers correctly.
# # Site security testers usually expect this header to be set.
# X-Content-Type-Options: "nosniff"
#
# Some more from https://help.dreamhost.com/hc/en-us/articles/360036486952-Security-headers
#
# # Referrer-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
# # controls how much referrer information should be sent to another server
# Referrer-Policy: no-referrer
# # Feature-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
# # (Experimental 2020) controls which browser features are allowed on your website, eg for sites allowing third-party content
# # CORS - Allow resource sharing with another domain (eg webfonts & ajax requests)
# # Access-Control-Allow-Origin - developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
#
# A further selection from https://github.com/bepsvpt/secure-headers/blob/master/config/secure-headers.php
#
# Clear-Site-Data - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data
# Clears browsing data (cookies, storage, cache) associated with the requesting website
# Expect-CT - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT
# Lets sites opt in to reporting and/or enforcement of Certificate Transparency requirements,
# to prevent the use of misissued certificates for that site from going unnoticed.
# (will likely become obsolete in June 2021)

View File

@ -0,0 +1,8 @@
---
Name: 'webapp-base-version-truncator'
---
Axllent\VersionTruncator\VersionTruncator:
keep_versions: 4 # how many (published) versions of each page to keep
keep_drafts: 2 # how many drafts of each page to keep
keep_redirects: true # keep page versions that have a different URLSegment (for redirects)
keep_old_page_types: false # keep page versions where page type (ClassName) has changed

12
_config/debugbar.yml Executable file
View File

@ -0,0 +1,12 @@
---
Name: 'webapp-debugbar'
After:
- 'framework'
- 'debugbar'
Only:
environment: 'dev'
---
LeKoala\DebugBar\DebugBar:
enabled_in_admin: false
query_limit: 500
max_header_length: 2048

28
_config/env-check.yml Executable file
View File

@ -0,0 +1,28 @@
---
Name: webapp-env-check
---
SilverStripe\EnvironmentCheck\EnvironmentCheckSuite:
registered_checks:
curl:
definition: 'HasFunctionCheck("curl_init")'
title: 'is curl available?'
gd:
definition: 'HasFunctionCheck("imagecreatetruecolor")'
title: 'Does PHP have GD2 support?'
db:
definition: 'DatabaseCheck("Page")'
title: 'Is the database accessible?'
url:
definition: 'URLCheck()'
title: 'Is the homepage accessible?'
registered_suites:
check:
- curl
- gd
- db
- url
health:
- db
- url

42
_config/locale-fluent.yml_ Executable file
View File

@ -0,0 +1,42 @@
---
Name: webapp-locale-fluent
After:
- webapp-extensions
- webapp-elemental
- webapp-locale
---
# Define Fluent locales
TractorCow\Fluent\Model\Locale:
default_records:
en:
Title: 'EN'
Locale: en_US
URLSegment: en
IsGlobalDefault: 1
us:
Locale: en_US
Title: 'EN'
URLSegment: en
ru:
Locale: ru_RU
Title: 'RU'
URLSegment: ru
Fallbacks:
- =>TractorCow\Fluent\Model\Locale.us
# Enable Fluent extensions
Page:
extensions:
- DNADesign\Elemental\TopPage\SiteTreeExtension
- DNADesign\Elemental\Extensions\ElementalPageExtension
- A2nt\CMSNiceties\Extensions\PageFluentExtension
DNADesign\Elemental\Models\ElementalArea:
extensions:
- DNADesign\Elemental\TopPage\FluentExtension
- A2nt\CMSNiceties\Extensions\ElementalArea
DNADesign\Elemental\Models\BaseElement:
extensions:
- DNADesign\Elemental\TopPage\FluentExtension
- A2nt\CMSNiceties\Extensions\ElementRows

59
_config/locale.yml Executable file
View File

@ -0,0 +1,59 @@
---
Name: webapp-locale
---
Symbiote\Addressable\Addressable:
allowed_countries:
'us': 'United States'
allowed_states:
'AL': 'Alabama'
'AK': 'Alaska'
'AZ': 'Arizona'
'AR': 'Arkansas'
'CA': 'California'
'CO': 'Colorado'
'CT': 'Connecticut'
'DE': 'Delaware'
'DC': 'District Of Columbia'
'FL': 'Florida'
'GA': 'Georgia'
'HI': 'Hawaii'
'ID': 'Idaho'
'IL': 'Illinois'
'IN': 'Indiana'
'IA': 'Iowa'
'KS': 'Kansas'
'KY': 'Kentucky'
'LA': 'Louisiana'
'ME': 'Maine'
'MD': 'Maryland'
'MA': 'Massachusetts'
'MI': 'Michigan'
'MN': 'Minnesota'
'MS': 'Mississippi'
'MO': 'Missouri'
'MT': 'Montana'
'NE': 'Nebraska'
'NV': 'Nevada'
'NH': 'New Hampshire'
'NJ': 'New Jersey'
'NM': 'New Mexico'
'NY': 'New York'
'NC': 'North Carolina'
'ND': 'North Dakota'
'OH': 'Ohio'
'OK': 'Oklahoma'
'OR': 'Oregon'
'PA': 'Pennsylvania'
'RI': 'Rhode Island'
'SC': 'South Carolina'
'SD': 'South Dakota'
'TN': 'Tennessee'
'TX': 'Texas'
'UT': 'Utah'
'VT': 'Vermont'
'VA': 'Virginia'
'WA': 'Washington'
'WV': 'West Virginia'
'WI': 'Wisconsin'
'WY': 'Wyoming'

52
_config/options-elements.yml Executable file
View File

@ -0,0 +1,52 @@
---
Name: webapp-options-elements
After:
- elemental
- elemental-list
- elementalvirtual
- webapp-base-extensions
---
Page:
searchable_elements:
- DNADesign\Elemental\Models\ElementContent
extensions:
- DNADesign\Elemental\Extensions\ElementalPageExtension
SilverStripe\CMS\Model\SiteTree:
allowed_elements:
- DNADesign\ElementalList\Model\ElementList
- DNADesign\Elemental\Models\ElementContent
- DNADesign\ElementalUserForms\Model\ElementForm
- Dynamic\Elements\Image\Elements\ElementImage
- Dynamic\Elements\Blog\Elements\ElementBlogPosts
- Dynamic\Elements\Oembed\Elements\ElementOembed
- Dynamic\Elements\Elements\ElementTestimonials
#- A2nt\ElementalBasics\Elements\TeamMembersElement
- A2nt\ElementalBasics\Elements\SliderElement
- A2nt\ElementalBasics\Elements\MapElement
#- A2nt\ElementalBasics\Elements\AccordionElement
- DNADesign\ElementalVirtual\Model\ElementVirtual
- A2nt\ElementalBasics\Elements\AccordionElement
- A2nt\ElementalBasics\Elements\CustomSnippetElement
- A2nt\ElementalBasics\Elements\InstagramElement
DNADesign\ElementalList\Model\ElementList:
inline_editable: false
default_global_elements: false
allowed_elements:
- DNADesign\ElementalList\Model\ElementList
- DNADesign\Elemental\Models\ElementContent
- DNADesign\ElementalUserForms\Model\ElementForm
- Dynamic\Elements\Image\Elements\ElementImage
- Dynamic\Elements\Blog\Elements\ElementBlogPosts
- Dynamic\Elements\Oembed\Elements\ElementOembed
- Dynamic\Elements\Elements\ElementTestimonials
#- A2nt\ElementalBasics\Elements\TeamMembersElement
- A2nt\ElementalBasics\Elements\SliderElement
- A2nt\ElementalBasics\Elements\MapElement
- A2nt\ElementalBasics\Elements\AccordionElement
- A2nt\ElementalBasics\Elements\CustomSnippetElement
- A2nt\ElementalBasics\Elements\InstagramElement
styles:
whiteframe: 'White Frame'
noframe: 'No Frame'

33
_config/options-widgets.yml Executable file
View File

@ -0,0 +1,33 @@
---
Name: webapp-options-widgets
---
# Blog + Widgets module extensions
Page:
extensions:
- A2nt\CMSNiceties\Widgets\WidgetPageExtension
SilverStripe\Blog\Model\Blog:
extensions:
- A2nt\CMSNiceties\Extensions\BlogExtension
SilverStripe\Blog\Model\BlogPost:
extensions:
- A2nt\CMSNiceties\Extensions\BlogPostExtension
SilverStripe\Widgets\Model\Widget:
icon: '<i class="icon font-icon-p-document"></i>'
extensions:
- A2nt\CMSNiceties\Widgets\WidgetExtension
SilverStripe\Blog\Widgets\BlogArchiveWidget:
icon: '<i class="icon font-icon-p-archive"></i>'
SilverStripe\Blog\Widgets\BlogCategoriesWidget:
icon: '<i class="icon font-icon-page-multiple"></i>'
SilverStripe\Blog\Widgets\BlogFeaturedPostsWidget:
icon: '<i class="icon font-icon-chart-line"></i>'
SilverStripe\Blog\Widgets\BlogRecentPostsWidget:
icon: '<i class="icon font-icon-back-in-time"></i>'
SilverStripe\Blog\Widgets\BlogTagsCloudWidget:
icon: '<i class="icon font-icon-tags"></i>'
SilverStripe\Blog\Widgets\BlogTagsWidget:
icon: '<i class="icon font-icon-tags"></i>'
only_available_in:
- CMSMain_HiddenClass

23
_config/shop.yml_ Executable file
View File

@ -0,0 +1,23 @@
---
Name: webapp-shop
---
SilverStripe\Core\Injector\Injector:
SilverShop\Checkout\SinglePageCheckoutComponentConfig:
class: A2nt\CMSNiceties\Models\CheckoutNoDeliveryConfig
SilverShop\Extension\ShopConfigExtension:
base_currency: USD
SilverShop\Model\Address:
extensions:
- A2nt\CMSNiceties\Extensions\AddressExtension
SilverShop\Cart\ShoppingCartController:
extensions:
- A2nt\CMSNiceties\Extensions\ShoppingCartControllerExtension
A2nt\CMSNiceties\Templates\DeferedRequirements:
custom_requirements:
SilverShop\Page\AccountPageController:
- SilverShop.Page.CheckoutPageController.js
- SilverShop.Page.CheckoutPageController.css

View File

@ -0,0 +1,41 @@
---
Name: webapp-templates-requirements
---
App\Templates\DeferredRequirements:
nofontawesome: false
version: false
static_domain: false
deferred: true
noreact: false
nojquery: true
jquery_version: '3.4.1'
SilverStripe\FontAwesome\FontAwesomeField:
version: '5.12.0'
SilverStripe\View\Requirements:
disable_flush_combined: true
SilverStripe\View\Requirements_Backend:
combine_in_dev: true
combine_hash_querystring: true
default_combined_files_folder: 'combined'
SilverStripe\Core\Injector\Injector:
# Create adapter that points to the custom directory root
SilverStripe\Assets\Flysystem\PublicAdapter.custom-adapter:
class: SilverStripe\Assets\Flysystem\PublicAssetAdapter
constructor:
Root: ./app/javascript
# Set flysystem filesystem that uses this adapter
League\Flysystem\Filesystem.custom-filesystem:
class: 'League\Flysystem\Filesystem'
constructor:
Adapter: '%$SilverStripe\Assets\Flysystem\PublicAdapter.custom-adapter'
# Create handler to generate assets using this filesystem
SilverStripe\Assets\Storage\GeneratedAssetHandler.custom-generated-assets:
class: SilverStripe\Assets\Flysystem\GeneratedAssets
properties:
Filesystem: '%$League\Flysystem\Filesystem.custom-filesystem'
# Assign this generator to the requirements builder
SilverStripe\View\Requirements_Backend:
properties:
AssetHandler: '%$SilverStripe\Assets\Storage\GeneratedAssetHandler.custom-generated-assets'

19
_config/templates-themes.yml Executable file
View File

@ -0,0 +1,19 @@
---
Name: webapp-templates-themes
After:
- webapp-options-elements
---
SilverStripe\View\SSViewer:
source_file_comments: true
themes:
- '$public'
- '$default'
App\Elements\SliderElement:
slide_width: 2140
slide_height: 700
# 2x container width to automatically resize images for 2K display
App\Elements\Extensions\ElementRows:
container_max_width: 2280
column_class: 'col-block col-md'

21
_config/webpack.yml Executable file
View File

@ -0,0 +1,21 @@
# Name: webapp-webpack
# that's important to place this file into /app/_config/webpack.yml
# with all configuration variables presented
# Cuz WebPack compiling script use it to set configuration
A2nt\CMSNiceties\Templates\WebpackTemplateProvider:
APPDIR: './app'
THEMESDIR: './themes'
HOSTNAME: 127.0.0.1
PORT: 3000
SRC: client/src
DIST: client/dist
TYPESJS: client/src/js/types
TYPESSCSS: client/src/scss/types
webp: false
NODE_ENV: production #production,development
HTTPS: true
injectClient: true
GRAPHQL_URL: '/graphql'
GRAPHQL_API_KEY: 'LgPaRkVPYa8IY7x3AjbLC8wx6oPPSlO01yPflFXecvQ'
#STATIC_URL: 'http://127.0.0.1'

27
composer.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "a2nt/cms-niceties",
"description": "Some useful CMS updates",
"type": "silverstripe-vendormodule",
"keywords": [
"silverstripe",
"elemental"
],
"license": "BSD-3-Clause",
"authors": [{
"name": "Tony Air",
"email": "tony@twma.pro"
}],
"minimum-stability": "dev",
"require": {
"silverstripe/cms": "^4",
"a2nt/silverstripe-elemental-basics": "*"
},
"require-dev": {
"phpunit/phpunit": "^5.7"
},
"autoload": {
"psr-4": {
"A2nt\\ElementalBasics\\": "src/"
}
}
}

31
lang/en.yml Executable file
View File

@ -0,0 +1,31 @@
en:
SilverStripe\Security\Member:
SURNAME: 'Last Name'
db_Surname: 'Last Name'
SUBJECTPASSWORDRESET: 'Your password reset link'
ENTEREMAIL: 'Please enter an email address to get a password reset link.'
PASSWORDRESETSENTHEADER: 'Password reset link sent'
PASSWORDRESETSENTTEXT: 'Thank you. A reset link has been sent, provided an account exists for this email address.'
Page:
LOADINGTEXT: 'LOADING ..'
JAVASCRIPTREQUIRED: 'Please, enable javascript!'
UPGRADEBROWSER: 'Upgrade your browser'
OUTDATEDBROWSER: 'You are using an outdated browser. For a faster, safer browsing experience, upgrade for free today.'
SilverShop\Model\Address:
db_Surname: 'Last Name'
SilverShop\Model\Order:
db_Surname: 'Last Name'
SilverShop\Page\CheckoutPage:
ProceedToPayment: 'Send Order to Store'
UndefinedOffset\NoCaptcha\Forms\NocaptchaField:
EMPTY: 'Please prove you are human - check the Captcha box.'
NOSCRIPT: 'You must enable JavaScript to submit this form'
VALIDATE_ERROR: 'Captcha could not be validated'
Dynamic\FlexSlider\Model\SlideImage:
SINGULARNAME: 'Slide'
PLURALNAME: 'Slides'
Addressable:
SUBURB: 'City'
Symbiote\AddressableAddressable:
SUBURB: 'City'

19
lang/ru.yml Executable file
View File

@ -0,0 +1,19 @@
ru:
Page:
LOADINGTEXT: "ЗАГРУЗКА .."
JAVASCRIPTREQUIRED: "Для корректной работы страницы требуется включить Javascript!"
UPGRADEBROWSER: "Обновите браузер!"
OUTDATEDBROWSER: "Вы используете устаревшую версию браузера. Обновите ваш браузер сейчас для повышения уровня безопасности вашей системы."
PaginationLabel: "Переключение страниц"
PaginationPrevious: "Назад"
PaginationNext: "Вперед"
SilverStripe\Blog\Model\Blog:
PostedIn: "Категории"
Tagged: "Отметки"
Comments: "Коментарии"
Posted: "Опубликовано"
By: " - "
AND: "и"
LessThanAMinuteToRead: "Меньше минуты на чтение"
MinutesToRead: "Минут на чтение"
READMORE: "Подробнее"

View File

@ -0,0 +1,29 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 8/26/18
* Time: 12:55 PM
*/
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Core\Extension;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldList;
class AddressExtension extends Extension
{
public function updateFormFields(FieldList $fields)
{
$holder = CompositeField::create();
foreach ($fields as $field) {
$holder->push($field);
$fields->remove($field);
}
$holder->addExtraClass('col-sm-6');
$fields->push($holder);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Blog\Forms\GridField\GridFieldConfigBlogPost;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataExtension;
class BlogExtension extends DataExtension
{
public function updateCMSFields(FieldList $fields)
{
$f = $fields->dataFieldByName('ChildPages');
if ($f) {
$f->setConfig(GridFieldConfigBlogPost::create(75));
}
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 7/2/18
* Time: 12:10 AM
*/
namespace A2nt\CMSNiceties\Extensions;
use DNADesign\Elemental\Models\ElementContent;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataExtension;
class BlogPostExtension extends DataExtension
{
private static $db = [
'Featured' => 'Boolean(0)',
];
public function updateCMSFields(FieldList $fields)
{
$mainTab = $fields->findOrMakeTab('Root.Main');
$mainTab->push(CheckboxField::create('Featured'));
}
}

View File

@ -0,0 +1,17 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 7/2/18
* Time: 1:05 AM
*/
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\ORM\HiddenClass;
use Page;
class CMSMain_HiddenClass extends Page implements HiddenClass
{
}

View File

@ -0,0 +1,26 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Blog\Forms\GridField\GridFieldConfigBlogPost;
use SilverStripe\Core\Extension;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataExtension;
class CompositeFieldExtension extends CompositeField
{
public function extraClass()
{
return 'composite '.parent::extraClass();
}
public function getAttributes()
{
$attrs = parent::getAttributes();
unset($attrs['name'], $attrs['type'], $attrs['disabled'], $attrs['readonly'], $attrs['autofocus']);
return $attrs;
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use Sheadawson\Linkable\Forms\EmbeddedObjectField;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\ORM\FieldType\DBHTMLText;
class EmbedObjectField extends EmbeddedObjectField
{
/**
* @param array $properties
* @return mixed|DBHTMLText
*/
public function FieldHolder($properties = [])
{
$name = $this->getName();
$fields = [
CheckboxField::create(
$name . '[autoplay]',
_t(self::CLASS.'AUTOPLAY', 'Autoplay video?')
)->setValue($this->object->getField('Autoplay')),
CheckboxField::create(
$name . '[loop]',
_t(self::CLASS.'LOOP', 'Loop video?')
)->setValue($this->object->getField('Loop')),
CheckboxField::create(
$name.'[controls]',
_t(self::CLASS.'CONTROLS', 'Show player controls?')
)->setValue($this->object->getField('Controls'))
];
return CompositeField::create(array_merge([
LiteralField::create(
$name.'Options',
parent::FieldHolder($properties)
)
], $fields));
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Core\Convert;
use SilverStripe\View\HTML;
class EmbedShortcodeProvider extends \SilverStripe\View\Shortcodes\EmbedShortcodeProvider
{
/**
* Build video embed tag
*
* @param array $arguments
* @param string $content Raw HTML content
* @return string
*/
protected static function videoEmbed($arguments, $content)
{
// Ensure outer div has given width (but leave height auto)
if (!empty($arguments['width'])) {
$arguments['style'] = 'width:' . (int) $arguments['width'] . 'px';
//.';height:' . (int) $arguments['height'] . 'px';
}
// Convert caption to <p>
if (!empty($arguments['caption'])) {
$xmlCaption = Convert::raw2xml($arguments['caption']);
$content .= "\n<p class=\"caption\">{$xmlCaption}</p>";
}
// Convert arguments to data-*argument_name*
foreach ($arguments as $k => $v) {
if($k === 'class' || $k === 'style') {
continue;
}
unset($arguments[$k]);
$arguments['data-'.$k] = $v;
}
$arguments['class'] .= ' embed-youtube embed-responsive embed-responsive-16by9';
$iframe = strpos($content, 'iframe');
if($iframe >= 0) {
$content = substr($content, 0, $iframe+6).' class="embed-responsive-item" '.substr($content, $iframe +7);
}
return HTML::createTag('div', $arguments, $content);
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataExtension;
class EmbeddedObjectExtension extends DataExtension
{
private static $db = [
'Autoplay' => 'Boolean(0)',
'Loop' => 'Boolean(0)',
'Controls' => 'Boolean(1)',
];
public function Embed()
{
$this->owner->Embed();
$this->setEmbedParams();
return $this->owner;
}
public function setEmbedParams($params = [])
{
$iframe_params = [];
if ($this->owner->getField('Autoplay')) {
$iframe_params[] = 'allow="autoplay"';
}
// YouTube params
if (stripos($this->owner->EmbedHTML, 'https://www.youtube.com/embed/') > 0) {
$url = $this->owner->getField('SourceURL');
preg_match(
'/^(?:http(?:s)?:\/\/)?(?:www\.)?(?:m\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&"\'>]+)/',
$url,
$matches
);
if (isset($matches[1])) {
$videoID = $matches[1];
$params = array_merge($params, [
'feature=oembed',
'wmode=transparent',
'enablejsapi=1',
'disablekb=1',
'iv_load_policy=3',
'modestbranding=1',
'rel=0',
'showinfo=0',
//'controls='.($this->owner->getField('Controls') ? '1': '0')
]);
if ($this->owner->getField('Autoplay')) {
$params[] = 'autoplay=1';
$params[] = 'mute=1';
}
if ($this->owner->getField('Loop')) {
$params[] = 'loop=1';
$params[] = 'playlist=' . $videoID;
}
$this->owner->EmbedHTML = preg_replace(
'/src="([A-z0-9:\/\.]+)\??(.*?)"/',
'src="https://www.youtube.com/embed/' . $videoID . '?' . implode('&', $params) . '" '
. implode(' ', $iframe_params),
$this->owner->EmbedHTML
);
}
}
if (stripos($this->owner->EmbedHTML, 'https://player.vimeo.com/video/') > 0) {
$url = $this->owner->getField('SourceURL');
preg_match(
'/^https:\/\/vimeo\.com\/([A-z0-9]+)/',
$url,
$matches
);
$videoID = $matches[1];
$params = array_merge($params, [
'controls='.($this->owner->getField('Controls') ? '1': '0'),
'background=1',
]);
if ($this->owner->getField('Autoplay')) {
$params[] = 'autoplay=1';
}
if ($this->owner->getField('Loop')) {
$params[] = 'loop=1';
}
$this->owner->EmbedHTML = preg_replace(
'/src="([A-z0-9:\/\.]+)\??(.*?)"/',
'src="https://player.vimeo.com/video/'.$videoID.'?' . implode('&', $params) . '" '
.implode(' ', $iframe_params),
$this->owner->EmbedHTML
);
}
}
public function updateCMSFields(FieldList $fields)
{
parent::updateCMSFields($fields);
$fields->removeByName([
'Width', 'Height', 'EmbedHTML', 'ThumbURL',
'Autoplay', 'Loop', 'Controls',
'ExtraClass', 'Type',
]);
$fields->addFieldsToTab('Root.Extra', [
CheckboxField::create('Autoplay'),
CheckboxField::create('Loop'),
CheckboxField::create('Controls'),
NumericField::create('Width'),
NumericField::create('Height'),
TextareaField::create('EmbedHTML'),
TextField::create('ThumbURL'),
TextField::create('ExtraClass'),
TextField::create('Type'),
]);
}
public function onBeforeWrite()
{
parent::onBeforeWrite();
$this->setEmbedParams();
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Forms\Form;
use SilverStripe\ORM\DataExtension;
class HtmlEditorFieldExtension extends DataExtension
{
public function updateMediaForm(Form $form)
{
$page_id = $_SESSION['CMSMain']['currentPage'];
$page_urlsegment = SiteTree::get()->byID($page_id)->URLSegment;
$computerUploadField = $form->Fields()->dataFieldByName('AssetUploadField');
$computerUploadField->setFolderName(sprintf("%s/images/%s", 'Uploads', $page_urlsegment));
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataExtension;
class ImageExtension extends DataExtension
{
public function updateCMSFields(FieldList $fields)
{
parent::updateCMSFields($fields);
/*$fields->removeByName([
'Filename',
]);*/
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Extension;
use SilverStripe\FontAwesome\FontAwesomeField;
use SilverStripe\View\Requirements;
use A2nt\CMSNiceties\Templates\DeferredRequirements;
class LeftAndMainExtension extends Extension
{
public function init()
{
$config = Config::inst()->get(DeferredRequirements::class);
// App libs
if (!$config['nofontawesome']) {
$v = !isset($config['fontawesome_version']) || !$config['fontawesome_version']
? Config::inst()->get(FontAwesomeField::class, 'version')
: $config['fontawesome_version'];
Requirements::css('//use.fontawesome.com/releases/v'.$v.'/css/all.css');
}
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 6/30/18
* Time: 11:37 PM
*/
namespace A2nt\CMSNiceties\Extensions;
use Sheadawson\Linkable\Forms\LinkField;
use Sheadawson\Linkable\Models\Link;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Security\MemberAuthenticator\LostPasswordHandler;
class LostPasswordHandlerExtension extends LostPasswordHandler
{
private static $url_handlers = [
'passwordsent' => 'passwordsent',
];
private static $allowed_actions = [
'passwordsent',
];
/**
* Show the "password sent" page, after a user has requested
* to reset their password.
*
* @return array
*/
public function passwordsent()
{
$message = _t(
'SilverStripe\\Security\\Security.PASSWORDRESETSENTTEXT',
"Thank you. A reset link has been sent, provided an account exists for this email address."
);
$email = $this->getRequest()->getVar('email');
$message = $email
? 'Thank you! A reset link has been sent to \''.$email.'\', provided an account exists for this email address.'
: $message;
return [
'Title' => _t(
'SilverStripe\\Security\\Security.PASSWORDRESETSENTHEADER',
"Password reset link sent".($email ? ' to \''.$email.'\'' : '')
),
'ElementalArea' => DBField::create_field('HTMLFragment', "<p>$message</p>"),
];
}
/**
* Avoid information disclosure by displaying the same status, regardless wether the email address actually exists
*
* @param array $data
* @return HTTPResponse
*/
protected function redirectToSuccess(array $data)
{
$link = $this->link('passwordsent').'?email='.$data['Email'];
return $this->redirect($this->addBackURLParam($link));
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use Dynamic\Elements\Blog\Elements\ElementBlogPosts;
use Innoweb\Sitemap\Pages\SitemapPage;
use Sheadawson\Linkable\Forms\LinkField;
use Sheadawson\Linkable\Models\Link;
use SilverStripe\AssetAdmin\Forms\UploadField;
use SilverStripe\Assets\Image;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridFieldEditButton;
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataExtension;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TreeMultiselectField;
use BetterBrief\GoogleMapField;
use A2nt\CMSNiceties\Models\Holiday;
use A2nt\CMSNiceties\Models\Notification;
use A2nt\CMSNiceties\Models\OpeningHour;
use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton;
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
use Symbiote\GridFieldExtensions\GridFieldTitleHeader;
class NotificationsExtension extends DataExtension
{
private static $db = [
'ShowNotifications' => 'Boolean(1)',
];
private static $has_many = [
'Notifications' => Notification::class,
];
public function updateCMSFields(FieldList $fields)
{
$tab = $fields->findOrMakeTab('Root.Notifications');
if(!$this->owner->exists()) {
$tab->push(LiteralField::create(
'NotificationsNotice',
'<p class="message notice">The object must be saved before notifications can be added</p>'
));
return null;
}
$items = $this->owner->Notifications();
$config = GridFieldConfig::create();
$config->addComponents([
new GridFieldToolbarHeader(),
new GridFieldTitleHeader(),
new GridFieldEditableColumns(),
new GridFieldAddNewInlineButton('toolbar-header-right'),
new GridFieldDetailForm(),
new GridFieldEditButton(),
new GridFieldDeleteAction(),
]);
$tab->setChildren(FieldList::create(
HeaderField::create('NotificationsHeader','Notifications'),
LiteralField::create(
'CurrentNotifications',
'<b>Current:</b>'
.$this->owner->renderWith('App\\Objects\\NotificationsList')
),
CheckboxField::create('ShowNotifications'),
GridField::create(
'Notifications',
'',
$items,
$config
)
));
}
public function NotificationsToday()
{
$items = $this->owner->Notifications();
$time = time();
return $items->filter([
'DateOn:LessThanOrEqual' => date('Y-m-d', $time),
'DateOff:GreaterThanOrEqual' => date('Y-m-d', $time),
]);
}
}

View File

@ -0,0 +1,189 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use Dynamic\Elements\Blog\Elements\ElementBlogPosts;
use Innoweb\Sitemap\Pages\SitemapPage;
use Sheadawson\Linkable\Forms\LinkField;
use Sheadawson\Linkable\Models\Link;
use SilverStripe\AssetAdmin\Forms\UploadField;
use SilverStripe\Assets\Image;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataExtension;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TreeMultiselectField;
use BetterBrief\GoogleMapField;
use A2nt\CMSNiceties\Models\Holiday;
use A2nt\CMSNiceties\Models\OpeningHour;
use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton;
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
use Symbiote\GridFieldExtensions\GridFieldTitleHeader;
class OpenningHoursExtension extends DataExtension
{
private static $db = [
'ShowOpeningHours' => 'Boolean(1)',
'OpenningHoursNote' => 'Varchar(255)',
];
private static $has_one = [
'OpeningHoursPage' => SiteTree::class,
];
private static $has_many = [
'OpeningHours' => OpeningHour::class,
'Holidays' => Holiday::class,
];
public function HoursLink()
{
return $this->owner->OpeningHoursPage()->Link();
}
public function updateCMSFields(FieldList $fields)
{
$tab = $fields->findOrMakeTab('Root.OpeningHours');
if(!$this->owner->exists()) {
$tab->push(LiteralField::create(
'OpeningHoursNotice',
'<p class="message notice">The object must be saved before opening hours can be added</p>'
));
return null;
}
$hours = $this->owner->OpeningHours();
$config = GridFieldConfig::create();
$config->addComponents([
new GridFieldToolbarHeader(),
new GridFieldTitleHeader(),
new GridFieldEditableColumns(),
new GridFieldAddNewInlineButton('toolbar-header-right'),
new GridFieldDeleteAction(),
]);
$tab->setChildren(FieldList::create(
HeaderField::create('OpeningHours','Opening Hours'),
LiteralField::create(
'CurrentOpeningHour',
'<b>Today:</b>'
.'<p class="message notice">'
.$this->owner->renderWith('App\\Objects\\OpeningHoursList')
.'</p>'
),
CheckboxField::create('ShowOpeningHours'),
DropdownField::create(
'OpeningHoursPageID',
'Opening Hours Page',
SiteTree::get()->map()->toArray()
),
/*TextareaField::create('OpenningHoursNote'),
LiteralField::create(
'OpeningHoursNote',
'<p><b>Please, specify time ranges. For example:</b><br/>'
.'Monday 10:00 AM - 2:00 PM<br/>'
.'Monday 3:00 PM - 6:00 PM<br/>'
.'Tuesday 12:00 AM - 2:00 PM<br/>'
.'Tuesday 3:00 PM - 4:00 PM<br/>'
.'...<br/>'
.'<b>Short day example durring holidays:</b><br/>'
.'Monday 12:00 AM - 2:00 PM 12/31/2018 - 01/06/2019'
.'</p>'
),*/
GridField::create(
'OpeningHours',
'Opening Hours',
$hours,
$config
)
));
$tab = $fields->findOrMakeTab('Root.Holidays');
$tab->push(GridField::create(
'Holidays',
'Holidays',
$this->owner->Holidays(),
$config
));
}
/**
* Get the opening hours
*
* @return OpeningHour|DataObject|null
*/
public function OpeningHoursToday()
{
$hours = $this->owner->OpeningHours();
$time = time();
$today = $hours->filter([
'Day' => date('l', $time),
'DisplayStart:LessThanOrEqual' => date('Y-m-d', $time),
'DisplayEnd:GreaterThanOrEqual' => date('Y-m-d', $time),
]);
return $today->exists() ? $today : $hours->filter([
'Day' => date('l', $time),
'DisplayStart' => null,
'DisplayEnd' => null,
]);
}
public function OpeningHoursJSON()
{
$hours = $this->owner->OpeningHours();
$result = [];
foreach ($hours as $hour) {
$from = str_replace(':00', '', date('g:i a', strtotime($hour->getField('From'))));
$till = str_replace(':00', '', date('g:i a', strtotime($hour->getField('Till'))));
$result['days'][$hour->getField('Day')][] = [
'From' => $from,
'Till' => $till,
'DisplayStart' => $hour->getField('DisplayStart'),
'DisplayEnd' => $hour->getField('DisplayEnd'),
];
}
$holidays = $this->owner->Holidays();
foreach ($holidays as $holiday) {
$result['holidays'][$holiday->getField('Date')] = $holiday->getField('Title');
}
return json_encode($result);
}
public function onBeforeWrite()
{
parent::onBeforeWrite();
if ($this->owner->exists() && !$this->owner->OpeningHours()->exists()) {
$this->createOpeningHours();
}
}
/**
* Set up the opening hours for each day of the week
*/
private function createOpeningHours()
{
$days = OpeningHour::singleton()->dbObject('Day')->enumValues();
foreach ($days as $day) {
$openingHour = OpeningHour::create();
$openingHour->Day = $day;
$this->owner->OpeningHours()->add($openingHour);
}
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Blog\Forms\GridField\GridFieldConfigBlogPost;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataExtension;
use TractorCow\Fluent\Model\Locale;
class PageFluentExtension extends DataExtension
{
/**
* Override default Fluent fallback
*
* @param string $query
* @param string $table
* @param string $field
* @param Locale $locale
*/
public function updateLocaliseSelect(&$query, $table, $field, Locale $locale)
{
// disallow elemental data inheritance in the case that published localised page instance already exists
if ($field == 'ElementalAreaID' && $this->owner->isPublishedInLocale()) {
$query = '"' . $table . '_Localised_' . $locale->getLocale() . '"."' . $field . '"';
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Core\Extension;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
class PlaceholderFormExtension extends Extension
{
public function updateFormFields(FieldList $fields)
{
foreach ($fields as $field) {
$this->setPlaceholder($field);
}
}
private function setPlaceholder($field)
{
if (is_a($field, TextField::class)) {
$field->setAttribute(
'placeholder',
$field->Title()
.($field->hasClass('requiredField') ? '*' : '')
);
$field->setTitle('');
}
if (is_a($field, CompositeField::class)) {
$children = $field->getChildren();
foreach ($children as $child) {
$this->setPlaceholder($child);
}
}
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 6/23/18
* Time: 1:23 PM
*/
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\ORM\DataExtension;
class ShoppingCartControllerExtension extends DataExtension
{
public function updateAddResponse($request, $response, $product, $quantity)
{
\PageController::setSiteWideMessage('+'.$quantity.' item(s) was added into the cart', 'success', $request);
}
public function updateRemoveResponse($request, $response, $product, $quantity)
{
\PageController::setSiteWideMessage(''.$quantity.' item(s) was removed from the cart', 'success', $request);
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use A2nt\SilverStripeMapboxField\MapboxField;
use Innoweb\Sitemap\Pages\SitemapPage;
use SilverStripe\AssetAdmin\Forms\UploadField;
use SilverStripe\Assets\Image;
use SilverStripe\Blog\Model\BlogPost;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataExtension;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TreeMultiselectField;
use SilverStripe\Forms\DropdownField;
//use BetterBrief\GoogleMapField;
class SiteConfigExtension extends DataExtension
{
private static $db = [
'ExtraCode' => 'Text',
'Longitude' => 'Decimal(10, 8)',
'Latitude' => 'Decimal(11, 8)',
'MapZoom' => 'Int',
//'MapAPIKey' => 'Varchar(255)',
'Description' => 'Varchar(255)',
'Address' => 'Varchar(255)',
'Suburb' => 'Varchar(255)',
'State' => 'Varchar(255)',
'ZipCode' => 'Varchar(6)',
];
private static $has_one = [
'PrivacyPolicy' => SiteTree::class,
'Sitemap' => SiteTree::class,
];
private static $many_many = [
'Navigation' => SiteTree::class,
];
public function updateCMSFields(FieldList $fields)
{
$img = Image::get()->filter([
'ParentID' => 0,
'FileFilename' => 'qrcode.png',
])->first();
if ($img) {
$fields->addFieldsToTab('Root.Main', [
LiteralField::create('QRCode', '<img src="'.$img->Link().'" alt="QR code" width="200" style="float:left" />'),
]);
}
$fields->addFieldsToTab('Root.Main', [
TreeMultiselectField::create(
'Navigation',
'Navigation',
SiteTree::class
)->setDisableFunction(static function ($el) {
return $el->getField('ParentID') !== 0;
}),
TextareaField::create('Description', 'Website Description'),
TextareaField::create('ExtraCode', 'Extra site-wide HTML code'),
DropdownField::create(
'PrivacyPolicyID',
'Privacy Policy Page',
SiteTree::get()->map()->toArray()
)->setEmptyString('(Select one)'),
DropdownField::create(
'SitemapID',
'Sitemap Page',
SitemapPage::get()->map()->toArray()
)->setEmptyString('(Select one)'),
]);
$mapTab = $fields->findOrMakeTab('Root.Maps');
$mapTab->setTitle('Address / Map');
$fields->addFieldsToTab('Root.Maps', [
TextField::create('Address'),
TextField::create('Suburb', 'City'),
TextField::create('State'),
TextField::create('ZipCode'),
]);
if (MapboxField::getAccessToken()) {
$fields->addFieldsToTab('Root.Maps', [
//TextField::create('MapAPIKey'),
TextField::create('MapZoom'),
MapboxField::create('Map', 'Choose a location', 'Latitude', 'Longitude'),
]);
} else {
$fields->addFieldsToTab('Root.Maps', [
LiteralField::create('MapNotice', '<p class="alert alert-info">No Map API keys specified.</p>')
]);
}
/*GoogleMapField::create(
$this->owner,
'Location',
[
'show_search_box' => true,
]
)*/
}
public function MapAPIKey()
{
return MapboxField::config()->get('access_token');
}
public function MapStyle()
{
return MapboxField::config()->get('map_style');
}
public function getGeoJSON()
{
return '{"type": "MarkerCollection","features": [{"type": "Feature","icon": "<i class=\'fas fa-map-marker-alt\'></i>",'
.'"properties": {"content": "'.$this->owner->getTitle().'"},"geometry": {"type": "Point",'
.'"coordinates": ['.$this->owner->getField('Longitude').','.$this->owner->getField('Latitude').']}}]}';
}
public function DirectionsLink()
{
return '<a href="https://www.google.com/maps/dir/Current+Location/'
.$this->owner->getField('Latitude').','
.$this->owner->getField('Longitude').'" class="btn btn-primary btn-directions" target="_blank">'
.'<i class="fas fa-road"></i> Get Directions</a>';
}
public function getLatestBlogPosts()
{
return BlogPost::get()->sort('PublishDate DESC');
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Control\Director;
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
class SiteMemberLoginForm extends MemberLoginForm
{
public function __construct(
$controller,
$authenticatorClass,
$name,
$fields = null,
$actions = null,
$checkCurrentUser = true
) {
parent::__construct($controller, $authenticatorClass, $name, $fields, $actions, $checkCurrentUser);
$fields = $this->Fields();
$actions = $this->Actions();
$email = $fields->fieldByName('Email');
if ($email) {
$email
->setAttribute('placeholder', 'your@email.com')
->setAttribute('autocomplete', 'email')
->setAttribute('type', 'email');
}
$pass = $fields->fieldByName('Password');
if($pass) {
//$pass->setAttribute('autocomplete', 'current-password');
$pass->setAttribute('placeholder', '**********');
$pass->setAutofocus(true);
}
$btn = $actions->fieldByName('action_doLogin');
if($btn) {
$btn->setUseButtonTag(true);
$btn->setButtonContent('<i class="fas fa-check"></i> '.$btn->Title());
$btn->addExtraClass('btn-lg');
}
if (Director::isLive()) {
$this->enableSpamProtection();
}
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Forms\TextareaField;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Forms\FieldList;
class SiteTreeExtension extends DataExtension
{
private static $db = [
'ExtraCode' => 'Text',
];
public function updateSettingsFields(FieldList $fields)
{
$fields->addFieldsToTab('Root.Settings', [
TextareaField::create(
'ExtraCode',
'Extra page specific HTML code'
),
]);
}
}

View File

@ -0,0 +1,74 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 6/30/18
* Time: 11:37 PM
* Ref: Dynamic\FlexSlider\Model\SlideImage
*/
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\DatetimeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\ToggleCompositeField;
use SilverStripe\ORM\DataExtension;
class SlideImageExtension extends DataExtension
{
private static $db = [
'Hide' => 'Boolean(0)',
'DateOn' => 'Datetime',
'DateOff' => 'Datetime',
];
private $_cache = [];
public function getElement()
{
if(!isset($this->_cache['element'])) {
$this->_cache['element'] = $this->owner->SlideshowElement();
}
return $this->_cache['element'];
}
public function getSlideWidth()
{
$element = $this->getElement();
return $element->getSlideWidth();
}
public function getSlideHeight()
{
$element = $this->getElement();
return $element->getSlideHeight();
}
public function updateCMSFields(FieldList $fields)
{
parent::updateCMSFields($fields);
$fields->removeByName([
'PageLinkID',
'Hide',
'DateOn',
'DateOff',
]);
$fields->dataFieldByName('Image')
->setTitle('Image ('.$this->getSlideWidth().' x '.$this->getSlideHeight().' px)');
$fields->addFieldToTab('Root.Main', ToggleCompositeField::create(
'ConfigHD',
'Slide Settings',
[
CheckboxField::create('Hide', 'Hide this slide? (That will hide the slide regardless of start/end fields)'),
DatetimeField::create('DateOn', 'When would you like to start showing the slide?'),
DatetimeField::create('DateOff', 'When would you like to stop showing the slide?'),
]
));
}
}

View File

@ -0,0 +1,75 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 6/30/18
* Time: 11:37 PM
*/
namespace A2nt\CMSNiceties\Extensions;
use Sheadawson\Linkable\Forms\LinkField;
use Sheadawson\Linkable\Models\Link;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Security\Member;
class SocialExtension extends DataExtension
{
private static $db = [
//'PhoneNumber' => 'Varchar(255)',
];
private static $has_one = [
'Facebook' => Link::class,
'LinkedIn' => Link::class,
'Pinterest' => Link::class,
'Instagram' => Link::class,
'Twitter' => Link::class,
'PublicEmail' => Link::class,
'PhoneNumber' => Link::class,
];
public function updateCMSFields(FieldList $fields)
{
parent::updateCMSFields($fields);
$linkFields = [
LinkField::create('FacebookID'),
LinkField::create('LinkedInID'),
LinkField::create('PinterestID'),
LinkField::create('InstagramID'),
LinkField::create('TwitterID'),
];
foreach ($linkFields as $field) {
$field->setAllowedTypes(['URL']);
}
$fields->findOrMakeTab('Root.Social');
$fields->addFieldsToTab('Root.Social', [
LinkField::create('PublicEmailID', 'Public Email')
->setAllowedTypes(['Email']),
LinkField::create('PhoneNumberID', 'Phone Number')
->setAllowedTypes(['Phone']),
]);
$fields->addFieldsToTab('Root.Social', $linkFields);
}
public static function byPhone($phone)
{
$links = Link::get()->filter('Phone', $phone);
if ($links->exists()) {
return Member::get()->filter(
'PhoneNumberID',
array_keys($links->map('ID', 'Title')->toArray())
)->first();
}
return null;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace A2nt\CMSNiceties\Extensions;
use SilverStripe\ORM\HiddenClass;
use SilverStripe\UserForms\Model\UserDefinedForm;
class UserDefinedForm_HiddenClass extends UserDefinedForm implements HiddenClass
{
}

View File

@ -0,0 +1,140 @@
<?php namespace A2nt\CMSNiceties\Forms\GridField;
/**
* Milkyway Multimedia
* SaveAllButton.php
*
* @package milkyway-multimedia/ss-gridfield-utils
* @author Mellisa Hankins <mell@milkywaymultimedia.com.au>
*/
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
use SilverStripe\Forms\GridField\GridField_ActionProvider;
use SilverStripe\Forms\GridField\GridField_FormAction;
use SilverStripe\Forms\GridField\GridField_SaveHandler;
use SilverStripe\Control\Controller;
class SaveAllButton implements GridField_HTMLProvider, GridField_ActionProvider
{
protected $targetFragment;
protected $actionName = 'saveallrecords';
public $buttonName;
public $publish = true;
public $completeMessage;
public $removeChangeFlagOnFormOnSave = false;
public function setButtonName($name)
{
$this->buttonName = $name;
return $this;
}
public function setRemoveChangeFlagOnFormOnSave($flag)
{
$this->removeChangeFlagOnFormOnSave = $flag;
return $this;
}
public function __construct($targetFragment = 'before', $publish = true, $action = 'saveallrecords')
{
$this->targetFragment = $targetFragment;
$this->publish = $publish;
$this->actionName = $action;
}
public function getHTMLFragments($gridField)
{
$singleton = singleton($gridField->getModelClass());
if (!$singleton->canEdit() && !$singleton->canCreate()) {
return [];
}
if (!$this->buttonName) {
if ($this->publish && $singleton->hasExtension('Versioned')) {
$this->buttonName = _t('GridField.SAVE_ALL_AND_PUBLISH', 'Save all and publish');
} else {
$this->buttonName = _t('GridField.SAVE_ALL', 'Save all');
}
}
$button = GridField_FormAction::create(
$gridField,
$this->actionName,
$this->buttonName,
$this->actionName,
null
);
$button->setAttribute('data-icon', 'disk')->addExtraClass('new new-link ui-button-text-icon-primary');
if ($this->removeChangeFlagOnFormOnSave) {
$button->addExtraClass('js-mwm-gridfield--saveall');
}
return [
$this->targetFragment => $button->Field(),
];
}
public function getActions($gridField)
{
return [$this->actionName];
}
public function handleAction(GridField $gridField, $actionName, $arguments, $data)
{
if ($actionName == $this->actionName) {
return $this->saveAllRecords($gridField, $arguments, $data);
}
}
protected function saveAllRecords(GridField $grid, $arguments, $data)
{
if (isset($data[$grid->Name])) {
$currValue = $grid->Value();
$grid->setValue($data[$grid->Name]);
$model = singleton($grid->List->dataClass());
foreach ($grid->getConfig()->getComponents() as $component) {
if ($component instanceof GridField_SaveHandler) {
$component->handleSave($grid, $model);
}
}
if ($this->publish) {
// Only use the viewable list items, since bulk publishing can take a toll on the system
$list = ($paginator = $grid->getConfig()->getComponentByType('GridFieldPaginator')) ? $paginator->getManipulatedData($grid, $grid->List) : $grid->List;
$list->each(
function ($item) {
if ($item->hasExtension('Versioned')) {
$item->writeToStage('Stage');
$item->publish('Stage', 'Live');
}
}
);
}
if ($model->exists()) {
$model->delete();
$model->destroy();
}
$grid->setValue($currValue);
if (Controller::curr() && $response = Controller::curr()->Response) {
if (!$this->completeMessage) {
$this->completeMessage = _t('GridField.DONE', 'Done.');
}
$response->addHeader('X-Status', rawurlencode($this->completeMessage));
}
}
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace A2nt\CMSNiceties\GraphQL;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\GraphQL\Auth\AuthenticatorInterface;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\Member;
use A2nt\CMSNiceties\Templates\WebpackTemplateProvider;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
class APIKeyAuthenticator implements AuthenticatorInterface
{
public function authenticate(HTTPRequest $request)
{
$member = Security::getCurrentUser();
if (Director::isLive()
&& $request->getHeader('apikey') !== WebpackTemplateProvider::config()['GRAPHQL_API_KEY']
) {
if ($member && Permission::checkMember($member, 'CMS_ACCESS')) {
return $member;
}
throw new ValidationException('Restricted resource', 401);
}
return Member::get()->first();
}
public function isApplicable(HTTPRequest $request)
{
if ($request->param('Controller') === '%$SilverStripe\GraphQL\Controller.admin') {
return false;
}
/*if($request->getHeader('apikey')){
return true;
}*/
return true;
return false;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace A2nt\CMSNiceties\GraphQL;
use GraphQL\Type\Schema;
use SilverStripe\GraphQL\Middleware\QueryMiddleware;
use A2nt\CMSNiceties\Templates\WebpackTemplateProvider;
class APIKeyMiddleware implements QueryMiddleware
{
public function process(Schema $schema, $query, $context, $params, callable $next)
{
var_dump($context);
die('saaddsdsads');
if($request->getHeader('apikey') === WebpackTemplateProvider::config()['GRAPHQL_API_KEY']) {
return $next($schema, $query, $context, $params);
}
throw new \Exception('Invalid API key token');
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace A2nt\CMSNiceties\GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use SilverStripe\GraphQL\TypeCreator;
class ElementTypeCreator extends TypeCreator
{
public function attributes()
{
return [
'name' => 'element'
];
}
public function fields()
{
return [
'_id' => ['type' => Type::nonNull(Type::id()),'resolve' => static function($object) {
return $object->ID;
}],
'ID' => ['type' => Type::nonNull(Type::id())],
'Title' => ['type' => Type::string()],
'ParentID' => ['type' => Type::id()],
'Render' => [
'type' => Type::string(),
'resolve' => static function($object, array $args, $context, ResolveInfo $info) {
return $object->getController()->forTemplate()->HTML();
}
],
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace A2nt\CMSNiceties\GraphQL;
use GraphQL\Type\Definition\Type;
use SilverStripe\GraphQL\TypeCreator;
use SilverStripe\GraphQL\Pagination\Connection;
class MemberTypeCreator extends TypeCreator
{
public function attributes()
{
return [
'name' => 'member'
];
}
public function fields()
{
return [
'ID' => ['type' => Type::nonNull(Type::id())],
'Email' => ['type' => Type::string()],
'FirstName' => ['type' => Type::string()],
'Surname' => ['type' => Type::string()],
];
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace A2nt\CMSNiceties\GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use SilverStripe\CMS\Controllers\ModelAsController;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\GraphQL\TypeCreator;
use SilverStripe\GraphQL\Pagination\Connection;
use SilverStripe\View\SSViewer;
class PageTypeCreator extends TypeCreator
{
public function attributes()
{
return [
'name' => 'page'
];
}
public function fields()
{
$elementsConnection = Connection::create('Elements')
->setConnectionType($this->manager->getType('element'))
->setDescription('A list of the page elements')
->setSortableFields(['ID', 'Title']);
return [
'_id' => ['type' => Type::nonNull(Type::id()),'resolve' => static function($object) {
return $object->ID;
}],
'ID' => ['type' => Type::nonNull(Type::id())],
'Title' => ['type' => Type::string()],
'Content' => ['type' => Type::string()],
'Link' => ['type' => Type::string(), 'resolve' => static function($object) {
return $object->Link();
}],
'URLSegment' => ['type' => Type::string()],
'ParentID' => ['type' => Type::id()],
'ClassName' => ['type' => Type::string()],
'CSSClass' => ['type' => Type::string(), 'resolve' => static function($object) {
return $object->CSSClass();
}],
'Summary' => ['type' => Type::string(), 'resolve' => static function($object) {
return $object->Summary();
}],
'HTML' => ['type' => Type::string(), 'resolve' => static function($object) {
// get action from request
$action = null;
/** @var \Page $object */
Director::set_current_page($object);
/** @var \PageController $controller */
$controller = ModelAsController::controller_for($object);
// find templates
$tpl = 'Page';
$tpls = SSViewer::get_templates_by_class(
$object->ClassName,
($action ? '_'.$action : ''),
\Page::class
);
foreach ($tpls as $tpl){
if(is_array($tpl)){
continue;
}
$a_tpl = explode('\\',$tpl);
$last_name = array_pop($a_tpl);
$a_tpl[] = 'Layout';
$a_tpl[] = $last_name;
$a_tpl = implode('\\', $a_tpl);
if(SSViewer::hasTemplate($a_tpl)){
break;
}
}
//
$tpl = ($tpl !== 'Page') ? $tpl : 'Layout/Page';
$action = $action ? $action : 'index';
/** @var HTTPRequest $request */
$request = new HTTPRequest('GET', $object->AbsoluteLink());
$request->setSession(new Session([]));
// a little dirty way to make forms working
Controller::curr()->config()->set('url_segment', $object->AbsoluteLink());
/*$controller->setRequest($request);*/
//$request->getSession()->init($request);
$controller->setRequest($request);
$controller->setAction($action);
//$controller->pushCurrent();
$controller->doInit();
$layout = $controller->renderWith($tpl);
return $controller
->customise(['Layout' => $layout])
->renderWith('GraphQLPage')->HTML();
}],
'Elements' => [
'type' => $elementsConnection->toType(),
'args' => $elementsConnection->args(),
'resolve' => static function($object, array $args, $context, ResolveInfo $info) use ($elementsConnection) {
return $elementsConnection->resolveList(
$object->ElementalArea()->Elements(),
$args,
$context
);
}
]
];
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace A2nt\CMSNiceties\GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use SilverStripe\Security\Member;
use SilverStripe\GraphQL\Pagination\Connection;
use SilverStripe\GraphQL\Pagination\PaginatedQueryCreator;
class PaginatedReadMembersQueryCreator extends PaginatedQueryCreator
{
public function createConnection()
{
return Connection::create('paginatedReadMembers')
->setConnectionType($this->manager->getType('member'))
->setArgs([
'Email' => [
'type' => Type::string()
]
])
->setSortableFields(['ID', 'FirstName', 'Email'])
->setConnectionResolver(static function ($object, array $args, $context, ResolveInfo $info) {
$member = Member::singleton();
if (!$member->canView($context['currentUser'])) {
throw new \InvalidArgumentException(sprintf(
'%s view access not permitted',
Member::class
));
}
$list = Member::get();
// Optional filtering by properties
if (isset($args['Email'])) {
$list = $list->filter('Email', $args['Email']);
}
return $list;
});
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace A2nt\CMSNiceties\GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\SS_List;
use SilverStripe\Security\Member;
use SilverStripe\GraphQL\Pagination\Connection;
use SilverStripe\GraphQL\Pagination\PaginatedQueryCreator;
class PaginatedReadPagesQueryCreator extends PaginatedQueryCreator
{
public function createConnection()
{
return Connection::create('readPages')
->setConnectionType($this->manager->getType('page'))
->setArgs([
'Link' => [
'type' => Type::string()
]
])
->setSortableFields(['Sort'])
->setConnectionResolver(static function ($object, array $args, $context, ResolveInfo $info) {
if (isset($args['Link'])) {
$link = $args['Link'];
if(SiteTree::has_extension('\TractorCow\Fluent\Extension\FluentSiteTreeExtension')) {
$arr = array_filter(explode('/', $args['Link']));
$locale = \TractorCow\Fluent\Model\Locale::get()->filter('URLSegment', array_shift($arr))->first();
\TractorCow\Fluent\State\FluentState::singleton()->setLocale($locale->Locale);
$link = implode('/', $arr);
}
$list = ArrayList::create();
$page = SiteTree::get_by_link($link);
$list->add($page);
}
/*$list = \Page::get();
// Optional filtering by properties
if (isset($args['ID'])) {
$list = $list->filter('ID', $args['ID']);
}*/
return $list;
});
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace A2nt\CMSNiceties\GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use SilverStripe\Security\Member;
use SilverStripe\GraphQL\OperationResolver;
use SilverStripe\GraphQL\QueryCreator;
class ReadMembersQueryCreator extends QueryCreator implements OperationResolver
{
public function attributes()
{
return [
'name' => 'readMembers'
];
}
public function args()
{
return [
'Email' => ['type' => Type::string()]
];
}
public function type()
{
return Type::listOf($this->manager->getType('member'));
}
public function resolve($object, array $args, $context, ResolveInfo $info)
{
$member = Member::singleton();
if (!$member->canView($context['currentUser'])) {
throw new \InvalidArgumentException(sprintf(
'%s view access not permitted',
Member::class
));
}
$list = Member::get();
// Optional filtering by properties
if (isset($args['Email'])) {
$list = $list->filter('Email', $args['Email']);
}
return $list;
}
}

57
src/Models/Holiday.php Executable file
View File

@ -0,0 +1,57 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 9/12/18
* Time: 2:55 AM
*/
namespace A2nt\CMSNiceties\Models;
use Dynamic\FlexSlider\Model\SlideImage;
use Sheadawson\Linkable\Forms\LinkField;
use Sheadawson\Linkable\Models\Link;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\SiteConfig\SiteConfig;
class Holiday extends DataObject
{
private static $table_name = 'Holiday';
private static $db = [
'Title' => 'Varchar(255)',
'Date' => 'Date',
];
private static $has_one = [
'Parent' => SiteConfig::class,
];
private static $summary_fields = [
'Title' => 'Title',
'Date' => 'Date',
];
private static $default_sort = 'Date ASC, Title ASC';
public function validate()
{
$result = parent::validate();
$exists = self::get()->filter([
'ID:not' => $this->ID,
'Date' => $this->getField('Date'),
])->exists();
if($exists) {
return $result->addError(
'Holiday was defined already.',
ValidationResult::TYPE_ERROR
);
}
return $result;
}
}

80
src/Models/Notification.php Executable file
View File

@ -0,0 +1,80 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 9/12/18
* Time: 2:55 AM
*/
namespace A2nt\CMSNiceties\Models;
use Dynamic\FlexSlider\Model\SlideImage;
use Sheadawson\Linkable\Forms\LinkField;
use Sheadawson\Linkable\Models\Link;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\SiteConfig\SiteConfig;
class Notification extends DataObject
{
private static $table_name = 'Notification';
private static $db = [
'Title' => 'Varchar(255)',
'Content' => 'Text',
'DateOn' => 'Date',
'DateOff' => 'Date',
'Area' => 'Enum("Site","Site")',
];
private static $has_one = [
'Parent' => SiteConfig::class,
'TargetLink' => Link::class,
];
private static $defaults = [
'Area' => 'Site',
];
private static $summary_fields = [
'Title' => 'Title',
'Content' => 'Text',
'DateOn' => 'Turn on date',
'DateOff' => 'Turn off date',
];
private static $default_sort = 'DateOn DESC, DateOff DESC, Title ASC';
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->addFieldsToTab('Root.Main', [
LinkField::create('TargetLinkID', 'Link'),
]);
return $fields;
}
public function validate()
{
$result = parent::validate();
if (!$this->getField('DateOn') || !$this->getField('DateOff')) {
return $result->addError(
'Turn on and turn off dates are required.',
ValidationResult::TYPE_ERROR
);
}
if (!$this->getField('Content')) {
return $result->addError(
'Text field required.',
ValidationResult::TYPE_ERROR
);
}
return $result;
}
}

90
src/Models/OpeningHour.php Executable file
View File

@ -0,0 +1,90 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 9/12/18
* Time: 2:55 AM
*/
namespace A2nt\CMSNiceties\Models;
use Dynamic\FlexSlider\Model\SlideImage;
use Sheadawson\Linkable\Forms\LinkField;
use Sheadawson\Linkable\Models\Link;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\SiteConfig\SiteConfig;
class OpeningHour extends DataObject
{
private static $table_name = 'OpeningHour';
private static $db = [
'Day' => 'Enum("Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday", "Monday")',
'From' => 'Time',
'Till' => 'Time',
'Comment' => 'Varchar(255)',
'DisplayStart' => 'Date',
'DisplayEnd' => 'Date',
];
private static $has_one = [
'Parent' => SiteConfig::class,
];
private static $defaults = [
'Day' => 'Monday',
'From' => '09:00:00',
'Till' => '22:00:00',
];
private static $summary_fields = [
'Day' => 'Day *',
'From' => 'From *',
'Till' => 'Till *',
//'Comment' => 'Comment',
'DisplayStart' => 'Display Start',
'DisplayEnd' => 'Display End',
];
private static $default_sort = 'Day ASC, From ASC';
public function validate()
{
$result = parent::validate();
if (!$this->getField('Day')
|| !$this->getField('From')
|| !$this->getField('Till')
|| $this->getField('From') > $this->getField('Till')
) {
return $result->addError(
'Day, From and Till fields are required or wrong time range was specified.',
ValidationResult::TYPE_ERROR
);
}
$exists = self::get()->filter([
'ID:not' => $this->ID,
'ParentID' => $this->getField('ParentID'),
'Day' => $this->getField('Day'),
'From:GreaterThanOrEqual' => $this->getField('From'),
'Till:LessThanOrEqual' => $this->getField('Till'),
])->exists();
if ($exists) {
return $result->addError(
'Hours were defined already for specified range.',
ValidationResult::TYPE_ERROR
);
}
return $result;
}
public function forTemplate()
{
return $this->renderWith(__CLASS__);
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 8/26/18
* Time: 1:40 PM
*/
namespace A2nt\CMSNiceties\Models;
use SilverShop\Checkout\Component\CheckoutComponent;
use SilverShop\Model\Order;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\View\SSViewer;
class CheckoutMapComponent extends CheckoutComponent
{
public function getFormFields(Order $order)
{
$config = SiteConfig::current_site_config();
return FieldList::create(
HeaderField::create('MapHeader', 'Pick up location'),
LiteralField::create(
'Map',
SSViewer::create('Objects\Map')->process($config)
)
);
}
public function validateData(Order $order, array $data)
{
}
public function setData(Order $order, array $data)
{
}
public function getData(Order $order)
{
return [];
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 8/26/18
* Time: 1:08 PM
*/
namespace A2nt\CMSNiceties\Models;
use SilverShop\Checkout\Checkout;
use SilverShop\Checkout\CheckoutComponentConfig;
use SilverShop\Checkout\Component\CustomerDetails;
use SilverShop\Checkout\Component\Notes;
use SilverShop\Checkout\Component\Payment;
use SilverShop\Checkout\Component\Terms;
use SilverShop\Checkout\Component\Membership;
use SilverShop\Model\Order;
use SilverStripe\Omnipay\GatewayInfo;
use SilverStripe\Security\Security;
class CheckoutNoDeliveryConfig extends CheckoutComponentConfig
{
public function __construct(Order $order)
{
parent::__construct($order);
$this->addComponent(CustomerDetails::create());
if (Checkout::member_creation_enabled() && !Security::getCurrentUser()) {
$this->addComponent(Membership::create());
}
if (count(GatewayInfo::getSupportedGateways()) > 1) {
$this->addComponent(Payment::create());
}
$this->addComponent(Notes::create());
$this->addComponent(CheckoutMapComponent::create());
$this->addComponent(Terms::create());
}
}

1
src/Shop/_manifest_exclude Executable file
View File

@ -0,0 +1 @@
Remove it in case u need silvershop module

39
src/Tasks/BrokenFilesTask.php Executable file
View File

@ -0,0 +1,39 @@
<?php
namespace A2nt\CMSNiceties\Tasks;
use SilverStripe\Assets\File;
use SilverStripe\Dev\BuildTask;
class BrokenFilesTask extends BuildTask
{
protected $title = 'Broken Files Task';
protected $description = 'Broken files report';
protected $enabled = true;
public function run($request)
{
$files = File::get();
$i = 0;
foreach ($files as $file) {
if (!$file->exists()) {
echo '<b style="color:red">File name was not found at SS DB: '
.$file->getField('Name').'</b><br/>'
.PHP_EOL;
$i++;
continue;
}
}
echo ($i > 0) ?
'<h2 style="color:red">Missing '.$i.' files</h2>'
: '<h2 style="color:green">All files are ok!</h2>';
die('Done!');
}
}

78
src/Tasks/BuildTask.php Executable file
View File

@ -0,0 +1,78 @@
<?php
namespace A2nt\CMSNiceties\Tasks;
use SilverStripe\Control\HTTPRequest;
class BuildTask extends \SilverStripe\Dev\BuildTask
{
protected $title = 'Base build task interface';
protected $description = 'Base build task interface';
protected $enabled = false;
protected $messages = [];
/**
* Implement this method in the task subclass to
* execute via the TaskRunner
*
* @param HTTPRequest $request
* @return
*/
public function run($request)
{
// TODO: Implement run() method.
return $this->render();
}
public function Title()
{
return $this->title;
}
protected function setMessage($msg, $type = 'msg')
{
if(is_array($msg)) {
$type = 'list';
}
$this->messages[] = [$type, $msg];
}
public function render()
{
echo '<style>'
.'.info{color:#053bff}'
.'.bad,.error{color:red}'
.'.good,.success{color:green}'
.'</style>';
foreach ($this->messages as $item) {
$type = $item[0];
$msg = $item[1];
switch ($type) {
case 'h2':
echo '<h2>'.$msg.'</h2>'.PHP_EOL;
break;
case 'h3':
echo '<h3>'.$msg.'</h3>'.PHP_EOL;
break;
case 'list':
echo '<ul>';
foreach ($msg as $m) {
echo '<li>'.$m.'</li>';
}
echo '</ul>';
break;
default:
echo $msg.'<br/>'.PHP_EOL;
break;
}
}
echo '<h2 class="success">Success!</h2>';
}
}

29
src/Tasks/CleanContentTask.php Executable file
View File

@ -0,0 +1,29 @@
<?php
namespace A2nt\CMSNiceties\Tasks;
use SilverStripe\Assets\File;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\BuildTask;
class CleanContentTask extends BuildTask
{
protected $title = 'Clean content task';
protected $description = 'Clean content task';
protected $enabled = true;
public function run($request)
{
$pages = SiteTree::get();
foreach ($pages as $p) {
$p->setField('Content', '');
$p->write();
echo '#'.$p->ID.' '.$p->getField('Title').'<br/>';
}
die('Done!');
}
}

105
src/Tasks/QRCodeTask.php Executable file
View File

@ -0,0 +1,105 @@
<?php
namespace A2nt\CMSNiceties\Tasks;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\LabelAlignment;
use Endroid\QrCode\QrCode;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Image;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Upload;
use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
class QRCodeTask extends BuildTask
{
protected $title = 'Generate website QR-code';
protected $description = 'Generate website QR-code';
protected $enabled = true;
public function run($request)
{
echo '<h1>'.$this->Title().'</h1>';
echo self::generateQR();
die('Done!');
}
public static function generateQR()
{
$qrCode = new QrCode(Director::absoluteBaseURL());
$qrCode->setSize(600);
$qrCode->setMargin(10);
$qrCode->setWriterByName('png');
$qrCode->setEncoding('UTF-8');
$qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH());
$qrCode->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0, 'a' => 0]);
$qrCode->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255, 'a' => 0]);
$qrCode->setLabel(Director::absoluteBaseURL(), 16, null, LabelAlignment::CENTER());
/*$qrCode->setLogoPath('/'.File::join_paths(
PUBLIC_PATH,
RESOURCES_DIR,
project(),
'client', 'dist', 'icons',
'apple-touch-icon-152x152.png'
));
$qrCode->setLogoSize(152, 152);*/
$qrCode->setValidateResult(true);
// Round block sizes to improve readability and make the blocks sharper in pixel based outputs (like png).
// There are three approaches:
$qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_MARGIN); // The size of the qr code is shrinked, if necessary, but the size of the final image remains unchanged due to additional margin being added (default)
$qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_ENLARGE); // The size of the qr code and the final image is enlarged, if necessary
$qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_SHRINK); // The size of the qr code and the final image is shrinked, if necessary
// Set additional writer options (SvgWriter example)
$qrCode->setWriterOptions(['exclude_xml_declaration' => true]);
// Directly output the QR code
/*header('Content-Type: '.$qrCode->getContentType());
echo $qrCode->writeString();
die();*/
// Save it to a file
$qrCode->writeFile(TEMP_PATH.'/qrcode.png');
$res = self::getAssetStore()->setFromLocalFile(
TEMP_PATH.'/qrcode.png',
'qrcode.png', null, null,
[
'conflict' => AssetStore::CONFLICT_OVERWRITE,
'visibility' => AssetStore::VISIBILITY_PUBLIC,
]
);
$img = Image::get()->filter([
'ParentID' => 0,
'FileFilename' => $res['Filename'],
])->first();
if(!$img) {
$img = Image::create();
}
$res['FileHash'] = $res['Hash'];
$res['FileFilename'] = $res['Filename'];
$res['ParentID'] = 0;
$img = $img->update($res);
$img->write();
$img->publishFile();
return '<img src="'.$qrCode->writeDataUri().'" width="300" alt="QR-code" /><br/>';
}
protected static function getAssetStore()
{
return Injector::inst()->get(AssetStore::class);
}
}

47
src/Tasks/RestoreFilesTask.php Executable file
View File

@ -0,0 +1,47 @@
<?php
namespace A2nt\CMSNiceties\Tasks;
use SilverStripe\Assets\File;
use SilverStripe\Dev\BuildTask;
class RestoreFilesTask extends BuildTask
{
protected $title = 'Restore Files Task';
protected $description = 'Restores file from specific folder';
protected $enabled = false;
public function run($request)
{
die('Specify path first');
$path = '*<Path to the folder with files to be restored>*';
$files = array_diff(scandir($path), ['.','..']);
foreach ($files as $fileName) {
$file = File::get()->filter('Name', $fileName);
if (!$file->count()) {
echo '<b style="color:red">File name was not found at SS DB: '.$fileName.'</b><br/>'.PHP_EOL;
continue;
}
foreach ($file as $f) {
if ($f->exists()) {
echo 'File #'.$f->ID.' already exists at SS file structure. <b style="color:green">'.$fileName.'</b><br/>' . PHP_EOL;
continue;
}
echo 'Found non existing at SS file system file and found it at SS DB.'
.' Creating the file #'.$f->ID.' at SS file system. "<b style="color:#053bff">' . $fileName . '"</b><br/>' . PHP_EOL;
$f->setFromLocalFile($path.'/'.$fileName);
$f->write();
$f->publishFile();
}
}
die('Success!');
}
}

View File

@ -0,0 +1,229 @@
<?php
/** @noinspection PhpUnusedPrivateFieldInspection */
namespace A2nt\CMSNiceties\Templates;
use SilverStripe\Control\Controller;
use SilverStripe\View\TemplateGlobalProvider;
use SilverStripe\View\Requirements;
use SilverStripe\Core\Config\Config;
use SilverStripe\Control\Director;
use SilverStripe\Core\Path;
use SilverStripe\FontAwesome\FontAwesomeField;
class DeferredRequirements implements TemplateGlobalProvider
{
private static $css = [];
private static $js = [];
private static $deferred = false;
private static $static_domain;
private static $version;
private static $nojquery = false;
private static $jquery_version = '3.4.1';
private static $nofontawesome = false;
private static $custom_requirements = [];
/**
* @return array
*/
public static function get_template_global_variables(): array
{
return [
'AutoRequirements' => 'Auto',
'DeferedCSS' => 'loadCSS',
'DeferedJS' => 'loadJS',
'WebpackActive' => 'webpackActive',
'EmptyImgSrc' => 'emptyImageSrc',
];
}
public static function Auto($class = false): string
{
$config = Config::inst()->get(self::class);
$projectName = WebpackTemplateProvider::projectName();
$mainTheme = WebpackTemplateProvider::mainTheme();
$mainTheme = $mainTheme ?: $projectName;
$dir = Path::join(
Director::publicFolder(),
RESOURCES_DIR,
$projectName,
'client',
'dist'
);
$cssPath = Path::join($dir, 'css');
$jsPath = Path::join($dir, 'js');
// Initialization
Requirements::block(THIRDPARTY_DIR.'/jquery/jquery.js');
/*if (defined('FONT_AWESOME_DIR')) {
Requirements::block(FONT_AWESOME_DIR.'/css/lib/font-awesome.min.css');
}*/
Requirements::set_force_js_to_bottom(true);
// Main libs
if (!$config['nojquery']) {
self::loadJS(
'//ajax.googleapis.com/ajax/libs/jquery/'
.$config['jquery_version'].'/jquery.min.js'
);
}
if (!$config['noreact']) {
if (!Director::isDev()) {
self::loadJS('https://unpkg.com/react@17/umd/react.production.min.js');
self::loadJS('https://unpkg.com/react-dom@17/umd/react-dom.production.min.js');
} else {
self::loadJS('https://unpkg.com/react@17/umd/react.development.js');
self::loadJS('https://unpkg.com/react-dom@17/umd/react-dom.development.js');
}
}
// App libs
if (!$config['nofontawesome']) {
$v = !isset($config['fontawesome_version']) || !$config['fontawesome_version']
? Config::inst()->get(FontAwesomeField::class, 'version')
: $config['fontawesome_version'];
self::loadCSS('//use.fontawesome.com/releases/v'.$v.'/css/all.css');
}
self::loadCSS($mainTheme.'.css');
// hot reloading
/*if (self::webpackActive()) {
self::loadJS('hot.js');
}*/
self::loadJS($mainTheme.'.js');
// Custom controller requirements
$curr_class = $class ?: get_class(Controller::curr());
if (isset($config['custom_requirements'][$curr_class])) {
foreach ($config['custom_requirements'][$curr_class] as $file) {
if (strpos($file, '.css')) {
self::loadCSS($file);
}
if (strpos($file, '.js')) {
self::loadJS($file);
}
}
}
$curr_class = str_replace('\\', '.', $curr_class);
// Controller requirements
$themePath = Path::join($cssPath, $mainTheme.'_'.$curr_class . '.css');
$projectPath = Path::join($cssPath, $projectName.'_'.$curr_class . '.css');
if ($mainTheme && file_exists($themePath)) {
self::loadCSS($mainTheme.'_'.$curr_class . '.css');
} elseif (file_exists($projectPath)) {
self::loadCSS($projectName.'_'.$curr_class . '.css');
}
$themePath = Path::join($jsPath, $mainTheme.'_'.$curr_class . '.js');
$projectPath = Path::join($jsPath, $projectName.'_'.$curr_class . '.js');
if ($mainTheme && file_exists($themePath)) {
self::loadJS($mainTheme.'_'.$curr_class . '.js');
} elseif (file_exists($projectPath)) {
self::loadJS($projectName.'_'.$curr_class . '.js');
}
return self::forTemplate();
}
public static function loadCSS($css): void
{
$external = (mb_strpos($css, '//') === 0 || mb_strpos($css, 'http') === 0);
if ($external) {
self::$css[] = $css;
} else {
WebpackTemplateProvider::loadCSS($css);
}
}
public static function loadJS($js): void
{
/*$external = (mb_substr($js, 0, 2) === '//' || mb_substr($js, 0, 4) === 'http');
if ($external || (self::getDeferred() && !self::_webpackActive())) {*/
// webpack supposed to load external JS
if (self::getDeferred() && !self::webpackActive()) {
self::$js[] = $js;
} else {
WebpackTemplateProvider::loadJS($js);
}
}
public static function webpackActive(): bool
{
return WebpackTemplateProvider::isActive();
}
public static function setDeferred($bool): void
{
Config::inst()->set(__CLASS__, 'deferred', $bool);
}
public static function getDeferred(): bool
{
return self::config()['deferred'];
}
public static function forTemplate(): string
{
$result = '';
self::$css = array_unique(self::$css);
foreach (self::$css as $css) {
$result .= '<i class="defer-cs" data-load="' . self::get_url($css) . '"></i>';
}
self::$js = array_unique(self::$js);
foreach (self::$js as $js) {
$result .= '<i class="defer-sc" data-load="' . self::get_url($js) . '"></i>';
}
$result .=
'<script>function lsc(a,b){var c=document.createElement("script");c.readyState'
.'?c.onreadystatechange=function(){"loaded"!=c.readyState&&"complete"!=c.readyState||(c.onreadystatechange=null,b())}'
.':c.onload=function(){b()},c.src=a,document.getElementsByTagName("body")[0].appendChild(c)}'
.'function lscd(a){a<s.length-1&&(a++,lsc(s.item(a).getAttribute("data-load"),function(){lscd(a)}))}'
.'for(var s=document.getElementsByClassName("defer-cs"),i=0;i<s.length;i++){var b=document.createElement("link");b.rel="stylesheet",'
.'b.type="text/css",b.href=s.item(i).getAttribute("data-load"),b.media="all";var c=document.getElementsByTagName("body")[0];'
.'c.appendChild(b)}var s=document.getElementsByClassName("defer-sc"),i=0;if(s.item(i)!==null)lsc(s.item(i).getAttribute("data-load"),function(){lscd(i)});'
.'</script>';
return $result;
}
private static function get_url($url): string
{
$config = self::config();
// external URL
if (strpos($url, '//') !== false) {
return $url;
}
$path = WebpackTemplateProvider::toPublicPath($url);
$absolutePath = Director::getAbsFile($path);
$hash = sha1_file($absolutePath);
$version = $config['version'] ? '&v='.$config['version'] : '';
//$static_domain = $config['static_domain'];
//$static_domain = $static_domain ?: '';
return Controller::join_links(WebpackTemplateProvider::toPublicPath($url), '?m='.$hash.$version);
}
public static function emptyImageSrc(): string
{
return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
}
public static function config(): array
{
return Config::inst()->get(__CLASS__);
}
}

View File

@ -0,0 +1,153 @@
<?php
/** @noinspection PhpUnusedPrivateFieldInspection */
/**
* Directs assets requests to Webpack server or to static files
*/
namespace A2nt\CMSNiceties\Templates;
use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\View\SSViewer;
use SilverStripe\View\TemplateGlobalProvider;
use SilverStripe\View\Requirements;
use SilverStripe\Control\Director;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Config\Config;
class WebpackTemplateProvider implements TemplateGlobalProvider
{
/**
* @var int port number
*/
private static $port = 3000;
/**
* @var string host name
*/
private static $hostname = 'localhost';
/**
* @var string assets static files directory
*/
private static $dist = 'client/dist';
private static $webp = false;
/**
* @return array
*/
public static function get_template_global_variables(): array
{
return [
'WebpackDevServer' => 'isActive',
'WebpackCSS' => 'loadCSS',
'WebpackJS' => 'loadJS',
'ResourcesURL' => 'resourcesURL',
'ProjectName' => 'themeName',
];
}
/**
* Load CSS file
* @param $path
*/
public static function loadCSS($path): void
{
/*if (self::isActive()) {
return;
}*/
Requirements::css(self::_getPath($path));
}
/**
* Load JS file
* @param $path
*/
public static function loadJS($path): void
{
Requirements::javascript(self::_getPath($path));
}
public static function projectName(): string
{
return Config::inst()->get(ModuleManifest::class, 'project');
}
public static function mainTheme()
{
$themes = Config::inst()->get(SSViewer::class, 'themes');
return is_array($themes) && $themes[0] !== '$public' && $themes[0] !== '$default' ? $themes[0] : false;
}
public static function resourcesURL($link = null): string
{
$cfg = self::config();
if ($cfg['webp'] && !self::isActive()) {
$link = str_replace(['.png','.jpg','.jpeg'], '.webp', $link);
}
return Controller::join_links(
Director::baseURL(),
'/resources/',
self::projectName(),
$cfg['dist'],
'img',
$link
);
}
/**
* Checks if dev mode is enabled and if webpack server is online
* @return bool
*/
public static function isActive(): bool
{
$cfg = self::config();
return Director::isDev() && @fsockopen(
$cfg['HOSTNAME'],
$cfg['PORT']
);
}
protected static function _getPath($path): string
{
return self::isActive() && strpos($path, '//') === false ?
self::_toDevServerPath($path) :
self::toPublicPath($path);
}
protected static function _toDevServerPath($path): string
{
$cfg = self::config();
return sprintf(
'%s%s:%s/%s',
($cfg['HTTPS'] ? 'https://' : 'http://'),
$cfg['HOSTNAME'],
$cfg['PORT'],
basename($path)
//Controller::join_links($cfg['APPDIR'], $cfg['SRC'], basename($path))
);
}
public static function toPublicPath($path): string
{
$cfg = self::config();
return strpos($path, '//') === false ?
Controller::join_links(
RESOURCES_DIR,
self::projectName(),
$cfg['dist'],
(strpos($path, '.css') ? 'css' : 'js'),
$path
)
: $path;
}
public static function config(): array
{
return Config::inst()->get(__CLASS__);
}
}

142
src/Tests/TestServer.php Executable file
View File

@ -0,0 +1,142 @@
<?php
/**
* Created by PhpStorm.
* User: tony
* Date: 10/31/18
* Time: 5:31 AM
*/
namespace A2nt\CMSNiceties\Tests;
use SilverStripe\Assets\Upload_Validator;
use SilverStripe\Core\Cache\FilesystemCacheFactory;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\BuildTask;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Assets\File;
class TestServer extends BuildTask
{
protected $title = 'Test server';
protected $description = 'Test server';
public function run($request)
{
echo '<style>table{width:100%}table td,table th{border:1px solid #dedede}</style>';
echo '<h2>Testing Server</h2>';
echo self::success('BASE_PATH: <b>'.BASE_PATH.'</b>');
echo self::success('PHP: <b>'.phpversion().'</b>');
$v = Deprecation::dump_settings()['version'];
if ($v) {
echo self::success('SilverStipe version: <b>'.$v.'</b>');
} else {
echo self::success('SilverStipe version unknown: <b>'
.(file_exists(BASE_PATH.'/framework') ? '3.x.x' : '4.x.x')
.'</b>');
}
echo self::success('Memory limit: <b>'.ini_get('memory_limit').'</b>');
if (is_writable(TEMP_FOLDER)) {
echo self::success('TMP (cache) dir <b>'.TEMP_FOLDER.'</b> dir is writable.');
} else {
echo self::error('TMP (cache) dir <b>'.TEMP_FOLDER.'</b> dir is no writable!');
}
echo '<h2>Testing Uploads</h2>';
$maxUpload = ini_get('upload_max_filesize');
$maxPost = ini_get('post_max_size');
echo self::success('PHP max upload size: '.$maxUpload);
echo self::success('PHP max post size: '.$maxPost);
echo self::success('So calculated max upload size: '.self::formatBytes(min(
self::memstring2bytes($maxUpload),
self::memstring2bytes($maxPost)
)));
$defaultSizes = Config::inst()->get(Upload_Validator::class, 'default_max_file_size');
if ($defaultSizes) {
if (!is_array($defaultSizes)) {
echo self::error('default_max_file_size miss-configuration, plz fix');
var_dump($defaultSizes);
die();
}
echo '<h3>Configured limits:</h3><table style="text-align:center">'
.'<thead><tr><th>File</th><th>Size limit</th></tr></thead><tbody>';
foreach ($defaultSizes as $k => $size) {
echo '<tr><td>'.$k.'</td><td>'.$size.'</td></tr>';
}
echo '</tbody></table>';
}
if (is_writable(ASSETS_DIR)) {
echo self::success('Assets dir <b>'.ASSETS_DIR.'</b> dir is writable.');
} else {
echo self::error('Assets dir <b>'.ASSETS_DIR.'</b> dir is no writable!');
}
if (function_exists('imagewebp')) {
echo self::success('WebP is available');
} else {
echo self::error('WebP is not available');
}
die();
}
public static function formatBytes($size, $precision = 2)
{
$base = log($size, 1024);
$suffixes = array('', 'K', 'M', 'G', 'T');
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[(string)floor($base)];
}
public static function error($text)
{
return '<span style="color:red">ERROR: '.$text.'</span><br/>';
}
public static function success($text)
{
return '<span style="color:green">SUCCESS: '.$text.'</span><br/>';
}
public static function warning($text)
{
return '<span style="color:#0014ff">WARNING: '.$text.'</span><br/>';
}
public static function renderValidation($result)
{
echo '<p>';
$msgs = $result->getMessages();
foreach ($msgs as $msg) {
echo self::error($msg['fieldName'].': '.$msg['message']);
}
echo '</p>';
}
public static function memstring2bytes($memString)
{
// Remove non-unit characters from the size
$unit = preg_replace('/[^bkmgtpezy]/i', '', $memString);
// Remove non-numeric characters from the size
$size = preg_replace('/[^0-9\.]/', '', $memString);
if ($unit) {
// Find the position of the unit in the ordered string which is the power
// of magnitude to multiply a kilobyte by
return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
}
return round($size);
}
}

18
src/Traits/PaginatedListing.php Executable file
View File

@ -0,0 +1,18 @@
<?php
namespace A2nt\CMSNiceties\Traits;
trait PaginatedListing
{
private $filter = [];
public function NextPage($pageID = null)
{
$vars = $this->getRequest()->requestVars();
$vars = array_filter($vars);
$vars['page'] = $pageID ? $pageID : '2';
return $this->Link('?'.http_build_query($vars));
}
}

66
src/Widgets/BannerWidget.php Executable file
View File

@ -0,0 +1,66 @@
<?php
namespace A2nt\CMSNiceties\Widgets;
use Sheadawson\Linkable\Forms\LinkField;
use Sheadawson\Linkable\Models\Link;
use SilverStripe\AssetAdmin\Forms\UploadField;
use SilverStripe\Assets\Image;
use SilverStripe\Widgets\Model\Widget;
if (!class_exists(Widget::class)) {
return;
}
class BannerWidget extends Widget
{
private static $title = 'Banner';
private static $cmsTitle = 'Banner';
private static $description = 'Shows banner with image and link.';
private static $icon = '<i class="icon font-icon-block-banner"></i>';
private static $table_name = 'BannerWidget';
private static $has_one = [
'Image' => Image::class,
'Link' => Link::class,
];
private static $owns = [
'Image',
'Link',
];
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->push(UploadField::create('Image', 'Image (minimal width 301px)')
->setAllowedFileCategories(['image/supported']));
$fields->push(LinkField::create('LinkID', 'Link'));
return $fields;
}
private $_random;
public function Random()
{
if (!$this->_random) {
$this->_random = self::get()->filter('Enabled', true)->sort('RAND()')->first();
}
return $this->_random;
}
public function onBeforeWrite()
{
$title = $this->getField('Title');
$img = $this->Image();
if(!$title && $img) {
$this->setField('Title', $img->getTitle());
}
parent::onBeforeWrite();
}
}

37
src/Widgets/ContentWidget.php Executable file
View File

@ -0,0 +1,37 @@
<?php
namespace A2nt\CMSNiceties\Widgets;
use Sheadawson\Linkable\Forms\LinkField;
use Sheadawson\Linkable\Models\Link;
use SilverStripe\AssetAdmin\Forms\UploadField;
use SilverStripe\Assets\Image;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\Widgets\Model\Widget;
if (!class_exists(Widget::class)) {
return;
}
class ContentWidget extends Widget
{
private static $title = 'Content';
private static $cmsTitle = 'Content';
private static $description = 'Shows text content.';
private static $icon = '<i class="icon font-icon-block-content"></i>';
private static $table_name = 'ContentWidget';
private static $db = [
'Text' => 'HTMLText',
];
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->push(HTMLEditorField::create('Text'));
return $fields;
}
}

60
src/Widgets/ElementWidget.php Executable file
View File

@ -0,0 +1,60 @@
<?php
namespace A2nt\CMSNiceties\Widgets;
use DNADesign\Elemental\Models\BaseElement;
use DNADesign\ElementalList\Model\ElementList;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\TreeDropdownField;
use SilverStripe\Widgets\Model\Widget;
if (!class_exists(Widget::class)) {
return;
}
class ElementWidget extends Widget
{
private static $title = 'Virtual Element';
private static $cmsTitle = 'Virtual Element';
private static $description = 'Adds existing element to side bar';
private static $icon = '<i class="icon font-icon-block-banner"></i>';
private static $table_name = 'ElementWidget';
private static $has_one = [
'Element' => BaseElement::class,
];
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->push(
DropdownField::create(
'ElementID',
'Displayed Element',
BaseElement::get()
->filter(['AvailableGlobally' => true])
->exclude(['ClassName' => ElementList::class])
)
/*TreeDropdownField::create(
'ElementID',
'Displayed Element',
SiteTree::class
)->setFilterFunction(static function($el){
return (bool) $el->getField('ElementalArea')->Elements()->count();
})*/
);
return $fields;
}
public function SimpleClassName()
{
$el = $this->getField('Element');
var_dump($el);
die();
return $el->getSimpleClassName();
}
}

56
src/Widgets/LinksWidget.php Executable file
View File

@ -0,0 +1,56 @@
<?php
namespace A2nt\CMSNiceties\Widgets;
use Sheadawson\Linkable\Forms\LinkField;
use Sheadawson\Linkable\Models\Link;
use SilverStripe\AssetAdmin\Forms\UploadField;
use SilverStripe\Assets\Image;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Widgets\Model\Widget;
if (!class_exists(Widget::class)) {
return;
}
class LinksWidget extends Widget
{
private static $title = 'Links';
private static $cmsTitle = 'Links';
private static $description = 'Shows listing of links.';
private static $icon = '<i class="icon font-icon-list"></i>';
private static $table_name = 'LinksWidget';
private static $many_many = [
'Links' => Link::class,
];
private static $owns = [
'Links',
];
public function getCMSFields()
{
$fields = parent::getCMSFields();
if($this->ID) {
$fields->push(GridField::create(
'Links',
'',
$this->Links(),
GridFieldConfig_RecordEditor::create()
));
}else{
$fields->push(LiteralField::create(
'Note',
'<p class="alert alert-warning"><b>Note:</b> The widget needs to be saved before adding a link.'
.' Enter the Title and click "+ Create" button at the bottom left corner of the screen</p>')
);
}
return $fields;
}
}

53
src/Widgets/SubmenuWidget.php Executable file
View File

@ -0,0 +1,53 @@
<?php
namespace A2nt\CMSNiceties\Widgets;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Widgets\Model\Widget;
if (!class_exists(Widget::class)) {
return;
}
class SubmenuWidget extends Widget
{
private static $title = 'Sub-Menu';
private static $cmsTitle = 'Sub-Menu';
private static $description = 'Shows sub menu.';
private static $icon = '<i class="icon font-icon-tree"></i>';
private static $table_name = 'SubmenuWidget';
private static $db = [
'TopLevelSubmenu' => 'Boolean(1)',
];
public function getPage()
{
$area = $this->Parent();
return \Page::get()->filter('SideBarID', $area->ID)->first();
}
public function getSubmenu()
{
$page = $this->getPage();
if(!$this->getField('TopLevelSubmenu')) {
return $page->Children();
}
return $page->Level(1)->Children();
}
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->push(CheckboxField::create(
'TopLevelSubmenu',
'Display sub-menu starting from the top level (otherwise current page children will be displayed)'
));
return $fields;
}
}

157
src/Widgets/WidgetAreaField.php Executable file
View File

@ -0,0 +1,157 @@
<?php
namespace A2nt\CMSNiceties\Widgets;
use DNADesign\Elemental\Controllers\ElementalAreaController;
use DNADesign\Elemental\Forms\ElementalAreaConfig;
use DNADesign\Elemental\Forms\ElementalAreaField;
use DNADesign\Elemental\Models\BaseElement;
use DNADesign\Elemental\Models\ElementalArea;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldGroup;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
use SilverStripe\Forms\GridField\GridFieldAddNewButton;
use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldConfig_Base;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\Forms\GridField\GridFieldDataColumns;
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridFieldEditButton;
use SilverStripe\Forms\TabSet;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\Widgets\Model\Widget;
use SilverStripe\Widgets\Model\WidgetArea;
use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClass;
use UndefinedOffset\SortableGridField\Forms\GridFieldSortableRows;
class WidgetAreaField extends GridField
{
/**
* @var ElementalArea $area
*/
protected $area;
/**
* @var array $type
*/
protected $types = [];
/**
* @var null
*/
protected $inputType = null;
protected $modelClassName = Widget::class;
/**
* @param string $name
* @param ElementalArea $area
* @param string[] $blockTypes
*/
public function __construct($name, WidgetArea $area, array $blockTypes)
{
$this->setTypes($blockTypes);
$config = GridFieldConfig_Base::create();
$config->getComponentByType(GridFieldDataColumns::class)->setDisplayFields([
'Icon' => '',
'Title' => 'Title',
'Enabled' => 'Enabled',
])->setFieldFormatting([
'Icon' => static function ($v, Widget $item) {
return '<span style="font-size:2rem">'.$item::config()->get('icon').'</span>';
},
'Enabled' => static function ($v, Widget $item) {
return $item->getField('Enabled') ? 'Yes' : 'No';
},
]);
$config->addComponent(new GridFieldEditButton());
$config->addComponent(new GridFieldDeleteAction(false));
$config->addComponent(new GridFieldDetailForm(null, false, false));
$config->addComponent(new GridFieldSortableRows('Sort'));
if (!empty($blockTypes)) {
/** @var GridFieldAddNewMultiClass $adder */
$adder = Injector::inst()->create(GridFieldAddNewMultiClass::class);
$adder->setClasses($blockTypes);
$config->addComponent($adder);
}
// By default, no need for a title on the editor. If there is more than one area then use `setTitle` to describe
parent::__construct($name, '', $area->Widgets(), $config);
$this->area = $area;
$this->addExtraClass('element-editor__container no-change-track');
}
/**
* @param array $types
*
* @return $this
*/
public function setTypes($types)
{
$this->types = $types;
return $this;
}
/**
* @return array
*/
public function getTypes()
{
$types = $this->types;
$this->extend('updateGetTypes', $types);
return $types;
}
/**
* @return ElementalArea
*/
public function getArea()
{
return $this->area;
}
public function saveInto(DataObjectInterface $dataObject)
{
parent::saveInto($dataObject);
$elementData = $this->Value();
$idPrefixLength = strlen(sprintf(ElementalAreaController::FORM_NAME_TEMPLATE, ''));
if (!$elementData) {
return;
}
foreach ($elementData as $form => $data) {
// Extract the ID
$elementId = (int) substr($form, $idPrefixLength);
// @var BaseElement $element
$element = $this->getArea()->Widgets()->byID($elementId);
if (!$element) {
// Ignore invalid elements
continue;
}
$data = ElementalAreaController::removeNamespacesFromFields($data, $element->ID);
$element->updateFromFormData($data);
$element->write();
}
}
}

50
src/Widgets/WidgetExtension.php Executable file
View File

@ -0,0 +1,50 @@
<?php
namespace A2nt\CMSNiceties\Widgets;
use DNADesign\Elemental\Forms\TextCheckboxGroupField;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TreeDropdownField;
use SilverStripe\ORM\DataExtension;
class WidgetExtension extends DataExtension
{
private static $db = [
'ShowTitle' => 'Boolean(1)',
];
public function updateCMSFields(FieldList $fields)
{
parent::updateCMSFields($fields);
// Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
$fields->removeByName('ShowTitle');
$fields->replaceField(
'Title',
TextCheckboxGroupField::create()
->setName('Title')
);
$fields->push(TreeDropdownField::create(
'MovePageID', 'Move widget to page', SiteTree::class
));
}
public function onBeforeWrite()
{
$obj = $this->owner;
$moveID = $obj->MovePageID;
if ($moveID) {
$page = \Page::get()->byID($moveID);
if($page) {
$sidebarID = $page->getField('SideBarID');
if($sidebarID) {
$obj->setField('ParentID', $sidebarID);
}
}
}
parent::onBeforeWrite();
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace A2nt\CMSNiceties\Widgets;
use DNADesign\Elemental\Forms\ElementalAreaField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Widgets\Forms\WidgetAreaEditor;
use SilverStripe\Widgets\Model\Widget;
use SilverStripe\Widgets\Model\WidgetArea;
class WidgetPageExtension extends \SilverStripe\Widgets\Extensions\WidgetPageExtension
{
public function updateCMSFields(FieldList $fields)
{
parent::updateCMSFields($fields);
$tab = $fields->findOrMakeTab('Root.Widgets');
$tab->setTitle('Sidebar');
$tab->removeByName('SideBar');
$widgetTypes = WidgetAreaEditor::create('Sidebar')->AvailableWidgets();
$available = [];
/** @var Widget $type */
foreach ($widgetTypes as $type) {
$available[get_class($type)] = $type->getCMSTitle();
}
$tab->push(WidgetAreaField::create(
'SideBar',
$this->owner->Sidebar(),
$available
));
}
public function onBeforeWrite()
{
parent::onBeforeWrite();
if (!$this->owner->getField('SideBarID')) {
$area = WidgetArea::create();
$area->write();
$this->owner->setField('SideBarID', $area->ID);
}
}
}

View File

@ -0,0 +1,8 @@
<img src="$Image.FocusFill(432,315).URL" alt="$Title" />
<% if $Link %>
<% with $Link %>
<a href="$URL"<% if $OpenInNewWindow %> target="_blank"<% end_if %> class="stretched-link">
<span class="visually-hidden">$Up.Title</span>
</a>
<% end_with %>
<% end_if %>

View File

@ -0,0 +1,3 @@
<% if $Text %>
<div class="typography">$Text</div>
<% end_if %>

View File

@ -0,0 +1,9 @@
<% if $Element %>
<% with $Element %>
<div class="element element__{$SimpleClassName}<% if $StyleVariant %> $StyleVariant<% end_if %><% if $ExtraClass %> $ExtraClass<% end_if %>" id="$Anchor">
<div class="element-container<% if $ContainerClass %> $ContainerClass<% end_if %>">
$forTemplate
</div>
</div>
<% end_with %>
<% end_if %>

View File

@ -0,0 +1,18 @@
<% if $Submenu %>
<nav>
<ul class="nav flex-column">
<% with $Page.Level(1) %>
<li class="nav-item-level1 nav-item {$CSSClass} $ExtraClass">
<b class="nav-link">
$MenuTitle.XML
<% if $isCurrent || $isSection %><i class="visually-hidden">(current)</i><% end_if %>
</b>
</li>
<% end_with %>
<% loop $Submenu %>
<% include NavItem %>
<% end_loop %>
</ul>
</nav>
<% end_if %>

View File

@ -0,0 +1,21 @@
<% if $Pages %>
<div class="$DefaultContainer">
<nav class="breadcrumbs" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="/" class="breadcrumb-link">Home</a>
</li>
<% loop $Pages %>
<li
class="breadcrumb-item<% if $Last %> current active<% end_if %>"
<% if $Last %> aria-current="page"<% end_if %>
>
<% if not Up.Unlinked %><a href="$Link" class="graphql-page breadcrumb-link breadcrumb-link__$Pos"><% end_if %>
$MenuTitle.XML
<% if not Up.Unlinked %></a><% end_if %>
</li>
<% end_loop %>
</ol>
</nav>
</div>
<% end_if %>

1
templates/GraphQLPage.ss Executable file
View File

@ -0,0 +1 @@
<% include MainContent Layout=$Layout %>

29
templates/Page.ss Executable file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="$ContentLocale.ATT" dir="$i18nScriptDirection.ATT">
<%-- manifest="/cache.appcache" --%>
<head>
<% include MetaHead %>
</head>
<body<% with $SiteConfig %> data-default-lng="$Longitude" data-default-lat="$Latitude"<% end_with %>>
<div class="wrapper">
<% include First %>
<div id="MetaLightboxApp"></div>
<header id="Header">
<% include Header %>
</header>
<main id="MainContent" class="page-content" data-ajax-region="LayoutAjax">
<% include MainContent Layout=$Layout %>
</main>
</div>
<footer id="Footer" class="site-footer footer">
<% include Footer %>
</footer>
<% include Last %>
</body>
</html>

4
templates/WidgetHolder.ss Executable file
View File

@ -0,0 +1,4 @@
<nav class="secondary element element__widget widget__$ClassName {$SimpleClassName}">
<% if $ShowTitle && $Title %><h2 class="widget-title">$Title</h2><% end_if %>
$Content
</nav>