Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototyping React #4

Draft
wants to merge 55 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c6a82c9
initial commit
Apr 14, 2020
8ff5744
bundle to js/mantis
Apr 14, 2020
1544044
added mantis.* to gitignore
Apr 14, 2020
cf76334
js minify script
Apr 14, 2020
b18c0e5
updated package json and hard coded params
Apr 15, 2020
ab9baa7
removed pack script
Apr 15, 2020
4a0391f
IssueService class
Apr 16, 2020
6813e22
Issue models
Apr 16, 2020
1ad7d55
Relationship model
Apr 17, 2020
3cccadf
Implement LocalizedStringsGetCommand
vboctor Apr 17, 2020
7fd1c14
removed console log
Apr 17, 2020
7c08e46
removed unneeded functions and models
Apr 17, 2020
226f998
Implemented ConfigsGetCommand
vboctor Apr 17, 2020
d917ee2
ConfigService
Apr 17, 2020
27d6c80
Include mantis.js in all pages
vboctor Apr 17, 2020
6a139ab
Remove RelationshipsParse() dead code
vboctor Apr 17, 2020
51d2ee4
Remove IssueRelationshipAddRequest
vboctor Apr 17, 2020
ddd64df
Remove ConfigService and use PHP commands instead
vboctor Apr 17, 2020
7b3e632
Emit array of relationship box buttons to HTML
vboctor Apr 17, 2020
27c692b
hard-coded strings to localized strings
Apr 17, 2020
d071180
config to react
Apr 17, 2020
f9cf8bb
relationship buttons
Apr 17, 2020
0eb3cfb
react check resolve bug
Apr 17, 2020
a34f555
removed relationship_get_summary_html
Apr 17, 2020
4e94a09
Remove dead code
vboctor Apr 17, 2020
f9dcac5
fixed wrong relationships box render
Apr 18, 2020
a5445fc
removed default_bug_dependant config
Apr 18, 2020
26d0385
delete relationship confirm dialog
Apr 18, 2020
98b2921
IssueViewPageCommand: support block resolving
vboctor Apr 18, 2020
aa8ae46
Fix escaping of json in HTML
vboctor Apr 18, 2020
883968a
Support auto-complete for related issue ids
vboctor Apr 18, 2020
1abc625
Add internal API to get issue basic info
vboctor Apr 19, 2020
0779139
input field enter key down handler
Apr 19, 2020
f50b583
webpack watch and clean script
Apr 19, 2020
3d23438
Fix setting of relationship warning
vboctor Apr 19, 2020
373e4d9
Expose IssueViewPageCommand via REST API
vboctor Apr 19, 2020
43065ab
Proper handling for relationships warning message
vboctor Apr 19, 2020
471362a
Add `IssueService.GetIssueBasic()`
vboctor Apr 19, 2020
1e109ca
webpack watch
Apr 20, 2020
a28ee88
toast message
Apr 20, 2020
e87e6c3
Merge branches 'react-relationships' and 'react-relationships' of git…
Apr 20, 2020
bc2e367
removed console log
Apr 20, 2020
f14b758
we don't need client/js folder. client/js -> client/
Apr 21, 2020
36304bf
added node_modules to gitignore
Apr 21, 2020
ba6d2c0
relationships input auto-complete
Apr 23, 2020
7eaf632
except issueId itself
Apr 23, 2020
c77a641
Handle arrow key control
Apr 23, 2020
eb20f60
Arrow key input prevents cursor change
Apr 23, 2020
9829146
dropdown mouse over
Apr 23, 2020
44c3803
working with min.js
Apr 24, 2020
a9c9f52
added watching minify script
Apr 24, 2020
1fbf45c
React plugin storybook component
Apr 24, 2020
586ed5b
code splitting
Apr 24, 2020
166f9cd
removed ReactSamplePlugin from Mantisbt repo
Apr 25, 2020
fa71dfd
keyboard event prevent
May 4, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,13 @@ custom_functions_inc.php
custom_relationships_inc.php
custom_strings_inc.php
api/soap/mc_config_inc.php


# React
client/js/node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Bundled files
js/mantis.*
105 changes: 10 additions & 95 deletions api/rest/restcore/config_rest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,117 +30,32 @@
/**
* A method that does the work to handle getting a set of configs via REST API.
*
* The following query string parameters are supported:
* - option/option[]: can be a string or an array
* - project_id: an optional parameter for project id to get configs for. Default ALL_PROJECTS (0).
* - user_id: an optional parameter for user id to get configs for. Default current user.
* can be 0 for ALL_USERS.
*
* The response will include a config key that is an array of requested configs. Configs
* that are not public or are not defined will be filtered out, and request will still succeed.
* This is to make it easier to request configs that maybe defined in some versions of MantisBT
* but not others.
*
* Note that only users with ADMINISTRATOR access can fetch configuration for other users.
*
* @param \Slim\Http\Request $p_request The request.
* @param \Slim\Http\Response $p_response The response.
* @param array $p_args Arguments
* @return \Slim\Http\Response The augmented response.
*/
function rest_config_get( \Slim\Http\Request $p_request, \Slim\Http\Response $p_response, array $p_args ) {
$t_query = array();
$t_options = $p_request->getParam( 'option' );
if( !is_array( $t_options ) ) {
$t_options = array( $t_options );
if( !is_null( $t_options ) ) {
$t_query['option'] = $t_options;
}

$t_project_id = $p_request->getParam( 'project_id' );
if( is_null( $t_project_id ) ) {
$t_project_id = ALL_PROJECTS;
}

if( $t_project_id != ALL_PROJECTS && !project_exists( $t_project_id ) ) {
return $p_response->withStatus( HTTP_STATUS_NOT_FOUND, "Project with id '$t_project_id' not found" );
if( !is_null( $t_project_id ) ) {
$t_query['project_id'] = $t_project_id;
}

$t_user_id = $p_request->getParam( 'user_id' );
if( is_null( $t_user_id ) ) {
$t_user_id = auth_get_current_user_id();
} else {
if( $t_user_id != ALL_USERS &&
$t_user_id != auth_get_current_user_id() &&
!current_user_is_administrator() ) {
return $p_response->withStatus( HTTP_STATUS_FORBIDDEN, 'Admin access required to get configs for other users' );
}

if( $t_user_id != ALL_USERS && user_exists( $t_user_id ) ) {
return $p_response->withStatus( HTTP_STATUS_NOT_FOUND, "User with id '$t_user_id' not found" );
}
if( !is_null( $t_user_id ) ) {
$t_query['user_id'] = $t_user_id;
}

$t_configs = array();
foreach( $t_options as $t_option ) {
# Filter out undefined configs rather than error, they may be valid in some MantisBT versions but not
# others.
if( !config_is_set( $t_option ) ) {
continue;
}

# Filter out private configs, since they can be private in some configs but public in others.
if( config_is_private( $t_option ) ) {
continue;
}

$t_value = config_get( $t_option, /* default */ null, $t_user_id, $t_project_id );
if( config_is_enum( $t_option ) ) {
$t_value = config_get_enum_as_array( $t_option, $t_value );
}

$t_config_pair = array(
"option" => $t_option,
"value" => $t_value
);

$t_configs[] = $t_config_pair;
}

# wrap all configs into a configs attribute to allow adding other information if needed in the future
# that belongs outside the configs response.
$t_result = array( 'configs' => $t_configs );
$t_data = array( 'query' => $t_query );
$t_command = new ConfigsGetCommand( $t_data );
$t_result = $t_command->execute( $t_data );

return $p_response->withStatus( HTTP_STATUS_SUCCESS )->withJson( $t_result );
}

/**
* Checks if the specific config option is an enum.
*
* @param string $p_option The option name.
* @return bool true enum, false otherwise.
*/
function config_is_enum( $p_option ) {
return stripos( $p_option, '_enum_string' ) !== false;
}

/**
* Gets the enum config option as an array with the id as the key and the value
* as an array with name and label (localized name) keys.
*
* @param string $p_enum_name The enum config option name.
* @param string $p_enum_string_value The enum config option value.
* @return array The enum array.
*/
function config_get_enum_as_array( $p_enum_name, $p_enum_string_value ) {
$t_enum_assoc_array = MantisEnum::getAssocArrayIndexedByValues( $p_enum_string_value );
$t_localized_enum_string = lang_get( $p_enum_name );

$t_enum_array = array();

foreach( $t_enum_assoc_array as $t_id => $t_name ) {
$t_label = MantisEnum::getLocalizedLabel( $p_enum_string_value, $t_localized_enum_string, $t_id );
$t_enum_entry = array( 'id' => $t_id, 'name' => $t_name, 'label' => $t_label );
$t_enum_array[] = $t_enum_entry;
}

return $t_enum_array;
}

19 changes: 5 additions & 14 deletions api/rest/restcore/lang_rest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,13 @@
*/
function rest_lang_get( \Slim\Http\Request $p_request, \Slim\Http\Response $p_response, array $p_args ) {
$t_strings = $p_request->getParam( 'string' );
if( !is_array( $t_strings ) ) {
$t_strings = array( $t_strings );
}

$t_current_language = lang_get_current();
$t_localized_strings = array();
foreach( $t_strings as $t_string ) {
if( !lang_exists( $t_string, $t_current_language) ) {
continue;
}
$t_data = array(
'query' => array( 'string' => $t_strings )
);

$t_localized_strings[] = array( 'name' => $t_string, 'localized' => lang_get( $t_string ) );
}

$t_result = array( 'strings' => $t_localized_strings );
$t_result['language'] = $t_current_language;
$t_command = new LocalizedStringsGetCommand( $t_data );
$t_result = $t_command->execute();

return $p_response->withStatus( HTTP_STATUS_SUCCESS )->withJson( $t_result );
}
29 changes: 25 additions & 4 deletions bug_view_inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,23 @@
);

$t_cmd = new IssueViewPageCommand( $t_data );
$t_result = $t_cmd->execute();
$t_view_command_result = $t_cmd->execute();

$t_issue = $t_result['issue'];
$t_issue_view = $t_result['issue_view'];
$t_flags = $t_result['flags'];
$t_issue = $t_view_command_result['issue'];
$t_issue_view = $t_view_command_result['issue_view'];
$t_flags = $t_view_command_result['flags'];

# Get localized strings
$t_strings = array( 'relation_graph', 'dependency_graph' );
$t_data = array( 'query' => array( 'string' => $t_strings ) );
$t_cmd = new LocalizedStringsGetCommand( $t_data );
$t_strings_command_results = $t_cmd->execute();

# Get config options
$t_configs = array( 'relationship_graph_enable', 'relationship_graph_view_on_click' );
$t_data = array( 'query' => array( 'option' => $t_configs ) );
$t_cmd = new ConfigsGetCommand( $t_data );
$t_configs_command_results = $t_cmd->execute();

compress_enable();

Expand All @@ -109,6 +121,14 @@
$t_top_buttons_enabled = !$t_force_readonly && ( $t_action_button_position == POSITION_TOP || $t_action_button_position == POSITION_BOTH );
$t_bottom_buttons_enabled = !$t_force_readonly && ( $t_action_button_position == POSITION_BOTTOM || $t_action_button_position == POSITION_BOTH );

#
# Emit issue information to hidden div
#

echo '<div class="hidden" id="issue-data" data-issue=' . "'" . json_encode( $t_view_command_result ) . "'" . '></div>';
echo '<div class="hidden" id="strings-data" data-strings=' . "'" . json_encode( $t_strings_command_results ) . "'" . '></div>';
echo '<div class="hidden" id="configs-data" data-configs=' . "'" . json_encode( $t_configs_command_results ) . "'" . '></div>';

#
# Start of Template
#
Expand Down Expand Up @@ -1021,6 +1041,7 @@ function bug_view_relationship_view_box( $p_bug_id, $p_can_update ) {
</div>
</div>
</div>
<div class="widget-body" id="relationships-body"></div>
</div>
</div>
<?php
Expand Down
10 changes: 10 additions & 0 deletions client/js/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}
144 changes: 144 additions & 0 deletions client/js/components/IssueRelationships.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React from 'react';
import { IssueService } from '../services';
import { Relationship } from '../models';

type Props = {
relationships: Array<Relationship>;
issueId: number;
canUpdate: boolean;
warning?: string;
}

type States = {
relationships: Array<Relationship>,
reqRelTyp: RelationshipTypeEnum,
reqRelDestIds: string,
}

enum RelationshipTypeEnum {
DUPLICATE_OF = 0,
RELATED_TO = 1,
PARENT_OF = 2,
CHILD_OF = 3,
HAS_DUPLICATE = 4,
}

export class IssueRelationships extends React.Component<Props, States> {

protected readonly Service: IssueService;

constructor(props: Props) {
super(props);

this.state = {
relationships: props.relationships,
reqRelTyp: RelationshipTypeEnum.RELATED_TO,
reqRelDestIds: ''
};
this.Service = new IssueService(props.issueId);
}

async handleRelationshipAdd() {
try {
this.state.reqRelDestIds.split('|').forEach(async (issueId) => {
const relationships = await this.Service.RelationshipAdd(this.state.reqRelTyp, parseInt(issueId));
this.setState({ relationships });
});
} catch (error) {

}
this.setState({
reqRelDestIds: '',
reqRelTyp: RelationshipTypeEnum.RELATED_TO
});
this.forceUpdate();
}

async handleRelationshipDelete(relId: number) {
try {
const relationships = await this.Service.RelationshipDelete(relId);
this.setState({ relationships });
} catch (error) {

}
this.forceUpdate();
}

render() {
const { relationships, reqRelDestIds, reqRelTyp } = this.state;
const { canUpdate, warning } = this.props;
return relationships.length ? (
<React.Fragment>
<div className='widget-toolbox padding-8 clearfix'>
{canUpdate && <div className='form-inline noprint'>
<label className='inline'>Current issue&nbsp;&nbsp;</label>
<select
className='input-sm'
name='rel_type'
onChange={(e) => this.setState({ reqRelTyp: parseInt(e.target.value) })}
value={reqRelTyp}
>
<option value={RelationshipTypeEnum.PARENT_OF}>parent of</option>
<option value={RelationshipTypeEnum.CHILD_OF}>child of</option>
<option value={RelationshipTypeEnum.DUPLICATE_OF}>duplicate of</option>
<option value={RelationshipTypeEnum.HAS_DUPLICATE}>has duplicate</option>
<option value={RelationshipTypeEnum.RELATED_TO}>related to</option>
</select>
&nbsp;
<input
type='text'
Copy link
Author

Choose a reason for hiding this comment

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

@waltergar Typing the issue id and then pressing Enter, no longer works.

Choose a reason for hiding this comment

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

@vboctor why not? I think it's working properly

className='input-sm'
onChange={(e) => this.setState({ reqRelDestIds: e.target.value })}
value={reqRelDestIds}
/>
&nbsp;
<button
onClick={() => this.handleRelationshipAdd()}
className='btn btn-primary btn-sm btn-white btn-round'
>Add</button>
Copy link
Author

Choose a reason for hiding this comment

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

@waltergar Replace all hard-coded strings with localized one that are now available in the html div that I added.

</div>}
</div>
<div className='widget-main no-padding'>
<div className='table-responsive'>
<table className='table table-bordered table-condensed table-hover'>
<tbody>
{relationships.map((relationship: Relationship, key: number) => (
<tr key={key}>
<td><span className='nowrap'>{relationship.type.label}</span></td>
<td><a href={`view.php?id=${relationship.issue.id}`}>{relationship.issue.id}</a></td>
<td>
<i className={`fa fa-square fa-status-box`} style={{ color: relationship.issue.status?.color }}></i>
&nbsp;
<span className='issue-status' title={relationship.issue.resolution?.name || ''}>{relationship.issue.status?.label}</span>
</td>
<td>
<span className='nowrap'>
<a href={`view_user_page.php?id=${relationship.issue.handler?.id}`}>{relationship.issue.handler?.name}</a>
</span>
</td>
<td>
{relationship.issue.summary}&nbsp;
{canUpdate && (
<a
className='red noprint zoom-130'
onClick={() => this.handleRelationshipDelete(relationship['id'])}
>
<i className='ace-icon fa fa-trash-o bigger-115'></i>
</a>
)}
</td>
</tr>
))}
{warning && (
<tr>
<td colSpan={5}><strong>{warning}</strong></td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</React.Fragment>
) : null
}
}
20 changes: 20 additions & 0 deletions client/js/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { IssueRelationships } from './components/IssueRelationships';

if (document.getElementById('issue-data')) {
const _compIssueData = document.getElementById('issue-data');
const issueData = JSON.parse(_compIssueData?.dataset.issue!);

if (issueData.issue && issueData.issue.relationships && document.getElementById('relationships-body')) {
ReactDOM.render(
<IssueRelationships
issueId={issueData.issue.id}
canUpdate={issueData.flags['relationships_can_update']}
relationships={issueData.issue.relationships}
/>,
document.getElementById('relationships-body'));
}
}


Loading