export params

This commit is contained in:
Yuri Kuznetsov
2022-12-15 11:37:13 +02:00
parent a131308e1a
commit d9d42fd664
14 changed files with 404 additions and 156 deletions

View File

@@ -43,21 +43,15 @@ use stdClass;
class Export
{
private $service;
public function __construct(Service $service)
{
$this->service = $service;
}
public function __construct(private Service $service)
{}
public function postActionProcess(Request $request): stdClass
{
$params = $this->fetchRawParamsFromRequest($request);
$serviceParams = ServiceParams::create()
->withIsIdle(
$request->getParsedBody()->idle ?? false
);
->withIsIdle($request->getParsedBody()->idle ?? false);
$result = $this->service->process($params, $serviceParams);
@@ -141,6 +135,14 @@ class Export
$params['format'] = $data->format;
}
return Params::fromRaw($params);
$obj = Params::fromRaw($params);
if (isset($data->params) && $data->params instanceof stdClass) {
foreach (get_object_vars($data->params) as $key => $value) {
$obj = $obj->withParam($key, $value);
}
}
return $obj;
}
}

View File

@@ -3,7 +3,9 @@
"exportAllFields": "Export all fields",
"fieldList": "Field List",
"format": "Format",
"status": "Status"
"status": "Status",
"xlsxRecordLinks": "Record Links",
"xlsxTitleRow": "Title Row"
},
"options": {
"format": {
@@ -17,6 +19,9 @@
"Failed": "Failed"
}
},
"tooltips": {
"xlsxTitleRow": "Title and date in the first row."
},
"messages": {
"exportProcessed": "Export has been processed. Download the [file]({url}).",
"infoText": "The export is being processed in idle by cron. It can take some time to finish. Closing this modal dialog won't affect the execution process."

View File

@@ -37,6 +37,25 @@
"enum": "Espo\\Tools\\Export\\Format\\Xlsx\\CellValuePreparators\\Enumeration",
"multiEnum": "Espo\\Tools\\Export\\Format\\Xlsx\\CellValuePreparators\\MultiEnum",
"array": "Espo\\Tools\\Export\\Format\\Xlsx\\CellValuePreparators\\MultiEnum"
},
"params": {
"fields": {
"recordLinks": {
"type": "bool",
"default": false
},
"titleRow": {
"type": "bool",
"default": true,
"tooltip": true
}
},
"layout": [
[
{"name": "recordLinks"},
{"name": "titleRow"}
]
]
}
}
}

View File

@@ -176,9 +176,15 @@ class Export
$fileName . '.' . $fileExtension :
"Export_{$entityType}.{$fileExtension}";
return (new ProcessorParams($fileName, $attributeList, $fieldList))
$processorParams = (new ProcessorParams($fileName, $attributeList, $fieldList))
->withName($params->getName())
->withEntityType($params->getEntityType());
foreach ($params->getParamList() as $n) {
$processorParams = $processorParams->withParam($n, $params->getParam($n));
}
return $processorParams;
}
private function getForeignAttributeType(Entity $entity, string $attribute): ?string

View File

@@ -66,6 +66,8 @@ use RuntimeException;
class Processor implements ProcessorInterface
{
private const FORMAT = 'xlsx';
private const PARAM_RECORD_LINKS = 'recordLinks';
private const PARAM_TITLE_ROW = 'titleRow';
/** @var array<string, CellValuePreparator> */
private array $preparatorsCache = [];
@@ -134,20 +136,22 @@ class Processor implements ProcessorInterface
$now = new DateTime();
$now->setTimezone(new DateTimeZone($this->config->get('timeZone', 'UTC')));
$sheet->setCellValue('A1', $this->sanitizeCellValue($exportName));
$sheet->setCellValue('B1',
SharedDate::PHPToExcel(strtotime($now->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)))
);
if ($params->getParam(self::PARAM_TITLE_ROW)) {
$sheet->setCellValue('A1', $this->sanitizeCellValue($exportName));
$sheet->setCellValue('B1',
SharedDate::PHPToExcel(strtotime($now->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)))
);
$sheet->getStyle('A1')->applyFromArray($this->titleStyle);
$sheet->getStyle('B1')->applyFromArray($this->dateStyle);
$sheet->getStyle('B1')
->getNumberFormat()
->setFormatCode($this->dateTime->getDateTimeFormat());
$sheet->getStyle('A1')->applyFromArray($this->titleStyle);
$sheet->getStyle('B1')->applyFromArray($this->dateStyle);
$sheet->getStyle('B1')
->getNumberFormat()
->setFormatCode($this->dateTime->getDateTimeFormat());
}
$azRange = $this->getColumnsRange($fieldList);
$rowNumber = 3;
$rowNumber = $params->getParam(self::PARAM_TITLE_ROW) ? 3 : 1;
$linkColList = [];
$lastIndex = 0;
@@ -176,9 +180,13 @@ class Processor implements ProcessorInterface
$sheet->setCellValue($col . $rowNumber, $this->sanitizeCellValue($label));
$sheet->getColumnDimension($col)->setAutoSize(true);
$linkTypeList = $params->getParam(self::PARAM_RECORD_LINKS) ?
['url', 'phone', 'email', 'link', 'linkParent'] :
['url'];
if (
in_array($type, ['phone', 'email', 'url', 'link', 'linkParent']) ||
$name === 'name'
in_array($type, $linkTypeList) ||
$params->getParam(self::PARAM_RECORD_LINKS) && $name === 'name'
) {
$linkColList[] = $col;
}

View File

@@ -40,19 +40,16 @@ use RuntimeException;
class Params
{
private string $entityType;
/**
* @var ?string[]
*/
/** @var ?string[] */
private $attributeList = null;
/**
* @var ?string[]
*/
/** @var ?string[] */
private $fieldList = null;
private ?string $fileName = null;
private ?string $format = null;
private ?string $name = null;
/** @var array<string, mixed> */
private array $params = [];
private ?SearchParams $searchParams = null;
private bool $applyAccessControl = true;
public function __construct(string $entityType)
@@ -61,7 +58,7 @@ class Params
}
/**
* @param array<string,mixed> $params
* @param array<string, mixed> $params
* @throws RuntimeException
*/
public static function fromRaw(array $params): self
@@ -144,7 +141,6 @@ class Params
public function withFormat(?string $format): self
{
$obj = clone $this;
$obj->format = $format;
return $obj;
@@ -153,7 +149,6 @@ class Params
public function withFileName(?string $fileName): self
{
$obj = clone $this;
$obj->fileName = $fileName;
return $obj;
@@ -162,7 +157,6 @@ class Params
public function withName(?string $name): self
{
$obj = clone $this;
$obj->name = $name;
return $obj;
@@ -171,19 +165,25 @@ class Params
public function withSearchParams(?SearchParams $searchParams): self
{
$obj = clone $this;
$obj->searchParams = $searchParams;
return $obj;
}
public function withParam(string $name, mixed $value): self
{
$obj = clone $this;
$obj->params[$name] = $value;
return $obj;
}
/**
* @param ?string[] $fieldList
*/
public function withFieldList(?array $fieldList): self
{
$obj = clone $this;
$obj->fieldList = $fieldList;
return $obj;
@@ -195,7 +195,6 @@ class Params
public function withAttributeList(?array $attributeList): self
{
$obj = clone $this;
$obj->attributeList = $attributeList;
return $obj;
@@ -204,7 +203,6 @@ class Params
public function withAccessControl(bool $applyAccessControl = true): self
{
$obj = clone $this;
$obj->applyAccessControl = $applyAccessControl;
return $obj;
@@ -274,6 +272,32 @@ class Params
return $this->fieldList;
}
/**
* Get a parameter list.
*
* @return string[]
*/
public function getParamList(): array
{
return array_keys($this->params);
}
/**
* Get a parameter value.
*/
public function getParam(string $name): mixed
{
return $this->params[$name] ?? null;
}
/**
* Has a parameter.
*/
public function hasParam(string $name): bool
{
return array_key_exists($name, $this->params);
}
/**
* Whether all fields should be exported.
*/

View File

@@ -43,6 +43,8 @@ class Params
private ?array $fieldList = null;
private ?string $name = null;
private ?string $entityType = null;
/** @var array<string, mixed> */
private array $params = [];
/**
* @param string[] $attributeList
@@ -93,6 +95,14 @@ class Params
return $obj;
}
public function withParam(string $name, mixed $value): self
{
$obj = clone $this;
$obj->params[$name] = $value;
return $obj;
}
/**
* An export file name.
*/
@@ -140,4 +150,12 @@ class Params
return $this->entityType;
}
/**
* Get a parameter value.
*/
public function getParam(string $name): mixed
{
return $this->params[$name] ?? null;
}
}

View File

@@ -31,18 +31,14 @@ namespace Espo\Tools\Export;
use Espo\Core\Exceptions\ForbiddenSilent;
use Espo\Core\Exceptions\NotFoundSilent;
use Espo\Core\Acl;
use Espo\Core\Acl\Table;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\Core\Job\JobSchedulerFactory;
use Espo\Core\Job\Job\Data as JobData;
use Espo\Tools\Export\Jobs\Process;
use Espo\ORM\EntityManager;
use Espo\Entities\Export as ExportEntity;
use Espo\Entities\User;
@@ -50,37 +46,15 @@ use stdClass;
class Service
{
private Factory $factory;
private Config $config;
private Acl $acl;
private User $user;
private Metadata $metadata;
private EntityManager $entityManager;
private JobSchedulerFactory $jobSchedulerFactory;
public function __construct(
Factory $factory,
Config $config,
Acl $acl,
User $user,
Metadata $metadata,
EntityManager $entityManager,
JobSchedulerFactory $jobSchedulerFactory
) {
$this->factory = $factory;
$this->config = $config;
$this->acl = $acl;
$this->user = $user;
$this->metadata = $metadata;
$this->entityManager = $entityManager;
$this->jobSchedulerFactory = $jobSchedulerFactory;
}
private Factory $factory,
private Config $config,
private Acl $acl,
private User $user,
private Metadata $metadata,
private EntityManager $entityManager,
private JobSchedulerFactory $jobSchedulerFactory
) {}
public function process(Params $params, ServiceParams $serviceParams): ServiceResult
{
@@ -121,8 +95,8 @@ class Service
public function getStatusData(string $id): stdClass
{
/** @var ExportEntity|null $entity */
$entity = $this->entityManager->getEntity(ExportEntity::ENTITY_TYPE, $id);
/** @var ?ExportEntity $entity */
$entity = $this->entityManager->getEntityById(ExportEntity::ENTITY_TYPE, $id);
if (!$entity) {
throw new NotFoundSilent();
@@ -140,8 +114,8 @@ class Service
public function subscribeToNotificationOnSuccess(string $id): void
{
/** @var ExportEntity|null $entity */
$entity = $this->entityManager->getEntity(ExportEntity::ENTITY_TYPE, $id);
/** @var ?ExportEntity $entity */
$entity = $this->entityManager->getEntityById(ExportEntity::ENTITY_TYPE, $id);
if (!$entity) {
throw new NotFoundSilent();

View File

@@ -1,5 +1 @@
<div class="panel panel-default no-side-margin">
<div class="panel-body">
<div class="record">{{{record}}}</div>
</div>
</div>
<div class="record no-side-margin">{{{record}}}</div>

View File

@@ -1,14 +0,0 @@
<div class="cell form-group" data-name="format">
<label class="control-label" data-name="format">{{translate 'format' category='fields' scope='Export'}}</label>
<div class="field" data-name="format">{{{formatField}}}</div>
</div>
<div class="cell form-group" data-name="exportAllFields">
<label class="control-label" data-name="exportAllFields">{{translate 'exportAllFields' category='fields' scope='Export'}}</label>
<div class="field" data-name="exportAllFields">{{{exportAllFieldsField}}}</div>
</div>
<div class="cell form-group" data-name="fieldList">
<label class="control-label" data-name="fieldList">{{translate 'fieldList' category='fields' scope='Export'}}</label>
<div class="field" data-name="fieldList">{{{fieldListField}}}</div>
</div>

View File

@@ -248,7 +248,10 @@ define('model', [], function () {
/**
* Set defs.
*
* @param {Object} defs
* @param {{
* fields?: Object.<string, *>,
* links?: Object.<string, *>,
* }} defs
*/
setDefs: function (defs) {
this.defs = defs || {};

View File

@@ -70,7 +70,7 @@ define('views/export/modals/export', ['views/modal', 'model'], function (Dep, Mo
this.model.set('exportAllFields', true);
}
var formatList =
let formatList =
this.getMetadata().get(['scopes', this.scope, 'exportFormatList']) ||
this.getMetadata().get('app.export.formatList');
@@ -80,52 +80,80 @@ define('views/export/modals/export', ['views/modal', 'model'], function (Dep, Mo
scope: this.scope,
model: this.model,
el: this.getSelector() + ' .record',
formatList: formatList,
});
},
getRecordView: function () {
return this.getView('record');
},
actionExport: function () {
var data = this.getView('record').fetch();
let recordView = this.getRecordView();
let data = recordView.fetch();
this.model.set(data);
if (this.getView('record').validate()) {
if (recordView.validate()) {
return;
}
var returnData = {
let returnData = {
exportAllFields: data.exportAllFields,
format: data.format,
};
if (!data.exportAllFields) {
var attributeList = [];
let attributeList = [];
data.fieldList.forEach(function (item) {
data.fieldList.forEach(item => {
if (item === 'id') {
attributeList.push('id');
return;
}
var type = this.getMetadata().get(['entityDefs', this.scope, 'fields', item, 'type']);
let type = this.getMetadata().get(['entityDefs', this.scope, 'fields', item, 'type']);
if (type) {
this.getFieldManager().getAttributeList(type, item).forEach(function (attribute) {
attributeList.push(attribute);
}, this);
this.getFieldManager().getAttributeList(type, item)
.forEach(attribute => {
attributeList.push(attribute);
});
}
if (~item.indexOf('_')) {
attributeList.push(item);
}
}, this);
});
returnData.attributeList = attributeList;
returnData.fieldList = data.fieldList;
}
returnData.params = {};
recordView.getFormatParamList(data.format).forEach(param => {
let name = recordView.modifyParamName(data.format, param);
let fieldView = recordView.getFieldView(name);
if (!fieldView || fieldView.disabled) {
return;
}
this.getFieldManager()
.getActualAttributeList(fieldView.type, param)
.forEach(subParam => {
let name = recordView.modifyParamName(data.format, subParam);
returnData.params[subParam] = data[name];
});
});
this.trigger('proceed', returnData);
this.close();
},
});
});

View File

@@ -26,51 +26,226 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
define('views/export/record/record', 'views/record/base', function (Dep) {
define('views/export/record/record', ['views/record/edit-for-modal'], function (Dep) {
return Dep.extend({
/**
* @class
* @name Class
* @memberOf module:views/export/record/record
* @extends module:views/record/edit-for-modal.Class
*/
return Dep.extend(/** @lends module:views/export/record/record.Class# */{
template: 'export/record/record',
/**
* @type {string[]},
*/
formatList: null,
/**
* @type {Object.<string, string[]>},
*/
customParams: null,
setup: function () {
Dep.prototype.setup.call(this);
},
setupBeforeFinal: function () {
this.formatList = this.options.formatList;
this.scope = this.options.scope;
var fieldList = this.getFieldManager().getEntityTypeFieldList(this.scope);
let fieldsData = this.getExportFieldsData();
var forbiddenFieldList = this.getAcl().getScopeForbiddenFieldList(this.scope);
this.setupExportFieldDefs(fieldsData);
this.setupExportLayout(fieldsData);
this.setupExportDynamicLogic();
fieldList = fieldList.filter(function (item) {
this.controlFormatField();
this.listenTo(this.model, 'change:format', () => this.controlFormatField());
this.controlAllFields();
this.listenTo(this.model, 'change:exportAllFields', () => this.controlAllFields());
Dep.prototype.setupBeforeFinal.call(this);
},
setupExportFieldDefs: function (fieldsData) {
let fieldDefs = {
format: {
type: 'enum',
options: this.formatList,
},
fieldList: {
type: 'multiEnum',
options: fieldsData.list,
required: true,
},
exportAllFields: {
type: 'bool',
},
};
this.customParams = {};
this.formatList.forEach(format => {
let fields = this.getFormatParamsDefs(format).fields || {};
this.customParams[format] = [];
for (let name in fields) {
let newName = this.modifyParamName(format, name);
this.customParams[format].push(name);
fieldDefs[newName] = Espo.Utils.cloneDeep(fields[name]);
}
});
this.model.setDefs({fields: fieldDefs});
},
setupExportLayout: function (fieldsData) {
this.detailLayout = [];
let mainPanel = {
rows: [
[
{name: 'format'},
false
],
[
{name: 'exportAllFields'},
false
],
[
{
name: 'fieldList',
options: {
translatedOptions: fieldsData.translations,
},
}
],
]
};
this.detailLayout.push(mainPanel);
this.formatList.forEach(format => {
let rows = this.getFormatParamsDefs(format).layout || [];
rows.forEach(row => {
row.forEach(item => {
item.name = this.modifyParamName(format, item.name);
});
})
this.detailLayout.push({
name: format,
rows: rows,
})
});
},
setupExportDynamicLogic: function () {
this.dynamicLogicDefs = {
fields: {},
};
this.formatList.forEach(format => {
let defs = this.getFormatParamsDefs(format).dynamicLogic || {};
this.customParams[format].forEach(param => {
let logic = defs[param] || {};
if (!logic.visible) {
logic.visible = {};
}
if (!logic.visible.conditionGroup) {
logic.visible.conditionGroup = [];
}
logic.visible.conditionGroup.push({
type: 'equals',
attribute: 'format',
value: format,
});
let newName = this.modifyParamName(format, param);
this.dynamicLogicDefs.fields[newName] = logic;
});
});
},
/**
* @param {string} format
* @return {string[]}
*/
getFormatParamList: function (format) {
return Object.keys(this.getFormatParamsDefs(format).fields || {});
},
/**
* @private
* @return {Object.<string, *>}
*/
getFormatParamsDefs: function (format) {
let defs = this.getMetadata().get(['app', 'export', 'formatDefs', format]) || {};
return Espo.Utils.cloneDeep(defs.params || {});
},
/**
* @param {string} format
* @param {string} name
* @return {string}
*/
modifyParamName: function (format, name) {
return format + Espo.Utils.upperCaseFirst(name);
},
/**
* @return {{
* translations: Object.<string, string>,
* list: string[]
* }}
*/
getExportFieldsData: function () {
let fieldList = this.getFieldManager().getEntityTypeFieldList(this.scope);
let forbiddenFieldList = this.getAcl().getScopeForbiddenFieldList(this.scope);
fieldList = fieldList.filter(item => {
return !~forbiddenFieldList.indexOf(item);
}, this);
});
fieldList = fieldList.filter(item => {
let defs = this.getMetadata().get(['entityDefs', this.scope, 'fields', item]) || {};
fieldList = fieldList.filter(function (item) {
var defs = this.getMetadata().get(['entityDefs', this.scope, 'fields', item]) || {};
if (defs.disabled) return;
if (defs.exportDisabled) return;
if (defs.type === 'map') return;
if (
defs.disabled ||
defs.exportDisabled ||
defs.type === 'map'
) {
return false
}
return true;
}, this);
});
this.getLanguage().sortFieldList(this.scope, fieldList);
fieldList.unshift('id');
var translatedOptions = {};
let fieldListTranslations = {};
fieldList.forEach(function (item) {
translatedOptions[item] = this.getLanguage().translate(item, 'fields', this.scope);
}, this);
fieldList.forEach(item => {
fieldListTranslations[item] = this.getLanguage().translate(item, 'fields', this.scope);
});
this.createField('exportAllFields', 'views/fields/bool', {});
let setFieldList = this.model.get('fieldList') || [];
var setFieldList = this.model.get('fieldList') || [];
setFieldList.forEach(function (item) {
setFieldList.forEach(item => {
if (~fieldList.indexOf(item)) {
return;
}
@@ -79,49 +254,52 @@ define('views/export/record/record', 'views/record/base', function (Dep) {
return;
}
var arr = item.split('_');
let arr = item.split('_');
fieldList.push(item);
var foreignScope = this.getMetadata().get(['entityDefs', this.scope, 'links', arr[0], 'entity']);
let foreignScope = this.getMetadata().get(['entityDefs', this.scope, 'links', arr[0], 'entity']);
if (!foreignScope) {
return;
}
translatedOptions[item] = this.getLanguage().translate(arr[0], 'links', this.scope) + '.' +
fieldListTranslations[item] = this.getLanguage().translate(arr[0], 'links', this.scope) + '.' +
this.getLanguage().translate(arr[1], 'fields', foreignScope);
}, this);
this.createField('fieldList', 'views/fields/multi-enum', {
required: true,
translatedOptions: translatedOptions,
options: fieldList,
});
var formatList =
this.getMetadata().get(['scopes', this.scope, 'exportFormatList']) ||
this.getMetadata().get('app.export.formatList');
this.createField('format', 'views/fields/enum', {
options: formatList
});
this.controlAllFields();
this.listenTo(this.model, 'change:exportAllFields', function () {
this.controlAllFields();
}, this);
return {
list: fieldList,
translations: fieldListTranslations,
};
},
controlAllFields: function () {
if (!this.model.get('exportAllFields')) {
this.showField('fieldList');
} else {
this.hideField('fieldList');
return;
}
this.hideField('fieldList');
},
controlFormatField: function () {
let format = this.model.get('format');
this.formatList
.filter(item => item !== format)
.forEach(format => {
this.hidePanel(format);
});
this.formatList
.filter(item => item === format)
.forEach(format => {
this.customParams[format].length ?
this.showPanel(format) :
this.hidePanel(format);
});
},
});
});

View File

@@ -1148,6 +1148,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) {
data.idle = idle;
data.format = dialogData.format;
data.params = dialogData.params;
Espo.Ui.notify(this.translate('pleaseWait', 'messages'));