##// END OF EJS Templates
diffs: store wide-diff mode in user sessions for saved state.
marcink -
r3642:ffe3283f new-ui
parent child Browse files
Show More
@@ -1,746 +1,746 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator,
31 31 CSRFRequired)
32 32 from rhodecode.lib.index import searcher_from_config
33 33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.model.db import (
36 36 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup)
37 37 from rhodecode.model.repo import RepoModel
38 38 from rhodecode.model.repo_group import RepoGroupModel
39 39 from rhodecode.model.scm import RepoGroupList, RepoList
40 40 from rhodecode.model.user import UserModel
41 41 from rhodecode.model.user_group import UserGroupModel
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class HomeView(BaseAppView):
47 47
48 48 def load_default_context(self):
49 49 c = self._get_local_tmpl_context()
50 50 c.user = c.auth_user.get_instance()
51 51
52 52 return c
53 53
54 54 @LoginRequired()
55 55 @view_config(
56 56 route_name='user_autocomplete_data', request_method='GET',
57 57 renderer='json_ext', xhr=True)
58 58 def user_autocomplete_data(self):
59 59 self.load_default_context()
60 60 query = self.request.GET.get('query')
61 61 active = str2bool(self.request.GET.get('active') or True)
62 62 include_groups = str2bool(self.request.GET.get('user_groups'))
63 63 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
64 64 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
65 65
66 66 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
67 67 query, active, include_groups)
68 68
69 69 _users = UserModel().get_users(
70 70 name_contains=query, only_active=active)
71 71
72 72 def maybe_skip_default_user(usr):
73 73 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
74 74 return False
75 75 return True
76 76 _users = filter(maybe_skip_default_user, _users)
77 77
78 78 if include_groups:
79 79 # extend with user groups
80 80 _user_groups = UserGroupModel().get_user_groups(
81 81 name_contains=query, only_active=active,
82 82 expand_groups=expand_groups)
83 83 _users = _users + _user_groups
84 84
85 85 return {'suggestions': _users}
86 86
87 87 @LoginRequired()
88 88 @NotAnonymous()
89 89 @view_config(
90 90 route_name='user_group_autocomplete_data', request_method='GET',
91 91 renderer='json_ext', xhr=True)
92 92 def user_group_autocomplete_data(self):
93 93 self.load_default_context()
94 94 query = self.request.GET.get('query')
95 95 active = str2bool(self.request.GET.get('active') or True)
96 96 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
97 97
98 98 log.debug('generating user group list, query:%s, active:%s',
99 99 query, active)
100 100
101 101 _user_groups = UserGroupModel().get_user_groups(
102 102 name_contains=query, only_active=active,
103 103 expand_groups=expand_groups)
104 104 _user_groups = _user_groups
105 105
106 106 return {'suggestions': _user_groups}
107 107
108 108 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
109 109 org_query = name_contains
110 110 allowed_ids = self._rhodecode_user.repo_acl_ids(
111 111 ['repository.read', 'repository.write', 'repository.admin'],
112 112 cache=False, name_filter=name_contains) or [-1]
113 113
114 114 query = Repository.query()\
115 115 .filter(Repository.archived.isnot(true()))\
116 116 .filter(or_(
117 117 # generate multiple IN to fix limitation problems
118 118 *in_filter_generator(Repository.repo_id, allowed_ids)
119 119 ))
120 120
121 121 query = query.order_by(case(
122 122 [
123 123 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
124 124 ],
125 125 ))
126 126 query = query.order_by(func.length(Repository.repo_name))
127 127 query = query.order_by(Repository.repo_name)
128 128
129 129 if repo_type:
130 130 query = query.filter(Repository.repo_type == repo_type)
131 131
132 132 if name_contains:
133 133 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
134 134 query = query.filter(
135 135 Repository.repo_name.ilike(ilike_expression))
136 136 query = query.limit(limit)
137 137
138 138 acl_iter = query
139 139
140 140 return [
141 141 {
142 142 'id': obj.repo_name,
143 143 'value': org_query,
144 144 'value_display': obj.repo_name,
145 145 'text': obj.repo_name,
146 146 'type': 'repo',
147 147 'repo_id': obj.repo_id,
148 148 'repo_type': obj.repo_type,
149 149 'private': obj.private,
150 150 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
151 151 }
152 152 for obj in acl_iter]
153 153
154 154 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
155 155 org_query = name_contains
156 156 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
157 157 ['group.read', 'group.write', 'group.admin'],
158 158 cache=False, name_filter=name_contains) or [-1]
159 159
160 160 query = RepoGroup.query()\
161 161 .filter(or_(
162 162 # generate multiple IN to fix limitation problems
163 163 *in_filter_generator(RepoGroup.group_id, allowed_ids)
164 164 ))
165 165
166 166 query = query.order_by(case(
167 167 [
168 168 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
169 169 ],
170 170 ))
171 171 query = query.order_by(func.length(RepoGroup.group_name))
172 172 query = query.order_by(RepoGroup.group_name)
173 173
174 174 if name_contains:
175 175 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
176 176 query = query.filter(
177 177 RepoGroup.group_name.ilike(ilike_expression))
178 178 query = query.limit(limit)
179 179
180 180 acl_iter = query
181 181
182 182 return [
183 183 {
184 184 'id': obj.group_name,
185 185 'value': org_query,
186 186 'value_display': obj.group_name,
187 187 'text': obj.group_name,
188 188 'type': 'repo_group',
189 189 'repo_group_id': obj.group_id,
190 190 'url': h.route_path(
191 191 'repo_group_home', repo_group_name=obj.group_name)
192 192 }
193 193 for obj in acl_iter]
194 194
195 195 def _get_user_list(self, name_contains=None, limit=20):
196 196 org_query = name_contains
197 197 if not name_contains:
198 198 return [], False
199 199
200 200 # TODO(marcink): should all logged in users be allowed to search others?
201 201 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
202 202 if not allowed_user_search:
203 203 return [], False
204 204
205 205 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
206 206 if len(name_contains) != 1:
207 207 return [], False
208 208
209 209 name_contains = name_contains[0]
210 210
211 211 query = User.query()\
212 212 .order_by(func.length(User.username))\
213 213 .order_by(User.username) \
214 214 .filter(User.username != User.DEFAULT_USER)
215 215
216 216 if name_contains:
217 217 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
218 218 query = query.filter(
219 219 User.username.ilike(ilike_expression))
220 220 query = query.limit(limit)
221 221
222 222 acl_iter = query
223 223
224 224 return [
225 225 {
226 226 'id': obj.user_id,
227 227 'value': org_query,
228 228 'value_display': 'user: `{}`'.format(obj.username),
229 229 'type': 'user',
230 230 'icon_link': h.gravatar_url(obj.email, 30),
231 231 'url': h.route_path(
232 232 'user_profile', username=obj.username)
233 233 }
234 234 for obj in acl_iter], True
235 235
236 236 def _get_user_groups_list(self, name_contains=None, limit=20):
237 237 org_query = name_contains
238 238 if not name_contains:
239 239 return [], False
240 240
241 241 # TODO(marcink): should all logged in users be allowed to search others?
242 242 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
243 243 if not allowed_user_search:
244 244 return [], False
245 245
246 246 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
247 247 if len(name_contains) != 1:
248 248 return [], False
249 249
250 250 name_contains = name_contains[0]
251 251
252 252 query = UserGroup.query()\
253 253 .order_by(func.length(UserGroup.users_group_name))\
254 254 .order_by(UserGroup.users_group_name)
255 255
256 256 if name_contains:
257 257 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
258 258 query = query.filter(
259 259 UserGroup.users_group_name.ilike(ilike_expression))
260 260 query = query.limit(limit)
261 261
262 262 acl_iter = query
263 263
264 264 return [
265 265 {
266 266 'id': obj.users_group_id,
267 267 'value': org_query,
268 268 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
269 269 'type': 'user_group',
270 270 'url': h.route_path(
271 271 'user_group_profile', user_group_name=obj.users_group_name)
272 272 }
273 273 for obj in acl_iter], True
274 274
275 275 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
276 276 repo_name = repo_group_name = None
277 277 if repo:
278 278 repo_name = repo.repo_name
279 279 if repo_group:
280 280 repo_group_name = repo_group.group_name
281 281
282 282 org_query = query
283 283 if not query or len(query) < 3 or not searcher:
284 284 return [], False
285 285
286 286 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
287 287
288 288 if len(commit_hashes) != 1:
289 289 return [], False
290 290
291 291 commit_hash = commit_hashes[0]
292 292
293 293 result = searcher.search(
294 294 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
295 295 repo_name, repo_group_name, raise_on_exc=False)
296 296
297 297 commits = []
298 298 for entry in result['results']:
299 299 repo_data = {
300 300 'repository_id': entry.get('repository_id'),
301 301 'repository_type': entry.get('repo_type'),
302 302 'repository_name': entry.get('repository'),
303 303 }
304 304
305 305 commit_entry = {
306 306 'id': entry['commit_id'],
307 307 'value': org_query,
308 308 'value_display': '`{}` commit: {}'.format(
309 309 entry['repository'], entry['commit_id']),
310 310 'type': 'commit',
311 311 'repo': entry['repository'],
312 312 'repo_data': repo_data,
313 313
314 314 'url': h.route_path(
315 315 'repo_commit',
316 316 repo_name=entry['repository'], commit_id=entry['commit_id'])
317 317 }
318 318
319 319 commits.append(commit_entry)
320 320 return commits, True
321 321
322 322 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
323 323 repo_name = repo_group_name = None
324 324 if repo:
325 325 repo_name = repo.repo_name
326 326 if repo_group:
327 327 repo_group_name = repo_group.group_name
328 328
329 329 org_query = query
330 330 if not query or len(query) < 3 or not searcher:
331 331 return [], False
332 332
333 333 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
334 334 if len(paths_re) != 1:
335 335 return [], False
336 336
337 337 file_path = paths_re[0]
338 338
339 339 search_path = searcher.escape_specials(file_path)
340 340 result = searcher.search(
341 341 'file.raw:*{}*'.format(search_path), 'path', auth_user,
342 342 repo_name, repo_group_name, raise_on_exc=False)
343 343
344 344 files = []
345 345 for entry in result['results']:
346 346 repo_data = {
347 347 'repository_id': entry.get('repository_id'),
348 348 'repository_type': entry.get('repo_type'),
349 349 'repository_name': entry.get('repository'),
350 350 }
351 351
352 352 file_entry = {
353 353 'id': entry['commit_id'],
354 354 'value': org_query,
355 355 'value_display': '`{}` file: {}'.format(
356 356 entry['repository'], entry['file']),
357 357 'type': 'file',
358 358 'repo': entry['repository'],
359 359 'repo_data': repo_data,
360 360
361 361 'url': h.route_path(
362 362 'repo_files',
363 363 repo_name=entry['repository'], commit_id=entry['commit_id'],
364 364 f_path=entry['file'])
365 365 }
366 366
367 367 files.append(file_entry)
368 368 return files, True
369 369
370 370 @LoginRequired()
371 371 @view_config(
372 372 route_name='repo_list_data', request_method='GET',
373 373 renderer='json_ext', xhr=True)
374 374 def repo_list_data(self):
375 375 _ = self.request.translate
376 376 self.load_default_context()
377 377
378 378 query = self.request.GET.get('query')
379 379 repo_type = self.request.GET.get('repo_type')
380 380 log.debug('generating repo list, query:%s, repo_type:%s',
381 381 query, repo_type)
382 382
383 383 res = []
384 384 repos = self._get_repo_list(query, repo_type=repo_type)
385 385 if repos:
386 386 res.append({
387 387 'text': _('Repositories'),
388 388 'children': repos
389 389 })
390 390
391 391 data = {
392 392 'more': False,
393 393 'results': res
394 394 }
395 395 return data
396 396
397 397 @LoginRequired()
398 398 @view_config(
399 399 route_name='repo_group_list_data', request_method='GET',
400 400 renderer='json_ext', xhr=True)
401 401 def repo_group_list_data(self):
402 402 _ = self.request.translate
403 403 self.load_default_context()
404 404
405 405 query = self.request.GET.get('query')
406 406
407 407 log.debug('generating repo group list, query:%s',
408 408 query)
409 409
410 410 res = []
411 411 repo_groups = self._get_repo_group_list(query)
412 412 if repo_groups:
413 413 res.append({
414 414 'text': _('Repository Groups'),
415 415 'children': repo_groups
416 416 })
417 417
418 418 data = {
419 419 'more': False,
420 420 'results': res
421 421 }
422 422 return data
423 423
424 424 def _get_default_search_queries(self, search_context, searcher, query):
425 425 if not searcher:
426 426 return []
427 427
428 428 is_es_6 = searcher.is_es_6
429 429
430 430 queries = []
431 431 repo_group_name, repo_name, repo_context = None, None, None
432 432
433 433 # repo group context
434 434 if search_context.get('search_context[repo_group_name]'):
435 435 repo_group_name = search_context.get('search_context[repo_group_name]')
436 436 if search_context.get('search_context[repo_name]'):
437 437 repo_name = search_context.get('search_context[repo_name]')
438 438 repo_context = search_context.get('search_context[repo_view_type]')
439 439
440 440 if is_es_6 and repo_name:
441 441 # files
442 442 def query_modifier():
443 443 qry = query
444 444 return {'q': qry, 'type': 'content'}
445 445 label = u'File search for `{}` in this repository.'.format(query)
446 446 file_qry = {
447 447 'id': -10,
448 448 'value': query,
449 449 'value_display': label,
450 450 'type': 'search',
451 451 'url': h.route_path('search_repo',
452 452 repo_name=repo_name,
453 453 _query=query_modifier())
454 454 }
455 455
456 456 # commits
457 457 def query_modifier():
458 458 qry = query
459 459 return {'q': qry, 'type': 'commit'}
460 460
461 461 label = u'Commit search for `{}` in this repository.'.format(query)
462 462 commit_qry = {
463 463 'id': -20,
464 464 'value': query,
465 465 'value_display': label,
466 466 'type': 'search',
467 467 'url': h.route_path('search_repo',
468 468 repo_name=repo_name,
469 469 _query=query_modifier())
470 470 }
471 471
472 472 if repo_context in ['commit', 'changelog']:
473 473 queries.extend([commit_qry, file_qry])
474 474 elif repo_context in ['files', 'summary']:
475 475 queries.extend([file_qry, commit_qry])
476 476 else:
477 477 queries.extend([commit_qry, file_qry])
478 478
479 479 elif is_es_6 and repo_group_name:
480 480 # files
481 481 def query_modifier():
482 482 qry = query
483 483 return {'q': qry, 'type': 'content'}
484 484
485 485 label = u'File search for `{}` in this repository group'.format(query)
486 486 file_qry = {
487 487 'id': -30,
488 488 'value': query,
489 489 'value_display': label,
490 490 'type': 'search',
491 491 'url': h.route_path('search_repo_group',
492 492 repo_group_name=repo_group_name,
493 493 _query=query_modifier())
494 494 }
495 495
496 496 # commits
497 497 def query_modifier():
498 498 qry = query
499 499 return {'q': qry, 'type': 'commit'}
500 500
501 501 label = u'Commit search for `{}` in this repository group'.format(query)
502 502 commit_qry = {
503 503 'id': -40,
504 504 'value': query,
505 505 'value_display': label,
506 506 'type': 'search',
507 507 'url': h.route_path('search_repo_group',
508 508 repo_group_name=repo_group_name,
509 509 _query=query_modifier())
510 510 }
511 511
512 512 if repo_context in ['commit', 'changelog']:
513 513 queries.extend([commit_qry, file_qry])
514 514 elif repo_context in ['files', 'summary']:
515 515 queries.extend([file_qry, commit_qry])
516 516 else:
517 517 queries.extend([commit_qry, file_qry])
518 518
519 519 # Global, not scoped
520 520 if not queries:
521 521 queries.append(
522 522 {
523 523 'id': -1,
524 524 'value': query,
525 525 'value_display': u'File search for: `{}`'.format(query),
526 526 'type': 'search',
527 527 'url': h.route_path('search',
528 528 _query={'q': query, 'type': 'content'})
529 529 })
530 530 queries.append(
531 531 {
532 532 'id': -2,
533 533 'value': query,
534 534 'value_display': u'Commit search for: `{}`'.format(query),
535 535 'type': 'search',
536 536 'url': h.route_path('search',
537 537 _query={'q': query, 'type': 'commit'})
538 538 })
539 539
540 540 return queries
541 541
542 542 @LoginRequired()
543 543 @view_config(
544 544 route_name='goto_switcher_data', request_method='GET',
545 545 renderer='json_ext', xhr=True)
546 546 def goto_switcher_data(self):
547 547 c = self.load_default_context()
548 548
549 549 _ = self.request.translate
550 550
551 551 query = self.request.GET.get('query')
552 552 log.debug('generating main filter data, query %s', query)
553 553
554 554 res = []
555 555 if not query:
556 556 return {'suggestions': res}
557 557
558 558 def no_match(name):
559 559 return {
560 560 'id': -1,
561 561 'value': "",
562 562 'value_display': name,
563 563 'type': 'text',
564 564 'url': ""
565 565 }
566 566 searcher = searcher_from_config(self.request.registry.settings)
567 567 has_specialized_search = False
568 568
569 569 # set repo context
570 570 repo = None
571 571 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
572 572 if repo_id:
573 573 repo = Repository.get(repo_id)
574 574
575 575 # set group context
576 576 repo_group = None
577 577 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
578 578 if repo_group_id:
579 579 repo_group = RepoGroup.get(repo_group_id)
580 580 prefix_match = False
581 581
582 582 # user: type search
583 583 if not prefix_match:
584 584 users, prefix_match = self._get_user_list(query)
585 585 if users:
586 586 has_specialized_search = True
587 587 for serialized_user in users:
588 588 res.append(serialized_user)
589 589 elif prefix_match:
590 590 has_specialized_search = True
591 591 res.append(no_match('No matching users found'))
592 592
593 593 # user_group: type search
594 594 if not prefix_match:
595 595 user_groups, prefix_match = self._get_user_groups_list(query)
596 596 if user_groups:
597 597 has_specialized_search = True
598 598 for serialized_user_group in user_groups:
599 599 res.append(serialized_user_group)
600 600 elif prefix_match:
601 601 has_specialized_search = True
602 602 res.append(no_match('No matching user groups found'))
603 603
604 604 # FTS commit: type search
605 605 if not prefix_match:
606 606 commits, prefix_match = self._get_hash_commit_list(
607 607 c.auth_user, searcher, query, repo, repo_group)
608 608 if commits:
609 609 has_specialized_search = True
610 610 unique_repos = collections.OrderedDict()
611 611 for commit in commits:
612 612 repo_name = commit['repo']
613 613 unique_repos.setdefault(repo_name, []).append(commit)
614 614
615 615 for _repo, commits in unique_repos.items():
616 616 for commit in commits:
617 617 res.append(commit)
618 618 elif prefix_match:
619 619 has_specialized_search = True
620 620 res.append(no_match('No matching commits found'))
621 621
622 622 # FTS file: type search
623 623 if not prefix_match:
624 624 paths, prefix_match = self._get_path_list(
625 625 c.auth_user, searcher, query, repo, repo_group)
626 626 if paths:
627 627 has_specialized_search = True
628 628 unique_repos = collections.OrderedDict()
629 629 for path in paths:
630 630 repo_name = path['repo']
631 631 unique_repos.setdefault(repo_name, []).append(path)
632 632
633 633 for repo, paths in unique_repos.items():
634 634 for path in paths:
635 635 res.append(path)
636 636 elif prefix_match:
637 637 has_specialized_search = True
638 638 res.append(no_match('No matching files found'))
639 639
640 640 # main suggestions
641 641 if not has_specialized_search:
642 642 repo_group_name = ''
643 643 if repo_group:
644 644 repo_group_name = repo_group.group_name
645 645
646 646 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
647 647 res.append(_q)
648 648
649 649 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
650 650 for serialized_repo_group in repo_groups:
651 651 res.append(serialized_repo_group)
652 652
653 653 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
654 654 for serialized_repo in repos:
655 655 res.append(serialized_repo)
656 656
657 657 if not repos and not repo_groups:
658 658 res.append(no_match('No matches found'))
659 659
660 660 return {'suggestions': res}
661 661
662 662 def _get_groups_and_repos(self, repo_group_id=None):
663 663 # repo groups groups
664 664 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
665 665 _perms = ['group.read', 'group.write', 'group.admin']
666 666 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
667 667 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
668 668 repo_group_list=repo_group_list_acl, admin=False)
669 669
670 670 # repositories
671 671 repo_list = Repository.get_all_repos(group_id=repo_group_id)
672 672 _perms = ['repository.read', 'repository.write', 'repository.admin']
673 673 repo_list_acl = RepoList(repo_list, perm_set=_perms)
674 674 repo_data = RepoModel().get_repos_as_dict(
675 675 repo_list=repo_list_acl, admin=False)
676 676
677 677 return repo_data, repo_group_data
678 678
679 679 @LoginRequired()
680 680 @view_config(
681 681 route_name='home', request_method='GET',
682 682 renderer='rhodecode:templates/index.mako')
683 683 def main_page(self):
684 684 c = self.load_default_context()
685 685 c.repo_group = None
686 686
687 687 repo_data, repo_group_data = self._get_groups_and_repos()
688 688 # json used to render the grids
689 689 c.repos_data = json.dumps(repo_data)
690 690 c.repo_groups_data = json.dumps(repo_group_data)
691 691
692 692 return self._get_template_context(c)
693 693
694 694 @LoginRequired()
695 695 @HasRepoGroupPermissionAnyDecorator(
696 696 'group.read', 'group.write', 'group.admin')
697 697 @view_config(
698 698 route_name='repo_group_home', request_method='GET',
699 699 renderer='rhodecode:templates/index_repo_group.mako')
700 700 @view_config(
701 701 route_name='repo_group_home_slash', request_method='GET',
702 702 renderer='rhodecode:templates/index_repo_group.mako')
703 703 def repo_group_main_page(self):
704 704 c = self.load_default_context()
705 705 c.repo_group = self.request.db_repo_group
706 706 repo_data, repo_group_data = self._get_groups_and_repos(
707 707 c.repo_group.group_id)
708 708
709 709 # json used to render the grids
710 710 c.repos_data = json.dumps(repo_data)
711 711 c.repo_groups_data = json.dumps(repo_group_data)
712 712
713 713 return self._get_template_context(c)
714 714
715 715 @LoginRequired()
716 716 @CSRFRequired()
717 717 @view_config(
718 718 route_name='markup_preview', request_method='POST',
719 719 renderer='string', xhr=True)
720 720 def markup_preview(self):
721 721 # Technically a CSRF token is not needed as no state changes with this
722 722 # call. However, as this is a POST is better to have it, so automated
723 723 # tools don't flag it as potential CSRF.
724 724 # Post is required because the payload could be bigger than the maximum
725 725 # allowed by GET.
726 726
727 727 text = self.request.POST.get('text')
728 728 renderer = self.request.POST.get('renderer') or 'rst'
729 729 if text:
730 730 return h.render(text, renderer=renderer, mentions=True)
731 731 return ''
732 732
733 733 @LoginRequired()
734 734 @CSRFRequired()
735 735 @view_config(
736 736 route_name='store_user_session_value', request_method='POST',
737 737 renderer='string', xhr=True)
738 738 def store_user_session_attr(self):
739 739 key = self.request.POST.get('key')
740 740 val = self.request.POST.get('val')
741 741
742 742 existing_value = self.request.session.get(key)
743 743 if existing_value != val:
744 744 self.request.session[key] = val
745 745
746 return 'stored:{}'.format(key)
746 return 'stored:{}:{}'.format(key, val)
@@ -1,836 +1,838 b''
1 1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45
46 46
47 47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 48 failHandler = failHandler || function() {};
49 49 postData = toQueryString(postData);
50 50 var request = $.ajax({
51 51 url: url,
52 52 type: 'POST',
53 53 data: postData,
54 54 headers: {'X-PARTIAL-XHR': true}
55 55 })
56 56 .done(function (data) {
57 57 successHandler(data);
58 58 })
59 59 .fail(function (data, textStatus, errorThrown) {
60 60 failHandler(data, textStatus, errorThrown)
61 61 });
62 62 return request;
63 63 };
64 64
65 65
66 66
67 67
68 68 /* Comment form for main and inline comments */
69 69 (function(mod) {
70 70
71 71 if (typeof exports == "object" && typeof module == "object") {
72 72 // CommonJS
73 73 module.exports = mod();
74 74 }
75 75 else {
76 76 // Plain browser env
77 77 (this || window).CommentForm = mod();
78 78 }
79 79
80 80 })(function() {
81 81 "use strict";
82 82
83 83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
84 84 if (!(this instanceof CommentForm)) {
85 85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
86 86 }
87 87
88 88 // bind the element instance to our Form
89 89 $(formElement).get(0).CommentForm = this;
90 90
91 91 this.withLineNo = function(selector) {
92 92 var lineNo = this.lineNo;
93 93 if (lineNo === undefined) {
94 94 return selector
95 95 } else {
96 96 return selector + '_' + lineNo;
97 97 }
98 98 };
99 99
100 100 this.commitId = commitId;
101 101 this.pullRequestId = pullRequestId;
102 102 this.lineNo = lineNo;
103 103 this.initAutocompleteActions = initAutocompleteActions;
104 104
105 105 this.previewButton = this.withLineNo('#preview-btn');
106 106 this.previewContainer = this.withLineNo('#preview-container');
107 107
108 108 this.previewBoxSelector = this.withLineNo('#preview-box');
109 109
110 110 this.editButton = this.withLineNo('#edit-btn');
111 111 this.editContainer = this.withLineNo('#edit-container');
112 112 this.cancelButton = this.withLineNo('#cancel-btn');
113 113 this.commentType = this.withLineNo('#comment_type');
114 114
115 115 this.resolvesId = null;
116 116 this.resolvesActionId = null;
117 117
118 118 this.closesPr = '#close_pull_request';
119 119
120 120 this.cmBox = this.withLineNo('#text');
121 121 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122 122
123 123 this.statusChange = this.withLineNo('#change_status');
124 124
125 125 this.submitForm = formElement;
126 126 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 127 this.submitButtonText = this.submitButton.val();
128 128
129 129 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
130 130 {'repo_name': templateContext.repo_name,
131 131 'commit_id': templateContext.commit_data.commit_id});
132 132
133 133 if (resolvesCommentId){
134 134 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
135 135 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
136 136 $(this.commentType).prop('disabled', true);
137 137 $(this.commentType).addClass('disabled');
138 138
139 139 // disable select
140 140 setTimeout(function() {
141 141 $(self.statusChange).select2('readonly', true);
142 142 }, 10);
143 143
144 144 var resolvedInfo = (
145 145 '<li class="resolve-action">' +
146 146 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
147 147 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
148 148 '</li>'
149 149 ).format(resolvesCommentId, _gettext('resolve comment'));
150 150 $(resolvedInfo).insertAfter($(this.commentType).parent());
151 151 }
152 152
153 153 // based on commitId, or pullRequestId decide where do we submit
154 154 // out data
155 155 if (this.commitId){
156 156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
157 157 {'repo_name': templateContext.repo_name,
158 158 'commit_id': this.commitId});
159 159 this.selfUrl = pyroutes.url('repo_commit',
160 160 {'repo_name': templateContext.repo_name,
161 161 'commit_id': this.commitId});
162 162
163 163 } else if (this.pullRequestId) {
164 164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
165 165 {'repo_name': templateContext.repo_name,
166 166 'pull_request_id': this.pullRequestId});
167 167 this.selfUrl = pyroutes.url('pullrequest_show',
168 168 {'repo_name': templateContext.repo_name,
169 169 'pull_request_id': this.pullRequestId});
170 170
171 171 } else {
172 172 throw new Error(
173 173 'CommentForm requires pullRequestId, or commitId to be specified.')
174 174 }
175 175
176 176 // FUNCTIONS and helpers
177 177 var self = this;
178 178
179 179 this.isInline = function(){
180 180 return this.lineNo && this.lineNo != 'general';
181 181 };
182 182
183 183 this.getCmInstance = function(){
184 184 return this.cm
185 185 };
186 186
187 187 this.setPlaceholder = function(placeholder) {
188 188 var cm = this.getCmInstance();
189 189 if (cm){
190 190 cm.setOption('placeholder', placeholder);
191 191 }
192 192 };
193 193
194 194 this.getCommentStatus = function() {
195 195 return $(this.submitForm).find(this.statusChange).val();
196 196 };
197 197 this.getCommentType = function() {
198 198 return $(this.submitForm).find(this.commentType).val();
199 199 };
200 200
201 201 this.getResolvesId = function() {
202 202 return $(this.submitForm).find(this.resolvesId).val() || null;
203 203 };
204 204
205 205 this.getClosePr = function() {
206 206 return $(this.submitForm).find(this.closesPr).val() || null;
207 207 };
208 208
209 209 this.markCommentResolved = function(resolvedCommentId){
210 210 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
211 211 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
212 212 };
213 213
214 214 this.isAllowedToSubmit = function() {
215 215 return !$(this.submitButton).prop('disabled');
216 216 };
217 217
218 218 this.initStatusChangeSelector = function(){
219 219 var formatChangeStatus = function(state, escapeMarkup) {
220 220 var originalOption = state.element;
221 221 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
222 222 '<span>' + escapeMarkup(state.text) + '</span>';
223 223 };
224 224 var formatResult = function(result, container, query, escapeMarkup) {
225 225 return formatChangeStatus(result, escapeMarkup);
226 226 };
227 227
228 228 var formatSelection = function(data, container, escapeMarkup) {
229 229 return formatChangeStatus(data, escapeMarkup);
230 230 };
231 231
232 232 $(this.submitForm).find(this.statusChange).select2({
233 233 placeholder: _gettext('Status Review'),
234 234 formatResult: formatResult,
235 235 formatSelection: formatSelection,
236 236 containerCssClass: "drop-menu status_box_menu",
237 237 dropdownCssClass: "drop-menu-dropdown",
238 238 dropdownAutoWidth: true,
239 239 minimumResultsForSearch: -1
240 240 });
241 241 $(this.submitForm).find(this.statusChange).on('change', function() {
242 242 var status = self.getCommentStatus();
243 243
244 244 if (status && !self.isInline()) {
245 245 $(self.submitButton).prop('disabled', false);
246 246 }
247 247
248 248 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
249 249 self.setPlaceholder(placeholderText)
250 250 })
251 251 };
252 252
253 253 // reset the comment form into it's original state
254 254 this.resetCommentFormState = function(content) {
255 255 content = content || '';
256 256
257 257 $(this.editContainer).show();
258 258 $(this.editButton).parent().addClass('active');
259 259
260 260 $(this.previewContainer).hide();
261 261 $(this.previewButton).parent().removeClass('active');
262 262
263 263 this.setActionButtonsDisabled(true);
264 264 self.cm.setValue(content);
265 265 self.cm.setOption("readOnly", false);
266 266
267 267 if (this.resolvesId) {
268 268 // destroy the resolve action
269 269 $(this.resolvesId).parent().remove();
270 270 }
271 271 // reset closingPR flag
272 272 $('.close-pr-input').remove();
273 273
274 274 $(this.statusChange).select2('readonly', false);
275 275 };
276 276
277 277 this.globalSubmitSuccessCallback = function(){
278 278 // default behaviour is to call GLOBAL hook, if it's registered.
279 279 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
280 280 commentFormGlobalSubmitSuccessCallback()
281 281 }
282 282 };
283 283
284 284 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
285 285 return _submitAjaxPOST(url, postData, successHandler, failHandler);
286 286 };
287 287
288 288 // overwrite a submitHandler, we need to do it for inline comments
289 289 this.setHandleFormSubmit = function(callback) {
290 290 this.handleFormSubmit = callback;
291 291 };
292 292
293 293 // overwrite a submitSuccessHandler
294 294 this.setGlobalSubmitSuccessCallback = function(callback) {
295 295 this.globalSubmitSuccessCallback = callback;
296 296 };
297 297
298 298 // default handler for for submit for main comments
299 299 this.handleFormSubmit = function() {
300 300 var text = self.cm.getValue();
301 301 var status = self.getCommentStatus();
302 302 var commentType = self.getCommentType();
303 303 var resolvesCommentId = self.getResolvesId();
304 304 var closePullRequest = self.getClosePr();
305 305
306 306 if (text === "" && !status) {
307 307 return;
308 308 }
309 309
310 310 var excludeCancelBtn = false;
311 311 var submitEvent = true;
312 312 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
313 313 self.cm.setOption("readOnly", true);
314 314
315 315 var postData = {
316 316 'text': text,
317 317 'changeset_status': status,
318 318 'comment_type': commentType,
319 319 'csrf_token': CSRF_TOKEN
320 320 };
321 321
322 322 if (resolvesCommentId) {
323 323 postData['resolves_comment_id'] = resolvesCommentId;
324 324 }
325 325
326 326 if (closePullRequest) {
327 327 postData['close_pull_request'] = true;
328 328 }
329 329
330 330 var submitSuccessCallback = function(o) {
331 331 // reload page if we change status for single commit.
332 332 if (status && self.commitId) {
333 333 location.reload(true);
334 334 } else {
335 335 $('#injected_page_comments').append(o.rendered_text);
336 336 self.resetCommentFormState();
337 337 timeagoActivate();
338 338
339 339 // mark visually which comment was resolved
340 340 if (resolvesCommentId) {
341 341 self.markCommentResolved(resolvesCommentId);
342 342 }
343 343 }
344 344
345 345 // run global callback on submit
346 346 self.globalSubmitSuccessCallback();
347 347
348 348 };
349 349 var submitFailCallback = function(data) {
350 350 alert(
351 351 "Error while submitting comment.\n" +
352 352 "Error code {0} ({1}).".format(data.status, data.statusText)
353 353 );
354 354 self.resetCommentFormState(text);
355 355 };
356 356 self.submitAjaxPOST(
357 357 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
358 358 };
359 359
360 360 this.previewSuccessCallback = function(o) {
361 361 $(self.previewBoxSelector).html(o);
362 362 $(self.previewBoxSelector).removeClass('unloaded');
363 363
364 364 // swap buttons, making preview active
365 365 $(self.previewButton).parent().addClass('active');
366 366 $(self.editButton).parent().removeClass('active');
367 367
368 368 // unlock buttons
369 369 self.setActionButtonsDisabled(false);
370 370 };
371 371
372 372 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
373 373 excludeCancelBtn = excludeCancelBtn || false;
374 374 submitEvent = submitEvent || false;
375 375
376 376 $(this.editButton).prop('disabled', state);
377 377 $(this.previewButton).prop('disabled', state);
378 378
379 379 if (!excludeCancelBtn) {
380 380 $(this.cancelButton).prop('disabled', state);
381 381 }
382 382
383 383 var submitState = state;
384 384 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
385 385 // if the value of commit review status is set, we allow
386 386 // submit button, but only on Main form, isInline means inline
387 387 submitState = false
388 388 }
389 389
390 390 $(this.submitButton).prop('disabled', submitState);
391 391 if (submitEvent) {
392 392 $(this.submitButton).val(_gettext('Submitting...'));
393 393 } else {
394 394 $(this.submitButton).val(this.submitButtonText);
395 395 }
396 396
397 397 };
398 398
399 399 // lock preview/edit/submit buttons on load, but exclude cancel button
400 400 var excludeCancelBtn = true;
401 401 this.setActionButtonsDisabled(true, excludeCancelBtn);
402 402
403 403 // anonymous users don't have access to initialized CM instance
404 404 if (this.cm !== undefined){
405 405 this.cm.on('change', function(cMirror) {
406 406 if (cMirror.getValue() === "") {
407 407 self.setActionButtonsDisabled(true, excludeCancelBtn)
408 408 } else {
409 409 self.setActionButtonsDisabled(false, excludeCancelBtn)
410 410 }
411 411 });
412 412 }
413 413
414 414 $(this.editButton).on('click', function(e) {
415 415 e.preventDefault();
416 416
417 417 $(self.previewButton).parent().removeClass('active');
418 418 $(self.previewContainer).hide();
419 419
420 420 $(self.editButton).parent().addClass('active');
421 421 $(self.editContainer).show();
422 422
423 423 });
424 424
425 425 $(this.previewButton).on('click', function(e) {
426 426 e.preventDefault();
427 427 var text = self.cm.getValue();
428 428
429 429 if (text === "") {
430 430 return;
431 431 }
432 432
433 433 var postData = {
434 434 'text': text,
435 435 'renderer': templateContext.visual.default_renderer,
436 436 'csrf_token': CSRF_TOKEN
437 437 };
438 438
439 439 // lock ALL buttons on preview
440 440 self.setActionButtonsDisabled(true);
441 441
442 442 $(self.previewBoxSelector).addClass('unloaded');
443 443 $(self.previewBoxSelector).html(_gettext('Loading ...'));
444 444
445 445 $(self.editContainer).hide();
446 446 $(self.previewContainer).show();
447 447
448 448 // by default we reset state of comment preserving the text
449 449 var previewFailCallback = function(data){
450 450 alert(
451 451 "Error while preview of comment.\n" +
452 452 "Error code {0} ({1}).".format(data.status, data.statusText)
453 453 );
454 454 self.resetCommentFormState(text)
455 455 };
456 456 self.submitAjaxPOST(
457 457 self.previewUrl, postData, self.previewSuccessCallback,
458 458 previewFailCallback);
459 459
460 460 $(self.previewButton).parent().addClass('active');
461 461 $(self.editButton).parent().removeClass('active');
462 462 });
463 463
464 464 $(this.submitForm).submit(function(e) {
465 465 e.preventDefault();
466 466 var allowedToSubmit = self.isAllowedToSubmit();
467 467 if (!allowedToSubmit){
468 468 return false;
469 469 }
470 470 self.handleFormSubmit();
471 471 });
472 472
473 473 }
474 474
475 475 return CommentForm;
476 476 });
477 477
478 478 /* comments controller */
479 479 var CommentsController = function() {
480 480 var mainComment = '#text';
481 481 var self = this;
482 482
483 483 this.cancelComment = function(node) {
484 484 var $node = $(node);
485 485 var $td = $node.closest('td');
486 486 $node.closest('.comment-inline-form').remove();
487 487 return false;
488 488 };
489 489
490 490 this.getLineNumber = function(node) {
491 491 var $node = $(node);
492 492 var lineNo = $node.closest('td').attr('data-line-no');
493 493 if (lineNo === undefined && $node.data('commentInline')){
494 494 lineNo = $node.data('commentLineNo')
495 495 }
496 496
497 497 return lineNo
498 498 };
499 499
500 500 this.scrollToComment = function(node, offset, outdated) {
501 501 if (offset === undefined) {
502 502 offset = 0;
503 503 }
504 504 var outdated = outdated || false;
505 505 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
506 506
507 507 if (!node) {
508 508 node = $('.comment-selected');
509 509 if (!node.length) {
510 510 node = $('comment-current')
511 511 }
512 512 }
513 513 $wrapper = $(node).closest('div.comment');
514 514 $comment = $(node).closest(klass);
515 515 $comments = $(klass);
516 516
517 517 // show hidden comment when referenced.
518 518 if (!$wrapper.is(':visible')){
519 519 $wrapper.show();
520 520 }
521 521
522 522 $('.comment-selected').removeClass('comment-selected');
523 523
524 524 var nextIdx = $(klass).index($comment) + offset;
525 525 if (nextIdx >= $comments.length) {
526 526 nextIdx = 0;
527 527 }
528 528 var $next = $(klass).eq(nextIdx);
529 529
530 530 var $cb = $next.closest('.cb');
531 531 $cb.removeClass('cb-collapsed');
532 532
533 533 var $filediffCollapseState = $cb.closest('.filediff').prev();
534 534 $filediffCollapseState.prop('checked', false);
535 535 $next.addClass('comment-selected');
536 536 scrollToElement($next);
537 537 return false;
538 538 };
539 539
540 540 this.nextComment = function(node) {
541 541 return self.scrollToComment(node, 1);
542 542 };
543 543
544 544 this.prevComment = function(node) {
545 545 return self.scrollToComment(node, -1);
546 546 };
547 547
548 548 this.nextOutdatedComment = function(node) {
549 549 return self.scrollToComment(node, 1, true);
550 550 };
551 551
552 552 this.prevOutdatedComment = function(node) {
553 553 return self.scrollToComment(node, -1, true);
554 554 };
555 555
556 556 this.deleteComment = function(node) {
557 557 if (!confirm(_gettext('Delete this comment?'))) {
558 558 return false;
559 559 }
560 560 var $node = $(node);
561 561 var $td = $node.closest('td');
562 562 var $comment = $node.closest('.comment');
563 563 var comment_id = $comment.attr('data-comment-id');
564 564 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
565 565 var postData = {
566 566 'csrf_token': CSRF_TOKEN
567 567 };
568 568
569 569 $comment.addClass('comment-deleting');
570 570 $comment.hide('fast');
571 571
572 572 var success = function(response) {
573 573 $comment.remove();
574 574 return false;
575 575 };
576 576 var failure = function(data, textStatus, xhr) {
577 577 alert("error processing request: " + textStatus);
578 578 $comment.show('fast');
579 579 $comment.removeClass('comment-deleting');
580 580 return false;
581 581 };
582 582 ajaxPOST(url, postData, success, failure);
583 583 };
584 584
585 585 this.toggleWideMode = function (node) {
586 586 if ($('#content').hasClass('wrapper')) {
587 587 $('#content').removeClass("wrapper");
588 588 $('#content').addClass("wide-mode-wrapper");
589 589 $(node).addClass('btn-success');
590 return true
590 591 } else {
591 592 $('#content').removeClass("wide-mode-wrapper");
592 593 $('#content').addClass("wrapper");
593 594 $(node).removeClass('btn-success');
595 return false
594 596 }
595 return false;
597
596 598 };
597 599
598 600 this.toggleComments = function(node, show) {
599 601 var $filediff = $(node).closest('.filediff');
600 602 if (show === true) {
601 603 $filediff.removeClass('hide-comments');
602 604 } else if (show === false) {
603 605 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
604 606 $filediff.addClass('hide-comments');
605 607 } else {
606 608 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
607 609 $filediff.toggleClass('hide-comments');
608 610 }
609 611 return false;
610 612 };
611 613
612 614 this.toggleLineComments = function(node) {
613 615 self.toggleComments(node, true);
614 616 var $node = $(node);
615 617 // mark outdated comments as visible before the toggle;
616 618 $(node.closest('tr')).find('.comment-outdated').show();
617 619 $node.closest('tr').toggleClass('hide-line-comments');
618 620 };
619 621
620 622 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
621 623 var pullRequestId = templateContext.pull_request_data.pull_request_id;
622 624 var commitId = templateContext.commit_data.commit_id;
623 625
624 626 var commentForm = new CommentForm(
625 627 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
626 628 var cm = commentForm.getCmInstance();
627 629
628 630 if (resolvesCommentId){
629 631 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
630 632 }
631 633
632 634 setTimeout(function() {
633 635 // callbacks
634 636 if (cm !== undefined) {
635 637 commentForm.setPlaceholder(placeholderText);
636 638 if (commentForm.isInline()) {
637 639 cm.focus();
638 640 cm.refresh();
639 641 }
640 642 }
641 643 }, 10);
642 644
643 645 // trigger scrolldown to the resolve comment, since it might be away
644 646 // from the clicked
645 647 if (resolvesCommentId){
646 648 var actionNode = $(commentForm.resolvesActionId).offset();
647 649
648 650 setTimeout(function() {
649 651 if (actionNode) {
650 652 $('body, html').animate({scrollTop: actionNode.top}, 10);
651 653 }
652 654 }, 100);
653 655 }
654 656
655 657 return commentForm;
656 658 };
657 659
658 660 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
659 661
660 662 var tmpl = $('#cb-comment-general-form-template').html();
661 663 tmpl = tmpl.format(null, 'general');
662 664 var $form = $(tmpl);
663 665
664 666 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
665 667 var curForm = $formPlaceholder.find('form');
666 668 if (curForm){
667 669 curForm.remove();
668 670 }
669 671 $formPlaceholder.append($form);
670 672
671 673 var _form = $($form[0]);
672 674 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
673 675 var commentForm = this.createCommentForm(
674 676 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
675 677 commentForm.initStatusChangeSelector();
676 678
677 679 return commentForm;
678 680 };
679 681
680 682 this.createComment = function(node, resolutionComment) {
681 683 var resolvesCommentId = resolutionComment || null;
682 684 var $node = $(node);
683 685 var $td = $node.closest('td');
684 686 var $form = $td.find('.comment-inline-form');
685 687
686 688 if (!$form.length) {
687 689
688 690 var $filediff = $node.closest('.filediff');
689 691 $filediff.removeClass('hide-comments');
690 692 var f_path = $filediff.attr('data-f-path');
691 693 var lineno = self.getLineNumber(node);
692 694 // create a new HTML from template
693 695 var tmpl = $('#cb-comment-inline-form-template').html();
694 696 tmpl = tmpl.format(escapeHtml(f_path), lineno);
695 697 $form = $(tmpl);
696 698
697 699 var $comments = $td.find('.inline-comments');
698 700 if (!$comments.length) {
699 701 $comments = $(
700 702 $('#cb-comments-inline-container-template').html());
701 703 $td.append($comments);
702 704 }
703 705
704 706 $td.find('.cb-comment-add-button').before($form);
705 707
706 708 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
707 709 var _form = $($form[0]).find('form');
708 710 var autocompleteActions = ['as_note', 'as_todo'];
709 711 var commentForm = this.createCommentForm(
710 712 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
711 713
712 714 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
713 715 form: _form,
714 716 parent: $td[0],
715 717 lineno: lineno,
716 718 f_path: f_path}
717 719 );
718 720
719 721 // set a CUSTOM submit handler for inline comments.
720 722 commentForm.setHandleFormSubmit(function(o) {
721 723 var text = commentForm.cm.getValue();
722 724 var commentType = commentForm.getCommentType();
723 725 var resolvesCommentId = commentForm.getResolvesId();
724 726
725 727 if (text === "") {
726 728 return;
727 729 }
728 730
729 731 if (lineno === undefined) {
730 732 alert('missing line !');
731 733 return;
732 734 }
733 735 if (f_path === undefined) {
734 736 alert('missing file path !');
735 737 return;
736 738 }
737 739
738 740 var excludeCancelBtn = false;
739 741 var submitEvent = true;
740 742 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
741 743 commentForm.cm.setOption("readOnly", true);
742 744 var postData = {
743 745 'text': text,
744 746 'f_path': f_path,
745 747 'line': lineno,
746 748 'comment_type': commentType,
747 749 'csrf_token': CSRF_TOKEN
748 750 };
749 751 if (resolvesCommentId){
750 752 postData['resolves_comment_id'] = resolvesCommentId;
751 753 }
752 754
753 755 var submitSuccessCallback = function(json_data) {
754 756 $form.remove();
755 757 try {
756 758 var html = json_data.rendered_text;
757 759 var lineno = json_data.line_no;
758 760 var target_id = json_data.target_id;
759 761
760 762 $comments.find('.cb-comment-add-button').before(html);
761 763
762 764 //mark visually which comment was resolved
763 765 if (resolvesCommentId) {
764 766 commentForm.markCommentResolved(resolvesCommentId);
765 767 }
766 768
767 769 // run global callback on submit
768 770 commentForm.globalSubmitSuccessCallback();
769 771
770 772 } catch (e) {
771 773 console.error(e);
772 774 }
773 775
774 776 // re trigger the linkification of next/prev navigation
775 777 linkifyComments($('.inline-comment-injected'));
776 778 timeagoActivate();
777 779
778 780 if (window.updateSticky !== undefined) {
779 781 // potentially our comments change the active window size, so we
780 782 // notify sticky elements
781 783 updateSticky()
782 784 }
783 785
784 786 commentForm.setActionButtonsDisabled(false);
785 787
786 788 };
787 789 var submitFailCallback = function(data){
788 790 alert(
789 791 "Error while submitting comment.\n" +
790 792 "Error code {0} ({1}).".format(data.status, data.statusText)
791 793 );
792 794 commentForm.resetCommentFormState(text)
793 795 };
794 796 commentForm.submitAjaxPOST(
795 797 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
796 798 });
797 799 }
798 800
799 801 $form.addClass('comment-inline-form-open');
800 802 };
801 803
802 804 this.createResolutionComment = function(commentId){
803 805 // hide the trigger text
804 806 $('#resolve-comment-{0}'.format(commentId)).hide();
805 807
806 808 var comment = $('#comment-'+commentId);
807 809 var commentData = comment.data();
808 810 if (commentData.commentInline) {
809 811 this.createComment(comment, commentId)
810 812 } else {
811 813 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
812 814 }
813 815
814 816 return false;
815 817 };
816 818
817 819 this.submitResolution = function(commentId){
818 820 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
819 821 var commentForm = form.get(0).CommentForm;
820 822
821 823 var cm = commentForm.getCmInstance();
822 824 var renderer = templateContext.visual.default_renderer;
823 825 if (renderer == 'rst'){
824 826 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
825 827 } else if (renderer == 'markdown') {
826 828 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
827 829 } else {
828 830 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
829 831 }
830 832
831 833 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
832 834 form.submit();
833 835 return false;
834 836 };
835 837
836 838 };
@@ -1,1025 +1,1031 b''
1 1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 2
3 3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 5 %></%def>
6 6
7 7 <%def name="action_class(action)">
8 8 <%
9 9 return {
10 10 '-': 'cb-deletion',
11 11 '+': 'cb-addition',
12 12 ' ': 'cb-context',
13 13 }.get(action, 'cb-empty')
14 14 %>
15 15 </%def>
16 16
17 17 <%def name="op_class(op_id)">
18 18 <%
19 19 return {
20 20 DEL_FILENODE: 'deletion', # file deleted
21 21 BIN_FILENODE: 'warning' # binary diff hidden
22 22 }.get(op_id, 'addition')
23 23 %>
24 24 </%def>
25 25
26 26
27 27
28 28 <%def name="render_diffset(diffset, commit=None,
29 29
30 30 # collapse all file diff entries when there are more than this amount of files in the diff
31 31 collapse_when_files_over=20,
32 32
33 33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 34 lines_changed_limit=500,
35 35
36 36 # add a ruler at to the output
37 37 ruler_at_chars=0,
38 38
39 39 # show inline comments
40 40 use_comments=False,
41 41
42 42 # disable new comments
43 43 disable_new_comments=False,
44 44
45 45 # special file-comments that were deleted in previous versions
46 46 # it's used for showing outdated comments for deleted files in a PR
47 47 deleted_files_comments=None,
48 48
49 49 # for cache purpose
50 50 inline_comments=None,
51 51
52 52 )">
53 53 %if use_comments:
54 54 <div id="cb-comments-inline-container-template" class="js-template">
55 55 ${inline_comments_container([], inline_comments)}
56 56 </div>
57 57 <div class="js-template" id="cb-comment-inline-form-template">
58 58 <div class="comment-inline-form ac">
59 59
60 60 %if c.rhodecode_user.username != h.DEFAULT_USER:
61 61 ## render template for inline comments
62 62 ${commentblock.comment_form(form_type='inline')}
63 63 %else:
64 64 ${h.form('', class_='inline-form comment-form-login', method='get')}
65 65 <div class="pull-left">
66 66 <div class="comment-help pull-right">
67 67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
68 68 </div>
69 69 </div>
70 70 <div class="comment-button pull-right">
71 71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
72 72 ${_('Cancel')}
73 73 </button>
74 74 </div>
75 75 <div class="clearfix"></div>
76 76 ${h.end_form()}
77 77 %endif
78 78 </div>
79 79 </div>
80 80
81 81 %endif
82 82 <%
83 83 collapse_all = len(diffset.files) > collapse_when_files_over
84 84 %>
85 85
86 86 %if c.user_session_attrs["diffmode"] == 'sideside':
87 87 <style>
88 88 .wrapper {
89 89 max-width: 1600px !important;
90 90 }
91 91 </style>
92 92 %endif
93 93
94 94 %if ruler_at_chars:
95 95 <style>
96 96 .diff table.cb .cb-content:after {
97 97 content: "";
98 98 border-left: 1px solid blue;
99 99 position: absolute;
100 100 top: 0;
101 101 height: 18px;
102 102 opacity: .2;
103 103 z-index: 10;
104 104 //## +5 to account for diff action (+/-)
105 105 left: ${ruler_at_chars + 5}ch;
106 106 </style>
107 107 %endif
108 108
109 109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
111 111 %if commit:
112 112 <div class="pull-right">
113 113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
114 114 ${_('Browse Files')}
115 115 </a>
116 116 </div>
117 117 %endif
118 118 <h2 class="clearinner">
119 119 ## invidual commit
120 120 % if commit:
121 121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
122 122 ${h.age_component(commit.date)}
123 123 % if diffset.limited_diff:
124 124 - ${_('The requested changes are too big and content was truncated.')}
125 125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
126 126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
127 127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
128 128 ## compare diff, has no file-selector and we want to show stats anyway
129 129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
130 130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
131 131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
132 132 % endif
133 133 % else:
134 134 ## pull requests/compare
135 135 ${_('File Changes')}
136 136 % endif
137 137
138 138 </h2>
139 139 </div>
140 140
141 141 %if diffset.has_hidden_changes:
142 142 <p class="empty_data">${_('Some changes may be hidden')}</p>
143 143 %elif not diffset.files:
144 144 <p class="empty_data">${_('No files')}</p>
145 145 %endif
146 146
147 147 <div class="filediffs">
148 148
149 149 ## initial value could be marked as False later on
150 150 <% over_lines_changed_limit = False %>
151 151 %for i, filediff in enumerate(diffset.files):
152 152
153 153 <%
154 154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
155 155 over_lines_changed_limit = lines_changed > lines_changed_limit
156 156 %>
157 157 ## anchor with support of sticky header
158 158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
159 159
160 160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
161 161 <div
162 162 class="filediff"
163 163 data-f-path="${filediff.patch['filename']}"
164 164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
165 165 >
166 166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
167 167 <div class="filediff-collapse-indicator"></div>
168 168 ${diff_ops(filediff)}
169 169 </label>
170 170
171 171 ${diff_menu(filediff, use_comments=use_comments)}
172 172 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
173 173
174 174 ## new/deleted/empty content case
175 175 % if not filediff.hunks:
176 176 ## Comment container, on "fakes" hunk that contains all data to render comments
177 177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
178 178 % endif
179 179
180 180 %if filediff.limited_diff:
181 181 <tr class="cb-warning cb-collapser">
182 182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
183 183 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
184 184 </td>
185 185 </tr>
186 186 %else:
187 187 %if over_lines_changed_limit:
188 188 <tr class="cb-warning cb-collapser">
189 189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
190 190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
191 191 <a href="#" class="cb-expand"
192 192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
193 193 </a>
194 194 <a href="#" class="cb-collapse"
195 195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
196 196 </a>
197 197 </td>
198 198 </tr>
199 199 %endif
200 200 %endif
201 201
202 202 % for hunk in filediff.hunks:
203 203 <tr class="cb-hunk">
204 204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
205 205 ## TODO: dan: add ajax loading of more context here
206 206 ## <a href="#">
207 207 <i class="icon-more"></i>
208 208 ## </a>
209 209 </td>
210 210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
211 211 @@
212 212 -${hunk.source_start},${hunk.source_length}
213 213 +${hunk.target_start},${hunk.target_length}
214 214 ${hunk.section_header}
215 215 </td>
216 216 </tr>
217 217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
218 218 % endfor
219 219
220 220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
221 221
222 222 ## outdated comments that do not fit into currently displayed lines
223 223 % for lineno, comments in unmatched_comments.items():
224 224
225 225 %if c.user_session_attrs["diffmode"] == 'unified':
226 226 % if loop.index == 0:
227 227 <tr class="cb-hunk">
228 228 <td colspan="3"></td>
229 229 <td>
230 230 <div>
231 231 ${_('Unmatched inline comments below')}
232 232 </div>
233 233 </td>
234 234 </tr>
235 235 % endif
236 236 <tr class="cb-line">
237 237 <td class="cb-data cb-context"></td>
238 238 <td class="cb-lineno cb-context"></td>
239 239 <td class="cb-lineno cb-context"></td>
240 240 <td class="cb-content cb-context">
241 241 ${inline_comments_container(comments, inline_comments)}
242 242 </td>
243 243 </tr>
244 244 %elif c.user_session_attrs["diffmode"] == 'sideside':
245 245 % if loop.index == 0:
246 246 <tr class="cb-comment-info">
247 247 <td colspan="2"></td>
248 248 <td class="cb-line">
249 249 <div>
250 250 ${_('Unmatched inline comments below')}
251 251 </div>
252 252 </td>
253 253 <td colspan="2"></td>
254 254 <td class="cb-line">
255 255 <div>
256 256 ${_('Unmatched comments below')}
257 257 </div>
258 258 </td>
259 259 </tr>
260 260 % endif
261 261 <tr class="cb-line">
262 262 <td class="cb-data cb-context"></td>
263 263 <td class="cb-lineno cb-context"></td>
264 264 <td class="cb-content cb-context">
265 265 % if lineno.startswith('o'):
266 266 ${inline_comments_container(comments, inline_comments)}
267 267 % endif
268 268 </td>
269 269
270 270 <td class="cb-data cb-context"></td>
271 271 <td class="cb-lineno cb-context"></td>
272 272 <td class="cb-content cb-context">
273 273 % if lineno.startswith('n'):
274 274 ${inline_comments_container(comments, inline_comments)}
275 275 % endif
276 276 </td>
277 277 </tr>
278 278 %endif
279 279
280 280 % endfor
281 281
282 282 </table>
283 283 </div>
284 284 %endfor
285 285
286 286 ## outdated comments that are made for a file that has been deleted
287 287 % for filename, comments_dict in (deleted_files_comments or {}).items():
288 288 <%
289 289 display_state = 'display: none'
290 290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
291 291 if open_comments_in_file:
292 292 display_state = ''
293 293 %>
294 294 <div class="filediffs filediff-outdated" style="${display_state}">
295 295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
296 296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
297 297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
298 298 <div class="filediff-collapse-indicator"></div>
299 299 <span class="pill">
300 300 ## file was deleted
301 301 <strong>${filename}</strong>
302 302 </span>
303 303 <span class="pill-group" style="float: left">
304 304 ## file op, doesn't need translation
305 305 <span class="pill" op="removed">removed in this version</span>
306 306 </span>
307 307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
308 308 <span class="pill-group" style="float: right">
309 309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
310 310 </span>
311 311 </label>
312 312
313 313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
314 314 <tr>
315 315 % if c.user_session_attrs["diffmode"] == 'unified':
316 316 <td></td>
317 317 %endif
318 318
319 319 <td></td>
320 320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
321 321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
322 322 </td>
323 323 </tr>
324 324 %if c.user_session_attrs["diffmode"] == 'unified':
325 325 <tr class="cb-line">
326 326 <td class="cb-data cb-context"></td>
327 327 <td class="cb-lineno cb-context"></td>
328 328 <td class="cb-lineno cb-context"></td>
329 329 <td class="cb-content cb-context">
330 330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
331 331 </td>
332 332 </tr>
333 333 %elif c.user_session_attrs["diffmode"] == 'sideside':
334 334 <tr class="cb-line">
335 335 <td class="cb-data cb-context"></td>
336 336 <td class="cb-lineno cb-context"></td>
337 337 <td class="cb-content cb-context"></td>
338 338
339 339 <td class="cb-data cb-context"></td>
340 340 <td class="cb-lineno cb-context"></td>
341 341 <td class="cb-content cb-context">
342 342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
343 343 </td>
344 344 </tr>
345 345 %endif
346 346 </table>
347 347 </div>
348 348 </div>
349 349 % endfor
350 350
351 351 </div>
352 352 </div>
353 353 </%def>
354 354
355 355 <%def name="diff_ops(filediff)">
356 356 <%
357 357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
358 358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
359 359 %>
360 360 <span class="pill">
361 361 %if filediff.source_file_path and filediff.target_file_path:
362 362 %if filediff.source_file_path != filediff.target_file_path:
363 363 ## file was renamed, or copied
364 364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
365 365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
366 366 <% final_path = filediff.target_file_path %>
367 367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
368 368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
369 369 <% final_path = filediff.target_file_path %>
370 370 %endif
371 371 %else:
372 372 ## file was modified
373 373 <strong>${filediff.source_file_path}</strong>
374 374 <% final_path = filediff.source_file_path %>
375 375 %endif
376 376 %else:
377 377 %if filediff.source_file_path:
378 378 ## file was deleted
379 379 <strong>${filediff.source_file_path}</strong>
380 380 <% final_path = filediff.source_file_path %>
381 381 %else:
382 382 ## file was added
383 383 <strong>${filediff.target_file_path}</strong>
384 384 <% final_path = filediff.target_file_path %>
385 385 %endif
386 386 %endif
387 387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
388 388 </span>
389 389 ## anchor link
390 390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
391 391
392 392 <span class="pill-group" style="float: right">
393 393
394 394 ## ops pills
395 395 %if filediff.limited_diff:
396 396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
397 397 %endif
398 398
399 399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
400 400 <span class="pill" op="created">created</span>
401 401 %if filediff['target_mode'].startswith('120'):
402 402 <span class="pill" op="symlink">symlink</span>
403 403 %else:
404 404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
405 405 %endif
406 406 %endif
407 407
408 408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
409 409 <span class="pill" op="renamed">renamed</span>
410 410 %endif
411 411
412 412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
413 413 <span class="pill" op="copied">copied</span>
414 414 %endif
415 415
416 416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
417 417 <span class="pill" op="removed">removed</span>
418 418 %endif
419 419
420 420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
421 421 <span class="pill" op="mode">
422 422 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
423 423 </span>
424 424 %endif
425 425
426 426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
427 427 <span class="pill" op="binary">binary</span>
428 428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
429 429 <span class="pill" op="modified">modified</span>
430 430 %endif
431 431 %endif
432 432
433 433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
434 434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
435 435
436 436 </span>
437 437
438 438 </%def>
439 439
440 440 <%def name="nice_mode(filemode)">
441 441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
442 442 </%def>
443 443
444 444 <%def name="diff_menu(filediff, use_comments=False)">
445 445 <div class="filediff-menu">
446 446
447 447 %if filediff.diffset.source_ref:
448 448
449 449 ## FILE BEFORE CHANGES
450 450 %if filediff.operation in ['D', 'M']:
451 451 <a
452 452 class="tooltip"
453 453 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
454 454 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
455 455 >
456 456 ${_('Show file before')}
457 457 </a> |
458 458 %else:
459 459 <span
460 460 class="tooltip"
461 461 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
462 462 >
463 463 ${_('Show file before')}
464 464 </span> |
465 465 %endif
466 466
467 467 ## FILE AFTER CHANGES
468 468 %if filediff.operation in ['A', 'M']:
469 469 <a
470 470 class="tooltip"
471 471 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
472 472 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
473 473 >
474 474 ${_('Show file after')}
475 475 </a>
476 476 %else:
477 477 <span
478 478 class="tooltip"
479 479 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
480 480 >
481 481 ${_('Show file after')}
482 482 </span>
483 483 %endif
484 484
485 485 % if use_comments:
486 486 |
487 487 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
488 488 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
489 489 </a>
490 490 % endif
491 491
492 492 %endif
493 493
494 494 </div>
495 495 </%def>
496 496
497 497
498 498 <%def name="inline_comments_container(comments, inline_comments)">
499 499 <div class="inline-comments">
500 500 %for comment in comments:
501 501 ${commentblock.comment_block(comment, inline=True)}
502 502 %endfor
503 503 % if comments and comments[-1].outdated:
504 504 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
505 505 style="display: none;}">
506 506 ${_('Add another comment')}
507 507 </span>
508 508 % else:
509 509 <span onclick="return Rhodecode.comments.createComment(this)"
510 510 class="btn btn-secondary cb-comment-add-button">
511 511 ${_('Add another comment')}
512 512 </span>
513 513 % endif
514 514
515 515 </div>
516 516 </%def>
517 517
518 518 <%!
519 519 def get_comments_for(diff_type, comments, filename, line_version, line_number):
520 520 if hasattr(filename, 'unicode_path'):
521 521 filename = filename.unicode_path
522 522
523 523 if not isinstance(filename, (unicode, str)):
524 524 return None
525 525
526 526 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
527 527
528 528 if comments and filename in comments:
529 529 file_comments = comments[filename]
530 530 if line_key in file_comments:
531 531 data = file_comments.pop(line_key)
532 532 return data
533 533 %>
534 534
535 535 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
536 536 %for i, line in enumerate(hunk.sideside):
537 537 <%
538 538 old_line_anchor, new_line_anchor = None, None
539 539
540 540 if line.original.lineno:
541 541 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
542 542 if line.modified.lineno:
543 543 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
544 544 %>
545 545
546 546 <tr class="cb-line">
547 547 <td class="cb-data ${action_class(line.original.action)}"
548 548 data-line-no="${line.original.lineno}"
549 549 >
550 550 <div>
551 551
552 552 <% line_old_comments = None %>
553 553 %if line.original.get_comment_args:
554 554 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
555 555 %endif
556 556 %if line_old_comments:
557 557 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
558 558 % if has_outdated:
559 559 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
560 560 % else:
561 561 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
562 562 % endif
563 563 %endif
564 564 </div>
565 565 </td>
566 566 <td class="cb-lineno ${action_class(line.original.action)}"
567 567 data-line-no="${line.original.lineno}"
568 568 %if old_line_anchor:
569 569 id="${old_line_anchor}"
570 570 %endif
571 571 >
572 572 %if line.original.lineno:
573 573 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
574 574 %endif
575 575 </td>
576 576 <td class="cb-content ${action_class(line.original.action)}"
577 577 data-line-no="o${line.original.lineno}"
578 578 >
579 579 %if use_comments and line.original.lineno:
580 580 ${render_add_comment_button()}
581 581 %endif
582 582 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
583 583
584 584 %if use_comments and line.original.lineno and line_old_comments:
585 585 ${inline_comments_container(line_old_comments, inline_comments)}
586 586 %endif
587 587
588 588 </td>
589 589 <td class="cb-data ${action_class(line.modified.action)}"
590 590 data-line-no="${line.modified.lineno}"
591 591 >
592 592 <div>
593 593
594 594 %if line.modified.get_comment_args:
595 595 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
596 596 %else:
597 597 <% line_new_comments = None%>
598 598 %endif
599 599 %if line_new_comments:
600 600 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
601 601 % if has_outdated:
602 602 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
603 603 % else:
604 604 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
605 605 % endif
606 606 %endif
607 607 </div>
608 608 </td>
609 609 <td class="cb-lineno ${action_class(line.modified.action)}"
610 610 data-line-no="${line.modified.lineno}"
611 611 %if new_line_anchor:
612 612 id="${new_line_anchor}"
613 613 %endif
614 614 >
615 615 %if line.modified.lineno:
616 616 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
617 617 %endif
618 618 </td>
619 619 <td class="cb-content ${action_class(line.modified.action)}"
620 620 data-line-no="n${line.modified.lineno}"
621 621 >
622 622 %if use_comments and line.modified.lineno:
623 623 ${render_add_comment_button()}
624 624 %endif
625 625 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
626 626 %if use_comments and line.modified.lineno and line_new_comments:
627 627 ${inline_comments_container(line_new_comments, inline_comments)}
628 628 %endif
629 629 </td>
630 630 </tr>
631 631 %endfor
632 632 </%def>
633 633
634 634
635 635 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
636 636 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
637 637
638 638 <%
639 639 old_line_anchor, new_line_anchor = None, None
640 640 if old_line_no:
641 641 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
642 642 if new_line_no:
643 643 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
644 644 %>
645 645 <tr class="cb-line">
646 646 <td class="cb-data ${action_class(action)}">
647 647 <div>
648 648
649 649 %if comments_args:
650 650 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
651 651 %else:
652 652 <% comments = None %>
653 653 %endif
654 654
655 655 % if comments:
656 656 <% has_outdated = any([x.outdated for x in comments]) %>
657 657 % if has_outdated:
658 658 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
659 659 % else:
660 660 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
661 661 % endif
662 662 % endif
663 663 </div>
664 664 </td>
665 665 <td class="cb-lineno ${action_class(action)}"
666 666 data-line-no="${old_line_no}"
667 667 %if old_line_anchor:
668 668 id="${old_line_anchor}"
669 669 %endif
670 670 >
671 671 %if old_line_anchor:
672 672 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
673 673 %endif
674 674 </td>
675 675 <td class="cb-lineno ${action_class(action)}"
676 676 data-line-no="${new_line_no}"
677 677 %if new_line_anchor:
678 678 id="${new_line_anchor}"
679 679 %endif
680 680 >
681 681 %if new_line_anchor:
682 682 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
683 683 %endif
684 684 </td>
685 685 <td class="cb-content ${action_class(action)}"
686 686 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
687 687 >
688 688 %if use_comments:
689 689 ${render_add_comment_button()}
690 690 %endif
691 691 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
692 692 %if use_comments and comments:
693 693 ${inline_comments_container(comments, inline_comments)}
694 694 %endif
695 695 </td>
696 696 </tr>
697 697 %endfor
698 698 </%def>
699 699
700 700
701 701 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
702 702 % if diff_mode == 'unified':
703 703 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
704 704 % elif diff_mode == 'sideside':
705 705 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
706 706 % else:
707 707 <tr class="cb-line">
708 708 <td>unknown diff mode</td>
709 709 </tr>
710 710 % endif
711 711 </%def>file changes
712 712
713 713
714 714 <%def name="render_add_comment_button()">
715 715 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
716 716 <span><i class="icon-comment"></i></span>
717 717 </button>
718 718 </%def>
719 719
720 720 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
721 721
722 722 <div id="diff-file-sticky" class="diffset-menu clearinner">
723 723 ## auto adjustable
724 724 <div class="sidebar__inner">
725 725 <div class="sidebar__bar">
726 726 <div class="pull-right">
727 727 <div class="btn-group">
728 728
729 729 ## DIFF OPTIONS via Select2
730 730 <div class="pull-left">
731 731 ${h.hidden('diff_menu')}
732 732 </div>
733 733
734 734 <a
735 735 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
736 736 title="${h.tooltip(_('View side by side'))}"
737 737 href="${h.current_route_path(request, diffmode='sideside')}">
738 738 <span>${_('Side by Side')}</span>
739 739 </a>
740 740
741 741 <a
742 742 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
743 743 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
744 744 <span>${_('Unified')}</span>
745 745 </a>
746 746
747 747 % if range_diff_on is True:
748 748 <a
749 749 title="${_('Turn off: Show the diff as commit range')}"
750 750 class="btn btn-primary"
751 751 href="${h.current_route_path(request, **{"range-diff":"0"})}">
752 752 <span>${_('Range Diff')}</span>
753 753 </a>
754 754 % elif range_diff_on is False:
755 755 <a
756 756 title="${_('Show the diff as commit range')}"
757 757 class="btn"
758 758 href="${h.current_route_path(request, **{"range-diff":"1"})}">
759 759 <span>${_('Range Diff')}</span>
760 760 </a>
761 761 % endif
762 762 </div>
763 763 </div>
764 764 <div class="pull-left">
765 765 <div class="btn-group">
766 766 <div class="pull-left">
767 767 ${h.hidden('file_filter')}
768 768 </div>
769 769 <a
770 770 class="btn"
771 771 href="#"
772 772 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
773 773 <a
774 774 class="btn"
775 775 href="#"
776 776 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
777 777 </div>
778 778 </div>
779 779 </div>
780 780 <div class="fpath-placeholder">
781 781 <i class="icon-file-text"></i>
782 782 <strong class="fpath-placeholder-text">
783 783 Context file:
784 784 </strong>
785 785 </div>
786 786 <div class="sidebar_inner_shadow"></div>
787 787 </div>
788 788 </div>
789 789
790 790 % if diffset:
791 791
792 792 %if diffset.limited_diff:
793 793 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
794 794 %else:
795 795 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}%>
796 796 %endif
797 797 ## case on range-diff placeholder needs to be updated
798 798 % if range_diff_on is True:
799 799 <% file_placeholder = _('Disabled on range diff') %>
800 800 % endif
801 801
802 802 <script>
803 803
804 804 var feedFilesOptions = function (query, initialData) {
805 805 var data = {results: []};
806 806 var isQuery = typeof query.term !== 'undefined';
807 807
808 808 var section = _gettext('Changed files');
809 809 var filteredData = [];
810 810
811 811 //filter results
812 812 $.each(initialData.results, function (idx, value) {
813 813
814 814 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
815 815 filteredData.push({
816 816 'id': this.id,
817 817 'text': this.text,
818 818 "ops": this.ops,
819 819 })
820 820 }
821 821
822 822 });
823 823
824 824 data.results = filteredData;
825 825
826 826 query.callback(data);
827 827 };
828 828
829 829 var formatFileResult = function(result, container, query, escapeMarkup) {
830 830 return function(data, escapeMarkup) {
831 831 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
832 832 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
833 833 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
834 834 '<span class="pill" op="added">{0}</span>' +
835 835 '<span class="pill" op="deleted">{1}</span>' +
836 836 '</span>'
837 837 ;
838 838 var added = data['ops']['added'];
839 839 if (added === 0) {
840 840 // don't show +0
841 841 added = 0;
842 842 } else {
843 843 added = '+' + added;
844 844 }
845 845
846 846 var deleted = -1*data['ops']['deleted'];
847 847
848 848 tmpl += pill.format(added, deleted);
849 849 return container.format(tmpl);
850 850
851 851 }(result, escapeMarkup);
852 852 };
853 853
854 854 var preloadFileFilterData = {
855 855 results: [
856 856 % for filediff in diffset.files:
857 857 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
858 858 text:"${filediff.patch['filename']}",
859 859 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
860 860 % endfor
861 861 ]
862 862 };
863 863
864 864 $(document).ready(function () {
865 865
866 866 var fileFilter = $("#file_filter").select2({
867 867 'dropdownAutoWidth': true,
868 868 'width': 'auto',
869 869 'placeholder': "${file_placeholder}",
870 870 containerCssClass: "drop-menu",
871 871 dropdownCssClass: "drop-menu-dropdown",
872 872 data: preloadFileFilterData,
873 873 query: function(query) {
874 874 feedFilesOptions(query, preloadFileFilterData);
875 875 },
876 876 formatResult: formatFileResult
877 877 });
878 878
879 879 % if range_diff_on is True:
880 880 fileFilter.select2("enable", false);
881 881 % endif
882 882
883 883 $("#file_filter").on('click', function (e) {
884 884 e.preventDefault();
885 885 var selected = $('#file_filter').select2('data');
886 886 var idSelector = "#"+selected.id;
887 887 window.location.hash = idSelector;
888 888 // expand the container if we quick-select the field
889 889 $(idSelector).next().prop('checked', false);
890 890 updateSticky()
891 891 });
892 892
893 893 var contextPrefix = _gettext('Context file: ');
894 894 ## sticky sidebar
895 895 var sidebarElement = document.getElementById('diff-file-sticky');
896 896 sidebar = new StickySidebar(sidebarElement, {
897 897 topSpacing: 0,
898 898 bottomSpacing: 0,
899 899 innerWrapperSelector: '.sidebar__inner'
900 900 });
901 901 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
902 902 // reset our file so it's not holding new value
903 903 $('.fpath-placeholder-text').html(contextPrefix)
904 904 });
905 905
906 906 updateSticky = function () {
907 907 sidebar.updateSticky();
908 908 Waypoint.refreshAll();
909 909 };
910 910
911 911 var animateText = $.debounce(100, function(fPath, anchorId) {
912 912 fPath = Select2.util.escapeMarkup(fPath);
913 913
914 914 // animate setting the text
915 915 var callback = function () {
916 916 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
917 917 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
918 918 };
919 919 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
920 920 });
921 921
922 922 ## dynamic file waypoints
923 923 var setFPathInfo = function(fPath, anchorId){
924 924 animateText(fPath, anchorId)
925 925 };
926 926
927 927 var codeBlock = $('.filediff');
928 928 // forward waypoint
929 929 codeBlock.waypoint(
930 930 function(direction) {
931 931 if (direction === "down"){
932 932 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
933 933 }
934 934 }, {
935 935 offset: 70,
936 936 context: '.fpath-placeholder'
937 937 }
938 938 );
939 939
940 940 // backward waypoint
941 941 codeBlock.waypoint(
942 942 function(direction) {
943 943 if (direction === "up"){
944 944 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
945 945 }
946 946 }, {
947 947 offset: function () {
948 948 return -this.element.clientHeight + 90
949 949 },
950 950 context: '.fpath-placeholder'
951 951 }
952 952 );
953 953
954 954 var preloadDiffMenuData = {
955 955 results: [
956 956 ## Wide diff mode
957 957 {
958 958 id: 1,
959 959 text: _gettext('Toggle Wide Mode diff'),
960 960 action: function () {
961 961 updateSticky();
962 Rhodecode.comments.toggleWideMode(this);
962 var wide = Rhodecode.comments.toggleWideMode(this);
963 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
963 964 return null;
964 965 },
965 966 url: null,
966 967 },
967 968
968 969 ## Whitespace change
969 970 % if request.GET.get('ignorews', '') == '1':
970 971 {
971 972 id: 2,
972 973 text: _gettext('Show whitespace changes'),
973 974 action: function () {},
974 975 url: "${h.current_route_path(request, ignorews=0)|n}"
975 976 },
976 977 % else:
977 978 {
978 979 id: 2,
979 980 text: _gettext('Hide whitespace changes'),
980 981 action: function () {},
981 982 url: "${h.current_route_path(request, ignorews=1)|n}"
982 983 },
983 984 % endif
984 985
985 986 ## FULL CONTEXT
986 987 % if request.GET.get('fullcontext', '') == '1':
987 988 {
988 989 id: 3,
989 990 text: _gettext('Hide full context diff'),
990 991 action: function () {},
991 992 url: "${h.current_route_path(request, fullcontext=0)|n}"
992 993 },
993 994 % else:
994 995 {
995 996 id: 3,
996 997 text: _gettext('Show full context diff'),
997 998 action: function () {},
998 999 url: "${h.current_route_path(request, fullcontext=1)|n}"
999 1000 },
1000 1001 % endif
1001 1002
1002 1003 ]
1003 1004 };
1004 1005
1006 // get stored diff mode and pre-enable it
1007 if (templateContext.session_attrs.wide_diff_mode === "true") {
1008 Rhodecode.comments.toggleWideMode(null);
1009 }
1010
1005 1011 $("#diff_menu").select2({
1006 1012 minimumResultsForSearch: -1,
1007 1013 containerCssClass: "drop-menu",
1008 1014 dropdownCssClass: "drop-menu-dropdown",
1009 1015 dropdownAutoWidth: true,
1010 1016 data: preloadDiffMenuData,
1011 1017 placeholder: "${_('Diff Options')}",
1012 1018 });
1013 1019 $("#diff_menu").on('select2-selecting', function (e) {
1014 1020 e.choice.action();
1015 1021 if (e.choice.url !== null) {
1016 1022 window.location = e.choice.url
1017 1023 }
1018 1024 });
1019 1025
1020 1026 });
1021 1027
1022 1028 </script>
1023 1029 % endif
1024 1030
1025 1031 </%def>
General Comments 0
You need to be logged in to leave comments. Login now