Improve list item CSS
This commit is contained in:
parent
dbdb824269
commit
35b53653a1
7 changed files with 306 additions and 84 deletions
|
|
@ -14,6 +14,6 @@ class Task(db.Model):
|
||||||
due: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
due: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
||||||
created: Mapped[datetime] = mapped_column(DateTime(timezone=False), default=now)
|
created: Mapped[datetime] = mapped_column(DateTime(timezone=False), default=now)
|
||||||
complete: Mapped[bool] = mapped_column(Boolean, default=False)
|
complete: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
completed: Mapped[int] = mapped_column(DateTime(timezone=False), nullable=True)
|
completed: Mapped[datetime|None] = mapped_column(DateTime(timezone=False), nullable=True)
|
||||||
namespace: Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id', ondelete='CASCADE'))
|
namespace: Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id', ondelete='CASCADE'))
|
||||||
owner: Mapped[int] = mapped_column(Integer, ForeignKey('user.id', ondelete='CASCADE'))
|
owner: Mapped[int] = mapped_column(Integer, ForeignKey('user.id', ondelete='CASCADE'))
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
import humanize
|
import humanize
|
||||||
|
|
@ -34,6 +34,8 @@ class TaskForUser:
|
||||||
due_rel: str
|
due_rel: str
|
||||||
created: datetime
|
created: datetime
|
||||||
complete: bool
|
complete: bool
|
||||||
|
completed: datetime|None
|
||||||
|
overdue: bool
|
||||||
namespace_id: int
|
namespace_id: int
|
||||||
|
|
||||||
can_edit: bool
|
can_edit: bool
|
||||||
|
|
@ -41,6 +43,33 @@ class TaskForUser:
|
||||||
can_complete: bool
|
can_complete: bool
|
||||||
can_uncomplete: bool
|
can_uncomplete: bool
|
||||||
|
|
||||||
|
@property
|
||||||
|
def delay_style(self) -> str:
|
||||||
|
''' Returns an appropriate 'animation-delay' property for
|
||||||
|
recently-completed tasks; otherwise an empty string.
|
||||||
|
Intended for use in jinja templating
|
||||||
|
'''
|
||||||
|
if self.just_complete and self.completed is not None:
|
||||||
|
delta = now() - self.completed
|
||||||
|
return f'animation-delay: {-1*delta.seconds}s;'
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def just_complete(self) -> bool:
|
||||||
|
''' Returns whether the task was recently finished (within 5 minutes).
|
||||||
|
|
||||||
|
Used for jinja templates.
|
||||||
|
'''
|
||||||
|
return (
|
||||||
|
self.complete
|
||||||
|
and (self.completed is not None)
|
||||||
|
and (
|
||||||
|
(now() - self.completed)
|
||||||
|
< timedelta(minutes=5)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_task(
|
def from_task(
|
||||||
cls,
|
cls,
|
||||||
|
|
@ -71,6 +100,12 @@ class TaskForUser:
|
||||||
_due_str(tsk.due),
|
_due_str(tsk.due),
|
||||||
tsk.created,
|
tsk.created,
|
||||||
tsk.complete,
|
tsk.complete,
|
||||||
|
(
|
||||||
|
tsk.completed.replace(tzinfo=timezone.utc)
|
||||||
|
if tsk.completed is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
tsk.due.replace(tzinfo=timezone.utc) < now(),
|
||||||
tsk.namespace,
|
tsk.namespace,
|
||||||
NPT.EDIT_ALL_TASKS in perms,
|
NPT.EDIT_ALL_TASKS in perms,
|
||||||
NPT.DELETE_ALL_TASKS in perms,
|
NPT.DELETE_ALL_TASKS in perms,
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,160 @@
|
||||||
|
@keyframes task-reward-fade-even {
|
||||||
|
0% {
|
||||||
|
background-color : var(--block-grey-bg);
|
||||||
|
color : var(--block-grey-text);
|
||||||
|
fill : var(--block-grey-text);
|
||||||
|
stroke : var(--block-grey-text);
|
||||||
|
}
|
||||||
|
0.5% {
|
||||||
|
background-color : var(--block-good-bg);
|
||||||
|
color : var(--block-good-text);
|
||||||
|
fill : var(--block-good-text);
|
||||||
|
stroke : var(--block-good-text);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
background-color : var(--block-good-bg);
|
||||||
|
color : var(--block-good-text);
|
||||||
|
fill : var(--block-good-text);
|
||||||
|
stroke : var(--block-good-text);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color : var(--block-grey-bg);
|
||||||
|
color : var(--block-grey-text);
|
||||||
|
fill : var(--block-grey-text);
|
||||||
|
stroke : var(--block-grey-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes task-reward-fade-odd {
|
||||||
|
0% {
|
||||||
|
background-color : var(--block-grey-2-bg);
|
||||||
|
color : var(--block-grey-2-text);
|
||||||
|
fill : var(--block-grey-2-text);
|
||||||
|
stroke : var(--block-grey-2-text);
|
||||||
|
}
|
||||||
|
0.5% {
|
||||||
|
background-color : var(--block-good-2-bg);
|
||||||
|
color : var(--block-good-2-text);
|
||||||
|
fill : var(--block-good-2-text);
|
||||||
|
stroke : var(--block-good-2-text);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
background-color : var(--block-good-2-bg);
|
||||||
|
color : var(--block-good-2-text);
|
||||||
|
fill : var(--block-good-2-text);
|
||||||
|
stroke : var(--block-good-2-text);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color : var(--block-grey-2-bg);
|
||||||
|
color : var(--block-grey-2-text);
|
||||||
|
fill : var(--block-grey-2-text);
|
||||||
|
stroke : var(--block-grey-2-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.list{
|
.list{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
--table-row-even-bg : var(--block-2-bg);
|
||||||
|
--table-row-even-text : var(--block-2-text);
|
||||||
|
--table-row-even-border-width: var(--block-2-border-width);
|
||||||
|
--table-row-even-border-color: var(--block-2-border-color);
|
||||||
|
--table-row-odd-bg : var(--block-3-bg);
|
||||||
|
--table-row-odd-text : var(--block-3-text);
|
||||||
|
--table-row-odd-border-width: var(--block-3-border-width);
|
||||||
|
--table-row-odd-border-color: var(--block-3-border-color);
|
||||||
|
|
||||||
|
tr {
|
||||||
|
|
||||||
|
--table-row-bg : var(--table-row-even-bg);
|
||||||
|
--table-row-text : var(--table-row-even-text);
|
||||||
|
--table-row-border-width : var(--table-row-even-border-width);
|
||||||
|
--table-row-border-color : var(--table-row-even-border-color);
|
||||||
|
|
||||||
|
|
||||||
|
background-color: var(--table-row-bg);
|
||||||
|
color: var(--table-row-text);
|
||||||
|
border: var(--table-row-border-width) solid var(--table-row-border-color);
|
||||||
|
transition: background-color 2s;
|
||||||
|
transition: color 2s;
|
||||||
|
transition: border 2s;
|
||||||
|
transition: all 2s;
|
||||||
|
|
||||||
|
.icon svg {
|
||||||
|
fill: var(--table-row-text);
|
||||||
|
stroke: var(--table-row-text);
|
||||||
|
transition: fill 2s;
|
||||||
|
transition: stroke 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.list{
|
&:nth-child(odd) {
|
||||||
border-collapse: collapse;
|
--table-row-bg : var(--table-row-odd-bg);
|
||||||
|
--table-row-text : var(--table-row-odd-text);
|
||||||
|
--table-row-border-width : var(--table-row-odd-border-width);
|
||||||
|
--table-row-border-color : var(--table-row-odd-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list tr {
|
&.overdue {
|
||||||
background-color: var(--table-row-bg-1);
|
--table-row-even-bg : var(--block-bad-bg);
|
||||||
color: var(--on-table-row);
|
--table-row-even-text : var(--block-bad-text);
|
||||||
|
--table-row-even-border-width: var(--block-bad-border-width);
|
||||||
|
--table-row-even-border-color: var(--block-bad-border-color);
|
||||||
|
--table-row-odd-bg : var(--block-bad-2-bg);
|
||||||
|
--table-row-odd-text : var(--block-bad-2-text);
|
||||||
|
--table-row-odd-border-width: var(--block-bad-2-border-width);
|
||||||
|
--table-row-odd-border-color: var(--block-bad-2-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list tr:nth-child(odd) {
|
&.complete {
|
||||||
background-color: var(--table-row-bg-2);
|
--table-row-even-bg : var(--block-grey-bg);
|
||||||
color: var(--on-table-row);
|
--table-row-even-text : var(--block-grey-text);
|
||||||
|
--table-row-even-border-width : var(--block-grey-border-width);
|
||||||
|
--table-row-even-border-color : var(--block-grey-border-color);
|
||||||
|
--table-row-odd-bg : var(--block-grey-2-bg);
|
||||||
|
--table-row-odd-text : var(--block-grey-2-text);
|
||||||
|
--table-row-odd-border-width : var(--block-grey-2-border-width);
|
||||||
|
--table-row-odd-border-color : var(--block-grey-2-border-color);
|
||||||
|
|
||||||
|
&.just-complete{
|
||||||
|
animation-name: task-reward-fade-even;
|
||||||
|
animation-duration: 300s;
|
||||||
|
|
||||||
|
.checkbox .icon svg {
|
||||||
|
animation-name: task-reward-fade-even;
|
||||||
|
animation-duration: 300s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list .task-header-row {
|
&:nth-child(odd) {
|
||||||
|
animation-name: task-reward-fade-odd;
|
||||||
|
|
||||||
|
.checkbox .icon svg {
|
||||||
|
animation-name: task-reward-fade-odd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-table-row {
|
||||||
|
&.complete {
|
||||||
|
font-style: italic;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-header-row {
|
||||||
border-bottom: 8px solid var(--bg);
|
border-bottom: 8px solid var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list td, .list th{
|
td, th{
|
||||||
border-right: 4px solid var(--bg);
|
border-right: 4px solid var(--bg);
|
||||||
border-left: 4px solid var(--bg);
|
border-left: 4px solid var(--bg);
|
||||||
|
border-top: var(--table-row-border-width) solid var(--table-row-border-color);
|
||||||
|
border-bottom: var(--table-row-border-width) solid var(--table-row-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list td {
|
td {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
|
|
||||||
&.nopad {
|
&.nopad {
|
||||||
|
|
@ -37,77 +166,82 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list .detail-view {
|
.detail-view {
|
||||||
display: none;
|
display: none;
|
||||||
border-bottom: 4px solid var(--bg);
|
border-bottom: 4px solid var(--bg);
|
||||||
border-top: 1px solid var(--bg);
|
border-top: 1px solid var(--bg);
|
||||||
}
|
|
||||||
|
|
||||||
.list .detail-view-elem {
|
&.shown {
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list .detail-view.shown {
|
|
||||||
display: table-row;
|
display: table-row;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.list .detail-view-elem .small-details {
|
.detail-view-elem {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.small-details {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list .detail-view-elem hr {
|
hr {
|
||||||
color: var(--on-table-row);
|
color: var(--table-row-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list .detail-view-elem .detail-view-header h1 {
|
.detail-view-header {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
h1 {
|
||||||
margin: 0 1rem;
|
margin: 0 1rem;
|
||||||
font-size: larger;
|
font-size: larger;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list .detail-view-elem .detail-view-header {
|
&::before {
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list .detail-view-elem .detail-view-header::before{
|
|
||||||
content: "";
|
content: "";
|
||||||
flex: 1 1;
|
flex: 1 1;
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
60deg,
|
60deg,
|
||||||
|
|
||||||
var(--on-table-row) 0,
|
var(--table-row-text) 0,
|
||||||
var(--on-table-row) 0.25rem,
|
var(--table-row-text) 0.25rem,
|
||||||
|
|
||||||
transparent 0.35rem,
|
transparent 0.35rem,
|
||||||
transparent 1.0rem,
|
transparent 1.0rem,
|
||||||
|
|
||||||
var(--on-table-row) 1.1rem
|
var(--table-row-text) 1.1rem
|
||||||
);
|
);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 2rem;
|
max-width: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list .detail-view-elem .detail-view-header::after{
|
&::after{
|
||||||
content: "";
|
content: "";
|
||||||
flex: 1 1;
|
flex: 1 1;
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
60deg,
|
60deg,
|
||||||
|
|
||||||
var(--on-table-row) 0,
|
var(--table-row-text) 0,
|
||||||
var(--on-table-row) 0.25rem,
|
var(--table-row-text) 0.25rem,
|
||||||
|
|
||||||
transparent 0.35rem,
|
transparent 0.35rem,
|
||||||
transparent 1.0rem,
|
transparent 1.0rem,
|
||||||
|
|
||||||
var(--on-table-row) 1.1rem
|
var(--table-row-text) 1.1rem
|
||||||
);
|
);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.list .detail-view-elem .main-description {
|
.main-description {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.list{
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,52 @@
|
||||||
--body-font: sans-serif;
|
--body-font: sans-serif;
|
||||||
--footer-font: sans-serif;
|
--footer-font: sans-serif;
|
||||||
|
|
||||||
|
--block-2-bg: #331826;
|
||||||
|
--block-2-border-color: var(--block-2-bg);
|
||||||
|
--block-2-text: #ffc4d1;
|
||||||
|
--block-2-border-width: 0;
|
||||||
|
|
||||||
|
--block-3-bg: #462837;
|
||||||
|
--block-3-border-color: var(--block-3-bg);
|
||||||
|
--block-3-text: #ffc4d1;
|
||||||
|
--block-3-border-width: 0;
|
||||||
|
|
||||||
|
--block-neutral-border-width : 0;
|
||||||
|
--block-neutral-2-border-width : 0;
|
||||||
|
--block-neutral-bg : #182633;
|
||||||
|
--block-neutral-border-color : var(--block-neutral-bg);
|
||||||
|
--block-neutral-text : #c4ecff;
|
||||||
|
--block-neutral-2-bg : #283e46;
|
||||||
|
--block-neutral-2-border-color : var(--block-neutral-2-bg);
|
||||||
|
--block-neutral-2-text : #c4e0ff;
|
||||||
|
|
||||||
|
--block-grey-border-width : 0;
|
||||||
|
--block-grey-2-border-width : 0;
|
||||||
|
--block-grey-bg : #262726;
|
||||||
|
--block-grey-border-color : var(--block-grey-bg);
|
||||||
|
--block-grey-text : #b6b6b6;
|
||||||
|
--block-grey-2-bg : #484948;
|
||||||
|
--block-grey-2-border-color : var(--block-grey-2-bg);
|
||||||
|
--block-grey-2-text : #a8a8a8;
|
||||||
|
|
||||||
|
--block-good-border-width : 0;
|
||||||
|
--block-good-2-border-width : 0;
|
||||||
|
--block-good-bg : #003f1c;
|
||||||
|
--block-good-border-color : var(--block-good-bg);
|
||||||
|
--block-good-text : #c4ffce;
|
||||||
|
--block-good-2-bg : #005333;
|
||||||
|
--block-good-2-border-color : var(--block-good-2-bg);
|
||||||
|
--block-good-2-text : #c4ffce;
|
||||||
|
|
||||||
|
--block-bad-border-width : 1px;
|
||||||
|
--block-bad-2-border-width : 1px;
|
||||||
|
--block-bad-bg : #360a0a;
|
||||||
|
--block-bad-border-color : #fd1c42;
|
||||||
|
--block-bad-text : #ff798f;
|
||||||
|
--block-bad-2-bg : #4b0d0d;
|
||||||
|
--block-bad-2-border-color : #fd1c42;
|
||||||
|
--block-bad-2-text : #ffa7b5;
|
||||||
|
|
||||||
--table-row-bg-1: #331826;
|
--table-row-bg-1: #331826;
|
||||||
--table-row-bg-2: #462837;
|
--table-row-bg-2: #462837;
|
||||||
--on-table-row: #ffc4d1;
|
--on-table-row: #ffc4d1;
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,10 @@ function update_reltimes(){
|
||||||
).forEach((el) => {
|
).forEach((el) => {
|
||||||
const time = new Date(Number(el.dataset.timestamp)*1000)
|
const time = new Date(Number(el.dataset.timestamp)*1000)
|
||||||
const delta = time - Date.now()
|
const delta = time - Date.now()
|
||||||
|
const tr = el.parentElement.parentElement.parentElement
|
||||||
|
if(delta < 0 && !tr.classList.contains('overdue')){
|
||||||
|
tr.classList.add('overdue')
|
||||||
|
}
|
||||||
el.textContent = sophontize(delta, time)
|
el.textContent = sophontize(delta, time)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro inline_task(task, list_id=0) %}
|
{% macro inline_task(task, list_id=0) %}
|
||||||
<tr class="task-table-row" id="{{ tlist_cid('row', task.id, list_id) }}">
|
<tr class="task-table-row{{' overdue' if task.overdue else ''}}{{' complete' if task.complete else ''}}{{' just-complete' if task.just_complete else ''}}" id="{{ tlist_cid('row', task.id, list_id) }}" style="{{task.delay_style}}">
|
||||||
<td class="checkbox" id="{{ tlist_cid('check', task.id, list_id) }}">
|
<td class="checkbox" id="{{ tlist_cid('check', task.id, list_id) }}">
|
||||||
{% if task.complete %}
|
{% if task.complete %}
|
||||||
{% if task.can_uncomplete %}
|
{% if task.can_uncomplete %}
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr></tr> <!-- placeholder for CSS styling -->
|
<tr></tr> <!-- placeholder for CSS styling -->
|
||||||
<tr class="detail-view tl-{{ list_id }}" id="{{ tlist_cid('task-detail', task.id, list_id) }}">
|
<tr class="detail-view tl-{{ list_id }}{{' overdue' if task.overdue else ''}}{{' complete' if task.complete else ''}}{{' just-complete' if task.just_complete else ''}}" id="{{ tlist_cid('task-detail', task.id, list_id) }}" style="{{task.delay_style}}">
|
||||||
<td class="detail-view-elem" colspan=3>
|
<td class="detail-view-elem" colspan=3>
|
||||||
<div class="detail-view-header">
|
<div class="detail-view-header">
|
||||||
<h1>Task Details: {{ task.name }}</h1>
|
<h1>Task Details: {{ task.name }}</h1>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ from taskflower.form.task import task_edit_form_for_task, task_form_for_user
|
||||||
from taskflower.sanitize.task import TaskForUser
|
from taskflower.sanitize.task import TaskForUser
|
||||||
from taskflower.types.either import Either, Left, Right, reduce_either
|
from taskflower.types.either import Either, Left, Right, reduce_either
|
||||||
from taskflower.types.option import Option
|
from taskflower.types.option import Option
|
||||||
|
from taskflower.util.time import now
|
||||||
from taskflower.web.errors import (
|
from taskflower.web.errors import (
|
||||||
ResponseErrorNotFound,
|
ResponseErrorNotFound,
|
||||||
response_from_exception
|
response_from_exception
|
||||||
|
|
@ -111,6 +112,7 @@ def complete(id: int):
|
||||||
def _do_complete(tsk: Task) -> Either[Exception, Task]:
|
def _do_complete(tsk: Task) -> Either[Exception, Task]:
|
||||||
try:
|
try:
|
||||||
tsk.complete = True
|
tsk.complete = True
|
||||||
|
tsk.completed = now()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return Right(tsk)
|
return Right(tsk)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -166,6 +168,7 @@ def uncomplete(id: int):
|
||||||
def _do_uncomplete(tsk: Task) -> Either[Exception, Task]:
|
def _do_uncomplete(tsk: Task) -> Either[Exception, Task]:
|
||||||
try:
|
try:
|
||||||
tsk.complete = False
|
tsk.complete = False
|
||||||
|
tsk.completed = None
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return Right(tsk)
|
return Right(tsk)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue