Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
94ee81a
BE: Speed up section minimize
justuswilhelm Feb 14, 2026
481d897
BE: Optimize SVG icon usage
justuswilhelm Feb 14, 2026
ae836ce
BE: Remove old comments
justuswilhelm Feb 14, 2026
694dcf1
BE: Show sub task progress in project ashboard
justuswilhelm Feb 14, 2026
4e61fea
BE: Add sub task empty state for task views
justuswilhelm Feb 14, 2026
389aed6
BE: Fix task view translations
justuswilhelm Feb 14, 2026
ae077f5
BE: Improve onboarding screens
justuswilhelm Feb 14, 2026
b145b63
BE: Fix missing task assignee in onboarding
justuswilhelm Feb 14, 2026
6c4e274
BE: Implement project list minimize
justuswilhelm Feb 14, 2026
aa5a557
BE: Fix type issue
justuswilhelm Feb 14, 2026
256a9a7
BE: Remove one if branch in selector
justuswilhelm Feb 14, 2026
13c8e34
BE: Assert request methods
justuswilhelm Feb 14, 2026
039c0d0
BE: Optimize user context and ws/proj visiting
justuswilhelm Feb 14, 2026
c589d6e
BE: Rename selector
justuswilhelm Feb 14, 2026
86168a3
BE: Add label/member list minimize
justuswilhelm Feb 14, 2026
1be8184
BE: Improve project view handling
justuswilhelm Feb 14, 2026
baef873
BE: Improve project view
justuswilhelm Feb 14, 2026
8950da4
BE: Show that section is empty after filtering
justuswilhelm Feb 14, 2026
a512ffc
BE: Improve section minimize
justuswilhelm Feb 14, 2026
1705e21
BE: Improve task detail empty states
justuswilhelm Feb 14, 2026
8538e2b
BE: Fix missing task count
justuswilhelm Feb 14, 2026
7e12d91
BE: Refactor task details
justuswilhelm Feb 14, 2026
8165715
BE: Update landing page copy
justuswilhelm Feb 15, 2026
f9833cc
BE: Improve landing HTML
justuswilhelm Feb 15, 2026
47db691
BE: Fix spelling issues
justuswilhelm Feb 15, 2026
5257466
BE: Refactor onboarding about you
justuswilhelm Feb 15, 2026
baab4bc
Bring back alpine
justuswilhelm Feb 15, 2026
37c66df
BE: Improve sidebar
justuswilhelm Feb 15, 2026
71e4c32
BE: Show task count in label options
justuswilhelm Feb 15, 2026
22cb694
BE: Add size argument to icon templatetag
justuswilhelm Feb 15, 2026
9bf527a
BE: Add cute new workspace preview
justuswilhelm Feb 15, 2026
8aecf40
BE: Add cute project title preview
justuswilhelm Feb 15, 2026
50e4f7b
BE: Improve onboarding new task
justuswilhelm Feb 15, 2026
f9084b7
BE: Improve onboarding new label
justuswilhelm Feb 15, 2026
256dbca
BE: Improve onboarding layout
justuswilhelm Feb 15, 2026
3af4d2c
BE: Improve onboarding task assign screen
justuswilhelm Feb 15, 2026
104dc13
BE: Allow disabling debug toolbar in dev
justuswilhelm Feb 15, 2026
a0ecf2a
BE: Fix missing user avatar on now assignee
justuswilhelm Feb 15, 2026
27e5685
BE: Improve filter re-render performance
justuswilhelm Feb 15, 2026
ee786e2
BE: Fix broken test
justuswilhelm Feb 15, 2026
d4573d5
BE: Update section to show spinner
justuswilhelm Feb 15, 2026
d1f26b3
BE: Improve side bar
justuswilhelm Feb 15, 2026
262bc7e
BE: Make sidebar more mobile friendly
justuswilhelm Feb 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions backend/projectify/lib/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2026 JWP Consulting GK
"""Test lib views."""

from django.test.client import Client
from django.urls import reverse

import pytest


class TestColoredIconView:
"""Test the colored_icon view."""

def test_get(self, client: Client) -> None:
"""Test getting an icon. Only test for one color."""
url = reverse(
"colored-icon",
kwargs={"icon": "external_links", "color": "primary"},
)
response = client.get(url)

assert response.status_code == 200
assert response["Content-Type"] == "image/svg+xml"
assert '<svg style="color: #2563EB' in response.content.decode()

@pytest.mark.parametrize(
("icon", "color"),
[
("made-up", "primary"),
("external_links", "wrong-color"),
("made-up", "wrong-color"),
],
)
def test_not_found(self, client: Client, icon: str, color: str) -> None:
"""Test that 404 is returned for invalid icon or color."""
url = reverse(
"colored-icon",
kwargs={"icon": icon, "color": color},
)
assert client.get(url).status_code == 404
42 changes: 40 additions & 2 deletions backend/projectify/lib/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2024 JWP Consulting GK
# SPDX-FileCopyrightText: 2026 JWP Consulting GK
"""View decorators."""

import logging
from pathlib import Path
from typing import Any, Protocol

from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.contrib.staticfiles import finders
from django.http import Http404, HttpRequest, HttpResponse
from django.views.decorators.cache import cache_control

from projectify.lib.types import AuthenticatedHttpRequest

logger = logging.getLogger(__name__)


class LoggedInViewP(Protocol):
"""Django view method that takes an AuthenticatedHttpRequest."""
Expand All @@ -30,3 +36,35 @@ def platform_view(func: LoggedInViewP) -> LoggedInViewP:
logged in.
"""
return login_required(func)


@cache_control(max_age=3600)
def colored_icon(request: HttpRequest, icon: str, color: str) -> HttpResponse:
"""Return a colored SVG icon."""
del request
# See `const colors` projectify/theme/static_src/tailwind.config.js
color_map = {
"primary": "#2563EB",
"destructive": "#dc2626",
"white": "#ffffff",
}

match finders.find(f"heroicons/{icon}.svg"):
case list() as paths:
raise ValueError(
f"Tried to look for icon {icon}, got a list of paths {paths}"
)
case None:
raise Http404(f"Icon '{icon}' not found")
case str() as icon_path:
pass

if color not in color_map:
raise Http404(f"Missing color '{color}' for icon '{icon}'")

svg_content = (
Path(icon_path)
.read_text()
.replace("<svg", f'<svg style="color: {color_map[color]}"')
)
return HttpResponse(svg_content, content_type="image/svg+xml")
43 changes: 30 additions & 13 deletions backend/projectify/onboarding/templates/onboarding/about_you.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,37 @@
<form method="post"
class="col-span-1 flex shrink grow flex-col gap-16 px-12 py-20 pb-8">
{% csrf_token %}
{% include "onboarding/common/form_description.html" with title="About you" text_1="Tell us your preferred name. You can also keep it empty and continue by clicking the button below." %}
<div class="flex flex-col">
<div class="flex flex-col items-start gap-2">{{ form }}</div>
</div>
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<button type="submit"
class="w-full bg-primary text-primary-content hover:bg-primary-hover active:bg-primary-pressed text-base flex min-w-max flex-row justify-center gap-2 rounded-lg px-4 py-2 font-bold disabled:bg-disabled disabled:text-disabled-primary-content">
{% trans "Continue" %}
</button>
</div>
</div>
{% include "onboarding/common/form_description.html" with title=_("About you") text_1=_("Tell us your preferred name. You can also keep it empty and continue by clicking the button below.") %}
{{ form }}
{% include "projectify/forms/submit.html" with text=_("Continue") %}
</form>
<div class="hidden h-full min-w-0 shrink grow flex-col items-center justify-center overflow-hidden bg-background md:flex xl:col-span-2">
<h1 class="w-full overflow-hidden text-ellipsis text-center text-4xl font-bold">{% trans "Welcome👋" %}</h1>
<h1 class="w-full overflow-hidden text-ellipsis text-center text-4xl font-bold">
{% translate "Welcome" as welcome %}
<span id="welcome-message">
{% if user.preferred_name %}
{% blocktrans with name=user.preferred_name %}Welcome, {{ name }}{% endblocktrans %}
{% else %}
{{ welcome }}
{% endif %}
</span>👋
</h1>
</div>
<script>
(() => {
const nameInput = document.querySelector('input[name="preferred_name"]');
if (!nameInput) { return; }
const welcomeMessage = document.getElementById('welcome-message');
if (!welcomeMessage) { return; }
const defaultMessage = '{{ welcome }}';
nameInput.addEventListener('input', (event) => {
const name = event.target.value.trim();
if (!name) {
welcomeMessage.textContent = defaultMessage;
return;
}
welcomeMessage.textContent = `${defaultMessage}, ${name}`;
});
})();
</script>
{% endblock onboarding_content %}
70 changes: 46 additions & 24 deletions backend/projectify/onboarding/templates/onboarding/assign_task.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{# SPDX-License-Identifier: AGPL-3.0-or-later #}
{% extends "onboarding_base.html" %}
{% load i18n %}
{% load projectify %}
{% block title %}
{% blocktrans %}Task "{{ task }}" has been assigned to you - Projectify{% endblocktrans %}
{% endblock title %}
Expand All @@ -13,34 +14,55 @@ <h1 class="min-w-fit max-w-lg text-4xl font-bold">
{% blocktrans %}Task "{{task}}" has been assigned to you!{% endblocktrans %}
</h1>
<div class="flex flex-col gap-2 text-lg">
<p>{% trans "Youre all set!" %}</p>
<p>{% trans "You're all set!" %}</p>
<p>
{% trans "If you wish to add new team members to your workspace, please go to the workspace settings menu next to your workspace name." %}
</p>
<a href="{% url "help:detail" page="billing" %}"
class="text-primary underline hover:text-primary-hover active:text-primary-pressed text-lg"
target="_blank">{% trans "Learn more about workspace billing settings" %}<span class="sr-only">(Opens in new tab)</span>
{% include "heroicons/external_links.svg" %}
</a>
{# TODO:: Use django template url tag #}
<a href="/dashboard/workspace/{{ workspace.uuid }}/settings/billing"
class="text-primary underline hover:text-primary-hover active:text-primary-pressed text-lg"
target="_blank">{% trans "Go to workspace billing setting" %}<span class="sr-only">(Opens in new tab)</span>
{% include "heroicons/external_links.svg" %}
</a>
</div>
</div>
<div class="flex flex-col">
<div class="flex flex-col items-start gap-2">{{ form }}</div>
</div>
<div class="flex flex-col gap-2">
<div class="flex gap-2">
{% anchor href="help:detail" label=_("Learn more about workspace billing settings") external=True page="billing" %}
{% anchor href="dashboard:workspaces:billing" label=_("Go to workspace billing settings") external=True workspace_uuid=workspace.uuid %}
</div>
</div>
<a href="{% url "dashboard:projects:detail" project.uuid %}"
class="w-full bg-primary text-primary-content hover:bg-primary-hover active:bg-primary-pressed text-base flex min-w-max flex-row justify-center gap-2 rounded-lg px-4 py-2 font-bold disabled:bg-disabled disabled:text-disabled-primary-content">{% trans "Get started" %}</a>
{% include "onboarding/common/step_counter.html" with step=5 step_count="5" %}
</div>
<div class="hidden h-full min-w-0 shrink grow flex-col items-center justify-center overflow-hidden bg-background md:flex xl:col-span-2 p-4">
<div class="ring-4 max-w-full flex flex-row ring-border bg-foreground py-2"
inert>
<div class="flex flex-col gap-2">
<div class="mx-4 px-4 flex items-center gap-2 font-bold border rounded p-2">
{% icon "briefcase" size=4 %}
<span class="max-w-16 truncate">{{ task.workspace.title }}</span>
</div>
<div class="mx-4 flex flex-row items-center justify-start text-utility gap-2">
{% icon "folder" size=4 %}
<div class="font-bold mr-auto">{% trans "Projects" %}</div>
{% icon "chevron-up" size=4 %}
</div>
<div class="flex w-full flex-row items-center gap-1 px-4 py-1">
{% icon "folder" size=4 %}
<div class="max-w-16 line-clamp-1 min-w-0 font-bold">{{ task.section.project.title }}</div>
</div>
</div>
<div class="min-w-0 shrink flex flex-col px-4">
<div class="flex flex-row items-center text-base-content min-w-0 gap-4 py-2">
{% icon "chevron-down" size=4 %}
<h1 class="line-clamp-1 min-w-0 shrink font-bold">{{ task.section.title }}</h1>
</div>
<div class="p-4 rounded-lg border border-border flex flex-col gap-2">
<div class="flex flex-row items-center gap-1 sm:gap-6">
<span class="shrink-0 font-bold">#1</span>
<span id="task-title" class="text-ellipsis line-clamp-3">{{ task.title }}</span>
</div>
<div class="flex flex-row items-center justify-start flex-wrap gap-2">
<div id="label-preview"
class="truncate rounded-full px-3 py-1 font-bold bg-label-orange text-label-text-orange">
{{ task.labels.first.name }}
</div>
{% user_avatar task.assignee %}
</div>
</div>
</div>
</div>
</div>
</div>
{% include "onboarding/common/step_counter.html" with step=5 step_count="5" %}
</div>
<div class="hidden h-full min-w-0 shrink grow flex-col items-center justify-center overflow-hidden bg-background md:flex xl:col-span-2">
</div>
{% endblock onboarding_content %}
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
{# SPDX-FileCopyrightText: 2025 JWP Consulting GK #}
{# SPDX-License-Identifier: AGPL-3.0-or-later #}
{% load i18n %}
{% load projectify %}
<div class="flex flex-col gap-4">
<h1 class="min-w-fit max-w-lg text-4xl font-bold">{% trans title %}</h1>
<h1 class="min-w-fit max-w-lg text-4xl font-bold">{{ title }}</h1>
<div class="flex flex-col gap-2 text-lg">
<div class="flex flex-col gap-3">
<p>{% trans text_1 %}</p>
{% if text_2 %}
<p>{% trans text_2 %}</p>
{% endif %}
<p>{{ text_1 }}</p>
{% if text_2 %}<p>{{ text_2 }}</p>{% endif %}
{% if href %}
{% if target_blank %}
<a href="{{ href }}"
class="text-primary underline hover:text-primary-hover active:text-primary-pressed text-lg"
target="_blank">{% trans href_label %}<span class="sr-only">{% trans "(Opens in new tab)" %}</span>
{% include "heroicons/external_links.svg" %}
</a>
{% else %}
<a href="{{ href }}"
class="text-primary underline hover:text-primary-hover active:text-primary-pressed text-lg">{% trans href_label %}</a>
{% endif %}
{% endif %}
</div>
</div>
<p>{% anchor href href_label external=target_blank %}</p>
{% endif %}
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{% with ''|center:step_count as range %}
{% for _ in range %}
{% if forloop.counter <= step %}
<li class="h-4 w-4 rounded bg-background bg-primary"></li>
<li class="h-4 w-4 rounded bg-primary"></li>
{% else %}
<li class="h-4 w-4 rounded bg-background"></li>
{% endif %}
Expand Down
78 changes: 61 additions & 17 deletions backend/projectify/onboarding/templates/onboarding/new_label.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{# SPDX-FileCopyrightText: 2025 JWP Consulting GK #}
{# SPDX-FileCopyrightText: 2025-2026 JWP Consulting GK #}
{# SPDX-License-Identifier: AGPL-3.0-or-later #}
{% extends "onboarding_base.html" %}
{% load i18n %}
{% load projectify %}
{% block title %}
{% blocktrans %}Create a label for "{{ task }}" - Projectify{% endblocktrans %}
{% endblock title %}
Expand All @@ -10,24 +11,67 @@
class="col-span-1 flex shrink grow flex-col gap-16 px-12 py-20 pb-8">
{% csrf_token %}
<div class="flex flex-col gap-4">
<h1 class="min-w-fit max-w-lg text-4xl font-bold">{% blocktrans %}Create a label for "{{task}}"{% endblocktrans %}</h1>
<div class="flex flex-col gap-2 text-lg">
<p>{% trans "Labels help you to filter between the types of tasks." %}</p>
</div>
</div>
<div class="flex flex-col">
<div class="flex flex-col items-start gap-2">{{ form }}</div>
</div>
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<button type="submit"
class="w-full bg-primary text-primary-content hover:bg-primary-hover active:bg-primary-pressed text-base flex min-w-max flex-row justify-center gap-2 rounded-lg px-4 py-2 font-bold disabled:bg-disabled disabled:text-disabled-primary-content">
{% trans "Continue" %}
</button>
</div>
<h1 class="min-w-0 text-4xl font-bold overflow-hidden">
{% blocktrans %}Create a label for "{{task}}"{% endblocktrans %}
</h1>
<p class="flex flex-col gap-2 text-lg">{% trans "Labels help you to filter between the types of tasks." %}</p>
</div>
{{ form }}
{% include "projectify/forms/submit.html" with text=_("Continue") %}
{% include "onboarding/common/step_counter.html" with step=4 step_count="5" %}
</form>
<div class="hidden h-full min-w-0 shrink grow flex-col items-center justify-center overflow-hidden bg-background md:flex xl:col-span-2">
<div class="hidden h-full min-w-0 shrink grow flex-col items-center justify-center overflow-hidden bg-background md:flex xl:col-span-2 p-4">
<div class="ring-4 max-w-full flex flex-row ring-border bg-foreground py-2"
inert>
<div class="flex flex-col gap-2">
<div class="mx-4 px-4 flex items-center gap-2 font-bold border rounded p-2">
{% icon "briefcase" size=4 %}
<span class="max-w-16 truncate">{{ task.workspace.title }}</span>
</div>
<div class="mx-4 flex flex-row items-center justify-start text-utility gap-2">
{% icon "folder" size=4 %}
<div class="font-bold mr-auto">{% trans "Projects" %}</div>
{% icon "chevron-up" size=4 %}
</div>
<div class="flex w-full flex-row items-center gap-1 px-4 py-1">
{% icon "folder" size=4 %}
<div class="max-w-16 line-clamp-1 min-w-0 font-bold">{{ task.section.project.title }}</div>
</div>
</div>
<div class="min-w-0 shrink flex flex-col px-4">
<div class="flex flex-row items-center text-base-content min-w-0 gap-4 py-2">
{% icon "chevron-down" size=4 %}
<h1 class="line-clamp-1 min-w-0 shrink font-bold">{{ task.section.title }}</h1>
</div>
<div class="p-4 rounded-lg border border-border flex flex-col gap-2">
<div class="flex flex-row items-center gap-1 sm:gap-6">
<span class="shrink-0 font-bold">#1</span>
<span id="task-title" class="text-ellipsis line-clamp-3">{{ task.title }}</span>
</div>
{% translate "Your label" as label_title_default %}
<div id="label-preview"
class="truncate rounded-full px-3 py-1 font-bold bg-label-orange text-label-text-orange">
{{ label_title_default }}
</div>
</div>
</div>
</div>
</div>
<script>
(() => {
const nameInput = document.querySelector('input[name="name"]');
if (!nameInput) { return; }
const labelPreview = document.getElementById('label-preview');
if (!labelPreview) { return; }
const defaultTitle = '{% trans "Your label" %}';
nameInput.addEventListener('input', (event) => {
const name = event.target.value.trim();
if (!name) {
labelPreview.textContent = defaultTitle;
return;
}
labelPreview.textContent = name;
});
})();
</script>
{% endblock onboarding_content %}
Loading
Loading