diff --git a/README.md b/README.md
index 672ccf26..31d8cf97 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,8 @@ All this made with the best technologies.
+
+
## Documentation
@@ -122,9 +124,12 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio
- #32 Default user seeder enhancement
- #31 Issue resolved
- **Release 1.2.0**
- - Scrum module #28
- - Design enhancement (Kanban / Scrum boards)
- - Referential updates
+ - Scrum module #28
+ - Design enhancement (Kanban / Scrum boards)
+ - Referential updates
+- **Release 1.2.1**
+ - Add jira integration #36
+ - New feature: Import jira projects / tickets
## Support us
diff --git a/app/Filament/Pages/JiraImport.php b/app/Filament/Pages/JiraImport.php
new file mode 100644
index 00000000..d0336cbb
--- /dev/null
+++ b/app/Filament/Pages/JiraImport.php
@@ -0,0 +1,270 @@
+form->fill();
+ }
+
+ protected static function shouldRegisterNavigation(): bool
+ {
+ return auth()->user()->can('Import from Jira');
+ }
+
+ protected function getSubheading(): string|Htmlable|null
+ {
+ return __('Use this section to login into your jira account and import tickets to this application');
+ }
+
+ protected static function getNavigationLabel(): string
+ {
+ return __('Jira import');
+ }
+
+ protected static function getNavigationGroup(): ?string
+ {
+ return __('Settings');
+ }
+
+ protected function getFormSchema(): array
+ {
+ return [
+ Card::make()
+ ->schema([
+ Wizard::make([
+ Wizard\Step::make(__('Jira login'))
+ ->schema([
+ Placeholder::make('info')
+ ->extraAttributes([
+ 'class' => 'bg-primary-500 rounded-lg border border-primary-600 text-white font-medium text-sm py-3 px-4'
+ ])
+ ->disableLabel()
+ ->content(__('Important: Your jira credentials are only used to communicate with jira REST API, and will not be stored in this application')),
+
+ Grid::make()
+ ->schema([
+ TextInput::make('host')
+ ->label(__('Host'))
+ ->helperText(__('The url used to access your jira account'))
+ ->required(),
+
+ TextInput::make('username')
+ ->label(__('Username'))
+ ->helperText(__('Your jira account username'))
+ ->required(),
+
+ TextInput::make('token')
+ ->label(__('API Token'))
+ ->helperText(__('Your jira account API Token'))
+ ->password()
+ ->required(),
+ ]),
+ ])
+ ->afterValidation(function () {
+ $this->loadingProjects = true;
+ $this->emit('updateJiraProjects');
+ }),
+
+ Wizard\Step::make(__('Jira projects'))
+ ->schema([
+ Placeholder::make('hint')
+ ->extraAttributes([
+ 'class' => 'bg-primary-500 rounded-lg border border-primary-600 text-white font-medium text-sm py-3 px-4'
+ ])
+ ->disableLabel()
+ ->visible(fn() => !$this->loadingProjects && $this->projects)
+ ->content(__('Choose your jira projects to import')),
+
+ Placeholder::make('loading')
+ ->extraAttributes([
+ 'class' => 'bg-warning-500 rounded-lg border border-warning-600 text-white font-medium text-sm py-3 px-4'
+ ])
+ ->disableLabel()
+ ->visible(fn() => $this->loadingProjects)
+ ->content(__('Loading projects, please wait...')),
+
+ Placeholder::make('info')
+ ->extraAttributes([
+ 'class' => 'bg-danger-500 rounded-lg border border-danger-600 text-white font-medium text-sm py-3 px-4'
+ ])
+ ->disableLabel()
+ ->visible(fn() => !$this->loadingProjects && !$this->projects)
+ ->content(__('Your jira credentials are incorrect, please go to previous step and re-enter your jira credentials')),
+
+ CheckboxList::make('selected_projects')
+ ->label(__('Jira projects'))
+ ->required()
+ ->visible(fn() => $this->projects)
+ ->options(function () {
+ $list = [];
+ if ($this->projects) {
+ foreach ($this->projects as $project) {
+ $list[$project->key] = new HtmlString(
+ "
"
+ . "
"
+ . "
"
+ . "
" . $project->name . ""
+ . "
/ " . $project->key . "
"
+ . "
"
+ . "
"
+ );
+ }
+ }
+ return $list;
+ }),
+
+ ])
+ ->afterValidation(function () {
+ $this->loadingTickets = true;
+ $this->emit('updateJiraTickets');
+ }),
+
+ Wizard\Step::make(__('Jira tickets'))
+ ->schema(function () {
+ $fields = [];
+
+ $fields[] = Placeholder::make('hint')
+ ->extraAttributes([
+ 'class' => 'bg-primary-500 rounded-lg border border-primary-600 text-white font-medium text-sm py-3 px-4'
+ ])
+ ->disableLabel()
+ ->visible(fn() => !$this->loadingTickets && $this->tickets)
+ ->content(__('Choose your jira projects to import'));
+
+ $fields[] = Placeholder::make('loading')
+ ->extraAttributes([
+ 'class' => 'bg-warning-500 rounded-lg border border-warning-600 text-white font-medium text-sm py-3 px-4'
+ ])
+ ->disableLabel()
+ ->visible(fn() => $this->loadingTickets)
+ ->content(__('Loading tickets, please wait...'));
+
+ if (!$this->loadingTickets) {
+ if ($this->tickets) {
+ foreach ($this->tickets as $projectKey => $ticket) {
+ if ($ticket['total'] > 0) {
+ $fields[] = Placeholder::make('tickets_' . Str::slug($projectKey))
+ ->label(__('Tickets for the project:') . ' ' . $projectKey)
+ ->extraAttributes([
+ 'style' => 'margin-bottom: -15px;'
+ ])
+ ->content('');
+
+ foreach ($ticket['issues'] as $issue) {
+ $fields[] = Checkbox::make('data.' . Str::slug($projectKey) . '_' . Str::slug($issue['code']))
+ ->label(function () use ($issue) {
+ return new HtmlString(
+ ""
+ . "
"
+ . "
" . $issue['code'] . " " . $issue['name'] . "
"
+ . "
"
+ . "
"
+ );
+ });
+ }
+ } else {
+ $fields[] = Placeholder::make('no_tickets_' . Str::slug($projectKey))
+ ->label(__('Tickets for the project:') . ' ' . $projectKey)
+ ->content(__('No tickets found!'));
+ }
+ }
+ } else {
+ $fields[] = Placeholder::make('info')
+ ->extraAttributes([
+ 'class' => 'bg-warning-500 rounded-lg border border-warning-600 text-white font-medium text-sm py-3 px-4'
+ ])
+ ->disableLabel()
+ ->visible(fn() => !$this->projects)
+ ->content(__('No tickets found!'));
+ }
+ }
+ return $fields;
+ }),
+ ])
+ ->submitAction(new HtmlString("")),
+ ]),
+ ];
+ }
+
+ public function import(): void
+ {
+ if ($this->data && sizeof($this->data)) {
+ $tickets = [];
+ foreach (array_keys($this->data) as $item) {
+ $url = $this->ticketsDataApi[$item];
+ $tickets[] = $this->getJiraTicketDetails($this->host, $this->username, $this->token, $url);
+ }
+ dispatch(new ImportJiraTicketsJob($tickets, auth()->user()));
+ $this->notify('success', __('The importation job is started, when finished you will be notified'), true);
+ $this->redirect(route('filament.pages.jira-import'));
+ } else {
+ $this->notify('warning', __('Please choose at least a jira ticket to import'));
+ }
+ }
+
+ public function updateJiraProjects(): void
+ {
+ $client = $this->connectToJira($this->host, $this->username, $this->token);
+ $this->projects = $this->getJiraProjects($client);
+ $this->loadingProjects = false;
+ }
+
+ public function updateJiraTickets(): void
+ {
+ $this->ticketsDataApi = [];
+ $client = $this->connectToJira($this->host, $this->username, $this->token);
+ $this->tickets = $this->getJiraTicketsByProject($client, $this->selected_projects);
+ foreach ($this->tickets as $projectKey => $ticket) {
+ foreach ($ticket['issues'] as $issue) {
+ $this->ticketsDataApi[Str::slug($projectKey) . '_' . Str::slug($issue['code'])] = $issue['data']->self;
+ }
+ }
+ $this->loadingTickets = false;
+ }
+}
diff --git a/app/Helpers/JiraHelper.php b/app/Helpers/JiraHelper.php
new file mode 100644
index 00000000..804f10ed
--- /dev/null
+++ b/app/Helpers/JiraHelper.php
@@ -0,0 +1,78 @@
+ $host,
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ 'Authorization' => 'Basic ' . base64_encode($username . ":" . $token)
+ ]
+ ]);
+ }
+
+ public function getJiraProjects(Client $client): array|null
+ {
+ try {
+ $response = $client->get('/rest/api/2/project');
+ return json_decode($response->getBody()->getContents());
+ } catch (GuzzleException $e) {
+ Log::error($e->getTraceAsString());
+ return null;
+ }
+ }
+
+ public function getJiraTicketsByProject(Client $client, $projectKeys): array|null
+ {
+ try {
+ $formatIssues = function ($issues) {
+ $results = [];
+ foreach ($issues as $issue) {
+ $results[] = [
+ 'code' => $issue->key,
+ 'name' => $issue->fields->summary,
+ 'data' => $issue
+ ];
+ }
+ return $results;
+ };
+ $results = [];
+ foreach ($projectKeys as $projectKey) {
+ $response = $client->get('/rest/api/2/search?jql=project=' . $projectKey);
+ $data = json_decode($response->getBody()->getContents());
+ $results[$projectKey] = [
+ 'total' => $data->total,
+ 'issues' => $formatIssues($data->issues)
+ ];
+ }
+ return $results;
+ } catch (GuzzleException $e) {
+ Log::error($e->getTraceAsString());
+ return null;
+ }
+ }
+
+ public function getJiraTicketDetails($host, $username, $token, $url)
+ {
+ try {
+ $client = $this->connectToJira($host, $username, $token);
+ $url = explode('/', $url);
+ $response = $client->get('/rest/api/2/issue/' . $url[sizeof($url) - 1]);
+ return json_decode($response->getBody()->getContents());
+ } catch (GuzzleException $e) {
+ Log::error($e->getTraceAsString());
+ return null;
+ }
+ }
+
+}
diff --git a/app/Jobs/ImportJiraTicketsJob.php b/app/Jobs/ImportJiraTicketsJob.php
new file mode 100644
index 00000000..854a7aa8
--- /dev/null
+++ b/app/Jobs/ImportJiraTicketsJob.php
@@ -0,0 +1,84 @@
+tickets = $tickets;
+ $this->user = $user;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ if ($this->tickets && sizeof($this->tickets)) {
+ foreach ($this->tickets as $ticket) {
+ $projectDetails = $ticket->fields->project;
+ $ticketData = $ticket->fields;
+
+ $project = Project::where('name', $projectDetails->name)->first();
+ if (!$project) {
+ $project = Project::create([
+ 'name' => $projectDetails->name,
+ 'description' => __('Project imported from Jira, project key:') . $projectDetails->key,
+ 'status_id' => ProjectStatus::where('is_default', true)->first()->id,
+ 'owner_id' => $this->user->id,
+ 'ticket_prefix' => $projectDetails->key
+ ]);
+
+ ProjectUser::create([
+ 'project_id' => $project->id,
+ 'user_id' => $this->user->id,
+ 'role' => config('system.projects.affectations.roles.can_manage')
+ ]);
+ }
+
+ Ticket::create([
+ 'name' => $ticketData->summary,
+ 'content' => $ticketData->description ?? __('No content found in jira ticket'),
+ 'owner_id' => $this->user->id,
+ 'status_id' => TicketStatus::where('is_default', true)->first()->id,
+ 'project_id' => $project->id,
+ 'type_id' => TicketType::where('is_default', true)->first()->id,
+ 'priority_id' => TicketPriority::where('is_default', true)->first()->id,
+ ]);
+ }
+ FilamentNotification::make()
+ ->title(__('Jira importation'))
+ ->icon('heroicon-o-cloud-download')
+ ->body(__('Jira tickets successfully imported'))
+ ->sendToDatabase($this->user);
+ }
+ }
+}
diff --git a/database/seeders/PermissionsSeeder.php b/database/seeders/PermissionsSeeder.php
index e76c446b..3c0175ec 100644
--- a/database/seeders/PermissionsSeeder.php
+++ b/database/seeders/PermissionsSeeder.php
@@ -26,7 +26,7 @@ class PermissionsSeeder extends Seeder
];
private array $extraPermissions = [
- 'Manage general settings'
+ 'Manage general settings', 'Import from Jira'
];
private string $defaultRole = 'Default role';
diff --git a/docs/_media/23.png b/docs/_media/23.png
new file mode 100644
index 00000000..66cbd1d8
Binary files /dev/null and b/docs/_media/23.png differ
diff --git a/docs/_media/24.png b/docs/_media/24.png
new file mode 100644
index 00000000..b163c609
Binary files /dev/null and b/docs/_media/24.png differ
diff --git a/docs/_media/25.png b/docs/_media/25.png
new file mode 100644
index 00000000..ad0bd6c9
Binary files /dev/null and b/docs/_media/25.png differ
diff --git a/docs/_media/26.png b/docs/_media/26.png
new file mode 100644
index 00000000..2bce1795
Binary files /dev/null and b/docs/_media/26.png differ
diff --git a/docs/installation.md b/docs/installation.md
index 805b5f5e..a5f2c4f3 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -36,6 +36,8 @@ In addition of the main [prerequisites](/?id=prerequisites), you must have this
+
+
diff --git a/github-contents/25.png b/github-contents/25.png
new file mode 100644
index 00000000..ad0bd6c9
Binary files /dev/null and b/github-contents/25.png differ
diff --git a/github-contents/26.png b/github-contents/26.png
new file mode 100644
index 00000000..2bce1795
Binary files /dev/null and b/github-contents/26.png differ
diff --git a/lang/fr.json b/lang/fr.json
index acfd4130..f80e6d20 100644
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -230,5 +230,31 @@
"Next sprint:": "Sprint suivant :",
"Sprint ended at": "Sprint terminé le",
"Sprint started at": "Sprint commencé le",
- "Started at:": "Commencé le :"
+ "Started at:": "Commencé le :",
+ "Jira import": "Import Jira",
+ "Jira login": "Authentification Jira",
+ "Jira projects": "Projets Jira",
+ "Jira tickets": "Tickets Jira",
+ "Username": "Nom d'utilisateur",
+ "Password": "Mot de passe",
+ "Your jira account username": "Le nom d'utilisateur de votre compte jira",
+ "Your jira account password": "Le mot de passe de votre compte jira",
+ "Important: Your jira credentials are only used to communicate with jira REST API, and will not be stored in this application": "Important : Vos informations d'identification Jira ne sont utilisées que pour communiquer avec l'API REST Jira et ne seront pas stockées dans cette application.",
+ "Use this section to login into your jira account and import tickets to this application": "Utilisez cette section pour vous connecter à votre compte jira puis importer vos tickets sur cette application",
+ "Host": "Hôte",
+ "The url used to access your jira account": "L'url utilisé pour accéder à votre compte jira",
+ "Your jira credentials are incorrect, please go to previous step and re-enter your jira credentials": "Vos informations d'identification Jira sont incorrectes, veuillez passer à l'étape précédente et saisir à nouveau vos informations d'identification Jira",
+ "Loading jira projects, please wait...": "Chargement des projets jira, veuillez patienter...",
+ "Choose your jira projects to import": "Choisissez vos projets jira à importer",
+ "No tickets found!": "Aucun ticket trouvé !",
+ "Tickets for the project:": "Tickets pour le projet :",
+ "Import": "Importer",
+ "Loading projects, please wait...": "Chargement des projets, veuillez patienter...",
+ "Loading tickets, please wait...": "Chargement des tickets, veuillez patienter...",
+ "Please choose at least a jira ticket to import": "Veuillez sélectionner au moins un ticket jira à importer",
+ "The importation job is started, when finished you will be notified": "La tâche d'importation est lancée, une fois terminée vous serez notifié",
+ "Jira importation": "Importation jira",
+ "Jira tickets successfully imported": "Tickets jira importés avec succès",
+ "Before you can import jira tickets you need to have all the referentials configured": "Avant de pouvoir importer des tickets jira, vous devez avoir configuré tous les référentiels",
+ "No content found in jira ticket": "Aucun contenu trouvé sur le ticket jira"
}
diff --git a/resources/views/filament/pages/jira-import.blade.php b/resources/views/filament/pages/jira-import.blade.php
new file mode 100644
index 00000000..91683242
--- /dev/null
+++ b/resources/views/filament/pages/jira-import.blade.php
@@ -0,0 +1,12 @@
+
+
+
+ {{ __('Important:') }}
+ {{ __('Before you can import jira tickets you need to have all the referentials configured') }}
+
+
+
+
+