Fossil
Check-in [3dc4613d]
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:fossil.message() and friends now use local timestamps instead of UTC. Fixed a bug in wikiedit which caused a newly-created page to disappear from the page selection list after it was saved. Other minor cleanups in adjacent code.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 3dc4613d19edd0d94bc8b15b81c7987b7caf884dd3961ceabecb1c740d25f16a
User & Date: stephan 2020-08-01 23:38:36
Context
2020-08-02
13:23
Tiny style tweak for the wikiedit list filter toggles. (check-in: b0a38d5f user: stephan tags: trunk)
2020-08-01
23:38
fossil.message() and friends now use local timestamps instead of UTC. Fixed a bug in wikiedit which caused a newly-created page to disappear from the page selection list after it was saved. Other minor cleanups in adjacent code. (check-in: 3dc4613d user: stephan tags: trunk)
22:25
Minor CSS tweak for mobile browsers. (check-in: bfd79af0 user: stephan tags: trunk)
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to src/fossil.bootstrap.js.

15
16
17
18
19
20
21















22
23
24
25
26
27
28
29
30
31
32
33
34
35



36
37
38
39
40
41
42
    if(!f.rx1){
      f.rx1 = /\.\d+Z$/;
    }
    const d = new Date();
    return d.toISOString().replace(f.rx1,'').split('T').join(' ');
  };
















  /*
  ** By default fossil.message() sends its arguments console.debug(). If
  ** fossil.message.targetElement is set, it is assumed to be a DOM
  ** element, its innerText gets assigned to the concatenation of all
  ** arguments (with a space between each), and the CSS 'error' class is
  ** removed from the object. Pass it a falsy value to clear the target
  ** element.
  **
  ** Returns this object.
  */
  F.message = function f(msg){
    const args = Array.prototype.slice.call(arguments,0);
    const tgt = f.targetElement;
    if(args.length) args.unshift(timestring(),'UTC:');



    if(tgt){
      tgt.classList.remove('error');
      tgt.innerText = args.join(' ');
    }
    else{
      if(args.length){
        args.unshift('Fossil status:');







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>













|
>
>
>







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
    if(!f.rx1){
      f.rx1 = /\.\d+Z$/;
    }
    const d = new Date();
    return d.toISOString().replace(f.rx1,'').split('T').join(' ');
  };

  /** Returns the local time string of Date object d, defaulting
      to the current time. */
  const localTimeString = function ff(d){
    if(!ff.pad){
      ff.pad = (x)=>(''+x).length>1 ? x : '0'+x;
    }
    d || (d = new Date());
    return [
      d.getFullYear(),'-',ff.pad(d.getMonth()+1/*sigh*/),
      '-',ff.pad(d.getDate()),
      ' ',ff.pad(d.getHours()),':',ff.pad(d.getMinutes()),
      ':',ff.pad(d.getSeconds())
    ].join('');
  };

  /*
  ** By default fossil.message() sends its arguments console.debug(). If
  ** fossil.message.targetElement is set, it is assumed to be a DOM
  ** element, its innerText gets assigned to the concatenation of all
  ** arguments (with a space between each), and the CSS 'error' class is
  ** removed from the object. Pass it a falsy value to clear the target
  ** element.
  **
  ** Returns this object.
  */
  F.message = function f(msg){
    const args = Array.prototype.slice.call(arguments,0);
    const tgt = f.targetElement;
    if(args.length) args.unshift(
      localTimeString()+':'
      //timestring(),'UTC:'
    );
    if(tgt){
      tgt.classList.remove('error');
      tgt.innerText = args.join(' ');
    }
    else{
      if(args.length){
        args.unshift('Fossil status:');

Changes to src/fossil.page.fileedit.js.

516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
        D.addClass(this.e.btnClear, 'hidden');
        D.option(D.disable(this.e.select),"No local edits");
        return;
      }
      D.enable(this.e.select);
      D.removeClass(this.e.btnClear, 'hidden');
      D.disable(D.option(this.e.select,0,"Select a local edit..."));
      const currentFinfo = theFinfo || P.finfo || {};
      ilist.sort(f.compare).forEach(function(finfo,n){
        const key = stasher.indexKey(finfo),
              branch = finfo.branch
              || P.fileSelectWidget.checkinBranchName(finfo.checkin)||'';
        /* Remember that we don't know the branch name for non-leaf versions
           which P.fileSelectWidget() has never seen/cached. */
        const opt = D.option(
          self.e.select, n+1/*value is (almost) irrelevant*/,
          [F.hashDigits(finfo.checkin, 6), ' [',branch||'?branch?','] ',
           f.timestring(new Date(finfo.stashTime)),' ',
           false ? finfo.filename : F.shortenFilename(finfo.filename)
          ].join('')
        );
        opt._finfo = finfo;
        if(0===f.compare(currentFinfo, finfo)){
          D.attr(opt, 'selected', true);







|








|







516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
        D.addClass(this.e.btnClear, 'hidden');
        D.option(D.disable(this.e.select),"No local edits");
        return;
      }
      D.enable(this.e.select);
      D.removeClass(this.e.btnClear, 'hidden');
      D.disable(D.option(this.e.select,0,"Select a local edit..."));
      const currentFinfo = theFinfo || P.finfo || {filename:''};
      ilist.sort(f.compare).forEach(function(finfo,n){
        const key = stasher.indexKey(finfo),
              branch = finfo.branch
              || P.fileSelectWidget.checkinBranchName(finfo.checkin)||'';
        /* Remember that we don't know the branch name for non-leaf versions
           which P.fileSelectWidget() has never seen/cached. */
        const opt = D.option(
          self.e.select, n+1/*value is (almost) irrelevant*/,
          [F.hashDigits(finfo.checkin), ' [',branch||'?branch?','] ',
           f.timestring(new Date(finfo.stashTime)),' ',
           false ? finfo.filename : F.shortenFilename(finfo.filename)
          ].join('')
        );
        opt._finfo = finfo;
        if(0===f.compare(currentFinfo, finfo)){
          D.attr(opt, 'selected', true);

Changes to src/fossil.page.wikiedit.js.

300
301
302
303
304
305
306

307
308
309
310
311
312
313
...
353
354
355
356
357
358
359


360
361
362
363
364
365
366
...
466
467
468
469
470
471
472



473
474
475
476
477
478
479
480
481
482
483
484
485
...
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
...
655
656
657
658
659
660
661
662
663
664
665
666
667
668

669

670
671
672
673
674
675
676
677
678
679
680
681
682
....
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
    e: {
      filterCheckboxes: {
        /*map of wiki page type to checkbox for list filtering purposes,
          except for "sandbox" type, which is assumed to be covered by
          the "normal" type filter. */},
    },
    cache: {

      names: {
        /* Map of page names to "something." We don't map to their
           winfo bits because those regularly get swapped out via
           de/serialization. We need this map to support the add-new-page
           feature, to give us a way to check for dupes without asking
           the server or walking through the whole selection list.
        */}
................................................................................
      var ndx = sel.selectedIndex;
      sel.value = name;
      if(sel.selectedIndex>-1){
        if(ndx === sel.selectedIndex) ndx = -1;
        sel.options.remove(sel.selectedIndex);
      }
      sel.selectedIndex = ndx;


    },

    /**
       Rebuilds the selection list. Necessary when it's loaded from
       the server or we locally create a new page.
    */
    _rebuildList: function callee(){
................................................................................
      else if(0===name.indexOf('tag/')) wtype = 'tag';
      /* ^^^ note that we're not validating that, e.g., checkin/XYZ
         has a full artifact ID after "checkin/". */
      const winfo = {
        name: name, type: wtype, mimetype: 'text/x-fossil-wiki',
        version: null, parent: null
      };



      $stash.updateWinfo(winfo, '');
      this._rebuildList();
      P.loadPage(winfo.name);
      return true;
    },
    
    /**
       Installs a wiki page selection list into the given parent DOM
       element and loads the page list from the server.
    */
    init: function(parentElem){
      const sel = D.select(), btn = D.addClass(D.button("Reload page list"), 'save');
      this.e.select = sel;
................................................................................
      $stash._fireStashEvent(/*read the page-load-time stash*/);
      delete this.init;
    },
    /**
       Regenerates the edit selection list.
    */
    updateList: function f(stasher,theWinfo){
      console.debug("updateList()",arguments);
      if(!f.compare){
        const cmpBase = (l,r)=>l<r ? -1 : (l===r ? 0 : 1);
        f.compare = (l,r)=>cmpBase(l.name, r.name);
        f.rxZ = /\.\d+Z$/ /* ms and 'Z' part of date string */;
        const pad=(x)=>(''+x).length>1 ? x : '0'+x;
        f.timestring = function ff(d){
          return [
            d.getFullYear(),'-',pad(d.getMonth()+1/*sigh*/),'-',pad(d.getDate()),
            '@',pad(d.getHours()),':',pad(d.getMinutes())
          ].join('');
        };
      }
      const index = stasher.getIndex(), ilist = [];
................................................................................
        /* The problem with this Clear button is that it allows the user
           to nuke a non-empty newly-added page without the failsafe confirmation
           we have if they use P.e.btnReload. Not yet sure how best to resolve that,
           so we'll leave the button hidden for the time being. */           
        D.removeClass(this.e.btnClear, 'hidden');
      }
      D.disable(D.option(this.e.select,0,"Select a local edit..."));
      const currentFinfo = theWinfo || P.winfo || {};
      ilist.sort(f.compare).forEach(function(winfo,n){
        const key = stasher.indexKey(winfo),
              rev = winfo.version || '';
        const opt = D.option(
          self.e.select, n+1/*value is (almost) irrelevant*/,
          [winfo.name,

           rev ? ' ['+F.hashDigits(rev, 6)+']' : ' [new/local]',

           ' @ ',
           f.timestring(new Date(winfo.stashTime))
          ].join('')
        );
        opt._winfo = winfo;
        if(0===f.compare(currentFinfo, winfo)){
          D.attr(opt, 'selected', true);
        }
      });
    }
  }/*P.stashWidget*/;

  /**
................................................................................
    if(!affirmPageLoaded()) return this;
    const self = this;
    const content = this.wikiContent();
    if(!callee.onload){
      callee.onload = function(w){
        const oldWinfo = self.winfo;
        self.unstashContent(oldWinfo);
        self.winfo = w;
        self.updatePageTitle();
        self.dispatchEvent('wiki-page-loaded', w);
        F.message("Saved page: ["+w.name+"].");
      }
    }
    const fd = new FormData(), w = P.winfo;
    fd.append('page',w.name);
    fd.append('mimetype', w.mimetype);







>







 







>
>







 







>
>
>





|







 







<


|


|







 







|






>
|
>
|




|







 







<
<







300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
...
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
...
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
...
628
629
630
631
632
633
634

635
636
637
638
639
640
641
642
643
644
645
646
647
...
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
....
1249
1250
1251
1252
1253
1254
1255


1256
1257
1258
1259
1260
1261
1262
    e: {
      filterCheckboxes: {
        /*map of wiki page type to checkbox for list filtering purposes,
          except for "sandbox" type, which is assumed to be covered by
          the "normal" type filter. */},
    },
    cache: {
      pageList: [],
      names: {
        /* Map of page names to "something." We don't map to their
           winfo bits because those regularly get swapped out via
           de/serialization. We need this map to support the add-new-page
           feature, to give us a way to check for dupes without asking
           the server or walking through the whole selection list.
        */}
................................................................................
      var ndx = sel.selectedIndex;
      sel.value = name;
      if(sel.selectedIndex>-1){
        if(ndx === sel.selectedIndex) ndx = -1;
        sel.options.remove(sel.selectedIndex);
      }
      sel.selectedIndex = ndx;
      delete this.cache.names[name];
      this.cache.pageList = this.cache.pageList.filter((wi)=>name !== wi.name);
    },

    /**
       Rebuilds the selection list. Necessary when it's loaded from
       the server or we locally create a new page.
    */
    _rebuildList: function callee(){
................................................................................
      else if(0===name.indexOf('tag/')) wtype = 'tag';
      /* ^^^ note that we're not validating that, e.g., checkin/XYZ
         has a full artifact ID after "checkin/". */
      const winfo = {
        name: name, type: wtype, mimetype: 'text/x-fossil-wiki',
        version: null, parent: null
      };
      this.cache.pageList.push(
        winfo/*keeps entry from getting lost from the list on save*/
      );
      $stash.updateWinfo(winfo, '');
      this._rebuildList();
      P.loadPage(winfo.name);
      return true;
    },

    /**
       Installs a wiki page selection list into the given parent DOM
       element and loads the page list from the server.
    */
    init: function(parentElem){
      const sel = D.select(), btn = D.addClass(D.button("Reload page list"), 'save');
      this.e.select = sel;
................................................................................
      $stash._fireStashEvent(/*read the page-load-time stash*/);
      delete this.init;
    },
    /**
       Regenerates the edit selection list.
    */
    updateList: function f(stasher,theWinfo){

      if(!f.compare){
        const cmpBase = (l,r)=>l<r ? -1 : (l===r ? 0 : 1);
        f.compare = (l,r)=>cmpBase(l.name.toLowerCase(), r.name.toLowerCase());
        f.rxZ = /\.\d+Z$/ /* ms and 'Z' part of date string */;
        const pad=(x)=>(''+x).length>1 ? x : '0'+x;
        f.timestring = function(d){
          return [
            d.getFullYear(),'-',pad(d.getMonth()+1/*sigh*/),'-',pad(d.getDate()),
            '@',pad(d.getHours()),':',pad(d.getMinutes())
          ].join('');
        };
      }
      const index = stasher.getIndex(), ilist = [];
................................................................................
        /* The problem with this Clear button is that it allows the user
           to nuke a non-empty newly-added page without the failsafe confirmation
           we have if they use P.e.btnReload. Not yet sure how best to resolve that,
           so we'll leave the button hidden for the time being. */           
        D.removeClass(this.e.btnClear, 'hidden');
      }
      D.disable(D.option(this.e.select,0,"Select a local edit..."));
      const currentWinfo = theWinfo || P.winfo || {name:''};
      ilist.sort(f.compare).forEach(function(winfo,n){
        const key = stasher.indexKey(winfo),
              rev = winfo.version || '';
        const opt = D.option(
          self.e.select, n+1/*value is (almost) irrelevant*/,
          [winfo.name,
           ' [',
           rev ? F.hashDigits(rev) : (
             winfo.type==='sandbox' ? 'sandbox' : 'new/local'
           ),'] ',
           f.timestring(new Date(winfo.stashTime))
          ].join('')
        );
        opt._winfo = winfo;
        if(0===f.compare(currentWinfo, winfo)){
          D.attr(opt, 'selected', true);
        }
      });
    }
  }/*P.stashWidget*/;

  /**
................................................................................
    if(!affirmPageLoaded()) return this;
    const self = this;
    const content = this.wikiContent();
    if(!callee.onload){
      callee.onload = function(w){
        const oldWinfo = self.winfo;
        self.unstashContent(oldWinfo);


        self.dispatchEvent('wiki-page-loaded', w);
        F.message("Saved page: ["+w.name+"].");
      }
    }
    const fd = new FormData(), w = P.winfo;
    fd.append('page',w.name);
    fd.append('mimetype', w.mimetype);

Changes to src/wiki.c.

805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840

841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
...
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
**          not send.
**
** Responds with JSON. On error, an object in the form documented by
** ajax_route_error(). On success, an object in the form documented
** for wiki_ajax_emit_page_object().
**
** The wikiajax API disallows saving of a sandbox pseudo-page, and
** will respond with an error if asked to save one.
**
** Reminder: the original implementation implements sandbox-page
** saving using:
**
**  db_set("sandbox",zBody,0);
**  db_set("sandbox-mimetype",zMimetype,0);
**
*/
static void wiki_ajax_route_save(void){
  const char *zPageName = P("page");
  const char *zMimetype = P("mimetype");
  const char *zContent = P("content");
  const int isNew = atoi(PD("isnew","0"))==1;
  Blob content = empty_blob;
  int parentRid = 0;
  int rollback = 0;

  if(!wiki_ajax_can_write(zPageName, &parentRid)){
    return;
  }else if(is_sandbox(zPageName)){
    ajax_route_error(403,"Saving a sandbox page is prohibited.");
    return;
  }
  
  /* These isNew checks are just me being pedantic. The hope is
     to avoid accidental addition of new pages which differ only
     by the case of their name. We could just as easily derive
     isNew based on whether or not the page already exists. */

  if(isNew){
    if(parentRid>0){
      ajax_route_error(403,"Requested a new page, "
                       "but it already exists with RID %d: %s",
                       parentRid, zPageName);
      return;
    }
  }else if(parentRid==0){
    ajax_route_error(403,"Creating new page [%s] requires passing "
                     "isnew=1.", zPageName);
    return;
  }

  blob_init(&content, zContent ? zContent : "", -1);
  db_begin_transaction();
  wiki_cmd_commit(zPageName, parentRid, &content, zMimetype, 0);
  rollback = wiki_ajax_emit_page_object(zPageName, 1) ? 0 : 1;
  db_end_transaction(rollback);
}

................................................................................
      wiki_ajax_emit_page_object(zName, includeContent);
    }
  }
  db_finalize(&q);
  db_end_transaction(0);
  CX("]");
}


/*
** WEBPAGE: wikiajax
**
** An internal dispatcher for wiki AJAX operations. Not for direct
** client use. All routes defined by this interface are app-internal,
** subject to change 







|
|
|
<









|










<
|
<
<
|
>












<







 







<







805
806
807
808
809
810
811
812
813
814

815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834

835


836
837
838
839
840
841
842
843
844
845
846
847
848
849

850
851
852
853
854
855
856
...
994
995
996
997
998
999
1000

1001
1002
1003
1004
1005
1006
1007
**          not send.
**
** Responds with JSON. On error, an object in the form documented by
** ajax_route_error(). On success, an object in the form documented
** for wiki_ajax_emit_page_object().
**
** The wikiajax API disallows saving of a sandbox pseudo-page, and
** will respond with an error if asked to save one. Should we want to
** enable it, it's implemented like this for any saved page for which
** is_sandbox(zPageName) is true:

**
**  db_set("sandbox",zBody,0);
**  db_set("sandbox-mimetype",zMimetype,0);
**
*/
static void wiki_ajax_route_save(void){
  const char *zPageName = P("page");
  const char *zMimetype = P("mimetype");
  const char *zContent = P("content");
  const int isNew = ajax_p_bool("isnew");
  Blob content = empty_blob;
  int parentRid = 0;
  int rollback = 0;

  if(!wiki_ajax_can_write(zPageName, &parentRid)){
    return;
  }else if(is_sandbox(zPageName)){
    ajax_route_error(403,"Saving a sandbox page is prohibited.");
    return;
  }

  /* These isNew checks are just me being pedantic. We could just as


     easily derive isNew based on whether or not the page already
     exists. */
  if(isNew){
    if(parentRid>0){
      ajax_route_error(403,"Requested a new page, "
                       "but it already exists with RID %d: %s",
                       parentRid, zPageName);
      return;
    }
  }else if(parentRid==0){
    ajax_route_error(403,"Creating new page [%s] requires passing "
                     "isnew=1.", zPageName);
    return;
  }

  blob_init(&content, zContent ? zContent : "", -1);
  db_begin_transaction();
  wiki_cmd_commit(zPageName, parentRid, &content, zMimetype, 0);
  rollback = wiki_ajax_emit_page_object(zPageName, 1) ? 0 : 1;
  db_end_transaction(rollback);
}

................................................................................
      wiki_ajax_emit_page_object(zName, includeContent);
    }
  }
  db_finalize(&q);
  db_end_transaction(0);
  CX("]");
}


/*
** WEBPAGE: wikiajax
**
** An internal dispatcher for wiki AJAX operations. Not for direct
** client use. All routes defined by this interface are app-internal,
** subject to change