Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
83 changes: 33 additions & 50 deletions app/components/homescreen/blocks/projects.html.erb
Original file line number Diff line number Diff line change
@@ -1,57 +1,40 @@
<%= widget_wrapper do %>
<% if @favorite_projects.any? %>
<p class="widget-box--additional-info"><%= t("projects.lists.favorited") %></p>
<ul class="widget-box--arrow-links">
<% @favorite_projects.each do |project| %>
<li>
<%= render(
Primer::Beta::Octicon.new(
icon: "star-fill",
classes: "op-primer--star-icon",
"aria-label": I18n.t(:label_favorite)
)
) %>

<%= link_to project, project_path(project),
title: short_project_description(project),
data: { test_selector: "favorite-project" } %>
</li>
<% end %>
</ul>
<% end %>

<% if @newest_projects.empty? %>
<p class="widget-box--additional-info">
<%= t("homescreen.additional.no_visible_projects") %>
</p>
<%= widget_wrapper do |container| %>
<% if @favorite_projects.empty? %>
<%= render(Primer::Beta::Blankslate.new(test_selector: "projects-widget-empty")) do |component| %>
<% component.with_visual_icon(icon: :project) %>
<% component.with_heading(tag: :h3).with_content(t("homescreen.additional.favorite_projects.no_results")) %>
<% component.with_description { t("homescreen.additional.favorite_projects.no_results_subtext") } %>
<% end %>
<% else %>
<p class="widget-box--additional-info"><%= t("homescreen.additional.projects") %></p>
<ul class="widget-box--arrow-links">
<% @newest_projects.each do |project| %>
<li>
<%= link_to project, project_path(project), title: short_project_description(project) %>
<small>(<%= format_date(project.created_at) %>)</small>
</li>
<% end %>
</ul>
<% end %>
<% @favorite_projects.each do |project| %>
<% container.with_row do %>
<%= flex_layout do |row| %>
<% row.with_column do %>
<%= render(
Primer::Beta::Octicon.new(
icon: "star-fill",
classes: "op-primer--star-icon",
"aria-label": I18n.t(:label_favorite)
)
) %>
<% end %>

<div class="widget-box--blocks--buttons">
<% if current_user.allowed_globally?(:add_project) %>
<%= link_to new_project_path,
{ class: "button -primary",
aria: { label: t(:label_project_new) },
title: t(:label_project_new) } do %>
<%= op_icon("button--icon icon-add") %>
<span class="button--text"><%= Project.model_name.human %></span>
<% row.with_column(ml: 2) do %>
<%= render(
Primer::Beta::Link.new(
font_weight: :bold,
href: project_path(project),
title: short_project_description(project),
data: { "test-selector": "favorite-project" }
)
) { project.name } %>
<% end %>
<% end %>
<% end %>
<% end %>

<%# If any project exists %>
<% unless @newest_projects.empty? %>
<%= link_to t(:label_project_view_all), projects_path,
class: "button -highlight-inverted",
title: t(:label_project_view_all) %>
<% container.with_footer do %>
<%= render(Primer::Beta::Link.new(href: projects_path)) { t(:label_project_view_all) } %>
<% end %>
</div>
<% end %>
<% end %>
7 changes: 2 additions & 5 deletions app/components/homescreen/blocks/projects.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,16 @@
module Homescreen
module Blocks
class Projects < Grids::WidgetComponent
include IconsHelper
include ProjectsHelper
include Redmine::I18n

def initialize(*)
super

@favorite_projects = Project.visible.active.favorited_by(current_user)
@newest_projects = Project.visible.newest.take(3)
@favorite_projects = Project.visible.active.favorited_by(current_user).to_a
end

def title
I18n.t(:label_project_plural)
I18n.t("projects.lists.favorited")
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def render_issues(widget)
end

def render_view_all_link(widget)
widget.with_row do
widget.with_footer do
helpers.link_to(
I18n.t("projects.settings.versions.show_work_packages"),
helpers.project_work_packages_version_path(version)
Expand Down
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3602,6 +3602,9 @@ en:
additional:
projects: "Newest visible projects in this instance."
no_visible_projects: "There are no visible projects in this instance."
favorite_projects:
no_results: "You have no favorite projects"
no_results_subtext: "Add one or multiple projects as favorite through their overview or in a project list."
users: "Newest registered users in this instance."
blocks:
community: "OpenProject community"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@
[resource]="resource" />
</widget-header>

<div class="op-widget-box--body">
@if ({hasCapability: hasCapability$ | async}; as context) {
@if (context.hasCapability === false) {
@if ({hasCapability: hasCapability$ | async}; as context) {
@if (context.hasCapability === false) {
<div class="op-widget-box--body">
<div class="op-toast -error">
<span [textContent]="text.missing_permission"></span>
</div>
}
@if (context.hasCapability === true) {
<turbo-frame [id]="frameId" [src]="src" loading="lazy" />
}
</div>
}
</div>
@if (context.hasCapability === true) {
<turbo-frame
class="op-widget-members--frame"
[id]="frameId"
[src]="src"
loading="lazy">
</turbo-frame>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.op-widget-members
display: flex
flex-direction: column
height: 100%
min-height: 0
overflow: hidden

&--frame
display: flex
flex-direction: column
flex: 1 1 0
min-height: 0
overflow: auto

> .op-widget-box--body
flex: 1 1 0
height: auto
min-height: 0

> .op-widget-box--footer
flex: 0 0 auto
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
import {
ChangeDetectionStrategy,
Component,
Injector,
inject,
ViewEncapsulation,
} from '@angular/core';
import { AbstractTurboWidgetComponent } from 'core-app/shared/components/grids/widgets/abstract-turbo-widget.component';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { CurrentUserService } from 'core-app/core/current-user/current-user.service';

@Component({
selector: 'op-members-widget',
templateUrl: './members.component.html',
styleUrls: ['./members.component.sass'],
encapsulation: ViewEncapsulation.None,
host: { class: 'op-widget-members' },
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
standalone: false,
})
export class WidgetMembersComponent extends AbstractTurboWidgetComponent {
protected readonly currentUser = inject(CurrentUserService);

text = {
missing_permission: this.I18n.t('js.grid.widgets.missing_permission'),
missing_permission: this.i18n.t('js.grid.widgets.missing_permission'),
};

hasCapability$ = this.currentUser.hasCapabilities$('memberships/read', this.currentProject.id);
constructor(
protected readonly I18n:I18nService,
protected readonly injector:Injector,
protected readonly currentProject:CurrentProjectService,
protected readonly currentUser:CurrentUserService,
) {
super(I18n, injector);
}
hasCapability$ = this.currentUser.hasCapabilities$('members/read', this.currentProject.id);

public get projectIdentifier() {
return this.currentProject.identifier;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,9 @@
[resource]="resource" />
</widget-header>

@if ((projects$ | async); as projects) {
<div class="op-widget-box--body">
@if (projects.length === 0) {
<div class="op-header-project-select--no-favorites">
<svg
class="op-header-project-select--no-favorites-icon"
star-icon
size="medium"
></svg>
<p>
<strong [textContent]="text.no_favorites"></strong>
<br/>
<span class="op-header-project-select--no-favorites-subtext"
[textContent]="text.no_favorites_subtext"></span>
</p>
</div>
}
@else {
<ul class="op-widget-project-favorites--list">
@for (project of projects; track project) {
<li>
<svg
star-fill-icon
class="op-primer--star-icon"
size="small"
></svg>
<a
class="op-widget-project-favorites--link"
[href]="projectPath(project)"
[textContent]="project.name">
</a>
</li>
}
</ul>
}
</div>
}
<turbo-frame
class="op-widget-project-favorites--frame"
[id]="frameId"
[src]="src"
loading="lazy">
</turbo-frame>
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
.op-widget-project-favorites
&--list
list-style: none
display: flex
flex-direction: column
height: 100%
min-height: 0
overflow: hidden

&--link
padding-left: 0.25rem
&--frame
display: flex
flex-direction: column
flex: 1 1 0
min-height: 0
overflow: auto

&--empty
align-items: center
justify-content: center
Original file line number Diff line number Diff line change
@@ -1,66 +1,22 @@
import { AbstractWidgetComponent } from 'core-app/shared/components/grids/widgets/abstract-widget.component';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
HostBinding,
Injector,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { ProjectResource } from 'core-app/features/hal/resources/project-resource';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { ApiV3FilterBuilder } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
import { Observable } from 'rxjs';
import { AbstractTurboWidgetComponent } from 'core-app/shared/components/grids/widgets/abstract-turbo-widget.component';

@Component({
selector: 'op-project-favorites-widget',
templateUrl: './widget-project-favorites.component.html',
styleUrls: ['./widget-project-favorites.component.sass'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class WidgetProjectFavoritesComponent extends AbstractWidgetComponent implements OnInit {
export class WidgetProjectFavoritesComponent extends AbstractTurboWidgetComponent {
@HostBinding('class.op-widget-project-favorites') className = true;

public text = {
no_favorites: this.i18n.t('js.favorite_projects.no_results'),
no_favorites_subtext: this.i18n.t('js.favorite_projects.no_results_subtext'),
};

public projects$:Observable<ProjectResource[]>;

constructor(
readonly halResource:HalResourceService,
readonly pathHelper:PathHelperService,
readonly i18n:I18nService,
protected readonly injector:Injector,
readonly timezone:TimezoneService,
readonly apiV3Service:ApiV3Service,
readonly currentProject:CurrentProjectService,
readonly cdr:ChangeDetectorRef,
) {
super(i18n, injector);
}

ngOnInit() {
const filters = new ApiV3FilterBuilder();
filters.add('favorited', '=', true);
filters.add('active', '=', true);

this.projects$ = this
.apiV3Service
.projects
.filtered(filters, { sortBy: '[["name","asc"]]', pageSize: '-1' })
.getPaginatedResults();
}

projectPath(project:ProjectResource) {
return this.pathHelper.projectPath(project.identifier);
}
override frameId = 'grids-widgets-project-favorites';
override name = 'project_favorites';
}
5 changes: 5 additions & 0 deletions frontend/src/global_styles/content/_widget_box.sass
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ $widget-box--enumeration-width: 20px
border-bottom-right-radius: var(--borderRadius-medium)
border-bottom-left-radius: var(--borderRadius-medium)

.op-widget-box--footer
margin-top: auto
padding: var(--stack-padding-normal)
border-top: var(--borderWidth-thin) solid var(--borderColor-muted)

.widget-box--arrow-links
list-style: none
margin: 0.5rem 0 1rem 0
Expand Down
Loading
Loading