Skip to content

Commit

Permalink
imp: Allow to give colors to labels
Browse files Browse the repository at this point in the history
  • Loading branch information
marien-probesys committed Jul 31, 2024
2 parents a032d86 + 5719594 commit 00cfe75
Show file tree
Hide file tree
Showing 21 changed files with 139 additions and 53 deletions.
1 change: 1 addition & 0 deletions docs/developers/import-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ The data is stored in a ZIP archive. It contains several files:
- id: string (unique)
- name: string (unique, not empty, max 50 chars)
- description: string, optional (max 250 chars)
- color: string, optional (must be `grey`, `primary`, `blue`, `green`, `orange`, or `red`)

It also contains a `tickets/` folder where each file corresponds to a ticket. For clarity reasons, the files can be put in sub-folders. Sub-folders have no meaning to the command, but can help to group tickets by organizations for instance. The name of the files doesn't matter, but they have to contain JSON objects:

Expand Down
11 changes: 5 additions & 6 deletions src/Entity/Label.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Label implements MonitorableEntityInterface, UidEntityInterface
use MonitorableEntityTrait;
use UidEntityTrait;

public const COLORS = ['grey', 'primary', 'blue', 'green', 'orange', 'red'];

#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
Expand Down Expand Up @@ -67,11 +69,8 @@ class Label implements MonitorableEntityInterface, UidEntityInterface
private ?string $description = null;

#[ORM\Column(length: 7)]
#[Assert\NotBlank(
message: new TranslatableMessage('label.color.required', [], 'errors'),
)]
#[Assert\CssColor(
formats: Assert\CssColor::HEX_LONG,
#[Assert\Choice(
choices: self::COLORS,
message: new TranslatableMessage('label.color.invalid', [], 'errors'),
)]
private ?string $color = null;
Expand All @@ -84,7 +83,7 @@ public function __construct()
{
$this->name = '';
$this->description = '';
$this->color = '#e0e1e6';
$this->color = 'grey';
$this->tickets = new ArrayCollection();
}

Expand Down
7 changes: 4 additions & 3 deletions src/Form/Type/LabelType.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'trim' => true,
]);

$builder->add('color', Type\HiddenType::class, [
'data' => '#e0e1e6',
'trim' => true,
$builder->add('color', Type\ChoiceType::class, [
'choices' => Entity\Label::COLORS,
'expanded' => true,
'multiple' => false,
]);
}

Expand Down
17 changes: 6 additions & 11 deletions src/Form/Type/TicketLabelsType.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
return $entityRepository->createQueryBuilder('l')
->orderBy('l.name', 'ASC');
},
'choice_label' => function (Entity\Label $label): string {
$labelName = htmlspecialchars($label->getName(), \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
$labelDescription = htmlspecialchars($label->getDescription(), \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');

$htmlLabel = "<span class=\"badge badge--grey\">{$labelName}</span>";

if ($labelDescription) {
$htmlLabel .= "&nbsp;<span class=\"text--small text--secondary\">{$labelDescription}</span>";
}

return $htmlLabel;
'choice_label' => 'name',
'choice_attr' => function (Entity\Label $label): array {
return [
'description' => $label->getDescription(),
'color' => $label->getColor(),
];
},
'expanded' => true,
'multiple' => true,
Expand Down
8 changes: 8 additions & 0 deletions src/Misc/AdditionalTranslations.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,11 @@
new TranslatableMessage('roles.permissions.orga.update.tickets.status');
new TranslatableMessage('roles.permissions.orga.update.tickets.title');
new TranslatableMessage('roles.permissions.orga.update.tickets.type');

// See templates/labels/_form.html.twig
new TranslatableMessage('common.colors.grey');
new TranslatableMessage('common.colors.primary');
new TranslatableMessage('common.colors.blue');
new TranslatableMessage('common.colors.green');
new TranslatableMessage('common.colors.orange');
new TranslatableMessage('common.colors.red');
6 changes: 6 additions & 0 deletions src/Service/DataImporter/DataImporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -747,10 +747,16 @@ private function processLabels(array $json): \Generator
$description = strval($jsonLabel['description']);
}

$color = 'grey';
if (isset($jsonLabel['color'])) {
$color = strval($jsonLabel['color']);
}

// Build the label
$label = new Label();
$label->setName($name);
$label->setDescription($description);
$label->setColor($color);

// Add the label to the index
try {
Expand Down
6 changes: 4 additions & 2 deletions src/Twig/TicketEventChangesFormatterExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,15 @@ private function formatLabelsChanges(Entity\User $user, array $changes): string

$removed = array_map(function ($label): string {
$name = $this->escape($label->getName());
return "<span class=\"badge badge--grey\">{$name}</span>";
$color = $this->escape($label->getColor());
return "<span class=\"badge badge--{$color}\">{$name}</span>";
}, $removedLabels);
$removed = implode(', ', $removed);

$added = array_map(function ($label): string {
$name = $this->escape($label->getName());
return "<span class=\"badge badge--grey\">{$name}</span>";
$color = $this->escape($label->getColor());
return "<span class=\"badge badge--{$color}\">{$name}</span>";
}, $addedLabels);
$added = implode(', ', $added);

Expand Down
26 changes: 26 additions & 0 deletions templates/labels/_form.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,32 @@
/>
</div>

<fieldset>
<legend>{{ 'labels.color' | trans }}</legend>

<div class="flow flow--small">
{{ form_errors(form.color) }}

<div class="flow flow--inline flow--smaller">
{% for color in form.color %}
<span>
<input
type="radio"
id="{{ field_id(color) }}"
name="{{ field_name(color) }}"
value="{{ field_value(color) }}"
{{ field_value(color) == field_value(form.color) ? 'checked' }}
/>

<label for="{{ field_id(color) }}" class="badge badge--big badge--{{ field_value(color) }}">
{{ ("common.colors." ~ field_value(color)) | trans }}
</label>
</span>
{% endfor %}
</div>
</div>
</div>

<div class="form__actions">
<button class="button--primary" type="submit">
{{ submit_label }}
Expand Down
2 changes: 1 addition & 1 deletion templates/labels/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<ul class="list--padded list--border list--nostyle">
{% for label in labels %}
<li class="cols cols--baseline flow" data-test="label-item">
<p class="badge badge--grey">
<p class="badge badge--{{ label.color }}">
{{ label.name }}
</p>

Expand Down
21 changes: 10 additions & 11 deletions templates/organizations/tickets/new.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -323,21 +323,20 @@
name="labels[]"
value="{{ label.uid }}"
{{ label.uid in labelUids ? 'checked' }}
{% if label.description %}
aria-describedby="label-{{ label.uid }}-description"
{% endif %}
/>

<label for="labels-{{ label.uid }}">
<span>
<span class="badge badge--grey">
{{ label.name }}
</span>
<label for="labels-{{ label.uid }}" class="badge badge--{{ label.color }}">
{{ label.name }}
</label>

{% if label.description %}
<span class="text--small text--secondary">
{{ label.description }}
</span>
{% endif %}
{% if label.description %}
<span id="label-{{ label.uid }}-description" class="text--small text--secondary">
{{ label.description }}
</span>
</label>
{% endif %}
</div>
{% endfor %}
</div>
Expand Down
2 changes: 1 addition & 1 deletion templates/tickets/_list.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
{% if ticket.labels %}
<ul class="list--nostyle flow flow--inline flow--smaller text--small">
{% for label in ticket.labels %}
<li class="badge badge--grey">
<li class="badge badge--{{ label.color }}">
{{ label.name }}
</li>
{% endfor %}
Expand Down
2 changes: 1 addition & 1 deletion templates/tickets/_search_form.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@
{{ label.name in ticketFilter.filter('label') ? 'checked' }}
/>

<label for="filter-label-{{ label.uid }}">
<label for="filter-label-{{ label.uid }}" class="badge badge--{{ label.color }}">
{{ label.name }}
</label>
</div>
Expand Down
13 changes: 11 additions & 2 deletions templates/tickets/labels/edit.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,20 @@
name="{{ field_name(label) }}"
value="{{ field_value(label) }}"
{{ field_value(label) in field_value(form.labels) ? 'checked' }}
{% if label.vars.attr.description %}
aria-describedby="{{ field_id(label, 'description') }}"
{% endif %}
/>

<label for="{{ field_id(label) }}">
{{ field_label(label) | raw }}
<label for="{{ field_id(label) }}" class="badge badge--{{ label.vars.attr.color }}">
{{ field_label(label) }}
</label>

{% if label.vars.attr.description %}
<span id="{{ field_id(label, 'description') }}" class="text--small text--secondary">
{{ label.vars.attr.description }}
</span>
{% endif %}
</div>
{% endfor %}
</div>
Expand Down
2 changes: 1 addition & 1 deletion templates/tickets/show.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@
{% if ticket.labels is not empty %}
<ul class="list--nostyle flow flow--inline flow--smaller">
{% for label in ticket.labels %}
<li class="badge badge--grey">
<li class="badge badge--{{ label.color }}">
{{ label.name }}
</li>
{% endfor %}
Expand Down
45 changes: 36 additions & 9 deletions tests/Controller/LabelsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public function testPostCreateCreatesTheLabelAndRedirects(): void
$this->grantAdmin($user->_real(), ['admin:manage:labels']);
$name = 'My label';
$description = 'My description';
$color = 'primary';

$this->assertSame(0, LabelFactory::count());

Expand All @@ -100,7 +101,7 @@ public function testPostCreateCreatesTheLabelAndRedirects(): void
'_token' => $this->generateCsrfToken($client, 'label'),
'name' => $name,
'description' => $description,
'color' => '#e0e1e6',
'color' => $color,
],
]);

Expand All @@ -109,6 +110,7 @@ public function testPostCreateCreatesTheLabelAndRedirects(): void
$this->assertResponseRedirects('/labels', 302);
$this->assertSame($name, $label->getName());
$this->assertSame($description, $label->getDescription());
$this->assertSame($color, $label->getColor());
$this->assertSame(20, strlen($label->getUid()));
}

Expand All @@ -127,7 +129,7 @@ public function testPostCreateFailsIfNameIsAlreadyUsed(): void
'label' => [
'_token' => $this->generateCsrfToken($client, 'label'),
'name' => $name,
'color' => '#e0e1e6',
'color' => 'grey',
],
]);

Expand All @@ -150,14 +152,35 @@ public function testPostCreateFailsIfNameIsEmpty(): void
'label' => [
'_token' => $this->generateCsrfToken($client, 'label'),
'name' => $name,
'color' => '#e0e1e6',
'color' => 'grey',
],
]);

$this->assertSelectorTextContains('#label_name-error', 'Enter a name');
$this->assertSame(0, LabelFactory::count());
}

public function testPostCreateFailsIfColorIsInvalid(): void
{
$client = static::createClient();
$user = UserFactory::createOne();
$client->loginUser($user->_real());
$this->grantAdmin($user->_real(), ['admin:manage:labels']);
$name = 'My label';
$color = 'not a color';

$crawler = $client->request(Request::METHOD_POST, '/labels/new', [
'label' => [
'_token' => $this->generateCsrfToken($client, 'label'),
'name' => $name,
'color' => $color,
],
]);

$this->assertSelectorTextContains('#label_color-error', 'The selected choice is invalid');
$this->assertSame(0, LabelFactory::count());
}

public function testPostCreateFailsIfCsrfTokenIsInvalid(): void
{
$client = static::createClient();
Expand All @@ -170,7 +193,7 @@ public function testPostCreateFailsIfCsrfTokenIsInvalid(): void
'label' => [
'_token' => 'not a token',
'name' => $name,
'color' => '#e0e1e6',
'color' => 'grey',
],
]);

Expand All @@ -192,7 +215,7 @@ public function testPostCreateFailsIfAccessIsForbidden(): void
'label' => [
'_token' => $this->generateCsrfToken($client, 'label'),
'name' => $name,
'color' => '#e0e1e6',
'color' => 'grey',
],
]);
}
Expand Down Expand Up @@ -234,24 +257,28 @@ public function testPostUpdateSavesTheLabelAndRedirects(): void
$newName = 'My label';
$initialDescription = 'description';
$newDescription = 'My description';
$initialColor = 'primary';
$newColor = 'red';
$label = LabelFactory::createOne([
'name' => $initialName,
'description' => $initialDescription,
'color' => $initialColor,
]);

$client->request(Request::METHOD_POST, "/labels/{$label->getUid()}/edit", [
'label' => [
'_token' => $this->generateCsrfToken($client, 'label'),
'name' => $newName,
'description' => $newDescription,
'color' => '#e0e1e6',
'color' => $newColor,
],
]);

$this->assertResponseRedirects('/labels', 302);
$label->_refresh();
$this->assertSame($newName, $label->getName());
$this->assertSame($newDescription, $label->getDescription());
$this->assertSame($newColor, $label->getColor());
}

public function testPostUpdateFailsIfNameIsAlreadyUsed(): void
Expand All @@ -273,7 +300,7 @@ public function testPostUpdateFailsIfNameIsAlreadyUsed(): void
'label' => [
'_token' => $this->generateCsrfToken($client, 'label'),
'name' => $newName,
'color' => '#e0e1e6',
'color' => 'grey',
],
]);

Expand Down Expand Up @@ -302,7 +329,7 @@ public function testPostUpdateFailsIfCsrfTokenIsInvalid(): void
'label' => [
'_token' => 'not a token',
'name' => $newName,
'color' => '#e0e1e6',
'color' => 'grey',
],
]);

Expand Down Expand Up @@ -330,7 +357,7 @@ public function testPostUpdateFailsIfAccessIsForbidden(): void
'label' => [
'_token' => $this->generateCsrfToken($client, 'label'),
'name' => $newName,
'color' => '#e0e1e6',
'color' => 'grey',
],
]);
}
Expand Down
Loading

0 comments on commit 00cfe75

Please sign in to comment.