support multiple teamleads in each team (#2702)

* fix jumping avatars
* fix line-break after color dot for long names
This commit is contained in:
Kevin Papst
2021-08-07 18:05:41 +02:00
committed by GitHub
parent e9986c92d6
commit b2d3272151
101 changed files with 1952 additions and 649 deletions

View File

@@ -23,13 +23,13 @@
{% for type, messages in app.session.flashbag.all %}
{% for message in messages %}
{% if type == 'error' %}
ALERT.error('{{ message|trans({}, domain) }}');
ALERT.error('{{ message|trans({}, domain)|e('js') }}');
{% elseif type == 'warning' %}
ALERT.warning('{{ message|trans({}, domain) }}');
ALERT.warning('{{ message|trans({}, domain)|e('js') }}');
{% elseif type == 'success' %}
ALERT.success('{{ message|trans({}, domain) }}');
ALERT.success('{{ message|trans({}, domain)|e('js') }}');
{% else %}
ALERT.info('{{ message|trans({}, domain) }}');
ALERT.info('{{ message|trans({}, domain)|e('js') }}');
{% endif %}
{% endfor %}
{% endfor %}
@@ -123,7 +123,7 @@
{% set hasActiveRecords = active_timesheets is not empty %}
{% if not hasActiveRecords %}
{# fake entry, because at least one html template node is needed #}
{% set active_timesheets = [{'id': '000', 'begin': create_date('now'), 'activity': {'name': ''}, 'project': {'name': '', 'customer': {'name': ''}}}] %}
{% set active_timesheets = [{'id': '000', 'begin': null, 'activity': {'name': ''}, 'project': {'name': '', 'customer': {'name': ''}}}] %}
{% endif %}
{% set active_limit = kimai_config.timesheetActiveEntriesHardLimit %}
<li class="messages-menu {% if active_limit > 1 and hasActiveRecords %}dropdown{% endif %}" data-api="{{ path('active_timesheet') }}" data-href="{{ path('stop_timesheet', {'id' : '000'}) }}" data-icon="{{ 'stop-small'|icon }}" style="{% if not hasActiveRecords %}display:none{% endif %}">
@@ -132,7 +132,7 @@
<div class="ddt-small ticktac-single ticktac-running">
<div class="ticktac-stop">
<a data-replacer="url" class="api-link" href="{{ path('stop_timesheet', {'id' : entry.id}) }}" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">
<i class="{{ 'stop-small'|icon }} fa-2x"></i><span data-replacer="duration" data-title="true" data-since="{{ entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
<i class="{{ 'stop-small'|icon }} fa-2x"></i><span data-replacer="duration" data-title="true" data-since="{{ entry.begin is null ? '' : entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
</a>
</div>
</div>
@@ -156,7 +156,7 @@
<h4>
<span data-replacer="activity">{{ entry.activity.name }}</span>
<small>
<span data-replacer="duration" data-title="true" data-since="{{ entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
<span data-replacer="duration" data-title="true" data-since="{{ entry.begin is null ? '' : entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
</small>
</h4>
<p><span data-replacer="project">{{ entry.project.name }}</span> (<span data-replacer="customer">{{ entry.project.customer.name }}</span>)</p>

View File

@@ -1,6 +1,8 @@
{% if form_theme is defined %}
{% form_theme form form_theme %}
{% endif %}
{% set _back = back is defined and back is not same as (false) ? back : false %}
{% set _reset = reset is defined and reset is same as (false) ? false : true %}
<div class="box box-primary">
{% block form_before %}{% endblock %}
{{ form_start(form) }}
@@ -19,10 +21,10 @@
</div>
<div class="box-footer">
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
{% if back|default(false) %}
<a href="{{ back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
{% if _back is not same as (false) %}
<a href="{{ _back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
{% endif %}
{% if reset|default(true) %}
{% if _reset is not same as (false) %}
<input type="reset" value="{{ 'action.reset'|trans }}" class="btn btn-link pull-right" />
{% endif %}
</div>

View File

@@ -8,6 +8,31 @@
{{ parent() }}
{% endblock form_label %}
{% block _team_edit_form_members_entry_user_widget %}
{# this will convert the select box into a hidden field, which are exchangable from an HTML perspective #}
{%- set type = 'hidden' -%}
{{ block('form_widget_simple') }}
{% endblock %}
{% block team_member_widget %}
<div class="row">
<div class="col-xs-7">
<div class="checkbox">
<button class="btn btn-default btn-xs" onclick="jQuery(this).parent().parent().parent().remove();return false;"><i class="{{ 'trash'|icon }}"></i></button>
{% if form.vars.data is not null and form.vars.data.user is not null %}
{{ form.vars.data.user.displayName }}
{% else %}
__USERNAME__
{% endif %}
</div>
{{ form_widget(form.user) }}
</div>
<div class="col-xs-5">
{{ form_widget(form.teamlead) }}
</div>
</div>
{% endblock team_member_widget %}
{% block daterange_widget %}
<div class="input-group">
<div class="input-group-addon">

View File

@@ -154,16 +154,20 @@
{# for @internal use only #}
{% macro label_color_dot(type, isVisible, name, url, color) %}
{% apply spaceless %}
<span class="label-{{ type }}{{ isVisible ? '' : ' label-invisible' }}">
<span class="name">
<i class="dot {{ 'dot'|icon }} fa-fw"{% if color is not empty %} style="color:{{ color }}"{% endif %}></i>
<span class="name-inner">
{%- if url is not empty -%}
<a href="{{ url }}">{{ name }}</a>
{%- else -%}
{{ name }}
{%- endif -%}
</span>
</span>
</span>
{% endapply %}
{% endmacro %}
{% macro color_dot(color, tooltip) %}
@@ -511,23 +515,25 @@
{% endif %}
<tbody>
{% for team in teams|sort((a, b) => a.name <=> b.name) %}
{% set users = team.users|filter(u => u.enabled) %}
{% set userTeamCount = users|length %}
{% set members = team.members|filter(m => m.user.enabled) %}
{% set userTeamCount = members|length %}
<tr{% if is_granted('edit', team) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_team_member', {'id': team.id}) }}"{% endif %}>
<td>
{{ _self.label_team(team) }}
{{ _self.label_color_dot('color', true, team.name, null, team.color|colorize(team.name)) }}
</td>
<td class="avatars">
{{ _self.user_avatar(team.teamlead, ('label.teamlead'|trans ~ ': ' ~ team.teamlead.displayName), 'teamlead') }}
{% set teamHiddenId = 'team_' ~ team.id ~ '_hiddenUser' ~ random() %}
{% set counter = 0 %}
{% for user in users %}
{% if user != team.teamlead %}
{% for member in members|sort((a, b) => a.teamlead < b.teamlead) %}
{% set user = member.user %}
{% if member.teamlead %}
{{ _self.user_avatar(user, ('label.teamlead'|trans ~ ': ' ~ user.displayName), 'teamlead') }}
{% else %}
{{ _self.user_avatar(user) }}
{% set counter = counter + 1 %}
{% endif %}
{% set counter = counter + 1 %}
{% if userTeamCount > (collapseAt + 1) and counter == (collapseAt - 1) and loop.index != userTeamCount %}
<a href="#" onclick="$('#{{ teamHiddenId }}').toggleClass('hidden');$(this).hide();return false;" class="badge">{{ 'label.plus_more'|trans({'%count%': (userTeamCount - collapseAt)}) }}</a>
<a href="#" onclick="$(this).parent().append($('#{{ teamHiddenId }}').contents());$('#{{ teamHiddenId }}').remove();$(this).remove();return false;" class="badge">{{ 'label.plus_more'|trans({'%count%': (userTeamCount - collapseAt)}) }}</a>
<span class="hidden" id="{{ teamHiddenId }}">
{% set counter = counter + 1 %}
{% endif %}

View File

@@ -8,25 +8,21 @@
{% block main %}
<div class="row">
<div class="col-md-6">
{{ include('default/_form.html.twig', {
'title': team.name|default('create'|trans),
'form': form,
'back': path('admin_team')
}) }}
{% include 'team/member-form.html.twig' %}
</div>
<div class="col-md-6">
{% if not customerForm is null %}
{{ include('default/_form.html.twig', {
'title': 'teams.customer_access'|trans({}, 'teams'),
'form': customerForm,
}) }}
{{ include('default/_form.html.twig', {
'title': 'teams.customer_access'|trans({}, 'teams'),
'form': customerForm,
}) }}
{% endif %}
{% if not projectForm is null %}
{{ include('default/_form.html.twig', {
'title': 'teams.project_access'|trans({}, 'teams'),
'form': projectForm,
}) }}
{{ include('default/_form.html.twig', {
'title': 'teams.project_access'|trans({}, 'teams'),
'form': projectForm,
}) }}
{% endif %}
</div>
</div>

View File

@@ -6,9 +6,5 @@
{% block page_actions %}{{ actions.team(team, 'edit') }}{% endblock %}
{% block main %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': team.name|default('create'|trans),
'form': form,
'back': path('admin_team')
}) }}
{% include 'team/member-form.html.twig' %}
{% endblock %}

View File

@@ -16,7 +16,7 @@
{% set columns = {
'name': {'class': ''},
'teamlead': {'class': ''},
'teamlead': {'class': '', 'orderBy': false},
'user': {'class': 'hidden-xs', 'orderBy': false},
'actions': {'class': 'actions alwaysVisible'},
} %}
@@ -30,11 +30,17 @@
{{ widgets.label_name(team.name, team.color) }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'teamlead') }}">
{{ widgets.label_user(team.teamlead) }}
{% for member in team.members %}
{% if member.teamlead %}
{{ widgets.label_user(member.user) }}
{% endif %}
{% endfor %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">
{% for user in team.users %}
{{ widgets.label_user(user) }}&nbsp;
{% for member in team.members %}
{% if not member.teamlead %}
{{ widgets.label_user(member.user) }}
{% endif %}
{% endfor %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">

View File

@@ -0,0 +1,44 @@
{% set formEditTemplate = app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = {
'title': team.name|default('create'|trans),
'form': form,
'back': path('admin_team'),
'reset': false
} %}
{% embed formEditTemplate with formOptions %}
{% block form_body %}
<div class="row">
<div class="col-md-9">
{{ form_row(form.name) }}
</div>
<div class="col-md-3">
{{ form_row(form.color) }}
</div>
</div>
{{ form_row(form.users) }}
{{ form_row(form.members) }}
{{ form_widget(form) }}
{% endblock %}
{% block form_after %}
{% set blockPrefix = form.vars.id %}
<script type="text/javascript">
jQuery(document).ready(function () {
jQuery('#{{ blockPrefix }}_users').change(function (e) {
var select = jQuery(e.target);
var selected = select.find(':selected');
var username = selected.text();
var userid = select.val();
selected.remove();
var prototype = jQuery('#{{ blockPrefix }}_members');
var counter = prototype.data('widget-counter') || prototype.children().length;
var newWidget = prototype.attr('data-prototype');
newWidget = newWidget.replace(/__name__/g, counter);
newWidget = newWidget.replace(/__USERNAME__/g, username);
prototype.data('widget-counter', ++counter);
jQuery(newWidget).prependTo(prototype).find('input[type=hidden]').val(userid);
});
});
</script>
{% endblock %}
{% endembed %}

View File

@@ -48,7 +48,7 @@
{% endembed %}
{% endfor %}
{% if user.teams is not empty and (is_granted('teams', user) or (app.user == user and is_granted('view_team_member'))) %}
{% if user.teams is not empty and (is_granted('teams', user) or is_granted('view_team_member', user)) %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}{{ 'label.my_teams'|trans }}{% endblock %}

View File

@@ -1,54 +0,0 @@
{% extends 'user/layout.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block profile_content %}
<div class="row">
<div class="col-md-12">
<div class="box box-{{ admin_lte_context.widget.type }} data_table">
<div class="box-body no-padding">
<div class="dataTables_wrapper form-inline dt-bootstrap">
<div class="row">
<div class="col-sm-12">
<table class="table table-hover dataTable" role="grid">
<thead>
<tr>
<th>{{ 'label.team'|trans }}</th>
<th>{{ 'label.teamlead'|trans }}</th>
<th>{{ 'label.user'|trans }}</th>
</tr>
</thead>
<tbody>
{% for team in user.teams %}
<tr>
<td>
{{ team.name }}
</td>
<td>
{{ widgets.user_avatar(team.teamlead) }}
{#
{{ widgets.label(team.teamlead.displayName, 'success', 'label.teamlead'|trans) }}
#}
</td>
<td class="avatars">
{% for user in team.users %}
{{ widgets.user_avatar(user) }}
{#
{{ widgets.label_user(user) }}
#}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}