Fossil SCM

/filepage no longer requires URL arguments: it has a leaf checkin/file selection list on the first tab.

stephan 2020-05-11 11:51 fileedit-ajaxify
Commit e184992161fdf362b5c7ed17a42f1113a062187e32e8b45d9f977bd403158bd1
2 files changed +167 -63 +128 -13
+167 -63
--- src/fileedit.c
+++ src/fileedit.c
@@ -743,11 +743,11 @@
743743
** Example:
744744
**
745745
** %fossil test-ci-mini -R REPO -m ... -r foo --as src/myfile.c myfile.c
746746
**
747747
*/
748
-void test_ci_mini_cmd(){
748
+void test_ci_mini_cmd(void){
749749
CheckinMiniInfo cimi; /* checkin state */
750750
int newRid = 0; /* RID of new version */
751751
const char * zFilename; /* argv[2] */
752752
const char * zComment; /* -m comment */
753753
const char * zCommentFile; /* -M FILE */
@@ -1048,16 +1048,17 @@
10481048
** Performs bootstrapping common to the /fileedit_xyz AJAX routes.
10491049
** Returns 0 if bootstrapping fails (wrong permissions), in which
10501050
** case it has reported the error and the route should immediately
10511051
** return. Returns true on success.
10521052
*/
1053
-static int fileedit_ajax_boostrap(){
1053
+static int fileedit_ajax_boostrap(void){
10541054
login_check_credentials();
10551055
if( !g.perm.Write ){
10561056
fileedit_ajax_error(403,"Write permissions required.");
10571057
return 0;
10581058
}
1059
+
10591060
return 1;
10601061
}
10611062
/*
10621063
** Returns true if the current user is allowed to edit the given
10631064
** filename, as determined by fileedit_is_editable(), else false,
@@ -1109,11 +1110,14 @@
11091110
** - *zRevUuid = the fully-expanded value of zRev (owned by the
11101111
** caller). zRevUuid may be NULL.
11111112
**
11121113
** - *vid = the RID of zRevUuid. May not be NULL.
11131114
**
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.
11151119
**
11161120
** Returns 0 if the given file is not in the given checkin or if
11171121
** fileedit_ajax_check_filename() fails, else returns true. If it
11181122
** returns false, it queues up an error response and the caller must
11191123
** return immediately.
@@ -1121,14 +1125,14 @@
11211125
static int fileedit_ajax_setup_filerev(const char * zRev,
11221126
char ** zRevUuid,
11231127
int * vid,
11241128
const char * zFilename,
11251129
int * frid){
1126
- char * zCi = 0; /* fully-resolved checkin UUID */
11271130
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)){
11301134
return 0;
11311135
}
11321136
*vid = symbolic_name_to_rid(zRev, "ci");
11331137
if(0==*vid){
11341138
fileedit_ajax_error(404,"Cannot resolve name as a checkin: %s",
@@ -1137,22 +1141,26 @@
11371141
}else if(*vid<0){
11381142
fileedit_ajax_error(400,"Checkin name is ambiguous: %s",
11391143
zRev);
11401144
return 0;
11411145
}
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
+ }
11501153
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);
11541162
}
11551163
return 1;
11561164
}
11571165
11581166
/*
@@ -1166,11 +1174,11 @@
11661174
** User must have Write access to use this page.
11671175
**
11681176
** Responds with the raw content of the given page. On error it
11691177
** produces a JSON response as documented for fileedit_ajax_error().
11701178
*/
1171
-void fileedit_ajax_content(){
1179
+void fileedit_ajax_content(void){
11721180
const char * zFilename = 0;
11731181
const char * zRev = 0;
11741182
int vid, frid;
11751183
Blob content = empty_blob;
11761184
const char * zMime;
@@ -1215,11 +1223,11 @@
12151223
** User must have Write access to use this page.
12161224
**
12171225
** Responds with the HTML content of the preview. On error it produces
12181226
** a JSON response as documented for fileedit_ajax_error().
12191227
*/
1220
-void fileedit_ajax_preview(){
1228
+void fileedit_ajax_preview(void){
12211229
const char * zFilename = 0;
12221230
const char * zContent = P("content");
12231231
int renderMode = atoi(PD("render_mode","0"));
12241232
int ln = atoi(PD("ln","0"));
12251233
int iframeHeight = atoi(PD("iframe_height","40"));
@@ -1253,11 +1261,11 @@
12531261
** User must have Write access to use this page.
12541262
**
12551263
** Responds with the HTML content of the diff. On error it produces a
12561264
** JSON response as documented for fileedit_ajax_error().
12571265
*/
1258
-void fileedit_ajax_diff(){
1266
+void fileedit_ajax_diff(void){
12591267
/*
12601268
** Reminder: we only need the filename to perform valdiation
12611269
** against fileedit_is_editable(), else this route could be
12621270
** abused to get diffs against content disallowed by the
12631271
** whitelist.
@@ -1410,11 +1418,99 @@
14101418
fossil_free(zFileUuid);
14111419
return rc ? rc : 500;
14121420
}
14131421
14141422
/*
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
14161512
**
14171513
** Required query parameters:
14181514
**
14191515
** filename=FILENAME
14201516
** checkin=Parent checkin UUID
@@ -1438,11 +1534,11 @@
14381534
** }
14391535
**
14401536
** On error it produces a JSON response as documented for
14411537
** fileedit_ajax_error().
14421538
*/
1443
-void fileedit_ajax_commit(){
1539
+void fileedit_ajax_commit(void){
14441540
Blob err = empty_blob; /* Error messages */
14451541
Blob manifest = empty_blob; /* raw new manifest */
14461542
CheckinMiniInfo cimi; /* checkin state */
14471543
int rc; /* generic result code */
14481544
int newVid = 0; /* new version's RID */
@@ -1500,15 +1596,15 @@
15001596
**
15011597
** All other parameters are for internal use only, submitted via the
15021598
** form-submission process, and may change with any given revision of
15031599
** this code.
15041600
*/
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'
15071603
because that param is handled
15081604
specially by the core. */
1509
- const char * zRev; /* checkin version */
1605
+ const char * zRev = 0; /* checkin version */
15101606
const char * zFileMime = 0; /* File mime type guess */
15111607
CheckinMiniInfo cimi; /* Checkin state */
15121608
int previewHtmlHeight = 0; /* iframe height (EMs) */
15131609
int previewRenderMode = FE_RENDER_GUESS; /* preview mode */
15141610
Blob err = empty_blob; /* Error report */
@@ -1531,18 +1627,17 @@
15311627
style_header("File Editor");
15321628
/* As of this point, don't use return or fossil_fatal(). Write any
15331629
** error in (&err) and goto end_footer instead so that we can be
15341630
** sure to do any cleanup and end the transaction cleanly.
15351631
*/
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
+ }
15441639
15451640
/********************************************************************
15461641
** All errors which "could" have happened up to this point are of a
15471642
** degree which keep us from rendering the rest of the page, and
15481643
** thus have already caused us to skipped to the end of the page to
@@ -1564,16 +1659,10 @@
15641659
}
15651660
CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. "
15661661
"USE AT YOUR OWN RISK, preferably on a test "
15671662
"repo.</p>\n");
15681663
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
-
15751664
/* Status bar */
15761665
CX("<div id='fossil-status-bar'>Async. status messages will go "
15771666
"here.</div>\n"/* will be moved into the tab container via JS */);
15781667
15791668
/* Main tab container... */
@@ -1581,26 +1670,27 @@
15811670
15821671
/***** File/version info tab *****/
15831672
{
15841673
CX("<div id='fileedit-tab-version' "
15851674
"data-tab-parent='fileedit-tabs' "
1586
- "data-tab-label='Version Info'"
1675
+ "data-tab-label='File Info &amp; Selection'"
15871676
">");
15881677
CX("File: "
1589
- "<code id='finfo-file-name'>(loading)</code><br>");
1678
+ "<code id='finfo-file-name'>" "???" "</code><br>");
15901679
CX("Checkin Version: "
15911680
"[<a id='timeline-link' href='#'>/timeline</a>] "
15921681
"[<a id='r-link' href='#'>/info</a>] "
15931682
/* %R/info/%!S */
1594
- "<code id='r-label'>(loading...)</code><br>"
1683
+ "<code id='r-label'>" "???" "</code><br>"
15951684
);
15961685
CX("Permalink: <code>"
1597
- "<a id='permalink' href='#'>(loading...)</a></code><br>"
1686
+ "<a id='permalink' href='#'>" "???" "</a></code><br>"
15981687
"(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>");
16021692
CX("</div>"/*#fileedit-tab-version*/);
16031693
}
16041694
16051695
/******* Content tab *******/
16061696
{
@@ -1664,11 +1754,13 @@
16641754
"1", 1,
16651755
"If on, the preview will automatically "
16661756
"refresh when this tab is selected.");
16671757
16681758
/* 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;
16701762
style_select_list_int("select-preview-mode",
16711763
"preview_render_mode",
16721764
"Preview Mode",
16731765
"Preview mode format.",
16741766
previewRenderMode,
@@ -1692,14 +1784,11 @@
16921784
FE_RENDER_WIKI, FE_RENDER_WIKI,
16931785
FE_RENDER_HTML_IFRAME, FE_RENDER_HTML_IFRAME,
16941786
FE_RENDER_HTML_INLINE, FE_RENDER_HTML_INLINE,
16951787
FE_RENDER_PLAIN_TEXT, FE_RENDER_PLAIN_TEXT);
16961788
/* 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;
17011790
style_select_list_int("select-preview-html-ems",
17021791
"preview_html_ems",
17031792
"HTML Preview IFrame Height (EMs)",
17041793
"Height (in EMs) of the iframe used for "
17051794
"HTML preview",
@@ -1846,25 +1935,40 @@
18461935
CX("<div id='fileedit-manifest'></div>\n"
18471936
/* Manifest gets rendered here after a commit. */);
18481937
}
18491938
18501939
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
+
18591967
if(stmt.pStmt){
18601968
db_finalize(&stmt);
18611969
}
1862
- if(blob_size(&err)){
1863
- CX("<div class='fileedit-error-report'>%s</div>",
1864
- blob_str(&err));
1865
- }
18661970
blob_reset(&err);
18671971
CheckinMiniInfo_cleanup(&cimi);
18681972
style_emit_script_fossil_bootstrap(0);
18691973
style_emit_script_fetch(0);
18701974
style_emit_script_tabs(0);
18711975
--- 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 &amp; 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
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -5,10 +5,127 @@
55
bootstrapping is complete and fossil.fetch() has been installed.
66
*/
77
const E = (s)=>document.querySelector(s),
88
D = F.dom,
99
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
+
10127
window.addEventListener("load", function() {
11128
P.tabs = new fossil.TabManager('#fileedit-tabs');
12129
P.e = {
13130
taEditor: E('#fileedit-content-editor'),
14131
taCommentSmall: E('#fileedit-comment'),
@@ -27,10 +144,11 @@
27144
preview: E('#fileedit-tab-preview'),
28145
diff: E('#fileedit-tab-diff'),
29146
commit: E('#fileedit-tab-commit')
30147
}
31148
};
149
+ P.fileSelector.init();
32150
/* Figure out which comment editor to show by default and
33151
hide the other one. By default we take the one which does
34152
not have the 'hidden' CSS class. If neither do, we default
35153
to single-line mode. */
36154
if(D.hasClass(P.e.taCommentSmall, 'hidden')){
@@ -197,20 +315,25 @@
197315
const purl = F.repoUrl('fileedit',purlArgs);
198316
e = E('#permalink');
199317
D.attr(D.append(D.clearElement(e),'?'+purlArgs),'href', purl);
200318
return this;
201319
};
320
+
321
+ const affirmHasFile = function(){
322
+ if(!P.finfo) F.error("No file is loaded.");
323
+ return !!P.finfo;
324
+ };
202325
203326
/**
204327
loadFile() loads (file,checkinVersion) and updates the relevant
205328
UI elements to reflect the loaded state.
206329
207330
Returns this object, noting that the load is async.
208331
*/
209332
P.loadFile = function(file,rev){
210333
if(0===arguments.length){
211
- if(!this.finfo) return this;
334
+ if(!affirmHasFile()) return this;
212335
file = this.finfo.filename;
213336
rev = this.finfo.checkin;
214337
}
215338
delete this.finfo;
216339
const self = this;
@@ -233,14 +356,11 @@
233356
preview.
234357
235358
Returns this object, noting that the operation is async.
236359
*/
237360
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;
242362
if(!f.target){
243363
f.target = this.e.tabs.preview.querySelector(
244364
'#fileedit-tab-preview-wrapper'
245365
);
246366
}
@@ -255,10 +375,11 @@
255375
256376
/**
257377
Callback for use with F.connectPagePreviewers()
258378
*/
259379
P._postPreview = function(content,callback){
380
+ if(!affirmHasFile()) return this;
260381
if(!content){
261382
callback(content);
262383
return this;
263384
}
264385
const fd = new FormData();
@@ -289,14 +410,11 @@
289410
page's input fields, and updates the UI with the diff view.
290411
291412
Returns this object, noting that the operation is async.
292413
*/
293414
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;
298416
const content = this.e.taEditor.value,
299417
self = this;
300418
if(!f.target){
301419
f.target = this.e.tabs.diff.querySelector(
302420
'#fileedit-tab-diff-wrapper'
@@ -330,14 +448,11 @@
330448
the UI.
331449
332450
Returns this object.
333451
*/
334452
P.commit = function f(){
335
- if(!this.finfo){
336
- F.error("No content is loaded.");
337
- return this;
338
- }
453
+ if(!affirmHasFile()) return this;
339454
const self = this;
340455
const content = this.e.taEditor.value,
341456
target = document.querySelector('#fileedit-manifest'),
342457
cbDryRun = E('[name=dry_run]'),
343458
isDryRun = cbDryRun.checked,
344459
--- 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button