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

Commit

Permalink
Merge pull request #140 from jolicode/feat/standup-reminder-by-client
Browse files Browse the repository at this point in the history
Client standup reminders
  • Loading branch information
xavierlacot authored Sep 19, 2023
2 parents 22c8e8e + aab373a commit 44ece70
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 17 deletions.
36 changes: 36 additions & 0 deletions migrations/Version20230918170924.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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)\'');
$this->addSql('UPDATE standup_meeting_reminder SET forecast_clients="a:0:{}"');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE standup_meeting_reminder DROP forecast_clients');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public function configureFields(string $pageName): iterable
return [
AssociationField::new('slackTeam')->onlyOnIndex(),
TextField::new('channelId')->onlyOnIndex(),
ArrayField::new('forecastClients')->onlyOnIndex(),
ArrayField::new('forecastProjects')->onlyOnIndex(),
TextField::new('updatedBy')->onlyOnIndex(),
TextField::new('time'),
Expand Down
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
32 changes: 28 additions & 4 deletions src/Entity/StandupMeetingReminder.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ class StandupMeetingReminder
private string $channelId;

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

/**
* @var array<array-key, string>
*/
#[ORM\Column(type: 'array')]
private array $forecastProjects = [];
Expand Down Expand Up @@ -92,15 +98,33 @@ public function setChannelId(string $channelId): self
}

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

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

return $this;
}

/**
* @return array<array-key, string>
*/
public function getForecastProjects(): ?array
public function getForecastProjects(): array
{
return $this->forecastProjects;
}

/**
* @param array<array-key, int> $forecastProjects
* @param array<array-key, string> $forecastProjects
*/
public function setForecastProjects(array $forecastProjects): self
{
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($standupMeetingReminder->getForecastClients()) || \in_array((string) $projects[$projectId]->getClientId(), $standupMeetingReminder->getForecastClients(), true))
&& (0 === \count($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 44ece70

Please sign in to comment.