@lapis-notes/bases 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -1
  3. package/dist/bases-plugin.js +18 -21
  4. package/dist/bases-view/bases.svelte.d.ts +7 -0
  5. package/dist/bases-view/bases.svelte.js +37 -7
  6. package/dist/bases-view/card-view/components/card-placeholder.svelte +7 -3
  7. package/dist/bases-view/card-view/components/card.svelte +23 -15
  8. package/dist/bases-view/cell.svelte +147 -16
  9. package/dist/bases-view/cell.svelte.d.ts +2 -0
  10. package/dist/bases-view/columns.js +3 -2
  11. package/dist/bases-view/components/bases-embed.svelte +28 -0
  12. package/dist/bases-view/components/bases-embed.svelte.d.ts +12 -0
  13. package/dist/bases-view/components/bases-error.svelte +26 -0
  14. package/dist/bases-view/components/bases-error.svelte.d.ts +6 -0
  15. package/dist/bases-view/components/datetime.svelte +3 -0
  16. package/dist/bases-view/components/datetime.svelte.d.ts +1 -0
  17. package/dist/bases-view/components/file-preview.svelte +57 -0
  18. package/dist/bases-view/components/file-preview.svelte.d.ts +8 -0
  19. package/dist/bases-view/components/file.svelte +22 -5
  20. package/dist/bases-view/components/property-picker.svelte +147 -0
  21. package/dist/bases-view/components/property-picker.svelte.d.ts +13 -0
  22. package/dist/bases-view/components/sort/sort-root.svelte +107 -88
  23. package/dist/bases-view/components/sort/sort-root.svelte.d.ts +1 -0
  24. package/dist/bases-view/components/views.svelte +96 -42
  25. package/dist/bases-view/document-core.js +30 -0
  26. package/dist/bases-view/filter-parser.d.ts +1 -1
  27. package/dist/bases-view/filter-parser.js +2 -1
  28. package/dist/bases-view/index.js +15 -0
  29. package/dist/bases-view/kanban-view/configuration.d.ts +8 -0
  30. package/dist/bases-view/kanban-view/configuration.js +40 -0
  31. package/dist/bases-view/kanban-view/index.d.ts +55 -0
  32. package/dist/bases-view/kanban-view/index.js +296 -0
  33. package/dist/bases-view/kanban-view/kanban.svelte +284 -0
  34. package/dist/bases-view/kanban-view/kanban.svelte.d.ts +7 -0
  35. package/dist/bases-view/kanban-view/mapping.d.ts +77 -0
  36. package/dist/bases-view/kanban-view/mapping.js +206 -0
  37. package/dist/bases-view/kanban-view/markdown-detail.svelte +123 -0
  38. package/dist/bases-view/kanban-view/markdown-detail.svelte.d.ts +13 -0
  39. package/dist/bases-view/kanban-view/settings.svelte +87 -0
  40. package/dist/bases-view/kanban-view/settings.svelte.d.ts +7 -0
  41. package/dist/bases-view/list-view/list.svelte +84 -84
  42. package/dist/bases-view/map-view/view.svelte +6 -1
  43. package/dist/bases-view/metadata-invalidation-core.d.ts +1 -0
  44. package/dist/bases-view/metadata-invalidation-core.js +3 -2
  45. package/dist/bases-view/models.d.ts +26 -0
  46. package/dist/bases-view/unknown-view/view.svelte +13 -6
  47. package/dist/bases-view/view-header.svelte +20 -6
  48. package/dist/bases-view/view-registration-inventory.d.ts +8 -0
  49. package/dist/bases-view/view-registration-inventory.js +2 -0
  50. package/dist/bases-view/view-settings.svelte +213 -237
  51. package/dist/index.d.ts +1 -1
  52. package/dist/styles.css +495 -74
  53. package/manifest.json +2 -1
  54. package/package.json +3 -2
  55. package/registry.json +85 -1
@@ -12,6 +12,7 @@
12
12
  import { Button } from "@lapismd/design-core/shadcn/button";
13
13
  import { Icon } from "@lapis-notes/api/icon";
14
14
  import { Input } from "@lapismd/design-core/shadcn/input";
15
+ import { ScrollArea } from "@lapismd/design-core/shadcn/scroll-area";
15
16
  import type { BasesViewBase } from "../models";
16
17
  import type { QueryController } from "../bases.svelte";
17
18
  import ViewSettings from "../view-settings.svelte";
@@ -78,49 +79,63 @@
78
79
  <Popover.Root bind:open>
79
80
  <Popover.Trigger>
80
81
  {#snippet child({ props }: { props: Record<string, unknown> })}
81
- <Button {...props} variant="outline" size="sm" class="bases-style-h-8-ed8a5d bases-style-border-dashed-a29b7a">
82
+ <Button
83
+ {...props}
84
+ variant="outline"
85
+ size="sm"
86
+ class="bases-style-h-8-ed8a5d bases-style-border-dashed-a29b7a"
87
+ >
82
88
  <Icon name={viewIcon(selected)} />
83
89
  {value.name}
84
90
  <ArrowUpDown />
85
91
  </Button>
86
92
  {/snippet}
87
93
  </Popover.Trigger>
88
- <Popover.Content class="bases-style-w-270px-448793 bases-style-min-w-220px-cbc208 bases-style-p-0-8a539c" align="start">
94
+ <Popover.Content
95
+ class="bases-style-w-270px-448793 bases-style-min-w-220px-cbc208 bases-style-p-0-8a539c"
96
+ align="start"
97
+ >
89
98
  {#if selected}
90
- <Command.Root>
91
- <div class="bases-style-p-2-7660b4">
92
- <div class="bases-style-flex-60fbb7 bases-style-justify-between-8ef226">
93
- <Button
94
- variant="ghost"
95
- size="sm"
96
- class="bases-style-flex-60fbb7 grow bases-style-justify-start-4b5cc1"
97
- onclick={(evt) => {
98
- selected = null;
99
- }}
100
- >
101
- <ChevronLeft /> Edit {selected.name}
102
- </Button>
103
- <DropdownMenu.Root>
104
- <DropdownMenu.Trigger>
105
- {#snippet child({ props }: { props: Record<string, unknown> })}
106
- <Button {...props} size="sm" variant="ghost"
107
- aria-label={`View actions for ${selected!.name}`}
108
- ><Ellipsis /></Button
109
- >
110
- {/snippet}
111
- </DropdownMenu.Trigger>
112
- <DropdownMenu.Content>
113
- <DropdownMenu.Item onclick={() => deleteView()}
114
- >Delete view</DropdownMenu.Item
99
+ <div class="bases-view-editor" data-ui-component="bases-view-editor">
100
+ <div class="bases-view-editor__header">
101
+ <Button
102
+ variant="ghost"
103
+ size="sm"
104
+ class="bases-style-flex-60fbb7 grow bases-style-justify-start-4b5cc1"
105
+ onclick={(evt) => {
106
+ selected = null;
107
+ }}
108
+ >
109
+ <ChevronLeft /> Edit {selected.name}
110
+ </Button>
111
+ <DropdownMenu.Root>
112
+ <DropdownMenu.Trigger>
113
+ {#snippet child({ props }: { props: Record<string, unknown> })}
114
+ <Button
115
+ {...props}
116
+ size="sm"
117
+ variant="ghost"
118
+ aria-label={`View actions for ${selected!.name}`}
119
+ ><Ellipsis /></Button
115
120
  >
116
- <DropdownMenu.Separator />
117
- <DropdownMenu.Item onclick={() => duplicateView()}
118
- >Duplicate view</DropdownMenu.Item
119
- >
120
- </DropdownMenu.Content>
121
- </DropdownMenu.Root>
122
- </div>
123
- <div class="bases-style-grid-f3c543 bases-style-grid-cols-1-d7c833 bases-style-items-center-3960ff bases-style-gap-2-77a2a2 bases-style-px-2-d5eab2 bases-style-pt-1-6b7d6e">
121
+ {/snippet}
122
+ </DropdownMenu.Trigger>
123
+ <DropdownMenu.Content>
124
+ <DropdownMenu.Item onclick={() => deleteView()}
125
+ >Delete view</DropdownMenu.Item
126
+ >
127
+ <DropdownMenu.Separator />
128
+ <DropdownMenu.Item onclick={() => duplicateView()}
129
+ >Duplicate view</DropdownMenu.Item
130
+ >
131
+ </DropdownMenu.Content>
132
+ </DropdownMenu.Root>
133
+ </div>
134
+ <ScrollArea
135
+ class="bases-view-editor__scroll"
136
+ viewportProps={{ role: "group", "aria-label": "View settings" }}
137
+ >
138
+ <div class="bases-view-editor__fields">
124
139
  <Input
125
140
  id="column_name"
126
141
  aria-label="View name"
@@ -128,14 +143,20 @@
128
143
  value={selected.name}
129
144
  class={cn("bases-style-col-span-2-40efc0 bases-style-h-8-ed8a5d")}
130
145
  />
146
+ <ViewSettings {controller} bind:view={selected} />
131
147
  </div>
132
- <ViewSettings {controller} bind:view={selected} />
133
- </div>
134
- </Command.Root>
148
+ </ScrollArea>
149
+ </div>
135
150
  {:else}
136
151
  <Command.Root>
137
- <Command.Input placeholder="Find or create" aria-controls="bases-view-selector-list" />
138
- <Command.List id="bases-view-selector-list" class="bases-style-p-1-eb6a3c">
152
+ <Command.Input
153
+ placeholder="Find or create"
154
+ aria-controls="bases-view-selector-list"
155
+ />
156
+ <Command.List
157
+ id="bases-view-selector-list"
158
+ class="bases-style-p-1-eb6a3c"
159
+ >
139
160
  <Command.Empty>No views found.</Command.Empty>
140
161
  {#each views as view}
141
162
  <Command.Item
@@ -154,8 +175,10 @@
154
175
  </Command.List>
155
176
  </Command.Root>
156
177
  {/if}
157
- {#if !selected}
158
- <div class="bases-style-flex-60fbb7 bases-style-w-full-6da6a3 bases-style-flex-col-8dddea bases-style-gap-1-44ee8b bases-style-border-t-b950dd bases-style-p-1-eb6a3c">
178
+ {#if !selected && !controller.readOnly}
179
+ <div
180
+ class="bases-style-flex-60fbb7 bases-style-w-full-6da6a3 bases-style-flex-col-8dddea bases-style-gap-1-44ee8b bases-style-border-t-b950dd bases-style-p-1-eb6a3c"
181
+ >
159
182
  <Button
160
183
  size="sm"
161
184
  onclick={() => (selected = value)}
@@ -176,3 +199,34 @@
176
199
  {/if}
177
200
  </Popover.Content>
178
201
  </Popover.Root>
202
+
203
+ <style>
204
+ .bases-view-editor {
205
+ display: flex;
206
+ flex-direction: column;
207
+ height: min(
208
+ 36rem,
209
+ var(--bits-popover-content-available-height, calc(100dvh - 2rem))
210
+ );
211
+ min-height: 0;
212
+ }
213
+
214
+ .bases-view-editor__header {
215
+ display: flex;
216
+ flex: none;
217
+ justify-content: space-between;
218
+ padding: 0.5rem;
219
+ }
220
+
221
+ :global(.bases-view-editor__scroll) {
222
+ flex: 1;
223
+ min-height: 0;
224
+ }
225
+
226
+ .bases-view-editor__fields {
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 1rem;
230
+ padding: 0.5rem 1rem 1rem;
231
+ }
232
+ </style>
@@ -112,6 +112,36 @@ function normalizeView(view, fallbackName) {
112
112
  : undefined,
113
113
  };
114
114
  }
115
+ if (candidate.type === "kanban" || candidate.type === "activity") {
116
+ return {
117
+ ...candidate,
118
+ type: candidate.type,
119
+ name: typeof candidate.name === "string" ? candidate.name : fallbackName,
120
+ order: Array.isArray(candidate.order)
121
+ ? candidate.order.filter((value) => typeof value === "string")
122
+ : [],
123
+ sort: Array.isArray(candidate.sort) ? candidate.sort : [],
124
+ filter: candidate.filter && typeof candidate.filter === "object"
125
+ ? candidate.filter
126
+ : emptyFilter(),
127
+ limit: typeof candidate.limit === "number" ? candidate.limit : 0,
128
+ summaries: candidate.summaries && typeof candidate.summaries === "object"
129
+ ? candidate.summaries
130
+ : undefined,
131
+ groupBy: candidate.groupBy && typeof candidate.groupBy === "object"
132
+ ? candidate.groupBy
133
+ : undefined,
134
+ titleProperty: typeof candidate.titleProperty === "string"
135
+ ? candidate.titleProperty
136
+ : undefined,
137
+ columnOrder: Array.isArray(candidate.columnOrder)
138
+ ? candidate.columnOrder.filter((value) => typeof value === "string")
139
+ : undefined,
140
+ columnMeta: candidate.columnMeta && typeof candidate.columnMeta === "object"
141
+ ? candidate.columnMeta
142
+ : undefined,
143
+ };
144
+ }
115
145
  if (typeof candidate.type === "string" && candidate.type.length > 0) {
116
146
  return {
117
147
  ...defaultCustomView(candidate.type),
@@ -24,4 +24,4 @@ export declare function toSQL(filter: unknown, formulas?: Record<string, string>
24
24
  export declare function resolveQuery(expr: Expression): unknown;
25
25
  export declare function isFilterColumn(column: string): [boolean, string, string];
26
26
  export declare function formulaColumn(name: string): BasesPropertyId;
27
- export declare function generateQuery(view: BasesViewBase, formulas: Record<string, string>, globalFilter?: Filters): string;
27
+ export declare function generateQuery(view: BasesViewBase, formulas: Record<string, string>, globalFilter?: Filters, requiredProperties?: BasesPropertyId[]): string;
@@ -261,11 +261,12 @@ export function formulaColumn(name) {
261
261
  }
262
262
  return `formula[${JSON.stringify(name)}]`;
263
263
  }
264
- export function generateQuery(view, formulas, globalFilter) {
264
+ export function generateQuery(view, formulas, globalFilter, requiredProperties = []) {
265
265
  let query = "SET identifier_quoting = bracket;\nSELECT\n";
266
266
  const columns = [
267
267
  ...new Set([
268
268
  ...view.order,
269
+ ...requiredProperties,
269
270
  ...(view.groupBy?.property ? [view.groupBy.property] : []),
270
271
  ...(typeof view.image === "string" && view.image.length > 0
271
272
  ? [view.image]
@@ -7,6 +7,8 @@ import { TableView } from "./table-view/index.svelte";
7
7
  import { UnknownView } from "./unknown-view";
8
8
  import { CardView } from "./card-view";
9
9
  import { ListView } from "./list-view";
10
+ import { KanbanBasesView, ActivityBasesView } from "./kanban-view";
11
+ import { boardRequiredProperties } from "./kanban-view/configuration";
10
12
  import { MapView } from "./map-view";
11
13
  import { normalizeBasesDocument } from "./document-core";
12
14
  import { SerializedWriteQueue } from "../serialized-write-queue";
@@ -51,6 +53,19 @@ export function registerBuiltInBasesViews(views) {
51
53
  list: {
52
54
  factory: (controller, containerEl) => new ListView(controller, containerEl),
53
55
  options: ListView.getViewOptions,
56
+ requiredProperties: (view) => view.order?.length ? [] : ["file.name"],
57
+ },
58
+ kanban: {
59
+ factory: (controller, containerEl) => new KanbanBasesView(controller, containerEl),
60
+ options: KanbanBasesView.getViewOptions,
61
+ requiredProperties: boardRequiredProperties,
62
+ columnFilters: (view) => Object.values((view.columnFilters ?? {})),
63
+ },
64
+ activity: {
65
+ factory: (controller, containerEl) => new ActivityBasesView(controller, containerEl),
66
+ options: ActivityBasesView.getViewOptions,
67
+ requiredProperties: boardRequiredProperties,
68
+ columnFilters: (view) => Object.values((view.columnFilters ?? {})),
54
69
  },
55
70
  map: {
56
71
  factory: (controller, containerEl) => new MapView(controller, containerEl),
@@ -0,0 +1,8 @@
1
+ import type { BasesPropertyId } from "@lapis-notes/api";
2
+ import type { ColumnDefinition } from "../columns";
3
+ import type { BasesViewBase } from "../models";
4
+ export declare function activityDateProperty(view: BasesViewBase, columns: readonly ColumnDefinition[]): BasesPropertyId | null;
5
+ /** Layout mappings participate in projection even when hidden in Properties. */
6
+ export declare function boardRequiredProperties(view: BasesViewBase, columns: readonly ColumnDefinition[]): BasesPropertyId[];
7
+ /** The excerpt is read-only derived content; persistence always edits the source. */
8
+ export declare function markdownExcerpt(source: string): string;
@@ -0,0 +1,40 @@
1
+ export function activityDateProperty(view, columns) {
2
+ if (typeof view.dateProperty === "string" && view.dateProperty)
3
+ return view.dateProperty;
4
+ const ordered = [
5
+ ...new Set([...view.order, ...columns.map((column) => column.id)]),
6
+ ];
7
+ return (ordered.find((id) => columns.some((column) => column.id === id &&
8
+ (column.type === "date" || column.type === "datetime"))) ?? null);
9
+ }
10
+ /** Layout mappings participate in projection even when hidden in Properties. */
11
+ export function boardRequiredProperties(view, columns) {
12
+ const ids = [
13
+ view.titleProperty || "file.name",
14
+ view.chipProperty,
15
+ view.descriptionProperty,
16
+ view.tagsProperty || "file.tags",
17
+ view.badgeProperty,
18
+ ];
19
+ if (view.type === "activity")
20
+ ids.push(activityDateProperty(view, columns));
21
+ return [
22
+ ...new Set(ids.filter((id) => typeof id === "string" && id.length > 0)),
23
+ ];
24
+ }
25
+ /** The excerpt is read-only derived content; persistence always edits the source. */
26
+ export function markdownExcerpt(source) {
27
+ const body = source
28
+ .replace(/^\uFEFF?---[^\S\r\n]*\r?\n[\s\S]*?\r?\n---[^\S\r\n]*(?:\r?\n|$)/, "")
29
+ .replace(/^\s*#{1,6}\s+.*$/gm, "");
30
+ const paragraph = body
31
+ .split(/\r?\n\s*\r?\n/)
32
+ .find((value) => value.trim() && !/^\s*#{1,6}\s/.test(value));
33
+ return (paragraph ?? "")
34
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
35
+ .replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_, path, label) => label || path)
36
+ .replace(/[*_`]/g, "")
37
+ .replace(/\s+/g, " ")
38
+ .trim()
39
+ .slice(0, 280);
40
+ }
@@ -0,0 +1,55 @@
1
+ import type { BasesOptions } from "@lapis-notes/api";
2
+ import { BasesView, BasesEntryGroup, QueryController, type BasesEntry, type BasesPropertyId } from "../bases.svelte";
3
+ import { type KanbanColumnMeta, type KanbanColumnMetaMap } from "./mapping";
4
+ import { type Filters } from "../models";
5
+ /**
6
+ * Kanban view: renders the grouped query result as a Design Core Kanban Board.
7
+ * Grouping, ordering and per-column presentation persist in the view document;
8
+ * card moves write the group value back to the grouped frontmatter property.
9
+ */
10
+ export declare class KanbanBasesView extends BasesView {
11
+ readonly controller: QueryController;
12
+ readonly scrollEl: HTMLElement;
13
+ type: string;
14
+ constructor(controller: QueryController, scrollEl: HTMLElement, layout?: "kanban" | "activity");
15
+ onDataUpdated(): void;
16
+ static getViewOptions(): BasesOptions[];
17
+ get chipProperty(): BasesPropertyId | null;
18
+ get descriptionProperty(): BasesPropertyId | null;
19
+ get tagsProperty(): BasesPropertyId;
20
+ get badgeProperty(): BasesPropertyId | null;
21
+ get badgeLabel(): string;
22
+ get badgeColor(): string;
23
+ get dateProperty(): BasesPropertyId | null;
24
+ get anchorColumnId(): string | null | undefined;
25
+ setAnchorColumn(id: string | null | undefined): void;
26
+ get propertyLabels(): Record<string, string>;
27
+ get propertyTypes(): Record<string, string>;
28
+ get columnFilters(): Record<string, Filters>;
29
+ setColumnFilter(id: string, filter: Filters): void;
30
+ ensureColumnFilter(id: string): void;
31
+ filterGroups(): {
32
+ groups: BasesEntryGroup[];
33
+ totals: Record<string, number>;
34
+ };
35
+ setGrouping(property: string): void;
36
+ get titleProperty(): BasesPropertyId;
37
+ get canEditTitle(): boolean;
38
+ saveTitle(entry: BasesEntry, title: string): Promise<string>;
39
+ get displayProperties(): BasesPropertyId[];
40
+ get columnOrder(): string[];
41
+ get columnMeta(): KanbanColumnMetaMap;
42
+ setColumnOrder(columnOrder: string[]): void;
43
+ setColumnMeta(columnId: string, meta: KanbanColumnMeta): void;
44
+ /** True when the grouped property can be written back on card moves. */
45
+ get canMoveCards(): boolean;
46
+ openEntry(entry: BasesEntry): void;
47
+ /** Writes the lane's group value to the entry's grouped property. */
48
+ setGroupValue(entry: BasesEntry, propertyId: BasesPropertyId, value: unknown): Promise<void>;
49
+ findEntry(cardId: string): BasesEntry | null;
50
+ }
51
+ /** Independent Bases layout sharing the generic Markdown card mapping. */
52
+ export declare class ActivityBasesView extends KanbanBasesView {
53
+ constructor(controller: QueryController, containerEl: HTMLElement);
54
+ static getViewOptions(): BasesOptions[];
55
+ }
@@ -0,0 +1,296 @@
1
+ import { normalizeMetadataValue } from "@lapis-notes/api/metadata-value";
2
+ import { set, unset } from "lodash-es";
3
+ import { BasesView, BasesEntryGroup, QueryController, } from "../bases.svelte";
4
+ import KanbanViewComponent from "./kanban.svelte";
5
+ import { groupIdentity, rawGroupValue, toColumnSources, valueToText, } from "./mapping";
6
+ import { activityDateProperty } from "./configuration";
7
+ import { isFilterGroup } from "../models";
8
+ /**
9
+ * Kanban view: renders the grouped query result as a Design Core Kanban Board.
10
+ * Grouping, ordering and per-column presentation persist in the view document;
11
+ * card moves write the group value back to the grouped frontmatter property.
12
+ */
13
+ export class KanbanBasesView extends BasesView {
14
+ controller;
15
+ scrollEl;
16
+ type = "kanban";
17
+ constructor(controller, scrollEl, layout = "kanban") {
18
+ super(controller);
19
+ this.controller = controller;
20
+ this.scrollEl = scrollEl;
21
+ this.type = layout;
22
+ this.mountViewComponent(KanbanViewComponent, { view: this }, scrollEl);
23
+ }
24
+ onDataUpdated() { }
25
+ static getViewOptions() {
26
+ return [
27
+ {
28
+ key: "titleProperty",
29
+ type: "property",
30
+ displayName: "Title property",
31
+ default: "file.name",
32
+ },
33
+ {
34
+ key: "chipProperty",
35
+ type: "property",
36
+ displayName: "Card chip property",
37
+ placeholder: "Title",
38
+ },
39
+ {
40
+ key: "descriptionProperty",
41
+ type: "property",
42
+ displayName: "Description property",
43
+ placeholder: "Markdown body",
44
+ },
45
+ {
46
+ key: "tagsProperty",
47
+ type: "property",
48
+ displayName: "Tags property",
49
+ default: "file.tags",
50
+ },
51
+ {
52
+ key: "badgeProperty",
53
+ type: "property",
54
+ displayName: "Badge property",
55
+ placeholder: "No badge",
56
+ },
57
+ {
58
+ key: "badgeLabel",
59
+ type: "text",
60
+ displayName: "Badge label",
61
+ placeholder: "Property display name",
62
+ },
63
+ {
64
+ key: "badgeColor",
65
+ type: "text",
66
+ displayName: "Badge colour",
67
+ placeholder: "Column colour",
68
+ },
69
+ ];
70
+ }
71
+ get chipProperty() {
72
+ return this.config.getAsPropertyId("chipProperty");
73
+ }
74
+ get descriptionProperty() {
75
+ return this.config.getAsPropertyId("descriptionProperty");
76
+ }
77
+ get tagsProperty() {
78
+ return this.config.getAsPropertyId("tagsProperty") ?? "file.tags";
79
+ }
80
+ get badgeProperty() {
81
+ return this.config.getAsPropertyId("badgeProperty");
82
+ }
83
+ get badgeLabel() {
84
+ const value = this.config.get("badgeLabel");
85
+ return typeof value === "string" ? value : "";
86
+ }
87
+ get badgeColor() {
88
+ const value = this.config.get("badgeColor");
89
+ return typeof value === "string" ? value : "";
90
+ }
91
+ get dateProperty() {
92
+ return activityDateProperty(this.controller.selectedView, this.controller.getAllColumns());
93
+ }
94
+ get anchorColumnId() {
95
+ const value = this.config.get("anchorColumnId");
96
+ return value === null
97
+ ? null
98
+ : typeof value === "string"
99
+ ? value
100
+ : undefined;
101
+ }
102
+ setAnchorColumn(id) {
103
+ // BasesViewConfig.set(null) removes a setting. Null is a meaningful value
104
+ // here, so retain it explicitly in the owning view document.
105
+ if (id === null)
106
+ this.controller.selectedView.anchorColumnId = null;
107
+ else
108
+ this.config.set("anchorColumnId", id);
109
+ }
110
+ get propertyLabels() {
111
+ return Object.fromEntries(this.controller
112
+ .getAllColumns()
113
+ .map((column) => [column.id, column.displayName]));
114
+ }
115
+ get propertyTypes() {
116
+ return Object.fromEntries(this.controller.getAllColumns().map((column) => [column.id, column.type]));
117
+ }
118
+ get columnFilters() {
119
+ const value = this.config.get("columnFilters");
120
+ if (!value || typeof value !== "object")
121
+ return {};
122
+ return Object.fromEntries(Object.entries(value).filter(([, filter]) => isFilterGroup(filter)));
123
+ }
124
+ setColumnFilter(id, filter) {
125
+ this.config.set("columnFilters", { ...this.columnFilters, [id]: filter });
126
+ }
127
+ ensureColumnFilter(id) {
128
+ if (!this.columnFilters[id])
129
+ this.setColumnFilter(id, { and: [] });
130
+ }
131
+ filterGroups() {
132
+ const totals = {};
133
+ const property = this.data.groupBy ?? "";
134
+ const groups = this.data.groupedData.map((group) => {
135
+ const id = groupIdentity(property, group.key);
136
+ totals[id] = group.entries.length;
137
+ const filter = this.columnFilters[id];
138
+ return new BasesEntryGroup(filter
139
+ ? this.controller.filterEntries(group.entries, filter)
140
+ : group.entries, group.key);
141
+ });
142
+ return { groups, totals };
143
+ }
144
+ setGrouping(property) {
145
+ // Migrate legacy label-based presentation once, before changing its group scope.
146
+ const current = this.data.groupBy;
147
+ if (current) {
148
+ const sources = toColumnSources(this.data.groupedData, current);
149
+ const meta = { ...this.columnMeta };
150
+ for (const source of sources) {
151
+ if (meta[source.label]) {
152
+ meta[source.id] ??= meta[source.label];
153
+ delete meta[source.label];
154
+ }
155
+ }
156
+ this.config.set("columnMeta", meta);
157
+ this.setColumnOrder(this.columnOrder.map((id) => sources.find((source) => source.label === id)?.id ?? id));
158
+ }
159
+ this.config.set("groupBy", property
160
+ ? {
161
+ property,
162
+ direction: this.controller.selectedView.groupBy?.direction ?? "ASC",
163
+ }
164
+ : undefined);
165
+ }
166
+ get titleProperty() {
167
+ return this.config.getAsPropertyId("titleProperty") ?? "file.name";
168
+ }
169
+ get canEditTitle() {
170
+ return (!this.controller.readOnly &&
171
+ (this.titleProperty === "file.name" ||
172
+ this.titleProperty === "file.basename" ||
173
+ (this.titleProperty.startsWith("note.") &&
174
+ ["text", "unknown"].includes(this.controller.getColumn(this.titleProperty)?.type ?? "unknown"))));
175
+ }
176
+ async saveTitle(entry, title) {
177
+ if (!this.canEditTitle)
178
+ throw new Error("This title property is read-only.");
179
+ if (this.titleProperty.startsWith("note.")) {
180
+ await this.setGroupValue(entry, this.titleProperty, title);
181
+ }
182
+ else {
183
+ if (!title.trim() ||
184
+ /[\\/]/.test(title) ||
185
+ title === "." ||
186
+ title === "..")
187
+ throw new Error("Enter a valid file name.");
188
+ const extension = entry.file.extension;
189
+ const name = extension && !title.endsWith(`.${extension}`)
190
+ ? `${title}.${extension}`
191
+ : title;
192
+ const directory = entry.file.path.slice(0, entry.file.path.lastIndexOf("/") + 1);
193
+ await this.controller.app.fileManager.renameFile(entry.file, directory + name);
194
+ }
195
+ return entry.file.path;
196
+ }
197
+ get displayProperties() {
198
+ const title = this.titleProperty;
199
+ return [
200
+ ...new Set(this.config
201
+ .getOrder()
202
+ .filter((propertyId) => Boolean(propertyId) && propertyId !== title)),
203
+ ];
204
+ }
205
+ get columnOrder() {
206
+ const configured = this.config.get("columnOrder");
207
+ return Array.isArray(configured)
208
+ ? configured.filter((value) => typeof value === "string")
209
+ : [];
210
+ }
211
+ get columnMeta() {
212
+ const configured = this.config.get("columnMeta");
213
+ if (!configured || typeof configured !== "object")
214
+ return {};
215
+ return Object.fromEntries(Object.entries(configured).flatMap(([columnId, value]) => {
216
+ if (!value || typeof value !== "object")
217
+ return [];
218
+ const record = value;
219
+ const title = typeof record.title === "string" && record.title.trim().length > 0
220
+ ? record.title.trim()
221
+ : undefined;
222
+ const color = typeof record.color === "string" && record.color.trim().length > 0
223
+ ? record.color.trim()
224
+ : undefined;
225
+ if (!title && !color)
226
+ return [];
227
+ return [[columnId, { title, color }]];
228
+ }));
229
+ }
230
+ setColumnOrder(columnOrder) {
231
+ this.config.set("columnOrder", columnOrder);
232
+ }
233
+ setColumnMeta(columnId, meta) {
234
+ const nextMeta = { ...this.columnMeta };
235
+ const title = meta.title?.trim() ? meta.title.trim() : undefined;
236
+ const color = meta.color?.trim() ? meta.color.trim() : undefined;
237
+ if (!title && !color) {
238
+ delete nextMeta[columnId];
239
+ }
240
+ else {
241
+ nextMeta[columnId] = { title, color };
242
+ }
243
+ this.config.set("columnMeta", Object.keys(nextMeta).length > 0 ? nextMeta : null);
244
+ }
245
+ /** True when the grouped property can be written back on card moves. */
246
+ get canMoveCards() {
247
+ const groupBy = this.data.groupBy;
248
+ return Boolean(!this.controller.readOnly &&
249
+ groupBy &&
250
+ groupBy.startsWith("note.") &&
251
+ this.data.groupedData.every((group) => {
252
+ const value = rawGroupValue(group.key);
253
+ return (value === null ||
254
+ ["string", "number", "boolean"].includes(typeof value));
255
+ }));
256
+ }
257
+ openEntry(entry) {
258
+ void this.controller.app.openFile(entry.file);
259
+ }
260
+ /** Writes the lane's group value to the entry's grouped property. */
261
+ async setGroupValue(entry, propertyId, value) {
262
+ if (this.controller.readOnly || !propertyId.startsWith("note."))
263
+ return;
264
+ const id = propertyId.substring(5);
265
+ const declaredType = this.controller.getColumn(propertyId)?.type ?? "text";
266
+ const normalized = value === null ? null : normalizeMetadataValue(declaredType, value);
267
+ await this.controller.app.fileManager.processFrontMatter(entry.file, (data) => {
268
+ if (normalized === null || normalized === undefined) {
269
+ unset(data, id);
270
+ }
271
+ else {
272
+ set(data, id, normalized);
273
+ }
274
+ });
275
+ }
276
+ findEntry(cardId) {
277
+ return this.data.data.find((entry) => entry.id === cardId) ?? null;
278
+ }
279
+ }
280
+ /** Independent Bases layout sharing the generic Markdown card mapping. */
281
+ export class ActivityBasesView extends KanbanBasesView {
282
+ constructor(controller, containerEl) {
283
+ super(controller, containerEl, "activity");
284
+ }
285
+ static getViewOptions() {
286
+ return [
287
+ ...super.getViewOptions(),
288
+ {
289
+ key: "dateProperty",
290
+ type: "property",
291
+ displayName: "Activity date property",
292
+ placeholder: "First date field in results",
293
+ },
294
+ ];
295
+ }
296
+ }