Merge branch '2.6' into port/2.6.4-2.6.6

Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
This commit is contained in:
Kevin Decherf 2023-09-18 16:50:07 +02:00
commit 2910fb6da4
17 changed files with 234 additions and 49 deletions

View file

@ -5,7 +5,7 @@ on:
push: push:
branches: branches:
- master - master
- 2.* - "2.**"
permissions: permissions:
contents: read contents: read

View file

@ -5,7 +5,7 @@ on:
push: push:
branches: branches:
- master - master
- 2.* - "2.**"
permissions: permissions:
contents: read contents: read

View file

@ -5,7 +5,7 @@ on:
push: push:
branches: branches:
- master - master
- 2.* - "2.**"
env: env:
PGPASSWORD: wallabagrocks PGPASSWORD: wallabagrocks

View file

@ -5,7 +5,7 @@ on:
push: push:
branches: branches:
- master - master
- 2.* - "2.**"
permissions: permissions:
contents: read contents: read

View file

@ -1,5 +1,32 @@
# Changelog # Changelog
## [2.6.6](https://github.com/wallabag/wallabag/tree/2.6.6)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.5...2.6.6)
### Security fix
* Force secure cookie on HTTPS connection by @j0k3r in https://github.com/wallabag/wallabag/pull/6924
### Fixes
* Fix checkboxes pointer events issue by @Simounet in https://github.com/wallabag/wallabag/pull/6897
* Add Google mailer by @j0k3r in https://github.com/wallabag/wallabag/pull/6899
* Improve performance on homepage by @Simounet in https://github.com/wallabag/wallabag/pull/6909
* Mass action layout improved by @Simounet in https://github.com/wallabag/wallabag/pull/6912
## [2.6.5](https://github.com/wallabag/wallabag/tree/2.6.5)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.4...2.6.5)
### Fixes
* "Fix checkboxes pointer-events disabled" by @Simounet https://github.com/wallabag/wallabag/pull/6874
* "Fix nav input styles" by @Simounet https://github.com/wallabag/wallabag/pull/6877
* "Change domain status filters html types" by @Simounet https://github.com/wallabag/wallabag/pull/6888
## [2.6.4](https://github.com/wallabag/wallabag/tree/2.6.4)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.3...2.6.4)
### Fixes
* Fix API token generation by @nicosomb https://github.com/wallabag/wallabag/pull/6869
* Fix checkboxes which were broken by @nicosomb https://github.com/wallabag/wallabag/pull/6864
## [2.6.3](https://github.com/wallabag/wallabag/tree/2.6.3) ## [2.6.3](https://github.com/wallabag/wallabag/tree/2.6.3)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.2...2.6.3) [Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.2...2.6.3)

View file

@ -14,25 +14,30 @@
} }
.mass-action { .mass-action {
margin: 10px 5px 10px 20px; margin: 20px 5px 10px 20px;
} }
.mass-action-group { .mass-action-group {
display: flex; display: flex;
padding: 3px; padding: 3px;
gap: 10px; align-items: center;
gap: 30px;
} }
.mass-action-button { .mass-action-button {
height: 24px; height: 36px;
line-height: 24px; line-height: 36px;
padding: 0 0.5rem; padding: 0 0.7rem;
i { i {
font-size: 1rem; font-size: 1rem;
} }
} }
.mass-action-button--tags {
border-radius: 2px 0 0 2px;
}
.entry-checkbox { .entry-checkbox {
margin: 10px 15px 10px 5px; margin: 10px 15px 10px 5px;
@ -64,11 +69,19 @@
.mass-action-tags { .mass-action-tags {
display: flex; display: flex;
align-items: center; margin-top: 10px;
gap: 10px;
.mass-action-tags-input { .mass-action-tags-input.mass-action-tags-input {
margin: 0; margin: 0;
padding: 0 5px;
height: 34px;
background: white;
border-bottom: 3px solid #c5ebef;
}
.mass-action-tags-input.mass-action-tags-input.mass-action-tags-input:focus {
border-bottom: 3px solid $blue-accent-color;
box-shadow: none;
} }
} }
@ -88,13 +101,16 @@
.results { .results {
display: flex; display: flex;
margin-bottom: 10px;
padding: 1rem 1rem 0; padding: 1rem 1rem 0;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
}
.nb-results { .nb-results {
display: inline-flex; display: inline-flex;
} margin-bottom: 20px;
gap: 30px;
} }
.results-item { .results-item {
@ -173,9 +189,38 @@ footer {
} }
@media screen and (min-width: 993px) { @media screen and (min-width: 993px) {
.results {
margin-bottom: 0;
}
.nb-results {
margin-bottom: 0;
gap: 0;
}
.mass-action-button {
height: 24px;
line-height: 24px;
padding: 0 0.5rem;
}
.mass-action-group {
gap: 10px;
}
.mass-action-tags {
margin-top: 0;
margin-left: 7px;
flex-wrap: initial;
}
.mass-action { .mass-action {
display: flex; display: flex;
margin-top: 10px;
align-items: center; align-items: center;
gap: 30px;
.mass-action-tags-input.mass-action-tags-input {
height: 21px;
}
} }
} }

View file

@ -70,6 +70,7 @@ nav {
.input-field input { .input-field input {
display: block; display: block;
font-size: 1.2rem;
line-height: inherit; line-height: inherit;
height: 3rem; height: 3rem;
} }
@ -79,6 +80,17 @@ nav {
box-shadow: none; box-shadow: none;
color: #444; color: #444;
} }
/* materializecss override */
.input-field.input-field input {
margin-bottom: 0;
border-bottom: none;
}
.input-field.input-field input:focus {
border-bottom: none;
box-shadow: initial;
}
} }
.nav-panel-top { .nav-panel-top {

View file

@ -15,6 +15,13 @@ div.settings div.file-field {
} }
} }
/* override materializecss pointer-event disabled on checkboxes */
[type="checkbox"]:not(:checked),
[type="checkbox"]:checked,
.input-field label {
pointer-events: initial;
}
.input-field label.active { .input-field label.active {
font-size: 1rem; font-size: 1rem;
} }

View file

@ -29,6 +29,7 @@ framework:
# handler_id set to null will use default session handler from php.ini # handler_id set to null will use default session handler from php.ini
handler_id: session.handler.native_file handler_id: session.handler.native_file
save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%" save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%"
cookie_secure: auto
fragments: ~ fragments: ~
http_method_override: true http_method_override: true
assets: ~ assets: ~
@ -84,13 +85,8 @@ doctrine_migrations:
executed_at_column_name: 'executed_at' executed_at_column_name: 'executed_at'
fos_rest: fos_rest:
zone:
- { path: ^/api }
- { path: ^/annotations }
param_fetcher_listener: true param_fetcher_listener: true
body_listener: true body_listener: true
exception:
serializer_error_renderer: true
view: view:
mime_types: mime_types:
csv: csv:
@ -116,6 +112,9 @@ fos_rest:
- { path: "^/api/entries/([0-9]+)/export.(.*)", priorities: ['epub', 'pdf', 'txt', 'csv'], fallback_format: json, prefer_extension: false } - { path: "^/api/entries/([0-9]+)/export.(.*)", priorities: ['epub', 'pdf', 'txt', 'csv'], fallback_format: json, prefer_extension: false }
- { path: "^/api", priorities: ['json', 'xml'], fallback_format: json, prefer_extension: false } - { path: "^/api", priorities: ['json', 'xml'], fallback_format: json, prefer_extension: false }
- { path: "^/annotations", priorities: ['json', 'xml'], fallback_format: json, prefer_extension: false } - { path: "^/annotations", priorities: ['json', 'xml'], fallback_format: json, prefer_extension: false }
# for an unknown reason, EACH REQUEST goes to FOS\RestBundle\EventListener\FormatListener
# so we need to add custom rule for custom api export but also for all other routes of the application...
- { path: '^/', priorities: ['text/html', '*/*'], fallback_format: html, prefer_extension: false }
nelmio_api_doc: nelmio_api_doc:
areas: areas:

View file

@ -1,5 +1,5 @@
wallabag_core: wallabag_core:
version: 2.6.3 version: 2.6.6
paypal_url: "https://liberapay.com/wallabag/donate" paypal_url: "https://liberapay.com/wallabag/donate"
languages: languages:
en: 'English' en: 'English'

View file

@ -130,6 +130,7 @@
"symfony/finder": "^4.4", "symfony/finder": "^4.4",
"symfony/form": "^4.4", "symfony/form": "^4.4",
"symfony/framework-bundle": "^4.4", "symfony/framework-bundle": "^4.4",
"symfony/google-mailer": "^4.4",
"symfony/http-foundation": "^4.4", "symfony/http-foundation": "^4.4",
"symfony/http-kernel": "^4.4", "symfony/http-kernel": "^4.4",
"symfony/mailer": "^4.4", "symfony/mailer": "^4.4",
@ -209,7 +210,11 @@
"incenteev-parameters": { "incenteev-parameters": {
"file": "app/config/parameters.yml" "file": "app/config/parameters.yml"
}, },
"public-dir": "web" "public-dir": "web",
"symfony": {
"allow-contrib": true,
"require": "4.4.*"
}
}, },
"scripts": { "scripts": {
"post-install-cmd": [ "post-install-cmd": [

View file

@ -675,9 +675,6 @@ class EntryController extends AbstractController
} }
} }
$nbEntriesUntagged = $this->entryRepository
->countUntaggedEntriesByUser($this->getUser()->getId());
return $this->render( return $this->render(
'@WallabagCore/Entry/entries.html.twig', [ '@WallabagCore/Entry/entries.html.twig', [
'form' => $form->createView(), 'form' => $form->createView(),
@ -685,7 +682,6 @@ class EntryController extends AbstractController
'currentPage' => $page, 'currentPage' => $page,
'searchTerm' => $searchTerm, 'searchTerm' => $searchTerm,
'isFiltered' => $form->isSubmitted(), 'isFiltered' => $form->isSubmitted(),
'nbEntriesUntagged' => $nbEntriesUntagged,
] ]
); );
} }

View file

@ -6,6 +6,7 @@ use Lexik\Bundle\FormFilterBundle\Filter\FilterOperands;
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\CheckboxFilterType; use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\CheckboxFilterType;
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\ChoiceFilterType; use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\ChoiceFilterType;
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\DateRangeFilterType; use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\DateRangeFilterType;
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\NumberFilterType;
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\NumberRangeFilterType; use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\NumberRangeFilterType;
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\TextFilterType; use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\TextFilterType;
use Lexik\Bundle\FormFilterBundle\Filter\Query\QueryInterface; use Lexik\Bundle\FormFilterBundle\Filter\Query\QueryInterface;
@ -102,10 +103,13 @@ class EntryFilterType extends AbstractType
return $filterQuery->createCondition($expression); return $filterQuery->createCondition($expression);
}, },
'label' => 'entry.filters.domain_label', 'label' => 'entry.filters.domain_label',
'attr' => [
'autocapitalize' => 'off',
],
]) ])
->add('httpStatus', TextFilterType::class, [ ->add('httpStatus', NumberFilterType::class, [
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
$value = $values['value']; $value = (int) $values['value'];
if (false === \array_key_exists($value, Response::$statusTexts)) { if (false === \array_key_exists($value, Response::$statusTexts)) {
return false; return false;
} }
@ -117,6 +121,11 @@ class EntryFilterType extends AbstractType
return $filterQuery->createCondition($expression, $parameters); return $filterQuery->createCondition($expression, $parameters);
}, },
'label' => 'entry.filters.http_status_label', 'label' => 'entry.filters.http_status_label',
'html5' => true,
'attr' => [
'min' => 100,
'max' => 527,
],
]) ])
->add('isArchived', CheckboxFilterType::class, [ ->add('isArchived', CheckboxFilterType::class, [
'label' => 'entry.filters.archived_label', 'label' => 'entry.filters.archived_label',

View file

@ -37,6 +37,20 @@ class EntryRepository extends ServiceEntityRepository
; ;
} }
/**
* Retrieves all entries count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForAllByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
;
}
/** /**
* Retrieves unread entries for a user. * Retrieves unread entries for a user.
* *
@ -52,6 +66,21 @@ class EntryRepository extends ServiceEntityRepository
; ;
} }
/**
* Retrieves unread entries count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForUnreadByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
->andWhere('e.isArchived = false')
;
}
/** /**
* Retrieves entries with the same domain. * Retrieves entries with the same domain.
* *
@ -94,6 +123,21 @@ class EntryRepository extends ServiceEntityRepository
; ;
} }
/**
* Retrieves read entries count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForArchiveByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
->andWhere('e.isArchived = true')
;
}
/** /**
* Retrieves starred entries for a user. * Retrieves starred entries for a user.
* *
@ -109,6 +153,21 @@ class EntryRepository extends ServiceEntityRepository
; ;
} }
/**
* Retrieves starred entries count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForStarredByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
->andWhere('e.isStarred = true')
;
}
/** /**
* Retrieves entries filtered with a search term for a user. * Retrieves entries filtered with a search term for a user.
* *
@ -169,6 +228,21 @@ class EntryRepository extends ServiceEntityRepository
; ;
} }
/**
* Retrieve entries with annotations count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForAnnotationsByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
->innerJoin('e.annotations', 'a')
;
}
/** /**
* Retrieve untagged entries for a user. * Retrieve untagged entries for a user.
* *
@ -588,6 +662,23 @@ class EntryRepository extends ServiceEntityRepository
return $qb->getQuery()->getArrayResult(); return $qb->getQuery()->getArrayResult();
} }
/**
* @param int $userId
*
* @return array
*/
public function findEmptyEntriesIdByUserId($userId = null)
{
$qb = $this->createQueryBuilder('e')
->select('e.id');
if (null !== $userId) {
$qb->where('e.user = :userid AND e.content IS NULL')->setParameter(':userid', $userId);
}
return $qb->getQuery()->getArrayResult();
}
/** /**
* Find all entries by url and owner. * Find all entries by url and owner.
* *

View file

@ -53,13 +53,11 @@
<button class="mass-action-button btn cyan darken-1" type="submit" name="toggle-read" title="{{ 'entry.list.toogle_as_read'|trans }}"><i class="material-icons">done</i></button> <button class="mass-action-button btn cyan darken-1" type="submit" name="toggle-read" title="{{ 'entry.list.toogle_as_read'|trans }}"><i class="material-icons">done</i></button>
<button class="mass-action-button btn cyan darken-1" type="submit" name="toggle-star" title="{{ 'entry.list.toogle_as_star'|trans }}" ><i class="material-icons">star</i></button> <button class="mass-action-button btn cyan darken-1" type="submit" name="toggle-star" title="{{ 'entry.list.toogle_as_star'|trans }}" ><i class="material-icons">star</i></button>
<button class="mass-action-button btn cyan darken-1" type="submit" name="delete" onclick="return confirm('{{ 'entry.confirm.delete_entries'|trans|escape('js') }}')" title="{{ 'entry.list.delete'|trans }}"><i class="material-icons">delete</i></button> <button class="mass-action-button btn cyan darken-1" type="submit" name="delete" onclick="return confirm('{{ 'entry.confirm.delete_entries'|trans|escape('js') }}')" title="{{ 'entry.list.delete'|trans }}"><i class="material-icons">delete</i></button>
<label for="mass-action-tags-displayed" class="mass-action-button btn cyan darken-1" type="button" title="{{ 'entry.list.add_tags'|trans }}"><i class="material-icons">label</i></label>
</div> </div>
<input id="mass-action-tags-displayed" class="toggle-checkbox" type="checkbox" />
<div class="mass-action-tags"> <div class="mass-action-tags">
<button class="btn cyan darken-1 mass-action-button mass-action-button--tags" type="submit" name="tag" title="{{ 'entry.list.add_tags'|trans }}"><i class="material-icons">label</i></button>
<input type="text" class="mass-action-tags-input" name="tags" placeholder="{{ 'entry.list.mass_action_tags_input_placeholder'|trans }}" /> <input type="text" class="mass-action-tags-input" name="tags" placeholder="{{ 'entry.list.mass_action_tags_input_placeholder'|trans }}" />
<button class="btn cyan darken-1" type="submit" name="tag">{{ 'entry.list.add_tags'|trans }}</button>
</div> </div>
</div> </div>
@ -117,9 +115,9 @@
<h4 class="center">{{ 'entry.filters.title'|trans }}</h4> <h4 class="center">{{ 'entry.filters.title'|trans }}</h4>
<div class="row"> <div class="row">
{% if current_route != 'untagged' and nbEntriesUntagged != 0 %} {% if current_route != 'untagged' %}
<div class="col s12 center-align"> <div class="col s12 center-align">
<a href="{{ path('untagged') }}">{{ 'tag.list.see_untagged_entries'|trans }} ({{ nbEntriesUntagged }})</a> <a href="{{ path('untagged') }}">{{ 'tag.list.see_untagged_entries'|trans }}</a>
</div> </div>
{% endif %} {% endif %}

View file

@ -168,7 +168,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col m12 l8"> <div class="col m12 l8">
<p class="footer-text" title="{{ display_stats()|raw|striptags }}"> <p class="footer-text">
{{ display_stats() }} {{ display_stats() }}
</p> </p>
</div> </div>

View file

@ -96,35 +96,32 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
switch ($type) { switch ($type) {
case 'starred': case 'starred':
$qb = $this->entryRepository->getBuilderForStarredByUser($user->getId()); $qb = $this->entryRepository->getCountBuilderForStarredByUser($user->getId());
break; break;
case 'archive': case 'archive':
$qb = $this->entryRepository->getBuilderForArchiveByUser($user->getId()); $qb = $this->entryRepository->getCountBuilderForArchiveByUser($user->getId());
break; break;
case 'unread': case 'unread':
$qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId()); $qb = $this->entryRepository->getCountBuilderForUnreadByUser($user->getId());
break; break;
case 'annotated': case 'annotated':
$qb = $this->entryRepository->getBuilderForAnnotationsByUser($user->getId()); $qb = $this->entryRepository->getCountBuilderForAnnotationsByUser($user->getId());
break; break;
case 'all': case 'all':
$qb = $this->entryRepository->getBuilderForAllByUser($user->getId()); $qb = $this->entryRepository->getCountBuilderForAllByUser($user->getId());
break; break;
default: default:
throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type)); throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type));
} }
// THANKS to PostgreSQL we CAN'T make a DEAD SIMPLE count(e.id)
// ERROR: column "e0_.id" must appear in the GROUP BY clause or be used in an aggregate function
$query = $qb $query = $qb
->select('e.id') ->select('COUNT(e.id)')
->groupBy('e.id')
->getQuery(); ->getQuery();
$query->useQueryCache(true); $query->useQueryCache(true);
$query->enableResultCache($this->lifeTime); $query->enableResultCache($this->lifeTime);
return \count($query->getArrayResult()); return $query->getSingleScalarResult();
} }
/** /**
@ -156,15 +153,14 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
return ''; return '';
} }
$query = $this->entryRepository->getBuilderForArchiveByUser($user->getId()) $query = $this->entryRepository->getCountBuilderForArchiveByUser($user->getId())
->select('e.id') ->select('COUNT(e.id)')
->groupBy('e.id')
->getQuery(); ->getQuery();
$query->useQueryCache(true); $query->useQueryCache(true);
$query->enableResultCache($this->lifeTime); $query->enableResultCache($this->lifeTime);
$nbArchives = \count($query->getArrayResult()); $nbArchives = $query->getSingleScalarResult();
$interval = $user->getCreatedAt()->diff(new \DateTime('now')); $interval = $user->getCreatedAt()->diff(new \DateTime('now'));
$nbDays = (int) $interval->format('%a') ?: 1; $nbDays = (int) $interval->format('%a') ?: 1;