mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge branch 'dev' into feature/63912-support-multiple-authentication-provider-user-links
This commit is contained in:
+1
-1
@@ -142,7 +142,7 @@ Naming/ClassAndModuleCamelCase:
|
||||
Naming/FileName:
|
||||
Enabled: false
|
||||
|
||||
Naming/PredicateName:
|
||||
Naming/PredicatePrefix:
|
||||
ForbiddenPrefixes:
|
||||
- is_
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ ruby File.read(File.expand_path(".ruby-version", __dir__)).strip
|
||||
|
||||
gem "actionpack-xml_parser", "~> 2.0.0"
|
||||
gem "activemodel-serializers-xml", "~> 1.0.1"
|
||||
gem "activerecord-import", "~> 2.1.0"
|
||||
gem "activerecord-import", "~> 2.2.0"
|
||||
gem "activerecord-session_store", "~> 2.2.0"
|
||||
gem "ox"
|
||||
gem "rails", "~> 8.0.1"
|
||||
@@ -62,7 +62,7 @@ gem "friendly_id", "~> 5.5.0"
|
||||
gem "acts_as_list", "~> 1.2.0"
|
||||
gem "acts_as_tree", "~> 2.9.0"
|
||||
gem "awesome_nested_set", "~> 3.8.0"
|
||||
gem "closure_tree", "~> 7.4.0"
|
||||
gem "closure_tree", "~> 8.0.0"
|
||||
gem "rubytree", "~> 2.1.0"
|
||||
# Only used in down migrations now.
|
||||
# Is to be removed once the referencing migrations have been squashed.
|
||||
@@ -225,7 +225,7 @@ gem "appsignal", "~> 3.10.0", require: false
|
||||
|
||||
# Yabeda integration
|
||||
gem "yabeda-activerecord"
|
||||
gem "yabeda-prometheus-mmap"
|
||||
gem "yabeda-prometheus-mmap", require: false
|
||||
gem "yabeda-puma-plugin"
|
||||
gem "yabeda-rails"
|
||||
|
||||
@@ -400,7 +400,7 @@ platforms :mri, :mingw, :x64_mingw do
|
||||
|
||||
# Have application level locks on the database to have a mutex shared between workers/hosts.
|
||||
# We e.g. employ this to safeguard the creation of journals.
|
||||
gem "with_advisory_lock", "~> 5.1.0"
|
||||
gem "with_advisory_lock", "~> 5.3.0"
|
||||
end
|
||||
|
||||
# Load Gemfile.modules explicitly to allow dependabot to work
|
||||
|
||||
+70
-70
@@ -208,7 +208,7 @@ PATH
|
||||
remote: modules/two_factor_authentication
|
||||
specs:
|
||||
openproject-two_factor_authentication (1.0.0)
|
||||
aws-sdk-sns (~> 1.99.0)
|
||||
aws-sdk-sns (~> 1.100.0)
|
||||
messagebird-rest (~> 1.4.2)
|
||||
rotp (~> 6.1)
|
||||
webauthn (~> 3.0)
|
||||
@@ -289,7 +289,7 @@ GEM
|
||||
activemodel (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
timeout (>= 0.4.0)
|
||||
activerecord-import (2.1.0)
|
||||
activerecord-import (2.2.0)
|
||||
activerecord (>= 4.2)
|
||||
activerecord-nulldb-adapter (1.1.1)
|
||||
activerecord (>= 6.0, < 8.1)
|
||||
@@ -342,26 +342,26 @@ GEM
|
||||
activerecord (>= 4.0)
|
||||
awesome_nested_set (3.8.0)
|
||||
activerecord (>= 4.0.0, < 8.1)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1105.0)
|
||||
aws-sdk-core (3.224.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1115.0)
|
||||
aws-sdk-core (3.225.2)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.101.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (1.104.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.186.1)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-s3 (1.189.1)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-sns (1.99.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-sns (1.100.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
axe-core-api (4.10.3)
|
||||
dumb_delegator
|
||||
@@ -378,7 +378,7 @@ GEM
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.0)
|
||||
benchmark (0.4.1)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
@@ -386,7 +386,7 @@ GEM
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.2.0)
|
||||
bigdecimal (3.2.1)
|
||||
bindata (2.5.1)
|
||||
bootsnap (1.18.6)
|
||||
msgpack (~> 1.2)
|
||||
@@ -416,13 +416,13 @@ GEM
|
||||
carrierwave (>= 1.0.0)
|
||||
fog-aws
|
||||
cbor (0.5.9.8)
|
||||
cgi (0.4.2)
|
||||
cgi (0.5.0)
|
||||
childprocess (5.1.0)
|
||||
logger (~> 1.5)
|
||||
climate_control (1.2.0)
|
||||
closure_tree (7.4.0)
|
||||
activerecord (>= 4.2.10)
|
||||
with_advisory_lock (>= 4.0.0)
|
||||
closure_tree (8.0.0)
|
||||
activerecord (>= 7.1.0)
|
||||
with_advisory_lock (>= 5.0.0, < 6.0.0)
|
||||
coderay (1.1.3)
|
||||
coercible (1.0.0)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
@@ -450,7 +450,7 @@ GEM
|
||||
crass (1.0.6)
|
||||
css_parser (1.21.1)
|
||||
addressable
|
||||
csv (3.3.4)
|
||||
csv (3.3.5)
|
||||
cuprite (0.17)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.17.0)
|
||||
@@ -626,7 +626,7 @@ GEM
|
||||
fugit (>= 1.1)
|
||||
railties (>= 6.0.0)
|
||||
thor (>= 0.14.1)
|
||||
google-apis-core (0.17.0)
|
||||
google-apis-core (0.18.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
@@ -636,7 +636,7 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
google-apis-gmail_v1 (0.43.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-env (2.3.0)
|
||||
google-cloud-env (2.3.1)
|
||||
base64 (~> 0.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-logging-utils (0.2.0)
|
||||
@@ -659,7 +659,7 @@ GEM
|
||||
rack
|
||||
gravatar_image_tag (1.2.0)
|
||||
hana (1.3.7)
|
||||
hashdiff (1.1.2)
|
||||
hashdiff (1.2.0)
|
||||
hashery (2.1.2)
|
||||
hashie (5.0.0)
|
||||
highline (3.1.2)
|
||||
@@ -784,7 +784,7 @@ GEM
|
||||
mime-types (3.7.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
||||
mime-types-data (3.2025.0527)
|
||||
mime-types-data (3.2025.0603)
|
||||
mini_magick (5.2.0)
|
||||
benchmark
|
||||
logger
|
||||
@@ -953,7 +953,7 @@ GEM
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.2)
|
||||
puffing-billy (4.0.1)
|
||||
puffing-billy (4.0.2)
|
||||
addressable (~> 2.5)
|
||||
em-http-request (~> 1.1, >= 1.1.0)
|
||||
em-synchrony
|
||||
@@ -967,12 +967,12 @@ GEM
|
||||
puma (>= 5.0, < 7)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.15)
|
||||
rack (2.2.17)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.2)
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.3.1)
|
||||
rack-mini-profiler (4.0.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-oauth2 (2.2.1)
|
||||
activesupport
|
||||
@@ -1013,7 +1013,7 @@ GEM
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
activesupport (>= 5.0.1.rc1)
|
||||
rails-dom-testing (2.2.0)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
@@ -1032,14 +1032,14 @@ GEM
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rake (13.3.0)
|
||||
rake-compiler-dock (1.9.1)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
rb_sys (0.9.115)
|
||||
rb_sys (0.9.116)
|
||||
rake-compiler-dock (= 1.9.1)
|
||||
rbtrace (0.5.1)
|
||||
rbtrace (0.5.2)
|
||||
ffi (>= 1.0.6)
|
||||
msgpack (>= 0.4.3)
|
||||
optimist (>= 3.0.0)
|
||||
@@ -1097,7 +1097,7 @@ GEM
|
||||
rspec-support (3.13.4)
|
||||
rspec-wait (1.0.1)
|
||||
rspec (>= 3.4)
|
||||
rubocop (1.75.8)
|
||||
rubocop (1.76.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -1105,10 +1105,10 @@ GEM
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-ast (>= 1.45.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.44.1)
|
||||
rubocop-ast (1.45.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-capybara (2.22.1)
|
||||
@@ -1226,7 +1226,7 @@ GEM
|
||||
openssl-signature_algorithm (~> 1.0)
|
||||
trailblazer-option (0.1.2)
|
||||
ttfunk (1.7.0)
|
||||
turbo-rails (2.0.13)
|
||||
turbo-rails (2.0.16)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
turbo_power (0.7.0)
|
||||
@@ -1251,7 +1251,7 @@ GEM
|
||||
public_suffix
|
||||
vcr (6.3.1)
|
||||
base64
|
||||
vernier (1.7.1)
|
||||
vernier (1.8.0)
|
||||
view_component (3.23.2)
|
||||
activesupport (>= 5.2.0, < 8.1)
|
||||
concurrent-ruby (~> 1)
|
||||
@@ -1282,12 +1282,12 @@ GEM
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.1)
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.7.7)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
will_paginate (4.0.1)
|
||||
with_advisory_lock (5.1.0)
|
||||
with_advisory_lock (5.3.0)
|
||||
activerecord (>= 6.1)
|
||||
zeitwerk (>= 2.6)
|
||||
xpath (3.2.0)
|
||||
@@ -1333,7 +1333,7 @@ DEPENDENCIES
|
||||
actionpack-xml_parser (~> 2.0.0)
|
||||
active_record_doctor (~> 1.15.0)
|
||||
activemodel-serializers-xml (~> 1.0.1)
|
||||
activerecord-import (~> 2.1.0)
|
||||
activerecord-import (~> 2.2.0)
|
||||
activerecord-nulldb-adapter (~> 1.1.1)
|
||||
activerecord-session_store (~> 2.2.0)
|
||||
acts_as_list (~> 1.2.0)
|
||||
@@ -1358,7 +1358,7 @@ DEPENDENCIES
|
||||
carrierwave (~> 1.3.4)
|
||||
carrierwave_direct (~> 2.1.0)
|
||||
climate_control
|
||||
closure_tree (~> 7.4.0)
|
||||
closure_tree (~> 8.0.0)
|
||||
colored2
|
||||
commonmarker (~> 2.3.0)
|
||||
compare-xml (~> 0.66)
|
||||
@@ -1538,7 +1538,7 @@ DEPENDENCIES
|
||||
warden-basic_auth (~> 0.2.1)
|
||||
webmock (~> 3.12)
|
||||
will_paginate (~> 4.0.0)
|
||||
with_advisory_lock (~> 5.1.0)
|
||||
with_advisory_lock (~> 5.3.0)
|
||||
yabeda-activerecord
|
||||
yabeda-prometheus-mmap
|
||||
yabeda-puma-plugin
|
||||
@@ -1558,7 +1558,7 @@ CHECKSUMS
|
||||
activemodel (8.0.2) sha256=0ae1fb7fa1fae0699ba041a9e97702df42ea3b13f2d39f2d0fde51fca5f0656c
|
||||
activemodel-serializers-xml (1.0.3) sha256=fa1b16305e7254cc58a59c68833e3c0a593a59c8ab95d3be5aaea7cd9416c397
|
||||
activerecord (8.0.2) sha256=793470b92c44e4198d0262ac60086b7822f0ea585079ad67e32a6e4c86f2d90a
|
||||
activerecord-import (2.1.0) sha256=7b1bc586c4b636e125c18f533c9ac4421dd2dbac8f4865dbf576382a338c2c94
|
||||
activerecord-import (2.2.0) sha256=f8ca99b196e50775723d1f1d192c379f656378dc9f5628240992a0d78807fa4b
|
||||
activerecord-nulldb-adapter (1.1.1) sha256=034c91106183b954b072fba14c2786adf1a2b9e852ce04f85f823afaf03e9820
|
||||
activerecord-session_store (2.2.0) sha256=65918054573683bf4f87af89e765e1fece14c9d71cfac1f11abe4687c96e2743
|
||||
activestorage (8.0.2) sha256=f83d221e0f06ae38f2200e55490bd155c76d0add330f6e300e8646048d672977
|
||||
@@ -1577,21 +1577,21 @@ CHECKSUMS
|
||||
attr_required (1.0.2) sha256=f0ebfc56b35e874f4d0ae799066dbc1f81efefe2364ca3803dc9ea6a4de6cb99
|
||||
auto_strip_attributes (2.6.0) sha256=a7e2e0cf744de2bcd947fd68014220702bcc88c81274c1cd9ce6f7316aae39b0
|
||||
awesome_nested_set (3.8.0) sha256=469daff411d80291dbb80d1973133e498048a7afc2519c545f62d2cdebc60eda
|
||||
aws-eventstream (1.3.2) sha256=7e2c3a55ca70d7861d5d3c98e47daa463ed539c349caba22b48305b8919572d7
|
||||
aws-partitions (1.1105.0) sha256=a68010773df339ffe8074eac22de4d08e5235a1b25a895c3b2cea73ec54a6ea1
|
||||
aws-sdk-core (3.224.0) sha256=c617aae3f43ba2bfe9f819c1a31c7c9dbda3e1d1a33c746c23c4dc15638817ac
|
||||
aws-sdk-kms (1.101.0) sha256=44d8b5b69ce7394cc02f30f9a35bea04ea12c947b5ffe1471535eea5119368d7
|
||||
aws-sdk-s3 (1.186.1) sha256=5b703b8aafc26ca4f3f927368412da5f4b8f74909bb0e4651eb8afe8aa105803
|
||||
aws-sdk-sns (1.99.0) sha256=a84cf111e375783f8d92093cb790757f6856acfe15cbf46a7f1a9868ded6ec0a
|
||||
aws-sigv4 (1.11.0) sha256=50a8796991a862324442036ad7a395920572b84bb6cd29b945e5e1800e8da1db
|
||||
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
|
||||
aws-partitions (1.1115.0) sha256=787dc8a8d8db2aeb1474b4ee9a71e9f8b315d8109063fea0021030a6522f9e6c
|
||||
aws-sdk-core (3.225.2) sha256=3ebed026b4bb527740cdf9f2a0c1b4a542d070ee015f8dd6bfc4c265d75dd4f8
|
||||
aws-sdk-kms (1.104.0) sha256=d65f13254452a9648fc3557018214e4c1809224c8538de576dd079772f0390f4
|
||||
aws-sdk-s3 (1.189.1) sha256=dd46336000eb3d78ff3ba4b648dd520c83c171ac29a04f13ddb08249fd1b7de4
|
||||
aws-sdk-sns (1.100.0) sha256=706bb13c5da789a5b9c6219b397980645536dd7f7301c09fbb8d3e8613f65a96
|
||||
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
|
||||
axe-core-api (4.10.3) sha256=6e10f3ed1c031804f16e8154d9d5dc658564d10850cee860e125fe665c3f0148
|
||||
axe-core-rspec (4.10.3) sha256=ca21d0111e2d0fcd0f1da922c9071337336732aa6a3a8dc21bed94c9a701527e
|
||||
axiom-types (0.1.1) sha256=c1ff113f3de516fa195b2db7e0a9a95fd1b08475a502ff660d04507a09980383
|
||||
base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507
|
||||
bcrypt (3.1.20) sha256=8410f8c7b3ed54a3c00cd2456bf13917d695117f033218e2483b2e40b0784099
|
||||
benchmark (0.4.0) sha256=0f12f8c495545e3710c3e4f0480f63f06b4c842cc94cec7f33a956f5180e874a
|
||||
benchmark (0.4.1) sha256=d4ef40037bba27f03b28013e219b950b82bace296549ec15a78016552f8d2cce
|
||||
better_html (2.1.1) sha256=046c3551d1488a3f2939a7cac6fabf2bde08c32e135c91fcd683380118e5af55
|
||||
bigdecimal (3.2.0) sha256=f220c34e07d98b04e02eb23193bee436fab9afcd551f43dce8837a1b4aa80762
|
||||
bigdecimal (3.2.1) sha256=1f68631e876c6aba8fe9b84b36983c55ad3293ff2d1ad4c6f115bde1e9d802e3
|
||||
bindata (2.5.1) sha256=53186a1ec2da943d4cb413583d680644eb810aacbf8902497aac8f191fad9e58
|
||||
bootsnap (1.18.6) sha256=0ae2393c1e911e38be0f24e9173e7be570c3650128251bf06240046f84a07d00
|
||||
brakeman (7.0.2) sha256=b602d91bcec6c5ce4d4bc9e081e01f621c304b7a69f227d1e58784135f333786
|
||||
@@ -1605,10 +1605,10 @@ CHECKSUMS
|
||||
carrierwave (1.3.4) sha256=81772dabd1830edbd7f4526d2ae2c79f974f1d48900c3f03f7ecb7c657463a21
|
||||
carrierwave_direct (2.1.0) sha256=b0d5c19c1d17a05940e488cff644a3b2946422d6d494b2174b48f6a90c5dddbf
|
||||
cbor (0.5.9.8) sha256=9ee097fc58d9bc5e406d112cd2d4e112c7354ec16f8b6ff34e4732c1e44b4eb7
|
||||
cgi (0.4.2) sha256=a3cb190d46a820ca01a3e28bd5b64e67003ff99d7884b70512448566f35347e6
|
||||
cgi (0.5.0) sha256=fe99f65bb2c146e294372ebb27602adbc3b4c008e9ea7038c6bd48c1ec9759da
|
||||
childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec
|
||||
climate_control (1.2.0) sha256=36b21896193fa8c8536fa1cd843a07cf8ddbd03aaba43665e26c53ec1bd70aa5
|
||||
closure_tree (7.4.0) sha256=e39171bbe35de3c06898c3caf2d2949bba2d379aa78fc1a03c1c63f165c1d2b5
|
||||
closure_tree (8.0.0) sha256=9f33c8b2511db80d3cb418cba34da85176585bb9f1077c5d58e873e2495dbbcb
|
||||
coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
|
||||
coercible (1.0.0) sha256=5081ad24352cc8435ce5472bc2faa30260c7ea7f2102cc6a9f167c4d9bffaadc
|
||||
color_conversion (0.1.2) sha256=99bea5fa412e1527a11389975aa6ad445ff8528ebae202c11d08c45ea2b94c96
|
||||
@@ -1629,7 +1629,7 @@ CHECKSUMS
|
||||
crack (1.0.0) sha256=c83aefdb428cdc7b66c7f287e488c796f055c0839e6e545fec2c7047743c4a49
|
||||
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
|
||||
css_parser (1.21.1) sha256=6cfd3ffc0a97333b39d2b1b49c95397b05e0e3b684d68f77ec471ba4ec2ef7c7
|
||||
csv (3.3.4) sha256=e96ecd5a8c3494aa5b596282249daba5c6033203c199248e6146e36d2a78d8cd
|
||||
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
|
||||
cuprite (0.17) sha256=b140d5dc70d08b97ad54bcf45cd95d0bd430e291e9dffe76fff851fddd57c12b
|
||||
daemons (1.4.1) sha256=8fc76d76faec669feb5e455d72f35bd4c46dc6735e28c420afb822fac1fa9a1d
|
||||
dalli (3.2.8) sha256=2e63595084d91fae2655514a02c5d4fc0f16c0799893794abe23bf628bebaaa5
|
||||
@@ -1703,9 +1703,9 @@ CHECKSUMS
|
||||
globalid (1.2.1) sha256=70bf76711871f843dbba72beb8613229a49429d1866828476f9c9d6ccc327ce9
|
||||
gon (6.4.0) sha256=e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0
|
||||
good_job (3.99.1) sha256=7d3869d8a8ee8ef7048fee5d746f41c21987b7822c20038a2f773036bef0830a
|
||||
google-apis-core (0.17.0) sha256=3d4408b26b3f4b517b869be3c5aba40db0e172b4481c20ff882ef47579dd08f8
|
||||
google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee
|
||||
google-apis-gmail_v1 (0.43.0) sha256=037a218338b2d40b31d6f4c9206e58a6e34062fceac8a310f8dbc92b1a2b947b
|
||||
google-cloud-env (2.3.0) sha256=db80b120fc163d1b83ffe8c0bc82e9ad0025ef0d51d987068c7278677ee5caf7
|
||||
google-cloud-env (2.3.1) sha256=0faac01eb27be78c2591d64433663b1a114f8f7af55a4f819755426cac9178e7
|
||||
google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b
|
||||
googleauth (1.14.0) sha256=62e7de11791890c3d3dc70582dfd9ab5516530e4e4f56d96451fd62c76475149
|
||||
grape (2.3.0) sha256=99484ae2907b06a9e109edf2911c383809bf7f7c00d65554e4d01f0388728bda
|
||||
@@ -1713,7 +1713,7 @@ CHECKSUMS
|
||||
gravatar_image_tag (1.2.0) sha256=eb5630fea846b711e713b934a0178fb9785f02f4eb9ced8d6faa4d537c40fdcf
|
||||
grids (1.0.0)
|
||||
hana (1.3.7) sha256=5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d
|
||||
hashdiff (1.1.2) sha256=2c30eeded6ed3dce8401d2b5b99e6963fe5f14ed85e60dd9e33c545a44b71a77
|
||||
hashdiff (1.2.0) sha256=c984f13e115bfc9953332e8e83bd9d769cfde9944e2d54e07eb9df7b76e140b5
|
||||
hashery (2.1.2) sha256=d239cc2310401903f6b79d458c2bbef5bf74c46f3f974ae9c1061fb74a404862
|
||||
hashie (5.0.0) sha256=9d6c4e51f2a36d4616cbc8a322d619a162d8f42815a792596039fc95595603da
|
||||
highline (3.1.2) sha256=67cbd34d19f6ef11a7ee1d82ffab5d36dfd5b3be861f450fc1716c7125f4bb4a
|
||||
@@ -1764,7 +1764,7 @@ CHECKSUMS
|
||||
meta-tags (2.22.1) sha256=e5ae1febbd320d396c7226d7edb868e5d63466c14b9c8b06622a1a74e6dce354
|
||||
method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
|
||||
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
|
||||
mime-types-data (3.2025.0527) sha256=11808d780cdb27ea5db0143dbfc261b91ed63ec5e595f424a35f9822cb497a02
|
||||
mime-types-data (3.2025.0603) sha256=00a122cf046ef3867c428ed5e6d97e759027b0caa375da7fba33a9799c8a3037
|
||||
mini_magick (5.2.0) sha256=2757ffbfdb1d38242d1da9ff1505360ab75d59dc02eb7ab79ff6d5acb1243f4a
|
||||
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
|
||||
mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
|
||||
@@ -1860,15 +1860,15 @@ CHECKSUMS
|
||||
pry-rescue (1.6.0) sha256=985bfd506d9866b587fd86790cf8445266a41b7f92c627fc5b21ec7d92aba6db
|
||||
psych (5.2.6) sha256=814328aa5dcb6d604d32126a20bc1cbcf05521a5b49dbb1a8b30a07e580f316e
|
||||
public_suffix (6.0.2) sha256=bfa7cd5108066f8c9602e0d6d4114999a5df5839a63149d3e8b0f9c1d3558394
|
||||
puffing-billy (4.0.1) sha256=d23ea4b2fd54d6d213a85c4a1f603459d9b1634d93ac012dab59d36f380e778a
|
||||
puffing-billy (4.0.2) sha256=c983610f737e3847301f64cf41dccd9dcedcac9219af54d6eaf8178cbe48b3cc
|
||||
puma (6.6.0) sha256=f25c06873eb3d5de5f0a4ebc783acc81a4ccfe580c760cfe323497798018ad87
|
||||
puma-plugin-statsd (2.6.0) sha256=c13ffe6a2fa2d3c245af673316e6d2556f5d0c151455d1934dffc96ce814ea03
|
||||
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
|
||||
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
||||
rack (2.2.15) sha256=cfd082f748540702228e9d83f16e033be9c3a3e58f428f60f6bce26df38c1067
|
||||
rack (2.2.17) sha256=5fe02a1ca80d6fb2271dba00985ee2962d6f5620b6f46dfed89f5301ac4699dd
|
||||
rack-attack (6.7.0) sha256=3ca47e8f66cd33b2c96af53ea4754525cd928ed3fa8da10ee6dad0277791d77c
|
||||
rack-cors (2.0.2) sha256=415d4e1599891760c5dc9ef0349c7fecdf94f7c6a03e75b2e7c2b54b82adda1b
|
||||
rack-mini-profiler (3.3.1) sha256=2bf0de7d5795f54581e453b248e42cc50e8d0529efac73828653a9ad2407a801
|
||||
rack-mini-profiler (4.0.0) sha256=c37bedcb7d01e33ad4addd8c4e742986e75db7cd8908cba3432c60b4e812e00f
|
||||
rack-oauth2 (2.2.1) sha256=c73aa87c508043e2258f02b4fb110cacba9b37d2ccf884e22487d014a120d1a5
|
||||
rack-protection (3.2.0) sha256=3c74ba7fc59066453d61af9bcba5b6fe7a9b3dab6f445418d3b391d5ea8efbff
|
||||
rack-session (1.0.2) sha256=a02115e5420b4de036839b9811e3f7967d73446a554b42aa45106af335851d76
|
||||
@@ -1878,17 +1878,17 @@ CHECKSUMS
|
||||
rackup (1.0.1) sha256=ba86604a28989fe1043bff20d819b360944ca08156406812dca6742b24b3c249
|
||||
rails (8.0.2) sha256=fdfaa5a83ec0388e02864e88d515959caedc88053b5f701c4deb1652d8f164c6
|
||||
rails-controller-testing (1.0.5) sha256=741448db59366073e86fc965ba403f881c636b79a2c39a48d0486f2607182e94
|
||||
rails-dom-testing (2.2.0) sha256=e515712e48df1f687a1d7c380fd7b07b8558faa26464474da64183a7426fa93b
|
||||
rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d
|
||||
rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560
|
||||
rails-i18n (8.0.1) sha256=15303195450bdac9a80636cf14c7e5ada2f43907cc60fcd19bbb3f81ab45be0d
|
||||
railties (8.0.2) sha256=0d7c3f40c49ba74980f1bac1d4bb153a9331c5ee8a9631d89c7bf79db82e5cf9
|
||||
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
||||
rake (13.2.1) sha256=46cb38dae65d7d74b6020a4ac9d48afed8eb8149c040eccf0523bec91907059d
|
||||
rake (13.3.0) sha256=96f5092d786ff412c62fde76f793cc0541bd84d2eb579caa529aa8a059934493
|
||||
rake-compiler-dock (1.9.1) sha256=e73720a29aba9c114728ce39cc0d8eef69ba61d88e7978c57bac171724cd4d53
|
||||
rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
|
||||
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
|
||||
rb_sys (0.9.115) sha256=4696a623b89131f4a12bd9e38710add1c64b478dc3d2e9275f61a65f42f743ec
|
||||
rbtrace (0.5.1) sha256=e8cba64d462bfb8ba102d7be2ecaacc789247d52ac587d8003549d909cb9c5dc
|
||||
rb_sys (0.9.116) sha256=c879891018535d4362455197065ea580541b10ffdfa940bec512ec1dd9a7def4
|
||||
rbtrace (0.5.2) sha256=a2d7d222ab81363aaa0e91337ddbf70df834885d401a80ea0339d86c71f31895
|
||||
rbtree3 (0.7.1) sha256=ab60ead728a5491b70df4f4065e180b18dbab5319f817ce1dbf5dd906f26d8ba
|
||||
rdoc (6.14.0) sha256=2c46de58d7129b8743fcf6d76e3db971bdc914150e15ac06b386549bd82ed7db
|
||||
recaptcha (5.19.0) sha256=b69c021a5b788da06901d2c95ab86f10a8634e6496343096cc5167f0318b2a39
|
||||
@@ -1914,8 +1914,8 @@ CHECKSUMS
|
||||
rspec-retry (0.6.2) sha256=6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858
|
||||
rspec-support (3.13.4) sha256=184b1814f6a968102b57df631892c7f1990a91c9a3b9e80ef892a0fc2a71a3f7
|
||||
rspec-wait (1.0.1) sha256=50ce9cc7cd20b8bb67b95a4ed56b59a16d4af7e86ae91b28dbf20eeaa2481d1f
|
||||
rubocop (1.75.8) sha256=c80ab4286c5dcfc49d7ad1787cdba5569b63b58c96ee7afde4ec47a9c8a85be9
|
||||
rubocop-ast (1.44.1) sha256=e3cc04203b2ef04f6d6cf5f85fe6d643f442b18cc3b23e3ada0ce5b6521b8e92
|
||||
rubocop (1.76.1) sha256=e15a2d750794cf2157d2de8b1b403dfa71b8dc3957a22ae6043b1bdf21e7e0e7
|
||||
rubocop-ast (1.45.1) sha256=94042e49adc17f187ba037b33f941ba7398fede77cdf4bffafba95190a473a3e
|
||||
rubocop-capybara (2.22.1) sha256=ced88caef23efea53f46e098ff352f8fc1068c649606ca75cb74650970f51c0c
|
||||
rubocop-factory_bot (2.27.1) sha256=9d744b5916778c1848e5fe6777cc69855bd96548853554ec239ba9961b8573fe
|
||||
rubocop-openproject (0.2.0) sha256=2300ed6b638a34a9368b458dc5fb618ae396da6a27670b50519ad1e3cea65ccb
|
||||
@@ -1971,7 +1971,7 @@ CHECKSUMS
|
||||
tpm-key_attestation (0.14.0) sha256=d05cc52b397f89c36a7307407e0e84d3ea1c7afce50e0a70b146f8ab17d2bf4b
|
||||
trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3
|
||||
ttfunk (1.7.0) sha256=2370ba484b1891c70bdcafd3448cfd82a32dd794802d81d720a64c15d3ef2a96
|
||||
turbo-rails (2.0.13) sha256=c40ac0a3ccd57c129925c8ac524a5dfd1e17fad080906e2d32135721a8bba22f
|
||||
turbo-rails (2.0.16) sha256=d24e1b60f0c575b3549ecda967e5391027143f8220d837ed792c8d48ea0ea38d
|
||||
turbo_power (0.7.0) sha256=ad95d147e0fa761d0023ad9ca00528c7b7ddf6bba8ca2e23755d5b21b290d967
|
||||
turbo_tests (2.2.0)
|
||||
typed_dag (2.0.2) sha256=b8dc65bf8cb5461715aa64650b7c96fc2075ef29fdd94554a93896c43453828c
|
||||
@@ -1985,7 +1985,7 @@ CHECKSUMS
|
||||
validate_email (0.1.6) sha256=9dfe9016d527b17a8d3a6e95e4dc50a125400eef899d13d4cc2a254393f82ee4
|
||||
validate_url (1.0.15) sha256=72fe164c0713d63a9970bd6700bea948babbfbdcec392f2342b6704042f57451
|
||||
vcr (6.3.1) sha256=37b56e157e720446a3f4d2d39919cabef8cb7b6c45936acffd2ef8229fec03ed
|
||||
vernier (1.7.1) sha256=3cb17d41a43f32a317d76bd8973eb94d2610e65c5442ebdec6accbfb0ed734a4
|
||||
vernier (1.8.0) sha256=c6cf9555c2a58affc25ebc45de0b4d45a993eb078f88f14f5b45d7bacb25acdd
|
||||
view_component (3.23.2) sha256=3c2fed4b9e38bf074fa3d42ca55eedbb2cc070e0f3c31d7c13a50b0db530892b
|
||||
virtus (2.0.0) sha256=8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2
|
||||
warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0
|
||||
@@ -1995,10 +1995,10 @@ CHECKSUMS
|
||||
webmock (3.25.1) sha256=ab9d5d9353bcbe6322c83e1c60a7103988efc7b67cd72ffb9012629c3d396323
|
||||
webrick (1.9.1) sha256=b42d3c94f166f3fb73d87e9b359def9b5836c426fc8beacf38f2184a21b2a989
|
||||
websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737
|
||||
websocket-driver (0.7.7) sha256=056d99f2cd545712cfb1291650fde7478e4f2661dc1db6a0fa3b966231a146b4
|
||||
websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
|
||||
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
|
||||
will_paginate (4.0.1) sha256=107b226ebe1d393d274575956a7c472e1eefdd97d8828e01b72d425d15a875b9
|
||||
with_advisory_lock (5.1.0) sha256=0692cd82013b271c59aa5e1f603b741ab94926dbce098990fbace5dd5412fed7
|
||||
with_advisory_lock (5.3.0) sha256=bdb1864430853fe9081a7765f2d5b7cc42725c9ccf1901d20e9989828901b94e
|
||||
xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e
|
||||
yabeda (0.13.1) sha256=3213025f22b7746602c8a4c41e2ed82d73a90bdc5489f6ef472142b06c1cf954
|
||||
yabeda-activerecord (0.1.1) sha256=ae338213c264f20d8642e8bf47ac6058c49a6f7c8c00c892cd5765332914f45f
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
@import "op_primer/border_box_table_component"
|
||||
@import "op_primer/form_helpers"
|
||||
@import "open_project/common/attribute_component"
|
||||
@import "open_project/common/attribute_help_text_component"
|
||||
@import "open_project/common/attribute_label_component"
|
||||
@import "open_project/common/submenu_component"
|
||||
@import "projects/row_component"
|
||||
@import "projects/phases/hover_card_component"
|
||||
|
||||
@@ -98,7 +98,7 @@ module Admin
|
||||
tag: :a,
|
||||
content_arguments: { data: { turbo_frame: ItemsComponent.wrapper_key } },
|
||||
href: new_child_custom_field_item_path(@root.custom_field_id, model.parent, position: model.sort_order)
|
||||
) { _1.with_leading_visual_icon(icon: "fold-up") }
|
||||
) { it.with_leading_visual_icon(icon: "fold-up") }
|
||||
end
|
||||
|
||||
def add_below_action_item(menu)
|
||||
@@ -107,16 +107,18 @@ module Admin
|
||||
tag: :a,
|
||||
content_arguments: { data: { turbo_frame: ItemsComponent.wrapper_key } },
|
||||
href: new_child_custom_field_item_path(@root.custom_field_id, model.parent, position: model.sort_order + 1)
|
||||
) { _1.with_leading_visual_icon(icon: "fold-down") }
|
||||
) { it.with_leading_visual_icon(icon: "fold-down") }
|
||||
end
|
||||
|
||||
def add_sub_item_action_item(menu)
|
||||
position = model.children.any? ? model.children.maximum(:sort_order) + 1 : 0
|
||||
|
||||
menu.with_item(
|
||||
label: I18n.t(:button_add_sub_item),
|
||||
tag: :a,
|
||||
content_arguments: { data: { turbo_frame: ItemsComponent.wrapper_key } },
|
||||
href: new_child_custom_field_item_path(@root.custom_field_id, model)
|
||||
) { _1.with_leading_visual_icon(icon: "op-arrow-in") }
|
||||
href: new_child_custom_field_item_path(@root.custom_field_id, model, position:)
|
||||
) { it.with_leading_visual_icon(icon: "op-arrow-in") }
|
||||
end
|
||||
|
||||
def move_to_top_action_item(menu)
|
||||
|
||||
@@ -60,7 +60,7 @@ module Admin
|
||||
end
|
||||
|
||||
def item_header
|
||||
render(Primer::Beta::Breadcrumbs.new) do |loaf|
|
||||
render(Primer::Beta::Breadcrumbs.new(data: { test_selector: "hierarchy-breadcrumbs" })) do |loaf|
|
||||
slices.each do |slice|
|
||||
loaf.with_item(href: slice[:href], target: nil) { slice[:label] }
|
||||
end
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<%#-- 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.
|
||||
|
||||
++#%>
|
||||
<%= render(Primer::Beta::Heading.new(tag: :h3, font_size: 4, mb: 2)) { @custom_field.name } %>
|
||||
|
||||
<%=
|
||||
render(Primer::OpenProject::TreeView.new(node_variant: :anchor)) do |tree_view|
|
||||
add_sub_tree(tree_view, hierarchy_items)
|
||||
end
|
||||
%>
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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 Admin
|
||||
module CustomFields
|
||||
module Hierarchy
|
||||
class TreeViewComponent < ApplicationComponent
|
||||
def initialize(custom_field:, active_item:)
|
||||
super
|
||||
|
||||
@custom_field = custom_field
|
||||
@active_item = active_item
|
||||
@hierarchy_service = ::CustomFields::Hierarchy::HierarchicalItemService.new
|
||||
end
|
||||
|
||||
def hierarchy_items
|
||||
hashed_hierarchy = @custom_field.hierarchy_root.hash_tree
|
||||
hashed_hierarchy.nil? ? {} : hashed_hierarchy.first[1]
|
||||
end
|
||||
|
||||
def add_sub_tree(tree, hierarchy_hash)
|
||||
hierarchy_hash.each do |item, child_hash|
|
||||
if child_hash.empty?
|
||||
tree.with_leaf(**item_options(item))
|
||||
else
|
||||
expanded = child_hash.any? { |child, _| current?(child) }
|
||||
|
||||
tree.with_sub_tree(expanded: expanded, **item_options(item)) do |sub_tree|
|
||||
add_sub_tree(sub_tree, child_hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def item_options(item)
|
||||
{
|
||||
label: item.label,
|
||||
current: current?(item),
|
||||
href: custom_field_item_path(@custom_field, item)
|
||||
}
|
||||
end
|
||||
|
||||
def current?(item)
|
||||
item.id == @active_item.id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -42,6 +42,21 @@ class ApplicationComponent < ViewComponent::Base
|
||||
end
|
||||
|
||||
class << self
|
||||
##
|
||||
# Generates a unique identifier suitable for use as an HTML `id` attribute
|
||||
# value. If passed a non-nil model instance, it will use the model's id to
|
||||
# generate the identifier (see {ActiveModel::Conversion#to_param}).
|
||||
# Otherwise, it will generate an identifier with random numbers.
|
||||
#
|
||||
# @see ActiveModel::Conversion#to_param
|
||||
#
|
||||
# @param [ActiveModel::Model] model an optional model instance.
|
||||
# @param [String] base_name a prefix for the unique identifier.
|
||||
# @return [String] a unique identifier.
|
||||
def generate_id(model = nil, base_name: name.demodulize.underscore.dasherize)
|
||||
"#{base_name}-#{model&.to_param || SecureRandom.uuid}"
|
||||
end
|
||||
|
||||
##
|
||||
# Defines options for this cell which can be used within the cell's template.
|
||||
# Options are passed to the cell during the render call.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<%=
|
||||
render(Primer::Alpha::Dialog.new(id: dialog_id, title:, size: :large)) do |dialog|
|
||||
dialog.with_header(variant: :large)
|
||||
|
||||
dialog.with_body do
|
||||
concat(
|
||||
render(Primer::Box.new(mt: 1, classes: "op-uc-container op-uc-container__no-permalinks")) do
|
||||
helpers.format_text(@attribute_help_text, :help_text)
|
||||
end
|
||||
)
|
||||
|
||||
if has_attachments?
|
||||
concat render(Primer::Beta::Heading.new(tag: :h4)) { I18n.t(:label_attachment_plural) }
|
||||
concat helpers.list_attachments(resource_representer, inputs: { allowUploading: false })
|
||||
end
|
||||
end
|
||||
|
||||
dialog.with_footer(show_divider: true) do
|
||||
if allowed_to_edit?
|
||||
concat(
|
||||
render(Primer::Beta::Button.new(tag: :a, href: edit_button_href)) do |button|
|
||||
button.with_leading_visual_icon(icon: :pencil)
|
||||
I18n.t(:button_edit)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
concat(
|
||||
render(Primer::Beta::Button.new(data: { close_dialog_id: dialog_id })) { I18n.t(:button_close) }
|
||||
)
|
||||
end
|
||||
end
|
||||
%>
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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 AttributeHelpTexts::ShowDialogComponent < ApplicationComponent
|
||||
include OpTurbo::Streamable
|
||||
|
||||
DIALOG_ID = "attribute-help-text-show-modal"
|
||||
|
||||
def initialize(attribute_help_text:, current_user: User.current)
|
||||
super
|
||||
@attribute_help_text = attribute_help_text
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dialog_id = DIALOG_ID
|
||||
|
||||
def title = @attribute_help_text.attribute_caption
|
||||
|
||||
def has_attachments? = @attribute_help_text.attachments.any?
|
||||
|
||||
def allowed_to_edit? = @current_user.allowed_globally?(:edit_attribute_help_texts)
|
||||
|
||||
def edit_button_href = url_helpers.edit_attribute_help_text_path(@attribute_help_text)
|
||||
|
||||
def resource_representer
|
||||
::API::V3::HelpTexts::HelpTextRepresenter.new(@attribute_help_text, current_user: @current_user, embed_links: false)
|
||||
end
|
||||
end
|
||||
@@ -44,7 +44,7 @@ module EnterpriseEdition
|
||||
VARIANT_OPTIONS = %i[inline medium large].freeze
|
||||
|
||||
# @param feature_key [Symbol, NilClass] The key of the feature to show the banner for.
|
||||
# @param variant [Symbol, NilClass] The variant of the banner comopnent.
|
||||
# @param variant [Symbol, NilClass] The variant of the banner component.
|
||||
# @param image [String, NilClass] Path to the image to show on the banner, or nil.
|
||||
# Only applicable and required when variant is :medium.
|
||||
# @param video [String, NilClass] Path to the video to show on the banner, or nil.
|
||||
@@ -55,7 +55,7 @@ module EnterpriseEdition
|
||||
# @param show_always [boolean] Always show the banner, regardless of the dismissed or feature state.
|
||||
# @param dismiss_key [String] Provide a string to identify this banner when being dismissed. Defaults to feature_key
|
||||
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
||||
def initialize(feature_key, # rubocop:disable Metrics/AbcSize
|
||||
def initialize(feature_key,
|
||||
variant: DEFAULT_VARIANT,
|
||||
image: nil,
|
||||
video: nil,
|
||||
@@ -68,12 +68,15 @@ module EnterpriseEdition
|
||||
@image = image
|
||||
@video = video
|
||||
@dismissable = dismissable
|
||||
@dismiss_key = dismiss_key
|
||||
@dismiss_key = dismiss_key.to_s
|
||||
|
||||
@show_always = show_always
|
||||
|
||||
self.feature_key = feature_key
|
||||
self.i18n_scope = i18n_scope
|
||||
|
||||
trial_overrides! if trial_feature?
|
||||
|
||||
if @variant == :medium && @image.nil?
|
||||
raise ArgumentError, "The 'image' parameter is required when the variant is :medium."
|
||||
end
|
||||
@@ -82,17 +85,7 @@ module EnterpriseEdition
|
||||
raise ArgumentError, "The 'video' parameter is required when the variant is :large."
|
||||
end
|
||||
|
||||
@system_arguments = system_arguments
|
||||
@system_arguments[:tag] = :div
|
||||
@system_arguments[:mb] ||= 2
|
||||
@system_arguments[:id] = "op-enterprise-banner-#{feature_key.to_s.tr('_', '-')}"
|
||||
@system_arguments[:test_selector] = "op-enterprise-banner"
|
||||
@system_arguments[:classes] = class_names(
|
||||
@system_arguments[:classes],
|
||||
"op-enterprise-banner",
|
||||
@variant == :medium ? "op-enterprise-banner_medium" : nil,
|
||||
@variant == :large ? "op-enterprise-banner_large" : nil
|
||||
)
|
||||
set_system_arguments(system_arguments, feature_key)
|
||||
|
||||
super
|
||||
end
|
||||
@@ -115,15 +108,39 @@ module EnterpriseEdition
|
||||
end
|
||||
|
||||
def wrapper_key
|
||||
"enterprise_banner_#{feature_key}"
|
||||
"enterprise_banner_#{@dismiss_key}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_system_arguments(system_arguments, feature_key)
|
||||
@system_arguments = system_arguments
|
||||
@system_arguments[:tag] = :div
|
||||
@system_arguments[:mb] ||= 2
|
||||
@system_arguments[:id] = "op-enterprise-banner-#{feature_key.to_s.tr('_', '-')}"
|
||||
@system_arguments[:test_selector] = "op-enterprise-banner"
|
||||
@system_arguments[:classes] = class_names(
|
||||
@system_arguments[:classes],
|
||||
"op-enterprise-banner",
|
||||
"op-enterprise-banner_medium" => @variant == :medium,
|
||||
"op-enterprise-banner_large" => @variant == :large,
|
||||
"op-enterprise-banner_trial" => trial_feature?
|
||||
)
|
||||
end
|
||||
|
||||
def trial_overrides!
|
||||
@dismissable = true
|
||||
@dismiss_key += "_trial" unless @dismiss_key.end_with?("_trial")
|
||||
@variant = :inline
|
||||
end
|
||||
|
||||
def render?
|
||||
return true if @show_always
|
||||
return false if dismissed?
|
||||
return true if feature_available? && trial_feature?
|
||||
return false if EnterpriseToken.hide_banners?
|
||||
|
||||
!(EnterpriseToken.hide_banners? || feature_available? || dismissed?)
|
||||
!feature_available?
|
||||
end
|
||||
|
||||
def feature_available?
|
||||
@@ -135,5 +152,9 @@ module EnterpriseEdition
|
||||
|
||||
User.current.pref.dismissed_banner?(@dismiss_key)
|
||||
end
|
||||
|
||||
def trial_feature?
|
||||
EnterpriseToken.trialling?(feature_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -58,6 +58,7 @@ module EnterpriseEdition
|
||||
|
||||
def buttons
|
||||
[
|
||||
buy_now_button,
|
||||
free_trial_button,
|
||||
upgrade_now_button,
|
||||
more_info_button
|
||||
@@ -74,6 +75,20 @@ module EnterpriseEdition
|
||||
)
|
||||
end
|
||||
|
||||
def buy_now_button
|
||||
return unless EnterpriseToken.active?
|
||||
return unless User.current.admin?
|
||||
|
||||
render(Primer::Beta::Button.new(
|
||||
classes: "upsell-colored-background",
|
||||
tag: :a,
|
||||
href: OpenProject::Enterprise.upgrade_path,
|
||||
align_self: :center
|
||||
)) do
|
||||
I18n.t("ee.upsell.buy_now_button")
|
||||
end
|
||||
end
|
||||
|
||||
# Allow providing a custom upgrade now button
|
||||
def upgrade_now_button
|
||||
nil
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<%=
|
||||
render Primer::Beta::Link.new(**@system_arguments) do
|
||||
render Primer::Beta::Octicon.new(:question, size: :xsmall)
|
||||
end
|
||||
%>
|
||||
<%= render @tooltip %>
|
||||
@@ -0,0 +1,71 @@
|
||||
# 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 Common
|
||||
class AttributeHelpTextComponent < ApplicationComponent
|
||||
def initialize(help_text:, **system_arguments) # rubocop:disable Metrics/AbcSize
|
||||
super()
|
||||
|
||||
@help_text = help_text
|
||||
|
||||
@system_arguments = system_arguments
|
||||
@system_arguments[:id] ||= self.class.generate_id(help_text)
|
||||
@system_arguments[:classes] = class_names(
|
||||
@system_arguments[:classes],
|
||||
"op-attribute-help-text"
|
||||
)
|
||||
@system_arguments[:data] ||= {}
|
||||
@system_arguments[:data][:controller] = "async-dialog"
|
||||
|
||||
@tooltip = Primer::Alpha::Tooltip.new(
|
||||
for_id: @system_arguments[:id],
|
||||
type: :label,
|
||||
text: I18n.t("js.help_texts.show_modal"),
|
||||
direction: :sw
|
||||
)
|
||||
@system_arguments[:aria] ||= {}
|
||||
@system_arguments[:aria][:labelledby] = @tooltip.id
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render?
|
||||
@help_text.present?
|
||||
end
|
||||
|
||||
def before_render
|
||||
return unless @help_text
|
||||
|
||||
@system_arguments[:href] = show_dialog_attribute_help_text_path(@help_text)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
//-- 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.
|
||||
//++
|
||||
|
||||
.op-attribute-help-text
|
||||
line-height: 1rem
|
||||
@@ -0,0 +1,4 @@
|
||||
<%= render Primer::ConditionalWrapper.new(condition: !@tag.nil?, trim: true, **@system_arguments) do %>
|
||||
<%= content %>
|
||||
<%= render OpenProject::Common::AttributeHelpTextComponent.new(help_text: @help_text) if @help_text.present? %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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 Common
|
||||
class AttributeLabelComponent < ApplicationComponent
|
||||
def initialize(
|
||||
attribute:,
|
||||
model:,
|
||||
tag: :span,
|
||||
current_user: User.current,
|
||||
**system_arguments
|
||||
)
|
||||
super()
|
||||
|
||||
@tag = tag
|
||||
@system_arguments = system_arguments
|
||||
@system_arguments[:tag] = @tag
|
||||
@system_arguments[:classes] = class_names(
|
||||
@system_arguments[:classes],
|
||||
"op-attribute-label"
|
||||
)
|
||||
|
||||
@help_text = ::AttributeHelpText.for(model)
|
||||
&.cached(current_user)
|
||||
&.[](attribute.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
//-- 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.
|
||||
//++
|
||||
|
||||
.op-attribute-label
|
||||
display: inline-flex
|
||||
|
||||
> .op-attribute-help-text
|
||||
align-self: baseline
|
||||
margin-left: var(--base-size-6)
|
||||
+1
-1
@@ -78,7 +78,7 @@
|
||||
data: { "projects--settings--border-box-filter-target": "searchItem" },
|
||||
test_selector: "project-life-cycle-step-#{definition.id}"
|
||||
) do
|
||||
render(Projects::Settings::LifeCycleSteps::StepComponent.new(definition:, active?: active))
|
||||
render(Projects::Settings::LifeCycle::PhaseComponent.new(definition:, active?: active))
|
||||
end
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
|
||||
module Projects
|
||||
module Settings
|
||||
module LifeCycleSteps
|
||||
module LifeCycle
|
||||
class IndexComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
include OpPrimer::ComponentHelpers
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
module Projects::Settings::LifeCycleSteps
|
||||
module Projects::Settings::LifeCycle
|
||||
class IndexPageHeaderComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
render(
|
||||
Projects::PhaseDefinitionComponent.new(
|
||||
definition,
|
||||
edit_link: User.current.admin?,
|
||||
edit_link: definition_editable?,
|
||||
phase_text_options: { classes: "filter-target-visible-text" }
|
||||
)
|
||||
)
|
||||
+7
-2
@@ -28,11 +28,12 @@
|
||||
|
||||
module Projects
|
||||
module Settings
|
||||
module LifeCycleSteps
|
||||
class StepComponent < ApplicationComponent
|
||||
module LifeCycle
|
||||
class PhaseComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
include OpPrimer::ComponentHelpers
|
||||
include OpTurbo::Streamable
|
||||
include Projects::LifeCycleDefinitionHelper
|
||||
|
||||
options :definition,
|
||||
:active?
|
||||
@@ -40,6 +41,10 @@ module Projects
|
||||
def toggle_aria_label
|
||||
I18n.t("projects.settings.life_cycle.step.use_in_project", step: definition.name)
|
||||
end
|
||||
|
||||
def definition_editable?
|
||||
User.current.admin? && allowed_to_customize_life_cycle?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+8
-6
@@ -31,7 +31,14 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
component_wrapper do
|
||||
flex_layout(data: wrapper_data_attributes) do |flex|
|
||||
flex.with_row do
|
||||
if allowed_to_customize_life_cycle?
|
||||
render EnterpriseEdition::BannerComponent.new(:customize_life_cycle,
|
||||
variant: :medium,
|
||||
image: "enterprise/project-lifecycle.png",
|
||||
mb: 3)
|
||||
end
|
||||
|
||||
if allowed_to_customize_life_cycle?
|
||||
flex.with_row do
|
||||
render(Primer::OpenProject::SubHeader.new) do |subheader|
|
||||
subheader.with_filter_input(
|
||||
name: "border-box-filter",
|
||||
@@ -57,11 +64,6 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
I18n.t("settings.project_phase_definitions.label_add")
|
||||
end
|
||||
end
|
||||
else
|
||||
render EnterpriseEdition::BannerComponent.new(:customize_life_cycle,
|
||||
variant: :medium,
|
||||
image: "enterprise/project-lifecycle.png",
|
||||
mb: 3)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
end
|
||||
end
|
||||
|
||||
render(strategy.manage_shares_component(modal_content:, errors:))
|
||||
render(strategy.upsell_banner(modal_content:))
|
||||
render(strategy.manage_shares_component(modal_content:, errors:)) if strategy.allow_feature?
|
||||
end
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<%=
|
||||
component_wrapper(tag: "turbo-frame") do
|
||||
render(EnterpriseEdition::BannerComponent.new(:work_package_sharing))
|
||||
modal_content.with_row do
|
||||
render(EnterpriseEdition::BannerComponent.new(:work_package_sharing))
|
||||
end
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -33,6 +33,14 @@ module Shares
|
||||
include OpTurbo::Streamable
|
||||
include OpPrimer::ComponentHelpers
|
||||
|
||||
def initialize(modal_content:)
|
||||
super
|
||||
|
||||
@modal_content = modal_content
|
||||
end
|
||||
|
||||
attr_reader :modal_content
|
||||
|
||||
def self.wrapper_key
|
||||
"share_modal_body"
|
||||
end
|
||||
|
||||
@@ -36,15 +36,17 @@ module WorkPackages
|
||||
include OpPrimer::ComponentHelpers
|
||||
|
||||
FORM_ID = "reminder-form"
|
||||
DEFAULT_TIME = "09:00"
|
||||
|
||||
attr_reader :remindable, :reminder, :errors
|
||||
attr_reader :remindable, :reminder, :errors, :preset
|
||||
|
||||
def initialize(remindable:, reminder:, errors: nil)
|
||||
def initialize(remindable:, reminder:, errors: nil, preset: nil)
|
||||
super
|
||||
|
||||
@remindable = remindable
|
||||
@reminder = reminder
|
||||
@errors = errors
|
||||
@preset = preset
|
||||
end
|
||||
|
||||
class << self
|
||||
@@ -55,9 +57,9 @@ module WorkPackages
|
||||
|
||||
def submit_path
|
||||
if @reminder.persisted?
|
||||
work_package_reminder_path(@remindable, @reminder)
|
||||
url_helpers.work_package_reminder_path(@remindable, @reminder)
|
||||
else
|
||||
work_package_reminders_path(@remindable)
|
||||
url_helpers.work_package_reminders_path(@remindable)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,11 +84,32 @@ module WorkPackages
|
||||
end
|
||||
|
||||
def remind_at_date_initial_value
|
||||
format_time_as_date(@reminder.remind_at, format: "%Y-%m-%d")
|
||||
return time_as_date(@reminder.remind_at) if @reminder.remind_at
|
||||
return calculate_preset_date if @preset
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def remind_at_time_initial_value
|
||||
format_time(@reminder.remind_at, include_date: false, format: "%H:%M")
|
||||
return format_time(@reminder.remind_at, include_date: false, format: "%H:%M") if @reminder.remind_at
|
||||
|
||||
DEFAULT_TIME
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def calculate_preset_date
|
||||
case @preset
|
||||
when "tomorrow" then time_as_date(1.day.from_now)
|
||||
when "three_days" then time_as_date(3.days.from_now)
|
||||
when "week" then time_as_date(7.days.from_now)
|
||||
when "month" then time_as_date(1.month.from_now)
|
||||
when "custom" then nil
|
||||
end
|
||||
end
|
||||
|
||||
def time_as_date(time)
|
||||
format_time_as_date(time, format: "%Y-%m-%d")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ProjectLifeCycleSteps
|
||||
module ProjectPhases
|
||||
class ActivationContract < ::ModelContract
|
||||
alias_method :project, :model
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ProjectLifeCycleSteps
|
||||
module ProjectPhases
|
||||
class BaseContract < ::ModelContract
|
||||
validate :validate_edit_project_phases_permission
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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 ProjectPhases
|
||||
class RescheduleContract < BaseContract
|
||||
alias_method :project, :model
|
||||
|
||||
protected
|
||||
|
||||
def validate_model?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ProjectLifeCycleSteps
|
||||
module ProjectPhases
|
||||
class UpdateContract < BaseContract
|
||||
validate :validate_start_after_preceeding_phases
|
||||
validate :validate_start_date_is_a_working_day
|
||||
@@ -69,7 +69,7 @@ module Queries
|
||||
end
|
||||
|
||||
def project_visible?
|
||||
Project.visible(user).exists?(id: project_id)
|
||||
::Project.visible(user).exists?(id: project_id)
|
||||
end
|
||||
|
||||
def may_not_manage_queries?
|
||||
|
||||
@@ -66,8 +66,15 @@ module Settings
|
||||
end
|
||||
|
||||
def unique_job
|
||||
WorkPackages::ApplyWorkingDaysChangeJob.new.check_concurrency do
|
||||
errors.add :base, :previous_working_day_changes_unprocessed
|
||||
[
|
||||
Projects::Phases::ApplyWorkingDaysChangeJob,
|
||||
WorkPackages::ApplyWorkingDaysChangeJob
|
||||
].each do |job_class|
|
||||
job_class.new.check_concurrency do
|
||||
errors.add :base, :previous_working_day_changes_unprocessed
|
||||
end
|
||||
|
||||
break if errors.added? :base, :previous_working_day_changes_unprocessed
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -37,10 +37,19 @@ module WorkPackages
|
||||
attribute :done_ratio,
|
||||
writable: true
|
||||
|
||||
# Use the default permission for the create contract, which is :add_work_packages.
|
||||
attribute_permission :project_phase_definition_id, :add_work_packages
|
||||
|
||||
# Do not validate predecessors or children presence when copying: when
|
||||
# copying, it's possible to create a work package in automatic scheduling
|
||||
# mode even if it has no predecessors or children yet. They will be added
|
||||
# later in the process.
|
||||
def validate_has_predecessors_or_children; end
|
||||
|
||||
# No validation happening on whether the phase is active in the project.
|
||||
# When copying a work package, e.g. from a project template, the phase
|
||||
# might not be active in the project yet. But when it is activated later,
|
||||
# the value should then be present.
|
||||
def validate_phase_active_in_project; end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,17 +27,29 @@
|
||||
#++
|
||||
|
||||
class AttributeHelpTextsController < ApplicationController
|
||||
include OpTurbo::ComponentStream
|
||||
include OpTurbo::DialogStreamHelper
|
||||
|
||||
layout "admin"
|
||||
menu_item :attribute_help_texts
|
||||
|
||||
before_action :authorize_global
|
||||
before_action :authorize_global, except: :show_dialog
|
||||
before_action :find_entry, only: %i(edit update destroy)
|
||||
before_action :find_type_scope
|
||||
|
||||
authorization_checked! :show_dialog
|
||||
|
||||
def index
|
||||
@texts_by_type = AttributeHelpText.all_by_scope
|
||||
end
|
||||
|
||||
def show_dialog
|
||||
@attribute_help_text = AttributeHelpText.visible(current_user).find(params[:id])
|
||||
respond_with_dialog(
|
||||
AttributeHelpTexts::ShowDialogComponent.new(attribute_help_text: @attribute_help_text)
|
||||
)
|
||||
end
|
||||
|
||||
def new
|
||||
@attribute_help_text = AttributeHelpText.new type: @attribute_scope
|
||||
end
|
||||
|
||||
@@ -47,9 +47,13 @@ class My::EnterpriseBannersController < ApplicationController
|
||||
|
||||
def dismiss
|
||||
pref = User.current.pref
|
||||
pref.dismiss_banner(@feature_key)
|
||||
pref.dismiss_banner(@dismiss_key)
|
||||
if pref.save
|
||||
remove_via_turbo_stream(component: EnterpriseEdition::BannerComponent.new(@feature_key))
|
||||
remove_via_turbo_stream(component: EnterpriseEdition::BannerComponent.new(
|
||||
@feature_key,
|
||||
dismiss_key: @dismiss_key,
|
||||
show_always: true
|
||||
))
|
||||
respond_with_turbo_streams
|
||||
else
|
||||
respond_with_flash_error(message: call.message)
|
||||
@@ -59,7 +63,11 @@ class My::EnterpriseBannersController < ApplicationController
|
||||
private
|
||||
|
||||
def get_feature_key
|
||||
@feature_key = params[:feature_key].to_sym
|
||||
raw_key = params[:feature_key]
|
||||
|
||||
@dismiss_key = raw_key
|
||||
@feature_key = raw_key.gsub(/_trial$/, "").to_sym
|
||||
|
||||
render_400 unless OpenProject::Token.lowest_plan_for(@feature_key)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,7 +66,7 @@ class Projects::Settings::LifeCycleStepsController < Projects::SettingsControlle
|
||||
end
|
||||
|
||||
def set_steps_active_status(definitions, active:)
|
||||
::ProjectLifeCycleSteps::ActivationService
|
||||
::ProjectPhases::ActivationService
|
||||
.new(user: current_user, project: @project, definitions:)
|
||||
.call(active:)
|
||||
end
|
||||
|
||||
@@ -40,7 +40,8 @@ class WorkPackages::RemindersController < ApplicationController
|
||||
def modal_body
|
||||
render modal_component_class.new(
|
||||
remindable: @work_package,
|
||||
reminder: @reminder
|
||||
reminder: @reminder,
|
||||
preset: params[:preset]
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
#++
|
||||
|
||||
class ApplicationForm < Primer::Forms::Base
|
||||
include AttributeHelpTexts::FormHelper
|
||||
|
||||
def self.settings_form
|
||||
form do |f|
|
||||
f = Settings::FormDecorator.new(f)
|
||||
@@ -41,9 +43,7 @@ class ApplicationForm < Primer::Forms::Base
|
||||
end
|
||||
|
||||
# @return [ActionView::Base] the view helper instance
|
||||
def helpers
|
||||
@view_context.helpers
|
||||
end
|
||||
delegate :helpers, to: :@view_context
|
||||
|
||||
# @return [ActiveRecord::Base] the model instance given to the form builder
|
||||
def model
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 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 AttributeHelpTexts
|
||||
module FormHelper
|
||||
def wrap_attribute_label_with_help_text(label, attribute)
|
||||
render OpenProject::Common::AttributeLabelComponent.new(
|
||||
model:,
|
||||
attribute:,
|
||||
tag: nil,
|
||||
current_user: User.current
|
||||
).with_content(label)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -92,12 +92,17 @@ module Projects::LifeCycles
|
||||
end
|
||||
|
||||
def autofocus?(field_name)
|
||||
start_date_blank = model.start_date.blank? && model.default_start_date.blank?
|
||||
case field_name
|
||||
when :start_date
|
||||
start_date_blank
|
||||
when :finish_date
|
||||
!start_date_blank && model.finish_date.blank?
|
||||
# let javascipt handle focusing when rendering for preview
|
||||
return false if model.changed?
|
||||
|
||||
field_name == autofocus_field_name
|
||||
end
|
||||
|
||||
def autofocus_field_name
|
||||
if start_date_disabled? || (model.start_date? && !model.finish_date?)
|
||||
:finish_date
|
||||
else
|
||||
:start_date
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -86,10 +86,8 @@ module Statuses
|
||||
}
|
||||
)
|
||||
|
||||
if readonly_work_packages_restricted?
|
||||
statuses_form.html_content do
|
||||
render(EnterpriseEdition::BannerComponent.new(:readonly_work_packages))
|
||||
end
|
||||
statuses_form.html_content do
|
||||
render(EnterpriseEdition::BannerComponent.new(:readonly_work_packages))
|
||||
end
|
||||
|
||||
statuses_form.check_box(
|
||||
|
||||
@@ -116,9 +116,9 @@ module RepositoriesHelper
|
||||
def render_changes_tree(tree)
|
||||
return "" if tree.nil?
|
||||
|
||||
output = "<ul>"
|
||||
output = +"<ul>"
|
||||
tree.keys.sort.each do |file|
|
||||
style = "change"
|
||||
style = +"change"
|
||||
text = File.basename(file)
|
||||
if s = tree[file][:s]
|
||||
style << " folder"
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
class AttributeHelpText < ApplicationRecord
|
||||
acts_as_attachable viewable_by_all_users: true
|
||||
|
||||
def self.cached(user)
|
||||
OpenProject::Cache.fetch([name, user]) { visible(user).select(:id, :attribute_name).index_by(&:attribute_name) }
|
||||
end
|
||||
|
||||
def self.for(model)
|
||||
subclasses.find { |child| child.name.demodulize == model.model_name }
|
||||
end
|
||||
|
||||
def self.available_types
|
||||
subclasses.map { |child| child.name.demodulize }
|
||||
end
|
||||
|
||||
@@ -47,7 +47,7 @@ class AttributeHelpText::Project < AttributeHelpText
|
||||
validates :attribute_name, inclusion: { in: ->(*) { available_attributes.keys } }
|
||||
|
||||
def type_caption
|
||||
Project.model_name.human
|
||||
::Project.model_name.human
|
||||
end
|
||||
|
||||
def self.visible_condition(_user)
|
||||
|
||||
@@ -45,6 +45,18 @@ class EnterpriseToken < ApplicationRecord
|
||||
current && !current.expired?
|
||||
end
|
||||
|
||||
def available_features
|
||||
EnterpriseToken.current&.available_features || []
|
||||
end
|
||||
|
||||
def trialling_features
|
||||
available_features.select { |feature| trialling?(feature) }
|
||||
end
|
||||
|
||||
def trialling?(feature)
|
||||
allows_to?(feature) && EnterpriseToken.current.trial?
|
||||
end
|
||||
|
||||
def hide_banners?
|
||||
OpenProject::Configuration.ee_hide_banners?
|
||||
end
|
||||
@@ -88,6 +100,7 @@ class EnterpriseToken < ApplicationRecord
|
||||
:plan,
|
||||
:features,
|
||||
:version,
|
||||
:trial?,
|
||||
to: :token_object
|
||||
|
||||
def token_object
|
||||
|
||||
@@ -139,6 +139,7 @@ class Project < ApplicationRecord
|
||||
datetime: :created_at
|
||||
|
||||
register_journal_formatted_fields "active", formatter_key: :active_status
|
||||
register_journal_formatted_fields "cause", formatter_key: :cause
|
||||
register_journal_formatted_fields "templated", formatter_key: :template
|
||||
register_journal_formatted_fields "identifier", "name", formatter_key: :plaintext
|
||||
register_journal_formatted_fields "status_explanation", "description", formatter_key: :diff
|
||||
|
||||
@@ -51,7 +51,8 @@ class Project::Phase < ApplicationRecord
|
||||
attr_readonly :definition_id
|
||||
|
||||
scope :active, -> { where(active: true) }
|
||||
scopes :order_by_position
|
||||
scopes :order_by_position,
|
||||
:covering_dates_or_days_of_week
|
||||
|
||||
class << self
|
||||
def visible(user = User.current)
|
||||
@@ -85,24 +86,24 @@ class Project::Phase < ApplicationRecord
|
||||
end
|
||||
|
||||
def follows_previous_phase?
|
||||
previous_finish_dates.last.present?
|
||||
!!previous_phase&.date_range_set?
|
||||
end
|
||||
|
||||
def default_start_date
|
||||
return @default_start_date if defined?(@default_start_date)
|
||||
|
||||
previous_finish_date = previous_finish_dates.compact.last
|
||||
previous_finish_date = previous_phase&.finish_date
|
||||
@default_start_date = previous_finish_date && Day.next_working(from: previous_finish_date).date
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def previous_finish_dates
|
||||
return @previous_finish_dates if defined?(@previous_finish_dates)
|
||||
def previous_phase
|
||||
return @previous_phase if defined?(@previous_phase)
|
||||
|
||||
@previous_finish_dates = project
|
||||
@previous_phase = project
|
||||
.available_phases
|
||||
.select { it.position < position }
|
||||
.map(&:finish_date)
|
||||
.last
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,4 +48,6 @@ class Project::PhaseDefinition < ApplicationRecord
|
||||
default_scope { order(:position) }
|
||||
|
||||
scopes :with_project_count
|
||||
|
||||
def to_s; name end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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 Project::Phases::Scopes::CoveringDatesOrDaysOfWeek
|
||||
extend ActiveSupport::Concern
|
||||
using CoreExtensions::SquishSql
|
||||
|
||||
class_methods do
|
||||
# Fetches all phases that cover specific days of the week, and/or specific dates.
|
||||
#
|
||||
# The period considered is from the phase start date to the due date.
|
||||
#
|
||||
# @param dates Date[] An array of the Date objects.
|
||||
# @param days_of_week number[] An array of the ISO days of the week to
|
||||
# consider. 1 is Monday, 7 is Sunday.
|
||||
def covering_dates_or_days_of_week(days_of_week: [], dates: [])
|
||||
days_of_week = Array(days_of_week)
|
||||
dates = Array(dates)
|
||||
return none if days_of_week.empty? && dates.empty?
|
||||
|
||||
ids_sql = sanitize_sql([<<~SQL.squish, { days_of_week:, dates: }])
|
||||
WITH
|
||||
-- select phases with at least one date
|
||||
phases_with_dates AS (
|
||||
SELECT
|
||||
id,
|
||||
COALESCE(start_date, finish_date) AS start_date,
|
||||
COALESCE(finish_date, start_date) AS finish_date
|
||||
FROM project_phases
|
||||
WHERE
|
||||
start_date IS NOT NULL
|
||||
OR finish_date IS NOT NULL
|
||||
)
|
||||
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
phases_with_dates
|
||||
WHERE
|
||||
-- Check if the range covers any of the provided days of week. It is
|
||||
-- done by comparing number of days from start_date to first target
|
||||
-- day of week with number of days in range. If number is less or
|
||||
-- eqal, then the range covers the target day of week
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM UNNEST(ARRAY[:days_of_week]::INT[]) AS target_dow
|
||||
WHERE (target_dow + 7 - EXTRACT(ISODOW FROM start_date)::INT) % 7 <= finish_date - start_date
|
||||
)
|
||||
OR
|
||||
-- Check if the range covers any of the provided dates
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM UNNEST(ARRAY[:dates]::DATE[]) AS target_date
|
||||
WHERE target_date BETWEEN start_date AND finish_date
|
||||
)
|
||||
SQL
|
||||
|
||||
where("id IN (#{ids_sql})")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
# 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 Queries
|
||||
module PhaseDefinitions
|
||||
# intentionally left empty, just exists to satisfy the autoloader
|
||||
end
|
||||
|
||||
module Project::PhaseDefinitions
|
||||
# This is needed for the phase definition filter to work, even though it is referenced in the
|
||||
# WorkPackages::Query
|
||||
::Queries::Register.register(PhaseDefinitionQuery) do
|
||||
# intentionally left empty
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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.
|
||||
#++
|
||||
|
||||
module Queries::Project
|
||||
module PhaseDefinitions
|
||||
class PhaseDefinitionQuery
|
||||
include Queries::BaseQuery
|
||||
include Queries::UnpersistedQuery
|
||||
|
||||
def self.model
|
||||
Project::PhaseDefinition
|
||||
end
|
||||
|
||||
def default_scope
|
||||
Project::PhaseDefinition.order(:position)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
# 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 Queries::ProjectPhases
|
||||
class PhaseQuery
|
||||
include Queries::BaseQuery
|
||||
include Queries::UnpersistedQuery
|
||||
|
||||
def self.model
|
||||
Project::Phase
|
||||
end
|
||||
|
||||
def default_scope
|
||||
Project::Phase.visible(User.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -62,6 +62,7 @@ module Queries::WorkPackages
|
||||
filter Filter::DatesIntervalFilter
|
||||
filter Filter::ParentFilter
|
||||
filter Filter::PrecedesFilter
|
||||
filter Filter::ProjectPhaseFilter
|
||||
filter Filter::FollowsFilter
|
||||
filter Filter::RelatesFilter
|
||||
filter Filter::DuplicatesFilter
|
||||
@@ -84,6 +85,7 @@ module Queries::WorkPackages
|
||||
exclude Filter::RelatableFilter
|
||||
|
||||
select Selects::PropertySelect
|
||||
select Selects::ProjectPhaseSelect
|
||||
select Selects::CustomFieldSelect
|
||||
select Selects::RelationToTypeSelect
|
||||
select Selects::RelationOfTypeSelect
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# 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 Queries::WorkPackages::Filter::ProjectPhaseFilter < Queries::WorkPackages::Filter::WorkPackageFilter
|
||||
def allowed_values
|
||||
@allowed_values ||= project_phase_definitions.map { |s| [s.name, s.id.to_s] }
|
||||
end
|
||||
|
||||
def available?
|
||||
project_phase_definitions.any?
|
||||
end
|
||||
|
||||
def type
|
||||
:list
|
||||
end
|
||||
|
||||
def self.key
|
||||
:project_phase_definition_id
|
||||
end
|
||||
|
||||
def ar_object_filter?
|
||||
true
|
||||
end
|
||||
|
||||
def value_objects
|
||||
available_definitions = project_phase_definitions.index_by(&:id)
|
||||
|
||||
values.filter_map { |id| available_definitions[id.to_i] }
|
||||
end
|
||||
|
||||
# TODO: copied over from ProjectPhaseSelect, refactor to use the same truth
|
||||
def joins
|
||||
<<~SQL.squish
|
||||
LEFT OUTER JOIN "projects" ON "projects"."id" = "work_packages"."project_id"
|
||||
LEFT OUTER JOIN (
|
||||
SELECT
|
||||
ph.id,
|
||||
ph.project_id,
|
||||
ph.definition_id AS active_phase_definition_id
|
||||
FROM project_phases ph
|
||||
WHERE ph.project_id IN (#{project_with_view_phases_permissions.to_sql})
|
||||
AND ph.active = true
|
||||
) AS active_phases
|
||||
ON active_phases.active_phase_definition_id = work_packages.project_phase_definition_id
|
||||
AND active_phases.project_id = work_packages.project_id
|
||||
SQL
|
||||
end
|
||||
|
||||
def project_with_view_phases_permissions
|
||||
Project.allowed_to(User.current, :view_project_phases).select(:id)
|
||||
end
|
||||
|
||||
def where
|
||||
placeholders = values.map { "?" }.join(",")
|
||||
|
||||
sql = if operator_strategy.to_sym == :"="
|
||||
<<~SQL.squish
|
||||
active_phases.active_phase_definition_id IS NOT NULL AND
|
||||
active_phases.active_phase_definition_id IN (#{placeholders})
|
||||
SQL
|
||||
else
|
||||
<<~SQL.squish
|
||||
active_phases.active_phase_definition_id IS NULL OR
|
||||
active_phases.active_phase_definition_id NOT IN (#{placeholders})
|
||||
SQL
|
||||
end
|
||||
|
||||
ActiveRecord::Base.sanitize_sql_array([sql, *values])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project_phase_feature_flag_enabled?
|
||||
OpenProject::FeatureDecisions.stages_and_gates_active?
|
||||
end
|
||||
|
||||
def project_phase_definitions
|
||||
return Project::PhaseDefinition.none unless project_phase_feature_flag_enabled?
|
||||
|
||||
Project::PhaseDefinition.order(Arel.sql("position"))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
# 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 Queries::WorkPackages::Selects::ProjectPhaseSelect < Queries::WorkPackages::Selects::WorkPackageSelect
|
||||
def initialize
|
||||
super(:project_phase,
|
||||
association: :project_phase_definition,
|
||||
group_by_column_name: :project_phase_definition,
|
||||
sortable: sortable_statement,
|
||||
groupable: group_by_statement,
|
||||
groupable_join: group_by_join_statement,
|
||||
groupable_select: groupable_select
|
||||
)
|
||||
end
|
||||
|
||||
def groupable_select
|
||||
"#{group_by_statement} as project_phase_definition_id"
|
||||
end
|
||||
|
||||
def group_by_statement
|
||||
active_phase_null_case(true_case: "NULL", false_case: "project_phase_definitions.id")
|
||||
end
|
||||
|
||||
def order_for_count
|
||||
active_phase_null_case(true_case: "1", false_case: "0")
|
||||
end
|
||||
|
||||
# Is called when the query is grouped by project phase definition. We ensure that only active project phases are considered.
|
||||
# Note that one project might have an active phase while another project has set the phase with the same definition to inactive.
|
||||
# Additionally, the permissions to view project phases are considered on a project level, too.
|
||||
def group_by_join_statement
|
||||
# The project is joined here anew which should not be necessary but is.
|
||||
# The necessity comes from AR's behaviour of automatically determining the alias for tables LEFT JOINed via includes.
|
||||
# To avoid conflicts, AR will search strings for occurrences of the table name and if found, an included table will be aliased
|
||||
# (potentially with a numbering). In this case, if the permission checks are part of the query, it will include a reference
|
||||
# to the projects table. Therefore, the include for projects, which happens in the query itself, will be considered needing
|
||||
# an alias. That assumption is wrong in this case as the reference to projects is in a subquery but AR does not know that.
|
||||
<<~SQL.squish
|
||||
LEFT OUTER JOIN "projects" ON "projects"."id" = "work_packages"."project_id"
|
||||
LEFT OUTER JOIN (
|
||||
SELECT
|
||||
ph.id,
|
||||
ph.project_id,
|
||||
ph.definition_id AS active_phase_definition_id
|
||||
FROM project_phases ph
|
||||
WHERE ph.project_id IN (#{project_with_view_phases_permissions.to_sql})
|
||||
AND ph.active = true
|
||||
) AS active_phases
|
||||
ON active_phases.active_phase_definition_id = work_packages.project_phase_definition_id
|
||||
AND active_phases.project_id = work_packages.project_id
|
||||
SQL
|
||||
end
|
||||
|
||||
def sortable_join_statement(_query)
|
||||
# Replicate the group by join to ensure the same conditions are applied (and the same alias for the join is used)
|
||||
group_by_join_statement
|
||||
end
|
||||
|
||||
def sortable_statement
|
||||
# We use the join alias from the group by join statement to ensure that work packages with an *inactive* project
|
||||
# phase are treated like work packages *without* a project phase. In the result list, they will belong to the
|
||||
# same group: without an active project phase.
|
||||
active_phase_null_case(true_case: "-1", false_case: "project_phase_definitions.position")
|
||||
end
|
||||
|
||||
def self.instances(context = nil)
|
||||
allowed = if context
|
||||
OpenProject::FeatureDecisions.stages_and_gates_active? &&
|
||||
User.current.allowed_in_project?(:view_project_phases, context)
|
||||
else
|
||||
OpenProject::FeatureDecisions.stages_and_gates_active? &&
|
||||
User.current.allowed_in_any_project?(:view_project_phases)
|
||||
end
|
||||
|
||||
if allowed
|
||||
[new]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project_with_view_phases_permissions
|
||||
Project.allowed_to(User.current, :view_project_phases).select(:id)
|
||||
end
|
||||
|
||||
def active_phase_null_case(true_case:, false_case:)
|
||||
"(CASE WHEN ACTIVE_PHASES.ID IS NULL THEN #{true_case} ELSE #{false_case} END)"
|
||||
end
|
||||
end
|
||||
@@ -145,7 +145,7 @@ class Queries::WorkPackages::Selects::PropertySelect < Queries::WorkPackages::Se
|
||||
}
|
||||
|
||||
def self.instances(_context = nil)
|
||||
property_selects.map do |name, options|
|
||||
property_selects.filter_map do |name, options|
|
||||
new(name, options)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,6 +32,7 @@ class Queries::WorkPackages::Selects::WorkPackageSelect
|
||||
:sortable_join,
|
||||
:groupable_join,
|
||||
:groupable_select,
|
||||
:group_by_column_name,
|
||||
:summable,
|
||||
:default_order,
|
||||
:association,
|
||||
@@ -129,6 +130,7 @@ class Queries::WorkPackages::Selects::WorkPackageSelect
|
||||
summable_select
|
||||
summable_work_packages_select
|
||||
association
|
||||
group_by_column_name
|
||||
null_handling
|
||||
default_order
|
||||
].each do |attribute|
|
||||
|
||||
@@ -289,11 +289,6 @@ class Query < ApplicationRecord
|
||||
.compact_blank
|
||||
.map(&:to_sym)
|
||||
|
||||
# Set column_names to blank/nil if it is equal to the default columns
|
||||
if col_names.map(&:to_s) == Setting.work_package_list_default_columns
|
||||
col_names.clear
|
||||
end
|
||||
|
||||
write_attribute(:column_names, col_names)
|
||||
end
|
||||
|
||||
|
||||
@@ -158,13 +158,8 @@ module ::Query::Results::GroupBy
|
||||
end
|
||||
|
||||
def transform_property_keys(groups)
|
||||
association = WorkPackage.reflect_on_all_associations.detect { |a| a.name == query.group_by_column.name.to_sym }
|
||||
|
||||
if association
|
||||
transform_association_property_keys(association, groups)
|
||||
else
|
||||
groups
|
||||
end
|
||||
association = find_association_for_group
|
||||
association ? transform_association_property_keys(association, groups) : groups
|
||||
end
|
||||
|
||||
def transform_association_property_keys(association, groups)
|
||||
@@ -213,4 +208,17 @@ module ::Query::Results::GroupBy
|
||||
|
||||
"#{order} #{column.null_handling(order == 'asc')}"
|
||||
end
|
||||
|
||||
def find_association_for_group
|
||||
WorkPackage.reflect_on_all_associations.detect do |association|
|
||||
matches_group_by_column?(association)
|
||||
end
|
||||
end
|
||||
|
||||
def matches_group_by_column?(association)
|
||||
# Some query columns override their groupable column name, prefer that if given:
|
||||
group_name = query.group_by_column.group_by_column_name || query.group_by_column.name
|
||||
|
||||
association.name == group_name.to_sym
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,6 +45,10 @@ class Reminder < ApplicationRecord
|
||||
.where.missing(:reminder_notifications)
|
||||
end
|
||||
|
||||
def visible?(user = User.current)
|
||||
creator == user && remindable.visible?(user)
|
||||
end
|
||||
|
||||
def unread_notifications?
|
||||
unread_notifications.exists?
|
||||
end
|
||||
|
||||
@@ -105,18 +105,19 @@ module SharingStrategies
|
||||
end
|
||||
end
|
||||
|
||||
def manage_shares_component(modal_content:, errors:)
|
||||
if EnterpriseToken.allows_to?(:project_list_sharing)
|
||||
super
|
||||
else
|
||||
Shares::ProjectQueries::UpsellComponent.new(modal_content:)
|
||||
end
|
||||
end
|
||||
|
||||
def title
|
||||
I18n.t(:label_share_project_list)
|
||||
end
|
||||
|
||||
def upsell_banner(modal_content:)
|
||||
Shares::ProjectQueries::UpsellComponent.new(modal_content:)
|
||||
end
|
||||
|
||||
def allow_feature?
|
||||
EnterpriseToken.allows_to?(:project_list_sharing) ||
|
||||
EnterpriseToken.trialling?(:project_list_sharing)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def virtual_owner_share
|
||||
|
||||
@@ -99,18 +99,19 @@ module SharingStrategies
|
||||
Shares::WorkPackages::DeleteContract
|
||||
end
|
||||
|
||||
def modal_body_component(errors)
|
||||
if EnterpriseToken.allows_to?(:work_package_sharing)
|
||||
super
|
||||
else
|
||||
Shares::WorkPackages::ModalUpsellComponent.new
|
||||
end
|
||||
end
|
||||
|
||||
def title
|
||||
I18n.t(:label_share_work_package)
|
||||
end
|
||||
|
||||
def upsell_banner(modal_content:)
|
||||
Shares::WorkPackages::ModalUpsellComponent.new(modal_content:)
|
||||
end
|
||||
|
||||
def allow_feature?
|
||||
EnterpriseToken.allows_to?(:work_package_sharing) ||
|
||||
EnterpriseToken.trialling?(:work_package_sharing)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project_member?(share)
|
||||
|
||||
@@ -127,7 +127,7 @@ class WorkPackage < ApplicationRecord
|
||||
where(author_id: author.id)
|
||||
}
|
||||
|
||||
scopes :covering_dates_and_days_of_week,
|
||||
scopes :covering_dates_or_days_of_week,
|
||||
:allowed_to,
|
||||
:for_scheduling,
|
||||
:include_derived_dates,
|
||||
@@ -335,7 +335,7 @@ class WorkPackage < ApplicationRecord
|
||||
end
|
||||
|
||||
def duration_in_hours
|
||||
duration ? duration * 24 : nil
|
||||
duration * 24 if duration
|
||||
end
|
||||
|
||||
def project_phase
|
||||
|
||||
@@ -63,19 +63,18 @@ module WorkPackage::Exports
|
||||
end
|
||||
|
||||
def self.process_match(match, _matched_string, context)
|
||||
context => { user:, work_package: }
|
||||
type = match[2].downcase
|
||||
model_s = match[1]
|
||||
model_s = match[1].downcase
|
||||
id = match[4] || match[3]
|
||||
attribute = match[6] || match[5]
|
||||
resolve_match(type, model_s, id, attribute, work_package, user)
|
||||
resolve_match(type, model_s, id, attribute, context)
|
||||
end
|
||||
|
||||
def self.resolve_match(type, model_s, id, attribute, work_package, user)
|
||||
if model_s == "workPackage"
|
||||
resolve_work_package_match(id || work_package.id, type, attribute, user)
|
||||
def self.resolve_match(type, model_s, id, attribute, context)
|
||||
if model_s == "workpackage"
|
||||
resolve_work_package_match(id || context[:work_package]&.id, type, attribute, context[:user])
|
||||
elsif model_s == "project"
|
||||
resolve_project_match(id || work_package.project.id, type, attribute, user)
|
||||
resolve_project_match(id || context[:project]&.id, type, attribute, context[:user])
|
||||
else
|
||||
msg_macro_error I18n.t("export.macro.model_not_found", model: model_s)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# 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 WorkPackage::Exports
|
||||
module Macros
|
||||
class WorkPackagesLinkHandler < OpenProject::TextFormatting::Matchers::LinkHandlers::WorkPackages
|
||||
def applicable?
|
||||
%w(# ## ###).include?(matcher.sep) && matcher.prefix.blank?
|
||||
end
|
||||
|
||||
def render_link(wp_id, matcher)
|
||||
link = "#{matcher.sep}#{wp_id}"
|
||||
"<mention class=\"mention\" data-id=\"#{wp_id}\" data-type=\"work_package\" data-text=\"#{link}\">#{
|
||||
link
|
||||
}</mention>"
|
||||
end
|
||||
end
|
||||
|
||||
class Links < OpenProject::TextFormatting::Matchers::ResourceLinksMatcher
|
||||
def self.link_handlers
|
||||
[WorkPackagesLinkHandler]
|
||||
end
|
||||
|
||||
def self.html_replacement?
|
||||
true
|
||||
end
|
||||
|
||||
# Faster inclusion check before the full regex is being applied
|
||||
def self.applicable?(content)
|
||||
/#\d/.match(content)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -30,21 +30,17 @@ require "mini_magick"
|
||||
|
||||
module WorkPackage::PDFExport::Common::Attachments
|
||||
def resize_image(file_path)
|
||||
tmp_file = Tempfile.new(["temp_image", File.extname(file_path)])
|
||||
@resized_images = [] if @resized_images.nil?
|
||||
|
||||
@resized_images << tmp_file
|
||||
resized_file_path = tmp_file.path
|
||||
tmp_file = temp_image_file(File.extname(file_path))
|
||||
|
||||
image = MiniMagick::Image.open(file_path)
|
||||
image.resize("x800>")
|
||||
image.write(resized_file_path)
|
||||
image.write(tmp_file)
|
||||
|
||||
resized_file_path
|
||||
tmp_file
|
||||
end
|
||||
|
||||
def pdf_embeddable?(content_type)
|
||||
%w[image/jpeg image/png].include?(content_type)
|
||||
%w[image/jpeg image/png image/gif].include?(content_type)
|
||||
end
|
||||
|
||||
def delete_all_resized_images
|
||||
@@ -59,18 +55,36 @@ module WorkPackage::PDFExport::Common::Attachments
|
||||
nil # return nil as if the id was wrong and the attachment obj has not been found
|
||||
end
|
||||
|
||||
def attachment_image_filepath(work_package, src)
|
||||
def attachment_image_filepath(src)
|
||||
# images are embedded into markup with the api-path as img.src
|
||||
attachment = attachment_by_api_content_src(work_package, src)
|
||||
attachment = attachment_by_api_content_src(src)
|
||||
return nil if attachment.nil? || !pdf_embeddable?(attachment.content_type)
|
||||
|
||||
local_file = attachment_image_local_file(attachment)
|
||||
return nil if local_file.nil?
|
||||
|
||||
resize_image(local_file.path)
|
||||
filename = local_file.path
|
||||
filename = convert_gif_to_png(filename) if attachment.content_type == "image/gif"
|
||||
|
||||
resize_image(filename)
|
||||
end
|
||||
|
||||
def attachment_by_api_content_src(_work_package, src)
|
||||
def temp_image_file(extension)
|
||||
tmp_file = Tempfile.new(["temp_image", extension])
|
||||
@resized_images = [] if @resized_images.nil?
|
||||
@resized_images << tmp_file
|
||||
tmp_file.path
|
||||
end
|
||||
|
||||
def convert_gif_to_png(filename)
|
||||
tmp_file = temp_image_file(".png")
|
||||
|
||||
image = MiniMagick::Image.open(filename)
|
||||
image.frames.first.write(tmp_file)
|
||||
tmp_file
|
||||
end
|
||||
|
||||
def attachment_by_api_content_src(src)
|
||||
attachment_regex = %r{/attachments/(\d+)/content}
|
||||
return nil unless src&.match?(attachment_regex)
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ module WorkPackage::PDFExport::Common::Badge
|
||||
def initialize(options)
|
||||
@color = options[:color]
|
||||
@document = options[:document]
|
||||
@radius = options[:radius]
|
||||
@radius = options[:radius] || 0
|
||||
@offset = options[:offset] || 0
|
||||
end
|
||||
|
||||
@@ -45,21 +45,30 @@ module WorkPackage::PDFExport::Common::Badge
|
||||
end
|
||||
end
|
||||
|
||||
def calc_grayscale_brightness(red, green, blue)
|
||||
# The brightness is calculated using the formula:
|
||||
# https://entropymine.com/imageworsener/grayscale/
|
||||
Math.sqrt(
|
||||
(0.2126 * (red**2)) +
|
||||
(0.7152 * (green**2)) +
|
||||
(0.0722 * (blue**2))
|
||||
).to_i
|
||||
end
|
||||
|
||||
def readable_color(pdf_background_color)
|
||||
sum = [
|
||||
brightness = calc_grayscale_brightness(*[
|
||||
pdf_background_color[0..1],
|
||||
pdf_background_color[2..3],
|
||||
pdf_background_color[4..5]
|
||||
].sum { |color_part| color_part.to_i(16) }
|
||||
lightness = sum / 3
|
||||
lightness < 130 ? "FFFFFF" : "000000"
|
||||
].map { |color_part| color_part.to_i(16) })
|
||||
brightness < 130 ? "FFFFFF" : "000000"
|
||||
end
|
||||
|
||||
def prawn_badge(text, color, offset: 0)
|
||||
badge = BadgeCallback.new({ color: color, radius: 8, document: pdf, offset: })
|
||||
def prawn_badge(text, color, offset: 0, radius: 8, font_size: 8)
|
||||
badge = BadgeCallback.new({ color: color, radius:, document: pdf, offset: })
|
||||
{
|
||||
text: (Prawn::Text::NBSP * 3) + text + (Prawn::Text::NBSP * 3),
|
||||
size: 8,
|
||||
size: font_size,
|
||||
color: readable_color(color),
|
||||
callback: badge
|
||||
}
|
||||
|
||||
@@ -129,11 +129,15 @@ module WorkPackage::PDFExport::Common::Common
|
||||
end
|
||||
|
||||
def draw_horizontal_line(top, left, right, height, color)
|
||||
pdf.stroke do
|
||||
previous_color = pdf.stroke_color
|
||||
previous_line_width = pdf.line_width
|
||||
@pdf.stroke do
|
||||
pdf.stroke_color = color
|
||||
pdf.line_width = height
|
||||
pdf.horizontal_line left, right, at: top
|
||||
end
|
||||
pdf.stroke_color = previous_color
|
||||
pdf.line_width = previous_line_width
|
||||
end
|
||||
|
||||
def draw_styled_text(text, opts)
|
||||
@@ -277,17 +281,17 @@ module WorkPackage::PDFExport::Common::Common
|
||||
end
|
||||
|
||||
def footer_date
|
||||
format_time(export_datetime)
|
||||
format_date(export_datetime)
|
||||
end
|
||||
|
||||
def current_page_nr
|
||||
pdf.page_number + @page_count - (with_cover? ? 1 : 0)
|
||||
end
|
||||
|
||||
def write_horizontal_line(y_position, height, color)
|
||||
def write_horizontal_line(y_position, height, color, left_padding: 0)
|
||||
draw_horizontal_line(
|
||||
y_position,
|
||||
pdf.bounds.left, pdf.bounds.right,
|
||||
pdf.bounds.left + left_padding, pdf.bounds.right,
|
||||
height, color
|
||||
)
|
||||
end
|
||||
@@ -297,6 +301,30 @@ module WorkPackage::PDFExport::Common::Common
|
||||
pdf.start_new_page unless is_first_on_page
|
||||
end
|
||||
|
||||
# Prawn table does not support inline formatting other than inline HTML formatting, so we have to convert the styling
|
||||
def prawn_table_cell_inline_formatting_data(text, style) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
||||
value = text || ""
|
||||
value = "<link href=\"#{style[:link]}\">#{value}</link>" if style.key?(:link)
|
||||
value = "<link anchor=\"#{style[:anchor]}\">#{value}</link>" if style.key?(:anchor)
|
||||
value = "<color rgb=\"#{style[:color]}\">#{value}</color>" if style.include?(:color)
|
||||
value = "<font size=\"#{style[:size]}\">#{value}</font>" if style.key?(:size)
|
||||
value = "<font character_spacing=\"#{style[:character_spacing]}\">#{value}</font>" if style.key?(:character_spacing)
|
||||
value = "<font name=\"#{style[:font]}\">#{value}</font>" if style.key?(:font)
|
||||
prawn_table_cell_inline_font_styles(value, style[:styles] || [])
|
||||
end
|
||||
|
||||
# Prawn table does not support inline formatting other than inline HTML formatting, so we have to convert the styles
|
||||
def prawn_table_cell_inline_font_styles(text, styles)
|
||||
value = text || ""
|
||||
value = "<b>#{value}</b>" if styles.include?(:bold)
|
||||
value = "<i>#{value}</i>" if styles.include?(:italic)
|
||||
value = "<u>#{value}</u>" if styles.include?(:underline)
|
||||
value = "<strikethrough>#{value}</strikethrough>" if styles.include?(:strikethrough)
|
||||
value = "<sub>#{value}</sub>" if styles.include?(:sub)
|
||||
value = "<sup>#{value}</sup>" if styles.include?(:sup)
|
||||
value
|
||||
end
|
||||
|
||||
def write_optional_page_break
|
||||
space_from_bottom = pdf.y - pdf.bounds.bottom
|
||||
if space_from_bottom < styles.page_break_threshold
|
||||
|
||||
@@ -29,48 +29,104 @@
|
||||
module WorkPackage::PDFExport::Common::Macro
|
||||
PREFORMATTED_BLOCKS = %w(pre code).freeze
|
||||
|
||||
def apply_markdown_field_macros(markdown, work_package)
|
||||
apply_macros(markdown, work_package, WorkPackage::Exports::Macros::Attributes)
|
||||
def apply_markdown_field_macros(markdown, context)
|
||||
return markdown if markdown.blank?
|
||||
|
||||
apply_macros(markdown, context)
|
||||
end
|
||||
|
||||
def macros
|
||||
[
|
||||
WorkPackage::Exports::Macros::Links,
|
||||
WorkPackage::Exports::Macros::Attributes
|
||||
]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def apply_macros(markdown, work_package, formatter)
|
||||
return markdown unless formatter.applicable?(markdown)
|
||||
def apply_macros(markdown, context)
|
||||
return markdown unless macros.any? { |macro| macro.applicable?(markdown) }
|
||||
|
||||
document = Markly.parse(markdown)
|
||||
document.walk do |node|
|
||||
if node.type == :html
|
||||
node.string_content = apply_macro_html(node.string_content, work_package, formatter) || node.string_content
|
||||
elsif node.type == :text
|
||||
node.string_content = apply_macro_text(node.string_content, work_package, formatter) || node.string_content
|
||||
document.walk { |node| apply_macros_node(node, context) }
|
||||
document.to_markdown
|
||||
.gsub("\\~", "~") # fix a bug in Markly that escapes tildes
|
||||
end
|
||||
|
||||
def apply_macros_node(node, context)
|
||||
if node.type == :html
|
||||
apply_macros_node_html(node, context)
|
||||
elsif node.type == :text && not_in_mention?(node)
|
||||
apply_macros_node_text(node, context)
|
||||
end
|
||||
end
|
||||
|
||||
def not_in_mention?(node)
|
||||
# Check if the text node is not inside a mention tag
|
||||
# e.g. <mention ...>#1234</mention>
|
||||
# We don't want to end up with
|
||||
# <mention ...><mention ...>#1234</mention></mention>
|
||||
node.previous.nil? || node.previous.type != :inline_html || node.previous.string_content.exclude?("<mention")
|
||||
end
|
||||
|
||||
def apply_macros_node_text(node, context)
|
||||
formatted, applied_macros = apply_macro_text(node.string_content, context)
|
||||
return if formatted == node.string_content
|
||||
|
||||
if has_html_macro?(applied_macros)
|
||||
fragment = Markly::Node.new(:inline_html)
|
||||
fragment.string_content = formatted
|
||||
node.insert_before(fragment)
|
||||
node.delete
|
||||
else
|
||||
node.string_content = formatted
|
||||
end
|
||||
end
|
||||
|
||||
def apply_macros_node_html(node, context)
|
||||
node.string_content = apply_macro_html(node.string_content, context)
|
||||
end
|
||||
|
||||
def applicable?(content)
|
||||
macros.any? { |macro| macro.applicable?(content) }
|
||||
end
|
||||
|
||||
def apply_macro_text(text, context)
|
||||
applicable_macros = macros.select { |macro| macro.applicable?(text) }
|
||||
return text if applicable_macros.empty?
|
||||
|
||||
applicable_macros.each do |macro|
|
||||
text = text.gsub(macro.regexp) do |matched_string|
|
||||
macro.process_match(Regexp.last_match, matched_string, context)
|
||||
end
|
||||
end
|
||||
document.to_markdown
|
||||
[text, applicable_macros]
|
||||
end
|
||||
|
||||
def apply_macro_text(text, work_package, formatter)
|
||||
return text unless formatter.applicable?(text)
|
||||
|
||||
text.gsub!(formatter.regexp) do |matched_string|
|
||||
matchdata = Regexp.last_match
|
||||
formatter.process_match(matchdata, matched_string, { user: User.current, work_package: })
|
||||
end
|
||||
end
|
||||
|
||||
def apply_macro_html(html, work_package, formatter)
|
||||
return html unless formatter.applicable?(html)
|
||||
def apply_macro_html(html, context)
|
||||
return html unless applicable?(html)
|
||||
|
||||
doc = Nokogiri::HTML.fragment(html)
|
||||
apply_macro_html_node(doc, work_package, formatter)
|
||||
apply_macro_html_node(doc, context)
|
||||
doc.to_html
|
||||
end
|
||||
|
||||
def apply_macro_html_node(node, work_package, formatter)
|
||||
def has_html_macro?(macros)
|
||||
macros.any? { |macro| macro.respond_to?(:html_replacement?) && macro.html_replacement? }
|
||||
end
|
||||
|
||||
def apply_macro_html_node(node, context)
|
||||
if node.text?
|
||||
node.content = apply_macro_text(node.content, work_package, formatter)
|
||||
formatted, applied_macros = apply_macro_text(node.content, context)
|
||||
if formatted != node.content
|
||||
if has_html_macro?(applied_macros)
|
||||
node.replace(formatted)
|
||||
else
|
||||
node.content = formatted
|
||||
end
|
||||
end
|
||||
elsif PREFORMATTED_BLOCKS.exclude?(node.name)
|
||||
node.children.each { |child| apply_macro_html_node(child, work_package, formatter) }
|
||||
node.children.each { |child| apply_macro_html_node(child, context) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -86,7 +86,7 @@ module WorkPackage::PDFExport::Common::StylesPage
|
||||
end
|
||||
|
||||
def page_break_threshold
|
||||
resolve_pt(@styles.dig(:page, :page_break_threshold), 200)
|
||||
@page_break_threshold ||= resolve_pt(@styles.dig(:page, :page_break_threshold), 200)
|
||||
end
|
||||
|
||||
def link_color
|
||||
|
||||
@@ -65,7 +65,8 @@ class WorkPackage::PDFExport::DocumentGenerator < Exports::Exporter
|
||||
|
||||
def render_doc
|
||||
generate_doc!(
|
||||
apply_markdown_field_macros(work_package.description || "", work_package),
|
||||
apply_markdown_field_macros(work_package.description || "",
|
||||
{ work_package:, project: work_package.project, user: User.current }),
|
||||
"contracts.yml"
|
||||
)
|
||||
end
|
||||
|
||||
@@ -34,6 +34,7 @@ module WorkPackage::PDFExport::Export::Markdown
|
||||
class MD2PDFExport
|
||||
include MarkdownToPDF::Core
|
||||
include MarkdownToPDF::Parser
|
||||
include WorkPackage::PDFExport::Common::Common
|
||||
|
||||
def initialize(styling_yml, pdf, hyphenation_language)
|
||||
@styles = MarkdownToPDF::Styles.new(styling_yml)
|
||||
@@ -68,9 +69,8 @@ module WorkPackage::PDFExport::Export::Markdown
|
||||
@hyphens.hyphenate(text)
|
||||
end
|
||||
|
||||
def handle_mention_html_tag(tag, node, opts)
|
||||
def handle_user_mention_html_tag(tag, node, opts)
|
||||
if tag.text.blank?
|
||||
# <mention class="mention" data-id="46012" data-type="work_package" data-text="#46012"></mention>
|
||||
# <mention class="mention" data-id="3" data-type="user" data-text="@Some User">
|
||||
text = tag.attr("data-text")
|
||||
if text.present? && !node.next.respond_to?(:string_content) && node.next.string_content != text
|
||||
@@ -78,9 +78,70 @@ module WorkPackage::PDFExport::Export::Markdown
|
||||
end
|
||||
end
|
||||
# <mention class="mention" data-id="3" data-type="user" data-text="@Some User">@Some User</mention>
|
||||
# the node text is used.
|
||||
[]
|
||||
end
|
||||
|
||||
def handle_wp_mention_html_tag(tag, node, opts)
|
||||
# <mention class="mention" data-id="185" data-type="work_package" data-text="#185">#185</mention>
|
||||
# <mention class="mention" data-id="185" data-type="work_package" data-text="##185">##185</mention>
|
||||
# <mention class="mention" data-id="185" data-type="work_package" data-text="###185">###185</mention>
|
||||
id = tag.attr("data-id")
|
||||
return [] if id.blank?
|
||||
|
||||
next_node = node&.next # there is no markdown node in a html table
|
||||
if next_node && next_node.type == :text && next_node.respond_to?(:string_content)
|
||||
# clear the text content, so it does not get rendered
|
||||
next_node.string_content = ""
|
||||
end
|
||||
wp_mention_macro(tag.attr("data-text") || "", id, opts)
|
||||
end
|
||||
|
||||
def wp_mention_macro(content, id, opts)
|
||||
count = content.count("#")
|
||||
if count > 1
|
||||
work_package = WorkPackage.find_by(id: id)
|
||||
unless work_package.nil? || !work_package.visible?
|
||||
# ##1234: {Type} #{ID}: {Subject}
|
||||
content = "#{work_package.type} ##{work_package.id}: #{work_package.subject}"
|
||||
if count == 3
|
||||
# ###1234: {Status} {Type} #{ID}: {Subject} ({Start Date} - {End Date})
|
||||
content = "#{work_package.status.name} #{content}#{work_package_dates(work_package)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
[text_hash(content, opts.merge({ link: url_helpers.work_package_url(id) }))]
|
||||
end
|
||||
|
||||
def work_package_dates(work_package)
|
||||
return "" if work_package.start_date.blank? && work_package.due_date.blank?
|
||||
|
||||
if work_package.due_date.present? && work_package.start_date == work_package.due_date
|
||||
return " (#{format_date(work_package.due_date)})"
|
||||
end
|
||||
|
||||
work_package_date_range(work_package)
|
||||
end
|
||||
|
||||
def work_package_date_range(work_package)
|
||||
content = [
|
||||
work_package.start_date.present? ? format_date(work_package.start_date) : I18n.t("label_no_start_date"),
|
||||
work_package.due_date.present? ? format_date(work_package.due_date) : I18n.t("label_no_due_date")
|
||||
].join(" - ")
|
||||
" (#{content})"
|
||||
end
|
||||
|
||||
def handle_mention_html_tag(tag, node, opts)
|
||||
type = tag.attr("data-type")
|
||||
if type == "work_package"
|
||||
handle_wp_mention_html_tag(tag, node, opts)
|
||||
elsif type == "user"
|
||||
handle_user_mention_html_tag(tag, node, opts)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def handle_unknown_inline_html_tag(tag, node, opts)
|
||||
result = if tag.name == "mention"
|
||||
handle_mention_html_tag(tag, node, opts)
|
||||
@@ -93,9 +154,13 @@ module WorkPackage::PDFExport::Export::Markdown
|
||||
end
|
||||
|
||||
def handle_unknown_html_tag(_tag, _node, opts)
|
||||
# unknown/unsupported html tags eg. <foo>hi</foo> are ignored
|
||||
# but scanned for supported or text children [true, ...]
|
||||
[true, opts]
|
||||
if tag.name == "mention"
|
||||
handle_mention_html_tag(tag, node, opts)
|
||||
else
|
||||
# unknown/unsupported html tags eg. <foo>hi</foo> are ignored
|
||||
# but scanned for supported or text children [true, ...]
|
||||
[true, opts]
|
||||
end
|
||||
end
|
||||
|
||||
def warn(text, element, node)
|
||||
@@ -103,10 +168,14 @@ module WorkPackage::PDFExport::Export::Markdown
|
||||
end
|
||||
end
|
||||
|
||||
def write_markdown!(work_package, markdown, styling_yml)
|
||||
md2pdf = MD2PDFExport.new(styling_yml, pdf, hyphenation_language)
|
||||
md2pdf.draw_markdown(markdown, pdf, ->(src) {
|
||||
with_images? ? attachment_image_filepath(work_package, src) : nil
|
||||
})
|
||||
def markdown_writer(styling_yml)
|
||||
@markdown_writer ||= MD2PDFExport.new(styling_yml, pdf, hyphenation_language)
|
||||
end
|
||||
|
||||
def write_markdown!(markdown, styling_yml)
|
||||
markdown_writer(styling_yml)
|
||||
.draw_markdown(markdown, pdf, ->(src) {
|
||||
with_images? ? attachment_image_filepath(src) : nil
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,8 +51,8 @@ module WorkPackage::PDFExport::Export::MarkdownField
|
||||
def write_markdown_field_value(work_package, markdown)
|
||||
with_margin(styles.wp_markdown_margins) do
|
||||
write_markdown!(
|
||||
work_package,
|
||||
apply_markdown_field_macros(markdown, work_package),
|
||||
apply_markdown_field_macros(markdown,
|
||||
{ work_package:, project: work_package.project, user: User.current }),
|
||||
styles.wp_markdown_styling_yml
|
||||
)
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,459 @@
|
||||
page:
|
||||
page_size: "A4"
|
||||
margin_top: 60
|
||||
margin_bottom: 60
|
||||
margin_left: 36
|
||||
margin_right: 36
|
||||
page_break_threshold: 100
|
||||
link_color: "175A8E"
|
||||
|
||||
page_header:
|
||||
offset: 20
|
||||
size: 8
|
||||
|
||||
page_footer:
|
||||
offset: -30
|
||||
size: 8
|
||||
spacing: 6
|
||||
|
||||
page_logo:
|
||||
height: 20
|
||||
align: "right"
|
||||
|
||||
page_heading:
|
||||
size: 14
|
||||
styles: [ "bold" ]
|
||||
margin_bottom: 2
|
||||
|
||||
page_subtitle:
|
||||
margin_top: 4
|
||||
margin_bottom: 10
|
||||
size: 10
|
||||
|
||||
cover:
|
||||
header:
|
||||
logo_height: 25
|
||||
border:
|
||||
color: 'd3dee3'
|
||||
height: 1
|
||||
offset: 6
|
||||
footer:
|
||||
size: 10
|
||||
color: '414d5f'
|
||||
offset: 30
|
||||
hero:
|
||||
padding_right: 144
|
||||
padding_top: 120
|
||||
title:
|
||||
max_height: 30
|
||||
spacing: 10
|
||||
font: 'SpaceMono'
|
||||
color: '414d5f'
|
||||
size: 10
|
||||
heading:
|
||||
spacing: 14
|
||||
color: '414d5f'
|
||||
styles:
|
||||
- bold
|
||||
size: 16
|
||||
dates:
|
||||
spacing: 4
|
||||
max_height: 16
|
||||
color: '414d5f'
|
||||
size: 10
|
||||
styles:
|
||||
- bold
|
||||
subheading:
|
||||
max_height: 30
|
||||
color: '414d5f'
|
||||
size: 10
|
||||
|
||||
notes:
|
||||
markdown_margin:
|
||||
margin: 16
|
||||
markdown:
|
||||
font:
|
||||
size: 10
|
||||
leading: 3
|
||||
header:
|
||||
size: 8
|
||||
styles: [ "bold" ]
|
||||
padding_top: 4
|
||||
header_1:
|
||||
size: 10
|
||||
header_2:
|
||||
size: 10
|
||||
header_3:
|
||||
size: 9
|
||||
paragraph:
|
||||
align: "left"
|
||||
unordered_list:
|
||||
spacing: 1
|
||||
unordered_list_point:
|
||||
spacing: 4
|
||||
ordered_list:
|
||||
spacing: 1
|
||||
ordered_list_point:
|
||||
spacing: 4
|
||||
spanning: true
|
||||
list_style_type: decimal
|
||||
ordered_list_point_2:
|
||||
list_style_type: lower-latin
|
||||
ordered_list_point_3:
|
||||
list_style_type: lower-roman
|
||||
ordered_list_point_4:
|
||||
list_style_type: upper-latin
|
||||
ordered_list_point_5:
|
||||
list_style_type: upper-roman
|
||||
task_list:
|
||||
spacing: 1
|
||||
task_list_point:
|
||||
spacing: 4
|
||||
checked: "☑"
|
||||
unchecked: "☐"
|
||||
link:
|
||||
color: "175A8E"
|
||||
styles: [ ]
|
||||
code:
|
||||
color: "880000"
|
||||
size: 9
|
||||
font: "SpaceMono"
|
||||
blockquote:
|
||||
background_color: "f4f9ff"
|
||||
size: 10
|
||||
styles: [ "italic" ]
|
||||
color: "0f3b66"
|
||||
border_color: "b8d6f4"
|
||||
border_width: 1
|
||||
padding: 4
|
||||
padding_left: 6
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
no_border_left: false
|
||||
no_border_right: true
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
image:
|
||||
align: "center"
|
||||
margin_bottom: 4
|
||||
caption:
|
||||
size: 8
|
||||
align: "center"
|
||||
codeblock:
|
||||
background_color: "F5F5F5"
|
||||
color: "880000"
|
||||
padding: 10
|
||||
size: 8
|
||||
margin_top: 10
|
||||
margin_bottom: 10
|
||||
font: "SpaceMono"
|
||||
table:
|
||||
auto_width: true
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
header:
|
||||
size: 9
|
||||
styles: [ "bold" ]
|
||||
background_color: "F0F0F0"
|
||||
cell:
|
||||
size: 8
|
||||
border_width: 0.25
|
||||
padding: 5
|
||||
html_table:
|
||||
auto_width: true
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
header:
|
||||
size: 9
|
||||
styles: [ "bold" ]
|
||||
cell:
|
||||
size: 8
|
||||
border_width: 0.25
|
||||
padding: 5
|
||||
alerts:
|
||||
NOTE:
|
||||
border_color: '0969da'
|
||||
alert_color: '0969da'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
TIP:
|
||||
border_color: '1a7f37'
|
||||
alert_color: '1a7f37'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
IMPORTANT:
|
||||
border_color: '8250df'
|
||||
alert_color: '8250df'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
WARNING:
|
||||
border_color: 'bf8700'
|
||||
alert_color: 'bf8700'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
CAUTION:
|
||||
border_color: 'd1242f'
|
||||
alert_color: 'd1242f'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
padding: '4mm'
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
|
||||
outcome:
|
||||
indent: 15
|
||||
title:
|
||||
size: 10
|
||||
styles: [ "bold" ]
|
||||
margin_top: 5
|
||||
margin_bottom: 5
|
||||
markdown_margin:
|
||||
margin: 16
|
||||
markdown:
|
||||
font:
|
||||
size: 10
|
||||
leading: 3
|
||||
header:
|
||||
size: 8
|
||||
styles: [ "bold" ]
|
||||
padding_top: 4
|
||||
header_1:
|
||||
size: 10
|
||||
header_2:
|
||||
size: 10
|
||||
header_3:
|
||||
size: 9
|
||||
paragraph:
|
||||
align: "left"
|
||||
unordered_list:
|
||||
spacing: 1
|
||||
unordered_list_point:
|
||||
spacing: 4
|
||||
ordered_list:
|
||||
spacing: 1
|
||||
ordered_list_point:
|
||||
spacing: 4
|
||||
spanning: true
|
||||
list_style_type: decimal
|
||||
ordered_list_point_2:
|
||||
list_style_type: lower-latin
|
||||
ordered_list_point_3:
|
||||
list_style_type: lower-roman
|
||||
ordered_list_point_4:
|
||||
list_style_type: upper-latin
|
||||
ordered_list_point_5:
|
||||
list_style_type: upper-roman
|
||||
task_list:
|
||||
spacing: 1
|
||||
task_list_point:
|
||||
spacing: 4
|
||||
checked: "☑"
|
||||
unchecked: "☐"
|
||||
link:
|
||||
color: "175A8E"
|
||||
styles: [ ]
|
||||
code:
|
||||
color: "880000"
|
||||
size: 9
|
||||
font: "SpaceMono"
|
||||
blockquote:
|
||||
background_color: "f4f9ff"
|
||||
size: 10
|
||||
styles: [ "italic" ]
|
||||
color: "0f3b66"
|
||||
border_color: "b8d6f4"
|
||||
border_width: 1
|
||||
padding: 4
|
||||
padding_left: 6
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
no_border_left: false
|
||||
no_border_right: true
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
image:
|
||||
align: "center"
|
||||
margin_bottom: 4
|
||||
caption:
|
||||
size: 8
|
||||
align: "center"
|
||||
codeblock:
|
||||
background_color: "F5F5F5"
|
||||
color: "880000"
|
||||
padding: 10
|
||||
size: 8
|
||||
margin_top: 10
|
||||
margin_bottom: 10
|
||||
font: "SpaceMono"
|
||||
table:
|
||||
auto_width: true
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
header:
|
||||
size: 9
|
||||
styles: [ "bold" ]
|
||||
background_color: "F0F0F0"
|
||||
cell:
|
||||
size: 8
|
||||
border_width: 0.25
|
||||
padding: 5
|
||||
html_table:
|
||||
auto_width: true
|
||||
margin_top: 4
|
||||
margin_bottom: 4
|
||||
header:
|
||||
size: 9
|
||||
styles: [ "bold" ]
|
||||
cell:
|
||||
size: 8
|
||||
border_width: 0.25
|
||||
padding: 5
|
||||
alerts:
|
||||
NOTE:
|
||||
border_color: '0969da'
|
||||
alert_color: '0969da'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
TIP:
|
||||
border_color: '1a7f37'
|
||||
alert_color: '1a7f37'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
IMPORTANT:
|
||||
border_color: '8250df'
|
||||
alert_color: '8250df'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
WARNING:
|
||||
border_color: 'bf8700'
|
||||
alert_color: 'bf8700'
|
||||
padding: '4mm'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
CAUTION:
|
||||
border_color: 'd1242f'
|
||||
alert_color: 'd1242f'
|
||||
size: 10
|
||||
styles: [ ]
|
||||
padding: '4mm'
|
||||
border_width: 2
|
||||
no_border_right: true
|
||||
no_border_left: false
|
||||
no_border_bottom: true
|
||||
no_border_top: true
|
||||
|
||||
heading:
|
||||
size: 12
|
||||
styles: [ "bold" ]
|
||||
margin_bottom: 8
|
||||
hr:
|
||||
color: "6E7781"
|
||||
height: 1.5
|
||||
margin_bottom: 10
|
||||
|
||||
participants:
|
||||
margin_bottom: 8
|
||||
cell:
|
||||
size: 10
|
||||
padding_left: 0
|
||||
padding_bottom: 7
|
||||
no_border: true
|
||||
|
||||
attachments:
|
||||
margin_bottom: 8
|
||||
cell:
|
||||
size: 10
|
||||
padding_left: 0
|
||||
padding_bottom: 7
|
||||
no_border: true
|
||||
|
||||
agenda_section:
|
||||
title:
|
||||
size: 12
|
||||
color: "000000"
|
||||
styles: [ "bold" ]
|
||||
subtitle:
|
||||
size: 11
|
||||
color: "636C76"
|
||||
title_cell:
|
||||
padding_top: 2
|
||||
padding_bottom: 10
|
||||
padding_left: 5
|
||||
padding_right: 5
|
||||
no_border: true
|
||||
background_color: "EAEAEA"
|
||||
title_margins:
|
||||
margin_top: 15
|
||||
margin_bottom: 5
|
||||
|
||||
agenda_item:
|
||||
indent: 5
|
||||
title_margin:
|
||||
margin_bottom: 10
|
||||
title_cell:
|
||||
padding_top: 0
|
||||
padding_bottom: 0
|
||||
padding_left: 5
|
||||
padding_right: 5
|
||||
no_border: true
|
||||
title:
|
||||
styles: [ "bold" ]
|
||||
size: 11
|
||||
subtitle:
|
||||
color: "636C76"
|
||||
size: 10
|
||||
hr:
|
||||
color: "D0D7DE"
|
||||
height: 1
|
||||
margin_top: 6
|
||||
margin_bottom: 6
|
||||
@@ -0,0 +1,163 @@
|
||||
# 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 WorkPackage::PDFExport::Export::Meetings::Styles
|
||||
class PDFStyles
|
||||
include MarkdownToPDF::Common
|
||||
include MarkdownToPDF::StyleHelper
|
||||
include WorkPackage::PDFExport::Common::Styles
|
||||
include WorkPackage::PDFExport::Common::StylesPage
|
||||
include WorkPackage::PDFExport::Common::StylesCover
|
||||
|
||||
def page_subtitle
|
||||
resolve_font(@styles[:page_subtitle])
|
||||
end
|
||||
|
||||
def page_subtitle_margins
|
||||
resolve_margin(@styles[:page_subtitle])
|
||||
end
|
||||
|
||||
def notes_markdown_margins
|
||||
resolve_margin(@styles.dig(:work_package, :markdown_margin))
|
||||
end
|
||||
|
||||
def notes_markdown_styling_yml
|
||||
resolve_markdown_styling(@styles.dig(:notes, :markdown) || {})
|
||||
end
|
||||
|
||||
def outcome_markdown_styling_yml
|
||||
resolve_markdown_styling(@styles.dig(:outcome, :markdown) || {})
|
||||
end
|
||||
|
||||
def heading
|
||||
resolve_font(@styles[:heading])
|
||||
end
|
||||
|
||||
def heading_hr
|
||||
{
|
||||
color: @styles.dig(:heading, :hr, :color),
|
||||
height: resolve_pt(@styles.dig(:heading, :hr, :height), 1)
|
||||
}
|
||||
end
|
||||
|
||||
def heading_hr_margins
|
||||
resolve_margin(@styles.dig(:heading, :hr))
|
||||
end
|
||||
|
||||
def agenda_item_title_margins
|
||||
resolve_margin(@styles.dig(:agenda_item, :title_margin))
|
||||
end
|
||||
|
||||
def agenda_item_indent
|
||||
@styles.dig(:agenda_item, :indent).presence || 5
|
||||
end
|
||||
|
||||
def outcome_title
|
||||
resolve_font(@styles.dig(:outcome, :title))
|
||||
end
|
||||
|
||||
def outcome_title_margins
|
||||
resolve_margin(@styles.dig(:outcome, :title))
|
||||
end
|
||||
|
||||
def outcome_indent
|
||||
@styles.dig(:outcome, :indent).presence || 15
|
||||
end
|
||||
|
||||
def agenda_item_title
|
||||
resolve_font(@styles.dig(:agenda_item, :title))
|
||||
end
|
||||
|
||||
def agenda_item_title_cell
|
||||
resolve_table_cell(@styles.dig(:agenda_item, :title_cell))
|
||||
end
|
||||
|
||||
def agenda_item_subtitle
|
||||
resolve_font(@styles.dig(:agenda_item, :subtitle))
|
||||
end
|
||||
|
||||
def agenda_item_hr
|
||||
{
|
||||
color: @styles.dig(:agenda_item, :hr, :color),
|
||||
height: resolve_pt(@styles.dig(:agenda_item, :hr, :height), 1)
|
||||
}
|
||||
end
|
||||
|
||||
def agenda_item_margins
|
||||
resolve_margin(@styles.dig(:agenda_item, :hr))
|
||||
end
|
||||
|
||||
def heading_margins
|
||||
resolve_margin(@styles[:heading])
|
||||
end
|
||||
|
||||
def participants_table_cell
|
||||
resolve_table_cell(@styles.dig(:participants, :cell))
|
||||
end
|
||||
|
||||
def participants_margins
|
||||
resolve_margin(@styles[:participants])
|
||||
end
|
||||
|
||||
def attachments_table_cell
|
||||
resolve_table_cell(@styles.dig(:attachments, :cell))
|
||||
end
|
||||
|
||||
def attachments_margins
|
||||
resolve_margin(@styles[:attachments])
|
||||
end
|
||||
|
||||
def agenda_section_title
|
||||
resolve_font(@styles.dig(:agenda_section, :title))
|
||||
end
|
||||
|
||||
def agenda_section_title_table_margins
|
||||
resolve_margin(@styles.dig(:agenda_section, :title_margins))
|
||||
end
|
||||
|
||||
def agenda_section_subtitle
|
||||
resolve_font(@styles.dig(:agenda_section, :subtitle))
|
||||
end
|
||||
|
||||
def agenda_section_title_cell
|
||||
resolve_table_cell(@styles.dig(:agenda_section, :title_cell))
|
||||
end
|
||||
end
|
||||
|
||||
def styles
|
||||
@styles ||= PDFStyles.new(styles_asset_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def styles_asset_path
|
||||
File.dirname(File.expand_path(__FILE__))
|
||||
end
|
||||
end
|
||||
@@ -135,7 +135,7 @@ module WorkPackage::PDFExport::Generator::Generator
|
||||
if src == logo_image_filename
|
||||
logo_image_filename
|
||||
else
|
||||
attachment_image_filepath(work_package, src)
|
||||
attachment_image_filepath(src)
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
+24
-26
@@ -26,7 +26,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module WorkPackages::Scopes::CoveringDatesAndDaysOfWeek
|
||||
module WorkPackages::Scopes::CoveringDatesOrDaysOfWeek
|
||||
extend ActiveSupport::Concern
|
||||
using CoreExtensions::SquishSql
|
||||
|
||||
@@ -38,7 +38,7 @@ module WorkPackages::Scopes::CoveringDatesAndDaysOfWeek
|
||||
# @param dates Date[] An array of the Date objects.
|
||||
# @param days_of_week number[] An array of the ISO days of the week to
|
||||
# consider. 1 is Monday, 7 is Sunday.
|
||||
def covering_dates_and_days_of_week(days_of_week: [], dates: [])
|
||||
def covering_dates_or_days_of_week(days_of_week: [], dates: [])
|
||||
work_packages_periods_cte = work_packages_periods_cte_for_covering_work_packages
|
||||
where_covers_periods(work_packages_periods_cte, days_of_week, dates)
|
||||
end
|
||||
@@ -59,30 +59,28 @@ module WorkPackages::Scopes::CoveringDatesAndDaysOfWeek
|
||||
-- select work packages dates
|
||||
WITH
|
||||
-- cte returning a table with work package id, period start_date and end_date
|
||||
#{work_packages_periods_cte},
|
||||
|
||||
-- All days between the start date of a work package and its due date
|
||||
covered_dates AS (
|
||||
SELECT
|
||||
id,
|
||||
generate_series(work_packages_periods.start_date,
|
||||
work_packages_periods.end_date,
|
||||
'1 day') AS date
|
||||
FROM work_packages_periods
|
||||
),
|
||||
|
||||
-- All days between the start date of a work package and its due date including the day of the week for each date
|
||||
covered_dates_and_wday AS (
|
||||
SELECT
|
||||
id,
|
||||
date,
|
||||
EXTRACT(isodow FROM date) dow
|
||||
FROM covered_dates
|
||||
)
|
||||
#{work_packages_periods_cte}
|
||||
|
||||
-- select id of work packages covering the given days
|
||||
SELECT id FROM covered_dates_and_wday
|
||||
WHERE dow IN (:days_of_week) OR date IN (:dates)
|
||||
SELECT id
|
||||
FROM work_packages_periods
|
||||
WHERE
|
||||
-- Check if the range covers any of the provided days of week. It is
|
||||
-- done by comparing number of days from start_date to first target
|
||||
-- day of week with number of days in range. If number is less or
|
||||
-- eqal, then the range covers the target day of week
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM UNNEST(ARRAY[:days_of_week]::INT[]) AS target_dow
|
||||
WHERE (target_dow + 7 - EXTRACT(ISODOW FROM start_date)::INT) % 7 <= (end_date - start_date)
|
||||
)
|
||||
OR
|
||||
-- Check if the range covers any of the provided dates
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM unnest(Array[:dates]::DATE[]) AS target_date
|
||||
WHERE target_date BETWEEN start_date AND end_date
|
||||
)
|
||||
SQL
|
||||
|
||||
covering_work_packages_query_sql = sanitize_sql([covering_work_packages_query_sql, { days_of_week:, dates: }])
|
||||
@@ -146,8 +144,8 @@ module WorkPackages::Scopes::CoveringDatesAndDaysOfWeek
|
||||
work_packages_periods AS (
|
||||
SELECT DISTINCT ON (succ_id)
|
||||
pred_id as id,
|
||||
pred_date as start_date,
|
||||
succ_date as end_date
|
||||
pred_date::DATE as start_date,
|
||||
succ_date::DATE as end_date
|
||||
FROM automatic_follows_relations
|
||||
ORDER BY succ_id, pred_date ASC
|
||||
)
|
||||
@@ -29,6 +29,8 @@ module BasicData
|
||||
class BaseRoleSeeder < ModelSeeder
|
||||
self.needs = []
|
||||
|
||||
self.attribute_names_for_lookups = %i[name]
|
||||
|
||||
def model_attributes(role_data)
|
||||
{
|
||||
type:,
|
||||
|
||||
@@ -33,6 +33,7 @@ module BasicData
|
||||
BasicData::ColorSeeder,
|
||||
BasicData::ColorSchemeSeeder
|
||||
]
|
||||
self.attribute_names_for_lookups = %i[name]
|
||||
|
||||
def model_attributes(type_data)
|
||||
{
|
||||
|
||||
@@ -26,40 +26,22 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
module BasicData
|
||||
class WorkflowSeeder < Seeder
|
||||
class WorkflowSeeder < ModelSeeder
|
||||
self.needs = [
|
||||
BasicData::ProjectRoleSeeder,
|
||||
BasicData::GlobalRoleSeeder,
|
||||
BasicData::StatusSeeder,
|
||||
BasicData::TypeSeeder
|
||||
]
|
||||
self.model_class = Workflow
|
||||
self.seed_data_model_key = "workflows"
|
||||
|
||||
def seed_data!
|
||||
if any_types_or_statuses_or_workflows_already_configured?
|
||||
print_status " *** Skipping types, statuses and workflows as there are already some configured"
|
||||
else
|
||||
seed_statuses
|
||||
seed_types
|
||||
seed_workflows
|
||||
end
|
||||
seed_workflows
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def any_types_or_statuses_or_workflows_already_configured?
|
||||
Type.where(is_standard: false).any? || Status.any? || Workflow.any?
|
||||
end
|
||||
|
||||
def seed_statuses
|
||||
print_status " ↳ Statuses"
|
||||
BasicData::StatusSeeder.new(seed_data).seed!
|
||||
end
|
||||
|
||||
def seed_types
|
||||
print_status " ↳ Types"
|
||||
BasicData::TypeSeeder.new(seed_data).seed!
|
||||
end
|
||||
|
||||
def seed_workflows
|
||||
member = seed_data.find_reference(:default_role_member)
|
||||
project_admin = seed_data.find_reference(:default_role_project_admin)
|
||||
|
||||
@@ -60,6 +60,11 @@ module DemoData
|
||||
]
|
||||
end
|
||||
|
||||
def applicable?
|
||||
seed_data.reference_exists?(:default_role_project_admin) &&
|
||||
types_seed_data.all? { |type_reference| seed_data.reference_exists?(type_reference) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_writer :project
|
||||
@@ -94,7 +99,7 @@ module DemoData
|
||||
def set_types
|
||||
print_status " -Assigning types."
|
||||
|
||||
project.types = seed_data.find_references(project_data.lookup("types"))
|
||||
project.types = seed_data.find_references(types_seed_data)
|
||||
end
|
||||
|
||||
def seed_categories
|
||||
@@ -142,6 +147,10 @@ module DemoData
|
||||
end
|
||||
end
|
||||
|
||||
def types_seed_data
|
||||
seed_data.lookup("types") || []
|
||||
end
|
||||
|
||||
def project_attributes
|
||||
parent = Project.find_by(identifier: project_data.lookup("parent"))
|
||||
{
|
||||
|
||||
@@ -34,8 +34,8 @@ module DemoData
|
||||
print_status " ↳ Updating settings"
|
||||
seed_settings
|
||||
|
||||
seed_data.each_data("projects") do |project_data|
|
||||
seed_project(project_data)
|
||||
project_seeders.each do |project_seeder|
|
||||
project_seeder.seed!
|
||||
Setting.demo_projects_available = true
|
||||
end
|
||||
|
||||
@@ -44,7 +44,7 @@ module DemoData
|
||||
end
|
||||
|
||||
def applicable?
|
||||
Project.count.zero?
|
||||
Project.count.zero? && project_seeders.all?(&:applicable?)
|
||||
end
|
||||
|
||||
def seed_settings
|
||||
@@ -55,11 +55,6 @@ module DemoData
|
||||
end
|
||||
end
|
||||
|
||||
def seed_project(project_data)
|
||||
project_seeder = ProjectSeeder.new(project_data)
|
||||
project_seeder.seed!
|
||||
end
|
||||
|
||||
def seed_form_configuration
|
||||
BasicData::TypeConfigurationSeeder.new(seed_data).seed!
|
||||
end
|
||||
@@ -74,5 +69,12 @@ module DemoData
|
||||
welcome_on_homescreen: 1
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project_seeders
|
||||
@project_seeders ||= seed_data.each_data("projects")
|
||||
.map { |project_data| ProjectSeeder.new(project_data) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,7 +47,9 @@ module DevelopmentData
|
||||
end
|
||||
|
||||
def applicable?
|
||||
recent_installation? && Project.where(identifier: project_identifiers).count == 0
|
||||
recent_installation? &&
|
||||
Project.where(identifier: project_identifiers).count == 0 &&
|
||||
seed_data.reference_exists?(:default_role_project_admin)
|
||||
end
|
||||
|
||||
# returns true if no projects have been created more than 1 hour ago,
|
||||
|
||||
@@ -37,6 +37,10 @@ class DevelopmentDataSeeder < CompositeSeeder
|
||||
]
|
||||
end
|
||||
|
||||
def applicable?
|
||||
DevelopmentData::CustomFieldsSeeder.new.applicable?
|
||||
end
|
||||
|
||||
def namespace
|
||||
"DevelopmentData"
|
||||
end
|
||||
|
||||
@@ -36,7 +36,7 @@ class Source::SeedData
|
||||
|
||||
def store_reference(reference, record)
|
||||
return if reference.nil?
|
||||
if registry.key?(reference)
|
||||
if reference_exists?(reference)
|
||||
raise ArgumentError, "an object with reference #{reference.inspect} is already registered"
|
||||
end
|
||||
|
||||
@@ -53,7 +53,7 @@ class Source::SeedData
|
||||
def find_reference(reference, *fallbacks, default: :__unset__)
|
||||
return if reference.nil?
|
||||
|
||||
existing_ref = [reference, *fallbacks].find { |ref| registry.key?(ref) }
|
||||
existing_ref = [reference, *fallbacks].find { |ref| reference_exists?(ref) }
|
||||
if existing_ref
|
||||
registry[existing_ref]
|
||||
elsif default != :__unset__
|
||||
@@ -72,6 +72,10 @@ class Source::SeedData
|
||||
Array(references).map { |reference| find_reference(reference, default:) }
|
||||
end
|
||||
|
||||
def reference_exists?(reference)
|
||||
registry.key?(reference)
|
||||
end
|
||||
|
||||
# Get a `SeedData` instance with only the given top level keys.
|
||||
#
|
||||
# Used in tests to get the real statuses, types and other data.
|
||||
@@ -110,7 +114,7 @@ class Source::SeedData
|
||||
|
||||
def each_data(path)
|
||||
sub_data = fetch(path)
|
||||
return if sub_data.nil?
|
||||
return to_enum(:each_data, path) unless block_given? || sub_data.nil?
|
||||
|
||||
sub_data.each_value do |item_data|
|
||||
yield self.class.new(item_data, registry)
|
||||
|
||||
@@ -39,6 +39,8 @@ module Standard
|
||||
::BasicData::ColorSchemeSeeder,
|
||||
::BasicData::ProjectPhaseColorSeeder,
|
||||
::BasicData::ProjectPhaseDefinitionSeeder,
|
||||
::BasicData::StatusSeeder,
|
||||
::BasicData::TypeSeeder,
|
||||
::BasicData::WorkflowSeeder,
|
||||
::BasicData::PrioritySeeder,
|
||||
::BasicData::SettingSeeder,
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ProjectLifeCycleSteps
|
||||
module ProjectPhases
|
||||
class ActivationService < ::BaseServices::BaseContracted
|
||||
alias_method :project, :model
|
||||
|
||||
@@ -88,7 +88,7 @@ module ProjectLifeCycleSteps
|
||||
end
|
||||
|
||||
def default_contract_class
|
||||
ProjectLifeCycleSteps::ActivationContract
|
||||
ProjectPhases::ActivationContract
|
||||
end
|
||||
end
|
||||
end
|
||||
+18
-20
@@ -28,24 +28,26 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ProjectLifeCycleSteps
|
||||
class UpdateService < ::BaseServices::Update
|
||||
delegate :project, to: :model
|
||||
module ProjectPhases
|
||||
class RescheduleService < ::BaseServices::BaseContracted
|
||||
alias_method :project, :model
|
||||
|
||||
def after_perform(*)
|
||||
reschedule_following_phases if model.date_range_set?
|
||||
|
||||
project.touch_and_save_journals
|
||||
|
||||
super
|
||||
def initialize(user:, project:, contract_class: nil, contract_options: {})
|
||||
super(user:, contract_class:, contract_options:)
|
||||
self.model = project
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reschedule_following_phases
|
||||
from = initial_reschedule_date
|
||||
def persist(service_call)
|
||||
reschedule_phases(**params)
|
||||
|
||||
following_phases.each do |phase|
|
||||
service_call
|
||||
end
|
||||
|
||||
def reschedule_phases(phases:, from:)
|
||||
phases.each do |phase|
|
||||
next unless phase.active?
|
||||
next unless phase.date_range_set?
|
||||
next unless phase.duration&.positive? # not updated using service that sets duration
|
||||
|
||||
@@ -58,14 +60,6 @@ module ProjectLifeCycleSteps
|
||||
end
|
||||
end
|
||||
|
||||
def initial_reschedule_date
|
||||
model.active? ? model.finish_date + 1 : model.start_date
|
||||
end
|
||||
|
||||
def following_phases
|
||||
project.available_phases.select { it.position > model.position }
|
||||
end
|
||||
|
||||
def calculate_date_range(from, duration:)
|
||||
days = working_days_from(from, count: duration)
|
||||
|
||||
@@ -80,5 +74,9 @@ module ProjectLifeCycleSteps
|
||||
end
|
||||
days
|
||||
end
|
||||
|
||||
def default_contract_class
|
||||
ProjectPhases::RescheduleContract
|
||||
end
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ProjectLifeCycleSteps
|
||||
module ProjectPhases
|
||||
class SetAttributesService < ::BaseServices::SetAttributes
|
||||
def perform(*)
|
||||
super.tap do
|
||||
@@ -0,0 +1,58 @@
|
||||
# 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 ProjectPhases
|
||||
class UpdateService < ::BaseServices::Update
|
||||
delegate :project, to: :model
|
||||
|
||||
def after_perform(*)
|
||||
reschedule_following_phases if model.date_range_set?
|
||||
|
||||
project.touch_and_save_journals
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reschedule_following_phases
|
||||
RescheduleService.new(user:, project:)
|
||||
.call(phases: following_phases, from: initial_reschedule_date)
|
||||
end
|
||||
|
||||
def initial_reschedule_date
|
||||
model.active? ? model.finish_date + 1 : model.start_date
|
||||
end
|
||||
|
||||
def following_phases
|
||||
project.available_phases.select { it.position > model.position }
|
||||
end
|
||||
end
|
||||
end
|
||||
+19
-21
@@ -28,29 +28,27 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
module My
|
||||
module TimeTracking
|
||||
class ViewModeSwitcherComponent < ApplicationComponent
|
||||
options :mode,
|
||||
:view_mode,
|
||||
:date
|
||||
|
||||
def call
|
||||
render(Primer::Alpha::TabNav.new(label: I18n.t(:label_view_mode_switcher))) do |component|
|
||||
component.with_tab(selected: view_mode == :calendar, href: link(:calendar)) do |tab|
|
||||
tab.with_text { I18n.t(:label_calendar) }
|
||||
tab.with_icon(icon: :calendar)
|
||||
end
|
||||
|
||||
component.with_tab(selected: view_mode == :list, href: link(:list)) do |tab|
|
||||
tab.with_text { I18n.t(:label_list) }
|
||||
tab.with_icon(icon: "op-view-list")
|
||||
end
|
||||
end
|
||||
module Projects
|
||||
module Copy
|
||||
class PhasesDependentService < Dependency
|
||||
def self.human_name
|
||||
I18n.t("label_life_cycle_step_plural")
|
||||
end
|
||||
|
||||
def link(new_view_mode)
|
||||
my_time_tracking_path(date: date, mode: mode, view_mode: new_view_mode)
|
||||
def source_count
|
||||
source.phases.count
|
||||
end
|
||||
|
||||
def copy_dependency(_params)
|
||||
source.phases.each do |source_phase|
|
||||
target.phases.create! **source_phase.attributes.slice(
|
||||
"definition_id",
|
||||
"start_date",
|
||||
"finish_date",
|
||||
"active",
|
||||
"duration"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -33,7 +33,7 @@ module Projects
|
||||
include Projects::Concerns::NewProjectService
|
||||
|
||||
def self.copy_dependencies
|
||||
[
|
||||
dependencies = [
|
||||
::Projects::Copy::MembersDependentService,
|
||||
::Projects::Copy::VersionsDependentService,
|
||||
::Projects::Copy::CategoriesDependentService,
|
||||
@@ -43,8 +43,15 @@ module Projects
|
||||
::Projects::Copy::QueriesDependentService,
|
||||
::Projects::Copy::BoardsDependentService,
|
||||
::Projects::Copy::OverviewDependentService,
|
||||
::Projects::Copy::PhasesDependentService,
|
||||
::Projects::Copy::StoragesDependentService
|
||||
]
|
||||
|
||||
if OpenProject::FeatureDecisions.stages_and_gates_active?
|
||||
dependencies
|
||||
else
|
||||
dependencies - [::Projects::Copy::PhasesDependentService]
|
||||
end
|
||||
end
|
||||
|
||||
# Project Folders and File Links aren't dependent services anymore,
|
||||
|
||||
@@ -58,11 +58,16 @@ class Settings::WorkingDaysAndHoursUpdateService < Settings::UpdateService
|
||||
|
||||
def after_perform(call)
|
||||
super.tap do
|
||||
WorkPackages::ApplyWorkingDaysChangeJob.perform_later(
|
||||
user_id: User.current.id,
|
||||
previous_working_days:,
|
||||
previous_non_working_days:
|
||||
)
|
||||
[
|
||||
Projects::Phases::ApplyWorkingDaysChangeJob,
|
||||
WorkPackages::ApplyWorkingDaysChangeJob
|
||||
].each do |job_class|
|
||||
job_class.perform_later(
|
||||
user_id: User.current.id,
|
||||
previous_working_days:,
|
||||
previous_non_working_days:
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -31,4 +31,14 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
<%= render(Admin::CustomFields::EditFormHeaderComponent.new(custom_field: @custom_field, selected: :items)) %>
|
||||
|
||||
<%= render(Admin::CustomFields::Hierarchy::ItemsComponent.new(item: @active_item)) %>
|
||||
<%=
|
||||
render(Primer::Alpha::Layout.new(stacking_breakpoint: :md)) do |content|
|
||||
content.with_main do
|
||||
render(Admin::CustomFields::Hierarchy::ItemsComponent.new(item: @active_item))
|
||||
end
|
||||
|
||||
content.with_sidebar(row_placement: :start, col_placement: :start, border: true, border_radius: 2, p: 3) do
|
||||
render(Admin::CustomFields::Hierarchy::TreeViewComponent.new(custom_field: @custom_field, active_item: @active_item))
|
||||
end
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
<% html_title t(:label_administration), t(:"attribute_help_texts.label_plural") %>
|
||||
|
||||
<% tabs = [
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user