Skip to content
Draft
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
151 changes: 151 additions & 0 deletions app/assets/images/templates/example.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions app/assets/images/templates/pmflexone.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions app/components/projects/settings/general/show_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,24 @@ See COPYRIGHT and LICENSE files for more details.
)
end
end

container.with_row(mb: 3) do
settings_primer_form_with(model: project, url: project_settings_general_path(project)) do |f|
concat(
render(Primer::Beta::Subhead.new) do |component|
component.with_heading(tag: :h3, size: :medium) { I18n.t("projects.settings.header_icon") }
end
)

concat(
render(
Primer::Forms::FormList.new(
Projects::Settings::IconForm.new(f),
Projects::Settings::Submit.new(f, label: I18n.t("projects.settings.button_update_icon"))
)
)
)
end
end
end
%>
9 changes: 9 additions & 0 deletions app/contracts/projects/base_contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class BaseContract < ::ModelContract
attribute :templated do
validate_templated_set_by_admin
end
attribute :icon do
validate_templated_present
end
Comment on lines +55 to +57
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation method validate_templated_present is incorrect for the icon attribute. This validator checks if model.templated.nil? and adds an error to :templated, but it's being used for the :icon attribute. This will validate the wrong attribute and produce confusing error messages. The icon validation should either validate icon-specific requirements or be removed if no validation is needed.

Suggested change
attribute :icon do
validate_templated_present
end
attribute :icon

Copilot uses AI. Check for mistakes.

validate :validate_user_allowed_to_manage

Expand Down Expand Up @@ -115,6 +118,12 @@ def validate_status_code_included
errors.add :status, :inclusion if model.status_code && Project.status_codes.keys.exclude?(model.status_code.to_s)
end

def validate_templated_present
if model.templated.nil?
errors.add(:templated, :blank)
end
end

def validate_templated_set_by_admin
if model.templated_changed? && !user.admin?
errors.add :templated, :error_unauthorized
Expand Down
38 changes: 38 additions & 0 deletions app/forms/projects/settings/icon_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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 Projects
module Settings
class IconForm < ApplicationForm
form do |f|
f.file_field name: :icon, label: attribute_name(:icon), accept: "foo", size: "15"
Comment on lines +33 to +34
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The accept parameter has a placeholder value 'foo' which should be replaced with appropriate file type restrictions (e.g., 'image/svg+xml,image/png,image/jpeg'). The size parameter value '15' also appears arbitrary and should be documented or replaced with a meaningful constant.

Suggested change
form do |f|
f.file_field name: :icon, label: attribute_name(:icon), accept: "foo", size: "15"
# The size of the file input field for project icon uploads.
ICON_FILE_FIELD_SIZE = 15
# Only allow SVG, PNG, and JPEG image uploads for project icons.
ICON_FILE_ACCEPT_TYPES = "image/svg+xml,image/png,image/jpeg"
form do |f|
f.file_field name: :icon, label: attribute_name(:icon), accept: ICON_FILE_ACCEPT_TYPES, size: ICON_FILE_FIELD_SIZE

Copilot uses AI. Check for mistakes.
end
end
end
end
2 changes: 1 addition & 1 deletion app/forms/projects/template_select_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class TemplateSelectForm < ApplicationForm
value: template.id,
label: template.name,
caption: format_caption(template.description),
icon: agile_image, # TODO: support customizable icons (OP #69068)
icon: template.icon,
checked: template.id == template_id
)
end
Expand Down
37 changes: 37 additions & 0 deletions app/models/concerns/has_icon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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 HasIcon
extend ActiveSupport::Concern

included do
has_one :icon, as: :iconifiable, dependent: :destroy
end
end
31 changes: 31 additions & 0 deletions app/models/custom_icon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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 CustomIcon < Icon
end
4 changes: 4 additions & 0 deletions app/models/icon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class Icon < ApplicationRecord
end
Comment on lines +3 to +4
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Icon model is missing a copyright header. All other ApplicationRecord models in the codebase include the standard OpenProject copyright notice (see color.rb, project.rb, etc. for examples).

Copilot uses AI. Check for mistakes.
31 changes: 31 additions & 0 deletions app/models/octicon_icon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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 OcticonIcon < Icon
end
1 change: 1 addition & 0 deletions app/models/permitted_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ def project
:templated,
:status_code,
:status_explanation,
:icon,
work_package_custom_field_ids: [],
type_ids: [],
enabled_module_names: [])
Expand Down
2 changes: 2 additions & 0 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class Project < ApplicationRecord

include ::Scopes::Scoped

include HasIcon

# Maximum length for project identifiers
IDENTIFIER_MAX_LENGTH = 100

Expand Down
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,11 @@ en:
header_details: Basic details
header_status: Project status
header_relations: Project relations
header_icon: Template icon
button_update_details: Update details
button_update_status_description: Update status description
button_update_parent_project: Update parent project
button_update_icon: Update icon
public_warning: >
This project is public.
Anyone who has access to this instance will be able to view and interact with this project depending on their role and associated permissions.
Expand Down Expand Up @@ -1995,6 +1997,7 @@ en:
group: "Group"
groups: "Groups"
hexcode: "Hex code"
icon: "Icon"
id: "ID"
is_default: "Default value"
is_for_all: "For all projects"
Expand Down
46 changes: 46 additions & 0 deletions db/migrate/20251111183800_create_icons.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 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 CreateIcons < ActiveRecord::Migration[8.0]
def change
create_table :icons do |t|
t.string :type, null: false

t.string :octicon_name
t.string :custom_file

t.references :iconifiable, polymorphic: true, null: false
t.timestamps
end

add_check_constraint :icons,
"(octicon_name IS NOT NULL) <> (custom_file IS NOT NULL)",
name: "only_one_icon_source"
end
end
1 change: 1 addition & 0 deletions frontend/src/custom-elements/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './primer-file-field';
40 changes: 40 additions & 0 deletions frontend/src/custom-elements/primer-file-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { controller, target } from '@github/catalyst';

@controller
class PrimerFileFieldElement extends HTMLElement {
@target inputElement:HTMLInputElement;

#abortController:AbortController | null;

@target button:HTMLElement;
@target label:HTMLElement;

connectedCallback() {
this.#abortController?.abort();
const {signal} = (this.#abortController = new AbortController());

this.inputElement.addEventListener('change', () => {
const files = this.inputElement.files!;
if (files.length === 0) {
this.label.textContent = 'no file selected';
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded text 'no file selected' should be internationalized using the i18n system like other user-facing strings in OpenProject. Consider using a translation key or accepting the text as a data attribute from the server-rendered HTML.

Suggested change
this.label.textContent = 'no file selected';
this.label.textContent = this.getAttribute('data-no-file-selected-label') || 'no file selected';

Copilot uses AI. Check for mistakes.
} else {
this.label.textContent = files[0].name;
}
}, { signal });
}

disconnectedCallback() {
this.#abortController?.abort();
}
}

// if (!window.customElements.get('primer-file-field')) {
// window.PrimerFileFieldElement = PrimerFileFieldElement
// window.customElements.define('primer-file-field', PrimerFileFieldElement)
// }
Comment on lines +31 to +34
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented-out code should be removed. The custom element registration is essential for the component to work; if this code is needed, it should be uncommented and properly integrated. If it's handled elsewhere, the commented code should be deleted.

Suggested change
// if (!window.customElements.get('primer-file-field')) {
// window.PrimerFileFieldElement = PrimerFileFieldElement
// window.customElements.define('primer-file-field', PrimerFileFieldElement)
// }
if (!window.customElements.get('primer-file-field')) {
window.PrimerFileFieldElement = PrimerFileFieldElement
window.customElements.define('primer-file-field', PrimerFileFieldElement)
}

Copilot uses AI. Check for mistakes.

declare global {
interface Window {
PrimerFileFieldElement:typeof PrimerFileFieldElement
}
}
34 changes: 34 additions & 0 deletions frontend/src/global_styles/openproject/_forms.sass
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,37 @@ input[type="number"]
.FormControl-advanced-checkbox-icon,
.FormControl-advanced-radio-icon
fill: var(--fgColor-accent)

.FormControl-file-wrap
position: relative
display: inline-flex
align-items: center
gap: 0.5rem
border: 1px solid #ccc
border-radius: 4px
padding: 0.4rem 0.5rem 0.4rem 0.5rem
width: 280px
box-sizing: border-box

.FormControl-file
position: absolute
inset: 0
opacity: 0
cursor: pointer

.file-label
flex: 1
font-size: 0.875rem
color: #666
white-space: nowrap
overflow: hidden
text-overflow: ellipsis

.file-button
padding: 0.35rem 0.75rem
border-radius: 4px
border: 1px solid #ccc
background: #f5f5f5
font-size: 0.85rem
cursor: pointer
white-space: nowrap
3 changes: 3 additions & 0 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { getMetaElement } from 'core-app/core/setup/globals/global-helpers';

import 'core-app/core/setup/init-vendors';
import 'core-app/core/setup/init-globals';

import './custom-elements';

import './stimulus/setup';
import './turbo/setup';
import { platformBrowser } from '@angular/platform-browser';
Expand Down
Loading
Loading