1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 21:22:58 +01:00

Fixes for stacked fields

This commit is contained in:
David Bomba 2023-11-06 12:54:29 +11:00
parent 8c2f94a1c0
commit d5ba3bcf46
2 changed files with 159 additions and 36 deletions

View File

@ -11,31 +11,32 @@
namespace App\Services\Template;
use App\Models\Quote;
use App\Utils\Number;
use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Design;
use App\Models\Vendor;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Project;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\Vendor;
use App\Transformers\ProjectTransformer;
use App\Transformers\PurchaseOrderTransformer;
use App\Transformers\QuoteTransformer;
use App\Transformers\TaskTransformer;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\Number;
use League\Fractal\Manager;
use App\Models\PurchaseOrder;
use App\Utils\VendorHtmlEngine;
use App\Models\RecurringInvoice;
use App\Utils\PaymentHtmlEngine;
use App\Utils\Traits\MakesDates;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\Traits\Pdf\PdfMaker;
use App\Utils\VendorHtmlEngine;
use League\Fractal\Manager;
use League\Fractal\Serializer\ArraySerializer;
use Twig\Extra\Intl\IntlExtension;
use App\Transformers\TaskTransformer;
use App\Transformers\QuoteTransformer;
use App\Transformers\ProjectTransformer;
use League\CommonMark\CommonMarkConverter;
use App\Transformers\PurchaseOrderTransformer;
use League\Fractal\Serializer\ArraySerializer;
class TemplateService
{
@ -61,6 +62,8 @@ class TemplateService
private Payment $payment;
private CommonMarkConverter $commonmark;
public function __construct(public ?Design $template = null)
{
$this->template = $template;
@ -74,6 +77,12 @@ class TemplateService
*/
private function init(): self
{
$this->commonmark = new CommonMarkConverter([
'allow_unsafe_links' => false,
]);
$this->document = new \DOMDocument();
$this->document->validateOnParse = true;
@ -111,6 +120,7 @@ class TemplateService
{
$this->compose()
->processData($data)
->parseGlobalStacks()
->parseNinjaBlocks()
->processVariables($data)
->parseVariables();
@ -135,6 +145,7 @@ class TemplateService
$this->parseNinjaBlocks()
->parseGlobalStacks()
->parseVariables();
return $this;
@ -563,8 +574,6 @@ class TemplateService
'refund_activity' => $this->getPaymentRefundActivity($payment),
];
nlog($data);
return $data;
}
@ -806,7 +815,7 @@ class TemplateService
*
* @return self
*/
private function parseGlobalStacks(): self
public function parseGlobalStacks(): self
{
$stacks = [
'entity-details',
@ -857,7 +866,7 @@ class TemplateService
})
->map(function ($variable) {
return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_details-' . substr($variable, 1)]];
});
})->toArray();
return $this;
@ -901,24 +910,130 @@ class TemplateService
{
$elements = [];
// $elements = [];
if (!$this->vendor) {
return $elements;
// if (!$this->vendor) {
// return $elements;
// }
// $variables = $this->context['pdf_variables']['vendor_details'];
// foreach ($variables as $variable) {
// $elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]];
// }
// return $elements;
return $this;
}
////////////////////////////////////////
// Dom Traversal
///////////////////////////////////////
public function updateElementProperties(string $element_id, array $elements): self
{
$node = $this->document->getElementById($element_id);
$this->createElementContent($node, $elements);
return $this;
}
public function updateElementProperty($element, string $attribute, ?string $value)
{
if ($attribute == 'hidden' && ($value == false || $value == 'false')) {
return $element;
}
$variables = $this->context['pdf_variables']['vendor_details'];
$element->setAttribute($attribute, $value);
foreach ($variables as $variable) {
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]];
if ($element->getAttribute($attribute) === $value) {
return $element;
}
return $elements;
return $element;
}
public function createElementContent($element, $children) :self
{
foreach ($children as $child) {
$contains_html = false;
if ($child['element'] !== 'script') {
if ($this->company->markdown_enabled && array_key_exists('content', $child)) {
$child['content'] = str_replace('<br>', "\r", $child['content']);
$child['content'] = $this->commonmark->convert($child['content'] ?? '');
}
}
if (isset($child['content'])) {
if (isset($child['is_empty']) && $child['is_empty'] === true) {
continue;
}
$contains_html = preg_match('#(?<=<)\w+(?=[^<]*?>)#', $child['content'], $m) != 0;
}
if ($contains_html) {
// If the element contains the HTML, we gonna display it as is. Backend is going to
// encode it for us, preventing any errors on the processing stage.
// Later, we decode this using Javascript so it looks like it's normal HTML being injected.
// To get all elements that need frontend decoding, we use 'data-state' property.
$_child = $this->document->createElement($child['element'], '');
$_child->setAttribute('data-state', 'encoded-html');
$_child->nodeValue = htmlspecialchars($child['content']);
} else {
// .. in case string doesn't contain any HTML, we'll just return
// raw $content.
$_child = $this->document->createElement($child['element'], isset($child['content']) ? htmlspecialchars($child['content']) : '');
}
$element->appendChild($_child);
if (isset($child['properties'])) {
foreach ($child['properties'] as $property => $value) {
$this->updateElementProperty($_child, $property, $value);
}
}
if (isset($child['elements'])) {
$this->createElementContent($_child, $child['elements']);
}
}
return $this;
}
}

View File

@ -165,6 +165,8 @@ class TemplateTest extends TestCase
</ninja>
';
private string $stack = '<html><div id="company-details"></div></html>';
protected function setUp() :void
{
parent::setUp();
@ -177,6 +179,23 @@ class TemplateTest extends TestCase
}
public function testStackResolution()
{
$partials['design']['includes'] = '';
$partials['design']['header'] = '';
$partials['design']['body'] = $this->stack;
$partials['design']['footer'] = '';
$ts = new TemplateService();
$x = $ts->setTemplate($partials)
->setCompany($this->company)
->parseGlobalStacks()
->getHtml();
nlog($x);
}
public function testDataMaps()
{
$start = microtime(true);
@ -319,19 +338,8 @@ class TemplateTest extends TestCase
];
});
$queries = \DB::getQueryLog();
$count = count($queries);
nlog("query count = {$count}");
$x = $invoices->toArray();
// nlog(json_encode($x));
// nlog(json_encode(htmlspecialchars(json_encode($x), ENT_QUOTES, 'UTF-8')));
// nlog($invoices->toJson());
$this->assertIsArray($invoices->toArray());
nlog("end invoices = " . microtime(true) - $start);
}
private function transformPayment(Payment $payment): array