Fossil SCM
/filepage no longer requires URL arguments: it has a leaf checkin/file selection list on the first tab.
Commit
e184992161fdf362b5c7ed17a42f1113a062187e32e8b45d9f977bd403158bd1
Parent
d0a83a38f5676db…
2 files changed
+167
-63
+128
-13
+167
-63
| --- src/fileedit.c | ||
| +++ src/fileedit.c | ||
| @@ -743,11 +743,11 @@ | ||
| 743 | 743 | ** Example: |
| 744 | 744 | ** |
| 745 | 745 | ** %fossil test-ci-mini -R REPO -m ... -r foo --as src/myfile.c myfile.c |
| 746 | 746 | ** |
| 747 | 747 | */ |
| 748 | -void test_ci_mini_cmd(){ | |
| 748 | +void test_ci_mini_cmd(void){ | |
| 749 | 749 | CheckinMiniInfo cimi; /* checkin state */ |
| 750 | 750 | int newRid = 0; /* RID of new version */ |
| 751 | 751 | const char * zFilename; /* argv[2] */ |
| 752 | 752 | const char * zComment; /* -m comment */ |
| 753 | 753 | const char * zCommentFile; /* -M FILE */ |
| @@ -1048,16 +1048,17 @@ | ||
| 1048 | 1048 | ** Performs bootstrapping common to the /fileedit_xyz AJAX routes. |
| 1049 | 1049 | ** Returns 0 if bootstrapping fails (wrong permissions), in which |
| 1050 | 1050 | ** case it has reported the error and the route should immediately |
| 1051 | 1051 | ** return. Returns true on success. |
| 1052 | 1052 | */ |
| 1053 | -static int fileedit_ajax_boostrap(){ | |
| 1053 | +static int fileedit_ajax_boostrap(void){ | |
| 1054 | 1054 | login_check_credentials(); |
| 1055 | 1055 | if( !g.perm.Write ){ |
| 1056 | 1056 | fileedit_ajax_error(403,"Write permissions required."); |
| 1057 | 1057 | return 0; |
| 1058 | 1058 | } |
| 1059 | + | |
| 1059 | 1060 | return 1; |
| 1060 | 1061 | } |
| 1061 | 1062 | /* |
| 1062 | 1063 | ** Returns true if the current user is allowed to edit the given |
| 1063 | 1064 | ** filename, as determined by fileedit_is_editable(), else false, |
| @@ -1109,11 +1110,14 @@ | ||
| 1109 | 1110 | ** - *zRevUuid = the fully-expanded value of zRev (owned by the |
| 1110 | 1111 | ** caller). zRevUuid may be NULL. |
| 1111 | 1112 | ** |
| 1112 | 1113 | ** - *vid = the RID of zRevUuid. May not be NULL. |
| 1113 | 1114 | ** |
| 1114 | -** - *frid = the RID of zFilename's blob content. May not be NULL. | |
| 1115 | +** - *frid = the RID of zFilename's blob content. May not be NULL | |
| 1116 | +** unless zFilename is also NULL. If BOTH of zFilename and frid are | |
| 1117 | +** NULL then no confirmation is done on the filename argument - only | |
| 1118 | +** zRev is checked. | |
| 1115 | 1119 | ** |
| 1116 | 1120 | ** Returns 0 if the given file is not in the given checkin or if |
| 1117 | 1121 | ** fileedit_ajax_check_filename() fails, else returns true. If it |
| 1118 | 1122 | ** returns false, it queues up an error response and the caller must |
| 1119 | 1123 | ** return immediately. |
| @@ -1121,14 +1125,14 @@ | ||
| 1121 | 1125 | static int fileedit_ajax_setup_filerev(const char * zRev, |
| 1122 | 1126 | char ** zRevUuid, |
| 1123 | 1127 | int * vid, |
| 1124 | 1128 | const char * zFilename, |
| 1125 | 1129 | int * frid){ |
| 1126 | - char * zCi = 0; /* fully-resolved checkin UUID */ | |
| 1127 | 1130 | char * zFileUuid; /* file UUID */ |
| 1128 | - | |
| 1129 | - if(!fileedit_ajax_check_filename(zFilename)){ | |
| 1131 | + const int checkFile = zFilename!=0 || frid!=0; | |
| 1132 | + | |
| 1133 | + if(checkFile && !fileedit_ajax_check_filename(zFilename)){ | |
| 1130 | 1134 | return 0; |
| 1131 | 1135 | } |
| 1132 | 1136 | *vid = symbolic_name_to_rid(zRev, "ci"); |
| 1133 | 1137 | if(0==*vid){ |
| 1134 | 1138 | fileedit_ajax_error(404,"Cannot resolve name as a checkin: %s", |
| @@ -1137,22 +1141,26 @@ | ||
| 1137 | 1141 | }else if(*vid<0){ |
| 1138 | 1142 | fileedit_ajax_error(400,"Checkin name is ambiguous: %s", |
| 1139 | 1143 | zRev); |
| 1140 | 1144 | return 0; |
| 1141 | 1145 | } |
| 1142 | - zFileUuid = fileedit_file_uuid(zFilename, *vid, 0); | |
| 1143 | - if(zFileUuid==0){ | |
| 1144 | - fileedit_ajax_error(404,"Checkin does not contain file."); | |
| 1145 | - return 0; | |
| 1146 | - } | |
| 1147 | - zCi = rid_to_uuid(*vid); | |
| 1148 | - *frid = fast_uuid_to_rid(zFileUuid); | |
| 1149 | - fossil_free(zFileUuid); | |
| 1146 | + if(checkFile){ | |
| 1147 | + zFileUuid = fileedit_file_uuid(zFilename, *vid, 0); | |
| 1148 | + if(zFileUuid==0){ | |
| 1149 | + fileedit_ajax_error(404,"Checkin does not contain file."); | |
| 1150 | + return 0; | |
| 1151 | + } | |
| 1152 | + } | |
| 1150 | 1153 | if(zRevUuid!=0){ |
| 1151 | - *zRevUuid = zCi; | |
| 1152 | - }else{ | |
| 1153 | - fossil_free(zCi); | |
| 1154 | + *zRevUuid = rid_to_uuid(*vid); | |
| 1155 | + } | |
| 1156 | + if(checkFile){ | |
| 1157 | + assert(zFileUuid!=0); | |
| 1158 | + if(frid!=0){ | |
| 1159 | + *frid = fast_uuid_to_rid(zFileUuid); | |
| 1160 | + } | |
| 1161 | + fossil_free(zFileUuid); | |
| 1154 | 1162 | } |
| 1155 | 1163 | return 1; |
| 1156 | 1164 | } |
| 1157 | 1165 | |
| 1158 | 1166 | /* |
| @@ -1166,11 +1174,11 @@ | ||
| 1166 | 1174 | ** User must have Write access to use this page. |
| 1167 | 1175 | ** |
| 1168 | 1176 | ** Responds with the raw content of the given page. On error it |
| 1169 | 1177 | ** produces a JSON response as documented for fileedit_ajax_error(). |
| 1170 | 1178 | */ |
| 1171 | -void fileedit_ajax_content(){ | |
| 1179 | +void fileedit_ajax_content(void){ | |
| 1172 | 1180 | const char * zFilename = 0; |
| 1173 | 1181 | const char * zRev = 0; |
| 1174 | 1182 | int vid, frid; |
| 1175 | 1183 | Blob content = empty_blob; |
| 1176 | 1184 | const char * zMime; |
| @@ -1215,11 +1223,11 @@ | ||
| 1215 | 1223 | ** User must have Write access to use this page. |
| 1216 | 1224 | ** |
| 1217 | 1225 | ** Responds with the HTML content of the preview. On error it produces |
| 1218 | 1226 | ** a JSON response as documented for fileedit_ajax_error(). |
| 1219 | 1227 | */ |
| 1220 | -void fileedit_ajax_preview(){ | |
| 1228 | +void fileedit_ajax_preview(void){ | |
| 1221 | 1229 | const char * zFilename = 0; |
| 1222 | 1230 | const char * zContent = P("content"); |
| 1223 | 1231 | int renderMode = atoi(PD("render_mode","0")); |
| 1224 | 1232 | int ln = atoi(PD("ln","0")); |
| 1225 | 1233 | int iframeHeight = atoi(PD("iframe_height","40")); |
| @@ -1253,11 +1261,11 @@ | ||
| 1253 | 1261 | ** User must have Write access to use this page. |
| 1254 | 1262 | ** |
| 1255 | 1263 | ** Responds with the HTML content of the diff. On error it produces a |
| 1256 | 1264 | ** JSON response as documented for fileedit_ajax_error(). |
| 1257 | 1265 | */ |
| 1258 | -void fileedit_ajax_diff(){ | |
| 1266 | +void fileedit_ajax_diff(void){ | |
| 1259 | 1267 | /* |
| 1260 | 1268 | ** Reminder: we only need the filename to perform valdiation |
| 1261 | 1269 | ** against fileedit_is_editable(), else this route could be |
| 1262 | 1270 | ** abused to get diffs against content disallowed by the |
| 1263 | 1271 | ** whitelist. |
| @@ -1410,11 +1418,99 @@ | ||
| 1410 | 1418 | fossil_free(zFileUuid); |
| 1411 | 1419 | return rc ? rc : 500; |
| 1412 | 1420 | } |
| 1413 | 1421 | |
| 1414 | 1422 | /* |
| 1415 | -** WEBPAGE: fileedit_commit | |
| 1423 | +** WEBPAGE: fileedit_filelist | |
| 1424 | +** | |
| 1425 | +** Fetches a JSON-format list of leaves and/or filenames for use in | |
| 1426 | +** creating a file selection list in /fileedit. It has different modes | |
| 1427 | +** of operation depending on its arguments: | |
| 1428 | +** | |
| 1429 | +** 'leaves': just fetch a list of open leaf versions, in this | |
| 1430 | +** format: | |
| 1431 | +** | |
| 1432 | +** [ | |
| 1433 | +** {checkin: UUID, branch: branchName, timestamp: string} | |
| 1434 | +** ] | |
| 1435 | +** | |
| 1436 | +** The entries are ordered newest first. | |
| 1437 | +** | |
| 1438 | +** 'checkin=CHECKIN_NAME': fetch the current list of is-editable files | |
| 1439 | +** for the current user and given checkin name: | |
| 1440 | +** | |
| 1441 | +** { | |
| 1442 | +** checkin: UUID, | |
| 1443 | +** editableFiles: [ filename1, ... filenameN ] // sorted by name | |
| 1444 | +** } | |
| 1445 | +** | |
| 1446 | +** On error it produces a JSON response as documented for | |
| 1447 | +** fileedit_ajax_error(). | |
| 1448 | +*/ | |
| 1449 | +void fileedit_ajax_filelist(void){ | |
| 1450 | + const char * zCi = PD("checkin",P("ci")); | |
| 1451 | + Blob sql = empty_blob; | |
| 1452 | + Stmt q = empty_Stmt; | |
| 1453 | + int i = 0; | |
| 1454 | + | |
| 1455 | + if(!fileedit_ajax_boostrap()){ | |
| 1456 | + return; | |
| 1457 | + } | |
| 1458 | + cgi_set_content_type("application/json"); | |
| 1459 | + if(zCi!=0){ | |
| 1460 | + char * zCiFull = 0; | |
| 1461 | + int vid = 0; | |
| 1462 | + if(0==fileedit_ajax_setup_filerev(zCi, &zCiFull, &vid, 0, 0)){ | |
| 1463 | + /* Error already reported */ | |
| 1464 | + return; | |
| 1465 | + } | |
| 1466 | + CX("{\"checkin\":\"%j\"," | |
| 1467 | + "\"editableFiles\":[", zCiFull); | |
| 1468 | + blob_append_sql(&sql, "SELECT filename FROM files_of_checkin(%Q) " | |
| 1469 | + "ORDER BY filename %s", | |
| 1470 | + zCiFull, filename_collation()); | |
| 1471 | + db_prepare_blob(&q, &sql); | |
| 1472 | + while( SQLITE_ROW==db_step(&q) ){ | |
| 1473 | + const char * zFilename = db_column_text(&q, 0); | |
| 1474 | + if(fileedit_is_editable(zFilename)){ | |
| 1475 | + if(i++){ | |
| 1476 | + CX(","); | |
| 1477 | + } | |
| 1478 | + CX("\"%j\"", zFilename); | |
| 1479 | + } | |
| 1480 | + } | |
| 1481 | + db_finalize(&q); | |
| 1482 | + CX("]}"); | |
| 1483 | + }else if(P("leaves")!=0){ | |
| 1484 | + blob_append(&sql, timeline_query_for_tty(), -1); | |
| 1485 | + blob_append_sql(&sql, " AND blob.rid IN (SElECT rid FROM leaf " | |
| 1486 | + "WHERE NOT EXISTS(" | |
| 1487 | + "SELECT 1 from tagxref WHERE tagid=%d AND " | |
| 1488 | + "tagtype>0 AND rid=leaf.rid" | |
| 1489 | + ")) " | |
| 1490 | + "ORDER BY mtime DESC", TAG_CLOSED); | |
| 1491 | + db_prepare_blob(&q, &sql); | |
| 1492 | + CX("["); | |
| 1493 | + while( SQLITE_ROW==db_step(&q) ){ | |
| 1494 | + if(i++){ | |
| 1495 | + CX(","); | |
| 1496 | + } | |
| 1497 | + CX("{"); | |
| 1498 | + CX("\"checkin\":\"%j\",", db_column_text(&q, 1)); | |
| 1499 | + CX("\"timestamp\":\"%j\",", db_column_text(&q, 2)); | |
| 1500 | + CX("\"branch\":\"%j\"", db_column_text(&q, 7)); | |
| 1501 | + CX("}"); | |
| 1502 | + } | |
| 1503 | + CX("]"); | |
| 1504 | + db_finalize(&q); | |
| 1505 | + }else{ | |
| 1506 | + fileedit_ajax_error(500, "Unhandled URL argument."); | |
| 1507 | + } | |
| 1508 | +} | |
| 1509 | + | |
| 1510 | +/* | |
| 1511 | +** WEBPAGE: fileedit_commit ajax | |
| 1416 | 1512 | ** |
| 1417 | 1513 | ** Required query parameters: |
| 1418 | 1514 | ** |
| 1419 | 1515 | ** filename=FILENAME |
| 1420 | 1516 | ** checkin=Parent checkin UUID |
| @@ -1438,11 +1534,11 @@ | ||
| 1438 | 1534 | ** } |
| 1439 | 1535 | ** |
| 1440 | 1536 | ** On error it produces a JSON response as documented for |
| 1441 | 1537 | ** fileedit_ajax_error(). |
| 1442 | 1538 | */ |
| 1443 | -void fileedit_ajax_commit(){ | |
| 1539 | +void fileedit_ajax_commit(void){ | |
| 1444 | 1540 | Blob err = empty_blob; /* Error messages */ |
| 1445 | 1541 | Blob manifest = empty_blob; /* raw new manifest */ |
| 1446 | 1542 | CheckinMiniInfo cimi; /* checkin state */ |
| 1447 | 1543 | int rc; /* generic result code */ |
| 1448 | 1544 | int newVid = 0; /* new version's RID */ |
| @@ -1500,15 +1596,15 @@ | ||
| 1500 | 1596 | ** |
| 1501 | 1597 | ** All other parameters are for internal use only, submitted via the |
| 1502 | 1598 | ** form-submission process, and may change with any given revision of |
| 1503 | 1599 | ** this code. |
| 1504 | 1600 | */ |
| 1505 | -void fileedit_page(){ | |
| 1506 | - const char * zFilename; /* filename. We'll accept 'name' | |
| 1601 | +void fileedit_page(void){ | |
| 1602 | + const char * zFilename = 0; /* filename. We'll accept 'name' | |
| 1507 | 1603 | because that param is handled |
| 1508 | 1604 | specially by the core. */ |
| 1509 | - const char * zRev; /* checkin version */ | |
| 1605 | + const char * zRev = 0; /* checkin version */ | |
| 1510 | 1606 | const char * zFileMime = 0; /* File mime type guess */ |
| 1511 | 1607 | CheckinMiniInfo cimi; /* Checkin state */ |
| 1512 | 1608 | int previewHtmlHeight = 0; /* iframe height (EMs) */ |
| 1513 | 1609 | int previewRenderMode = FE_RENDER_GUESS; /* preview mode */ |
| 1514 | 1610 | Blob err = empty_blob; /* Error report */ |
| @@ -1531,18 +1627,17 @@ | ||
| 1531 | 1627 | style_header("File Editor"); |
| 1532 | 1628 | /* As of this point, don't use return or fossil_fatal(). Write any |
| 1533 | 1629 | ** error in (&err) and goto end_footer instead so that we can be |
| 1534 | 1630 | ** sure to do any cleanup and end the transaction cleanly. |
| 1535 | 1631 | */ |
| 1536 | - if(fileedit_setup_cimi_from_p(&cimi, &err)!=0){ | |
| 1537 | - goto end_footer; | |
| 1538 | - } | |
| 1539 | - zFilename = cimi.zFilename; | |
| 1540 | - zRev = cimi.zParentUuid; | |
| 1541 | - assert(zRev); | |
| 1542 | - assert(zFilename); | |
| 1543 | - zFileMime = mimetype_from_name(cimi.zFilename); | |
| 1632 | + if(fileedit_setup_cimi_from_p(&cimi, &err)==0){ | |
| 1633 | + zFilename = cimi.zFilename; | |
| 1634 | + zRev = cimi.zParentUuid; | |
| 1635 | + assert(zRev); | |
| 1636 | + assert(zFilename); | |
| 1637 | + zFileMime = mimetype_from_name(cimi.zFilename); | |
| 1638 | + } | |
| 1544 | 1639 | |
| 1545 | 1640 | /******************************************************************** |
| 1546 | 1641 | ** All errors which "could" have happened up to this point are of a |
| 1547 | 1642 | ** degree which keep us from rendering the rest of the page, and |
| 1548 | 1643 | ** thus have already caused us to skipped to the end of the page to |
| @@ -1564,16 +1659,10 @@ | ||
| 1564 | 1659 | } |
| 1565 | 1660 | CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. " |
| 1566 | 1661 | "USE AT YOUR OWN RISK, preferably on a test " |
| 1567 | 1662 | "repo.</p>\n"); |
| 1568 | 1663 | |
| 1569 | - /******* Hidden fields *******/ | |
| 1570 | - CX("<input type='hidden' name='checkin' value='%s'>", | |
| 1571 | - cimi.zParentUuid); | |
| 1572 | - CX("<input type='hidden' name='filename' value='%T'>", | |
| 1573 | - zFilename); | |
| 1574 | - | |
| 1575 | 1664 | /* Status bar */ |
| 1576 | 1665 | CX("<div id='fossil-status-bar'>Async. status messages will go " |
| 1577 | 1666 | "here.</div>\n"/* will be moved into the tab container via JS */); |
| 1578 | 1667 | |
| 1579 | 1668 | /* Main tab container... */ |
| @@ -1581,26 +1670,27 @@ | ||
| 1581 | 1670 | |
| 1582 | 1671 | /***** File/version info tab *****/ |
| 1583 | 1672 | { |
| 1584 | 1673 | CX("<div id='fileedit-tab-version' " |
| 1585 | 1674 | "data-tab-parent='fileedit-tabs' " |
| 1586 | - "data-tab-label='Version Info'" | |
| 1675 | + "data-tab-label='File Info & Selection'" | |
| 1587 | 1676 | ">"); |
| 1588 | 1677 | CX("File: " |
| 1589 | - "<code id='finfo-file-name'>(loading)</code><br>"); | |
| 1678 | + "<code id='finfo-file-name'>" "???" "</code><br>"); | |
| 1590 | 1679 | CX("Checkin Version: " |
| 1591 | 1680 | "[<a id='timeline-link' href='#'>/timeline</a>] " |
| 1592 | 1681 | "[<a id='r-link' href='#'>/info</a>] " |
| 1593 | 1682 | /* %R/info/%!S */ |
| 1594 | - "<code id='r-label'>(loading...)</code><br>" | |
| 1683 | + "<code id='r-label'>" "???" "</code><br>" | |
| 1595 | 1684 | ); |
| 1596 | 1685 | CX("Permalink: <code>" |
| 1597 | - "<a id='permalink' href='#'>(loading...)</a></code><br>" | |
| 1686 | + "<a id='permalink' href='#'>" "???" "</a></code><br>" | |
| 1598 | 1687 | "(Clicking the permalink will reload the page and discard " |
| 1599 | - "all edits!)", | |
| 1600 | - zFilename, cimi.zParentUuid, | |
| 1601 | - zFilename, cimi.zParentUuid); | |
| 1688 | + "all edits!)"); | |
| 1689 | + | |
| 1690 | + CX("<h1>Select a file to edit:</h1>"); | |
| 1691 | + CX("<div id='fileedit-file-selector'></div>"); | |
| 1602 | 1692 | CX("</div>"/*#fileedit-tab-version*/); |
| 1603 | 1693 | } |
| 1604 | 1694 | |
| 1605 | 1695 | /******* Content tab *******/ |
| 1606 | 1696 | { |
| @@ -1664,11 +1754,13 @@ | ||
| 1664 | 1754 | "1", 1, |
| 1665 | 1755 | "If on, the preview will automatically " |
| 1666 | 1756 | "refresh when this tab is selected."); |
| 1667 | 1757 | |
| 1668 | 1758 | /* Default preview rendering mode selection... */ |
| 1669 | - previewRenderMode = fileedit_render_mode_for_mimetype(zFileMime); | |
| 1759 | + previewRenderMode = zFileMime | |
| 1760 | + ? fileedit_render_mode_for_mimetype(zFileMime) | |
| 1761 | + : FE_RENDER_GUESS; | |
| 1670 | 1762 | style_select_list_int("select-preview-mode", |
| 1671 | 1763 | "preview_render_mode", |
| 1672 | 1764 | "Preview Mode", |
| 1673 | 1765 | "Preview mode format.", |
| 1674 | 1766 | previewRenderMode, |
| @@ -1692,14 +1784,11 @@ | ||
| 1692 | 1784 | FE_RENDER_WIKI, FE_RENDER_WIKI, |
| 1693 | 1785 | FE_RENDER_HTML_IFRAME, FE_RENDER_HTML_IFRAME, |
| 1694 | 1786 | FE_RENDER_HTML_INLINE, FE_RENDER_HTML_INLINE, |
| 1695 | 1787 | FE_RENDER_PLAIN_TEXT, FE_RENDER_PLAIN_TEXT); |
| 1696 | 1788 | /* Allow selection of HTML preview iframe height */ |
| 1697 | - previewHtmlHeight = atoi(PD("preview_html_ems","0")); | |
| 1698 | - if(!previewHtmlHeight){ | |
| 1699 | - previewHtmlHeight = 40; | |
| 1700 | - } | |
| 1789 | + previewHtmlHeight = 40; | |
| 1701 | 1790 | style_select_list_int("select-preview-html-ems", |
| 1702 | 1791 | "preview_html_ems", |
| 1703 | 1792 | "HTML Preview IFrame Height (EMs)", |
| 1704 | 1793 | "Height (in EMs) of the iframe used for " |
| 1705 | 1794 | "HTML preview", |
| @@ -1846,25 +1935,40 @@ | ||
| 1846 | 1935 | CX("<div id='fileedit-manifest'></div>\n" |
| 1847 | 1936 | /* Manifest gets rendered here after a commit. */); |
| 1848 | 1937 | } |
| 1849 | 1938 | |
| 1850 | 1939 | CX("</div>"/*#fileedit-tab-commit*/); |
| 1851 | - | |
| 1852 | - /* Dynamically populate the editor... */ | |
| 1853 | - blob_appendf(&endScript, | |
| 1854 | - "window.addEventListener('load'," | |
| 1855 | - "()=>fossil.page.loadFile('%j','%j'), false);\n", | |
| 1856 | - zFilename, cimi.zParentUuid); | |
| 1857 | - | |
| 1858 | -end_footer: | |
| 1940 | + | |
| 1941 | + { | |
| 1942 | + /* Dynamically populate the editor or display a warning | |
| 1943 | + ** about having no file loaded... */ | |
| 1944 | + blob_appendf(&endScript, | |
| 1945 | + "window.addEventListener('load',"); | |
| 1946 | + if(zRev && zFilename){ | |
| 1947 | + assert(0==blob_size(&err)); | |
| 1948 | + blob_appendf(&endScript, | |
| 1949 | + "()=>fossil.page.loadFile(\"%j\",'%j')", | |
| 1950 | + zFilename, cimi.zParentUuid); | |
| 1951 | + }else{ | |
| 1952 | + blob_appendf(&endScript,"function(){"); | |
| 1953 | + if(blob_size(&err)>0){ | |
| 1954 | + blob_appendf(&endScript, | |
| 1955 | + "fossil.error(\"%j\");\n" | |
| 1956 | + "fossil.page.tabs.switchToTab(0);\n", | |
| 1957 | + blob_str(&err)); | |
| 1958 | + }else{ | |
| 1959 | + blob_appendf(&endScript, | |
| 1960 | + "fossil.error('No file/version selected.')"); | |
| 1961 | + } | |
| 1962 | + blob_appendf(&endScript,"}"); | |
| 1963 | + } | |
| 1964 | + blob_appendf(&endScript,", false);\n"); | |
| 1965 | + } | |
| 1966 | + | |
| 1859 | 1967 | if(stmt.pStmt){ |
| 1860 | 1968 | db_finalize(&stmt); |
| 1861 | 1969 | } |
| 1862 | - if(blob_size(&err)){ | |
| 1863 | - CX("<div class='fileedit-error-report'>%s</div>", | |
| 1864 | - blob_str(&err)); | |
| 1865 | - } | |
| 1866 | 1970 | blob_reset(&err); |
| 1867 | 1971 | CheckinMiniInfo_cleanup(&cimi); |
| 1868 | 1972 | style_emit_script_fossil_bootstrap(0); |
| 1869 | 1973 | style_emit_script_fetch(0); |
| 1870 | 1974 | style_emit_script_tabs(0); |
| 1871 | 1975 |
| --- src/fileedit.c | |
| +++ src/fileedit.c | |
| @@ -743,11 +743,11 @@ | |
| 743 | ** Example: |
| 744 | ** |
| 745 | ** %fossil test-ci-mini -R REPO -m ... -r foo --as src/myfile.c myfile.c |
| 746 | ** |
| 747 | */ |
| 748 | void test_ci_mini_cmd(){ |
| 749 | CheckinMiniInfo cimi; /* checkin state */ |
| 750 | int newRid = 0; /* RID of new version */ |
| 751 | const char * zFilename; /* argv[2] */ |
| 752 | const char * zComment; /* -m comment */ |
| 753 | const char * zCommentFile; /* -M FILE */ |
| @@ -1048,16 +1048,17 @@ | |
| 1048 | ** Performs bootstrapping common to the /fileedit_xyz AJAX routes. |
| 1049 | ** Returns 0 if bootstrapping fails (wrong permissions), in which |
| 1050 | ** case it has reported the error and the route should immediately |
| 1051 | ** return. Returns true on success. |
| 1052 | */ |
| 1053 | static int fileedit_ajax_boostrap(){ |
| 1054 | login_check_credentials(); |
| 1055 | if( !g.perm.Write ){ |
| 1056 | fileedit_ajax_error(403,"Write permissions required."); |
| 1057 | return 0; |
| 1058 | } |
| 1059 | return 1; |
| 1060 | } |
| 1061 | /* |
| 1062 | ** Returns true if the current user is allowed to edit the given |
| 1063 | ** filename, as determined by fileedit_is_editable(), else false, |
| @@ -1109,11 +1110,14 @@ | |
| 1109 | ** - *zRevUuid = the fully-expanded value of zRev (owned by the |
| 1110 | ** caller). zRevUuid may be NULL. |
| 1111 | ** |
| 1112 | ** - *vid = the RID of zRevUuid. May not be NULL. |
| 1113 | ** |
| 1114 | ** - *frid = the RID of zFilename's blob content. May not be NULL. |
| 1115 | ** |
| 1116 | ** Returns 0 if the given file is not in the given checkin or if |
| 1117 | ** fileedit_ajax_check_filename() fails, else returns true. If it |
| 1118 | ** returns false, it queues up an error response and the caller must |
| 1119 | ** return immediately. |
| @@ -1121,14 +1125,14 @@ | |
| 1121 | static int fileedit_ajax_setup_filerev(const char * zRev, |
| 1122 | char ** zRevUuid, |
| 1123 | int * vid, |
| 1124 | const char * zFilename, |
| 1125 | int * frid){ |
| 1126 | char * zCi = 0; /* fully-resolved checkin UUID */ |
| 1127 | char * zFileUuid; /* file UUID */ |
| 1128 | |
| 1129 | if(!fileedit_ajax_check_filename(zFilename)){ |
| 1130 | return 0; |
| 1131 | } |
| 1132 | *vid = symbolic_name_to_rid(zRev, "ci"); |
| 1133 | if(0==*vid){ |
| 1134 | fileedit_ajax_error(404,"Cannot resolve name as a checkin: %s", |
| @@ -1137,22 +1141,26 @@ | |
| 1137 | }else if(*vid<0){ |
| 1138 | fileedit_ajax_error(400,"Checkin name is ambiguous: %s", |
| 1139 | zRev); |
| 1140 | return 0; |
| 1141 | } |
| 1142 | zFileUuid = fileedit_file_uuid(zFilename, *vid, 0); |
| 1143 | if(zFileUuid==0){ |
| 1144 | fileedit_ajax_error(404,"Checkin does not contain file."); |
| 1145 | return 0; |
| 1146 | } |
| 1147 | zCi = rid_to_uuid(*vid); |
| 1148 | *frid = fast_uuid_to_rid(zFileUuid); |
| 1149 | fossil_free(zFileUuid); |
| 1150 | if(zRevUuid!=0){ |
| 1151 | *zRevUuid = zCi; |
| 1152 | }else{ |
| 1153 | fossil_free(zCi); |
| 1154 | } |
| 1155 | return 1; |
| 1156 | } |
| 1157 | |
| 1158 | /* |
| @@ -1166,11 +1174,11 @@ | |
| 1166 | ** User must have Write access to use this page. |
| 1167 | ** |
| 1168 | ** Responds with the raw content of the given page. On error it |
| 1169 | ** produces a JSON response as documented for fileedit_ajax_error(). |
| 1170 | */ |
| 1171 | void fileedit_ajax_content(){ |
| 1172 | const char * zFilename = 0; |
| 1173 | const char * zRev = 0; |
| 1174 | int vid, frid; |
| 1175 | Blob content = empty_blob; |
| 1176 | const char * zMime; |
| @@ -1215,11 +1223,11 @@ | |
| 1215 | ** User must have Write access to use this page. |
| 1216 | ** |
| 1217 | ** Responds with the HTML content of the preview. On error it produces |
| 1218 | ** a JSON response as documented for fileedit_ajax_error(). |
| 1219 | */ |
| 1220 | void fileedit_ajax_preview(){ |
| 1221 | const char * zFilename = 0; |
| 1222 | const char * zContent = P("content"); |
| 1223 | int renderMode = atoi(PD("render_mode","0")); |
| 1224 | int ln = atoi(PD("ln","0")); |
| 1225 | int iframeHeight = atoi(PD("iframe_height","40")); |
| @@ -1253,11 +1261,11 @@ | |
| 1253 | ** User must have Write access to use this page. |
| 1254 | ** |
| 1255 | ** Responds with the HTML content of the diff. On error it produces a |
| 1256 | ** JSON response as documented for fileedit_ajax_error(). |
| 1257 | */ |
| 1258 | void fileedit_ajax_diff(){ |
| 1259 | /* |
| 1260 | ** Reminder: we only need the filename to perform valdiation |
| 1261 | ** against fileedit_is_editable(), else this route could be |
| 1262 | ** abused to get diffs against content disallowed by the |
| 1263 | ** whitelist. |
| @@ -1410,11 +1418,99 @@ | |
| 1410 | fossil_free(zFileUuid); |
| 1411 | return rc ? rc : 500; |
| 1412 | } |
| 1413 | |
| 1414 | /* |
| 1415 | ** WEBPAGE: fileedit_commit |
| 1416 | ** |
| 1417 | ** Required query parameters: |
| 1418 | ** |
| 1419 | ** filename=FILENAME |
| 1420 | ** checkin=Parent checkin UUID |
| @@ -1438,11 +1534,11 @@ | |
| 1438 | ** } |
| 1439 | ** |
| 1440 | ** On error it produces a JSON response as documented for |
| 1441 | ** fileedit_ajax_error(). |
| 1442 | */ |
| 1443 | void fileedit_ajax_commit(){ |
| 1444 | Blob err = empty_blob; /* Error messages */ |
| 1445 | Blob manifest = empty_blob; /* raw new manifest */ |
| 1446 | CheckinMiniInfo cimi; /* checkin state */ |
| 1447 | int rc; /* generic result code */ |
| 1448 | int newVid = 0; /* new version's RID */ |
| @@ -1500,15 +1596,15 @@ | |
| 1500 | ** |
| 1501 | ** All other parameters are for internal use only, submitted via the |
| 1502 | ** form-submission process, and may change with any given revision of |
| 1503 | ** this code. |
| 1504 | */ |
| 1505 | void fileedit_page(){ |
| 1506 | const char * zFilename; /* filename. We'll accept 'name' |
| 1507 | because that param is handled |
| 1508 | specially by the core. */ |
| 1509 | const char * zRev; /* checkin version */ |
| 1510 | const char * zFileMime = 0; /* File mime type guess */ |
| 1511 | CheckinMiniInfo cimi; /* Checkin state */ |
| 1512 | int previewHtmlHeight = 0; /* iframe height (EMs) */ |
| 1513 | int previewRenderMode = FE_RENDER_GUESS; /* preview mode */ |
| 1514 | Blob err = empty_blob; /* Error report */ |
| @@ -1531,18 +1627,17 @@ | |
| 1531 | style_header("File Editor"); |
| 1532 | /* As of this point, don't use return or fossil_fatal(). Write any |
| 1533 | ** error in (&err) and goto end_footer instead so that we can be |
| 1534 | ** sure to do any cleanup and end the transaction cleanly. |
| 1535 | */ |
| 1536 | if(fileedit_setup_cimi_from_p(&cimi, &err)!=0){ |
| 1537 | goto end_footer; |
| 1538 | } |
| 1539 | zFilename = cimi.zFilename; |
| 1540 | zRev = cimi.zParentUuid; |
| 1541 | assert(zRev); |
| 1542 | assert(zFilename); |
| 1543 | zFileMime = mimetype_from_name(cimi.zFilename); |
| 1544 | |
| 1545 | /******************************************************************** |
| 1546 | ** All errors which "could" have happened up to this point are of a |
| 1547 | ** degree which keep us from rendering the rest of the page, and |
| 1548 | ** thus have already caused us to skipped to the end of the page to |
| @@ -1564,16 +1659,10 @@ | |
| 1564 | } |
| 1565 | CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. " |
| 1566 | "USE AT YOUR OWN RISK, preferably on a test " |
| 1567 | "repo.</p>\n"); |
| 1568 | |
| 1569 | /******* Hidden fields *******/ |
| 1570 | CX("<input type='hidden' name='checkin' value='%s'>", |
| 1571 | cimi.zParentUuid); |
| 1572 | CX("<input type='hidden' name='filename' value='%T'>", |
| 1573 | zFilename); |
| 1574 | |
| 1575 | /* Status bar */ |
| 1576 | CX("<div id='fossil-status-bar'>Async. status messages will go " |
| 1577 | "here.</div>\n"/* will be moved into the tab container via JS */); |
| 1578 | |
| 1579 | /* Main tab container... */ |
| @@ -1581,26 +1670,27 @@ | |
| 1581 | |
| 1582 | /***** File/version info tab *****/ |
| 1583 | { |
| 1584 | CX("<div id='fileedit-tab-version' " |
| 1585 | "data-tab-parent='fileedit-tabs' " |
| 1586 | "data-tab-label='Version Info'" |
| 1587 | ">"); |
| 1588 | CX("File: " |
| 1589 | "<code id='finfo-file-name'>(loading)</code><br>"); |
| 1590 | CX("Checkin Version: " |
| 1591 | "[<a id='timeline-link' href='#'>/timeline</a>] " |
| 1592 | "[<a id='r-link' href='#'>/info</a>] " |
| 1593 | /* %R/info/%!S */ |
| 1594 | "<code id='r-label'>(loading...)</code><br>" |
| 1595 | ); |
| 1596 | CX("Permalink: <code>" |
| 1597 | "<a id='permalink' href='#'>(loading...)</a></code><br>" |
| 1598 | "(Clicking the permalink will reload the page and discard " |
| 1599 | "all edits!)", |
| 1600 | zFilename, cimi.zParentUuid, |
| 1601 | zFilename, cimi.zParentUuid); |
| 1602 | CX("</div>"/*#fileedit-tab-version*/); |
| 1603 | } |
| 1604 | |
| 1605 | /******* Content tab *******/ |
| 1606 | { |
| @@ -1664,11 +1754,13 @@ | |
| 1664 | "1", 1, |
| 1665 | "If on, the preview will automatically " |
| 1666 | "refresh when this tab is selected."); |
| 1667 | |
| 1668 | /* Default preview rendering mode selection... */ |
| 1669 | previewRenderMode = fileedit_render_mode_for_mimetype(zFileMime); |
| 1670 | style_select_list_int("select-preview-mode", |
| 1671 | "preview_render_mode", |
| 1672 | "Preview Mode", |
| 1673 | "Preview mode format.", |
| 1674 | previewRenderMode, |
| @@ -1692,14 +1784,11 @@ | |
| 1692 | FE_RENDER_WIKI, FE_RENDER_WIKI, |
| 1693 | FE_RENDER_HTML_IFRAME, FE_RENDER_HTML_IFRAME, |
| 1694 | FE_RENDER_HTML_INLINE, FE_RENDER_HTML_INLINE, |
| 1695 | FE_RENDER_PLAIN_TEXT, FE_RENDER_PLAIN_TEXT); |
| 1696 | /* Allow selection of HTML preview iframe height */ |
| 1697 | previewHtmlHeight = atoi(PD("preview_html_ems","0")); |
| 1698 | if(!previewHtmlHeight){ |
| 1699 | previewHtmlHeight = 40; |
| 1700 | } |
| 1701 | style_select_list_int("select-preview-html-ems", |
| 1702 | "preview_html_ems", |
| 1703 | "HTML Preview IFrame Height (EMs)", |
| 1704 | "Height (in EMs) of the iframe used for " |
| 1705 | "HTML preview", |
| @@ -1846,25 +1935,40 @@ | |
| 1846 | CX("<div id='fileedit-manifest'></div>\n" |
| 1847 | /* Manifest gets rendered here after a commit. */); |
| 1848 | } |
| 1849 | |
| 1850 | CX("</div>"/*#fileedit-tab-commit*/); |
| 1851 | |
| 1852 | /* Dynamically populate the editor... */ |
| 1853 | blob_appendf(&endScript, |
| 1854 | "window.addEventListener('load'," |
| 1855 | "()=>fossil.page.loadFile('%j','%j'), false);\n", |
| 1856 | zFilename, cimi.zParentUuid); |
| 1857 | |
| 1858 | end_footer: |
| 1859 | if(stmt.pStmt){ |
| 1860 | db_finalize(&stmt); |
| 1861 | } |
| 1862 | if(blob_size(&err)){ |
| 1863 | CX("<div class='fileedit-error-report'>%s</div>", |
| 1864 | blob_str(&err)); |
| 1865 | } |
| 1866 | blob_reset(&err); |
| 1867 | CheckinMiniInfo_cleanup(&cimi); |
| 1868 | style_emit_script_fossil_bootstrap(0); |
| 1869 | style_emit_script_fetch(0); |
| 1870 | style_emit_script_tabs(0); |
| 1871 |
| --- src/fileedit.c | |
| +++ src/fileedit.c | |
| @@ -743,11 +743,11 @@ | |
| 743 | ** Example: |
| 744 | ** |
| 745 | ** %fossil test-ci-mini -R REPO -m ... -r foo --as src/myfile.c myfile.c |
| 746 | ** |
| 747 | */ |
| 748 | void test_ci_mini_cmd(void){ |
| 749 | CheckinMiniInfo cimi; /* checkin state */ |
| 750 | int newRid = 0; /* RID of new version */ |
| 751 | const char * zFilename; /* argv[2] */ |
| 752 | const char * zComment; /* -m comment */ |
| 753 | const char * zCommentFile; /* -M FILE */ |
| @@ -1048,16 +1048,17 @@ | |
| 1048 | ** Performs bootstrapping common to the /fileedit_xyz AJAX routes. |
| 1049 | ** Returns 0 if bootstrapping fails (wrong permissions), in which |
| 1050 | ** case it has reported the error and the route should immediately |
| 1051 | ** return. Returns true on success. |
| 1052 | */ |
| 1053 | static int fileedit_ajax_boostrap(void){ |
| 1054 | login_check_credentials(); |
| 1055 | if( !g.perm.Write ){ |
| 1056 | fileedit_ajax_error(403,"Write permissions required."); |
| 1057 | return 0; |
| 1058 | } |
| 1059 | |
| 1060 | return 1; |
| 1061 | } |
| 1062 | /* |
| 1063 | ** Returns true if the current user is allowed to edit the given |
| 1064 | ** filename, as determined by fileedit_is_editable(), else false, |
| @@ -1109,11 +1110,14 @@ | |
| 1110 | ** - *zRevUuid = the fully-expanded value of zRev (owned by the |
| 1111 | ** caller). zRevUuid may be NULL. |
| 1112 | ** |
| 1113 | ** - *vid = the RID of zRevUuid. May not be NULL. |
| 1114 | ** |
| 1115 | ** - *frid = the RID of zFilename's blob content. May not be NULL |
| 1116 | ** unless zFilename is also NULL. If BOTH of zFilename and frid are |
| 1117 | ** NULL then no confirmation is done on the filename argument - only |
| 1118 | ** zRev is checked. |
| 1119 | ** |
| 1120 | ** Returns 0 if the given file is not in the given checkin or if |
| 1121 | ** fileedit_ajax_check_filename() fails, else returns true. If it |
| 1122 | ** returns false, it queues up an error response and the caller must |
| 1123 | ** return immediately. |
| @@ -1121,14 +1125,14 @@ | |
| 1125 | static int fileedit_ajax_setup_filerev(const char * zRev, |
| 1126 | char ** zRevUuid, |
| 1127 | int * vid, |
| 1128 | const char * zFilename, |
| 1129 | int * frid){ |
| 1130 | char * zFileUuid; /* file UUID */ |
| 1131 | const int checkFile = zFilename!=0 || frid!=0; |
| 1132 | |
| 1133 | if(checkFile && !fileedit_ajax_check_filename(zFilename)){ |
| 1134 | return 0; |
| 1135 | } |
| 1136 | *vid = symbolic_name_to_rid(zRev, "ci"); |
| 1137 | if(0==*vid){ |
| 1138 | fileedit_ajax_error(404,"Cannot resolve name as a checkin: %s", |
| @@ -1137,22 +1141,26 @@ | |
| 1141 | }else if(*vid<0){ |
| 1142 | fileedit_ajax_error(400,"Checkin name is ambiguous: %s", |
| 1143 | zRev); |
| 1144 | return 0; |
| 1145 | } |
| 1146 | if(checkFile){ |
| 1147 | zFileUuid = fileedit_file_uuid(zFilename, *vid, 0); |
| 1148 | if(zFileUuid==0){ |
| 1149 | fileedit_ajax_error(404,"Checkin does not contain file."); |
| 1150 | return 0; |
| 1151 | } |
| 1152 | } |
| 1153 | if(zRevUuid!=0){ |
| 1154 | *zRevUuid = rid_to_uuid(*vid); |
| 1155 | } |
| 1156 | if(checkFile){ |
| 1157 | assert(zFileUuid!=0); |
| 1158 | if(frid!=0){ |
| 1159 | *frid = fast_uuid_to_rid(zFileUuid); |
| 1160 | } |
| 1161 | fossil_free(zFileUuid); |
| 1162 | } |
| 1163 | return 1; |
| 1164 | } |
| 1165 | |
| 1166 | /* |
| @@ -1166,11 +1174,11 @@ | |
| 1174 | ** User must have Write access to use this page. |
| 1175 | ** |
| 1176 | ** Responds with the raw content of the given page. On error it |
| 1177 | ** produces a JSON response as documented for fileedit_ajax_error(). |
| 1178 | */ |
| 1179 | void fileedit_ajax_content(void){ |
| 1180 | const char * zFilename = 0; |
| 1181 | const char * zRev = 0; |
| 1182 | int vid, frid; |
| 1183 | Blob content = empty_blob; |
| 1184 | const char * zMime; |
| @@ -1215,11 +1223,11 @@ | |
| 1223 | ** User must have Write access to use this page. |
| 1224 | ** |
| 1225 | ** Responds with the HTML content of the preview. On error it produces |
| 1226 | ** a JSON response as documented for fileedit_ajax_error(). |
| 1227 | */ |
| 1228 | void fileedit_ajax_preview(void){ |
| 1229 | const char * zFilename = 0; |
| 1230 | const char * zContent = P("content"); |
| 1231 | int renderMode = atoi(PD("render_mode","0")); |
| 1232 | int ln = atoi(PD("ln","0")); |
| 1233 | int iframeHeight = atoi(PD("iframe_height","40")); |
| @@ -1253,11 +1261,11 @@ | |
| 1261 | ** User must have Write access to use this page. |
| 1262 | ** |
| 1263 | ** Responds with the HTML content of the diff. On error it produces a |
| 1264 | ** JSON response as documented for fileedit_ajax_error(). |
| 1265 | */ |
| 1266 | void fileedit_ajax_diff(void){ |
| 1267 | /* |
| 1268 | ** Reminder: we only need the filename to perform valdiation |
| 1269 | ** against fileedit_is_editable(), else this route could be |
| 1270 | ** abused to get diffs against content disallowed by the |
| 1271 | ** whitelist. |
| @@ -1410,11 +1418,99 @@ | |
| 1418 | fossil_free(zFileUuid); |
| 1419 | return rc ? rc : 500; |
| 1420 | } |
| 1421 | |
| 1422 | /* |
| 1423 | ** WEBPAGE: fileedit_filelist |
| 1424 | ** |
| 1425 | ** Fetches a JSON-format list of leaves and/or filenames for use in |
| 1426 | ** creating a file selection list in /fileedit. It has different modes |
| 1427 | ** of operation depending on its arguments: |
| 1428 | ** |
| 1429 | ** 'leaves': just fetch a list of open leaf versions, in this |
| 1430 | ** format: |
| 1431 | ** |
| 1432 | ** [ |
| 1433 | ** {checkin: UUID, branch: branchName, timestamp: string} |
| 1434 | ** ] |
| 1435 | ** |
| 1436 | ** The entries are ordered newest first. |
| 1437 | ** |
| 1438 | ** 'checkin=CHECKIN_NAME': fetch the current list of is-editable files |
| 1439 | ** for the current user and given checkin name: |
| 1440 | ** |
| 1441 | ** { |
| 1442 | ** checkin: UUID, |
| 1443 | ** editableFiles: [ filename1, ... filenameN ] // sorted by name |
| 1444 | ** } |
| 1445 | ** |
| 1446 | ** On error it produces a JSON response as documented for |
| 1447 | ** fileedit_ajax_error(). |
| 1448 | */ |
| 1449 | void fileedit_ajax_filelist(void){ |
| 1450 | const char * zCi = PD("checkin",P("ci")); |
| 1451 | Blob sql = empty_blob; |
| 1452 | Stmt q = empty_Stmt; |
| 1453 | int i = 0; |
| 1454 | |
| 1455 | if(!fileedit_ajax_boostrap()){ |
| 1456 | return; |
| 1457 | } |
| 1458 | cgi_set_content_type("application/json"); |
| 1459 | if(zCi!=0){ |
| 1460 | char * zCiFull = 0; |
| 1461 | int vid = 0; |
| 1462 | if(0==fileedit_ajax_setup_filerev(zCi, &zCiFull, &vid, 0, 0)){ |
| 1463 | /* Error already reported */ |
| 1464 | return; |
| 1465 | } |
| 1466 | CX("{\"checkin\":\"%j\"," |
| 1467 | "\"editableFiles\":[", zCiFull); |
| 1468 | blob_append_sql(&sql, "SELECT filename FROM files_of_checkin(%Q) " |
| 1469 | "ORDER BY filename %s", |
| 1470 | zCiFull, filename_collation()); |
| 1471 | db_prepare_blob(&q, &sql); |
| 1472 | while( SQLITE_ROW==db_step(&q) ){ |
| 1473 | const char * zFilename = db_column_text(&q, 0); |
| 1474 | if(fileedit_is_editable(zFilename)){ |
| 1475 | if(i++){ |
| 1476 | CX(","); |
| 1477 | } |
| 1478 | CX("\"%j\"", zFilename); |
| 1479 | } |
| 1480 | } |
| 1481 | db_finalize(&q); |
| 1482 | CX("]}"); |
| 1483 | }else if(P("leaves")!=0){ |
| 1484 | blob_append(&sql, timeline_query_for_tty(), -1); |
| 1485 | blob_append_sql(&sql, " AND blob.rid IN (SElECT rid FROM leaf " |
| 1486 | "WHERE NOT EXISTS(" |
| 1487 | "SELECT 1 from tagxref WHERE tagid=%d AND " |
| 1488 | "tagtype>0 AND rid=leaf.rid" |
| 1489 | ")) " |
| 1490 | "ORDER BY mtime DESC", TAG_CLOSED); |
| 1491 | db_prepare_blob(&q, &sql); |
| 1492 | CX("["); |
| 1493 | while( SQLITE_ROW==db_step(&q) ){ |
| 1494 | if(i++){ |
| 1495 | CX(","); |
| 1496 | } |
| 1497 | CX("{"); |
| 1498 | CX("\"checkin\":\"%j\",", db_column_text(&q, 1)); |
| 1499 | CX("\"timestamp\":\"%j\",", db_column_text(&q, 2)); |
| 1500 | CX("\"branch\":\"%j\"", db_column_text(&q, 7)); |
| 1501 | CX("}"); |
| 1502 | } |
| 1503 | CX("]"); |
| 1504 | db_finalize(&q); |
| 1505 | }else{ |
| 1506 | fileedit_ajax_error(500, "Unhandled URL argument."); |
| 1507 | } |
| 1508 | } |
| 1509 | |
| 1510 | /* |
| 1511 | ** WEBPAGE: fileedit_commit ajax |
| 1512 | ** |
| 1513 | ** Required query parameters: |
| 1514 | ** |
| 1515 | ** filename=FILENAME |
| 1516 | ** checkin=Parent checkin UUID |
| @@ -1438,11 +1534,11 @@ | |
| 1534 | ** } |
| 1535 | ** |
| 1536 | ** On error it produces a JSON response as documented for |
| 1537 | ** fileedit_ajax_error(). |
| 1538 | */ |
| 1539 | void fileedit_ajax_commit(void){ |
| 1540 | Blob err = empty_blob; /* Error messages */ |
| 1541 | Blob manifest = empty_blob; /* raw new manifest */ |
| 1542 | CheckinMiniInfo cimi; /* checkin state */ |
| 1543 | int rc; /* generic result code */ |
| 1544 | int newVid = 0; /* new version's RID */ |
| @@ -1500,15 +1596,15 @@ | |
| 1596 | ** |
| 1597 | ** All other parameters are for internal use only, submitted via the |
| 1598 | ** form-submission process, and may change with any given revision of |
| 1599 | ** this code. |
| 1600 | */ |
| 1601 | void fileedit_page(void){ |
| 1602 | const char * zFilename = 0; /* filename. We'll accept 'name' |
| 1603 | because that param is handled |
| 1604 | specially by the core. */ |
| 1605 | const char * zRev = 0; /* checkin version */ |
| 1606 | const char * zFileMime = 0; /* File mime type guess */ |
| 1607 | CheckinMiniInfo cimi; /* Checkin state */ |
| 1608 | int previewHtmlHeight = 0; /* iframe height (EMs) */ |
| 1609 | int previewRenderMode = FE_RENDER_GUESS; /* preview mode */ |
| 1610 | Blob err = empty_blob; /* Error report */ |
| @@ -1531,18 +1627,17 @@ | |
| 1627 | style_header("File Editor"); |
| 1628 | /* As of this point, don't use return or fossil_fatal(). Write any |
| 1629 | ** error in (&err) and goto end_footer instead so that we can be |
| 1630 | ** sure to do any cleanup and end the transaction cleanly. |
| 1631 | */ |
| 1632 | if(fileedit_setup_cimi_from_p(&cimi, &err)==0){ |
| 1633 | zFilename = cimi.zFilename; |
| 1634 | zRev = cimi.zParentUuid; |
| 1635 | assert(zRev); |
| 1636 | assert(zFilename); |
| 1637 | zFileMime = mimetype_from_name(cimi.zFilename); |
| 1638 | } |
| 1639 | |
| 1640 | /******************************************************************** |
| 1641 | ** All errors which "could" have happened up to this point are of a |
| 1642 | ** degree which keep us from rendering the rest of the page, and |
| 1643 | ** thus have already caused us to skipped to the end of the page to |
| @@ -1564,16 +1659,10 @@ | |
| 1659 | } |
| 1660 | CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. " |
| 1661 | "USE AT YOUR OWN RISK, preferably on a test " |
| 1662 | "repo.</p>\n"); |
| 1663 | |
| 1664 | /* Status bar */ |
| 1665 | CX("<div id='fossil-status-bar'>Async. status messages will go " |
| 1666 | "here.</div>\n"/* will be moved into the tab container via JS */); |
| 1667 | |
| 1668 | /* Main tab container... */ |
| @@ -1581,26 +1670,27 @@ | |
| 1670 | |
| 1671 | /***** File/version info tab *****/ |
| 1672 | { |
| 1673 | CX("<div id='fileedit-tab-version' " |
| 1674 | "data-tab-parent='fileedit-tabs' " |
| 1675 | "data-tab-label='File Info & Selection'" |
| 1676 | ">"); |
| 1677 | CX("File: " |
| 1678 | "<code id='finfo-file-name'>" "???" "</code><br>"); |
| 1679 | CX("Checkin Version: " |
| 1680 | "[<a id='timeline-link' href='#'>/timeline</a>] " |
| 1681 | "[<a id='r-link' href='#'>/info</a>] " |
| 1682 | /* %R/info/%!S */ |
| 1683 | "<code id='r-label'>" "???" "</code><br>" |
| 1684 | ); |
| 1685 | CX("Permalink: <code>" |
| 1686 | "<a id='permalink' href='#'>" "???" "</a></code><br>" |
| 1687 | "(Clicking the permalink will reload the page and discard " |
| 1688 | "all edits!)"); |
| 1689 | |
| 1690 | CX("<h1>Select a file to edit:</h1>"); |
| 1691 | CX("<div id='fileedit-file-selector'></div>"); |
| 1692 | CX("</div>"/*#fileedit-tab-version*/); |
| 1693 | } |
| 1694 | |
| 1695 | /******* Content tab *******/ |
| 1696 | { |
| @@ -1664,11 +1754,13 @@ | |
| 1754 | "1", 1, |
| 1755 | "If on, the preview will automatically " |
| 1756 | "refresh when this tab is selected."); |
| 1757 | |
| 1758 | /* Default preview rendering mode selection... */ |
| 1759 | previewRenderMode = zFileMime |
| 1760 | ? fileedit_render_mode_for_mimetype(zFileMime) |
| 1761 | : FE_RENDER_GUESS; |
| 1762 | style_select_list_int("select-preview-mode", |
| 1763 | "preview_render_mode", |
| 1764 | "Preview Mode", |
| 1765 | "Preview mode format.", |
| 1766 | previewRenderMode, |
| @@ -1692,14 +1784,11 @@ | |
| 1784 | FE_RENDER_WIKI, FE_RENDER_WIKI, |
| 1785 | FE_RENDER_HTML_IFRAME, FE_RENDER_HTML_IFRAME, |
| 1786 | FE_RENDER_HTML_INLINE, FE_RENDER_HTML_INLINE, |
| 1787 | FE_RENDER_PLAIN_TEXT, FE_RENDER_PLAIN_TEXT); |
| 1788 | /* Allow selection of HTML preview iframe height */ |
| 1789 | previewHtmlHeight = 40; |
| 1790 | style_select_list_int("select-preview-html-ems", |
| 1791 | "preview_html_ems", |
| 1792 | "HTML Preview IFrame Height (EMs)", |
| 1793 | "Height (in EMs) of the iframe used for " |
| 1794 | "HTML preview", |
| @@ -1846,25 +1935,40 @@ | |
| 1935 | CX("<div id='fileedit-manifest'></div>\n" |
| 1936 | /* Manifest gets rendered here after a commit. */); |
| 1937 | } |
| 1938 | |
| 1939 | CX("</div>"/*#fileedit-tab-commit*/); |
| 1940 | |
| 1941 | { |
| 1942 | /* Dynamically populate the editor or display a warning |
| 1943 | ** about having no file loaded... */ |
| 1944 | blob_appendf(&endScript, |
| 1945 | "window.addEventListener('load',"); |
| 1946 | if(zRev && zFilename){ |
| 1947 | assert(0==blob_size(&err)); |
| 1948 | blob_appendf(&endScript, |
| 1949 | "()=>fossil.page.loadFile(\"%j\",'%j')", |
| 1950 | zFilename, cimi.zParentUuid); |
| 1951 | }else{ |
| 1952 | blob_appendf(&endScript,"function(){"); |
| 1953 | if(blob_size(&err)>0){ |
| 1954 | blob_appendf(&endScript, |
| 1955 | "fossil.error(\"%j\");\n" |
| 1956 | "fossil.page.tabs.switchToTab(0);\n", |
| 1957 | blob_str(&err)); |
| 1958 | }else{ |
| 1959 | blob_appendf(&endScript, |
| 1960 | "fossil.error('No file/version selected.')"); |
| 1961 | } |
| 1962 | blob_appendf(&endScript,"}"); |
| 1963 | } |
| 1964 | blob_appendf(&endScript,", false);\n"); |
| 1965 | } |
| 1966 | |
| 1967 | if(stmt.pStmt){ |
| 1968 | db_finalize(&stmt); |
| 1969 | } |
| 1970 | blob_reset(&err); |
| 1971 | CheckinMiniInfo_cleanup(&cimi); |
| 1972 | style_emit_script_fossil_bootstrap(0); |
| 1973 | style_emit_script_fetch(0); |
| 1974 | style_emit_script_tabs(0); |
| 1975 |
+128
-13
| --- src/fossil.page.fileedit.js | ||
| +++ src/fossil.page.fileedit.js | ||
| @@ -5,10 +5,127 @@ | ||
| 5 | 5 | bootstrapping is complete and fossil.fetch() has been installed. |
| 6 | 6 | */ |
| 7 | 7 | const E = (s)=>document.querySelector(s), |
| 8 | 8 | D = F.dom, |
| 9 | 9 | P = F.page; |
| 10 | + | |
| 11 | + /** | |
| 12 | + Manager object for the checkin/file selection list. | |
| 13 | + */ | |
| 14 | + P.fileSelector = { | |
| 15 | + e:{ | |
| 16 | + container: E('#fileedit-file-selector') | |
| 17 | + }, | |
| 18 | + finfo: {}, | |
| 19 | + cache: { | |
| 20 | + checkins: undefined, | |
| 21 | + files:{} | |
| 22 | + }, | |
| 23 | + loadLeaves: function(){ | |
| 24 | + D.append(D.clearElement(this.e.ciListLabel),"Loading leaves..."); | |
| 25 | + D.disable(this.e.btnLoadFile, this.e.selectFiles, this.e.selectCi); | |
| 26 | + const self = this; | |
| 27 | + F.fetch('fileedit_filelist',{ | |
| 28 | + urlParams:'leaves', | |
| 29 | + responseType: 'json', | |
| 30 | + onload: function(list){ | |
| 31 | + D.append(D.clearElement(self.e.ciListLabel),"Open leaves:"); | |
| 32 | + self.cache.checkins = list; | |
| 33 | + D.clearElement(D.enable(self.e.selectCi)); | |
| 34 | + let loadThisOne; | |
| 35 | + list.forEach(function(o,n){ | |
| 36 | + if(!n) loadThisOne = o; | |
| 37 | + D.option(self.e.selectCi, o.checkin, | |
| 38 | + o.timestamp+' ['+o.branch+']: ' | |
| 39 | + +F.hashDigits(o.checkin)); | |
| 40 | + }); | |
| 41 | + self.loadFiles(loadThisOne ? loadThisOne.checkin : false); | |
| 42 | + } | |
| 43 | + }); | |
| 44 | + }, | |
| 45 | + loadFiles: function(ciUuid){ | |
| 46 | + delete this.finfo.filename; | |
| 47 | + this.finfo.checkin = ciUuid; | |
| 48 | + const selFiles = this.e.selectFiles; | |
| 49 | + if(!ciUuid){ | |
| 50 | + D.clearElement(D.disable(selFiles, this.e.btnLoadFile)); | |
| 51 | + return this; | |
| 52 | + } | |
| 53 | + const onload = (response)=>{ | |
| 54 | + D.clearElement(D.enable(selFiles, this.e.btnLoadFile)); | |
| 55 | + D.append( | |
| 56 | + D.clearElement(this.e.fileListLabel), | |
| 57 | + "Editable files for ", | |
| 58 | + D.a(F.repoUrl('timeline',{ | |
| 59 | + c: ciUuid | |
| 60 | + }), F.hashDigits(ciUuid)), | |
| 61 | + ':' | |
| 62 | + ); | |
| 63 | + this.cache.files[response.checkin] = response; | |
| 64 | + response.editableFiles.forEach(function(fn){ | |
| 65 | + D.option(selFiles, fn); | |
| 66 | + }); | |
| 67 | + }; | |
| 68 | + const got = this.cache.files[ciUuid]; | |
| 69 | + if(got){ | |
| 70 | + onload(got); | |
| 71 | + return this; | |
| 72 | + } | |
| 73 | + D.disable(selFiles,this.e.btnLoadFile); | |
| 74 | + D.clearElement(selFiles); | |
| 75 | + D.append(D.clearElement(this.e.fileListLabel), | |
| 76 | + "Loading files for "+F.hashDigits(ciUuid)+"..."); | |
| 77 | + F.fetch('fileedit_filelist',{ | |
| 78 | + urlParams:{checkin: ciUuid}, | |
| 79 | + responseType: 'json', | |
| 80 | + onload | |
| 81 | + }); | |
| 82 | + return this; | |
| 83 | + }, | |
| 84 | + init: function(){ | |
| 85 | + const selCi = this.e.selectCi = D.select(), | |
| 86 | + selFiles = this.e.selectFiles | |
| 87 | + = D.addClass(D.select(), 'file-list'), | |
| 88 | + btnLoad = this.e.btnLoadFile = | |
| 89 | + D.addClass(D.button("Load file"), "flex-shrink"), | |
| 90 | + filesLabel = this.e.fileListLabel = | |
| 91 | + D.addClass(D.div(),'flex-shrink','file-list-label'), | |
| 92 | + ciLabel = this.e.ciListLabel = | |
| 93 | + D.addClass(D.div(),'flex-shrink','checkin-list-label') | |
| 94 | + ; | |
| 95 | + D.attr(selCi, 'title',"The list of opened leaves."); | |
| 96 | + D.attr(selFiles, 'title', | |
| 97 | + "The list of editable files for the selected checkin."); | |
| 98 | + D.attr(btnLoad, 'title', | |
| 99 | + "Load the selected file into the editor."); | |
| 100 | + D.disable(selCi, selFiles, btnLoad); | |
| 101 | + D.attr(selFiles, 'size', 10); | |
| 102 | + D.append( | |
| 103 | + this.e.container, | |
| 104 | + ciLabel, | |
| 105 | + selCi, | |
| 106 | + filesLabel, | |
| 107 | + selFiles, | |
| 108 | + btnLoad | |
| 109 | + ); | |
| 110 | + | |
| 111 | + this.loadLeaves(); | |
| 112 | + selCi.addEventListener( | |
| 113 | + 'change', (e)=>this.loadFiles(e.target.value), false | |
| 114 | + ); | |
| 115 | + btnLoad.addEventListener( | |
| 116 | + 'click', (e)=>{ | |
| 117 | + this.finfo.filename = selFiles.value; | |
| 118 | + if(this.finfo.filename){ | |
| 119 | + P.loadFile(this.finfo.filename, this.finfo.checkin); | |
| 120 | + } | |
| 121 | + }, false | |
| 122 | + ); | |
| 123 | + delete this.init; | |
| 124 | + } | |
| 125 | + }; | |
| 126 | + | |
| 10 | 127 | window.addEventListener("load", function() { |
| 11 | 128 | P.tabs = new fossil.TabManager('#fileedit-tabs'); |
| 12 | 129 | P.e = { |
| 13 | 130 | taEditor: E('#fileedit-content-editor'), |
| 14 | 131 | taCommentSmall: E('#fileedit-comment'), |
| @@ -27,10 +144,11 @@ | ||
| 27 | 144 | preview: E('#fileedit-tab-preview'), |
| 28 | 145 | diff: E('#fileedit-tab-diff'), |
| 29 | 146 | commit: E('#fileedit-tab-commit') |
| 30 | 147 | } |
| 31 | 148 | }; |
| 149 | + P.fileSelector.init(); | |
| 32 | 150 | /* Figure out which comment editor to show by default and |
| 33 | 151 | hide the other one. By default we take the one which does |
| 34 | 152 | not have the 'hidden' CSS class. If neither do, we default |
| 35 | 153 | to single-line mode. */ |
| 36 | 154 | if(D.hasClass(P.e.taCommentSmall, 'hidden')){ |
| @@ -197,20 +315,25 @@ | ||
| 197 | 315 | const purl = F.repoUrl('fileedit',purlArgs); |
| 198 | 316 | e = E('#permalink'); |
| 199 | 317 | D.attr(D.append(D.clearElement(e),'?'+purlArgs),'href', purl); |
| 200 | 318 | return this; |
| 201 | 319 | }; |
| 320 | + | |
| 321 | + const affirmHasFile = function(){ | |
| 322 | + if(!P.finfo) F.error("No file is loaded."); | |
| 323 | + return !!P.finfo; | |
| 324 | + }; | |
| 202 | 325 | |
| 203 | 326 | /** |
| 204 | 327 | loadFile() loads (file,checkinVersion) and updates the relevant |
| 205 | 328 | UI elements to reflect the loaded state. |
| 206 | 329 | |
| 207 | 330 | Returns this object, noting that the load is async. |
| 208 | 331 | */ |
| 209 | 332 | P.loadFile = function(file,rev){ |
| 210 | 333 | if(0===arguments.length){ |
| 211 | - if(!this.finfo) return this; | |
| 334 | + if(!affirmHasFile()) return this; | |
| 212 | 335 | file = this.finfo.filename; |
| 213 | 336 | rev = this.finfo.checkin; |
| 214 | 337 | } |
| 215 | 338 | delete this.finfo; |
| 216 | 339 | const self = this; |
| @@ -233,14 +356,11 @@ | ||
| 233 | 356 | preview. |
| 234 | 357 | |
| 235 | 358 | Returns this object, noting that the operation is async. |
| 236 | 359 | */ |
| 237 | 360 | P.preview = function f(switchToTab){ |
| 238 | - if(!this.finfo){ | |
| 239 | - F.error("No content is loaded."); | |
| 240 | - return this; | |
| 241 | - } | |
| 361 | + if(!affirmHasFile()) return this; | |
| 242 | 362 | if(!f.target){ |
| 243 | 363 | f.target = this.e.tabs.preview.querySelector( |
| 244 | 364 | '#fileedit-tab-preview-wrapper' |
| 245 | 365 | ); |
| 246 | 366 | } |
| @@ -255,10 +375,11 @@ | ||
| 255 | 375 | |
| 256 | 376 | /** |
| 257 | 377 | Callback for use with F.connectPagePreviewers() |
| 258 | 378 | */ |
| 259 | 379 | P._postPreview = function(content,callback){ |
| 380 | + if(!affirmHasFile()) return this; | |
| 260 | 381 | if(!content){ |
| 261 | 382 | callback(content); |
| 262 | 383 | return this; |
| 263 | 384 | } |
| 264 | 385 | const fd = new FormData(); |
| @@ -289,14 +410,11 @@ | ||
| 289 | 410 | page's input fields, and updates the UI with the diff view. |
| 290 | 411 | |
| 291 | 412 | Returns this object, noting that the operation is async. |
| 292 | 413 | */ |
| 293 | 414 | P.diff = function f(sbs){ |
| 294 | - if(!this.finfo){ | |
| 295 | - F.error("No content is loaded."); | |
| 296 | - return this; | |
| 297 | - } | |
| 415 | + if(!affirmHasFile()) return this; | |
| 298 | 416 | const content = this.e.taEditor.value, |
| 299 | 417 | self = this; |
| 300 | 418 | if(!f.target){ |
| 301 | 419 | f.target = this.e.tabs.diff.querySelector( |
| 302 | 420 | '#fileedit-tab-diff-wrapper' |
| @@ -330,14 +448,11 @@ | ||
| 330 | 448 | the UI. |
| 331 | 449 | |
| 332 | 450 | Returns this object. |
| 333 | 451 | */ |
| 334 | 452 | P.commit = function f(){ |
| 335 | - if(!this.finfo){ | |
| 336 | - F.error("No content is loaded."); | |
| 337 | - return this; | |
| 338 | - } | |
| 453 | + if(!affirmHasFile()) return this; | |
| 339 | 454 | const self = this; |
| 340 | 455 | const content = this.e.taEditor.value, |
| 341 | 456 | target = document.querySelector('#fileedit-manifest'), |
| 342 | 457 | cbDryRun = E('[name=dry_run]'), |
| 343 | 458 | isDryRun = cbDryRun.checked, |
| 344 | 459 |
| --- src/fossil.page.fileedit.js | |
| +++ src/fossil.page.fileedit.js | |
| @@ -5,10 +5,127 @@ | |
| 5 | bootstrapping is complete and fossil.fetch() has been installed. |
| 6 | */ |
| 7 | const E = (s)=>document.querySelector(s), |
| 8 | D = F.dom, |
| 9 | P = F.page; |
| 10 | window.addEventListener("load", function() { |
| 11 | P.tabs = new fossil.TabManager('#fileedit-tabs'); |
| 12 | P.e = { |
| 13 | taEditor: E('#fileedit-content-editor'), |
| 14 | taCommentSmall: E('#fileedit-comment'), |
| @@ -27,10 +144,11 @@ | |
| 27 | preview: E('#fileedit-tab-preview'), |
| 28 | diff: E('#fileedit-tab-diff'), |
| 29 | commit: E('#fileedit-tab-commit') |
| 30 | } |
| 31 | }; |
| 32 | /* Figure out which comment editor to show by default and |
| 33 | hide the other one. By default we take the one which does |
| 34 | not have the 'hidden' CSS class. If neither do, we default |
| 35 | to single-line mode. */ |
| 36 | if(D.hasClass(P.e.taCommentSmall, 'hidden')){ |
| @@ -197,20 +315,25 @@ | |
| 197 | const purl = F.repoUrl('fileedit',purlArgs); |
| 198 | e = E('#permalink'); |
| 199 | D.attr(D.append(D.clearElement(e),'?'+purlArgs),'href', purl); |
| 200 | return this; |
| 201 | }; |
| 202 | |
| 203 | /** |
| 204 | loadFile() loads (file,checkinVersion) and updates the relevant |
| 205 | UI elements to reflect the loaded state. |
| 206 | |
| 207 | Returns this object, noting that the load is async. |
| 208 | */ |
| 209 | P.loadFile = function(file,rev){ |
| 210 | if(0===arguments.length){ |
| 211 | if(!this.finfo) return this; |
| 212 | file = this.finfo.filename; |
| 213 | rev = this.finfo.checkin; |
| 214 | } |
| 215 | delete this.finfo; |
| 216 | const self = this; |
| @@ -233,14 +356,11 @@ | |
| 233 | preview. |
| 234 | |
| 235 | Returns this object, noting that the operation is async. |
| 236 | */ |
| 237 | P.preview = function f(switchToTab){ |
| 238 | if(!this.finfo){ |
| 239 | F.error("No content is loaded."); |
| 240 | return this; |
| 241 | } |
| 242 | if(!f.target){ |
| 243 | f.target = this.e.tabs.preview.querySelector( |
| 244 | '#fileedit-tab-preview-wrapper' |
| 245 | ); |
| 246 | } |
| @@ -255,10 +375,11 @@ | |
| 255 | |
| 256 | /** |
| 257 | Callback for use with F.connectPagePreviewers() |
| 258 | */ |
| 259 | P._postPreview = function(content,callback){ |
| 260 | if(!content){ |
| 261 | callback(content); |
| 262 | return this; |
| 263 | } |
| 264 | const fd = new FormData(); |
| @@ -289,14 +410,11 @@ | |
| 289 | page's input fields, and updates the UI with the diff view. |
| 290 | |
| 291 | Returns this object, noting that the operation is async. |
| 292 | */ |
| 293 | P.diff = function f(sbs){ |
| 294 | if(!this.finfo){ |
| 295 | F.error("No content is loaded."); |
| 296 | return this; |
| 297 | } |
| 298 | const content = this.e.taEditor.value, |
| 299 | self = this; |
| 300 | if(!f.target){ |
| 301 | f.target = this.e.tabs.diff.querySelector( |
| 302 | '#fileedit-tab-diff-wrapper' |
| @@ -330,14 +448,11 @@ | |
| 330 | the UI. |
| 331 | |
| 332 | Returns this object. |
| 333 | */ |
| 334 | P.commit = function f(){ |
| 335 | if(!this.finfo){ |
| 336 | F.error("No content is loaded."); |
| 337 | return this; |
| 338 | } |
| 339 | const self = this; |
| 340 | const content = this.e.taEditor.value, |
| 341 | target = document.querySelector('#fileedit-manifest'), |
| 342 | cbDryRun = E('[name=dry_run]'), |
| 343 | isDryRun = cbDryRun.checked, |
| 344 |
| --- src/fossil.page.fileedit.js | |
| +++ src/fossil.page.fileedit.js | |
| @@ -5,10 +5,127 @@ | |
| 5 | bootstrapping is complete and fossil.fetch() has been installed. |
| 6 | */ |
| 7 | const E = (s)=>document.querySelector(s), |
| 8 | D = F.dom, |
| 9 | P = F.page; |
| 10 | |
| 11 | /** |
| 12 | Manager object for the checkin/file selection list. |
| 13 | */ |
| 14 | P.fileSelector = { |
| 15 | e:{ |
| 16 | container: E('#fileedit-file-selector') |
| 17 | }, |
| 18 | finfo: {}, |
| 19 | cache: { |
| 20 | checkins: undefined, |
| 21 | files:{} |
| 22 | }, |
| 23 | loadLeaves: function(){ |
| 24 | D.append(D.clearElement(this.e.ciListLabel),"Loading leaves..."); |
| 25 | D.disable(this.e.btnLoadFile, this.e.selectFiles, this.e.selectCi); |
| 26 | const self = this; |
| 27 | F.fetch('fileedit_filelist',{ |
| 28 | urlParams:'leaves', |
| 29 | responseType: 'json', |
| 30 | onload: function(list){ |
| 31 | D.append(D.clearElement(self.e.ciListLabel),"Open leaves:"); |
| 32 | self.cache.checkins = list; |
| 33 | D.clearElement(D.enable(self.e.selectCi)); |
| 34 | let loadThisOne; |
| 35 | list.forEach(function(o,n){ |
| 36 | if(!n) loadThisOne = o; |
| 37 | D.option(self.e.selectCi, o.checkin, |
| 38 | o.timestamp+' ['+o.branch+']: ' |
| 39 | +F.hashDigits(o.checkin)); |
| 40 | }); |
| 41 | self.loadFiles(loadThisOne ? loadThisOne.checkin : false); |
| 42 | } |
| 43 | }); |
| 44 | }, |
| 45 | loadFiles: function(ciUuid){ |
| 46 | delete this.finfo.filename; |
| 47 | this.finfo.checkin = ciUuid; |
| 48 | const selFiles = this.e.selectFiles; |
| 49 | if(!ciUuid){ |
| 50 | D.clearElement(D.disable(selFiles, this.e.btnLoadFile)); |
| 51 | return this; |
| 52 | } |
| 53 | const onload = (response)=>{ |
| 54 | D.clearElement(D.enable(selFiles, this.e.btnLoadFile)); |
| 55 | D.append( |
| 56 | D.clearElement(this.e.fileListLabel), |
| 57 | "Editable files for ", |
| 58 | D.a(F.repoUrl('timeline',{ |
| 59 | c: ciUuid |
| 60 | }), F.hashDigits(ciUuid)), |
| 61 | ':' |
| 62 | ); |
| 63 | this.cache.files[response.checkin] = response; |
| 64 | response.editableFiles.forEach(function(fn){ |
| 65 | D.option(selFiles, fn); |
| 66 | }); |
| 67 | }; |
| 68 | const got = this.cache.files[ciUuid]; |
| 69 | if(got){ |
| 70 | onload(got); |
| 71 | return this; |
| 72 | } |
| 73 | D.disable(selFiles,this.e.btnLoadFile); |
| 74 | D.clearElement(selFiles); |
| 75 | D.append(D.clearElement(this.e.fileListLabel), |
| 76 | "Loading files for "+F.hashDigits(ciUuid)+"..."); |
| 77 | F.fetch('fileedit_filelist',{ |
| 78 | urlParams:{checkin: ciUuid}, |
| 79 | responseType: 'json', |
| 80 | onload |
| 81 | }); |
| 82 | return this; |
| 83 | }, |
| 84 | init: function(){ |
| 85 | const selCi = this.e.selectCi = D.select(), |
| 86 | selFiles = this.e.selectFiles |
| 87 | = D.addClass(D.select(), 'file-list'), |
| 88 | btnLoad = this.e.btnLoadFile = |
| 89 | D.addClass(D.button("Load file"), "flex-shrink"), |
| 90 | filesLabel = this.e.fileListLabel = |
| 91 | D.addClass(D.div(),'flex-shrink','file-list-label'), |
| 92 | ciLabel = this.e.ciListLabel = |
| 93 | D.addClass(D.div(),'flex-shrink','checkin-list-label') |
| 94 | ; |
| 95 | D.attr(selCi, 'title',"The list of opened leaves."); |
| 96 | D.attr(selFiles, 'title', |
| 97 | "The list of editable files for the selected checkin."); |
| 98 | D.attr(btnLoad, 'title', |
| 99 | "Load the selected file into the editor."); |
| 100 | D.disable(selCi, selFiles, btnLoad); |
| 101 | D.attr(selFiles, 'size', 10); |
| 102 | D.append( |
| 103 | this.e.container, |
| 104 | ciLabel, |
| 105 | selCi, |
| 106 | filesLabel, |
| 107 | selFiles, |
| 108 | btnLoad |
| 109 | ); |
| 110 | |
| 111 | this.loadLeaves(); |
| 112 | selCi.addEventListener( |
| 113 | 'change', (e)=>this.loadFiles(e.target.value), false |
| 114 | ); |
| 115 | btnLoad.addEventListener( |
| 116 | 'click', (e)=>{ |
| 117 | this.finfo.filename = selFiles.value; |
| 118 | if(this.finfo.filename){ |
| 119 | P.loadFile(this.finfo.filename, this.finfo.checkin); |
| 120 | } |
| 121 | }, false |
| 122 | ); |
| 123 | delete this.init; |
| 124 | } |
| 125 | }; |
| 126 | |
| 127 | window.addEventListener("load", function() { |
| 128 | P.tabs = new fossil.TabManager('#fileedit-tabs'); |
| 129 | P.e = { |
| 130 | taEditor: E('#fileedit-content-editor'), |
| 131 | taCommentSmall: E('#fileedit-comment'), |
| @@ -27,10 +144,11 @@ | |
| 144 | preview: E('#fileedit-tab-preview'), |
| 145 | diff: E('#fileedit-tab-diff'), |
| 146 | commit: E('#fileedit-tab-commit') |
| 147 | } |
| 148 | }; |
| 149 | P.fileSelector.init(); |
| 150 | /* Figure out which comment editor to show by default and |
| 151 | hide the other one. By default we take the one which does |
| 152 | not have the 'hidden' CSS class. If neither do, we default |
| 153 | to single-line mode. */ |
| 154 | if(D.hasClass(P.e.taCommentSmall, 'hidden')){ |
| @@ -197,20 +315,25 @@ | |
| 315 | const purl = F.repoUrl('fileedit',purlArgs); |
| 316 | e = E('#permalink'); |
| 317 | D.attr(D.append(D.clearElement(e),'?'+purlArgs),'href', purl); |
| 318 | return this; |
| 319 | }; |
| 320 | |
| 321 | const affirmHasFile = function(){ |
| 322 | if(!P.finfo) F.error("No file is loaded."); |
| 323 | return !!P.finfo; |
| 324 | }; |
| 325 | |
| 326 | /** |
| 327 | loadFile() loads (file,checkinVersion) and updates the relevant |
| 328 | UI elements to reflect the loaded state. |
| 329 | |
| 330 | Returns this object, noting that the load is async. |
| 331 | */ |
| 332 | P.loadFile = function(file,rev){ |
| 333 | if(0===arguments.length){ |
| 334 | if(!affirmHasFile()) return this; |
| 335 | file = this.finfo.filename; |
| 336 | rev = this.finfo.checkin; |
| 337 | } |
| 338 | delete this.finfo; |
| 339 | const self = this; |
| @@ -233,14 +356,11 @@ | |
| 356 | preview. |
| 357 | |
| 358 | Returns this object, noting that the operation is async. |
| 359 | */ |
| 360 | P.preview = function f(switchToTab){ |
| 361 | if(!affirmHasFile()) return this; |
| 362 | if(!f.target){ |
| 363 | f.target = this.e.tabs.preview.querySelector( |
| 364 | '#fileedit-tab-preview-wrapper' |
| 365 | ); |
| 366 | } |
| @@ -255,10 +375,11 @@ | |
| 375 | |
| 376 | /** |
| 377 | Callback for use with F.connectPagePreviewers() |
| 378 | */ |
| 379 | P._postPreview = function(content,callback){ |
| 380 | if(!affirmHasFile()) return this; |
| 381 | if(!content){ |
| 382 | callback(content); |
| 383 | return this; |
| 384 | } |
| 385 | const fd = new FormData(); |
| @@ -289,14 +410,11 @@ | |
| 410 | page's input fields, and updates the UI with the diff view. |
| 411 | |
| 412 | Returns this object, noting that the operation is async. |
| 413 | */ |
| 414 | P.diff = function f(sbs){ |
| 415 | if(!affirmHasFile()) return this; |
| 416 | const content = this.e.taEditor.value, |
| 417 | self = this; |
| 418 | if(!f.target){ |
| 419 | f.target = this.e.tabs.diff.querySelector( |
| 420 | '#fileedit-tab-diff-wrapper' |
| @@ -330,14 +448,11 @@ | |
| 448 | the UI. |
| 449 | |
| 450 | Returns this object. |
| 451 | */ |
| 452 | P.commit = function f(){ |
| 453 | if(!affirmHasFile()) return this; |
| 454 | const self = this; |
| 455 | const content = this.e.taEditor.value, |
| 456 | target = document.querySelector('#fileedit-manifest'), |
| 457 | cbDryRun = E('[name=dry_run]'), |
| 458 | isDryRun = cbDryRun.checked, |
| 459 |