Add table view (#293)

* chore: bump version

* feat: add basic table view

* feat: format time data as a time string

* feat: add basePath

* refactor: remove unused handlers

* feat: add context menu to table

* feat: add title support

* feat: remove slash from path

* feat: handle custom rendering of elements

* feat: handle row click

* feat: add min width to columns

* feat: add hover over

* feat: add file icons

* feat: enable table setting
This commit is contained in:
DecafDev 2024-08-02 18:04:44 -06:00 committed by GitHub
parent fca7f1c5cd
commit aae9d6c394
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 671 additions and 46 deletions

View file

@ -1,7 +1,7 @@
{
"id": "vault-explorer",
"name": "Vault Explorer",
"version": "1.38.0",
"version": "1.39.0",
"minAppVersion": "1.4.13",
"description": "Explore your vault in visual format",
"author": "DecafDev",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-vault-explorer",
"version": "1.38.0",
"version": "1.39.0",
"description": "Explore your vault in visual format",
"main": "main.js",
"scripts": {

View file

@ -95,7 +95,12 @@ export const DEFAULT_SETTINGS: VaultExplorerPluginSettings = {
filterGroupsWidth: "300px",
shouldWrapFilterGroups: false,
pageSize: 25,
viewOrder: [TExplorerView.GRID, TExplorerView.LIST, TExplorerView.FEED],
viewOrder: [
TExplorerView.GRID,
TExplorerView.LIST,
TExplorerView.TABLE,
TExplorerView.FEED,
],
configDir: ".vaultexplorer",
pluginVersion: null,
logLevel: LOG_LEVEL_WARN,

View file

@ -28,6 +28,7 @@ import Migrate_1_31_0 from "./migrate_1_31_0";
import Migrate_1_33_0 from "./migrate_1_33_0";
import Migrate_1_37_0 from "./migrate_1_37_0";
import Migrate_1_38_0 from "./migrate_1_38_0";
import Migrate_1_39_0 from "./migrate_1_39_0";
const migrations: TMigration[] = [
{
@ -165,6 +166,11 @@ const migrations: TMigration[] = [
to: "1.38.0",
migrate: Migrate_1_38_0,
},
{
from: "1.38.0",
to: "1.39.0",
migrate: Migrate_1_39_0,
},
];
export const preformMigrations = (

View file

@ -1,10 +1,11 @@
import { VaultExplorerPluginSettings_1_37_2 } from "src/types/types-1-37-0";
import MigrationInterface from "./migration_interface";
import { VaultExplorerPluginSettings } from "src/types";
import { VaultExplorerPluginSettings_1_38_0 } from "src/types/types-1.38.0";
export default class Migrate_1_38_0 implements MigrationInterface {
migrate(data: Record<string, unknown>) {
const typedData = data as unknown as VaultExplorerPluginSettings_1_37_2;
const typedData = data as unknown as VaultExplorerPluginSettings_1_38_0;
const newData: VaultExplorerPluginSettings = {
...typedData,
loadBodyTags: true,

View file

@ -0,0 +1,26 @@
import MigrationInterface from "./migration_interface";
import { TExplorerView, VaultExplorerPluginSettings } from "src/types";
import { VaultExplorerPluginSettings_1_38_0 } from "src/types/types-1.38.0";
export default class Migrate_1_39_0 implements MigrationInterface {
migrate(data: Record<string, unknown>) {
const typedData = data as unknown as VaultExplorerPluginSettings_1_38_0;
const newData: VaultExplorerPluginSettings = {
...typedData,
viewOrder: [
TExplorerView.GRID,
TExplorerView.LIST,
TExplorerView.TABLE,
TExplorerView.FEED,
],
views: {
...typedData.views,
table: {
isEnabled: true,
},
},
};
return newData as unknown as Record<string, unknown>;
}
}

View file

@ -247,8 +247,6 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab {
new Setting(containerEl).setName("Table view").addToggle((toggle) =>
toggle
.setDisabled(true) //TODO implement
.setTooltip("This view is not yet implemented.")
.setValue(this.plugin.settings.views.table.isEnabled)
.onChange(async (value) => {
this.plugin.settings.views.table.isEnabled = value;

View file

@ -36,6 +36,7 @@ export default class VaultExplorerView extends ItemView {
const containerEl = this.containerEl.children[1];
store.plugin.set(this.plugin);
this.component = new VaultExplorerApp({
target: containerEl,
});

View file

@ -49,8 +49,8 @@
const dispatch = createEventDispatcher();
let plugin: VaultExplorerPlugin;
store.plugin.subscribe((value) => {
plugin = value;
store.plugin.subscribe((p) => {
plugin = p;
enableFileIcons = plugin.settings.enableFileIcons;
removeH1 = plugin.settings.views.feed.removeH1;
collapseStyle = plugin.settings.views.feed.collapseStyle;
@ -158,10 +158,6 @@
});
}
function handleTitleContextMenu(e: Event) {
handleCardContextMenu(e);
}
function handleCardMouseOver(e: MouseEvent) {
const targetEl = e.currentTarget as HTMLElement;
plugin.app.workspace.trigger("hover-link", {
@ -233,14 +229,6 @@
role="link"
class="vault-explorer-feed-card__title"
on:focus={() => {}}
on:click={(e) => {
e.preventDefault();
handleTitleClick();
}}
on:contextmenu={(e) => {
e.preventDefault();
handleTitleContextMenu(e);
}}
on:keydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
@ -285,7 +273,7 @@
}
.vault-explorer-feed-card:focus-visible {
box-shadow: 0 0 0 3px var(--background-modifier-border-focus);
box-shadow: 0 0 0 3px var(--background-modifier-focus);
}
.vault-explorer-feed-card__title {

View file

@ -159,10 +159,6 @@
handleCardClick();
}
function handleTitleContextMenu(e: Event) {
handleCardContextMenu(e);
}
$: hasFooterContent =
tags != null || custom1 != null || custom2 != null || custom3 != null;
@ -249,10 +245,6 @@
e.preventDefault();
handleTitleClick();
}}
on:contextmenu={(e) => {
e.preventDefault();
handleTitleContextMenu(e);
}}
on:keydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();

View file

@ -106,10 +106,6 @@
openInCurrentTab(plugin, path);
}
function handleTitleContextMenu(e: Event) {
handleItemContextMenu(e);
}
function handleFavoriteChange(filePath: string, value: boolean) {
dispatch("favoritePropertyChange", { filePath, value });
}
@ -166,14 +162,6 @@
role="link"
class={titleClassName}
on:focus={() => {}}
on:click={(e) => {
e.preventDefault();
handleTitleClick();
}}
on:contextmenu={(e) => {
e.preventDefault();
handleTitleContextMenu(e);
}}
on:keydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();

View file

@ -0,0 +1,278 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte";
import { openContextMenu } from "../services/context-menu";
import { formatAsBearTimeString } from "../services/time-string";
import { FileRenderData } from "../types";
import store from "src/svelte/shared/services/store";
import VaultExplorerPlugin from "src/main";
import Tag from "src/svelte/shared/components/tag.svelte";
import Wrap from "src/svelte/shared/components/wrap.svelte";
import { openInCurrentTab } from "../services/open-file";
import { HOVER_LINK_SOURCE_ID } from "src/constants";
import Stack from "src/svelte/shared/components/stack.svelte";
import { getIconIdForFile } from "../services/file-icon";
import Icon from "src/svelte/shared/components/icon.svelte";
import EventManager from "src/event/event-manager";
import { PluginEvent } from "src/event/types";
interface TColumn {
key: string;
label: string;
classNames?: string;
format?: (value: unknown) => string;
}
export let data: FileRenderData[];
export let startIndex: number;
export let pageLength: number;
let filteredItems: FileRenderData[] = [];
let plugin: VaultExplorerPlugin | null = null;
let enableFileIcons: boolean = true;
let columns: TColumn[] = [
{
key: "baseName",
label: "Name",
classNames: "vault-explorer-table-view__title-text",
},
{ key: "extension", label: "Extension" },
{ key: "basePath", label: "Folder" },
{
key: "tags",
label: "Tags",
},
{
key: "isFavorite",
label: "Favorite",
format: (value: unknown) => (value === true ? "Yes" : "No"),
},
{
key: "createdMillis",
label: "Created",
format: (value: unknown) => formatAsBearTimeString(value as number),
},
{
key: "modifiedMillis",
label: "Modified",
format: (value: unknown) => formatAsBearTimeString(value as number),
},
];
const dispatch = createEventDispatcher();
store.plugin.subscribe((p) => {
plugin = p;
enableFileIcons = plugin.settings.enableFileIcons;
});
onMount(() => {
function handleFileIconsChange() {
if (plugin === null) return;
enableFileIcons = plugin.settings.enableFileIcons;
}
EventManager.getInstance().on(
PluginEvent.FILE_ICONS_SETTING_CHANGE,
handleFileIconsChange,
);
return () => {
EventManager.getInstance().off(
PluginEvent.FILE_ICONS_SETTING_CHANGE,
handleFileIconsChange,
);
};
});
function handleFavoriteChange(filePath: string, value: boolean) {
dispatch("favoritePropertyChange", { filePath, value });
}
function handleRowClick(path: string) {
if (plugin === null) return;
openInCurrentTab(plugin, path);
}
function handleRowMouseOver(e: MouseEvent, path: string) {
if (plugin === null) return;
const targetEl = e.currentTarget as HTMLElement;
plugin.app.workspace.trigger("hover-link", {
event: e,
linktext: path,
source: HOVER_LINK_SOURCE_ID,
targetEl,
hoverParent: targetEl.parentElement,
});
}
function handleRowContextMenu(
e: Event,
path: string,
isFavorite: boolean | null,
) {
if (plugin === null) return;
const nativeEvent = e as MouseEvent;
openContextMenu(plugin, path, nativeEvent, {
isFavorite,
onFavoriteChange: handleFavoriteChange,
});
}
function getValue(item: FileRenderData, column: TColumn): unknown {
const { key, format } = column;
const itemValue = item[key as keyof FileRenderData] ?? "";
if (format !== undefined) {
return format(itemValue);
}
return itemValue;
}
function asStringArray(value: unknown): string[] {
return value as string[];
}
$: {
if (startIndex < data.length) {
filteredItems = Array.from({ length: pageLength }, (_, i) => {
const index = startIndex + i;
return data[index];
});
} else {
filteredItems = [];
}
}
</script>
<div class="vault-explorer-table-view">
<table>
<thead>
<tr>
{#each columns as column (column.key)}
<th>{column.label}</th>
{/each}
</tr>
</thead>
<tbody>
{#each filteredItems as filteredItem}
<tr
tabindex="0"
role="button"
class="vault-explorer-list-item"
on:click={() => handleRowClick(filteredItem.path)}
on:keydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleRowClick(filteredItem.path);
}
}}
on:focus={() => {}}
on:contextmenu={(e) =>
handleRowContextMenu(
e,
filteredItem.path,
filteredItem.isFavorite,
)}
on:mouseover={(e) =>
handleRowMouseOver(e, filteredItem.path)}
>
{#each columns as column (column.key)}
{@const value = getValue(filteredItem, column)}
<td>
{#if column.key == "tags"}
<Wrap spacingX="sm" spacingY="sm">
{#each asStringArray(value) as tag}
<Tag name={tag} variant="unstyled" />
{/each}
</Wrap>
{:else if column.key == "baseName"}
<div class="vault-explorer-table-view__title">
<Stack spacing="xs">
{#if enableFileIcons}
<Icon
iconId={getIconIdForFile(
filteredItem.baseName,
filteredItem.extension,
)}
/>
{/if}
<div
class="vault-explorer-table-view__title-text"
>
{value}
</div>
</Stack>
</div>
{:else}
<div>{value}</div>
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
<style>
.vault-explorer-table-view {
width: 100%;
overflow-x: auto;
}
.vault-explorer-table-view table {
border-collapse: collapse;
}
.vault-explorer-table-view th:first-child,
.vault-explorer-table-view td:first-child {
width: 300px;
max-width: 300px;
overflow: hidden;
}
.vault-explorer-table-view__title {
width: 100%;
}
.vault-explorer-table-view__title-text {
color: var(--text-accent);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.vault-explorer-table-view th {
text-align: left;
}
.vault-explorer-table-view th,
.vault-explorer-table-view td {
border: 1px solid var(--background-modifier-border);
padding: 8px;
}
.vault-explorer-table-view th:nth-child(4) {
min-width: 250px;
}
.vault-explorer-table-view th:nth-child(6) {
min-width: 175px;
}
.vault-explorer-table-view th:nth-child(7) {
min-width: 175px;
}
.vault-explorer-table-view tr:hover:not(:has(th)) {
background-color: var(--background-modifier-hover);
}
.vault-explorer-table-view tr:focus-visible:not(:has(th)) {
box-shadow: 0 0 0 3px var(--background-modifier-border-focus);
}
</style>

View file

@ -60,6 +60,7 @@
favoritesStore,
TFavoritesCache,
} from "./services/favorites-store";
import TableView from "./components/table-view.svelte";
// ============================================
// Variables
@ -965,6 +966,13 @@
{pageLength}
on:favoritePropertyChange={handleFavoritePropertyChange}
/>
{:else if currentView === "table"}
<TableView
data={renderData}
{startIndex}
{pageLength}
on:favoritePropertyChange={handleFavoritePropertyChange}
/>
{:else if currentView === "feed"}
<FeedView
data={renderData}

View file

@ -221,12 +221,16 @@ export const formatFileDataForRender = ({
}
const displayName = extension === "md" ? basename : name;
const basePath = path.includes("/")
? path.substring(0, path.lastIndexOf("/"))
: "/";
return {
id: fileId,
displayName,
baseName: basename,
path,
baseName: basename,
basePath,
extension,
url,
content: fileContent,

View file

@ -2,6 +2,7 @@ export interface FileRenderData {
id: string;
displayName: string;
path: string;
basePath: string;
extension: string;
baseName: string;
content: string | null;

327
src/types/types-1.38.0.ts Normal file
View file

@ -0,0 +1,327 @@
export interface VaultExplorerPluginSettings_1_38_0 {
properties: {
favorite: string;
url: string;
image: string;
createdDate: string;
modifiedDate: string;
custom1: string;
custom2: string;
custom3: string;
};
filters: {
search: TSearchFilter;
favorites: TFavoritesFilter;
sort: TSortFilter;
timestamp: TTimestampFilter;
custom: TCustomFilter;
};
views: {
dashboard: TDashboardView;
grid: TGridView;
list: TListView;
table: TTableView;
feed: TFeedView;
recommended: TRecommendedView;
related: TRelatedView;
};
titleWrapping: WordBreak;
enableClockUpdates: boolean;
enableFileIcons: boolean;
loadBodyTags: boolean;
currentView: TExplorerView | null;
pageSize: number;
filterGroupsWidth: string;
shouldWrapFilterGroups: boolean;
viewOrder: TExplorerView[];
configDir: string;
pluginVersion: string | null;
logLevel: string;
}
type FileInteractionStyle = "title" | "content";
interface BaseView {
isEnabled: boolean;
}
interface TTableView extends BaseView {}
interface TListView extends BaseView {
showTags: boolean;
}
interface TGridView extends BaseView {
coverImageSources: CoverImageSource[];
loadSocialMediaImage: boolean;
}
interface CoverImageSource {
type: CoverImageSourceType;
isEnabled: boolean;
}
type CoverImageSourceType =
| "image-property"
| "url-property"
| "frontmatter"
| "body";
interface TDashboardView extends BaseView {}
type CollapseStyle = "no-new-lines" | "no-extra-new-lines";
interface TFeedView extends BaseView {
collapseStyle: CollapseStyle;
removeH1: boolean;
lineClampSmall: number;
lineClampMedium: number;
lineClampLarge: number;
}
interface TRecommendedView extends BaseView {}
interface TRelatedView extends BaseView {}
interface BaseFilter {
isEnabled: boolean;
}
interface TSearchFilter extends BaseFilter {
value: string;
}
interface TFavoritesFilter extends BaseFilter {
value: boolean;
}
interface TSortFilter extends BaseFilter {
value: SortFilterOption;
}
interface TTimestampFilter extends BaseFilter {
value: TimestampFilterOption;
}
interface TCustomFilter extends BaseFilter {
selectedGroupId: string;
groups: TFilterGroup[];
}
type TimestampFilterOption =
| "created-today"
| "modified-today"
| "created-this-week"
| "modified-this-week"
| "created-2-weeks"
| "modified-2-weeks"
| "all";
type SortFilterOption =
| "file-name-asc"
| "file-name-desc"
| "modified-asc"
| "modified-desc"
| "random";
type WordBreak = "normal" | "break-word";
enum TExplorerView {
DASHBOARD = "dashboard",
GRID = "grid",
LIST = "list",
FEED = "feed",
TABLE = "table",
RECOMMENDED = "recommended",
RELATED = "related",
}
type FilterOperator = "and" | "or";
enum TextFilterCondition {
IS = "is",
IS_NOT = "is-not",
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
STARTS_WITH = "starts-with",
ENDS_WITH = "ends-with",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum ListFilterCondition {
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum NumberFilterCondition {
IS_EQUAL = "is-equal",
IS_NOT_EQUAL = "is-not-equal",
IS_GREATER = "is-greater",
IS_LESS = "is-less",
IS_GREATER_OR_EQUAL = "is-greater-or-equal",
IS_LESS_OR_EQUAL = "is-less-or-equal",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum CheckboxFilterCondition {
IS = "is",
IS_NOT = "is-not",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum DateFilterCondition {
IS = "is",
IS_BEFORE = "is-before",
IS_AFTER = "is-after",
IS_ON_OR_BEFORE = "is-on-or-before",
IS_ON_OR_AFTER = "is-on-or-after",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum ContentFilterCondition {
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
IS_EMPTY = "is-empty",
IS_NOT_EMPTY = "is-not-empty",
}
enum FolderFilterCondition {
IS = "is",
IS_NOT = "is-not",
}
enum FileNameFilterCondition {
IS = "is",
IS_NOT = "is-not",
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
STARTS_WITH = "starts-with",
ENDS_WITH = "ends-with",
}
type FilterCondition =
| TextFilterCondition
| NumberFilterCondition
| DateFilterCondition
| CheckboxFilterCondition
| ListFilterCondition
| ContentFilterCondition
| FolderFilterCondition
| FileNameFilterCondition;
//This matches the Obsidian property types
enum PropertyType {
TEXT = "text",
NUMBER = "number",
LIST = "list",
CHECKBOX = "checkbox",
DATE = "date",
DATETIME = "datetime",
}
enum FilterRuleType {
PROPERTY = "property",
FOLDER = "folder",
FILE_NAME = "file-name",
CONTENT = "content",
}
enum DatePropertyFilterValue {
TODAY = "today",
TOMORROW = "tomorrow",
YESTERDAY = "yesterday",
ONE_WEEK_FROM_NOW = "one-week-from-now",
ONE_WEEK_AGO = "one-week-ago",
ONE_MONTH_FROM_NOW = "one-month-from-now",
ONE_MONTH_AGO = "one-month-ago",
CUSTOM = "custom",
}
interface BaseFilterRule {
id: string;
operator: FilterOperator;
type: FilterRuleType;
condition: FilterCondition;
isEnabled: boolean;
value: string;
matchWhenPropertyDNE: boolean;
}
interface TextPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.TEXT;
propertyName: string;
condition: TextFilterCondition;
}
interface NumberPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.NUMBER;
propertyName: string;
condition: NumberFilterCondition;
}
interface ListPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.LIST;
propertyName: string;
condition: ListFilterCondition;
}
interface CheckboxPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.CHECKBOX;
propertyName: string;
condition: CheckboxFilterCondition;
}
interface DatePropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.DATE | PropertyType.DATETIME;
propertyName: string;
condition: DateFilterCondition;
valueData: string;
}
interface FolderFilterRule extends BaseFilterRule {
type: FilterRuleType.FOLDER;
condition: FolderFilterCondition;
includeSubfolders: boolean;
}
interface FileNameFilterRule extends BaseFilterRule {
type: FilterRuleType.FILE_NAME;
condition: FileNameFilterCondition;
}
interface ContentFilterRule extends BaseFilterRule {
type: FilterRuleType.CONTENT;
condition: ContentFilterCondition;
}
type TFilterRule =
| PropertyFilterRule
| FolderFilterRule
| FileNameFilterRule
| ContentFilterRule;
type PropertyFilterRule =
| TextPropertyFilterRule
| NumberPropertyFilterRule
| ListPropertyFilterRule
| CheckboxPropertyFilterRule
| DatePropertyFilterRule;
interface TFilterGroup {
id: string;
name: string;
rules: TFilterRule[];
isEnabled: boolean;
isSticky: boolean;
}

View file

@ -75,7 +75,7 @@
"showTags": true
},
"table": {
"isEnabled": false
"isEnabled": true
},
"feed": {
"isEnabled": true,
@ -95,7 +95,7 @@
"isEnabled": false
}
},
"currentView": "grid",
"currentView": "table",
"titleWrapping": "normal",
"enableClockUpdates": true,
"enableFileIcons": true,
@ -106,9 +106,10 @@
"viewOrder": [
"grid",
"list",
"table",
"feed"
],
"configDir": ".vaultexplorer",
"pluginVersion": "1.38.0",
"pluginVersion": "1.39.0",
"logLevel": "trace"
}

View file

@ -130,5 +130,6 @@
"1.37.0": "1.4.13",
"1.37.1": "1.4.13",
"1.37.2": "1.4.13",
"1.38.0": "1.4.13"
"1.38.0": "1.4.13",
"1.39.0": "1.4.13"
}