/ src / ui / update-preview-modal.ts
update-preview-modal.ts
  1  /**
  2   * Update Preview Modal
  3   *
  4   * Shows update summary with accept/reject actions
  5   */
  6  
  7  import { App, Modal, Setting } from 'obsidian';
  8  import { FetchResult } from '../services/git-service';
  9  import { UpdateSummary } from '../services/update-summary-service';
 10  
 11  export class UpdatePreviewModal extends Modal {
 12    private nodeName: string;
 13    private updateStatus: FetchResult;
 14    private summary: UpdateSummary;
 15    private onAccept: () => void;
 16    private onReject: () => void;
 17  
 18    constructor(
 19      app: App,
 20      nodeName: string,
 21      updateStatus: FetchResult,
 22      summary: UpdateSummary,
 23      onAccept: () => void,
 24      onReject: () => void
 25    ) {
 26      super(app);
 27      this.nodeName = nodeName;
 28      this.updateStatus = updateStatus;
 29      this.summary = summary;
 30      this.onAccept = onAccept;
 31      this.onReject = onReject;
 32    }
 33  
 34    onOpen() {
 35      const { contentEl } = this;
 36      contentEl.empty();
 37  
 38      console.log('[UpdatePreviewModal] Opening with data:', {
 39        nodeName: this.nodeName,
 40        commitCount: this.updateStatus.commits.length,
 41        commits: this.updateStatus.commits,
 42        summary: this.summary
 43      });
 44  
 45      // Title
 46      contentEl.createEl('h2', { text: `Updates Available for ${this.nodeName}` });
 47  
 48      // Overall impact (highlighted)
 49      const impactEl = contentEl.createDiv({ cls: 'update-preview-impact' });
 50      impactEl.createEl('strong', { text: this.summary.overallImpact });
 51  
 52      // Summary section
 53      contentEl.createEl('h3', { text: "What's New" });
 54      const userFacingEl = contentEl.createDiv({ cls: 'update-preview-section' });
 55      userFacingEl.createEl('p', { text: this.summary.userFacingChanges });
 56  
 57      // Technical improvements
 58      contentEl.createEl('h3', { text: 'Technical Improvements' });
 59      const technicalEl = contentEl.createDiv({ cls: 'update-preview-section' });
 60      technicalEl.createEl('p', { text: this.summary.technicalImprovements });
 61  
 62      // Stats
 63      contentEl.createEl('h3', { text: 'Update Details' });
 64      const statsEl = contentEl.createDiv({ cls: 'update-preview-stats' });
 65      statsEl.createEl('p', { text: `📊 ${this.updateStatus.commits.length} commits` });
 66      statsEl.createEl('p', { text: `📁 ${this.updateStatus.filesChanged} files changed` });
 67      statsEl.createEl('p', { text: `➕ ${this.updateStatus.insertions} lines added` });
 68      statsEl.createEl('p', { text: `➖ ${this.updateStatus.deletions} lines removed` });
 69  
 70      // Commit list (collapsible)
 71      const commitsEl = contentEl.createDiv({ cls: 'update-preview-commits' });
 72      const detailsEl = commitsEl.createEl('details');
 73      detailsEl.createEl('summary', { text: `View ${this.updateStatus.commits.length} commit${this.updateStatus.commits.length > 1 ? 's' : ''}` });
 74  
 75      const commitList = detailsEl.createEl('ul');
 76      this.updateStatus.commits.forEach((commit) => {
 77        const date = new Date(commit.timestamp * 1000).toLocaleDateString();
 78        const li = commitList.createEl('li');
 79        li.createEl('strong', { text: commit.subject });
 80        li.createEl('br');
 81        li.createEl('span', {
 82          text: `${commit.author} • ${date}`,
 83          cls: 'update-preview-commit-meta'
 84        });
 85        if (commit.body) {
 86          li.createEl('br');
 87          // Format commit body: convert bullet points to clean list
 88          const formattedBody = this.formatCommitBody(commit.body);
 89          li.createEl('span', { text: formattedBody, cls: 'update-preview-commit-body' });
 90        }
 91      });
 92  
 93      // Action buttons
 94      const buttonContainer = contentEl.createDiv({ cls: 'modal-button-container' });
 95  
 96      new Setting(buttonContainer)
 97        .addButton((btn) =>
 98          btn
 99            .setButtonText('Apply Update')
100            .setCta()
101            .onClick(() => {
102              console.log('[UpdatePreviewModal] User clicked Apply');
103              this.close();
104              this.onAccept();
105            })
106        )
107        .addButton((btn) =>
108          btn
109            .setButtonText('Not Now')
110            .onClick(() => {
111              console.log('[UpdatePreviewModal] User clicked Not Now');
112              this.close();
113              this.onReject();
114            })
115        );
116  
117      // Add custom styles
118      this.addStyles();
119    }
120  
121    onClose() {
122      const { contentEl } = this;
123      contentEl.empty();
124    }
125  
126    /**
127     * Format commit body text for display
128     * Converts bullet points (- item) to clean comma-separated list
129     */
130    private formatCommitBody(body: string): string {
131      // Split by newlines and filter out empty lines
132      const lines = body.split('\n').map(l => l.trim()).filter(l => l.length > 0);
133  
134      // Convert bullet points to clean list
135      const cleaned = lines.map(line => {
136        // Remove leading dash/bullet
137        return line.replace(/^[-*•]\s*/, '');
138      });
139  
140      // Join with commas for cleaner reading
141      return cleaned.join(', ');
142    }
143  
144    private addStyles() {
145      const style = document.createElement('style');
146      style.textContent = `
147        .update-preview-impact {
148          background: var(--background-secondary);
149          padding: 1em;
150          border-radius: 8px;
151          margin: 1em 0;
152          font-size: 1.1em;
153          text-align: center;
154        }
155  
156        .update-preview-section {
157          margin: 0.5em 0 1.5em 0;
158          line-height: 1.6;
159        }
160  
161        .update-preview-stats {
162          display: grid;
163          grid-template-columns: 1fr 1fr;
164          gap: 0.5em;
165          margin: 1em 0;
166        }
167  
168        .update-preview-stats p {
169          margin: 0;
170          padding: 0.5em;
171          background: var(--background-secondary);
172          border-radius: 4px;
173        }
174  
175        .update-preview-commits {
176          margin: 1em 0 2em 0;
177        }
178  
179        .update-preview-commits details {
180          background: var(--background-secondary);
181          padding: 1em;
182          border-radius: 8px;
183        }
184  
185        .update-preview-commits summary {
186          cursor: pointer;
187          font-weight: bold;
188          margin-bottom: 0.5em;
189        }
190  
191        .update-preview-commits ul {
192          margin-top: 1em;
193          padding-left: 1.5em;
194        }
195  
196        .update-preview-commits li {
197          margin: 1em 0;
198          line-height: 1.6;
199        }
200  
201        .update-preview-commit-meta {
202          color: var(--text-muted);
203          font-size: 0.9em;
204        }
205  
206        .update-preview-commit-body {
207          color: var(--text-muted);
208          font-size: 0.9em;
209          font-style: italic;
210        }
211  
212        .modal-button-container {
213          margin-top: 2em;
214          display: flex;
215          justify-content: flex-end;
216          gap: 0.5em;
217        }
218      `;
219      document.head.appendChild(style);
220    }
221  }