mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-11-13 12:23:44 +01:00
Merge pull request #613 from eitchtee/feat/transaction-convert-menu
feat(transactions): convert a transaction into a quick transaction, recurring transaction or installment plan
This commit is contained in:
10 files changed
+462
-77
No files matched your search
@@ -887,11 +887,17 @@ class InstallmentPlanForm(forms.ModelForm):
|
||||
),
|
||||
)
|
||||
|
||||
def save(self, **kwargs):
|
||||
def save(self, source_transaction=None, **kwargs):
|
||||
is_new = not self.instance.id
|
||||
|
||||
instance = super().save(**kwargs)
|
||||
if is_new:
|
||||
if is_new and source_transaction:
|
||||
# The converted transaction becomes the first installment
|
||||
source_transaction.installment_plan = instance
|
||||
source_transaction.installment_id = instance.installment_start
|
||||
source_transaction.save()
|
||||
instance.update_transactions()
|
||||
elif is_new:
|
||||
instance.create_transactions()
|
||||
else:
|
||||
instance.update_transactions()
|
||||
@@ -1145,12 +1151,12 @@ class RecurringTransactionForm(forms.ModelForm):
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def save(self, **kwargs):
|
||||
def save(self, source_transaction=None, **kwargs):
|
||||
is_new = not self.instance.id
|
||||
|
||||
instance = super().save(**kwargs)
|
||||
if is_new:
|
||||
instance.create_upcoming_transactions()
|
||||
instance.create_upcoming_transactions(first_transaction=source_transaction)
|
||||
else:
|
||||
instance.update_unpaid_transactions()
|
||||
instance.generate_upcoming_transactions()
|
||||
|
||||
@@ -915,7 +915,7 @@ class RecurringTransaction(models.Model):
|
||||
instance = super().save(*args, **kwargs)
|
||||
return instance
|
||||
|
||||
def create_upcoming_transactions(self):
|
||||
def create_upcoming_transactions(self, first_transaction=None):
|
||||
current_date = self.start_date
|
||||
reference_date = self.reference_date
|
||||
end_date = min(
|
||||
@@ -925,6 +925,12 @@ class RecurringTransaction(models.Model):
|
||||
timezone.now().date() + (self.get_recurrence_delta() * self.keep_at_most),
|
||||
)
|
||||
|
||||
# When converting an existing transaction, it becomes the first occurrence
|
||||
if first_transaction:
|
||||
self.adopt_transaction(first_transaction, current_date, reference_date)
|
||||
current_date = self.get_next_date(current_date)
|
||||
reference_date = self.get_next_date(reference_date)
|
||||
|
||||
while current_date <= end_date:
|
||||
self.create_transaction(current_date, reference_date)
|
||||
current_date = self.get_next_date(current_date)
|
||||
@@ -959,6 +965,25 @@ class RecurringTransaction(models.Model):
|
||||
created_transaction.tags.set(self.tags(manager="all_objects").all())
|
||||
created_transaction.entities.set(self.entities(manager="all_objects").all())
|
||||
|
||||
def adopt_transaction(self, existing_transaction, date, reference_date):
|
||||
existing_transaction.account = self.account
|
||||
existing_transaction.type = self.type
|
||||
existing_transaction.date = date
|
||||
existing_transaction.reference_date = reference_date.replace(day=1)
|
||||
existing_transaction.description = (
|
||||
self.description if self.add_description_to_transaction else ""
|
||||
)
|
||||
existing_transaction.category = self.category
|
||||
existing_transaction.notes = self.notes if self.add_notes_to_transaction else ""
|
||||
existing_transaction.recurring_transaction = self
|
||||
|
||||
if not existing_transaction.is_paid: # Don't update value for paid transactions
|
||||
existing_transaction.amount = self.amount
|
||||
|
||||
existing_transaction.save()
|
||||
existing_transaction.tags.set(self.tags(manager="all_objects").all())
|
||||
existing_transaction.entities.set(self.entities(manager="all_objects").all())
|
||||
|
||||
def get_recurrence_delta(self):
|
||||
if self.recurrence_type == self.RecurrenceType.DAY:
|
||||
return relativedelta(days=self.recurrence_interval)
|
||||
|
||||
@@ -2,11 +2,15 @@ from datetime import date
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.accounts.models import Account, AccountGroup
|
||||
from apps.currencies.models import Currency
|
||||
from apps.transactions.models import (
|
||||
InstallmentPlan,
|
||||
QuickTransaction,
|
||||
RecurringTransaction,
|
||||
Transaction,
|
||||
TransactionCategory,
|
||||
TransactionTag,
|
||||
@@ -172,3 +176,154 @@ class TransactionSimpleAddViewTests(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
form = response.context["form"]
|
||||
self.assertEqual(form.initial.get("category"), self.category.id)
|
||||
|
||||
|
||||
@override_settings(
|
||||
STORAGES={
|
||||
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||
"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
|
||||
},
|
||||
WHITENOISE_AUTOREFRESH=True,
|
||||
)
|
||||
class TransactionConvertViewTests(TestCase):
|
||||
"""Tests for converting a transaction into quick/recurring/installment"""
|
||||
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(
|
||||
email="testuser@test.com", password="testpass123"
|
||||
)
|
||||
self.client.login(username="testuser@test.com", password="testpass123")
|
||||
|
||||
self.currency = Currency.objects.create(
|
||||
code="USD", name="US Dollar", decimal_places=2, prefix="$ "
|
||||
)
|
||||
self.account = Account.objects.create(
|
||||
name="Test Account", currency=self.currency
|
||||
)
|
||||
self.category = TransactionCategory.objects.create(name="Test Category")
|
||||
self.tag = TransactionTag.objects.create(name="TestTag")
|
||||
|
||||
self.transaction = Transaction.objects.create(
|
||||
account=self.account,
|
||||
type=Transaction.Type.EXPENSE,
|
||||
date=date(2030, 1, 15),
|
||||
amount=50,
|
||||
description="Gym",
|
||||
category=self.category,
|
||||
is_paid=True,
|
||||
)
|
||||
self.transaction.tags.add(self.tag)
|
||||
|
||||
def _get(self, url_name):
|
||||
return self.client.get(
|
||||
reverse(url_name, kwargs={"transaction_id": self.transaction.id}),
|
||||
HTTP_HX_REQUEST="true",
|
||||
)
|
||||
|
||||
def _post(self, url_name, data):
|
||||
return self.client.post(
|
||||
reverse(url_name, kwargs={"transaction_id": self.transaction.id}),
|
||||
data,
|
||||
HTTP_HX_REQUEST="true",
|
||||
)
|
||||
|
||||
def test_quick_transaction_form_is_prefilled_without_name(self):
|
||||
response = self._get("transaction_convert_to_quick_transaction")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
initial = response.context["form"].initial
|
||||
self.assertEqual(initial["description"], "Gym")
|
||||
self.assertEqual(initial["tags"], ["TestTag"])
|
||||
self.assertNotIn("name", initial)
|
||||
|
||||
def test_quick_transaction_requires_name_and_keeps_transaction(self):
|
||||
data = {
|
||||
"account": self.account.id,
|
||||
"type": "EX",
|
||||
"amount": "50",
|
||||
"description": "Gym",
|
||||
}
|
||||
response = self._post("transaction_convert_to_quick_transaction", data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(QuickTransaction.objects.exists())
|
||||
|
||||
response = self._post(
|
||||
"transaction_convert_to_quick_transaction", {**data, "name": "Gym"}
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertTrue(QuickTransaction.objects.filter(name="Gym").exists())
|
||||
self.assertTrue(Transaction.objects.filter(id=self.transaction.id).exists())
|
||||
|
||||
def test_recurring_transaction_adopts_original_as_first_occurrence(self):
|
||||
response = self._post(
|
||||
"transaction_convert_to_recurring_transaction",
|
||||
{
|
||||
"account": self.account.id,
|
||||
"type": "EX",
|
||||
"amount": "60",
|
||||
"description": "Gym",
|
||||
"category": self.category.id,
|
||||
"tags": ["TestTag"],
|
||||
"start_date": "2030-01-15",
|
||||
"recurrence_type": "month",
|
||||
"recurrence_interval": 1,
|
||||
"keep_at_most": 6,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
recurring = RecurringTransaction.all_objects.get()
|
||||
self.transaction.refresh_from_db()
|
||||
self.assertEqual(self.transaction.recurring_transaction, recurring)
|
||||
# Paid amount is preserved
|
||||
self.assertEqual(self.transaction.amount, 50)
|
||||
self.assertEqual(
|
||||
recurring.transactions.filter(date=date(2030, 1, 15)).count(), 1
|
||||
)
|
||||
self.assertEqual(recurring.last_generated_date, date(2030, 1, 15))
|
||||
|
||||
def test_installment_plan_adopts_original_as_first_installment(self):
|
||||
response = self._post(
|
||||
"transaction_convert_to_installment_plan",
|
||||
{
|
||||
"account": self.account.id,
|
||||
"type": "EX",
|
||||
"installment_amount": "50",
|
||||
"description": "TV",
|
||||
"add_description_to_transaction": "on",
|
||||
"number_of_installments": 3,
|
||||
"installment_start": 1,
|
||||
"start_date": "2030-01-15",
|
||||
"recurrence": "monthly",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
plan = InstallmentPlan.all_objects.get()
|
||||
self.transaction.refresh_from_db()
|
||||
self.assertEqual(self.transaction.installment_plan, plan)
|
||||
self.assertEqual(self.transaction.installment_id, 1)
|
||||
self.assertTrue(self.transaction.is_paid)
|
||||
self.assertEqual(self.transaction.description, "TV")
|
||||
self.assertEqual(plan.transactions.count(), 3)
|
||||
|
||||
def test_linked_transaction_cannot_be_converted_into_plan(self):
|
||||
self._post(
|
||||
"transaction_convert_to_installment_plan",
|
||||
{
|
||||
"account": self.account.id,
|
||||
"type": "EX",
|
||||
"installment_amount": "50",
|
||||
"description": "TV",
|
||||
"number_of_installments": 2,
|
||||
"installment_start": 1,
|
||||
"start_date": "2030-01-15",
|
||||
"recurrence": "monthly",
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
self._get("transaction_convert_to_recurring_transaction").status_code, 404
|
||||
)
|
||||
self.assertEqual(
|
||||
self._get("transaction_convert_to_installment_plan").status_code, 404
|
||||
)
|
||||
@@ -398,8 +398,18 @@ urlpatterns = [
|
||||
name="quick_transaction_add_as_transaction",
|
||||
),
|
||||
path(
|
||||
"transactions/<int:transaction_id>/add-as-quick-transaction/",
|
||||
views.quick_transaction_add_as_quick_transaction,
|
||||
name="quick_transaction_add_as_quick_transaction",
|
||||
"transaction/<int:transaction_id>/convert/quick-transaction/",
|
||||
views.transaction_convert_to_quick_transaction,
|
||||
name="transaction_convert_to_quick_transaction",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/convert/recurring-transaction/",
|
||||
views.transaction_convert_to_recurring_transaction,
|
||||
name="transaction_convert_to_recurring_transaction",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/convert/installment-plan/",
|
||||
views.transaction_convert_to_installment_plan,
|
||||
name="transaction_convert_to_installment_plan",
|
||||
),
|
||||
]
|
||||
@@ -6,3 +6,4 @@ from .actions import *
|
||||
from .installment_plans import *
|
||||
from .recurring_transactions import *
|
||||
from .quick_transactions import *
|
||||
from .conversions import *
|
||||
@@ -0,0 +1,159 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import transaction as db_transaction
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
from apps.transactions.forms import (
|
||||
InstallmentPlanForm,
|
||||
QuickTransactionForm,
|
||||
RecurringTransactionForm,
|
||||
)
|
||||
from apps.transactions.models import Transaction
|
||||
|
||||
|
||||
def _initial_from_transaction(transaction: Transaction):
|
||||
return {
|
||||
"account": transaction.account_id,
|
||||
"type": transaction.type,
|
||||
"description": transaction.description,
|
||||
"notes": transaction.notes,
|
||||
"category": transaction.category_id,
|
||||
"tags": list(transaction.tags.values_list("name", flat=True)),
|
||||
"entities": list(transaction.entities.values_list("name", flat=True)),
|
||||
}
|
||||
|
||||
|
||||
def _get_unlinked_transaction(transaction_id):
|
||||
# Transactions already generated by a plan or recurrence can't be converted again
|
||||
return get_object_or_404(
|
||||
Transaction,
|
||||
id=transaction_id,
|
||||
installment_plan__isnull=True,
|
||||
recurring_transaction__isnull=True,
|
||||
)
|
||||
|
||||
|
||||
def _render_convert_form(request, form, title, transaction_id, url_name):
|
||||
return render(
|
||||
request,
|
||||
"transactions/fragments/convert.html",
|
||||
{
|
||||
"form": form,
|
||||
"title": title,
|
||||
"form_url": reverse(url_name, kwargs={"transaction_id": transaction_id}),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def transaction_convert_to_quick_transaction(request, transaction_id):
|
||||
transaction = get_object_or_404(Transaction, id=transaction_id)
|
||||
|
||||
if request.method == "POST":
|
||||
form = QuickTransactionForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Item added successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
||||
)
|
||||
else:
|
||||
form = QuickTransactionForm(
|
||||
initial={
|
||||
**_initial_from_transaction(transaction),
|
||||
"is_paid": transaction.is_paid,
|
||||
"amount": transaction.amount,
|
||||
"mute": transaction.mute,
|
||||
}
|
||||
)
|
||||
|
||||
return _render_convert_form(
|
||||
request,
|
||||
form,
|
||||
_("Convert into quick transaction"),
|
||||
transaction_id,
|
||||
"transaction_convert_to_quick_transaction",
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def transaction_convert_to_recurring_transaction(request, transaction_id):
|
||||
transaction = _get_unlinked_transaction(transaction_id)
|
||||
|
||||
if request.method == "POST":
|
||||
form = RecurringTransactionForm(request.POST)
|
||||
if form.is_valid():
|
||||
with db_transaction.atomic():
|
||||
form.save(source_transaction=transaction)
|
||||
messages.success(request, _("Recurring Transaction added successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
||||
)
|
||||
else:
|
||||
form = RecurringTransactionForm(
|
||||
initial={
|
||||
**_initial_from_transaction(transaction),
|
||||
"amount": transaction.amount,
|
||||
"start_date": transaction.date,
|
||||
"reference_date": transaction.reference_date,
|
||||
"recurrence_interval": 1,
|
||||
"recurrence_type": "month",
|
||||
}
|
||||
)
|
||||
|
||||
return _render_convert_form(
|
||||
request,
|
||||
form,
|
||||
_("Convert into recurring transaction"),
|
||||
transaction_id,
|
||||
"transaction_convert_to_recurring_transaction",
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def transaction_convert_to_installment_plan(request, transaction_id):
|
||||
transaction = _get_unlinked_transaction(transaction_id)
|
||||
|
||||
if request.method == "POST":
|
||||
form = InstallmentPlanForm(request.POST)
|
||||
if form.is_valid():
|
||||
with db_transaction.atomic():
|
||||
form.save(source_transaction=transaction)
|
||||
messages.success(request, _("Installment Plan added successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
||||
)
|
||||
else:
|
||||
form = InstallmentPlanForm(
|
||||
initial={
|
||||
**_initial_from_transaction(transaction),
|
||||
"installment_amount": transaction.amount,
|
||||
"start_date": transaction.date,
|
||||
"reference_date": transaction.reference_date,
|
||||
}
|
||||
)
|
||||
|
||||
return _render_convert_form(
|
||||
request,
|
||||
form,
|
||||
_("Convert into installment plan"),
|
||||
transaction_id,
|
||||
"transaction_convert_to_installment_plan",
|
||||
)
|
||||
@@ -162,70 +162,3 @@ def quick_transaction_add_as_transaction(request, quick_transaction_id):
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def quick_transaction_add_as_quick_transaction(request, transaction_id):
|
||||
transaction: Transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||
|
||||
if (
|
||||
transaction.description
|
||||
and QuickTransaction.objects.filter(
|
||||
name__startswith=transaction.description
|
||||
).exists()
|
||||
) or QuickTransaction.objects.filter(
|
||||
name__startswith=_("Quick Transaction")
|
||||
).exists():
|
||||
if transaction.description:
|
||||
count = QuickTransaction.objects.filter(
|
||||
name__startswith=transaction.description
|
||||
).count()
|
||||
qt_name = transaction.description + f" ({count + 1})"
|
||||
else:
|
||||
count = QuickTransaction.objects.filter(
|
||||
name__startswith=_("Quick Transaction")
|
||||
).count()
|
||||
qt_name = _("Quick Transaction") + f" ({count + 1})"
|
||||
else:
|
||||
qt_name = transaction.description or _("Quick Transaction")
|
||||
|
||||
transaction_data = model_to_dict(
|
||||
transaction,
|
||||
exclude=[
|
||||
"id",
|
||||
"name",
|
||||
"owner",
|
||||
"account",
|
||||
"category",
|
||||
"tags",
|
||||
"entities",
|
||||
"date",
|
||||
"reference_date",
|
||||
"installment_plan",
|
||||
"installment_id",
|
||||
"recurring_transaction",
|
||||
"deleted",
|
||||
"deleted_at",
|
||||
"internal_id",
|
||||
],
|
||||
)
|
||||
|
||||
new_quick_transaction = QuickTransaction(**transaction_data)
|
||||
new_quick_transaction.account = transaction.account
|
||||
new_quick_transaction.category = transaction.category
|
||||
|
||||
new_quick_transaction.name = qt_name
|
||||
|
||||
new_quick_transaction.save()
|
||||
new_quick_transaction.tags.set(transaction.tags.all())
|
||||
new_quick_transaction.entities.set(transaction.entities.all())
|
||||
|
||||
messages.success(request, _("Item added successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "toasts",
|
||||
},
|
||||
)
|
||||
@@ -203,9 +203,50 @@
|
||||
hx-target="closest .transaction" hx-swap="outerHTML"><i
|
||||
class="fa-solid fa-eye-slash fa-fw mr-2"></i>{% translate 'Hide from summaries' %}</a></li>
|
||||
{% endif %}
|
||||
<li class="dropdown-submenu">
|
||||
<a href="#" _="on click halt"><i
|
||||
class="fa-solid fa-shuffle fa-fw mr-2"></i>{% translate 'Convert' %}<i
|
||||
class="fa-solid fa-chevron-right fa-fw fa-xs ms-auto"></i></a>
|
||||
<ul class="dropdown-submenu-content menu">
|
||||
<li><a href="#"
|
||||
hx-get="{% url 'quick_transaction_add_as_quick_transaction' transaction_id=transaction.id %}"><i
|
||||
class="fa-solid fa-person-running fa-fw mr-2"></i>{% translate 'Add as quick transaction' %}</a>
|
||||
hx-get="{% url 'transaction_convert_to_quick_transaction' transaction_id=transaction.id %}"
|
||||
hx-target="#generic-offcanvas" hx-swap="innerHTML"><i
|
||||
class="fa-solid fa-person-running fa-fw mr-2"></i>{% translate 'Quick Transaction' %}</a>
|
||||
</li>
|
||||
{% if transaction.installment_plan or transaction.recurring_transaction %}
|
||||
<li class="menu-disabled" aria-disabled="true">
|
||||
<a class="flex items-center">
|
||||
<i class="fa-solid fa-repeat fa-fw mr-2"></i>
|
||||
<div>
|
||||
{% translate 'Recurring Transaction' %}
|
||||
<div
|
||||
class="block text-base-content/60 text-xs font-medium">{% translate 'Already part of a plan or recurrence' %}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-disabled" aria-disabled="true">
|
||||
<a class="flex items-center">
|
||||
<i class="fa-solid fa-divide fa-fw mr-2"></i>
|
||||
<div>
|
||||
{% translate 'Installment Plan' %}
|
||||
<div
|
||||
class="block text-base-content/60 text-xs font-medium">{% translate 'Already part of a plan or recurrence' %}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="#"
|
||||
hx-get="{% url 'transaction_convert_to_recurring_transaction' transaction_id=transaction.id %}"
|
||||
hx-target="#generic-offcanvas" hx-swap="innerHTML"><i
|
||||
class="fa-solid fa-repeat fa-fw mr-2"></i>{% translate 'Recurring Transaction' %}</a>
|
||||
</li>
|
||||
<li><a href="#"
|
||||
hx-get="{% url 'transaction_convert_to_installment_plan' transaction_id=transaction.id %}"
|
||||
hx-target="#generic-offcanvas" hx-swap="innerHTML"><i
|
||||
class="fa-solid fa-divide fa-fw mr-2"></i>{% translate 'Installment Plan' %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<hr class="my-1 hr">
|
||||
<li><a href="#"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends 'extends/offcanvas.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form hx-post="{{ form_url }}" hx-target="#generic-offcanvas" novalidate>
|
||||
{% crispy form %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -402,3 +402,48 @@ $breakpoints: (
|
||||
--dropdown-link-disabled-color: #adb5bd;
|
||||
--dropdown-header-color: #adb5bd;
|
||||
}
|
||||
|
||||
// Flyout submenu inside a dropdown menu, opened on hover/focus
|
||||
.dropdown-submenu {
|
||||
position: relative;
|
||||
// Block layout (not daisyUI's wrapping flex column), so the inline submenu's
|
||||
// height is measured at its real width instead of a near-zero fit-content one
|
||||
display: block;
|
||||
|
||||
> .dropdown-submenu-content {
|
||||
display: none;
|
||||
// Inline (mobile) submenu fits the menu's current width instead of widening it
|
||||
contain: inline-size;
|
||||
width: auto; // stretch to the parent item (daisyUI's .menu is fit-content)
|
||||
white-space: normal;
|
||||
margin-inline-start: 0.5rem;
|
||||
padding: 0;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .dropdown-submenu-content,
|
||||
&:focus-within > .dropdown-submenu-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
> .dropdown-submenu-content {
|
||||
position: absolute;
|
||||
top: calc(-1 * (var(--dropdown-padding-y) + var(--dropdown-border-width)));
|
||||
left: 100%;
|
||||
margin: 0;
|
||||
z-index: var(--dropdown-z-index);
|
||||
width: max-content;
|
||||
contain: none;
|
||||
white-space: nowrap;
|
||||
padding: var(--dropdown-padding-y) var(--dropdown-padding-x);
|
||||
background-color: var(--dropdown-bg);
|
||||
border: var(--dropdown-border-width) solid var(--dropdown-border-color);
|
||||
border-radius: var(--dropdown-border-radius);
|
||||
box-shadow: var(--dropdown-box-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user