Remove legacy frontend

pull/7385/head
Oliver Günther 6 years ago
parent 5167f4fca8
commit 8e32895ce3
  1. 1
      Procfile.dev
  2. 13
      app/helpers/angular_helper.rb
  3. 6
      app/views/account/register.html.erb
  4. 4
      app/views/attribute_help_texts/_tab.html.erb
  5. 4
      app/views/my/account.html.erb
  6. 12
      app/views/users/_mail_notifications.html.erb
  7. 6
      app/views/users/deletion_info.html.erb
  8. 5
      bin/setup_dev
  9. 66
      frontend/doc/API.md
  10. 39
      frontend/doc/LEGACY.md
  11. 10
      frontend/doc/README.md
  12. 4
      frontend/legacy/README.md
  13. 94
      frontend/legacy/app/openproject-legacy-app.ts
  14. 17
      frontend/legacy/postcss.config.js
  15. 39
      frontend/legacy/tsconfig.json
  16. 58
      frontend/legacy/typings/open-project-legacy.typings.d.ts
  17. 227
      frontend/legacy/webpack.config.js
  18. 6
      frontend/package.json
  19. 4
      frontend/src/app/components/wp-edit/wp-notification.service.ts
  20. 105
      frontend/src/app/modules/common/hide-section/show-section-dropdown.component.ts
  21. 2
      frontend/src/app/modules/common/openproject-common.module.ts
  22. 4
      frontend/src/app/modules/common/remote-field-updater/remote-field-updater.component.ts
  23. 4
      modules/costs/app/assets/stylesheets/costs/costs.sass
  24. 2
      modules/costs/app/controllers/cost_objects_controller.rb
  25. 2
      modules/costs/app/helpers/costlog_helper.rb
  26. 8
      modules/costs/app/models/variable_cost_object.rb
  27. 2
      modules/costs/app/views/cost_objects/_form.html.erb
  28. 2
      modules/costs/app/views/cost_objects/_list.html.erb
  29. 2
      modules/costs/app/views/cost_objects/items/_material_budget_item.html.erb
  30. 2
      modules/costs/app/views/cost_types/edit.html.erb
  31. 4
      modules/costs/app/views/costlog/edit.html.erb
  32. 2
      modules/costs/app/views/hourly_rates/edit.html.erb
  33. 4
      modules/costs/config/routes.rb
  34. 82
      modules/costs/frontend/legacy-app/components/budget/cost-budget-subform.directive.ts
  35. 14
      modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts
  36. 10
      modules/costs/frontend/module/augment/cost-subform.augment.service.ts
  37. 6
      modules/costs/frontend/module/augment/planned-costs-form.ts
  38. 7
      modules/costs/frontend/module/main.ts
  39. 2
      modules/my_project_page/app/views/my_projects_overviews/index.html.erb
  40. 5
      modules/my_project_page/app/views/my_projects_overviews/page_layout.html.erb
  41. 16
      modules/reporting_engine/lib/reporting_engine/patches/big_decimal_patch.rb
  42. 2
      modules/two_factor_authentication/app/views/two_factor_authentication/my/two_factor_devices/index.html.erb
  43. 2
      modules/two_factor_authentication/app/views/users/_two_factor_authentication_admin.html.erb
  44. 4
      package.json

@ -1,4 +1,3 @@
web: bundle exec rails server -p 3000 -b ${HOST:="127.0.0.1"} --environment ${RAILS_ENV:="development"}
legacy: cd frontend && RAILS_ENV=${RAILS_ENV:="development"} npm run legacy-webpack-watch
angular: npm run serve
worker: bundle exec rake jobs:work

@ -34,17 +34,4 @@ module AngularHelper
options[:class] = options.fetch(:class, '') + ' op-angular-component'
tag(component, options)
end
def activate_angular_js(type = :div, options = {}, &block)
content_for(:header_tags) do
javascript_include_tag 'bundles/openproject-legacy-app'
end
if block_given?
merged_options = options.merge('ng-app': 'OpenProjectLegacy')
content_tag(type, merged_options, &block)
else
'ng-app="OpenProjectLegacy"'.html_safe
end
end
end

@ -27,11 +27,9 @@ See docs/COPYRIGHT.rdoc for more details.
++#%>
<%= activate_angular_js do %>
<section data-augmented-model-wrapper
<section data-augmented-model-wrapper
data-modal-initialize-now="true"
data-modal-class-name="registration-modal modal-wrapper -highlight">
<% @user ||= User.new %>
<%= render partial: '/account/register' %>
</section>
<% end %>
</section>

@ -29,7 +29,7 @@ See docs/COPYRIGHT.rdoc for more details.
<% entries = @texts_by_type[tab[:name]] || [] %>
<% if entries.any? %>
<%= activate_angular_js :div, class: 'generic-table--container' do %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table class="generic-table">
<colgroup>
@ -91,7 +91,7 @@ See docs/COPYRIGHT.rdoc for more details.
</tbody>
</table>
</div>
<% end %>
</div>
<% else %>
<%= no_results_box %>
<% end %>

@ -33,8 +33,7 @@ See docs/COPYRIGHT.rdoc for more details.
<%= toolbar title: l(:label_profile) %>
<%= error_messages_for 'user' %>
<%= activate_angular_js do %>
<%= password_confirmation_form_for @user,
<%= password_confirmation_form_for @user,
as: :user,
url: { action: 'update_account' },
builder: ::TabularFormBuilder,
@ -66,5 +65,4 @@ See docs/COPYRIGHT.rdoc for more details.
</section>
<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-checkmark' %>
<% end %>
<% end %>

@ -36,22 +36,21 @@ See docs/COPYRIGHT.rdoc for more details.
<%= initialize_hide_sections_with [{key: 'notified_projects'}], active_sections %>
<%= activate_angular_js do %>
<div class="form--field">
<%= styled_label_tag "user_mail_notification", t(:'user.settings.mail_notifications') %>
<div class="form--field-container">
<div class="form--select-container">
<show-section-dropdown opt-value="selected" hide-sec-with-name="notified_projects">
<%= styled_select_tag 'user[mail_notification]',
options_for_select(user_mail_notification_options(@user),
@user.mail_notification),
options_for_select(user_mail_notification_options(@user), @user.mail_notification),
container_class: '-wide' %>
<show-section-dropdown opt-value="selected"
hide-sec-with-name="notified_projects">
</show-section-dropdown>
</div>
</div>
</div>
<hide-section section-name="notified_projects">
<section class="hide-section" data-section-name="notified_projects">
<div id="notified-projects" class="form--field -no-label">
<div class="form--field-container -vertical">
<% @user.projects.each do |project| %>
@ -68,7 +67,7 @@ See docs/COPYRIGHT.rdoc for more details.
<%= t(:'user.settings.mail_project_explanaition') %>
</div>
</div>
</hide-section>
</section>
<div class="form--field">
<%= styled_label_tag 'self_notified', t(:'user.settings.mail_self_notified') %>
@ -78,4 +77,3 @@ See docs/COPYRIGHT.rdoc for more details.
</div>
</div>
</div>
<% end %>

@ -28,12 +28,11 @@ See docs/COPYRIGHT.rdoc for more details.
++#%>
<% html_title(l(:label_administration), "#{ l('account.deletion_info.heading', name: @user.name)}") -%>
<%= activate_angular_js do %>
<%= labelled_tabular_form_for(
<%= labelled_tabular_form_for(
:user,
url: user_path(@user),
html: {
method: :delete, class: 'confirm_required form danger-zone',
method: :delete, class: 'confirm_required request-for-confirmation form danger-zone',
data: password_confirmation_data_attribute
}) do %>
<div class='wiki'>
@ -66,5 +65,4 @@ See docs/COPYRIGHT.rdoc for more details.
</div>
</section>
</div>
<% end %>
<% end %>

@ -22,15 +22,10 @@ try 'npm install --no-shrinkwrap >> log/setup_dev.log'
echo "Linking plugin modules"
try 'bundle exec rake openproject:plugins:register_frontend >> log/setup_dev.log'
printf "Building legacy webpack bundle ... "
try 'npm run legacy-webpack >> log/setup_dev.log'
echo "done."
echo "---------------------------------------"
echo "Done. Now start the following services"
echo '- Rails server `RAILS_ENV=development ./bin/rails s`'
echo '- Angular CLI: `npm run serve`'
echo '- (Optional, only if you work on frontend/legacy): `npm run legacy-webpack-watch`'
echo ""
echo 'You can also run `foreman start -f Procfile.dev` to run all the above on a single terminal.'

@ -1,66 +0,0 @@
API Handling
============
In general, the OpenProject Frontend uses _all_ of the existing working APIs to provide its functionality, as the current working version for `APIv3` is not feature complete.
The documentation for these APIs and their capabilities:
- [APIv3](http://opf.github.io/apiv3-doc/)
To get a feeling for which API is used at which point, please refer to the `PathHelper` located at `./frontend/app/helpers/path-helper.js`. It is used throughout the application to centralize knowledge about paths.
## HAL
While having a `PathHelper` certainly helps, the long-term idea is to leverage the [HAL](http://stateless.co/hal_specification.html)-capabilities of the APIv3 (thereby leaving `v2` behind) to let any client discover the paths available for a resource by inspecting the responses from any given call.
__Note:__ All responses from the APIv3 are thereby of `Content-Type: application/hal+json` and not just `Content-Type: application/json`. Some developer client tools sometimes get confused with that and will not interpret the formatting correctly.
Example:
```json
// calling a project
{
"_type": "Project",
"_links": {
"self": {
"href": "/api/v3/projects/1",
"title": "Lorem"
},
"createWorkPackage": {
"href": "/api/v3/projects/1/work_packages/form",
"method": "post"
},
"createWorkPackageImmediate": {
"href": "/api/v3/projects/1/work_packages",
"method": "post"
},
"categories": { "href": "/api/v3/projects/1/categories" },
"types": { "href": "/api/v3/projects/1/types" },
"versions": { "href": "/api/v3/projects/1/versions" }
},
"id": 1,
"identifier": "project_identifier",
"name": "Project example",
"description": "Lorem ipsum dolor sit amet"
}
```
The `Project` structure contains links to ressources associated. Given the knowledge about `_links`, one may easily infer the path from the response:
```javascript
// some magic to retrieve an object, note that the services used are examplary
// and can not be found in the actual codebase
ProjectsService.getProject('project_identifier').then(function(project) {
var pathToVersions = project._links.versions.href;
// the VersionsService has knowledge about pathToVersions in its
// forProject method
VersionsService.forProject(project).then(function(versions) {
// versions should be the result of the call to pathtoVersions
console.log(versions);
});
});
```
This is in principle a very good concept to delegate responsibility of inference to the client and absolves the client of having to know each path in the application in advance.

@ -1,39 +0,0 @@
# Additional information on Legacy frontend
The legacy bundle is only used from Rails to add functionality to specific parts of the application.
## Loading and bootstrapping the legacy AngularJS frontend
To bootstrap the AngularJS frontend from Rails, use the `activate_angular_js` helper block:
```html
<!-- @see ./app/helpers/angular_helper.rb -->
<%= activate_angular_js do %>
<persistent-toggle identifier="repository.checkout_instructions">
<div>
Something rendered from Rails ...
</div>
</persistent-toggle>
<% end %>
```
The legacy frontend with AngularJS can be bootstrapped _with_ content contained within. This is not possible in Angular,
since the root component needs to be empty (or will be emptied during bootstrap).
## Passing information and configuration from Rails to Angular
There are three ways of passing information from Rails to `AngularJS`:
1. Using tag attributes written directly to the DOM by the rendering process of Rails as in the example before.
2. Using the `gon` gem
This is included by all layouts in `<head>`:
```js
<%= nonced_javascript_tag do %>
<%= include_gon(need_tag: false) -%>
<% end %>
```
`gon` will provide arbitrary settings from Rails to all JavaScript functionality, including `AngularJS`. In an `angular` context a `ConfigurationService` is provided for picking up the settings.

@ -7,16 +7,6 @@ from the previous frontend that cannot be converted to Angular. (Mainly because
- **The Angular frontend** is located at `frontend/src` and uses the Angular CLI to compile and serve locally.
## Legacy frontend
When developing, the legacy bundle can be watched with `npm run legacy-webpack-watch` in separate tab.
It will result in a single output bundle at `app/assets/javascripts/bundles/openproject-legacy-app.js`.
That bundle is loaded manually by templates in Rails whenever a legacy directive is used with `<%= activate_angular_js %>`.
For production, this bundle is also produced when running the `rake assets:precompile` task.
For more information, see [LEGACY](./LEGACY.md).
## Angular frontend
When developing, `npm run serve` will open a proxy server (webpack-dev-server) that will serve assets from memory.

@ -1,4 +0,0 @@
# OpenProject Legacy AngularJS Frontend
These directives are remains of the AngularJS frontend used solely for the purpose of
retaining functionality of embedded components in Rails templates.

@ -1,94 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// 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 doc/COPYRIGHT.rdoc for more details.
//++
import {ExpressionService} from "../../common/expression.service";
require('angular');
var angularDragula:any = require('angular-dragula');
export const opTemplatesModule = angular.module('openproject.templates', []);
export const openprojectLegacyModule = angular.module('OpenProjectLegacy', [
angularDragula(angular)
]);
// Bootstrap app
openprojectLegacyModule
.config([
'$compileProvider',
'$httpProvider',
function($compileProvider:any, $httpProvider:any) {
// Disable debugInfo outside development mode
$compileProvider.debugInfoEnabled(window.OpenProject.environment === 'development');
$httpProvider.defaults.headers.common['X-CSRF-TOKEN'] = jQuery(
'meta[name=csrf-token]').attr('content');
$httpProvider.defaults.headers.common['X-Authentication-Scheme'] = 'Session';
// Add X-Requested-With for request.xhr?
$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// prepend a given base path to requests performed via $http
//
$httpProvider.interceptors.push(function($q:ng.IQService) {
return {
'request': function(config:any) {
// OpenProject can run in a subpath e.g. https://mydomain/open_project.
// We append the path found as the base-tag value to all http requests
// to the server except:
// * when the path is already appended
// * when we are getting a template
if (!config.url.match('(^/templates|\\.html$|^' + window.appBasePath + ')')) {
config.url = window.appBasePath + (config.url as string);
}
return config || $q.when(config);
}
};
});
}
])
.run([
'$rootScope',
function($rootScope:any) {
// Set the escaping target of opening double curly braces
// This is what returned by rails-angular-xss when it discoveres double open curly braces
// See https://github.com/opf/rails-angular-xss for more information.
$rootScope.DOUBLE_LEFT_CURLY_BRACE = ExpressionService.UNESCAPED_EXPRESSION;
// Mark the bootstrap has run for testing purposes.
document.body.classList.add('__ng-bootstrap-has-run');
$rootScope.$on('$stateChangeError',
function(event:JQueryEventObject){
event.preventDefault();
// transitionTo() promise will be rejected with
// a 'transition prevented' error
});
}
]);

@ -1,17 +0,0 @@
var fs = require('fs');
var path = require('path');
var _ = require('lodash');
var autoprefixer = require('autoprefixer');
var browsersListConfig = fs.readFileSync(path.join(__dirname, '..', 'browserslist'), 'utf8');
var browsersList = _.filter(browsersListConfig.split('\n'), function (entry) {
return entry && entry.charAt(0) !== '#';
});
module.exports = {
plugins: [
autoprefixer({
browsers: browsersList, cascade: false
})
]
}

@ -1,39 +0,0 @@
{
"compilerOptions": {
"target": "ES5",
"module": "es2015",
"moduleResolution": "node",
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"noEmitOnError": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// Increase strictness
// Enable strict once "use strict" errors can be resolved
// "strict": true;
"noImplicitAny": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"strictFunctionTypes": true,
// Causes lots of errors in linked angularjs properties
"strictPropertyInitialization": false,
"noFallthroughCasesInSwitch": true,
"strictNullChecks": true,
"skipLibCheck": true,
"preserveSymlinks": true,
"baseUrl": ".",
"typeRoots": [
"../node_modules/@types",
"./typings/open-project-legacy.typings.d.ts"
],
"paths": {
"core-app/*": ["./app/*"],
"core-components/*": ["./app/components/*"]
}
},
"compileOnSave": false,
"exclude": [
"node_modules"
]
}

@ -1,58 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// 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 doc/COPYRIGHT.rdoc for more details.
//++
/// <reference path="../../node_modules/@types/angular/index.d.ts" />
/// <reference path="../../node_modules/@types/lodash/index.d.ts" />
import * as TAngular from 'angular';
import * as TLodash from 'lodash';
import {InputState, State} from "reactivestates";
import {GlobalI18n} from "../../src/app/modules/common/i18n/i18n.service";
export interface IPluginContext {
classes:any;
services:any;
bootstrap(element:HTMLElement):void;
}
declare global {
interface Window {
appBasePath:string;
OpenProject:{
guardedLocalStorage(key:string, newValue?:string):string|void,
environment:string,
getPluginContext():Promise<IPluginContext>,
pluginContext:InputState<IPluginContext>
};
}
const angular:typeof TAngular;
const _:typeof TLodash;
const I18n:GlobalI18n;
}

@ -1,227 +0,0 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// 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 doc/COPYRIGHT.rdoc for more details.
// ++
var webpack = require('webpack');
var path = require('path');
var _ = require('lodash');
//var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var CleanWebpackPlugin = require('clean-webpack-plugin');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
var mode = 'production';
var production = true;
if (process.env['RAILS_ENV'] == 'development') {
mode = 'development';
production = false;
}
var debug_output = (!production || !!process.env['OP_FRONTEND_DEBUG_OUTPUT']);
var node_root = path.resolve(__dirname, '..', 'node_modules');
var output_root = path.resolve(__dirname, '..', '..', 'app', 'assets', 'javascripts');
var bundle_output = path.resolve(output_root, 'bundles');
var loaders = [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
logLevel: 'info',
configFile: path.resolve(__dirname, 'tsconfig.json')
}
}
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
{
test: /\.png$/,
use: [
{
loader: 'url-loader',
options: {
limit: '100000',
mimetype: 'image/png'
}
}
]
},
{
test: /\.gif$/,
use: ['file-loader']
},
{
test: /\.jpg$/,
use: ['file-loader']
},
];
loaders.push({
test: /\.html$/,
use: [
{
loader: 'ngtemplate-loader',
options: {
module: 'OpenProjectLegacy',
relativeTo: path.resolve(__dirname, './app')
}
},
{
loader: 'html-loader',
options: {
minimize: false
}
}
]
});
function getLegacyWebpackConfig() {
var config = {
mode: mode,
devtool: 'source-map',
context: path.resolve(__dirname, 'app'),
entry: {
'legacy-app': './openproject-legacy-app'
},
output: {
filename: 'openproject-[name].js',
path: bundle_output,
publicPath: '/assets/bundles/'
},
module: {
rules: loaders
},
resolve: {
// Don't map symlinks from dynamically linked plugins to their real paths
symlinks: false,
modules: [
path.resolve(__dirname, '..', 'node_modules')
],
extensions: ['.ts', '.tsx', '.js'],
// Allow empty import without extension
// enforceExtension: true,
alias: {
'angular': path.resolve(node_root, 'angular', 'angular.min.js'),
'angular-dragula': path.resolve(node_root, 'angular-dragula', 'dist', 'angular-dragula.min.js'),
'core-app': path.resolve(__dirname, 'app'),
'core-components': path.resolve(__dirname, 'app', 'components'),
}
},
externals: {
"I18n": "I18n",
"_": "_",
},
optimization: {
splitChunks: {
cacheGroups: {
common: {
name: "common",
chunks: "initial",
minChunks: 2
}
}
}
},
plugins: [
// Define modes for debug output
new webpack.DefinePlugin({
DEBUG: !!debug_output,
PRODUCTION: !!production
}),
// Clean the output directory
new CleanWebpackPlugin(['bundles'], {
root: output_root,
verbose: true,
exclude: ['openproject-vendors.js']
}),
// new BundleAnalyzerPlugin(),
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: "openproject-[name].css",
chunkFilename: "[id].css"
}),
]
};
if (production) {
console.log("Applying webpack.optimize plugins for production.");
// Add compression and optimization plugins
// to the webpack build.
config.plugins.push(
new webpack.LoaderOptionsPlugin({
// Let loaders know that we're in minification mode
minimize: true
})
);
config.optimization.minimizer = [
// we specify a custom UglifyJsPlugin here to get source maps in production
new UglifyJsPlugin({
cache: true,
parallel: true,
uglifyOptions: {
compress: true,
mangle: false,
ecma: 5,
},
sourceMap: true
})
];
}
return config;
}
module.exports = getLegacyWebpackConfig;

@ -134,14 +134,12 @@
},
"scripts": {
"prebuild": "./scripts/link_plugin_placeholder.js",
"build": "ng build --prod && npm run legacy-webpack",
"build": "ng build --prod",
"preserve": "./scripts/link_plugin_placeholder.js",
"serve": "node --max_old_space_size=8096 ./node_modules/@angular/cli/bin/ng serve --public-host http://localhost:4200",
"pretest": "./scripts/link_plugin_placeholder.js",
"test": "ng test --watch=false",
"tslint_typechecks": "./node_modules/.bin/tslint -p . -c tslint_typechecks.json",
"generate-typings": "tsc -d -p src/tsconfig.app.json",
"legacy-webpack": "./node_modules/.bin/webpack --colors --config legacy/webpack.config.js",
"legacy-webpack-watch": "RAILS_ENV=development ./node_modules/.bin/webpack --config legacy/webpack.config.js --display-error-details --watch --colors --cache --debug"
"generate-typings": "tsc -d -p src/tsconfig.app.json"
}
}

@ -115,7 +115,7 @@ export class WorkPackageNotificationService {
public retrieveError(response:unknown):ErrorResource|unknown {
// we try to detect what we got, this may either be an HttpErrorResponse,
// some older XHR response object or a string
let errorBody:any;
let errorBody:any = response;
// Angular http response have an error body attribute
if (response instanceof HttpErrorResponse) {
@ -131,7 +131,7 @@ export class WorkPackageNotificationService {
return this.halResourceService.createHalResourceOfClass(ErrorResource, errorBody);
}
return response;
return errorBody;
}
protected handleErrorResponse(errorResource:any, workPackage?:WorkPackageResource) {

@ -1,61 +1,56 @@
// //-- copyright
// // OpenProject is a project management system.
// // Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
// //
// // 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-2017 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 docs/COPYRIGHT.rdoc for more details.
// //++
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// import {HideSectionService} from "./hide-section.service";
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// export class ShowSectionDropdownComponent {
// public optValue:string; // value of option for which hide-section should be visible
// public hideSecWithName:string; // section-name of hide-section
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// constructor(protected HideSectionService:HideSectionService,
// private $element:ng.IAugmentedJQuery) {
// }
// 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.
//
// $onInit() {
// this.$element.change(event => {
// let selectedOption = jQuery("option:selected", event.target);
// 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.
//
// if (selectedOption.val() !== this.optValue) {
// this.HideSectionService.hide(this.hideSecWithName);
// }
// else {
// this.HideSectionService.show({key: this.hideSecWithName, label: ""});
// }
// });
// }
// }
// 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.
//
// openprojectLegacyModule.component('showSectionDropdown', {
// template: '<ng-transclude></ng-transclude>',
// transclude: true,
// controller: ShowSectionDropdownComponent,
// bindings: {
// optValue: "@",
// hideSecWithName: "@"
// }
// });
// See docs/COPYRIGHT.rdoc for more details.
//++
import {HideSectionService} from "./hide-section.service";
import {Component, ElementRef, OnInit} from "@angular/core";
@Component({
selector: 'show-section-dropdown',
template: ''
})
export class ShowSectionDropdownComponent implements OnInit {
public optValue:string; // value of option for which hide-section should be visible
public hideSecWithName:string; // section-name of hide-section
constructor(private HideSectionService:HideSectionService,
private elementRef:ElementRef) {
}
ngOnInit() {
const target = jQuery(this.elementRef.nativeElement).prev();
target.on('change', event => {
let selectedOption = jQuery("option:selected", event.target);
if (selectedOption.val() !== this.optValue) {
this.HideSectionService.hide(this.hideSecWithName);
} else {
this.HideSectionService.show(this.hideSecWithName);
}
});
}
}

@ -92,6 +92,7 @@ import {HideSectionLinkComponent} from "core-app/modules/common/hide-section/hid
import {HideSectionService} from "core-app/modules/common/hide-section/hide-section.service";
import {RemoteFieldUpdaterComponent} from 'core-app/modules/common/remote-field-updater/remote-field-updater.component';
import {AutofocusDirective} from "core-app/modules/common/autofocus/autofocus.directive";
import {ShowSectionDropdownComponent} from "core-app/modules/common/hide-section/show-section-dropdown.component";
export function bootstrapModule(injector:Injector) {
return () => {
@ -242,6 +243,7 @@ export function bootstrapModule(injector:Injector) {
PersistentToggleComponent,
AutocompleteSelectDecorationComponent,
HideSectionLinkComponent,
ShowSectionDropdownComponent,
AddSectionDropdownComponent,
RemoteFieldUpdaterComponent,

@ -64,7 +64,7 @@ export class RemoteFieldUpdaterComponent implements OnInit {
// special cases where the tab code is not correctly recognized (undefined).
// Thus the focus is kept on the first element of the result list.
let keyCodesArray = [keyCodes.TAB, keyCodes.ENTER, keyCodes.SHIFT];
if (event.which && keyCodesArray.indexOf(event.which) === -1) {
if (event.type === 'change' || (event.which && keyCodesArray.indexOf(event.which) === -1)) {
this.updater();
}
}, 500));
@ -106,7 +106,7 @@ export class RemoteFieldUpdaterComponent implements OnInit {
// Replace the given target
this.target.html(response);
} else {
_.each(response.data, (val:string, selector:string) => {
_.each(response, (val:string, selector:string) => {
jQuery('#' + selector).html(val);
});
}

@ -31,3 +31,7 @@ table.list.members
.progress-bar .inner-progress.done
background-color: #E1B9B9
.budget-row-template,
.subform-row-template
display: none

@ -187,7 +187,7 @@ class CostObjectsController < ApplicationController
cost_type = CostType.where(id: params[:cost_type_id]).first
if cost_type && params[:units].present?
volume = BigDecimal.new(Rate.clean_currency(params[:units])) rescue 0.0
volume = BigDecimal(Rate.clean_currency(params[:units])) rescue 0.0
@costs = (volume * cost_type.rate_at(params[:fixed_date]).rate rescue 0.0)
@unit = volume == 1.0 ? cost_type.unit : cost_type.unit_plural
else

@ -49,7 +49,7 @@ module CostlogHelper
value = value.strip
value.gsub!(t(:currency_delimiter), '') if value.include?(t(:currency_delimiter)) && value.include?(t(:currency_separator))
value.gsub(',', '.')
BigDecimal.new(value)
BigDecimal(value)
end
def to_currency_with_empty(rate)

@ -52,11 +52,11 @@ class VariableCostObject < CostObject
end
def material_budget
@material_budget ||= material_budget_items.visible_costs.inject(BigDecimal.new('0.0000')) { |sum, i| sum += i.costs }
@material_budget ||= material_budget_items.visible_costs.inject(BigDecimal('0.0000')) { |sum, i| sum += i.costs }
end
def labor_budget
@labor_budget ||= labor_budget_items.visible_costs.inject(BigDecimal.new('0.0000')) { |sum, i| sum += i.costs }
@labor_budget ||= labor_budget_items.visible_costs.inject(BigDecimal('0.0000')) { |sum, i| sum += i.costs }
end
def spent
@ -66,7 +66,7 @@ class VariableCostObject < CostObject
def spent_material
@spent_material ||= begin
if cost_entries.blank?
BigDecimal.new('0.0000')
BigDecimal('0.0000')
else
cost_entries.visible_costs(User.current, project).sum("CASE
WHEN #{CostEntry.table_name}.overridden_costs IS NULL THEN
@ -80,7 +80,7 @@ class VariableCostObject < CostObject
def spent_labor
@spent_labor ||= begin
if time_entries.blank?
BigDecimal.new('0.0000')
BigDecimal('0.0000')
else
time_entries.visible_costs(User.current, project).sum("CASE
WHEN #{TimeEntry.table_name}.overridden_costs IS NULL THEN

@ -36,10 +36,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
</div>
<% if @cost_object.kind == "VariableCostObject" -%>
<%= activate_angular_js do %>
<%= render partial: 'cost_objects/subform/material_budget_subform' %>
<%= render partial: 'cost_objects/subform/labor_budget_subform' %>
<% end %>
<%- end %>
<div style="clear: both;"> </div>

@ -74,7 +74,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
</tr>
</thead>
<tbody>
<% total_budget = BigDecimal.new("0"); labor_budget = BigDecimal.new("0"); material_budget = BigDecimal.new("0"); spent = BigDecimal.new("0") %>
<% total_budget = BigDecimal("0"); labor_budget = BigDecimal("0"); material_budget = BigDecimal("0"); spent = BigDecimal("0") %>
<% cost_objects.each do |cost_object| %>
<tr id="cost_object-<%= cost_object.id %>" class="<%= cost_object.css_classes %>">
<td><%= link_to cost_object.id, cost_object_path(cost_object.id) %></td>

@ -75,7 +75,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
<%= cost_form.hidden_field :budget, value: material_budget_item.budget %>
<% end %>
<cost-unit-subform obj-id="<%= obj_id %>" obj-name="<%= "#{name_prefix}[budget]" %>">
<a id="<%= obj_id %>" class="costs--edit-planned-costs-btn" role="button" class="icon-context icon-edit" title="<%= t(:help_click_to_edit) %>">
<a id="<%= obj_id %>" class="costs--edit-planned-costs-btn icon-context icon-edit" role="button" title="<%= t(:help_click_to_edit) %>">
<%= number_to_currency(material_budget_item.budget || material_budget_item.calculated_costs(@cost_object.fixed_date)) %>
</a>
</cost-unit-subform>

@ -31,7 +31,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
<%= toolbar title: CostType.model_name.human %>
<%= activate_angular_js do %>
<costs-subform item-count="<%= @cost_type.rates.length %>">
<%= labelled_tabular_form_for @cost_type do |f| %>
<%= error_messages_for 'cost_type' %>
@ -112,4 +111,3 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
</div>
<% end %>
</costs-subform>
<% end %>

@ -89,7 +89,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
<% if @cost_entry.cost_type.nil? %>
<%= f.text_field :units, size: 6, required: true, container_class: '-slim' %>
<% else %>
<% suffix = @cost_enngtry.units == 1 ? @cost_entry.cost_type.unit : @cost_entry.cost_type.unit_plural %>
<% suffix = @cost_entry.units == 1 ? @cost_entry.cost_type.unit : @cost_entry.cost_type.unit_plural %>
<%= f.text_field :units,
size: 6,
required: true,
@ -116,7 +116,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
<br /><em><%= t(:help_override_rate) %></em>
<% end %>
</cost-unit-subform>
</span>ng
</span>
</div>
<div class="form--field">

@ -29,7 +29,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
<p><strong><%= t(:label_current_default_rate) %>:</strong> <%= number_to_currency(default_rate.rate)%></p>
<% end %>
<%= activate_angular_js do %>
<costs-subform item-count="<%= @rates.count %>">
<%= labelled_tabular_form_for @user, url: {action: 'update', project_id: @project}, method: :put do |f| %>
<%= back_url_hidden_field_tag %>
@ -90,4 +89,3 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
</div>
<% end %>
</costs-subform>
<% end %>

@ -22,8 +22,8 @@ OpenProject::Application.routes.draw do
resources :cost_entries, controller: 'costlog', only: [:new, :create]
resources :cost_objects, only: [:new, :create, :index] do
get :update_labor_budget_item, on: :collection
get :update_material_budget_item, on: :collection
match :update_labor_budget_item, on: :collection, via: %i[get post]
match :update_material_budget_item, on: :collection, via: %i[get post]
end
resources :hourly_rates, only: [:show, :edit, :update] do

@ -1,82 +0,0 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// 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 doc/COPYRIGHT.rdoc for more details.
// ++
import {PluginContextService} from "core-app/services/plugin-context.service";
/*eslint no-eval: "error"*/
export class CostBudgetSubformController {
// Container for rows
private container: ng.IAugmentedJQuery;
// Template for new rows to insert, is rendered with INDEX placeholder
private rowTemplate: string;
// Current row index
public rowIndex: number;
// subform item count as output by rails
public itemCount: string;
// Updater URL for the rows contained here
public updateUrl: string;
constructor(public $element:ng.IAugmentedJQuery,
public $http:ng.IHttpService,
public pluginContext:PluginContextService,
private $scope:ng.IScope,
private $compile:any) {
}
}
function costsBudgetSubform():any {
return {
restrict: 'E',
scope: {
updateUrl: '@',
itemCount: '@'
},
link: (scope:ng.IScope,
element:ng.IAugmentedJQuery,
attr:ng.IAttributes,
ctrl:any) => {
const template = element.find('.budget-row-template');
ctrl.rowTemplate = template[0].outerHTML;
template.remove();
},
bindToController: true,
controller: CostBudgetSubformController,
controllerAs: '$ctrl'
};
}
angular.module('OpenProjectLegacy').directive('costsBudgetSubform', costsBudgetSubform);

@ -31,15 +31,20 @@ import {HttpClient} from '@angular/common/http';
import {WorkPackageNotificationService} from "core-app/components/wp-edit/wp-notification.service";
@Injectable()
export class CostSubformAugmentService {
export class CostBudgetSubformAugmentService {
constructor(private wpNotifications:WorkPackageNotificationService,
private http:HttpClient) {
}
listen() {
jQuery('costs-budget-subform').each((i, match) => {
let el = jQuery(match);
const container = el.find('.budget-item-container');
const template:string = el.find('.budget-row-template')[0].outerHTML;
const templateEl = el.find('.budget-row-template');
templateEl.detach();
const template = templateEl[0].outerHTML;
let rowIndex = parseInt(el.attr('item-count') as string);
// Refresh row on changes
@ -57,7 +62,10 @@ export class CostSubformAugmentService {
// Add new row handler
el.find('.budget-add-row').click((evt) => {
evt.preventDefault();
container.append(template.replace(/INDEX/g, rowIndex.toString()));
let row = jQuery(template.replace(/INDEX/g, rowIndex.toString()));
row.show();
row.removeClass('budget-row-template');
container.append(row);
rowIndex += 1;
return false;
});

@ -36,7 +36,10 @@ export class CostSubformAugmentService {
let el = jQuery(match);
const container = el.find('.subform-container');
const template = el.find('.subform-row-template')[0].outerHTML;
const templateEl = el.find('.subform-row-template');
templateEl.detach();
const template = templateEl[0].outerHTML;
let rowIndex = parseInt(el.attr('item-count')!);
el.on('click', '.delete-row-button,.delete-budget-item', (evt:any) => {
@ -47,7 +50,10 @@ export class CostSubformAugmentService {
// Add new row handler
el.find('.add-row-button,.wp-inline-create--add-link').click((evt:any) => {
evt.preventDefault();
container.append(template.replace(/INDEX/g, rowIndex.toString()));
let row = jQuery(template.replace(/INDEX/g, rowIndex.toString()));
row.show();
row.removeClass('subform-row-template');
container.append(row);
rowIndex += 1;
container.find('.costs-date-picker').datepicker();

@ -34,7 +34,7 @@ export class PlannedCostsFormAugment {
static listen() {
jQuery(document).on('click', '.costs--edit-planned-costs-btn', (evt) => {
const form = jQuery(evt.target).closest('cost-unit-subform');
const form = jQuery(evt.target as any).closest('cost-unit-subform') as JQuery;
new PlannedCostsFormAugment(form);
});
}
@ -42,7 +42,7 @@ export class PlannedCostsFormAugment {
constructor(public $element:JQuery) {
this.objId = this.$element.attr('obj-id')!;
this.objName = this.$element.attr('obj-name')!;
this.obj = jQuery(this.objId);
this.obj = jQuery(`#${this.objId}`) as any;
this.makeEditable('#' + this.objId, this.objName);
}
@ -77,7 +77,7 @@ export class PlannedCostsFormAugment {
<section class="form--section" id="${id}_section">
<div class="form--field">
<div class="form--field-container">
<div id="${id}_cancel" class="form--field-affix -transparent icon icon-close"></div>';
<div id="${id}_cancel" class="form--field-affix -transparent icon icon-close"></div>
<div id="${id}_editor" class="form--text-field-container">
<input id="${id}_edit" class="form--text-field" name="${name}" value="${value}" class="currency" type="text" />
</div>

@ -32,8 +32,9 @@ import {BudgetResource} from './hal/resources/budget-resource';
import {multiInput} from 'reactivestates';
import {CostSubformAugmentService} from "./augment/cost-subform.augment.service";
import {PlannedCostsFormAugment} from "core-app/modules/plugins/linked/openproject-costs/augment/planned-costs-form";
import {CostBudgetSubformAugmentService} from "core-app/modules/plugins/linked/openproject-costs/augment/cost-budget-subform.augment.service";
export function initializeCostsPlugin() {
export function initializeCostsPlugin(injector:Injector) {
return () => {
window.OpenProject.getPluginContext().then((pluginContext:OpenProjectPluginContext) => {
pluginContext.services.editField.extendFieldType('select', ['Budget']);
@ -78,6 +79,9 @@ export function initializeCostsPlugin() {
// Augment previous cost-subforms
new CostSubformAugmentService();
PlannedCostsFormAugment.listen();
const budgetSubform = injector.get(CostBudgetSubformAugmentService);
budgetSubform.listen();
});
};
}
@ -86,6 +90,7 @@ export function initializeCostsPlugin() {
@NgModule({
providers: [
{ provide: APP_INITIALIZER, useFactory: initializeCostsPlugin, deps: [Injector], multi: true },
CostBudgetSubformAugmentService,
],
})
export class PluginModule {

@ -37,7 +37,6 @@ See doc/COPYRIGHT.md for more details.
<% end %>
<% end %>
<%= activate_angular_js do %>
<div id="invisible-grid" class="widget-boxes project-overview">
<% top_fields.each do |f| %>
<%= rendered_field f %>
@ -48,4 +47,3 @@ See doc/COPYRIGHT.md for more details.
<% end %>
</div>
</div>
<% end %>

@ -73,8 +73,5 @@ See doc/COPYRIGHT.md for more details.
</div>
</div>
<%= activate_angular_js do %>
<overview-page-layout>
</overview-page-layout>
<% end %>
<overview-page-layout></overview-page-layout>

@ -18,23 +18,21 @@
#++
module ReportingEngine::Patches::BigDecimalPatch
module BigDecimal
::BigDecimal.send :include, self
class BigDecimal
def to_d; self end
end
module Integer
::Integer.send :include, self
class Integer
def to_d; to_f.to_d end
end
module String
::String.send :include, self
def to_d; ::BigDecimal.new(self) end
class String
def to_d
BigDecimal self
end
end
module NilClass
::NilClass.send :include, self
class NilClass
def to_d; 0 end
end
end

@ -26,7 +26,6 @@
</section>
<% end %>
<%= activate_angular_js do %>
<% breadcrumb_paths(t(:label_my_account), t('two_factor_authentication.label_two_factor_authentication')) -%>
<%= toolbar title: t('two_factor_authentication.label_two_factor_authentication') do %>
<li class="toolbar-item">
@ -65,4 +64,3 @@
class: 'button'
end %>
</section>
<% end %>

@ -31,7 +31,6 @@
<% end %>
</section>
<%= activate_angular_js do %>
<section class="admin--edit-section">
<%= toolbar title: t('two_factor_authentication.label_devices') do %>
<% unless devices.empty? %>
@ -58,4 +57,3 @@
<%= cell ::TwoFactorAuthentication::Devices::TableCell, devices, admin_table: true || @user != User.current %>
</div>
</section>
<% end %>

@ -6,9 +6,7 @@
"test": "cd frontend && npm test && cd ..",
"tslint_typechecks": "cd frontend && npm run tslint_typechecks && cd ..",
"serve": "cd frontend && npm run serve",
"serve-public": "cd frontend && ./node_modules/.bin/ng serve --host 0.0.0.0",
"legacy-webpack": "cd frontend && npm run legacy-webpack && cd ..",
"legacy-webpack-watch": "cd frontend && npm run legacy-webpack-watch"
"serve-public": "cd frontend && ./node_modules/.bin/ng serve --host 0.0.0.0"
},
"private": true,
"engines": {

Loading…
Cancel
Save