OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View 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');
}
}

View 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');
}
}

View 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.
}
}

View 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() {}
}

View 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() {}
}

View 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() {}
}

View 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() {}
}

View 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() {}
}

View 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),
];
}
}

View 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;
}
}

View 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' => ''],
],
],
];
}
}

View 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]);
}
}

View 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'), []),
];
}
}

View 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;
}

View 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');
}
}
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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>