Skip to content
This repository has been archived by the owner on Mar 29, 2024. It is now read-only.

Commit

Permalink
allow to configure standup meeting reminders that are associated to a…
Browse files Browse the repository at this point in the history
…ll the projects for one or more clients, not only one or more projects
  • Loading branch information
xavierlacot committed Sep 19, 2023
1 parent 22c8e8e commit ed55f97
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 13 deletions.
35 changes: 35 additions & 0 deletions migrations/Version20230918170924.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

/*
* This file is part of JoliCode's Forecast Tools project.
*
* (c) JoliCode <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20230918170924 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add a way to attach standup reminders to a client, not only to projects';
}

public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE standup_meeting_reminder ADD forecast_clients LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\'');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE standup_meeting_reminder DROP forecast_clients');
}
}
4 changes: 4 additions & 0 deletions src/Controller/SlackCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public function dataSource(Request $request, StandupMeetingReminderHandler $stan
return new JsonResponse($standupMeetingReminderHandler->listProjects($payload));
}

if ('selected_clients' === $payload['action_id']) {
return new JsonResponse($standupMeetingReminderHandler->listClients($payload));
}

return new JsonResponse('<3 you, Slack');
}

Expand Down
24 changes: 24 additions & 0 deletions src/Entity/StandupMeetingReminder.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ class StandupMeetingReminder
#[ORM\Column(type: 'string', length: 15)]
private string $channelId;

/**
* @var array<array-key, int>
*/
#[ORM\Column(type: 'array')]
private array $forecastClients = [];

/**
* @var array<array-key, int>
*/
Expand Down Expand Up @@ -91,6 +97,24 @@ public function setChannelId(string $channelId): self
return $this;
}

/**
* @return array<array-key, int>
*/
public function getForecastClients(): ?array
{
return $this->forecastClients;
}

/**
* @param array<array-key, int> $forecastClients
*/
public function setForecastClients(array $forecastClients): self
{
$this->forecastClients = $forecastClients;

return $this;
}

/**
* @return array<array-key, int>
*/
Expand Down
160 changes: 148 additions & 12 deletions src/StandupMeetingReminder/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

use function Symfony\Component\String\u;

class Handler
{
final public const ACTION_PREFIX = 'standup-reminder';
Expand Down Expand Up @@ -51,7 +53,7 @@ public function handleRequest(Request $request): void
case self::SLACK_COMMAND_OPTION_LIST:
$this->listReminders($request);

// try to preload available projects so that they're in cache
// try to preload available clients and projects so that they're in cache
$this->loadProjects($request->request->get('team_id'));
break;
case '':
Expand Down Expand Up @@ -130,8 +132,24 @@ public function handleBlockAction(array $payload): void
*/
public function handleSubmission(array $payload): JsonResponse
{
if (
0 === (is_countable($payload['view']['state']['values']['clients']['selected_clients']['selected_options']) ? \count($payload['view']['state']['values']['clients']['selected_clients']['selected_options']) : 0)
+ (is_countable($payload['view']['state']['values']['projects']['selected_projects']['selected_options']) ? \count($payload['view']['state']['values']['projects']['selected_projects']['selected_options']) : 0)
) {
return new JsonResponse([
'response_action' => 'errors',
'errors' => [
'clients' => 'Please choose at least one client or project.',
'projects' => 'Please choose at least one client or project.',
],
]);
}

$selectedClientsForDisplay = [];
$selectedClientIds = [];
$selectedProjectsForDisplay = [];
$selectedProjectIds = [];
$restrictionDescription = '';
$slackTeam = $this->slackTeamRepository->findOneBy([
'teamId' => $payload['team']['id'],
]);
Expand All @@ -143,18 +161,43 @@ public function handleSubmission(array $payload): JsonResponse
$channelId = $privateMetadata['channel_id'];
}

foreach ($payload['view']['state']['values']['clients']['selected_clients']['selected_options'] as $client) {
$selectedClientsForDisplay[] = sprintf('"%s"', $client['text']['text']);
$selectedClientIds[] = $client['value'];
}

foreach ($payload['view']['state']['values']['projects']['selected_projects']['selected_options'] as $project) {
$selectedProjectsForDisplay[] = sprintf('"%s"', $project['text']['text']);
$selectedProjectIds[] = $project['value'];
}

if (\count($selectedProjectsForDisplay) > 1) {
$lastProject = ' and ' . array_pop($selectedProjectsForDisplay);
} else {
$lastProject = '';
$projectsByAccount = $this->loadProjects($payload['team']['id']);

foreach ($projectsByAccount as $data) {
foreach ($data['projects'] as $project) {
if (
\count($selectedClientIds) > 0
&& \in_array($project->getId(), $selectedProjectIds, true)
&& !\in_array($project->getClientId(), $selectedClientIds, true)
) {
return new JsonResponse([
'response_action' => 'errors',
'errors' => [
'projects' => 'Please choose projects that match the selected client(s).',
],
]);
}
}
}

if (\count($selectedClientsForDisplay) > 0) {
$restrictionDescription .= ' for the client(s) ' . u(', ')->join($selectedClientsForDisplay, ' and ');
}

if (\count($selectedProjectsForDisplay) > 0) {
$restrictionDescription .= ' on the project(s) ' . u(', ')->join($selectedProjectsForDisplay, ' and ');
}

$selectedProjectsForDisplay = implode(', ', $selectedProjectsForDisplay) . $lastProject;
$selectedTime = $payload['view']['state']['values']['time']['selected_time']['selected_option']['value'];
$standupMeetingReminder = $this->standupMeetingReminderRepository->findOneBy([
'channelId' => $channelId,
Expand All @@ -171,19 +214,19 @@ public function handleSubmission(array $payload): JsonResponse

$standupMeetingReminder->setUpdatedBy('@' . $payload['user']['username']);
$standupMeetingReminder->setIsEnabled(true);
$standupMeetingReminder->setForecastClients($selectedClientIds);
$standupMeetingReminder->setForecastProjects($selectedProjectIds);
$standupMeetingReminder->setTime($selectedTime);
$this->em->persist($standupMeetingReminder);
$this->em->flush();

$client = \JoliCode\Slack\ClientFactory::create($slackTeam->getAccessToken());
$message = sprintf(
'<@%s> %s a stand-up reminder in this channel. It will run each day at `%s` and ping people working on the project%s %s.',
'<@%s> %s a stand-up reminder in this channel. It will run each day at `%s` and ping people working%s.',
$payload['user']['username'],
$actionName,
$selectedTime,
('' !== $lastProject) ? 's' : '',
$selectedProjectsForDisplay
$restrictionDescription
);
$client->chatPostMessage([
'channel' => $channelId,
Expand Down Expand Up @@ -214,6 +257,56 @@ public function handleSubmission(array $payload): JsonResponse
return new JsonResponse(['response_action' => 'clear']);
}

/**
* @param array<string, mixed> $payload
*
* @return array<string, array<array-key, array<string, array<int|string, mixed>>>>
*/
public function listClients(array $payload): array
{
$availableClients = [];
$searched = mb_strtolower((string) $payload['value']);
$projectsByAccount = $this->loadProjects($payload['team']['id']);

foreach ($projectsByAccount as $data) {
$accountClients = [];

foreach ($data['clients'] as $client) {
if (false !== mb_strpos(mb_strtolower((string) $client->getName()), $searched)) {
$accountClients[] = [
'text' => [
'type' => 'plain_text',
'text' => mb_substr((string) $client->getName(), 0, 75),
],
'value' => (string) $client->getId(),
];
}
}

if (\count($accountClients) > 0) {
usort($accountClients, fn ($a, $b) => strcmp($a['text']['text'], $b['text']['text']));

$availableClients[] = [
'label' => [
'type' => 'plain_text',
'text' => $data['forecastAccount']->getName(),
],
'options' => $accountClients,
];
}
}

if (1 === \count($availableClients)) {
return [
'options' => $availableClients[0]['options'],
];
}

return [
'option_groups' => $availableClients,
];
}

/**
* @param array<string, mixed> $payload
*
Expand Down Expand Up @@ -289,7 +382,7 @@ public function loadProjects(string $teamId): array
$this->forecastDataSelector->setForecastAccount($forecastAccount);
$projectsByAccount[] = [
'forecastAccount' => $forecastAccount,
'clients' => $this->forecastDataSelector->getClientsById(),
'clients' => $this->forecastDataSelector->getClientsById(true),
'projects' => $this->forecastDataSelector->getProjects(true),
];
}
Expand Down Expand Up @@ -347,6 +440,7 @@ private function displayModalForm(string $teamId, ?string $channelId, string $tr
$slackTeam = $this->slackTeamRepository->findOneByTeamId($teamId);
$availableTimes = [];
$initialProjects = [];
$initialClients = [];
$initialTime = null;
$initialHour = 10;
$initialMinute = 0;
Expand Down Expand Up @@ -377,11 +471,24 @@ private function displayModalForm(string $teamId, ?string $channelId, string $tr

foreach ($forecastAccounts as $forecastAccount) {
$this->forecastDataSelector->setForecastAccount($forecastAccount);
$clients = $this->forecastDataSelector->getClients(true);
$projects = $this->forecastDataSelector->getProjects(true);

foreach ($clients as $client) {
if (\in_array((string) $client->getId(), $standupMeetingReminder->getForecastClients(), true)) {
$initialClients[] = [
'text' => [
'type' => 'plain_text',
'text' => mb_substr($client->getName(), 0, 75),
],
'value' => (string) $client->getId(),
];
}
}

foreach ($projects as $project) {
if (\in_array((string) $project->getId(), $standupMeetingReminder->getForecastProjects(), true)) {
$projectCode = (null !== $project->getCode()) ? '[' . $project->getCode() . '] ' : '';
$projectCode = null !== $project->getCode() && '' !== $project->getCode() ? '[' . $project->getCode() . '] ' : '';
$initialProjects[] = [
'text' => [
'type' => 'plain_text',
Expand Down Expand Up @@ -433,12 +540,35 @@ private function displayModalForm(string $teamId, ?string $channelId, string $tr
}
}

$clientsBlock = [
'type' => 'input',
'block_id' => 'clients',
'label' => [
'type' => 'plain_text',
'text' => 'Choose clients',
],
'element' => [
'type' => 'multi_external_select',
'action_id' => 'selected_clients',
'placeholder' => [
'type' => 'plain_text',
'text' => 'Select clients',
],
'min_query_length' => 3,
],
'optional' => true,
];

if (\count($initialClients) > 0) {
$clientsBlock['element']['initial_options'] = $initialClients;
}

$projectsBlock = [
'type' => 'input',
'block_id' => 'projects',
'label' => [
'type' => 'plain_text',
'text' => 'Select one or more Forecast projects',
'text' => 'Choose Forecast projects',
],
'element' => [
'type' => 'multi_external_select',
Expand All @@ -449,12 +579,18 @@ private function displayModalForm(string $teamId, ?string $channelId, string $tr
],
'min_query_length' => 3,
],
'optional' => true,
'hint' => [
'type' => 'plain_text',
'text' => 'Choose one or more projects to restrict the notification to these projects only. If no specific project is choosen, all the projects attached to the selected client(s) will be used.',
],
];

if (\count($initialProjects) > 0) {
$projectsBlock['element']['initial_options'] = $initialProjects;
}

$blocks[] = $clientsBlock;
$blocks[] = $projectsBlock;
$blocks[] = [
'type' => 'input',
Expand Down
8 changes: 7 additions & 1 deletion src/StandupMeetingReminder/Sender.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,15 @@ private function findParticipants(StandupMeetingReminder $standupMeetingReminder
$today = new \DateTime('today');
$assignments = $this->forecastDataSelector->getAssignments($today, new \DateTime('tomorrow'));
$assignments = array_values(array_filter($assignments, fn ($assignment): bool => $assignment->getStartDate()->format('Y-m-d') <= $today->format('Y-m-d') && $assignment->getEndDate()->format('Y-m-d') >= $today->format('Y-m-d')));
$projects = $this->forecastDataSelector->getProjectsById(enabled: true);

foreach ($assignments as $assignment) {
if (\in_array((string) $assignment->getProjectId(), $standupMeetingReminder->getForecastProjects(), true)) {
$projectId = $assignment->getProjectId();

if (
(0 === \count((array) $standupMeetingReminder->getForecastClients()) || \in_array((string) $projects[$projectId]->getClientId(), $standupMeetingReminder->getForecastClients(), true))
&& (0 === \count((array) $standupMeetingReminder->getForecastProjects()) || \in_array((string) $projectId, $standupMeetingReminder->getForecastProjects(), true))
) {
if (null !== $assignment->getPersonId()) {
$members[$people[$assignment->getPersonId()]->getEmail()] = $memberName = sprintf(
'%s %s',
Expand Down

0 comments on commit ed55f97

Please sign in to comment.