I use SonataFormatter with SonataAdmin. I can see the textarea but the toolbar doesn't appear.
I can force the toolbar in runtime with the code bellow. Am i missing something ?
CKEDITOR.replace( 'textbox id', {
toolbar: [
{ name: 'document', items: [ 'Source', '-', 'NewPage', 'Preview', '-', 'Templates' ] }, // Defines toolbar group with name (used to create voice label) and items in 3 subgroups.
[ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo' ], // Defines toolbar group without name.
'/', // Line break - next group will be placed in new line.
{ name: 'basicstyles', items: [ 'Bold', 'Italic' ] }
]
});
My config.yml
sonata_formatter:
formatters:
markdown:
service: sonata.formatter.text.markdown
extensions:
- sonata.formatter.twig.control_flow
- sonata.formatter.twig.gist
text:
service: sonata.formatter.text.text
extensions:
- sonata.formatter.twig.control_flow
- sonata.formatter.twig.gist
rawhtml:
service: sonata.formatter.text.raw
extensions:
- sonata.formatter.twig.control_flow
- sonata.formatter.twig.gist
richhtml:
service: sonata.formatter.text.raw
extensions:
- sonata.formatter.twig.control_flow
- sonata.formatter.twig.gist
ivory_ck_editor:
configs:
default:
language: '%locale%'
toolbar: standard
My admin class:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('content', 'sonata_formatter_type', array(
'event_dispatcher' => $formMapper->getFormBuilder()->getEventDispatcher(),
'format_field' => 'contentFormatter',
'source_field' => 'rawContent',
'source_field_options' => array(
'horizontal_input_wrapper_class' => $this->getConfigurationPool()->getOption('form_type') == 'horizontal' ? 'col-lg-12': '',
'attr' => array('class' => $this->getConfigurationPool()->getOption('form_type') == 'horizontal' ? 'span10 col-sm-10 col-md-10': '', 'rows' => 20)
),
'ckeditor_context' => 'default',
'target_field' => 'content',
'listener' => true,
))
;
}
I forget to add:
form:
resources:
- 'SonataFormatterBundle:Form:formatter.html.twig'
Related
I use ApiPlatform (PHP 8 / Symfony 6) to create a simple API with a JWT authentification.
Authentification work correctly, I can generate a token. When I use PostMan to test a authenticated operation, no problem, I add manually the Bearer header with my token.
Now I would like to use the documentation auto generated to do this.
I have created a JwtDecorator to add my login route and a security schema. Now I can add my token with the "Authorize" green button. But after, when I execute a authenticated operation, the token is not add in the header of the cUrl query. I don't understand why.
security.yaml
security:
enable_authenticator_manager: true
password_hashers:
App\Entity\User: 'auto'
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/login
stateless: true
json_login:
check_path: /login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/
stateless: true
jwt: ~
access_control:
- { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI
- { path: ^/docs, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI docs
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/, roles: PUBLIC_ACCESS }
JwtDecorator.php
<?php
namespace App\OpenApi;
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\OpenApi;
use ApiPlatform\OpenApi\Model;
final class JwtDecorator implements OpenApiFactoryInterface
{
public function __construct(
private OpenApiFactoryInterface $decorated
) {}
public function __invoke(array $context = []): OpenApi
{
$openApi = ($this->decorated)($context);
$schemas = $openApi->getComponents()->getSchemas();
$schemas['Token'] = new \ArrayObject([
'type' => 'object',
'properties' => [
'token' => [
'type' => 'string',
'readOnly' => true,
],
],
]);
$schemas['Credentials'] = new \ArrayObject([
'type' => 'object',
'properties' => [
'username' => [
'type' => 'string',
'example' => 'test#gmail.com',
],
'password' => [
'type' => 'string',
'example' => '123456',
],
],
]);
$schemas = $openApi->getComponents()->getSecuritySchemes() ?? [];
$schemas['JWT'] = new \ArrayObject([
'type' => 'http',
'scheme' => 'bearer',
'bearerFormat' => 'JWT',
]);
$pathItem = new Model\PathItem(
ref: 'JWT Token',
post: new Model\Operation(
operationId: 'postCredentialsItem',
tags: ['Token'],
responses: [
'200' => [
'description' => 'Get JWT token',
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/Token',
],
],
],
],
],
summary: 'Get JWT token to login.',
requestBody: new Model\RequestBody(
description: 'Generate new JWT Token',
content: new \ArrayObject([
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/Credentials',
],
],
]),
),
security: [],
),
);
$openApi->getPaths()->addPath('/login', $pathItem);
return $openApi;
}
}
NotificationCategory.php
<?php
namespace App\Entity;
use ...
#[ORM\Entity(repositoryClass: NotificationCategoryRepository::class)]
#[ApiResource(
openapiContext: ['security' => [['Bearer Authentication' => []]]],
security: "is_granted('ROLE_USER')"
)]
class NotificationCategory
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['read:Notification:get'])]
private ?string $name = null;
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Notification::class)]
private Collection $notifications;
...
}
I finally found. I was not using the correct schema name in my entity's openapiContext. In JwtDecorator file, I name the schema JWT, and in NotificationCategory entity : Bearer Authentication.
So I replaced :
#[ApiResource(
openapiContext: ['security' => [['Bearer Authentication' => []]]],
security: "is_granted('ROLE_USER')"
)]
By :
#[ApiResource(
openapiContext: ['security' => [['JWT' => []]]],
security: "is_granted('ROLE_USER')"
)]
It work.
I'm trying to have a Category array display on the frontend with items that reference the Category below it.
I've been able to render the category titles on the page, but not the items with references to that category by doing:
export default function Menu({data}) {
const {category} = data;
return (
<section>
{category?.length > 0 &&
category
.map((category) => (
<article key={category._id}>
{category?.title &&
<h1 className="text-3xl">{category.title}</h1>}
))}
</section>
export async function getStaticProps() {
const category = await client
.fetch(
`*[_type == "category"] {
title,
"menuItems": *[_type == "menuItem" && references(^._id)] {
title,
description,
price,
...
}
}
`);
return {
props: {
data: { category }
},
}
}
I've been trying to use .map to find and return the reference data ...
Here are the Sanity schemas I have for Category and menuItem
for Category:
export default {
name: 'category',
title: 'Category',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
},
],
};
for menuItem:
export default {
name: 'menuItem',
title: 'Menu Item',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'category',
title: 'Category',
type: 'reference',
to: [{ type: 'category' }],
},
{
name: 'price',
title: 'Price',
type: 'number',
},
{
name: 'description',
title: 'Description',
type: 'string',
},
],
};
I know I'm doing something wrong, but can't sort it out. Can anyone help?
Thank you so much
I'm trying to integrate algolia search with sanity CMS using the sanity-algolia library (ref https://www.sanity.io/plugins/sanity-algolia)
But when i try to get the plain text from Portable Text rich text content using the pt::text function.
i get expected '}' following object body , and i dont really know where im missing a bracket.
(another note: since im hosting sanity by using sanity start, I am using nextjs (my frontend) to run the function instead using the /api routes in nextJS) So sanity has a webhook to this route.
details about the error:
details: {
description: "expected '}' following object body",
end: 174,
query: '* [(_id in $created || _id in $updated) && _type in $types] {\n' +
' _id,\n' +
' _type,\n' +
' _rev,\n' +
' _type == "post" => {\n' +
' title,\n' +
' "path": slug.current,\n' +
' "body": pt::text(body)\n' +
' }\n' +
'}',
start: 107,
type: 'queryParseError'
}
this is the serverless function im running:
export default async function handler(req, res) {
if (req.headers["content-type"] !== "application/json") {
res.status(400);
res.json({ message: "Bad request" });
return;
}
const algoliaIndex = algolia.initIndex("dev_kim_miles");
const sanityAlgolia = indexer(
{
post: {
index: algoliaIndex,
projection: `{
title,
"path": slug.current,
"body": pt::text(body)
}`,
},
},
(document) => {
console.log(document);
return document;
},
(document) => {
if (document.hasOwnProperty("isHidden")) {
return !document.isHidden;
}
return true;
}
);
return sanityAlgolia
.webhookSync(sanityClient, req.body)
.then(() => res.status(200).send("ok"));
}
and my post schema from sanity:
export default {
name: 'post',
title: 'Post',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
},
{
name: 'author',
title: 'Author',
type: 'reference',
to: {type: 'author'},
},
{
name: 'mainImage',
title: 'Main image',
type: 'image',
options: {
hotspot: true,
},
},
{
name: 'categories',
title: 'Categories',
type: 'array',
of: [{type: 'reference', to: {type: 'category'}}],
},
{
name: 'publishedAt',
title: 'Published at',
type: 'datetime',
},
{
name: 'body',
title: 'Body',
type: 'blockContent',
},
{
name: 'extra',
title: 'extra',
type: 'blockContent',
},
],
preview: {
select: {
title: 'title',
author: 'author.name',
media: 'mainImage',
},
prepare(selection) {
const {author} = selection
return Object.assign({}, selection, {
subtitle: author && `by ${author}`,
})
},
},
}
sanity api v1 doesnt work with functions, I was using
const sanityClient = client({
dataset: "production",
useCdn: true,
projectId: "project_id",
});
which defaults to v1, but in order to use function i added a apiVersion parameter, which made it use later api version:
const sanityClient = client({
dataset: "production",
useCdn: true,
projectId: "9dz8b3g1",
apiVersion: "2021-03-25",
});
I'm trying to override Sonata User Admin class, on my bundle. For the moment, I just override the configureListFields method:
namespace App\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\UserBundle\Admin\Model\UserAdmin as BaseUserAdmin;
class UserAdmin extends BaseUserAdmin
{
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper):void
{
$listMapper
->addIdentifier('username')
->add('email')
->add('groups')
->add('enabled', null, ['editable' => true])
->add('accountType')
->add('createdAt')
;
if ($this->isGranted('ROLE_ALLOWED_TO_SWITCH')) {
$listMapper
->add('impersonating', 'string', ['template' => '#SonataUser/Admin/Field/impersonating.html.twig'])
;
}
}
}
I refresh my page and I got the list of users without problem. But when I click into a user to edit it, I have this error: Call to a member function getClass() on null on these lines:
$now = new \DateTime();
$genderOptions = [
'choices' => \call_user_func([$this->getUserManager()->getClass(), 'getGenderList']),
'required' => true,
'translation_domain' => $this->getTranslationDomain(),
];
// NEXT_MAJOR: Remove this when dropping support for SF 2.8
if (method_exists(FormTypeInterface::class, 'setDefaultOptions')) {
$genderOptions['choices_as_values'] = true;
}
Sonata_admin.yaml file:
sonata_admin:
title: 'Staff Admin Panel'
templates:
dashboard: '#SonataAdmin/Core/dashboard.html.twig'
security:
handler: sonata.admin.security.handler.role
role_admin: ROLE_ADMIN
role_super_admin: ROLE_SUPER_ADMIN
# information:
# GUEST: [VIEW, LIST]
# STAFF: [EDIT, LIST, CREATE]
# EDITOR: [OPERATOR, EXPORT]
# ADMIN: [MASTER]
# admin_permissions: [CREATE, LIST, DELETE, UNDELETE, EXPORT, OPERATOR, MASTER]
# object_permissions: [VIEW, EDIT, DELETE, UNDELETE, OPERATOR, MASTER, OWNER]
dashboard:
blocks:
- { type: sonata.admin.block.admin_list, position: left }
groups:
content:
label: Contenu
icon: '<i class="fa fa-file-text-o"></i>'
items:
- app.admin.specialty
- app.admin.cities
- app.admin.colleges
- app.admin.building
sonata.admin.group.media:
label: Médiathèque
icon: '<i class="fa fa-camera-retro"></i>'
items:
- sonata.media.admin.media
settings:
label: Paramètres
icon: '<i class="fa fa-cog"></i>'
items:
- sonata.classification.admin.category
- sonata.classification.admin.context
- sonata.classification.admin.tag
- sonata.classification.admin.collection
- app.admin.icon
sonata.admin.group.administration:
label: Utilisateur et Groupes
label_catalogue: SonataAdminBundle
icon: '<i class="fa fa-users"></i>'
items:
- app.admin.user
- sonata.user.admin.group
sonata_block:
blocks:
sonata.admin.block.admin_list:
contexts: [admin]
sonata_user:
security_acl: true
manager_type: orm
class:
user: App\Application\Sonata\UserBundle\Entity\User
group: App\Application\Sonata\UserBundle\Entity\Group
fos_user.yaml file:
fos_user:
db_driver: orm # valid values are 'orm', 'mongodb' and 'couchdb'
user_class: App\Entity\User #App\Application\Sonata\UserBundle\Entity\User
firewall_name: main
registration:
form:
type: App\Application\Sonata\UserBundle\Form\RegistrationType
group:
group_class: App\Application\Sonata\UserBundle\Entity\Group
group_manager: sonata.user.orm.group_manager
service:
user_manager: sonata.user.orm.user_manager
mailer: fos_user.mailer.noop
from_email:
address: "%env(MAILER_SENDER_ADDRESS)%"
sender_name: "%env(MAILER_SENDER_NAME)%"
If you want to access to your method getGenderList() from configureFormFields() you can do that :
protected function configureFormFields(FormMapper $formMapper)
{
$now = new \DateTime();
$genderOptions = [
'choices' => $this->getSubject()->getGenderList(), // here
'required' => true,
'translation_domain' => $this->getTranslationDomain(),
];
// NEXT_MAJOR: Remove this when dropping support for SF 2.8
if (method_exists(FormTypeInterface::class, 'setDefaultOptions')) {
$genderOptions['choices_as_values'] = true;
}
}
See the Symfony docs about it.
The following is my code for my TinyMCE text area but I would like to edit the buttons shown into neat sections. I found some nicely formatted code below. Any ideas how I can duplicate the formatting (button grouping and rows) as the below code.
My code
<?php $content = ''; $editor_id = 'txtArea'; wp_editor( $content, $editor_id, $settings = array(
'editor_class'=>'ckeditor',
'media_buttons'=>false,
'quicktags'=>false,
'tinymce' => array(
'theme_advanced_buttons1' => ,
'theme_advanced_buttons2' => ,
'theme_advanced_buttons3' => ,
'theme_advanced_buttons4' => ,
The Formatted code I found
{ name: 'format', items : [ 'Format','Font','FontSize' ] },
{ name: 'colors', items : [ 'TextColor','BGColor' ] },
{ name: 'clipboard', items : [ 'Cut','Copy','Paste','PasteText','PasteFromWord','-','Undo','Redo' ] },
{ name: 'editing', items : [ 'Find','Replace','-','SelectAll'] },
{ name: 'tools', items : [ 'Maximize'] },
'/',
{ name: 'basicstyles', items : [ 'Bold','Italic','Underline','Strike','Subscript','Superscript','-','RemoveFormat' ] },
{ name: 'paragraph', items : [ 'NumberedList','BulletedList','-','Outdent','Indent','-','Blockquote','CreateDiv',
'-','JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock'] },
{ name: 'links', items : [ 'Link','Unlink','Anchor' ] },
{ name: 'source', items : [ 'Source'] }
Thank you in advance for your help
I think I found my own answer because it seems to be working
<?php $content = ''; $editor_id = 'txtArea'; wp_editor( $content, $editor_id, $settings = array(
'editor_class'=>'ckeditor',
'media_buttons'=>false,
'quicktags'=>false,
//'tinymce' => array(
//'theme_advanced_buttons1' =>'' ,
//'theme_advanced_buttons2' =>'' ,
//'theme_advanced_buttons3' =>'' ,
//'theme_advanced_buttons4' =>'')
));?>
<script>
CKEDITOR.replace('txtArea', {toolbar:'Standard'});
CKEDITOR.editorConfig = function( config ) {
config.toolbar_Standard = [
{ name: 'format', items : [ 'Format','Font','FontSize' ] },
{ name: 'colors', items : [ 'TextColor','BGColor' ] },
{ name: 'clipboard', items : [ 'Cut','Copy','Paste','PasteText','PasteFromWord','-','Undo','Redo' ] },
{ name: 'editing', items : [ 'Find','Replace','-','SelectAll'] },
{ name: 'tools', items : [ 'Maximize'] },
'/',
{ name: 'basicstyles', items : [ 'Bold','Italic','Underline','Strike','Subscript','Superscript','-','RemoveFormat' ] },
{ name: 'paragraph', items : [ 'NumberedList','BulletedList','-','Outdent','Indent','-','Blockquote','CreateDiv',
'-','JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock'] },
{ name: 'links', items : [ 'Link','Unlink','Anchor' ] },
{ name: 'source', items : [ 'Source'] }
];
};
</script>
It may not be the prettiest code in the world but as long as it works :)