mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-20 11:29:57 -06:00
Fix PDF layout designer logo issues and dropdown menu behavior
- Fix logo becoming invisible after resize in designer by reloading images from URL when loading saved designs - Fix logo size not being preserved by converting scale changes to width/height during resize - Fix logo resizing for invoice PDF layouts with proper transformend event handling - Fix PDF templates dropdown closing parent admin menu by keeping ancestor dropdowns open - Fix JavaScript syntax error with Jinja2 template in logo rendering - Fix logo display in preview with proper URL embedding
This commit is contained in:
20
Dockerfile
20
Dockerfile
@@ -65,14 +65,17 @@ COPY requirements.txt .
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
# Create non-root user early (before copying files)
|
||||
RUN useradd -m -u 1000 timetracker
|
||||
|
||||
# Copy project files with correct ownership
|
||||
COPY --chown=timetracker:timetracker . .
|
||||
|
||||
# Also install certificate generation script to a stable path used by docs/compose
|
||||
COPY scripts/generate-certs.sh /scripts/generate-certs.sh
|
||||
COPY --chown=timetracker:timetracker scripts/generate-certs.sh /scripts/generate-certs.sh
|
||||
|
||||
# Copy compiled assets from frontend stage (overwriting the stale one from COPY .)
|
||||
COPY --from=frontend /app/app/static/dist/output.css /app/app/static/dist/output.css
|
||||
COPY --chown=timetracker:timetracker --from=frontend /app/app/static/dist/output.css /app/app/static/dist/output.css
|
||||
|
||||
# Create all directories and set permissions in a single layer
|
||||
RUN mkdir -p \
|
||||
@@ -88,7 +91,7 @@ RUN mkdir -p \
|
||||
&& chmod -R 755 /app/app/static/uploads /app/static/uploads
|
||||
|
||||
# Copy the startup script
|
||||
COPY docker/start-fixed.py /app/start.py
|
||||
COPY --chown=timetracker:timetracker docker/start-fixed.py /app/start.py
|
||||
|
||||
# Fix line endings and set permissions in a single layer
|
||||
RUN find /app/docker -name "*.sh" -o -name "*.py" | xargs dos2unix 2>/dev/null || true \
|
||||
@@ -113,10 +116,9 @@ RUN find /app/docker -name "*.sh" -o -name "*.py" | xargs dos2unix 2>/dev/null |
|
||||
/app/docker/simple_test.sh \
|
||||
/scripts/generate-certs.sh
|
||||
|
||||
# Create non-root user and set ownership
|
||||
RUN useradd -m -u 1000 timetracker \
|
||||
&& chown -R timetracker:timetracker \
|
||||
/app \
|
||||
# Set ownership only for directories that need write access
|
||||
# (Most files already have correct ownership from COPY --chown)
|
||||
RUN chown -R timetracker:timetracker \
|
||||
/data \
|
||||
/app/logs \
|
||||
/app/instance \
|
||||
|
||||
@@ -1863,6 +1863,33 @@ function initializePDFEditor() {
|
||||
}
|
||||
});
|
||||
|
||||
// For Image nodes, convert scale changes to width/height changes
|
||||
// This ensures the size is stored correctly when resizing
|
||||
if (node.className === 'Image') {
|
||||
transformer.on('transformend', function() {
|
||||
const image = node;
|
||||
const scaleX = image.scaleX();
|
||||
const scaleY = image.scaleY();
|
||||
|
||||
// Update width and height based on scale
|
||||
const newWidth = image.width() * scaleX;
|
||||
const newHeight = image.height() * scaleY;
|
||||
|
||||
// Reset scale to 1 and apply the new dimensions
|
||||
image.width(newWidth);
|
||||
image.height(newHeight);
|
||||
image.scaleX(1);
|
||||
image.scaleY(1);
|
||||
|
||||
// Update properties panel if visible
|
||||
if (selectedElement === image) {
|
||||
updatePropertiesPanel(image);
|
||||
}
|
||||
|
||||
layer.draw();
|
||||
});
|
||||
}
|
||||
|
||||
layer.add(transformer);
|
||||
layer.draw();
|
||||
|
||||
@@ -2538,7 +2565,7 @@ function initializePDFEditor() {
|
||||
const w = Math.round(attrs.width || 100);
|
||||
const h = Math.round(attrs.height || 50);
|
||||
{% raw %}
|
||||
bodyContent += ` <img src="{{ get_logo_base64(settings.get_logo_path()) }}" style="position:absolute;left:${x}px;top:${y}px;width:${w}px;height:${h}px;opacity:${opacity}" alt="Logo">\n`;
|
||||
bodyContent += ` <img src="{{ get_logo_base64(settings.get_logo_path()) if settings.has_logo() and settings.get_logo_path() else '' }}" style="position:absolute;left:${x}px;top:${y}px;width:${w}px;height:${h}px;opacity:${opacity}" alt="Logo">\n`;
|
||||
{% endraw %}
|
||||
} else if (child.className === 'Rect') {
|
||||
const w = Math.round(attrs.width || 100);
|
||||
@@ -3092,11 +3119,96 @@ table tr:last-child td {
|
||||
// Redraw grid for new size
|
||||
drawGrid();
|
||||
|
||||
// Fix logo images: Konva Image nodes need to reload the image from URL
|
||||
// When Konva Image nodes are serialized/deserialized, the image data is lost
|
||||
// We need to reload the image from the URL and restore all attributes
|
||||
if (LOGO_URL) {
|
||||
const logoImages = layer.find('[name="logo"]');
|
||||
logoImages.forEach(function(logoNode) {
|
||||
if (logoNode.className === 'Image') {
|
||||
// Save all attributes before reloading
|
||||
const savedAttrs = {
|
||||
x: logoNode.x(),
|
||||
y: logoNode.y(),
|
||||
width: logoNode.width(),
|
||||
height: logoNode.height(),
|
||||
scaleX: logoNode.scaleX(),
|
||||
scaleY: logoNode.scaleY(),
|
||||
rotation: logoNode.rotation(),
|
||||
opacity: logoNode.opacity(),
|
||||
draggable: logoNode.draggable(),
|
||||
visible: logoNode.visible()
|
||||
};
|
||||
|
||||
// Store reference to update elements array
|
||||
const oldNode = logoNode;
|
||||
const parent = logoNode.getParent();
|
||||
const index = logoNode.getZIndex();
|
||||
|
||||
// Reload the image from URL
|
||||
Konva.Image.fromURL(LOGO_URL, function(newImage) {
|
||||
// Calculate actual size considering scale
|
||||
// If scaleX/scaleY are not 1, the actual size is width*scaleX and height*scaleY
|
||||
let finalWidth = savedAttrs.width;
|
||||
let finalHeight = savedAttrs.height;
|
||||
|
||||
// If scale was applied, we need to account for it
|
||||
// Reset scale to 1 and apply the scaled dimensions as the new width/height
|
||||
if (savedAttrs.scaleX !== 1 || savedAttrs.scaleY !== 1) {
|
||||
finalWidth = savedAttrs.width * savedAttrs.scaleX;
|
||||
finalHeight = savedAttrs.height * savedAttrs.scaleY;
|
||||
}
|
||||
|
||||
// Restore all attributes, but reset scale to 1 and use calculated dimensions
|
||||
newImage.setAttrs({
|
||||
x: savedAttrs.x,
|
||||
y: savedAttrs.y,
|
||||
width: finalWidth,
|
||||
height: finalHeight,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
rotation: savedAttrs.rotation,
|
||||
opacity: savedAttrs.opacity,
|
||||
draggable: savedAttrs.draggable,
|
||||
visible: savedAttrs.visible
|
||||
});
|
||||
newImage.name('logo');
|
||||
|
||||
// Replace the old node with the new one
|
||||
oldNode.destroy();
|
||||
parent.add(newImage);
|
||||
newImage.zIndex(index);
|
||||
|
||||
// Update elements array
|
||||
const elementIndex = elements.findIndex(e => e.node === oldNode);
|
||||
if (elementIndex !== -1) {
|
||||
elements[elementIndex].node = newImage;
|
||||
} else {
|
||||
// Add to elements array if not found
|
||||
elements.push({ type: 'logo', node: newImage });
|
||||
}
|
||||
|
||||
// Setup selection for the new image
|
||||
setupSelection(newImage);
|
||||
|
||||
layer.draw();
|
||||
}, function(error) {
|
||||
console.error('Failed to load logo image:', error);
|
||||
// Keep the old node if image fails to load
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Re-setup selections for all elements (skip background and grid lines)
|
||||
layer.children.forEach(child => {
|
||||
if (child !== background &&
|
||||
child.className !== 'Transformer' &&
|
||||
!(child.className === 'Line' && child.attrs.name === 'grid-line')) {
|
||||
// Skip logo images as they're handled above
|
||||
if (child.className === 'Image' && child.attrs.name === 'logo') {
|
||||
return;
|
||||
}
|
||||
setupSelection(child);
|
||||
// If it's a table Group, also setup selection on children
|
||||
if (child.className === 'Group' && (child.attrs.name === 'items-table' || child.attrs.name === 'expenses-table')) {
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
{% set work_open = ep.startswith('projects.') or ep.startswith('tasks.') or ep.startswith('issues.') or ep.startswith('timer.') or ep.startswith('kanban.') or ep.startswith('weekly_goals.') or ep.startswith('project_templates.') or ep.startswith('gantt.') %}
|
||||
{% set calendar_open = ep.startswith('calendar.') %}
|
||||
{% set finance_open = ep.startswith('reports.') or ep.startswith('invoices.') or ep.startswith('recurring_invoices.') or ep.startswith('payments.') or ep.startswith('expenses.') or ep.startswith('mileage.') or ep.startswith('per_diem.') or ep.startswith('budget_alerts.') or ep.startswith('invoice_approvals.') or ep.startswith('payment_gateways.') or ep.startswith('scheduled_reports.') or ep.startswith('custom_reports.') %}
|
||||
{% set crm_open = ep.startswith('clients.') or ep.startswith('quotes.') %}
|
||||
{% set crm_open = ep.startswith('clients.') or ep.startswith('quotes.') or ep.startswith('contacts.') or ep.startswith('deals.') or ep.startswith('leads.') %}
|
||||
{% set inventory_open = ep.startswith('inventory.') %}
|
||||
{% set analytics_open = ep.startswith('analytics.') %}
|
||||
{% set tools_open = ep.startswith('import_export.') or ep.startswith('saved_filters.') or ep.startswith('integrations.') %}
|
||||
@@ -244,7 +244,7 @@
|
||||
<span class="ml-3 sidebar-label">{{ _('Dashboard') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% if (not settings or settings.ui_allow_calendar) and current_user.ui_show_calendar %}
|
||||
{% if is_module_enabled('calendar') %}
|
||||
<li class="mt-2">
|
||||
<button onclick="toggleDropdown('calendarDropdown')" data-dropdown="calendarDropdown" class="w-full flex items-center p-2 rounded-lg {% if calendar_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
||||
<i class="fas fa-calendar-alt w-6 text-center"></i>
|
||||
@@ -300,14 +300,14 @@
|
||||
<i class="fas fa-folder w-4 mr-2"></i>{{ _('Projects') }}
|
||||
</a>
|
||||
</li>
|
||||
{% if settings.ui_allow_project_templates and current_user.ui_show_project_templates %}
|
||||
{% if is_module_enabled('project_templates') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_project_templates %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('project_templates.list_templates') }}">
|
||||
<i class="fas fa-layer-group w-4 mr-2"></i>{{ _('Project Templates') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_gantt_chart and current_user.ui_show_gantt_chart %}
|
||||
{% if is_module_enabled('gantt') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if ep.startswith('gantt.') %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('gantt.gantt_view') }}">
|
||||
<i class="fas fa-project-diagram w-4 mr-2"></i>{{ _('Gantt Chart') }}
|
||||
@@ -319,27 +319,34 @@
|
||||
<i class="fas fa-tasks w-4 mr-2"></i>{{ _('Tasks') }}
|
||||
</a>
|
||||
</li>
|
||||
{% if settings.ui_allow_issues and current_user.ui_show_issues %}
|
||||
{% if is_module_enabled('issues') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_issues %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('issues.list_issues') }}">
|
||||
<i class="fas fa-bug w-4 mr-2"></i>{{ _('Issues') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_kanban_board and current_user.ui_show_kanban_board %}
|
||||
{% if is_module_enabled('kanban') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_kanban %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('kanban.board') }}">
|
||||
<i class="fas fa-columns w-4 mr-2"></i>{{ _('Kanban Board') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_weekly_goals and current_user.ui_show_weekly_goals %}
|
||||
{% if is_module_enabled('weekly_goals') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_goals %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('weekly_goals.index') }}">
|
||||
<i class="fas fa-bullseye w-4 mr-2"></i>{{ _('Weekly Goals') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_module_enabled('time_entry_templates') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_templates %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('time_entry_templates.list_templates') }}">
|
||||
<i class="fas fa-clipboard-list w-4 mr-2"></i>{{ _('Time Entry Templates') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="mt-2">
|
||||
@@ -351,12 +358,37 @@
|
||||
<ul id="crmDropdown" class="{% if not crm_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
|
||||
{% set nav_active_clients = ep.startswith('clients.') %}
|
||||
{% set nav_active_quotes = ep.startswith('quotes.') %}
|
||||
{% set nav_active_contacts = ep.startswith('contacts.') %}
|
||||
{% set nav_active_deals = ep.startswith('deals.') %}
|
||||
{% set nav_active_leads = ep.startswith('leads.') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_clients %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('clients.list_clients') }}">
|
||||
<i class="fas fa-users w-4 mr-2"></i>{{ _('Clients') }}
|
||||
</a>
|
||||
</li>
|
||||
{% if settings.ui_allow_quotes and current_user.ui_show_quotes %}
|
||||
{% if is_module_enabled('contacts') %}
|
||||
<li>
|
||||
<span class="block px-2 py-1 text-text-muted-light dark:text-text-muted-dark text-sm">
|
||||
<i class="fas fa-address-book w-4 mr-2"></i>{{ _('Contacts') }}
|
||||
<span class="text-xs ml-2">({{ _('via Clients') }})</span>
|
||||
</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_module_enabled('deals') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_deals %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('deals.list_deals') }}">
|
||||
<i class="fas fa-handshake w-4 mr-2"></i>{{ _('Deals') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_module_enabled('leads') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_leads %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('leads.list_leads') }}">
|
||||
<i class="fas fa-user-tag w-4 mr-2"></i>{{ _('Leads') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_module_enabled('quotes') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_quotes %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('quotes.list_quotes') }}">
|
||||
<i class="fas fa-file-contract w-4 mr-2"></i>{{ _('Quotes') }}
|
||||
@@ -382,7 +414,7 @@
|
||||
{% set nav_active_perdiem = ep.startswith('per_diem.') and not ep.startswith('per_diem.list_rates') %}
|
||||
{% set nav_active_budget = ep.startswith('budget_alerts.') %}
|
||||
{% set reports_open = ep.startswith('reports.') or ep.startswith('scheduled_reports.') or ep.startswith('custom_reports.') %}
|
||||
{% if settings.ui_allow_reports and current_user.ui_show_reports %}
|
||||
{% if is_module_enabled('reports') %}
|
||||
<li>
|
||||
<button onclick="toggleDropdown('reportsDropdown', event)" data-dropdown="reportsDropdown" class="w-full flex items-center px-2 py-1 rounded {% if reports_open %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
||||
<i class="fas fa-chart-bar w-4 mr-2"></i>
|
||||
@@ -396,7 +428,7 @@
|
||||
<i class="fas fa-chart-line w-4 mr-2"></i>{{ _('All Reports') }}
|
||||
</a>
|
||||
</li>
|
||||
{% if settings.ui_allow_report_builder and current_user.ui_show_report_builder %}
|
||||
{% if is_module_enabled('custom_reports') %}
|
||||
<li>
|
||||
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'custom_reports.report_builder' or ep == 'custom_reports.view_custom_report' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('custom_reports.report_builder') }}">
|
||||
<i class="fas fa-magic w-4 mr-2"></i>{{ _('Report Builder') }}
|
||||
@@ -408,7 +440,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_scheduled_reports and current_user.ui_show_scheduled_reports %}
|
||||
{% if is_module_enabled('scheduled_reports') %}
|
||||
<li>
|
||||
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep.startswith('scheduled_reports.') %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('scheduled_reports.list_scheduled') }}">
|
||||
<i class="fas fa-clock w-4 mr-2"></i>{{ _('Scheduled Reports') }}
|
||||
@@ -418,59 +450,63 @@
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_module_enabled('invoices') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_invoices %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('invoices.list_invoices') }}">
|
||||
<i class="fas fa-file-invoice w-4 mr-2"></i>{{ _('Invoices') }}
|
||||
</a>
|
||||
</li>
|
||||
{% if settings.ui_allow_invoice_approvals and current_user.ui_show_invoice_approvals %}
|
||||
{% endif %}
|
||||
{% if is_module_enabled('invoice_approvals') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_invoice_approvals %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('invoice_approvals.list_approvals') }}">
|
||||
<i class="fas fa-check-circle w-4 mr-2"></i>{{ _('Invoice Approvals') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_payment_gateways and current_user.ui_show_payment_gateways %}
|
||||
{% if is_module_enabled('payment_gateways') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_payment_gateways %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('payment_gateways.list_gateways') }}">
|
||||
<i class="fas fa-credit-card w-4 mr-2"></i>{{ _('Payment Gateways') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_recurring_invoices and current_user.ui_show_recurring_invoices %}
|
||||
{% if is_module_enabled('recurring_invoices') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_recurring_invoices %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('recurring_invoices.list_recurring_invoices') }}">
|
||||
<i class="fas fa-sync-alt w-4 mr-2"></i>{{ _('Recurring Invoices') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_payments and current_user.ui_show_payments %}
|
||||
{% if is_module_enabled('payments') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_payments %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('payments.list_payments') }}">
|
||||
<i class="fas fa-credit-card w-4 mr-2"></i>{{ _('Payments') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_module_enabled('expenses') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_expenses %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('expenses.list_expenses') }}">
|
||||
<i class="fas fa-receipt w-4 mr-2"></i>{{ _('Expenses') }}
|
||||
</a>
|
||||
</li>
|
||||
{% if settings.ui_allow_mileage and current_user.ui_show_mileage %}
|
||||
{% endif %}
|
||||
{% if is_module_enabled('mileage') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_mileage %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('mileage.list_mileage') }}">
|
||||
<i class="fas fa-car w-4 mr-2"></i>{{ _('Mileage') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_per_diem and current_user.ui_show_per_diem %}
|
||||
{% if is_module_enabled('per_diem') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_perdiem %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('per_diem.list_per_diem') }}">
|
||||
<i class="fas fa-utensils w-4 mr-2"></i>{{ _('Per Diem') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_budget_alerts and current_user.ui_show_budget_alerts %}
|
||||
{% if is_module_enabled('budget_alerts') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_budget %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('budget_alerts.budget_dashboard') }}">
|
||||
<i class="fas fa-exclamation-triangle w-4 mr-2"></i>{{ _('Budget Alerts') }}
|
||||
@@ -479,7 +515,7 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% if settings.ui_allow_inventory and current_user.ui_show_inventory %}
|
||||
{% if is_module_enabled('inventory') %}
|
||||
<li class="mt-2">
|
||||
<button onclick="toggleDropdown('inventoryDropdown')" data-dropdown="inventoryDropdown" class="w-full flex items-center p-2 rounded-lg {% if inventory_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
||||
<i class="fas fa-boxes w-6 text-center"></i>
|
||||
@@ -556,7 +592,7 @@
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_analytics and current_user.ui_show_analytics %}
|
||||
{% if is_module_enabled('analytics') %}
|
||||
<li class="mt-2">
|
||||
<a href="{{ url_for('analytics.analytics_dashboard') }}" class="flex items-center p-2 rounded-lg {% if analytics_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
||||
<i class="fas fa-chart-line w-6 text-center"></i>
|
||||
@@ -564,7 +600,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if settings.ui_allow_tools and current_user.ui_show_tools %}
|
||||
{% if is_module_enabled('integrations') or is_module_enabled('import_export') or is_module_enabled('saved_filters') %}
|
||||
<li class="mt-2">
|
||||
<button onclick="toggleDropdown('toolsDropdown')" data-dropdown="toolsDropdown" class="w-full flex items-center p-2 rounded-lg {% if tools_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
||||
<i class="fas fa-tools w-6 text-center"></i>
|
||||
@@ -575,21 +611,27 @@
|
||||
{% set nav_active_import_export = ep.startswith('import_export.') %}
|
||||
{% set nav_active_filters = ep.startswith('saved_filters.') %}
|
||||
{% set nav_active_integrations = ep.startswith('integrations.') %}
|
||||
{% if is_module_enabled('integrations') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_integrations %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('integrations.list_integrations') }}">
|
||||
<i class="fas fa-plug w-4 mr-2"></i>{{ _('Integrations') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_module_enabled('import_export') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_import_export %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('import_export.import_export_page') }}">
|
||||
<i class="fas fa-exchange-alt w-4 mr-2"></i>{{ _('Import / Export') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_module_enabled('saved_filters') %}
|
||||
<li>
|
||||
<a class="block px-2 py-1 rounded {% if nav_active_filters %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('saved_filters.list_filters') }}">
|
||||
<i class="fas fa-filter w-4 mr-2"></i>{{ _('Saved Filters') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
@@ -650,6 +692,11 @@
|
||||
<i class="fas fa-sliders-h w-4 mr-2"></i>{{ _('Settings') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.manage_modules' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.manage_modules') }}">
|
||||
<i class="fas fa-puzzle-piece w-4 mr-2"></i>{{ _('Module Management') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.email_support' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.email_support') }}">
|
||||
<i class="fas fa-envelope w-4 mr-2"></i>{{ _('Email Configuration') }}
|
||||
@@ -666,7 +713,7 @@
|
||||
<span class="flex-1 text-left">{{ _('PDF Templates') }}</span>
|
||||
<i class="fas fa-chevron-down text-xs"></i>
|
||||
</button>
|
||||
<ul id="pdfDropdown" class="{% if pdf_open %}{% else %}hidden {% endif %}mt-2 space-y-2 ml-6">
|
||||
<ul id="pdfDropdown" class="{% if pdf_open %}{% else %}hidden {% endif %}mt-2 space-y-2 ml-6" onclick="event.stopPropagation();">
|
||||
<li>
|
||||
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.pdf_layout' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.pdf_layout') }}">
|
||||
<i class="fas fa-file-invoice w-4 mr-2"></i>{{ _('Invoice PDF') }}
|
||||
@@ -1351,6 +1398,21 @@
|
||||
'reportsDropdown': 'financeDropdown' // Reports is nested under Finance
|
||||
};
|
||||
|
||||
// Prevent parent dropdowns from closing when clicking inside nested dropdowns
|
||||
// Add click handlers to nested dropdowns to stop propagation
|
||||
if (id in nestedDropdowns) {
|
||||
const nestedDropdown = document.getElementById(id);
|
||||
if (nestedDropdown) {
|
||||
// Remove existing handler if any to avoid duplicates
|
||||
nestedDropdown.removeEventListener('click', nestedDropdown._stopPropagationHandler);
|
||||
// Add new handler
|
||||
nestedDropdown._stopPropagationHandler = function(e) {
|
||||
e.stopPropagation();
|
||||
};
|
||||
nestedDropdown.addEventListener('click', nestedDropdown._stopPropagationHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a nested dropdown, don't close its parent
|
||||
const parentDropdown = nestedDropdowns[id];
|
||||
|
||||
@@ -1368,9 +1430,21 @@
|
||||
// Close all other top-level dropdowns in the sidebar (accordion behavior)
|
||||
// Note: Nested dropdowns (like pdfDropdown) are not in this list, so they work independently
|
||||
const allSidebarDropdowns = ['workDropdown', 'financeDropdown', 'adminDropdown', 'crmDropdown', 'toolsDropdown'];
|
||||
|
||||
// Build a list of all ancestor dropdowns that should stay open
|
||||
const ancestorsToKeepOpen = [];
|
||||
if (parentDropdown) {
|
||||
ancestorsToKeepOpen.push(parentDropdown);
|
||||
// Check if parent has a grandparent
|
||||
const grandparent = nestedDropdowns[parentDropdown];
|
||||
if (grandparent) {
|
||||
ancestorsToKeepOpen.push(grandparent);
|
||||
}
|
||||
}
|
||||
|
||||
allSidebarDropdowns.forEach(dropdownId => {
|
||||
// Don't close the parent if this is a nested dropdown
|
||||
if (dropdownId === parentDropdown) {
|
||||
// Don't close if this dropdown is an ancestor of the nested dropdown being opened
|
||||
if (ancestorsToKeepOpen.includes(dropdownId)) {
|
||||
return;
|
||||
}
|
||||
// Don't close adminDropdown if clicking on any of its submenus
|
||||
@@ -1400,18 +1474,20 @@
|
||||
// Toggle the clicked dropdown
|
||||
if (isCurrentlyHidden) {
|
||||
dropdown.classList.remove('hidden');
|
||||
// If opening a submenu of adminDropdown, ensure adminDropdown is also open
|
||||
if (parentDropdown === 'adminDropdown') {
|
||||
const adminDropdown = document.getElementById('adminDropdown');
|
||||
if (adminDropdown) {
|
||||
adminDropdown.classList.remove('hidden');
|
||||
// If this is a nested dropdown, ensure all ancestor dropdowns are open
|
||||
if (parentDropdown) {
|
||||
// Open the direct parent
|
||||
const parent = document.getElementById(parentDropdown);
|
||||
if (parent) {
|
||||
parent.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
// If opening reportsDropdown, ensure financeDropdown is also open
|
||||
if (parentDropdown === 'financeDropdown') {
|
||||
const financeDropdown = document.getElementById('financeDropdown');
|
||||
if (financeDropdown) {
|
||||
financeDropdown.classList.remove('hidden');
|
||||
// Check if parent has a grandparent and open it too
|
||||
const grandparent = nestedDropdowns[parentDropdown];
|
||||
if (grandparent) {
|
||||
const grandparentDropdown = document.getElementById(grandparent);
|
||||
if (grandparentDropdown) {
|
||||
grandparentDropdown.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1428,6 +1504,57 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent parent dropdowns from closing when clicking inside nested dropdowns
|
||||
// This handles clicks on links and other elements inside nested dropdowns
|
||||
document.addEventListener('click', function(e) {
|
||||
// Check if the click is inside a nested dropdown
|
||||
const clickedInsideNested = e.target.closest('#pdfDropdown') ||
|
||||
e.target.closest('#reportsDropdown') ||
|
||||
e.target.closest('#adminUserMgmtDropdown') ||
|
||||
e.target.closest('#adminSecurityDropdown') ||
|
||||
e.target.closest('#adminIntegrationsDropdown') ||
|
||||
e.target.closest('#adminDataDropdown') ||
|
||||
e.target.closest('#adminMaintenanceDropdown');
|
||||
|
||||
if (clickedInsideNested) {
|
||||
// Find the parent dropdown and keep it open
|
||||
const nestedDropdowns = {
|
||||
'pdfDropdown': 'adminSettingsDropdown',
|
||||
'adminUserMgmtDropdown': 'adminDropdown',
|
||||
'adminSettingsDropdown': 'adminDropdown',
|
||||
'adminSecurityDropdown': 'adminDropdown',
|
||||
'adminIntegrationsDropdown': 'adminDropdown',
|
||||
'adminDataDropdown': 'adminDropdown',
|
||||
'adminMaintenanceDropdown': 'adminDropdown',
|
||||
'reportsDropdown': 'financeDropdown'
|
||||
};
|
||||
|
||||
// Find which nested dropdown was clicked
|
||||
let clickedDropdownId = null;
|
||||
for (const [nestedId, parentId] of Object.entries(nestedDropdowns)) {
|
||||
if (e.target.closest('#' + nestedId)) {
|
||||
clickedDropdownId = nestedId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (clickedDropdownId && nestedDropdowns[clickedDropdownId]) {
|
||||
const parentId = nestedDropdowns[clickedDropdownId];
|
||||
const parentDropdown = document.getElementById(parentId);
|
||||
if (parentDropdown) {
|
||||
parentDropdown.classList.remove('hidden');
|
||||
}
|
||||
// Also keep adminDropdown open if parent is adminSettingsDropdown
|
||||
if (parentId === 'adminSettingsDropdown') {
|
||||
const adminDropdown = document.getElementById('adminDropdown');
|
||||
if (adminDropdown) {
|
||||
adminDropdown.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, true); // Use capture phase to catch events early
|
||||
</script>
|
||||
|
||||
<!-- Enhanced UI Scripts -->
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
{% set logo_data = get_logo_base64(logo_path) %}
|
||||
{% if logo_data %}
|
||||
<img src="{{ logo_data }}" alt="{{ _('Company Logo') }}" class="company-logo">
|
||||
{% else %}
|
||||
{# Fallback to file URI if base64 fails #}
|
||||
<img src="{{ Path(logo_path).resolve().as_uri() if Path and logo_path else 'file://' ~ logo_path }}" alt="{{ _('Company Logo') }}" class="company-logo">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
{% set logo_data = get_logo_base64(logo_path) %}
|
||||
{% if logo_data %}
|
||||
<img src="{{ logo_data }}" alt="{{ _('Company Logo') }}" class="company-logo">
|
||||
{% else %}
|
||||
{# Fallback to file URI if base64 fails #}
|
||||
<img src="{{ Path(logo_path).resolve().as_uri() if Path and logo_path else 'file://' ~ logo_path }}" alt="{{ _('Company Logo') }}" class="company-logo">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1868,6 +1868,65 @@ function initializePDFEditor() {
|
||||
}
|
||||
});
|
||||
|
||||
// For Image nodes, convert scale changes to width/height changes
|
||||
// This ensures the size is stored correctly when resizing
|
||||
if (node.className === 'Image') {
|
||||
// Get the actual displayed dimensions (accounting for any existing scale)
|
||||
const image = node;
|
||||
const currentScaleX = image.scaleX();
|
||||
const currentScaleY = image.scaleY();
|
||||
let originalWidth = image.width() * currentScaleX;
|
||||
let originalHeight = image.height() * currentScaleY;
|
||||
|
||||
// If image already has scale, normalize it first
|
||||
if (currentScaleX !== 1 || currentScaleY !== 1) {
|
||||
image.width(originalWidth);
|
||||
image.height(originalHeight);
|
||||
image.scaleX(1);
|
||||
image.scaleY(1);
|
||||
layer.draw();
|
||||
}
|
||||
|
||||
// Handle transform end - convert scale to width/height
|
||||
const handleTransformEnd = function() {
|
||||
const scaleX = image.scaleX();
|
||||
const scaleY = image.scaleY();
|
||||
|
||||
// Only update if scale has changed
|
||||
if (scaleX !== 1 || scaleY !== 1) {
|
||||
// Update width and height based on scale
|
||||
const newWidth = originalWidth * scaleX;
|
||||
const newHeight = originalHeight * scaleY;
|
||||
|
||||
// Reset scale to 1 and apply the new dimensions
|
||||
image.width(newWidth);
|
||||
image.height(newHeight);
|
||||
image.scaleX(1);
|
||||
image.scaleY(1);
|
||||
|
||||
// Update original dimensions for next transform
|
||||
originalWidth = newWidth;
|
||||
originalHeight = newHeight;
|
||||
|
||||
// Update properties panel if visible
|
||||
if (selectedElement === image) {
|
||||
updatePropertiesPanel(image);
|
||||
}
|
||||
|
||||
layer.draw();
|
||||
}
|
||||
};
|
||||
|
||||
transformer.on('transformend', handleTransformEnd);
|
||||
|
||||
// Also update on transform (during drag) to keep properties panel in sync
|
||||
transformer.on('transform', function() {
|
||||
if (selectedElement === image) {
|
||||
updatePropertiesPanel(image);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
layer.add(transformer);
|
||||
layer.draw();
|
||||
|
||||
@@ -2543,7 +2602,7 @@ function initializePDFEditor() {
|
||||
const w = Math.round(attrs.width || 100);
|
||||
const h = Math.round(attrs.height || 50);
|
||||
{% raw %}
|
||||
bodyContent += ` <img src="{{ get_logo_base64(settings.get_logo_path()) }}" style="position:absolute;left:${x}px;top:${y}px;width:${w}px;height:${h}px;opacity:${opacity}" alt="Logo">\n`;
|
||||
bodyContent += ` <img src="{{ get_logo_base64(settings.get_logo_path()) if settings.has_logo() and settings.get_logo_path() else '' }}" style="position:absolute;left:${x}px;top:${y}px;width:${w}px;height:${h}px;opacity:${opacity}" alt="Logo">\n`;
|
||||
{% endraw %}
|
||||
} else if (child.className === 'Rect') {
|
||||
const w = Math.round(attrs.width || 100);
|
||||
@@ -3097,11 +3156,96 @@ table tr:last-child td {
|
||||
// Redraw grid for new size
|
||||
drawGrid();
|
||||
|
||||
// Fix logo images: Konva Image nodes need to reload the image from URL
|
||||
// When Konva Image nodes are serialized/deserialized, the image data is lost
|
||||
// We need to reload the image from the URL and restore all attributes
|
||||
if (LOGO_URL) {
|
||||
const logoImages = layer.find('[name="logo"]');
|
||||
logoImages.forEach(function(logoNode) {
|
||||
if (logoNode.className === 'Image') {
|
||||
// Save all attributes before reloading
|
||||
const savedAttrs = {
|
||||
x: logoNode.x(),
|
||||
y: logoNode.y(),
|
||||
width: logoNode.width(),
|
||||
height: logoNode.height(),
|
||||
scaleX: logoNode.scaleX(),
|
||||
scaleY: logoNode.scaleY(),
|
||||
rotation: logoNode.rotation(),
|
||||
opacity: logoNode.opacity(),
|
||||
draggable: logoNode.draggable(),
|
||||
visible: logoNode.visible()
|
||||
};
|
||||
|
||||
// Store reference to update elements array
|
||||
const oldNode = logoNode;
|
||||
const parent = logoNode.getParent();
|
||||
const index = logoNode.getZIndex();
|
||||
|
||||
// Reload the image from URL
|
||||
Konva.Image.fromURL(LOGO_URL, function(newImage) {
|
||||
// Calculate actual size considering scale
|
||||
// If scaleX/scaleY are not 1, the actual size is width*scaleX and height*scaleY
|
||||
let finalWidth = savedAttrs.width;
|
||||
let finalHeight = savedAttrs.height;
|
||||
|
||||
// If scale was applied, we need to account for it
|
||||
// Reset scale to 1 and apply the scaled dimensions as the new width/height
|
||||
if (savedAttrs.scaleX !== 1 || savedAttrs.scaleY !== 1) {
|
||||
finalWidth = savedAttrs.width * savedAttrs.scaleX;
|
||||
finalHeight = savedAttrs.height * savedAttrs.scaleY;
|
||||
}
|
||||
|
||||
// Restore all attributes, but reset scale to 1 and use calculated dimensions
|
||||
newImage.setAttrs({
|
||||
x: savedAttrs.x,
|
||||
y: savedAttrs.y,
|
||||
width: finalWidth,
|
||||
height: finalHeight,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
rotation: savedAttrs.rotation,
|
||||
opacity: savedAttrs.opacity,
|
||||
draggable: savedAttrs.draggable,
|
||||
visible: savedAttrs.visible
|
||||
});
|
||||
newImage.name('logo');
|
||||
|
||||
// Replace the old node with the new one
|
||||
oldNode.destroy();
|
||||
parent.add(newImage);
|
||||
newImage.zIndex(index);
|
||||
|
||||
// Update elements array
|
||||
const elementIndex = elements.findIndex(e => e.node === oldNode);
|
||||
if (elementIndex !== -1) {
|
||||
elements[elementIndex].node = newImage;
|
||||
} else {
|
||||
// Add to elements array if not found
|
||||
elements.push({ type: 'logo', node: newImage });
|
||||
}
|
||||
|
||||
// Setup selection for the new image
|
||||
setupSelection(newImage);
|
||||
|
||||
layer.draw();
|
||||
}, function(error) {
|
||||
console.error('Failed to load logo image:', error);
|
||||
// Keep the old node if image fails to load
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Re-setup selections for all elements (skip background and grid lines)
|
||||
layer.children.forEach(child => {
|
||||
if (child !== background &&
|
||||
child.className !== 'Transformer' &&
|
||||
!(child.className === 'Line' && child.attrs.name === 'grid-line')) {
|
||||
// Skip logo images as they're handled above
|
||||
if (child.className === 'Image' && child.attrs.name === 'logo') {
|
||||
return;
|
||||
}
|
||||
setupSelection(child);
|
||||
// If it's a table Group, also setup selection on children
|
||||
if (child.className === 'Group' && (child.attrs.name === 'items-table' || child.attrs.name === 'expenses-table')) {
|
||||
|
||||
Reference in New Issue
Block a user