OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
181
app/Domain/Connector/Controllers/Integration.php
Normal file
181
app/Domain/Connector/Controllers/Integration.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Controllers;
|
||||
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
use Leantime\Domain\Auth\Services\Auth;
|
||||
use Leantime\Domain\Connector\Models\Integration as IntegrationModel;
|
||||
use Leantime\Domain\Connector\Services\Connector;
|
||||
use Leantime\Domain\Connector\Services\Integrations as IntegrationService;
|
||||
use Leantime\Domain\Connector\Services\Providers;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Integration extends Controller
|
||||
{
|
||||
private Providers $providerService;
|
||||
|
||||
private IntegrationService $integrationService;
|
||||
|
||||
private Connector $connectorService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
Providers $providerService,
|
||||
IntegrationService $integrationService,
|
||||
Connector $connectorService
|
||||
): void {
|
||||
Auth::authOrRedirect([Roles::$owner, Roles::$admin], true);
|
||||
|
||||
$this->providerService = $providerService;
|
||||
$this->integrationService = $integrationService;
|
||||
$this->connectorService = $connectorService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the integration wizard step.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
public function get(array $params): Response
|
||||
{
|
||||
return $this->handleIntegration($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles integration wizard form submissions.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
public function post(array $params): Response
|
||||
{
|
||||
return $this->handleIntegration($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes the request to the correct integration wizard step.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
private function handleIntegration(array $params): Response
|
||||
{
|
||||
if (! session()->exists('currentImportEntity')) {
|
||||
session(['currentImportEntity' => '']);
|
||||
}
|
||||
|
||||
if (! isset($params['provider'])) {
|
||||
return new Response;
|
||||
}
|
||||
|
||||
$provider = $this->providerService->getProvider($params['provider']);
|
||||
$this->tpl->assign('provider', $provider);
|
||||
|
||||
$currentIntegration = app()->make(IntegrationModel::class);
|
||||
|
||||
if (isset($params['integrationId'])) {
|
||||
$currentIntegration = $this->integrationService->get($params['integrationId']);
|
||||
if (is_object($currentIntegration)) {
|
||||
$this->tpl->assign('integrationId', $currentIntegration->id);
|
||||
}
|
||||
}
|
||||
|
||||
if (! isset($params['step'])) {
|
||||
return $this->tpl->display('connector.newIntegration');
|
||||
}
|
||||
|
||||
if ($params['step'] == 'connect') {
|
||||
$connection = $provider->connect();
|
||||
|
||||
if ($connection instanceof Response) {
|
||||
return $connection;
|
||||
}
|
||||
}
|
||||
|
||||
if ($params['step'] == 'entity') {
|
||||
$this->tpl->assign('providerEntities', $provider->getEntities());
|
||||
$this->tpl->assign('leantimeEntities', $this->integrationService->getAvailableEntities());
|
||||
|
||||
return $this->tpl->display('connector.integrationEntity');
|
||||
}
|
||||
|
||||
if ($params['step'] == 'fields') {
|
||||
return $this->handleFieldsStep($this->incomingRequest->request->all(), $provider, $currentIntegration);
|
||||
}
|
||||
|
||||
if ($params['step'] == 'sync') {
|
||||
return $this->tpl->display('connector.integrationSync');
|
||||
}
|
||||
|
||||
if ($params['step'] == 'parse') {
|
||||
return $this->handleParseStep($this->incomingRequest->request->all(), $provider);
|
||||
}
|
||||
|
||||
if ($params['step'] == 'import') {
|
||||
return $this->handleImportStep();
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the fields mapping step.
|
||||
*
|
||||
* @param array $params POST request parameters
|
||||
* @param object $provider Provider instance
|
||||
* @param IntegrationModel $currentIntegration Integration being configured
|
||||
*/
|
||||
private function handleFieldsStep(array $params, object $provider, IntegrationModel $currentIntegration): Response
|
||||
{
|
||||
$entity = $this->integrationService->resolveImportEntity($params, $currentIntegration);
|
||||
|
||||
if ($entity === null) {
|
||||
$this->tpl->setNotification('Entity not set', 'error');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/connector/integration?provider='.$provider->id.'');
|
||||
}
|
||||
|
||||
$this->tpl->assign('providerFields', $this->integrationService->resolveProviderFields($currentIntegration, $provider));
|
||||
$this->tpl->assign('flags', $this->connectorService->getEntityFlags($entity));
|
||||
$this->tpl->assign('leantimeFields', $this->integrationService->getEntityFields($entity));
|
||||
|
||||
return $this->tpl->display('connector.integrationFields');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the import review/parse step.
|
||||
*
|
||||
* @param array $params POST request parameters
|
||||
* @param object $provider Provider instance
|
||||
*/
|
||||
private function handleParseStep(array $params, object $provider): Response
|
||||
{
|
||||
$values = $provider->geValues();
|
||||
$fields = $this->connectorService->getFieldMappings($params);
|
||||
$flags = $this->connectorService->parseValues($fields, $values, session('currentImportEntity'));
|
||||
|
||||
$this->tpl->assign('values', $values);
|
||||
$this->tpl->assign('fields', $fields);
|
||||
$this->tpl->assign('flags', $flags);
|
||||
|
||||
return $this->tpl->display('connector.integrationImport');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the final import execution step.
|
||||
*/
|
||||
private function handleImportStep(): Response
|
||||
{
|
||||
$payload = $this->integrationService->getCachedImportPayload();
|
||||
|
||||
$result = $this->connectorService->importValues($payload['fields'], $payload['values'], session('currentImportEntity'));
|
||||
|
||||
if ($result !== true) {
|
||||
$this->tpl->setNotification('There was a problem with the import '.$result, 'error');
|
||||
}
|
||||
|
||||
return $this->tpl->display('connector.integrationConfirm');
|
||||
}
|
||||
}
|
||||
34
app/Domain/Connector/Controllers/Providers.php
Normal file
34
app/Domain/Connector/Controllers/Providers.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Controllers;
|
||||
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
use Leantime\Domain\Auth\Services\Auth;
|
||||
|
||||
class Providers extends Controller
|
||||
{
|
||||
/**
|
||||
* constructor - initialize private variables
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
Auth::authOrRedirect([Roles::$owner, Roles::$admin, Roles::$manager, Roles::$editor]);
|
||||
}
|
||||
|
||||
/**
|
||||
* get - handle get requests
|
||||
*/
|
||||
public function get($params)
|
||||
{
|
||||
return $this->tpl->displayPartial('connectors.providers');
|
||||
}
|
||||
|
||||
/**
|
||||
* post - handle post requests
|
||||
*/
|
||||
public function post($params)
|
||||
{
|
||||
return $this->tpl->displayPartial('connectors.providers');
|
||||
}
|
||||
}
|
||||
42
app/Domain/Connector/Controllers/Show.php
Normal file
42
app/Domain/Connector/Controllers/Show.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Controllers;
|
||||
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
use Leantime\Domain\Auth\Services\Auth;
|
||||
use Leantime\Domain\Connector\Services;
|
||||
|
||||
class Show extends Controller
|
||||
{
|
||||
private Services\Providers $providerService;
|
||||
|
||||
/**
|
||||
* constructor - initialize private variables
|
||||
*/
|
||||
public function init(Services\Providers $projectService)
|
||||
{
|
||||
Auth::authOrRedirect([Roles::$owner, Roles::$admin], true);
|
||||
$this->providerService = $projectService;
|
||||
}
|
||||
|
||||
/**
|
||||
* get - handle get requests
|
||||
*/
|
||||
public function get($params)
|
||||
{
|
||||
$providers = $this->providerService->getProviders();
|
||||
|
||||
$this->tpl->assign('providers', $providers);
|
||||
|
||||
return $this->tpl->display('connector.show');
|
||||
}
|
||||
|
||||
/**
|
||||
* post - handle post requests
|
||||
*/
|
||||
public function post($params)
|
||||
{
|
||||
// Redirect.
|
||||
}
|
||||
}
|
||||
25
app/Domain/Connector/Models/Entity.php
Normal file
25
app/Domain/Connector/Models/Entity.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Models;
|
||||
|
||||
class Entity
|
||||
{
|
||||
public int $id;
|
||||
|
||||
public string $name;
|
||||
|
||||
public string $authData;
|
||||
|
||||
public string $notes;
|
||||
|
||||
// Leantime domain object
|
||||
public mixed $leantimeEntity;
|
||||
|
||||
// Array of field objects
|
||||
public array $fieldMappings = [];
|
||||
|
||||
// External domain object
|
||||
public mixed $providerEntity;
|
||||
|
||||
public function __construct() {}
|
||||
}
|
||||
18
app/Domain/Connector/Models/Field.php
Normal file
18
app/Domain/Connector/Models/Field.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Models;
|
||||
|
||||
class Field
|
||||
{
|
||||
public int $id;
|
||||
|
||||
public int $entityConnectionId;
|
||||
|
||||
public string $leantimeFields;
|
||||
|
||||
public string $providerEntity;
|
||||
|
||||
public string $typeConnector;
|
||||
|
||||
public function __construct() {}
|
||||
}
|
||||
20
app/Domain/Connector/Models/FieldTypes.php
Normal file
20
app/Domain/Connector/Models/FieldTypes.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Models;
|
||||
|
||||
final class FieldTypes
|
||||
{
|
||||
public static string $int = 'int';
|
||||
|
||||
public static string $shortString = 'varchar(255)';
|
||||
|
||||
public static string $array = 'array';
|
||||
|
||||
public static string $text = 'text';
|
||||
|
||||
public static string $email = 'email';
|
||||
|
||||
public static string $dateTime = 'dateTime';
|
||||
|
||||
public function __construct() {}
|
||||
}
|
||||
46
app/Domain/Connector/Models/Integration.php
Normal file
46
app/Domain/Connector/Models/Integration.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Models;
|
||||
|
||||
use Leantime\Core\Db\DbColumn;
|
||||
|
||||
class Integration
|
||||
{
|
||||
#[DbColumn('id')]
|
||||
public int $id;
|
||||
|
||||
#[DbColumn('providerId')]
|
||||
public ?string $providerId;
|
||||
|
||||
#[DbColumn('method')]
|
||||
public ?string $method;
|
||||
|
||||
#[DbColumn('entity')]
|
||||
public ?string $entity;
|
||||
|
||||
#[DbColumn('fields')]
|
||||
public ?string $fields;
|
||||
|
||||
#[DbColumn('schedule')]
|
||||
public ?string $schedule;
|
||||
|
||||
#[DbColumn('notes')]
|
||||
public ?string $notes;
|
||||
|
||||
#[DbColumn('auth')]
|
||||
public ?string $auth;
|
||||
|
||||
#[DbColumn('meta')]
|
||||
public ?string $meta;
|
||||
|
||||
#[DbColumn('createdOn')]
|
||||
public ?string $createdOn;
|
||||
|
||||
#[DbColumn('createdBy')]
|
||||
public ?string $createdBy;
|
||||
|
||||
#[DbColumn('lastSync')]
|
||||
public ?string $lastSync;
|
||||
|
||||
public function __construct() {}
|
||||
}
|
||||
74
app/Domain/Connector/Models/Provider.php
Normal file
74
app/Domain/Connector/Models/Provider.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Models;
|
||||
|
||||
class Provider
|
||||
{
|
||||
// Unique identifier of provider
|
||||
public $id;
|
||||
|
||||
// Friendly name
|
||||
public string $name;
|
||||
|
||||
public string $description;
|
||||
|
||||
// Image to show in UI
|
||||
public string $image;
|
||||
|
||||
// Entities available to sync/import as part of this provider
|
||||
// This should be a list of strings with the exact entity name as they appear in the provider api
|
||||
// project, issue, epic, ticket or similar
|
||||
public array $availableEntities = [];
|
||||
|
||||
public array $availableMethods = []; // import and/or sync
|
||||
|
||||
/**
|
||||
* Define the steps for provider integration. Some steps may not be needed for some providers
|
||||
* (for example CSV does not need a sync)
|
||||
* Only used for status indicator. Controller does not check this.
|
||||
*
|
||||
* @var array|string[]
|
||||
*/
|
||||
public array $steps = [
|
||||
'connect',
|
||||
'entity',
|
||||
'fields',
|
||||
'sync',
|
||||
'parse',
|
||||
'import',
|
||||
];
|
||||
|
||||
public array $stepDetails = [
|
||||
'connect' => [
|
||||
'title' => 'Connect',
|
||||
'position' => 1,
|
||||
],
|
||||
'entity' => [
|
||||
'title' => 'Entity Mapping',
|
||||
'position' => 2,
|
||||
],
|
||||
'fields' => [
|
||||
'title' => 'Field Matching',
|
||||
'position' => 3,
|
||||
],
|
||||
'sync' => [
|
||||
'title' => 'Synchonize',
|
||||
'position' => 4,
|
||||
],
|
||||
'parse' => [
|
||||
'title' => 'Validate',
|
||||
'position' => 5,
|
||||
],
|
||||
'import' => [
|
||||
'title' => 'Import',
|
||||
'position' => 6,
|
||||
],
|
||||
];
|
||||
|
||||
public array $button = [
|
||||
'url' => '',
|
||||
'text' => '',
|
||||
];
|
||||
|
||||
public function __construct() {}
|
||||
}
|
||||
33
app/Domain/Connector/Permissions/ConnectorPermissions.php
Normal file
33
app/Domain/Connector/Permissions/ConnectorPermissions.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Permissions;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\Permission;
|
||||
use Leantime\Core\Auth\Permissions\ProvidesPermissions;
|
||||
|
||||
/**
|
||||
* The Connector (integrations / import) permission vocabulary — the verbs only.
|
||||
*
|
||||
* Connector integrations hold stored third-party credentials and drive data import, so reading,
|
||||
* creating, editing, and deleting them is an installation-wide administrative capability. The
|
||||
* single verb below is COMPANY-WIDE (`projectScoped = false`); call sites gate with
|
||||
* `#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]`, which by the default role
|
||||
* map lands on admin/owner only.
|
||||
*/
|
||||
final class ConnectorPermissions implements ProvidesPermissions
|
||||
{
|
||||
/** Read, create, edit, delete, or import connector integrations (company-wide). */
|
||||
public const MANAGE = 'connector.manage';
|
||||
|
||||
public function domain(): string
|
||||
{
|
||||
return 'connector';
|
||||
}
|
||||
|
||||
public function permissions(): array
|
||||
{
|
||||
return [
|
||||
new Permission(self::MANAGE, 'Manage integrations', false),
|
||||
];
|
||||
}
|
||||
}
|
||||
15
app/Domain/Connector/Repositories/Integrations.php
Normal file
15
app/Domain/Connector/Repositories/Integrations.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Repositories;
|
||||
|
||||
use Leantime\Core\Db\Repository;
|
||||
use Leantime\Domain\Connector\Models\Integration;
|
||||
|
||||
class Integrations extends Repository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->entity = 'integration';
|
||||
$this->model = Integration::class;
|
||||
}
|
||||
}
|
||||
129
app/Domain/Connector/Repositories/LeantimeEntities.php
Normal file
129
app/Domain/Connector/Repositories/LeantimeEntities.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Repository
|
||||
*/
|
||||
|
||||
namespace Leantime\Domain\Connector\Repositories;
|
||||
|
||||
use Leantime\Domain\Connector\Models\FieldTypes;
|
||||
|
||||
class LeantimeEntities
|
||||
{
|
||||
public array $availableLeantimeEntities = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
// TODO: there's gotta be a better way to manage these fields using the tickets model we already have.
|
||||
$this->availableLeantimeEntities = [
|
||||
'tickets' => [
|
||||
'name' => 'To-Dos',
|
||||
'fields' => [
|
||||
'id' => ['name' => 'Id', 'accepts' => fieldTypes::$int, 'default' => 0],
|
||||
'headline' => ['name' => 'Title', 'accepts' => fieldTypes::$shortString, 'default' => ''],
|
||||
'description' => ['name' => 'Description', 'accepts' => fieldTypes::$text, 'default' => ''],
|
||||
'type' => ['name' => 'Type', 'accepts' => fieldTypes::$shortString, 'restrict' => ['bug', 'task', 'story'], 'default' => ''],
|
||||
'editorId' => ['name' => 'Assigned To', 'accepts' => fieldTypes::$email, 'default' => ''],
|
||||
'priority' => ['name' => 'Priority', 'accepts' => fieldTypes::$shortString, 'restrict' => ['high'], 'default' => ''],
|
||||
'date' => ['name' => 'Created On', 'accepts' => fieldTypes::$dateTime, 'default' => ''],
|
||||
'dateToFinish' => ['name' => 'Due Date', 'accepts' => fieldTypes::$dateTime, 'default' => ''],
|
||||
'status' => ['name' => 'Status', 'accepts' => fieldTypes::$shortString, 'restrict' => []],
|
||||
'storypoints' => ['name' => 'Effort', 'accepts' => fieldTypes::$shortString, 'restrict' => ['xxl', 'xl']],
|
||||
'hourRemaining' => ['name' => 'Hours Remaining', 'accepts' => fieldTypes::$int, 'default' => ''],
|
||||
'planHours' => ['name' => 'Plan Hours', 'accepts' => fieldTypes::$int, 'default' => ''],
|
||||
'sprint' => ['name' => 'Sprint', 'accepts' => fieldTypes::$shortString, 'default' => ''],
|
||||
'tags' => ['name' => 'Tags', 'accepts' => fieldTypes::$text, 'default' => ''],
|
||||
'editFrom' => ['name' => 'Edit From', 'accepts' => fieldTypes::$dateTime, 'default' => ''],
|
||||
'editTo' => ['name' => 'Edit To', 'accepts' => fieldTypes::$dateTime, 'default' => ''],
|
||||
'milestoneid' => ['name' => 'Milestone', 'accepts' => fieldTypes::$shortString, 'default' => ''],
|
||||
'projectName' => ['name' => 'Project', 'accepts' => fieldTypes::$shortString, 'default' => ''],
|
||||
],
|
||||
],
|
||||
'projects' => [
|
||||
'name' => 'Projects',
|
||||
'fields' => [
|
||||
'id' => ['name' => 'Id'],
|
||||
'name' => ['name' => 'Project Name'],
|
||||
'details' => ['name' => 'Details'],
|
||||
'clientId' => ['name' => 'ClientId'],
|
||||
'hourBudget' => ['name' => 'Hour Budget'],
|
||||
'assignedUsers' => ['name' => 'Assigned Users'],
|
||||
'dollarBudget' => ['name' => 'Dollar Budget'],
|
||||
'psettings' => ['name' => 'Permission Settings'],
|
||||
'start' => ['name' => 'Start Date'],
|
||||
'end' => ['name' => 'End Date'],
|
||||
],
|
||||
],
|
||||
'users' => [
|
||||
'name' => 'Users',
|
||||
'fields' => [
|
||||
'firstname' => ['name' => 'First Name'],
|
||||
'lastname' => ['name' => 'Last Name'],
|
||||
'phone' => ['name' => 'Phone'],
|
||||
'user' => ['name' => 'Email'],
|
||||
'role' => ['name' => 'Role'],
|
||||
'clientId' => ['name' => 'ClientId'],
|
||||
'password' => ['name' => 'Password'],
|
||||
'jobTitle' => ['name' => 'Job Title'],
|
||||
'jobLevel' => ['name' => 'Job Level'],
|
||||
'department' => ['name' => 'Department'],
|
||||
'sendInvite' => ['name' => 'Send Invite'],
|
||||
],
|
||||
],
|
||||
'ideas' => [
|
||||
'name' => 'Ideas',
|
||||
'fields' => [
|
||||
'itemId' => ['name' => 'Id'],
|
||||
'description' => ['name' => 'Title'],
|
||||
'data' => ['name' => 'Description'],
|
||||
'author' => ['name' => 'Author'],
|
||||
'status' => ['name' => 'Status'],
|
||||
'canvasId' => ['name' => 'CanvasId'],
|
||||
'milestoneId' => ['name' => 'MilestoneId'],
|
||||
],
|
||||
],
|
||||
'goals' => [
|
||||
'name' => 'Goals',
|
||||
'fields' => [
|
||||
'itemId' => ['name' => 'Id'],
|
||||
'title' => ['name' => 'Title'], // required
|
||||
'description' => ['name' => 'Metric'],
|
||||
'status' => ['name' => 'Status'],
|
||||
'relates' => ['name' => 'Relates'],
|
||||
'startValue' => ['name' => 'Start Value'], // required
|
||||
'currentValue' => ['name' => 'Current Value'], // required
|
||||
'canvasId' => ['name' => 'CanvasId'], // required
|
||||
'endValue' => ['name' => 'End Value'], // required
|
||||
'kpi' => ['name' => 'Strategy KPI'],
|
||||
'startDate' => ['name' => 'Start Date'],
|
||||
'endDate' => ['name' => 'End Date'],
|
||||
'setting' => ['name' => 'Setting'],
|
||||
'metricType' => ['name' => 'Metric Type'], // should be number percent or currency
|
||||
'assignedTo' => ['name' => 'Assigned To'],
|
||||
'parent' => ['name' => 'Parent'],
|
||||
],
|
||||
],
|
||||
'milestones' => [
|
||||
'name' => 'Milestones',
|
||||
'fields' => [
|
||||
'id' => ['name' => 'id', 'accepts' => fieldTypes::$int, 'default' => 0],
|
||||
'headline' => ['name' => 'Title', 'accepts' => fieldTypes::$shortString, 'default' => ''],
|
||||
'description' => ['name' => 'Description', 'accepts' => fieldTypes::$text, 'default' => ''],
|
||||
'editorId' => ['name' => 'Assigned To', 'accepts' => fieldTypes::$email, 'default' => ''],
|
||||
'priority' => ['name' => 'Priority', 'accepts' => fieldTypes::$shortString, 'restrict' => ['high'], 'default' => ''],
|
||||
'status' => ['name' => 'Status', 'accepts' => fieldTypes::$shortString, 'restrict' => []],
|
||||
'storypoints' => ['name' => 'Effort', 'accepts' => fieldTypes::$shortString, 'restrict' => ['xxl', 'xl']],
|
||||
'hourRemaining' => ['name' => 'Hours Remaining', 'accepts' => fieldTypes::$int, 'default' => ''],
|
||||
'planHours' => ['name' => 'Plan Hours', 'accepts' => fieldTypes::$int, 'default' => ''],
|
||||
'sprint' => ['name' => 'Sprint', 'accepts' => fieldTypes::$shortString, 'default' => ''],
|
||||
'tags' => ['name' => 'T ags', 'accepts' => fieldTypes::$text, 'default' => ''],
|
||||
'editFrom' => ['name' => 'Edit From', 'accepts' => fieldTypes::$dateTime, 'default' => ''],
|
||||
'editTo' => ['name' => 'Edit To', 'accepts' => fieldTypes::$dateTime, 'default' => ''],
|
||||
'projectName' => ['name' => 'Project', 'accepts' => fieldTypes::$shortString, 'default' => ''],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
}
|
||||
}
|
||||
820
app/Domain/Connector/Services/Connector.php
Normal file
820
app/Domain/Connector/Services/Connector.php
Normal file
@@ -0,0 +1,820 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
use Leantime\Domain\Goalcanvas\Repositories\Goalcanvas;
|
||||
use Leantime\Domain\Ideas\Repositories\Ideas;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
use Leantime\Domain\Tickets\Services\Tickets;
|
||||
use Leantime\Domain\Users\Services\Users;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class Connector
|
||||
{
|
||||
public function __construct(
|
||||
private Users $userService,
|
||||
private Projects $projectService,
|
||||
private Tickets $ticketService,
|
||||
private \Leantime\Domain\Tickets\Repositories\Tickets $ticketRepository,
|
||||
private Goalcanvas $goalCanvasRepo,
|
||||
private Ideas $ideaRepo,
|
||||
) {}
|
||||
|
||||
public function getEntityFlags($entity)
|
||||
{
|
||||
$flags = [];
|
||||
if ($entity == 'tickets') {
|
||||
$flags[] = 'If you do not have an Editor (User) email/ID field then all imported entities will be assigned to you.';
|
||||
$flags[] = 'Headline and Project are required fields.';
|
||||
} else {
|
||||
if ($entity == 'projects') {
|
||||
$flags[] = 'If there are no Assigned Users, the project will be assigned to you.';
|
||||
$flags[] = 'If there are more than one assigned users, ensure that it is a comma separated list.';
|
||||
$flags[] = 'If you decide to set permissions settings they have to be one of the following types: all, clients or restricted.';
|
||||
$flags[] = 'Project Name and Client Id re required fields';
|
||||
} else {
|
||||
if ($entity == 'users') {
|
||||
// TODO add flags for users
|
||||
$flags[] = 'First Name, Role, Email, and Send Invite are required fields';
|
||||
$flags[] = 'The Send Invite field should be either Yes/No or else the user will not be imported. If Send Invite is set to No the user will have to reset the password on their own.';
|
||||
$flags[] = "Roles have to be one of the following values 'readonly', 'commenter', 'editor', 'manager', 'admin', 'owner'.";
|
||||
} else {
|
||||
if ($entity == 'ideas') {
|
||||
// TODO add flags for ideas
|
||||
$flags[] = 'Description, Data, and CanvasId are required fields.';
|
||||
$flags[] = 'If you do not have an Author field then you will be assigned as the Author.';
|
||||
} else {
|
||||
if ($entity == 'goals') {
|
||||
// TODO add flags for goals
|
||||
$flags[] = 'Title, CanvasId, Start Value, Current Value, and End Value are required fields';
|
||||
} else {
|
||||
if ($entity == 'milestones') {
|
||||
// TODO add flags for milestones
|
||||
$flags[] = 'If you do not have an Editor (User) email/ID field then all imported entities will be assigned to you.';
|
||||
$flags[] = 'Headline, Edit From, Edit To and Project are required fields';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
public function getFieldMappings($postParams)
|
||||
{
|
||||
$fields = [];
|
||||
foreach ($postParams as $fieldmapping) {
|
||||
// Checking if the field mapping is selected
|
||||
if (
|
||||
! empty($fieldmapping)
|
||||
&& strpos($fieldmapping, '|') !== false
|
||||
) {
|
||||
$mappingParts = explode('|', $fieldmapping);
|
||||
$sourceField = $mappingParts[0];
|
||||
$leantimeField = $mappingParts[1];
|
||||
|
||||
$fields[] = ['sourceField' => $sourceField, 'leantimeField' => $leantimeField];
|
||||
}
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function parseValues($fields, $values, $entity)
|
||||
{
|
||||
if ($entity == 'tickets') {
|
||||
return $this->parseTickets($fields, $values);
|
||||
} elseif ($entity == 'projects') {
|
||||
return $this->parseProjects($fields, $values);
|
||||
} elseif ($entity == 'users') {
|
||||
return $this->parseUsers($fields, $values);
|
||||
} elseif ($entity == 'ideas') {
|
||||
return $this->parseIdeas($fields, $values);
|
||||
} elseif ($entity == 'goals') {
|
||||
return $this->parseGoals($fields, $values);
|
||||
} elseif ($entity == 'milestones') {
|
||||
return $this->parseMilestones($fields, $values);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function parseTickets($fields, $values)
|
||||
{
|
||||
$matchingSourceField = '';
|
||||
$flags = [];
|
||||
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'editorId') {
|
||||
$matchingSourceField = $item['sourceField'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$headlineFlag = true;
|
||||
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'headline') {
|
||||
$headlineFlag = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($headlineFlag) {
|
||||
$flags[] = 'You must have a headline column';
|
||||
}
|
||||
|
||||
if ($matchingSourceField) {
|
||||
foreach ($values as &$row) {
|
||||
if (strpos($row[$matchingSourceField], '@') !== false) {
|
||||
$id = false;
|
||||
if (isset($row[$matchingSourceField])) {
|
||||
$id = $this->userService->getUserByEmail(trim($row[$matchingSourceField]))['id'] ?? false;
|
||||
|
||||
}
|
||||
if ($id) {
|
||||
$row['editorId'] = $id;
|
||||
} else {
|
||||
$flags[] = $row[$matchingSourceField].' '.'is not a valid User';
|
||||
}
|
||||
} else {
|
||||
$flags[] = 'The Author/userId column must contain only valid User emails';
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$id = session('userdata.id');
|
||||
foreach ($values as &$row) {
|
||||
$row['editorId'] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
// PROJECT Name Check
|
||||
$matchingProjectSourceField = '';
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'projectName') {
|
||||
$matchingProjectSourceField = $item['sourceField'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchingProjectSourceField) {
|
||||
$allProjects = $this->projectService->getAllProjects();
|
||||
|
||||
foreach ($values as &$row) {
|
||||
$projectId = $this->projectService->getProjectIdByName(
|
||||
$allProjects,
|
||||
$row[$matchingProjectSourceField]
|
||||
);
|
||||
if (! $projectId) {
|
||||
$flags[] = $row[$matchingProjectSourceField].' '.'is not a valid Project';
|
||||
} else {
|
||||
$row['projectId'] = $projectId;
|
||||
$statusLabels = $this->ticketService->getStatusLabels($projectId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$flags[] = 'You must have a column matching to a valid Project.';
|
||||
}
|
||||
|
||||
// Status Field Mapping
|
||||
$matchingStatusField = '';
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'status') {
|
||||
$matchingStatusField = $item['sourceField'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchingStatusField) {
|
||||
foreach ($values as &$row) {
|
||||
$getStatus = $this->ticketRepository->getStatusIdByName($row[$matchingStatusField], $row['projectId'] ?? null);
|
||||
if ($getStatus !== false) {
|
||||
$row['status'] = $getStatus;
|
||||
} else {
|
||||
$row['status'] = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status Field Mapping
|
||||
$matchingDateField = '';
|
||||
$dateArray = ['date', 'dateToFinish', 'editFrom', 'editTo'];
|
||||
foreach ($fields as $item) {
|
||||
if (in_array($item['leantimeField'], $dateArray)) {
|
||||
$matchingDateField = $item['sourceField'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchingDateField) {
|
||||
foreach ($values as &$row) {
|
||||
|
||||
if ($row[$matchingDateField] !== '') {
|
||||
try {
|
||||
dtHelper()->parseUserDateTime($row[$matchingDateField]);
|
||||
} catch (\Exception $e) {
|
||||
$flags[] = $matchingDateField.': '.$row[$matchingDateField].' '.'is not a valid date. Please use the date format defined in your profile or iso8601';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Storypoints/Effort Field Validation
|
||||
// Only a fixed set of values is renderable (see Tickets repository efforts).
|
||||
// An out-of-range value (e.g. "4") imports fine but later crashes ticket/dashboard views.
|
||||
$matchingStorypointsField = '';
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'storypoints') {
|
||||
$matchingStorypointsField = $item['sourceField'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchingStorypointsField) {
|
||||
// Cast keys to strings: PHP coerces numeric-string array keys (e.g. '1')
|
||||
// to ints, which would break the strict comparison below against the
|
||||
// string CSV value.
|
||||
$validStorypoints = array_map('strval', array_keys($this->ticketService->getEffortLabels()));
|
||||
foreach ($values as &$row) {
|
||||
$storypoints = trim((string) ($row[$matchingStorypointsField] ?? ''));
|
||||
if ($storypoints !== '' && ! in_array($storypoints, $validStorypoints, true)) {
|
||||
$flags[] = $matchingStorypointsField.': '.$storypoints.' '.'is not a valid story point value. Use one of: '.implode(', ', $validStorypoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->cacheSerializedFieldValues($fields, $values);
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
private function parseProjects($fields, $values)
|
||||
{
|
||||
$matchingProjectNameSourceField = '';
|
||||
$matchingClientIdSourceField = '';
|
||||
$matchingUsersSourceField = '';
|
||||
$flags = [];
|
||||
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'name') {
|
||||
$matchingProjectNameSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'clientId') {
|
||||
$matchingClientIdSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'assignedUsers') {
|
||||
$matchingUsersSourceField = $item['sourceField'];
|
||||
}
|
||||
}
|
||||
if ($matchingUsersSourceField) {
|
||||
foreach ($values as $row) {
|
||||
$emails = explode(',', $row[$matchingUsersSourceField]);
|
||||
$users = [];
|
||||
|
||||
foreach ($emails as $email) {
|
||||
$user = $this->userService->getUserByEmail(trim($email));
|
||||
if ($user) {
|
||||
$users[] = $user;
|
||||
} else {
|
||||
$flags[] = $email.' is not a valid user.';
|
||||
}
|
||||
}
|
||||
$row[$matchingUsersSourceField] = $users;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $matchingProjectNameSourceField) {
|
||||
$flags[] = 'You must have a column with Project Names';
|
||||
}
|
||||
if (! $matchingClientIdSourceField) {
|
||||
$flags[] = 'You must have a column with ClientId';
|
||||
}
|
||||
|
||||
$this->cacheSerializedFieldValues($fields, $values);
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
private function parseUsers($fields, $values)
|
||||
{
|
||||
$matchingUsernameSourceField = '';
|
||||
$matchingRoleSourceField = '';
|
||||
$matchingSendInviteSourceField = '';
|
||||
$matchingFirstNameSourceField = '';
|
||||
$flags = [];
|
||||
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'firstname') {
|
||||
$matchingFirstNameSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'user') {
|
||||
$matchingUsernameSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'role') {
|
||||
$matchingRoleSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'sendInvite') {
|
||||
$matchingSendInviteSourceField = $item['sourceField'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchingSendInviteSourceField && $matchingUsernameSourceField && $matchingRoleSourceField && $matchingFirstNameSourceField) {
|
||||
foreach ($values as &$row) {
|
||||
if (strtolower($row[$matchingSendInviteSourceField]) == 'yes') {
|
||||
$row['sendInvite'] = true;
|
||||
} elseif (strtolower($row[$matchingSendInviteSourceField]) == 'no') {
|
||||
$row['sendInvite'] = false;
|
||||
} else {
|
||||
$flags[] = 'The sendInvite column must contain only yes or no';
|
||||
}
|
||||
if (str_contains($row[$matchingUsernameSourceField], '@')) {
|
||||
$user = $this->userService->getUserByEmail(
|
||||
email: trim($row[$matchingUsernameSourceField]),
|
||||
status: ''
|
||||
);
|
||||
if ($user) {
|
||||
$row['id'] = $user['id'];
|
||||
}
|
||||
} else {
|
||||
$flags[] = 'The Username column must contain only valid emails';
|
||||
}
|
||||
$rolesKey = array_search(
|
||||
strtolower(trim($row[$matchingRoleSourceField])),
|
||||
Roles::getRoles()
|
||||
);
|
||||
if (! $rolesKey) {
|
||||
$flags[] = $row[$matchingRoleSourceField].' is not a valid role.';
|
||||
} else {
|
||||
$row[$matchingRoleSourceField] = $rolesKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! $matchingSendInviteSourceField) {
|
||||
$flags[] = 'You must have a column specifying if the user should receive an invite.';
|
||||
}
|
||||
if (! $matchingUsernameSourceField) {
|
||||
$flags[] = 'You must have a column with Emails.';
|
||||
}
|
||||
if (! $matchingRoleSourceField) {
|
||||
$flags[] = 'You must have a column with Roles.';
|
||||
}
|
||||
if (! $matchingFirstNameSourceField) {
|
||||
$flags[] = 'You must have a column with First Names.';
|
||||
}
|
||||
|
||||
$this->cacheSerializedFieldValues($fields, $values);
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
private function parseIdeas($fields, $values)
|
||||
{
|
||||
|
||||
$matchingAuthorSourceField = '';
|
||||
$matchingCanvasIdSourceField = '';
|
||||
$matchingDataSourceField = '';
|
||||
$matchingDescriptionSourceField = '';
|
||||
$flags = [];
|
||||
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'author') {
|
||||
$matchingAuthorSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'canvasId') {
|
||||
$matchingCanvasIdSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'description') {
|
||||
$matchingDescriptionSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'data') {
|
||||
$matchingDataSourceField = $item['sourceField'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchingAuthorSourceField) {
|
||||
foreach ($values as &$row) {
|
||||
if (str_contains($row[$matchingAuthorSourceField], '@')) {
|
||||
$id = $this->userService->getUserByEmail(
|
||||
$row[$matchingAuthorSourceField]
|
||||
)['id'];
|
||||
if ($id) {
|
||||
$row['author'] = $id;
|
||||
} else {
|
||||
$flags[] = $row[$matchingAuthorSourceField].' '.'is not a valid User';
|
||||
}
|
||||
} else {
|
||||
$flags[] = 'The Author column must contain only valid User emails';
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$id = session('userdata.id');
|
||||
foreach ($values as &$row) {
|
||||
$row['author'] = $id;
|
||||
}
|
||||
}
|
||||
if ($matchingCanvasIdSourceField) {
|
||||
foreach ($values as &$row) {
|
||||
if (
|
||||
! $this->ideaRepo->getSingleCanvas(
|
||||
$row[$matchingCanvasIdSourceField]
|
||||
)
|
||||
) {
|
||||
$flags[] = $row[$matchingCanvasIdSourceField].' '.'is not a valid Canvas.';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! $matchingDataSourceField) {
|
||||
$flags[] = 'You must have a column matching to Data.';
|
||||
}
|
||||
if (! $matchingDescriptionSourceField) {
|
||||
$flags[] = 'You must have a column matching to Description.';
|
||||
}
|
||||
if (! $matchingCanvasIdSourceField) {
|
||||
$flags[] = 'You must have a column matching to CanvasId.';
|
||||
}
|
||||
|
||||
$this->cacheSerializedFieldValues($fields, $values);
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
private function parseGoals($fields, $values)
|
||||
{
|
||||
// TODO add import logic and validation
|
||||
$matchingStartValueSourceField = '';
|
||||
$matchingCurrentValueSourceField = '';
|
||||
$matchingCanvasIdSourceField = '';
|
||||
$matchingEndValueSourceField = '';
|
||||
$matchingTitleSourceField = '';
|
||||
$flags = [];
|
||||
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'title') {
|
||||
$matchingTitleSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'canvasId') {
|
||||
$matchingCanvasIdSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'currentValue') {
|
||||
$matchingCurrentValueSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'startValue') {
|
||||
$matchingStartValueSourceField = $item['sourceField'];
|
||||
}
|
||||
if ($item['leantimeField'] === 'endValue') {
|
||||
$matchingEndValueSourceField = $item['sourceField'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchingCanvasIdSourceField) {
|
||||
foreach ($values as &$row) {
|
||||
if (
|
||||
! $this->goalCanvasRepo->getSingleCanvas(
|
||||
$row[$matchingCanvasIdSourceField]
|
||||
)
|
||||
) {
|
||||
$flags[] = $row[$matchingCanvasIdSourceField].' '.'is not a valid Canvas.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $matchingTitleSourceField) {
|
||||
$flags[] = 'You must have a column matching to Title.';
|
||||
}
|
||||
if (! $matchingCanvasIdSourceField) {
|
||||
$flags[] = 'You must have a column matching to CanvasId.';
|
||||
}
|
||||
if (! $matchingCurrentValueSourceField) {
|
||||
$flags[] = 'You must have a column matching to CurrentValue.';
|
||||
}
|
||||
if (! $matchingStartValueSourceField) {
|
||||
$flags[] = 'You must have a column matching to StartValue.';
|
||||
}
|
||||
if (! $matchingEndValueSourceField) {
|
||||
$flags[] = 'You must have a column matching to EndValue.';
|
||||
}
|
||||
|
||||
$this->cacheSerializedFieldValues($fields, $values);
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
private function parseMilestones($fields, $values)
|
||||
{
|
||||
$matchingSourceField = '';
|
||||
$flags = [];
|
||||
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'editorId') {
|
||||
$matchingSourceField = $item['sourceField'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$headlineFlag = true;
|
||||
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'headline') {
|
||||
$headlineFlag = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($headlineFlag) {
|
||||
$flags[] = 'You must have a headline column';
|
||||
}
|
||||
|
||||
if ($matchingSourceField) {
|
||||
foreach ($values as &$row) {
|
||||
if (str_contains($row[$matchingSourceField], '@')) {
|
||||
$id = $this->userService->getUserByEmail(
|
||||
$row[$matchingSourceField]
|
||||
)['id'];
|
||||
if ($id) {
|
||||
$row['editorId'] = $id;
|
||||
} else {
|
||||
$flags[] = $row[$matchingSourceField].' '.'is not a valid User';
|
||||
}
|
||||
} else {
|
||||
$flags[] = 'The Author/userId column must contain only valid User emails';
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$id = session('userdata.id');
|
||||
foreach ($values as &$row) {
|
||||
$row['editorId'] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
// PROJECT Name Check
|
||||
$matchingProjectSourceField = '';
|
||||
|
||||
foreach ($fields as $item) {
|
||||
if ($item['leantimeField'] === 'projectName') {
|
||||
$matchingProjectSourceField = $item['sourceField'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchingProjectSourceField) {
|
||||
$allProjects = $this->projectService->getAllProjects();
|
||||
|
||||
foreach ($values as &$row) {
|
||||
$projectId = $this->projectService->getProjectIdByName(
|
||||
$allProjects,
|
||||
$row[$matchingProjectSourceField]
|
||||
);
|
||||
if (! $projectId) {
|
||||
$flags[] = $row[$matchingProjectSourceField].' '.'is not a valid Project';
|
||||
} //
|
||||
else {
|
||||
$row['projectId'] = $projectId;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$flags[] = 'You must have a column matching to a valid Project.';
|
||||
}
|
||||
|
||||
$matchingEndDateSourceField = '';
|
||||
$matchingStartDateSourceField = '';
|
||||
foreach ($fields as $field) {
|
||||
if ($field['leantimeField'] === 'editFrom') {
|
||||
$matchingStartDateSourceField = $field['sourceField'];
|
||||
}
|
||||
if ($field['leantimeField'] === 'editTo') {
|
||||
$matchingEndDateSourceField = $field['sourceField'];
|
||||
}
|
||||
}
|
||||
if (! $matchingEndDateSourceField) {
|
||||
$flags[] = 'You must have a column matching to a valid Edit To.';
|
||||
}
|
||||
if (! $matchingStartDateSourceField) {
|
||||
$flags[] = 'You must have a column matching to a valid Edit From.';
|
||||
}
|
||||
|
||||
$this->cacheSerializedFieldValues($fields, $values);
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
public function importValues($fields, $values, $entity)
|
||||
{
|
||||
|
||||
$finalMappings = [];
|
||||
foreach ($fields as $field) {
|
||||
array_push($finalMappings, $field['sourceField']);
|
||||
array_push($finalMappings, $field['leantimeField']);
|
||||
}
|
||||
|
||||
if ($entity == 'tickets') {
|
||||
return $this->importTickets($finalMappings, $values);
|
||||
} elseif ($entity == 'projects') {
|
||||
return $this->importProjects($finalMappings, $values);
|
||||
} elseif ($entity == 'users') {
|
||||
return $this->importUsers($finalMappings, $values);
|
||||
} elseif ($entity == 'ideas') {
|
||||
return $this->importIdeas($finalMappings, $values);
|
||||
} elseif ($entity == 'goals') {
|
||||
return $this->importGoals($finalMappings, $values);
|
||||
} elseif ($entity == 'milestones') {
|
||||
return $this->importMilestones($finalMappings, $values);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function importTickets($finalMappings, $finalValues)
|
||||
{
|
||||
foreach ($finalValues as $row) {
|
||||
$ticket = [];
|
||||
|
||||
for ($i = 0; $i < count($finalMappings); $i = $i + 2) {
|
||||
$ticket[$finalMappings[$i + 1]] = $row[$finalMappings[$i]];
|
||||
}
|
||||
$ticket['editorId'] = $row['editorId'] ?? '';
|
||||
$ticket['projectId'] = $row['projectId'] ?? '';
|
||||
$ticket['status'] = $row['status'] ?? 3;
|
||||
$ticket['type'] = $row['type'] ?? 'task';
|
||||
|
||||
try {
|
||||
$ticket['date'] = dtHelper()->parseUserDateTime(trim($ticket['date']))->formatDateForUser();
|
||||
} catch (\Exception $e) {
|
||||
$ticket['date'] = dtHelper()->userNow()->formatDateForUser();
|
||||
}
|
||||
|
||||
try {
|
||||
$ticket['dateToFinish'] = dtHelper()->parseUserDateTime(trim($ticket['dateToFinish']))->formatDateForUser();
|
||||
} catch (\Exception $e) {
|
||||
$ticket['dateToFinish'] = '';
|
||||
}
|
||||
|
||||
try {
|
||||
$ticket['editFrom'] = dtHelper()->parseUserDateTime(trim($ticket['dateToFinish']))->formatDateForUser();
|
||||
} catch (\Exception $e) {
|
||||
$ticket['editFrom'] = '';
|
||||
}
|
||||
|
||||
try {
|
||||
$ticket['editTo'] = dtHelper()->parseUserDateTime(trim($ticket['dateToFinish']))->formatDateForUser();
|
||||
} catch (\Exception $e) {
|
||||
$ticket['editTo'] = '';
|
||||
}
|
||||
|
||||
try {
|
||||
if (isset($ticket['id']) && is_numeric($ticket['id'])) {
|
||||
$this->ticketService->updateTicket($ticket);
|
||||
} else {
|
||||
$this->ticketService->addTicket($ticket);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
|
||||
return $e->getMessage();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
private function importProjects($finalMappings, $finalValues)
|
||||
{
|
||||
$psettings = ['all', 'clients', 'restricted'];
|
||||
foreach ($finalValues as $row) {
|
||||
$values = [];
|
||||
for ($i = 0; $i < count($finalMappings); $i = $i + 2) {
|
||||
if ($finalMappings[$i + 1] == 'psettings') {
|
||||
if (! in_array($row[$finalMappings[$i]], $psettings)) {
|
||||
$row[$finalMappings[$i]] = 'restricted';
|
||||
}
|
||||
}
|
||||
$values[$finalMappings[$i + 1]] = $row[$finalMappings[$i]];
|
||||
}
|
||||
|
||||
try {
|
||||
if (isset($values['id']) && is_numeric($values['id'])) {
|
||||
$this->projectService->editProject($values, $values['id']);
|
||||
} else {
|
||||
$this->projectService->addProject($values);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function importUsers($finalMappings, $finalValues)
|
||||
{
|
||||
// TODO add users
|
||||
foreach ($finalValues as $row) {
|
||||
$values = [];
|
||||
for ($i = 0; $i < count($finalMappings); $i = $i + 2) {
|
||||
if ($finalMappings[$i + 1] != 'sendInvite' || $finalMappings[$i + 1] != 'id') {
|
||||
$values[$finalMappings[$i + 1]] = $row[$finalMappings[$i]];
|
||||
}
|
||||
}
|
||||
$values['notifications'] = 1;
|
||||
$values['source'] = 'csvImport'; // TODO: will have to change when other integrations are added
|
||||
if (isset($row['id']) && $row['id'] > 0) {
|
||||
$currentUser = $this->userService->getUser($row['id']);
|
||||
$currentUser['user'] = $values['user'];
|
||||
foreach ($currentUser as $key => &$userValues) {
|
||||
if (isset($values[$key])) {
|
||||
$userValues = $values[$key];
|
||||
}
|
||||
}
|
||||
$this->userService->editUser($currentUser, $row['id']);
|
||||
} elseif (isset($row['sendInvite']) && $row['sendInvite'] == true) {
|
||||
$this->userService->createUserInvite($values);
|
||||
} else {
|
||||
$values['status'] = 'a';
|
||||
if (! isset($values['password'])) {
|
||||
$tempPasswordVar = Uuid::uuid4()->toString();
|
||||
$values['password'] = $tempPasswordVar;
|
||||
}
|
||||
$this->userService->addUser($values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function importIdeas($finalMappings, $finalValues)
|
||||
{
|
||||
|
||||
foreach ($finalValues as $row) {
|
||||
$values = [];
|
||||
for ($i = 0; $i < count($finalMappings); $i = $i + 2) {
|
||||
$values[$finalMappings[$i + 1]] = $row[$finalMappings[$i]];
|
||||
}
|
||||
if (isset($values['itemId'])) {
|
||||
$this->ideaRepo->editCanvasItem($values);
|
||||
} else {
|
||||
$this->ideaRepo->addCanvasItem($values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function importGoals($finalMappings, $finalValues)
|
||||
{
|
||||
foreach ($finalValues as $row) {
|
||||
$values = [];
|
||||
for ($i = 0; $i < count($finalMappings); $i = $i + 2) {
|
||||
$values[$finalMappings[$i + 1]] = $row[$finalMappings[$i]];
|
||||
}
|
||||
$values['box'] = 'goal';
|
||||
if (! isset($values['author'])) {
|
||||
$values['author'] = session('userdata.id');
|
||||
}
|
||||
if (isset($values['itemId'])) {
|
||||
$this->goalCanvasRepo->editCanvasItem($values);
|
||||
} else {
|
||||
$this->goalCanvasRepo->addCanvasItem($values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function importMilestones($finalMappings, $finalValues)
|
||||
{
|
||||
// TODO add milestones
|
||||
foreach ($finalValues as $row) {
|
||||
$ticket = [];
|
||||
|
||||
for ($i = 0; $i < count($finalMappings); $i = $i + 2) {
|
||||
$ticket[$finalMappings[$i + 1]] = $row[$finalMappings[$i]];
|
||||
}
|
||||
$ticket['editorId'] = $row['editorId'];
|
||||
$ticket['projectId'] = $row['projectId'];
|
||||
if (! isset($ticket['status'])) {
|
||||
$ticket['status'] = 3;
|
||||
}
|
||||
$ticket['type'] = 'milestone';
|
||||
if (isset($ticket['id'])) {
|
||||
$this->ticketService->updateTicket($ticket);
|
||||
} else {
|
||||
$this->ticketService->addTicket($ticket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function cacheSerializedFieldValues($fields, $values)
|
||||
{
|
||||
|
||||
$serializedFields = serialize($fields);
|
||||
$serializedValues = serialize($values);
|
||||
|
||||
session(['serFields' => $serializedFields]);
|
||||
session(['serValues' => $serializedValues]);
|
||||
}
|
||||
}
|
||||
207
app/Domain/Connector/Services/Integrations.php
Normal file
207
app/Domain/Connector/Services/Integrations.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Services;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Domain\Connector\Models\Integration as IntegrationModel;
|
||||
use Leantime\Domain\Connector\Permissions\ConnectorPermissions;
|
||||
use Leantime\Domain\Connector\Repositories\Integrations as IntegrationsRepo;
|
||||
use Leantime\Domain\Connector\Repositories\LeantimeEntities;
|
||||
|
||||
class Integrations
|
||||
{
|
||||
private IntegrationsRepo $integrationRepo;
|
||||
|
||||
private LeantimeEntities $leantimeEntities;
|
||||
|
||||
/**
|
||||
* Initializes the service dependencies.
|
||||
*/
|
||||
public function __construct(IntegrationsRepo $integrationRepo, LeantimeEntities $leantimeEntities)
|
||||
{
|
||||
$this->integrationRepo = $integrationRepo;
|
||||
$this->leantimeEntities = $leantimeEntities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single integration by id.
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param int $id Integration id
|
||||
* @return object|array|false The integration record or false when not found
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function get(int $id): object|array|false
|
||||
{
|
||||
return $this->integrationRepo->get($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a ticket from an integration. Not yet implemented.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function updateTicket(object|array $object): bool
|
||||
{
|
||||
// TODO: Implement update() method.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new integration record.
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function create(object|array $object): int|false
|
||||
{
|
||||
return $this->integrationRepo->insert($object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an integration. Not yet implemented.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
// TODO: Implement delete() method.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all integrations matching the given search params. Not yet implemented.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function getAll(?array $searchparams = null): array|false
|
||||
{
|
||||
// TODO: Implement getAll() method.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches an integration record with the given values.
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param int $id Integration id
|
||||
* @param array $params Column => value pairs to update
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function patch(int $id, array $params): bool
|
||||
{
|
||||
return $this->integrationRepo->patch($id, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of Leantime entities available as import targets.
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @return array Map of entity key => entity definition
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function getAvailableEntities(): array
|
||||
{
|
||||
return $this->leantimeEntities->availableLeantimeEntities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available field definitions for a given Leantime entity.
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param string $entity Entity key (e.g. tickets, projects, users)
|
||||
* @return array Map of field key => field definition
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function getEntityFields(string $entity): array
|
||||
{
|
||||
return $this->leantimeEntities->availableLeantimeEntities[$entity]['fields'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the import entity for the fields-mapping step.
|
||||
*
|
||||
* Encapsulates the request/session fallback chain: prefers the submitted
|
||||
* leantimeEntities value (persisting it to the session), otherwise falls
|
||||
* back to the previously stored session entity. When an entity is resolved
|
||||
* it hydrates the given integration model and patches the integration record.
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param array $request Request parameters (expects optional 'leantimeEntities')
|
||||
* @param IntegrationModel $currentIntegration Integration model to hydrate with the resolved entity
|
||||
* @return string|null The resolved entity key, or null when no entity could be determined
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function resolveImportEntity(array $request, IntegrationModel $currentIntegration): ?string
|
||||
{
|
||||
if (isset($request['leantimeEntities'])) {
|
||||
$entity = $request['leantimeEntities'];
|
||||
session(['currentImportEntity' => $entity]);
|
||||
} elseif (session()->exists('currentImportEntity') && session('currentImportEntity') != '') {
|
||||
$entity = session('currentImportEntity');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentIntegration->entity = $entity;
|
||||
|
||||
$this->patch($currentIntegration->id, ['entity' => $entity]);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the provider fields used in the fields-mapping step.
|
||||
*
|
||||
* Uses the persisted integration fields when present, otherwise falls back
|
||||
* to the live provider fields.
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param IntegrationModel $currentIntegration Integration model that may carry stored fields
|
||||
* @param object $provider Provider instance exposing getFields()
|
||||
* @return array List of provider field identifiers
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function resolveProviderFields(IntegrationModel $currentIntegration, object $provider): array
|
||||
{
|
||||
if (isset($currentIntegration->fields) && $currentIntegration->fields != '') {
|
||||
return explode(',', $currentIntegration->fields);
|
||||
}
|
||||
|
||||
return $provider->getFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the cached, serialized import payload (fields + values) from the session.
|
||||
*
|
||||
* Pairs with Connector::cacheSerializedFieldValues() which writes the keys.
|
||||
* Uses safe_unserialize() to avoid object injection.
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @return array{values: array, fields: array} The decoded values and field mappings
|
||||
*/
|
||||
#[RequiresPermission(ConnectorPermissions::MANAGE, global: true)]
|
||||
public function getCachedImportPayload(): array
|
||||
{
|
||||
return [
|
||||
'values' => safe_unserialize(session('serValues'), []),
|
||||
'fields' => safe_unserialize(session('serFields'), []),
|
||||
];
|
||||
}
|
||||
}
|
||||
18
app/Domain/Connector/Services/ProviderIntegration.php
Normal file
18
app/Domain/Connector/Services/ProviderIntegration.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Services;
|
||||
|
||||
use Leantime\Domain\Connector\Models\Entity;
|
||||
|
||||
interface ProviderIntegration
|
||||
{
|
||||
public function connect(): mixed;
|
||||
|
||||
public function sync(Entity $entity): mixed;
|
||||
|
||||
public function getFields(): mixed;
|
||||
|
||||
public function getEntities(): mixed;
|
||||
|
||||
public function getValues(Entity $entity): mixed;
|
||||
}
|
||||
46
app/Domain/Connector/Services/Providers.php
Normal file
46
app/Domain/Connector/Services/Providers.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Connector\Services;
|
||||
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use Leantime\Domain\Connector\Models\Provider;
|
||||
|
||||
class Providers
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
private array $providers = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loadProviders();
|
||||
}
|
||||
|
||||
public function loadProviders(): void
|
||||
{
|
||||
|
||||
// Default Providers
|
||||
$provider = app()->make(\Leantime\Domain\CsvImport\Services\CsvImport::class);
|
||||
$this->providers[$provider->id] = $provider;
|
||||
|
||||
// providerId => provider
|
||||
$this->providers = self::dispatch_filter('providerList', $this->providers);
|
||||
}
|
||||
|
||||
public function getProviders(): array
|
||||
{
|
||||
return $this->providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getProvider($providerId): object
|
||||
{
|
||||
if (isset($this->providers[$providerId])) {
|
||||
return $this->providers[$providerId];
|
||||
} else {
|
||||
throw new \Exception('Provider does not exist');
|
||||
}
|
||||
}
|
||||
}
|
||||
47
app/Domain/Connector/Templates/integrationConfirm.blade.php
Normal file
47
app/Domain/Connector/Templates/integrationConfirm.blade.php
Normal file
@@ -0,0 +1,47 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$providerFields = $providerFields ?? [];
|
||||
$provider = $provider ?? null;
|
||||
$leantimeFields = $leantimeFields ?? [];
|
||||
$numberOfFields = $maxFields ?? 0;
|
||||
$urlAppend = '';
|
||||
if (isset($integrationId) && is_numeric($integrationId)) {
|
||||
$urlAppend = '&integrationId=' . $integrationId;
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><i class="fa-solid fa-circle-nodes"></i></div>
|
||||
<div class="pagetitle">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<h1>{!! __('headlines.connector') !!} // {{ $provider->name }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
@include('connector::submodules.importProgress')
|
||||
</div>
|
||||
<div class="maincontentinner">
|
||||
<div class='center'>
|
||||
<div style='width:30%' class='svgContainer'>
|
||||
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_party_re_nmwj.svg') !!}
|
||||
</div>
|
||||
<br />
|
||||
<h3>Integration Success</h3>
|
||||
<p>Your data was synced successfully.</p>
|
||||
<br />
|
||||
<a class='btn btn-default' href='{{ BASE_URL }}/connector/show'>Go back to integrations</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
82
app/Domain/Connector/Templates/integrationEntity.blade.php
Normal file
82
app/Domain/Connector/Templates/integrationEntity.blade.php
Normal file
@@ -0,0 +1,82 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$providerEntities = $providerEntities ?? [];
|
||||
$provider = $provider ?? null;
|
||||
$leantimeEntities = $leantimeEntities ?? [];
|
||||
$integrationId = $integrationId ?? null;
|
||||
|
||||
$urlAppend = '';
|
||||
if (isset($integrationId) && is_numeric($integrationId)) {
|
||||
$urlAppend = '&integrationId=' . $integrationId;
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><i class="fa-solid fa-circle-nodes"></i></div>
|
||||
<div class="pagetitle">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<h1>{!! __('headlines.connector') !!} // {{ $provider->name }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
@include('connector::submodules.importProgress')
|
||||
</div>
|
||||
<div class="maincontentinner center">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<h5 class="subtitle">What are you importing?</h5>
|
||||
<br />
|
||||
On this screen you can choose what you would like to synchronize. Choose an entity on the left and map it to someting in Leantime on the right.
|
||||
The arrow indicates that we will synchronize from one location to the other.<br /><br />
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/connector/integration/?provider={{ $provider->id }}&step=fields{{ $urlAppend }}">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3"></div>
|
||||
<div class="col-md-2 right">
|
||||
<h1>From (your integration)</h1>
|
||||
<label for="providerEntities">{{ $provider->name }}</label>
|
||||
<select name="providerEntities" id="providerEntities" style="width:100%;">
|
||||
@foreach ($providerEntities as $key => $entity)
|
||||
<option value="{{ $key }}">{{ $entity['name'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2" style="padding-top:50px;">
|
||||
<i class="fa fa-arrow-right"></i>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<h1>To (Leantime)</h1>
|
||||
|
||||
<label for="leantimeEntities">Leantime</label>
|
||||
<select name="leantimeEntities" id="leantimeEntities" style="width:100%;">
|
||||
@foreach ($leantimeEntities as $key => $entity)
|
||||
<option value="{{ $key }}">{{ $entity['name'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3"></div>
|
||||
</div>
|
||||
|
||||
<div class="left">
|
||||
<x-global::forms.button tag="a" link="{{ BASE_URL }}/connector/integration/?provider={{ $provider->id }}" contentRole="tertiary" class="pull-left">Back</x-global::forms.button>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<x-global::forms.button tag="input" inputType="submit" labelText="Next" />
|
||||
</div>
|
||||
<div class="clearall"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
92
app/Domain/Connector/Templates/integrationFields.blade.php
Normal file
92
app/Domain/Connector/Templates/integrationFields.blade.php
Normal file
@@ -0,0 +1,92 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$providerFields = $providerFields ?? [];
|
||||
$provider = $provider ?? null;
|
||||
$leantimeFields = $leantimeFields ?? [];
|
||||
$numberOfFields = $maxFields ?? 0;
|
||||
$flags = $flags ?? [];
|
||||
$urlAppend = '';
|
||||
|
||||
if (isset($integrationId) && is_numeric($integrationId)) {
|
||||
$urlAppend = '&integrationId=' . $integrationId;
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><i class="fa-solid fa-circle-nodes"></i></div>
|
||||
<div class="pagetitle">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<h1>{!! __('headlines.connector') !!} // {{ $provider->name }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
@include('connector::submodules.importProgress')
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="maincontentinner center">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
<h5 class="subtitle">Match Fields</h5>
|
||||
<p class="mb-2">Match the fields from your source to the corresponding fields in Leantime</p><br />
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/connector/integration/?provider={{ $provider->id }}&step=parse{{ $urlAppend }}">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="center">Source Field</th>
|
||||
<th class="center">Leantime Field</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($providerFields as $key => $entity)
|
||||
<tr>
|
||||
<td class="center">{{ $entity }}</td>
|
||||
<td class="center">
|
||||
<select class="form-control" name="field_{{ md5($entity) }}">
|
||||
@foreach ($leantimeFields as $key2 => $fields)
|
||||
<option value="{{ $entity }}|{{ $key2 }}" {{ ($entity == $fields['name'] && !in_array($key2, ['id', 'itemId'])) ? "selected='selected'" : '' }}>
|
||||
{{ $fields['name'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
<option value="">Don't map</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="left">
|
||||
<x-global::forms.button tag="a" link="{{ BASE_URL }}/connector/integration/?provider={{ $provider->id }}" contentRole="tertiary" class="pull-left">Back</x-global::forms.button>
|
||||
</div>
|
||||
<div class="right">
|
||||
<x-global::forms.button inputType="submit" contentRole="primary">Next</x-global::forms.button>
|
||||
</div>
|
||||
<div class="clearall"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="maincontentinner">
|
||||
<h5 class="subtitle">Requirements for a successful import</h5>
|
||||
<p>Please review these requirements and make sure your import and mapping covers everything.</p>
|
||||
@foreach ($flags as $flag)
|
||||
<hr />
|
||||
<p style="padding-left:10px"><strong>{{ $flag }}</strong></p>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
84
app/Domain/Connector/Templates/integrationImport.blade.php
Normal file
84
app/Domain/Connector/Templates/integrationImport.blade.php
Normal file
@@ -0,0 +1,84 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$providerFields = $providerFields ?? [];
|
||||
$provider = $provider ?? null;
|
||||
$leantimeFields = $leantimeFields ?? [];
|
||||
$numberOfFields = $maxFields ?? 0;
|
||||
$values = $values ?? [];
|
||||
$flags = $flags ?? [];
|
||||
$fields = $fields ?? [];
|
||||
$urlAppend = '';
|
||||
if (isset($integrationId) && is_numeric($integrationId)) {
|
||||
$urlAppend = '&integrationId=' . $integrationId;
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><i class="fa-solid fa-circle-nodes"></i></div>
|
||||
<div class="pagetitle">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<h1>{!! __('headlines.integrations') !!} // {{ $provider->name }} </h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
@include('connector::submodules.importProgress')
|
||||
</div>
|
||||
<div class="maincontentinner center">
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<h5 class="subtitle">Review</h5>
|
||||
|
||||
@if (!empty($flags))
|
||||
<p style="font-style: oblique">Please resolve the following errors and reconnect your integration:</p>
|
||||
<ul style="padding-left: 20px; margin-bottom: 20px;">
|
||||
@php $messages = []; @endphp
|
||||
@foreach ($flags as $flag)
|
||||
@if (!in_array($flag, $messages))
|
||||
<li style="margin-right: 10px; color: red; font-style: oblique;">{{ $flag }}</li>
|
||||
@php $messages[] = $flag; @endphp
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
<x-global::forms.button tag="a" class="pull-left" link="{{ BASE_URL }}/connector/integration?provider={{ $provider->id }}&step=fields{{ $urlAppend }}" contentRole="tertiary">Go Back</x-global::forms.button>
|
||||
@else
|
||||
<x-global::forms.button tag="a" class="right" link="{{ BASE_URL }}/connector/integration?provider={{ $provider->id }}&step=import" contentRole="primary">Confirm</x-global::forms.button>
|
||||
@endif
|
||||
<div class="clearall"></div>
|
||||
|
||||
<p>All set, we are importing the data you see below.</p>
|
||||
<br />
|
||||
|
||||
<table width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach ($fields as $sourceField => $leantimeField)
|
||||
<th>{{ $leantimeField['leantimeField'] }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($values as $record)
|
||||
<tr>
|
||||
@foreach ($record as $value)
|
||||
<td>{{ $value }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
|
||||
<div class="clearall"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
33
app/Domain/Connector/Templates/integrations.blade.php
Normal file
33
app/Domain/Connector/Templates/integrations.blade.php
Normal file
@@ -0,0 +1,33 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><span class="fa fa-plug"></span></div>
|
||||
<div class="pagetitle">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<h1>{!! __('headlines.integrations') !!}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function() {
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
49
app/Domain/Connector/Templates/newIntegration.blade.php
Normal file
49
app/Domain/Connector/Templates/newIntegration.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$provider = $provider ?? null;
|
||||
@endphp
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><i class="fa-solid fa-circle-nodes"></i></div>
|
||||
<div class="pagetitle">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<h1>{!! __('headlines.integrations') !!} // {{ $provider->name }} </h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
@include('connector::submodules.importProgress')
|
||||
</div>
|
||||
|
||||
<div class="maincontentinner center">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<img width="200" src="{{ BASE_URL }}/{{ $provider->image }}" />
|
||||
<h5 class="subtitle">New Integration</h5>
|
||||
|
||||
{{ $provider->name }}<br />
|
||||
{!! $provider->description !!}<br /><br />
|
||||
|
||||
<x-global::forms.button tag="a" link="{{ BASE_URL }}/connector/integration?provider={{ $provider->id }}&step=connect" contentRole="primary">Click Here to Connect</x-global::forms.button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function() {
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
33
app/Domain/Connector/Templates/providers.blade.php
Normal file
33
app/Domain/Connector/Templates/providers.blade.php
Normal file
@@ -0,0 +1,33 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><span class="fa fa-plug"></span></div>
|
||||
<div class="pagetitle">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<h1>{!! __('headlines.providers') !!}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function() {
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
61
app/Domain/Connector/Templates/show.blade.php
Normal file
61
app/Domain/Connector/Templates/show.blade.php
Normal file
@@ -0,0 +1,61 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><i class="fa-solid fa-circle-nodes"></i></div>
|
||||
<div class="pagetitle">
|
||||
<h1>Integrations</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
<h5 class="subtitle">Sync Leantime with your external applications</h5>
|
||||
<p>Available Integrations</p>
|
||||
|
||||
<div class="row">
|
||||
@foreach ($providers as $provider)
|
||||
<div class="col-md-3">
|
||||
<div class="profileBox">
|
||||
<div class="commentImage gradient">
|
||||
<img src="{{ BASE_URL }}/{{ $provider->image }}"/>
|
||||
</div>
|
||||
<span class="userName">
|
||||
<strong>{{ $provider->name }}</strong>
|
||||
<br /><small>Available methods: {{ implode(', ', $provider->methods) }}</small>
|
||||
<br /><br />
|
||||
{!! $provider->description !!}
|
||||
</span>
|
||||
<br />
|
||||
|
||||
@if (isset($provider->button))
|
||||
<x-global::forms.button tag="a" link="{{ $provider->button['url'] }}" contentRole="primary">{{ $provider->button['text'] }}</x-global::forms.button>
|
||||
@else
|
||||
<x-global::forms.button tag="a" link="{{ BASE_URL }}/connector/integration?provider={{ $provider->id }}" contentRole="primary">Create New Integration</x-global::forms.button>
|
||||
@endif
|
||||
|
||||
<div class="clearall"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Existing Integrations section placeholder --}}
|
||||
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function() {
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
0
app/Domain/Connector/Templates/styles.css
Normal file
0
app/Domain/Connector/Templates/styles.css
Normal file
@@ -0,0 +1,51 @@
|
||||
@php
|
||||
$currentStep = $_GET['step'] ?? 'connect';
|
||||
|
||||
$totalSteps = count($provider->steps);
|
||||
$completed = $provider->stepDetails[$currentStep]['position'];
|
||||
$halfStep = (1 / $totalSteps * 100) / 2;
|
||||
$percentDone = ($completed / $totalSteps) * 100 - $halfStep;
|
||||
|
||||
$i = 0;
|
||||
@endphp
|
||||
<br />
|
||||
<div class="projectSteps">
|
||||
<div class="progressWrapper">
|
||||
<div class="progress">
|
||||
<div
|
||||
id="progressChecklistBar"
|
||||
class="progress-bar progress-bar-success tx-transition"
|
||||
role="progressbar"
|
||||
aria-valuenow="0"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style="width: {{ $percentDone }}%"
|
||||
><span class="sr-only"></span></div>
|
||||
</div>
|
||||
|
||||
@foreach ($provider->steps as $step)
|
||||
@php
|
||||
$i++;
|
||||
$stepClass = '';
|
||||
if ($currentStep == $step) {
|
||||
$stepClass = 'current';
|
||||
}
|
||||
if ($provider->stepDetails[$currentStep]['position'] > $provider->stepDetails[$step]['position']) {
|
||||
$stepClass = 'complete';
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="step {{ $stepClass }}" style="left: {{ ($i / $totalSteps * 100) - $halfStep }}%;">
|
||||
<a href="javascript:void(0)" data-toggle="dropdown" class="dropdown-toggle">
|
||||
<span class="innerCircle"></span>
|
||||
<span class="title">
|
||||
@if ($provider->stepDetails[$currentStep]['position'] > $provider->stepDetails[$step]['position'])
|
||||
<i class="fa fa-check"></i>
|
||||
@endif
|
||||
{{ $provider->stepDetails[$step]['title'] }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user