Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ const en = {
'sourceControl.filter.changes': 'Changes',
'sourceControl.filter.local': 'Local',
'sourceControl.filter.remote': 'Incoming',
'sourceControl.filter.conflict': 'Conflict',
'sourceControl.filter.readyToPush': 'Ready to Push',
'sourceControl.filter.remoteChanges': 'Incoming',
'sourceControl.filter.conflicts': 'Conflicts',
Expand Down Expand Up @@ -216,10 +215,8 @@ const en = {
'sourceControl.detail.back': 'Back',
'sourceControl.mobile.filesSelected': '{count} files selected',
'sourceControl.mobile.sync': 'Sync',
'sourceControl.info.lastSync': 'Last sync: {time}',
'sourceControl.info.lastChecked': 'Last checked: {time}',
'sourceControl.info.justChecked': 'Last checked: just now',
'sourceControl.info.neverSynced': 'Never synced',
'sourceControl.search.placeholder': 'Filter by path…',
'sourceControl.search.clear': 'Clear filter',
'sourceControl.folder.selectAll': 'Select all in folder',
Expand Down
3 changes: 0 additions & 3 deletions src/i18n/locales/zh-cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ const zhCn: Partial<Record<TranslationKey, string>> = {
'sourceControl.filter.changes': '更改',
'sourceControl.filter.local': '本地',
'sourceControl.filter.remote': '传入',
'sourceControl.filter.conflict': '冲突',
'sourceControl.filter.readyToPush': '待推送',
'sourceControl.filter.remoteChanges': '传入',
'sourceControl.filter.conflicts': '冲突',
Expand Down Expand Up @@ -218,10 +217,8 @@ const zhCn: Partial<Record<TranslationKey, string>> = {
'sourceControl.detail.back': '返回',
'sourceControl.mobile.filesSelected': '已选 {count} 个文件',
'sourceControl.mobile.sync': '同步',
'sourceControl.info.lastSync': '上次同步:{time}',
'sourceControl.info.lastChecked': '上次检查:{time}',
'sourceControl.info.justChecked': '上次检查:刚刚',
'sourceControl.info.neverSynced': '尚未同步',
'sourceControl.search.placeholder': '按路径过滤…',
'sourceControl.search.clear': '清除过滤',
'sourceControl.folder.selectAll': '选取文件夹内全部项目',
Expand Down
3 changes: 0 additions & 3 deletions src/i18n/locales/zh-tw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ const zhTw: Partial<Record<TranslationKey, string>> = {
'sourceControl.filter.changes': '變更',
'sourceControl.filter.local': '本地',
'sourceControl.filter.remote': '傳入',
'sourceControl.filter.conflict': '衝突',
'sourceControl.filter.readyToPush': '待推送',
'sourceControl.filter.remoteChanges': '傳入',
'sourceControl.filter.conflicts': '衝突',
Expand Down Expand Up @@ -218,10 +217,8 @@ const zhTw: Partial<Record<TranslationKey, string>> = {
'sourceControl.detail.back': '返回',
'sourceControl.mobile.filesSelected': '已選 {count} 個檔案',
'sourceControl.mobile.sync': '同步',
'sourceControl.info.lastSync': '上次同步:{time}',
'sourceControl.info.lastChecked': '上次檢查:{time}',
'sourceControl.info.justChecked': '上次檢查:剛剛',
'sourceControl.info.neverSynced': '尚未同步',
'sourceControl.search.placeholder': '以路徑過濾…',
'sourceControl.search.clear': '清除過濾',
'sourceControl.folder.selectAll': '選取資料夾內全部項目',
Expand Down
9 changes: 4 additions & 5 deletions src/ui/source-control/FilterMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { SourceControlCounts } from '../../logic/source-control/SourceContr
* bucket in the view (see `SourceControlView`) rather than via a domain
* change. Surfaced as an opt-in overview; the default stays on Needs Sync
* so a quiet workspace stays quiet.
* - **Incoming / Conflict / Synced** — the matching domain filters (chip id stays `remote`; the label reads "Incoming" — a file only on the remote, or changed only on the remote, is something coming *in*).
* - **Incoming / Synced** — the matching domain filters (chip id stays `remote`; the label reads "Incoming" — a file only on the remote, or changed only on the remote, is something coming *in*).
*
* "Local" (domain `changes`) is intentionally not a chip: Needs Sync already
* covers local-side changes, and a standalone local-only view added a
Expand All @@ -30,12 +30,11 @@ export interface FilterChip {
count: (counts: SourceControlCounts) => number;
}

/** The five filter chips, in display order. */
/** The four filter chips, in display order. */
export const FILTER_CHIPS: readonly FilterChip[] = [
{ id: 'all', filter: 'all', showSynced: true, labelKey: 'sourceControl.filter.all', count: c => c.all + c.synced },
{ id: 'needsSync', filter: 'all', showSynced: false, labelKey: 'sourceControl.filter.needsSync', count: c => c.all },
{ id: 'remote', filter: 'remote-changes', showSynced: false, labelKey: 'sourceControl.filter.remote', count: c => c['remote-changes'] },
{ id: 'conflict', filter: 'conflicts', showSynced: false, labelKey: 'sourceControl.filter.conflict', count: c => c.conflicts },
{ id: 'synced', filter: 'synced', showSynced: true, labelKey: 'sourceControl.filter.synced', count: c => c.synced },
];

Expand All @@ -55,8 +54,8 @@ export interface FilterMenuOptions {
}

/**
* Renders the Source Control filter row: five chips — All / Needs Sync /
* Incoming / Conflict / Synced. On mobile a single `<select>` dropdown
* Renders the Source Control filter row: four chips — All / Needs Sync /
* Incoming / Synced. On mobile a single `<select>` dropdown
* replaces the chips (same chip ids, counts inline as "Label (N)").
*
* Per-filter counts come straight from the ViewModel's single-source counts
Expand Down
10 changes: 0 additions & 10 deletions src/ui/source-control/SourceControlHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ export interface SourceControlWorkspaceInfo {
serviceName: string;
branch: string;
vaultFolder: string;
/** Epoch ms of the most recent successful push/pull, or 0 if nothing has synced yet. */
lastSyncTime: number;
/**
* Epoch ms the Source Control view last completed a status refresh
* (any reason — manual, startup, local-change), or 0 if it hasn't
Expand Down Expand Up @@ -104,14 +102,6 @@ function renderInfoStrip(container: HTMLElement, info: SourceControlWorkspaceInf
folder.createSpan({ text: ` ${info.vaultFolder}` });
}

strip.createSpan({ cls: 'scv-info-sep', text: '·' });
strip.createSpan({
cls: 'scv-info-time',
text: info.lastSyncTime > 0
? t('sourceControl.info.lastSync', { time: new Date(info.lastSyncTime).toLocaleTimeString() })
: t('sourceControl.info.neverSynced'),
});

if (info.lastCheckedAt > 0) {
strip.createSpan({ cls: 'scv-info-sep', text: '·' });
const elapsed = Date.now() - info.lastCheckedAt;
Expand Down
4 changes: 1 addition & 3 deletions src/ui/source-control/SourceControlItemView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,8 @@ export class SourceControlItemView extends ItemView {

private getWorkspaceInfo(): SourceControlWorkspaceInfo {
const info = this.plugin.syncWorkspace.getInfo();
const lastSyncTime = Object.values(this.plugin.settings.syncMetadata)
.reduce((latest, metadata) => Math.max(latest, metadata.lastSyncedAt), 0);
const lastCheckedAt = this.plugin.refreshState.getLastCheckedAt();
return { ...info, lastSyncTime, lastCheckedAt };
return { ...info, lastCheckedAt };
}

getViewType(): string { return SOURCE_CONTROL_VIEW_TYPE; }
Expand Down
8 changes: 4 additions & 4 deletions tests/ui/source-control/FilterMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ describe('renderFilterMenu', () => {
callbacks = { onFilterChange: vi.fn() };
});

it('renders the five chips (All/Needs Sync/Incoming/Conflict/Synced)', () => {
it('renders the four chips (All/Needs Sync/Incoming/Synced)', () => {
renderFilterMenu(container, { filter: 'all', showSynced: false }, zeroCounts, callbacks);

const chips = Array.from(container.querySelectorAll('.scv-filter-option')).map(el => el.getAttribute('data-filter'));
expect(chips).toEqual(['all', 'needsSync', 'remote', 'conflict', 'synced']);
expect(chips).toEqual(['all', 'needsSync', 'remote', 'synced']);
});

it('labels the chips with their display names', () => {
renderFilterMenu(container, { filter: 'all', showSynced: false }, zeroCounts, callbacks);

const labels = Array.from(container.querySelectorAll('.scv-filter-option .scv-filter-label')).map(el => el.textContent);
expect(labels).toEqual(['All', 'Needs Sync', 'Incoming', 'Conflict', 'Synced']);
expect(labels).toEqual(['All', 'Needs Sync', 'Incoming', 'Synced']);
});

it('marks the current (filter, showSynced) chip as active', () => {
Expand Down Expand Up @@ -66,6 +66,6 @@ describe('renderFilterMenu', () => {
expect(container.querySelector('.scv-filter-dropdown')).not.toBeNull();
expect(container.querySelector('.scv-filter-option')).toBeNull();
const options = Array.from(container.querySelectorAll('.scv-filter-dropdown option')).map(o => (o as HTMLOptionElement).value);
expect(options).toEqual(['all', 'needsSync', 'remote', 'conflict', 'synced']);
expect(options).toEqual(['all', 'needsSync', 'remote', 'synced']);
});
});
26 changes: 11 additions & 15 deletions tests/ui/source-control/SourceControlView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ function buildView(changes: SyncChange[], callbacks: Partial<SourceControlViewCa
serviceName: 'GitHub',
branch: 'main',
vaultFolder: '',
lastSyncTime: 0,
lastCheckedAt: 0,
}));
return { view, selection, operations, refreshState, refreshSource, onSync, onRefresh };
Expand All @@ -46,7 +45,6 @@ function buildViewWithRepository(changes: SyncChange[], callbacks: Partial<Sourc
serviceName: 'GitHub',
branch: 'main',
vaultFolder: '',
lastSyncTime: 0,
lastCheckedAt: 0,
}));
return { view, repository, selection, operations, refreshState, refreshSource, onSync, onRefresh };
Expand Down Expand Up @@ -110,23 +108,23 @@ describe('SourceControlView', () => {

it('shows a flat tree (no sections) once a specific filter is selected', () => {
const { view } = buildView([
{ id: toChangeId('c-1'), path: 'a.md', kind: 'conflict' },
{ id: toChangeId('c-1'), path: 'a.md', kind: 'remote-only' },
{ id: toChangeId('c-2'), path: 'b.md', kind: 'local-only' },
]);
view.render(container);

(container.querySelector('.scv-filter-option[data-filter="conflict"]') as HTMLButtonElement).click();
(container.querySelector('.scv-filter-option[data-filter="remote"]') as HTMLButtonElement).click();

expect(container.querySelectorAll('.scv-section')).toHaveLength(0);
expect(container.querySelectorAll('.scv-change-item')).toHaveLength(1);
expect(view.getFilter()).toBe('conflicts');
expect(view.getFilter()).toBe('remote-changes');
});

it('shows the empty state when the active filter has no items', () => {
const { view } = buildView([{ id: toChangeId('c-1'), path: 'a.md', kind: 'local-only' }]);
view.render(container);

(container.querySelector('.scv-filter-option[data-filter="conflict"]') as HTMLButtonElement).click();
(container.querySelector('.scv-filter-option[data-filter="remote"]') as HTMLButtonElement).click();

expect(container.querySelector('.scv-empty')).not.toBeNull();
});
Expand Down Expand Up @@ -1284,7 +1282,7 @@ describe('SourceControlView', () => {
const view = new SourceControlView(
viewModel,
{ onSync: vi.fn(), onRefresh: vi.fn() },
() => ({ serviceName: 'GitHub', branch: 'main', vaultFolder: '', lastSyncTime: 0, lastCheckedAt }),
() => ({ serviceName: 'GitHub', branch: 'main', vaultFolder: '', lastCheckedAt }),
);
return { view, refreshState };
}
Expand All @@ -1294,28 +1292,26 @@ describe('SourceControlView', () => {
view.render(container);

const infoTimes = container.querySelectorAll('.scv-info-time');
// Only the "Never synced" line is present; no "Last checked" line.
expect(infoTimes).toHaveLength(1);
expect(infoTimes[0]?.textContent).toBe('Never synced');
expect(infoTimes).toHaveLength(0);
});

it('shows "Last checked: just now" when the last refresh was within a minute', () => {
const { view } = buildViewWithInfo(Date.now());
view.render(container);

const infoTimes = container.querySelectorAll('.scv-info-time');
expect(infoTimes).toHaveLength(2);
expect(infoTimes[1]?.textContent).toBe('Last checked: just now');
expect(infoTimes).toHaveLength(1);
expect(infoTimes[0]?.textContent).toBe('Last checked: just now');
});

it('shows "Last checked: <time>" when the last refresh was over a minute ago', () => {
const { view } = buildViewWithInfo(Date.now() - 120_000);
view.render(container);

const infoTimes = container.querySelectorAll('.scv-info-time');
expect(infoTimes).toHaveLength(2);
expect(infoTimes[1]?.textContent).toContain('Last checked:');
expect(infoTimes[1]?.textContent).not.toBe('Last checked: just now');
expect(infoTimes).toHaveLength(1);
expect(infoTimes[0]?.textContent).toContain('Last checked:');
expect(infoTimes[0]?.textContent).not.toBe('Last checked: just now');
});
});
});
Loading