@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.
- package/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/dist/bases-plugin.js +18 -21
- package/dist/bases-view/bases.svelte.d.ts +7 -0
- package/dist/bases-view/bases.svelte.js +37 -7
- package/dist/bases-view/card-view/components/card-placeholder.svelte +7 -3
- package/dist/bases-view/card-view/components/card.svelte +23 -15
- package/dist/bases-view/cell.svelte +147 -16
- package/dist/bases-view/cell.svelte.d.ts +2 -0
- package/dist/bases-view/columns.js +3 -2
- package/dist/bases-view/components/bases-embed.svelte +28 -0
- package/dist/bases-view/components/bases-embed.svelte.d.ts +12 -0
- package/dist/bases-view/components/bases-error.svelte +26 -0
- package/dist/bases-view/components/bases-error.svelte.d.ts +6 -0
- package/dist/bases-view/components/datetime.svelte +3 -0
- package/dist/bases-view/components/datetime.svelte.d.ts +1 -0
- package/dist/bases-view/components/file-preview.svelte +57 -0
- package/dist/bases-view/components/file-preview.svelte.d.ts +8 -0
- package/dist/bases-view/components/file.svelte +22 -5
- package/dist/bases-view/components/property-picker.svelte +147 -0
- package/dist/bases-view/components/property-picker.svelte.d.ts +13 -0
- package/dist/bases-view/components/sort/sort-root.svelte +107 -88
- package/dist/bases-view/components/sort/sort-root.svelte.d.ts +1 -0
- package/dist/bases-view/components/views.svelte +96 -42
- package/dist/bases-view/document-core.js +30 -0
- package/dist/bases-view/filter-parser.d.ts +1 -1
- package/dist/bases-view/filter-parser.js +2 -1
- package/dist/bases-view/index.js +15 -0
- package/dist/bases-view/kanban-view/configuration.d.ts +8 -0
- package/dist/bases-view/kanban-view/configuration.js +40 -0
- package/dist/bases-view/kanban-view/index.d.ts +55 -0
- package/dist/bases-view/kanban-view/index.js +296 -0
- package/dist/bases-view/kanban-view/kanban.svelte +284 -0
- package/dist/bases-view/kanban-view/kanban.svelte.d.ts +7 -0
- package/dist/bases-view/kanban-view/mapping.d.ts +77 -0
- package/dist/bases-view/kanban-view/mapping.js +206 -0
- package/dist/bases-view/kanban-view/markdown-detail.svelte +123 -0
- package/dist/bases-view/kanban-view/markdown-detail.svelte.d.ts +13 -0
- package/dist/bases-view/kanban-view/settings.svelte +87 -0
- package/dist/bases-view/kanban-view/settings.svelte.d.ts +7 -0
- package/dist/bases-view/list-view/list.svelte +84 -84
- package/dist/bases-view/map-view/view.svelte +6 -1
- package/dist/bases-view/metadata-invalidation-core.d.ts +1 -0
- package/dist/bases-view/metadata-invalidation-core.js +3 -2
- package/dist/bases-view/models.d.ts +26 -0
- package/dist/bases-view/unknown-view/view.svelte +13 -6
- package/dist/bases-view/view-header.svelte +20 -6
- package/dist/bases-view/view-registration-inventory.d.ts +8 -0
- package/dist/bases-view/view-registration-inventory.js +2 -0
- package/dist/bases-view/view-settings.svelte +213 -237
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +495 -74
- package/manifest.json +2 -1
- package/package.json +3 -2
- 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
|
|
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
|
|
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
|
-
<
|
|
91
|
-
<div class="bases-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<DropdownMenu.
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
</Command.Root>
|
|
148
|
+
</ScrollArea>
|
|
149
|
+
</div>
|
|
135
150
|
{:else}
|
|
136
151
|
<Command.Root>
|
|
137
|
-
<Command.Input
|
|
138
|
-
|
|
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
|
|
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]
|
package/dist/bases-view/index.js
CHANGED
|
@@ -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
|
+
}
|