mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
[74198] Remove newest projects in project widget on homepage (#23136)
* Add footer component for widget box * Add footer component to the widget box as a slot * Change projects widget to show the favorite projects * Fix failing test * Change the subitems widget * Change the costs and budgets widgets * Change the meeting widget * Change the WPs widget in version * Change memebers widget in project overview * Change the favorite projects widget in my page # Conflicts: # frontend/src/app/shared/components/grids/widgets/project-favorites/widget-project-favorites.component.ts * Add the widget box to the lookbook * Add footer for members widget in dashboard * Fix members widget capability check * Add feature spec for favorites projects in my page * Remove committed demo project gitlink * Remove temporary body variables from the costs and budgets widget templates * Remove the scroll for favorites widget * Remove scrollbar for members and favorite projects widgets * Change projects block to favorite projects * Refine feature specs * Fix the widget footer styles globally * Rename the component name from project favorites to favorite projects * Rename the test selector for project name * Move widget content inside the body * grid widgets stretch their content area so widget footers stay pinned to the bottom * Ensure frontend-rendered grid widgets keep their turbo-loaded content in the widget flex layout so server-rendered footers stay pinned to the bottom
This commit is contained in:
committed by
GitHub
parent
a152141163
commit
bfa2588bf4
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# -- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
module Homescreen
|
||||
module Blocks
|
||||
class FavoriteProjects < Grids::WidgetComponent
|
||||
def call
|
||||
render(Grids::Widgets::FavoriteProjects.new(current_user:))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,57 +0,0 @@
|
||||
<%= 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>
|
||||
<% 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 %>
|
||||
|
||||
<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>
|
||||
<% 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) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -66,7 +66,7 @@ module Projects
|
||||
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)
|
||||
|
||||
@@ -38,7 +38,7 @@ OpenProject::Static::Homescreen.manage :blocks do |blocks|
|
||||
if: Proc.new { Setting.welcome_on_homescreen? && Setting.welcome_text.present? }
|
||||
},
|
||||
{
|
||||
name: "projects"
|
||||
name: "favorite_projects"
|
||||
},
|
||||
{
|
||||
name: "new_features",
|
||||
|
||||
@@ -3639,6 +3639,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"
|
||||
|
||||
@@ -79,8 +79,8 @@ import {
|
||||
TimeEntriesCurrentUserConfigurationModalComponent,
|
||||
} from './widgets/time-entries/current-user/configuration-modal/configuration.modal';
|
||||
import {
|
||||
WidgetProjectFavoritesComponent,
|
||||
} from 'core-app/shared/components/grids/widgets/project-favorites/widget-project-favorites.component';
|
||||
WidgetFavoriteProjectsComponent,
|
||||
} from 'core-app/shared/components/grids/widgets/favorite-projects/widget-favorite-projects.component';
|
||||
import { IconModule } from 'core-app/shared/components/icon/icon.module';
|
||||
import { OpenprojectEnterpriseModule } from 'core-app/features/enterprise/openproject-enterprise.module';
|
||||
import { ErrorBlankSlateComponent } from './widgets/error-blankslate/error-blankslate.component';
|
||||
@@ -128,7 +128,7 @@ import { ErrorBlankSlateComponent } from './widgets/error-blankslate/error-blank
|
||||
WidgetProjectDescriptionComponent,
|
||||
WidgetProjectStatusComponent,
|
||||
WidgetSubprojectsComponent,
|
||||
WidgetProjectFavoritesComponent,
|
||||
WidgetFavoriteProjectsComponent,
|
||||
WidgetTimeEntriesCurrentUserComponent,
|
||||
WidgetTimeEntriesProjectComponent,
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ export abstract class AbstractWidgetComponent extends UntilDestroyedMixin {
|
||||
|
||||
@HostBinding('style.grid-row-end') gridRowEnd:number;
|
||||
|
||||
@HostBinding('class.grid--widget-host') gridWidgetHost = true;
|
||||
|
||||
@Input() resource:GridWidgetResource;
|
||||
|
||||
@Output() resourceChanged = new EventEmitter<WidgetChangeset>();
|
||||
@@ -45,6 +47,7 @@ export abstract class AbstractWidgetComponent extends UntilDestroyedMixin {
|
||||
* We arbitrarily restrict this for some resources however,
|
||||
* whose component classes will set this to false.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
|
||||
public get isEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<widget-header
|
||||
[name]="widgetName"
|
||||
[editable]="false">
|
||||
|
||||
<widget-menu slot="menu"
|
||||
[resource]="resource" />
|
||||
</widget-header>
|
||||
|
||||
<turbo-frame
|
||||
class="grid--widget-content"
|
||||
[id]="frameId"
|
||||
[src]="src"
|
||||
loading="lazy">
|
||||
</turbo-frame>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostBinding,
|
||||
} from '@angular/core';
|
||||
import { AbstractTurboWidgetComponent } from 'core-app/shared/components/grids/widgets/abstract-turbo-widget.component';
|
||||
|
||||
@Component({
|
||||
selector: 'op-favorite-projects-widget',
|
||||
templateUrl: './widget-favorite-projects.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class WidgetFavoriteProjectsComponent extends AbstractTurboWidgetComponent {
|
||||
@HostBinding('class.op-widget-favorite-projects') className = true;
|
||||
|
||||
override frameId = 'grids-widgets-favorite-projects';
|
||||
override name = 'project_favorites';
|
||||
}
|
||||
@@ -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 grid--widget-content">
|
||||
<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="grid--widget-content"
|
||||
[id]="frameId"
|
||||
[src]="src"
|
||||
loading="lazy">
|
||||
</turbo-frame>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { AbstractTurboWidgetComponent } from 'core-app/shared/components/grids/widgets/abstract-turbo-widget.component';
|
||||
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',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
standalone: false,
|
||||
})
|
||||
export class WidgetMembersComponent extends AbstractTurboWidgetComponent {
|
||||
protected readonly currentProject = inject(CurrentProjectService);
|
||||
protected readonly currentUser = inject(CurrentUserService);
|
||||
|
||||
get text() {
|
||||
return { missing_permission: this.i18n.t('js.grid.widgets.missing_permission') };
|
||||
}
|
||||
text = {
|
||||
missing_permission: this.i18n.t('js.grid.widgets.missing_permission'),
|
||||
};
|
||||
|
||||
hasCapability$ = this.currentUser.hasCapabilities$('memberships/read', this.currentProject.id);
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
[resource]="resource" />
|
||||
</widget-header>
|
||||
|
||||
<turbo-frame [id]="frameId" [src]="src" loading="lazy" />
|
||||
<turbo-frame class="grid--widget-content" [id]="frameId" [src]="src" loading="lazy" />
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@
|
||||
[resource]="resource" />
|
||||
</widget-header>
|
||||
|
||||
<turbo-frame [id]="frameId" [src]="src" loading="lazy" />
|
||||
<turbo-frame class="grid--widget-content" [id]="frameId" [src]="src" loading="lazy" />
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
<widget-header
|
||||
[name]="widgetName"
|
||||
[editable]="false">
|
||||
|
||||
<widget-menu slot="menu"
|
||||
[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>
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
.op-widget-project-favorites
|
||||
&--list
|
||||
list-style: none
|
||||
|
||||
&--link
|
||||
padding-left: 0.25rem
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { AbstractWidgetComponent } from 'core-app/shared/components/grids/widgets/abstract-widget.component';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, OnInit, ViewEncapsulation, inject } from '@angular/core';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
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 {
|
||||
readonly halResource = inject(HalResourceService);
|
||||
readonly pathHelper = inject(PathHelperService);
|
||||
readonly timezone = inject(TimezoneService);
|
||||
readonly apiV3Service = inject(ApiV3Service);
|
||||
readonly currentProject = inject(CurrentProjectService);
|
||||
readonly cdr = inject(ChangeDetectorRef);
|
||||
|
||||
@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[]>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -10,4 +10,4 @@
|
||||
[resource]="resource" />
|
||||
</widget-header>
|
||||
|
||||
<turbo-frame [id]="frameId" [src]="src" loading="lazy" />
|
||||
<turbo-frame class="grid--widget-content" [id]="frameId" [src]="src" loading="lazy" />
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@
|
||||
[resource]="resource" />
|
||||
</widget-header>
|
||||
|
||||
<turbo-frame [id]="frameId" [src]="src" loading="lazy" />
|
||||
<turbo-frame class="grid--widget-content" [id]="frameId" [src]="src" loading="lazy" />
|
||||
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
} from 'core-app/shared/components/grids/widgets/project-status/project-status.component';
|
||||
import { WidgetSubprojectsComponent } from 'core-app/shared/components/grids/widgets/subprojects/subprojects.component';
|
||||
import {
|
||||
WidgetProjectFavoritesComponent,
|
||||
} from 'core-app/shared/components/grids/widgets/project-favorites/widget-project-favorites.component';
|
||||
WidgetFavoriteProjectsComponent,
|
||||
} from 'core-app/shared/components/grids/widgets/favorite-projects/widget-favorite-projects.component';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -237,7 +237,7 @@ export class GridWidgetsService {
|
||||
},
|
||||
{
|
||||
identifier: 'project_favorites',
|
||||
component: WidgetProjectFavoritesComponent,
|
||||
component: WidgetFavoriteProjectsComponent,
|
||||
title: this.I18n.t('js.grid.widgets.project_favorites.title'),
|
||||
properties: {
|
||||
name: this.I18n.t('js.grid.widgets.project_favorites.title'),
|
||||
|
||||
@@ -107,11 +107,16 @@ $grid--widget-padding: 20px 20px 20px 20px
|
||||
.grid--area-content
|
||||
height: 100%
|
||||
|
||||
ng-component, widget-wp-graph
|
||||
.grid--widget-host
|
||||
display: flex
|
||||
flex-direction: column
|
||||
height: 100%
|
||||
|
||||
> .grid--widget-content
|
||||
display: flex
|
||||
flex-direction: column
|
||||
flex: 1 1 auto
|
||||
|
||||
&.cdk-drag-preview
|
||||
overflow: hidden
|
||||
background: white
|
||||
|
||||
@@ -140,6 +140,10 @@ $widget-box--enumeration-width: 20px
|
||||
overflow-y: auto
|
||||
@include styled-scroll-bar
|
||||
|
||||
&.op-widget-box--empty
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
// styles specific to the custom-text widget
|
||||
&.-custom-text
|
||||
a.inplace-editing--trigger-link
|
||||
@@ -217,6 +221,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
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module OpenProject
|
||||
module Grids
|
||||
# @logical_path OpenProject/Grids
|
||||
# @display min_height 300px
|
||||
class WidgetBoxComponentPreview < Lookbook::Preview
|
||||
# Use the default body for generic widget content.
|
||||
def default
|
||||
render_with_template
|
||||
end
|
||||
|
||||
# Use rows for the primary repeated widget content.
|
||||
def with_rows
|
||||
render_with_template
|
||||
end
|
||||
|
||||
# Use the footer for secondary navigation or actions that should stay at the bottom,
|
||||
# such as "View all ..." links.
|
||||
def with_footer
|
||||
render_with_template
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="widget-boxes -flex">
|
||||
<%= render(::Grids::WidgetBoxComponent.new(key: "widget-box-preview", title: "Widget title")) do |widget| %>
|
||||
<% widget.with_body do %>
|
||||
Widget body content
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<div class="widget-boxes -flex" style="height: 280px;">
|
||||
<%= render(::Grids::WidgetBoxComponent.new(key: "widget-box-footer-preview", title: "Widget title")) do |widget| %>
|
||||
<% widget.with_row do %>
|
||||
First row
|
||||
<% end %>
|
||||
|
||||
<% widget.with_row do %>
|
||||
Second row
|
||||
<% end %>
|
||||
|
||||
<% widget.with_footer do %>
|
||||
<%= render(Primer::Beta::Link.new(href: "#")) { "View all items" } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="widget-boxes -flex">
|
||||
<%= render(::Grids::WidgetBoxComponent.new(key: "widget-box-rows-preview", title: "Widget title")) do |widget| %>
|
||||
<% widget.with_row do %>
|
||||
First row
|
||||
<% end %>
|
||||
|
||||
<% widget.with_row do %>
|
||||
Second row
|
||||
<% end %>
|
||||
|
||||
<% widget.with_row do %>
|
||||
Third row
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -28,35 +28,40 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
++# %>
|
||||
|
||||
<%=
|
||||
widget_wrapper do
|
||||
widget_wrapper do |container|
|
||||
if has_budgets_data?
|
||||
flex_layout do |flex|
|
||||
flex.with_row do
|
||||
render(Primer::Beta::Text.new(color: :subtle, font_size: :small)) do
|
||||
if project.children.any?
|
||||
t(".count_caption_with_subitems.#{project.workspace_type.presence_in(%w[project program portfolio]) || 'project'}",
|
||||
count: budget_count)
|
||||
else
|
||||
t(".count_caption", count: budget_count)
|
||||
container.with_body do
|
||||
flex_layout do |flex|
|
||||
flex.with_row do
|
||||
render(Primer::Beta::Text.new(color: :subtle, font_size: :small)) do
|
||||
if project.children.any?
|
||||
t(
|
||||
".count_caption_with_subitems.#{project.workspace_type.presence_in(%w[project program portfolio]) || 'project'}",
|
||||
count: budget_count
|
||||
)
|
||||
else
|
||||
t(".count_caption", count: budget_count)
|
||||
end
|
||||
end
|
||||
end
|
||||
flex.with_row(mt: 3, align: :center) do
|
||||
helpers.angular_component_tag(
|
||||
"opce-budget-by-cost-type",
|
||||
currency: Setting.costs_currency,
|
||||
"chart-data": {
|
||||
labels: chart_labels,
|
||||
datasets: [{
|
||||
label: t(:label_budget),
|
||||
data: chart_data
|
||||
}]
|
||||
}.to_json
|
||||
)
|
||||
end
|
||||
end
|
||||
flex.with_row(mt: 3, align: :center) do
|
||||
helpers.angular_component_tag(
|
||||
"opce-budget-by-cost-type",
|
||||
currency: Setting.costs_currency,
|
||||
"chart-data": {
|
||||
labels: chart_labels,
|
||||
datasets: [{
|
||||
label: t(:label_budget),
|
||||
data: chart_data
|
||||
}]
|
||||
}.to_json
|
||||
)
|
||||
end
|
||||
flex.with_row(mt: 3) do
|
||||
render(Primer::Beta::Link.new(href: projects_budgets_path(project))) { t(".view_details") }
|
||||
end
|
||||
end
|
||||
|
||||
container.with_footer(test_selector: "budget-by-cost-type-widget-footer") do
|
||||
render(Primer::Beta::Link.new(href: projects_budgets_path(project))) { t(".view_details") }
|
||||
end
|
||||
elsif has_budgets?
|
||||
render(Primer::Beta::Blankslate.new) do |blankslate|
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Budgets::Widgets::BudgetByCostType, type: :component do
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def render_component(...)
|
||||
render_inline(described_class.new(...))
|
||||
end
|
||||
@@ -65,6 +67,12 @@ RSpec.describe Budgets::Widgets::BudgetByCostType, type: :component do
|
||||
expect(rendered_component).to have_css("opce-budget-by-cost-type")
|
||||
end
|
||||
|
||||
it "renders view details link in the widget footer" do
|
||||
expect(rendered_component).to have_test_selector("budget-by-cost-type-widget-footer") do |footer|
|
||||
expect(footer).to have_link(href: projects_budgets_path(project))
|
||||
end
|
||||
end
|
||||
|
||||
it "displays caption with workspace type (no subitems for leaf project)" do
|
||||
expect(rendered_component).to have_text("Data aggregated from 1 budget")
|
||||
expect(rendered_component).to have_no_text(/Project/)
|
||||
|
||||
@@ -28,23 +28,26 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
++# %>
|
||||
|
||||
<%=
|
||||
widget_wrapper do
|
||||
widget_wrapper do |container|
|
||||
if has_spending_data?
|
||||
flex_layout do |flex|
|
||||
flex.with_row(mt: 3, align: :center) do
|
||||
helpers.angular_component_tag(
|
||||
"opce-actual-costs",
|
||||
currency: Setting.costs_currency,
|
||||
"chart-data": {
|
||||
labels: chart_labels,
|
||||
datasets: chart_datasets
|
||||
}.to_json
|
||||
)
|
||||
end
|
||||
flex.with_row(mt: 3) do
|
||||
render(Primer::Beta::Link.new(href: cost_reports_path(project))) { t(".view_details") }
|
||||
container.with_body do
|
||||
flex_layout do |flex|
|
||||
flex.with_row(mt: 3, align: :center) do
|
||||
helpers.angular_component_tag(
|
||||
"opce-actual-costs",
|
||||
currency: Setting.costs_currency,
|
||||
"chart-data": {
|
||||
labels: chart_labels,
|
||||
datasets: chart_datasets
|
||||
}.to_json
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
container.with_footer(test_selector: "actual-costs-widget-footer") do
|
||||
render(Primer::Beta::Link.new(href: cost_reports_path(project))) { t(".view_details") }
|
||||
end
|
||||
else
|
||||
render(Primer::Beta::Blankslate.new) do |blankslate|
|
||||
blankslate.with_visual_icon(icon: :"op-cost-reports")
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Costs::Widgets::ActualCosts, type: :component do
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def render_component(...)
|
||||
render_inline(described_class.new(...))
|
||||
end
|
||||
@@ -70,6 +72,12 @@ RSpec.describe Costs::Widgets::ActualCosts, type: :component do
|
||||
expect(rendered_component).to have_css("opce-actual-costs")
|
||||
end
|
||||
|
||||
it "renders view details link in the widget footer" do
|
||||
expect(rendered_component).to have_test_selector("actual-costs-widget-footer") do |footer|
|
||||
expect(footer).to have_link(href: cost_reports_path(project))
|
||||
end
|
||||
end
|
||||
|
||||
it "passes currency attribute" do
|
||||
expect(rendered_component).to have_element "opce-actual-costs" do |element|
|
||||
expect(element["currency"]).to eq(Setting.costs_currency)
|
||||
|
||||
@@ -39,5 +39,6 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= footer %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -40,6 +40,7 @@ module Grids
|
||||
}
|
||||
|
||||
renders_one :body, Body
|
||||
renders_one :footer, Footer
|
||||
|
||||
renders_many :rows, Row
|
||||
|
||||
@@ -88,7 +89,7 @@ module Grids
|
||||
end
|
||||
|
||||
def render?
|
||||
rows.any? || header? || body?
|
||||
rows.any? || header? || body? || footer?
|
||||
end
|
||||
|
||||
def default_header
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Grids
|
||||
class WidgetBoxComponent < ApplicationComponent
|
||||
class Footer < ApplicationComponent
|
||||
def initialize(**system_arguments)
|
||||
super()
|
||||
|
||||
@system_arguments = system_arguments
|
||||
@system_arguments[:tag] = :div
|
||||
@system_arguments[:classes] = class_names(
|
||||
"op-widget-box--footer",
|
||||
system_arguments[:classes]
|
||||
)
|
||||
end
|
||||
|
||||
def call
|
||||
render(Primer::BaseComponent.new(**@system_arguments)) { content }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,73 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%=
|
||||
widget_wrapper do |container|
|
||||
if favorite_projects.empty?
|
||||
container.with_body(classes: "op-widget-box--empty") do
|
||||
render(Primer::Beta::Blankslate.new(test_selector: "favorite-projects-widget-empty")) do |component|
|
||||
component.with_visual_icon(icon: :project)
|
||||
component.with_heading(tag: :h3).with_content(I18n.t("homescreen.additional.favorite_projects.no_results"))
|
||||
component.with_description { I18n.t("homescreen.additional.favorite_projects.no_results_subtext") }
|
||||
end
|
||||
end
|
||||
else
|
||||
favorite_projects.each do |project|
|
||||
container.with_row do
|
||||
helpers.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
|
||||
|
||||
row.with_column(ml: 2) do
|
||||
render(
|
||||
Primer::Beta::Link.new(
|
||||
font_weight: :bold,
|
||||
href: helpers.project_path(project),
|
||||
title: short_project_description(project),
|
||||
data: { "test-selector": "favorite-projects-widget--project-name" }
|
||||
)
|
||||
) { project.name }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
container.with_footer(test_selector: "favorite-projects-widget-footer") do
|
||||
render(Primer::Beta::Link.new(href: helpers.projects_path)) { I18n.t(:label_project_view_all) }
|
||||
end
|
||||
end
|
||||
end
|
||||
%>
|
||||
+10
-15
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# -- copyright
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
@@ -26,24 +26,19 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
#++
|
||||
|
||||
module Homescreen
|
||||
module Blocks
|
||||
class Projects < Grids::WidgetComponent
|
||||
include IconsHelper
|
||||
module Grids
|
||||
module Widgets
|
||||
class FavoriteProjects < Grids::WidgetComponent
|
||||
include ProjectsHelper
|
||||
include Redmine::I18n
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
|
||||
@favorite_projects = Project.visible.active.favorited_by(current_user)
|
||||
@newest_projects = Project.visible.newest.take(3)
|
||||
end
|
||||
|
||||
def title
|
||||
I18n.t(:label_project_plural)
|
||||
I18n.t("projects.lists.favorited")
|
||||
end
|
||||
|
||||
def favorite_projects
|
||||
@favorite_projects ||= Project.visible.active.favorited_by(current_user).order(name: :asc).to_a
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -28,7 +28,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
++#%>
|
||||
|
||||
<%=
|
||||
widget_wrapper do
|
||||
widget_wrapper do |container|
|
||||
if can_view_members?
|
||||
if members_by_role.any?
|
||||
flex_layout(style: "gap: var(--base-size-8, 8px)") do |flex|
|
||||
@@ -41,29 +41,30 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
flex.with_row(mb: 2) do
|
||||
concat render(
|
||||
Users::AvatarComponent
|
||||
.with_collection(
|
||||
role_data[:members],
|
||||
size: :mini,
|
||||
link: false,
|
||||
show_name: true,
|
||||
spacer_component:
|
||||
)
|
||||
)
|
||||
Users::AvatarComponent
|
||||
.with_collection(
|
||||
role_data[:members],
|
||||
size: :mini,
|
||||
link: false,
|
||||
show_name: true,
|
||||
spacer_component:
|
||||
)
|
||||
)
|
||||
|
||||
if role_data[:has_more]
|
||||
concat render(
|
||||
Primer::Beta::Link.new(
|
||||
href: project_members_path(@project, role_id: role_data[:role].id),
|
||||
ml: 1,
|
||||
font_weight: :semibold,
|
||||
classes: "op-principal"
|
||||
).with_content(t(".x_more", count: role_data[:remaining])))
|
||||
Primer::Beta::Link.new(
|
||||
href: project_members_path(@project, role_id: role_data[:role].id),
|
||||
ml: 1,
|
||||
font_weight: :semibold,
|
||||
classes: "op-principal"
|
||||
).with_content(t(".x_more", count: role_data[:remaining]))
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
flex.with_row(mt: 4) do
|
||||
container.with_footer(test_selector: "members-widget-footer") do
|
||||
render Primer::Beta::Link.new(href: project_members_path(@project)) do
|
||||
t(".view_all_members")
|
||||
end
|
||||
|
||||
@@ -88,7 +88,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
end
|
||||
|
||||
if has_more_subitems?
|
||||
container.with_row do
|
||||
container.with_footer do
|
||||
render(Primer::Beta::Link.new(href: view_all_subitems_path)) { t(".view_all_subitems") }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class Grids::Widgets::FavoriteProjectsController < Grids::WidgetController
|
||||
skip_before_action :load_and_authorize_in_optional_project
|
||||
|
||||
no_authorization_required! :show
|
||||
|
||||
def show
|
||||
render_widget Grids::Widgets::FavoriteProjects.new(current_user:)
|
||||
end
|
||||
end
|
||||
@@ -33,7 +33,9 @@ en:
|
||||
title: 'Subitems'
|
||||
project_favorites:
|
||||
title: 'Favorite projects'
|
||||
no_results: 'You currently have no favorite projects. Click on the star icon in the project dashboard to add one to your favorites.'
|
||||
no_results: >-
|
||||
You currently have no favorite projects. Add one or multiple projects
|
||||
as favorite through their overview or in a project list.
|
||||
time_entries_current_user:
|
||||
title: 'My spent time'
|
||||
displayed_days: 'Days displayed in the widget:'
|
||||
|
||||
@@ -44,6 +44,7 @@ Rails.application.routes.draw do
|
||||
# global widget routes
|
||||
namespace :widgets do
|
||||
resource :news, only: %i[show]
|
||||
resource :project_favorites, controller: :favorite_projects, only: %i[show]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,4 +45,16 @@ RSpec.describe Grids::WidgetBoxComponent, type: :component do
|
||||
it "renders turbo-frame around content" do
|
||||
expect(rendered_component).to have_element :"turbo-frame", id: "cool_widget", target: "_top"
|
||||
end
|
||||
|
||||
context "with footer content" do
|
||||
subject(:rendered_component) do
|
||||
render_inline(described_class.new(key: "cool_widget", title: "Cool Widget")) do |component|
|
||||
component.with_footer { "Footer link" }
|
||||
end
|
||||
end
|
||||
|
||||
it "renders a widget footer" do
|
||||
expect(rendered_component).to have_css ".op-widget-box--footer", text: "Footer link"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Grids::Widgets::FavoriteProjects, type: :component do
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
shared_let(:admin) { create(:admin) }
|
||||
|
||||
current_user { admin }
|
||||
|
||||
subject(:rendered_component) { render_inline(described_class.new(current_user: admin)) }
|
||||
|
||||
context "with no favorite projects" do
|
||||
let!(:visible_project) { create(:project, name: "Visible project") }
|
||||
|
||||
it "renders the empty state with the project icon" do
|
||||
expect(rendered_component).to have_css("h3", text: "Favorite projects")
|
||||
expect(rendered_component).to have_test_selector("favorite-projects-widget-empty")
|
||||
expect(rendered_component).to have_octicon(:project)
|
||||
expect(rendered_component).to have_text("You have no favorite projects")
|
||||
expect(rendered_component).to have_no_link(visible_project.name)
|
||||
expect(rendered_component).to have_no_link("View all projects")
|
||||
end
|
||||
end
|
||||
|
||||
context "with favorite projects" do
|
||||
let!(:favorite_project) { create(:project, name: "Favorite project") }
|
||||
let!(:visible_project) { create(:project, name: "Visible project") }
|
||||
|
||||
before do
|
||||
create(:favorite, user: admin, favorited: favorite_project)
|
||||
end
|
||||
|
||||
it "renders only favorite projects and a link to all projects" do
|
||||
expect(rendered_component).to have_css("turbo-frame#grids-widgets-favorite-projects")
|
||||
expect(rendered_component).to have_css("h3", text: "Favorite projects")
|
||||
expect(rendered_component).to have_link(favorite_project.name, href: project_path(favorite_project))
|
||||
expect(rendered_component).to have_test_selector("favorite-projects-widget--project-name", text: favorite_project.name)
|
||||
expect(rendered_component).to have_test_selector("favorite-projects-widget-footer") do |footer|
|
||||
expect(footer).to have_link("View all projects", href: projects_path)
|
||||
end
|
||||
|
||||
expect(rendered_component).to have_no_link(visible_project.name)
|
||||
end
|
||||
|
||||
context "when a favorited project is not visible to the user" do
|
||||
let(:user) { create(:user) }
|
||||
let!(:favorite_project) { create(:public_project, name: "Visible favorite project") }
|
||||
let!(:invisible_favorite_project) { create(:private_project, name: "Invisible favorite project") }
|
||||
|
||||
current_user { user }
|
||||
|
||||
subject(:rendered_component) { render_inline(described_class.new(current_user: user)) }
|
||||
|
||||
before do
|
||||
create(:favorite, user:, favorited: favorite_project)
|
||||
create(:favorite, user:, favorited: invisible_favorite_project)
|
||||
end
|
||||
|
||||
it "only renders visible favorite projects" do
|
||||
expect(rendered_component).to have_link(favorite_project.name, href: project_path(favorite_project))
|
||||
expect(rendered_component).to have_no_link(invisible_favorite_project.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -112,13 +112,14 @@ RSpec.describe Grids::Widgets::Members, type: :component do
|
||||
|
||||
it "renders members items", :aggregate_failures do
|
||||
expect(rendered_component).to have_element class: "op-widget-box--body" do |body|
|
||||
expect(body).to have_link href: project_members_path(project)
|
||||
expect(body).to have_element :"opce-principal"
|
||||
end
|
||||
end
|
||||
|
||||
it "renders link to view all members" do
|
||||
expect(rendered_component).to have_link href: project_members_path(project)
|
||||
expect(rendered_component).to have_test_selector("members-widget-footer") do |footer|
|
||||
expect(footer).to have_link href: project_members_path(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -166,10 +166,13 @@ RSpec.describe Grids::Widgets::Subitems, type: :component do
|
||||
context "and a limit less than the number of all subitems" do
|
||||
let(:params) { { limit: 2 } }
|
||||
|
||||
it "renders specified subitems, along with a 'view all' item", :aggregate_failures do
|
||||
it "renders specified subitems and a footer link to view all subitems", :aggregate_failures do
|
||||
expect(rendered_component).to have_list "Subitems" do |list|
|
||||
expect(list).to have_list_item count: 2, text: /My Project No. \d+/
|
||||
expect(list).to have_list_item text: "View all subitems"
|
||||
end
|
||||
|
||||
expect(rendered_component).to have_css(".op-widget-box--footer") do |footer|
|
||||
expect(footer).to have_link "View all subitems"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Grids::Widgets::FavoriteProjectsController do
|
||||
shared_let(:user) { create(:user) }
|
||||
current_user { user }
|
||||
|
||||
describe "GET #show" do
|
||||
let(:widget_instance) { instance_double(Grids::Widgets::FavoriteProjects, render_in: "content") }
|
||||
|
||||
before do
|
||||
allow(Grids::Widgets::FavoriteProjects)
|
||||
.to receive(:new)
|
||||
.and_return(widget_instance)
|
||||
|
||||
get :show
|
||||
end
|
||||
|
||||
it "renders widget", :aggregate_failures do
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq "content"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -139,6 +139,24 @@ RSpec.describe Grids::WidgetController do
|
||||
end
|
||||
end
|
||||
|
||||
describe "project_favorites routing" do
|
||||
describe "GET #show" do
|
||||
it do
|
||||
expect(get("/widgets/project_favorites"))
|
||||
.to route_to(controller: "grids/widgets/favorite_projects", action: "show")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "project_favorites named routing" do
|
||||
describe "GET #show" do
|
||||
it do
|
||||
expect(get(widgets_project_favorites_path))
|
||||
.to route_to(controller: "grids/widgets/favorite_projects", action: "show")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "subitems routing" do
|
||||
describe "GET #show" do
|
||||
it do
|
||||
|
||||
@@ -68,7 +68,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
end
|
||||
end
|
||||
|
||||
container.with_row do
|
||||
container.with_footer do
|
||||
render(Primer::Beta::Link.new(href: all_meetings_link)) { t("meeting.widgets.view_details") }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -87,22 +87,22 @@ RSpec.describe Meetings::Widgets::Meetings, type: :component do
|
||||
let!(:meeting_other) { create(:meeting, project: project_red, author:, start_time: 3.weeks.from_now) }
|
||||
|
||||
it "does not render meetings the user is not participating in" do
|
||||
expect(rendered_component).to have_list_item(count: 3) # 2 participating + "View all"
|
||||
expect(rendered_component).to have_list_item(count: 2)
|
||||
expect(rendered_component).to have_no_link href: project_meeting_path(project_red, meeting_other)
|
||||
end
|
||||
end
|
||||
|
||||
it "renders meetings items from all projects", :aggregate_failures do
|
||||
expect(rendered_component).to have_list_item(count: 3)
|
||||
expect(rendered_component).to have_list_item(count: 2)
|
||||
expect(rendered_component).to have_link href: project_meeting_path(project_red, meeting_red)
|
||||
expect(rendered_component).to have_list_item(position: 2) do |item|
|
||||
expect(item).to have_link href: project_meeting_path(project_blue, meeting_blue)
|
||||
expect(item).to have_content("2 hrs") # Duration is formatted
|
||||
expect(item).to have_content("Project: #{project_blue.name}")
|
||||
expect(item).to have_text("2 hrs")
|
||||
expect(item).to have_text("Project: #{project_blue.name}")
|
||||
end
|
||||
|
||||
expect(rendered_component).to have_list_item(position: 3) do |item|
|
||||
expect(item).to have_link href: meetings_path
|
||||
expect(item).to have_content("View all meetings")
|
||||
expect(rendered_component).to have_css(".op-widget-box--footer") do |footer|
|
||||
expect(footer).to have_link "View all meetings", href: meetings_path
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -129,16 +129,15 @@ RSpec.describe Meetings::Widgets::Meetings, type: :component do
|
||||
end
|
||||
|
||||
it "renders only this project’s meetings which the user participates in" do
|
||||
expect(rendered_component).to have_list_item(count: 2)
|
||||
expect(rendered_component).to have_list_item(count: 1)
|
||||
expect(rendered_component).to have_list_item(position: 1) do |item|
|
||||
expect(item).to have_link href: project_meeting_path(project_red, meeting_red)
|
||||
expect(item).to have_content("1 hr")
|
||||
expect(item).to have_no_content("Project: #{project_red.name}") # Project is not repeated
|
||||
expect(item).to have_text("1 hr")
|
||||
expect(item).to have_no_text("Project: #{project_red.name}") # Project is not repeated
|
||||
end
|
||||
|
||||
expect(rendered_component).to have_list_item(position: 2) do |item|
|
||||
expect(item).to have_link href: project_meetings_path(project_red)
|
||||
expect(item).to have_content("View all meetings")
|
||||
expect(rendered_component).to have_css(".op-widget-box--footer") do |footer|
|
||||
expect(footer).to have_link "View all meetings", href: project_meetings_path(project_red)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module MyPage
|
||||
class GridRegistration < ::Grids::Configuration::Registration
|
||||
grid_class "Grids::MyPage"
|
||||
|
||||
@@ -54,7 +54,7 @@ RSpec.describe "onboarding tour for new users",
|
||||
select "Deutsch", from: "user_language"
|
||||
click_button "Save"
|
||||
|
||||
expect(page).to have_text "Neueste sichtbare Projekte in dieser Instanz."
|
||||
expect(page).to have_text "Favorisierte Projekte"
|
||||
end
|
||||
|
||||
it "I can start the tour without selecting a language" do
|
||||
|
||||
@@ -85,7 +85,7 @@ RSpec.describe "Favorite projects", :js do
|
||||
visit home_path
|
||||
|
||||
expect(page).to have_text "Favorite projects"
|
||||
expect(page).to have_test_selector "favorite-project", text: "My favorite!"
|
||||
expect(page).to have_test_selector "favorite-projects-widget--project-name", text: "My favorite!"
|
||||
|
||||
retry_block do
|
||||
top_menu.toggle unless top_menu.open?
|
||||
@@ -127,7 +127,7 @@ RSpec.describe "Favorite projects", :js do
|
||||
visit home_path
|
||||
|
||||
expect(page).to have_text "Favorite projects"
|
||||
expect(page).to have_test_selector "favorite-project", text: "My favorite!"
|
||||
expect(page).to have_test_selector "favorite-projects-widget--project-name", text: "My favorite!"
|
||||
expect(page).to have_no_text "Other project"
|
||||
|
||||
my_page.visit!
|
||||
@@ -146,7 +146,7 @@ RSpec.describe "Favorite projects", :js do
|
||||
visit home_path
|
||||
|
||||
expect(page).to have_text "Favorite projects"
|
||||
expect(page).to have_test_selector "favorite-project", text: "My favorite!"
|
||||
expect(page).to have_test_selector "favorite-projects-widget--project-name", text: "My favorite!"
|
||||
|
||||
retry_block do
|
||||
top_menu.toggle unless top_menu.open?
|
||||
|
||||
Reference in New Issue
Block a user