Skip to content

Commit 06cce97

Browse files
Merge pull request #195 from MaddyGuthridge/maddy-backlinks
Support backlink data
2 parents 5cb185b + 3127f28 commit 06cce97

File tree

10 files changed

+175
-12
lines changed

10 files changed

+175
-12
lines changed

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
"cSpell.language": "en",
33
"cSpell.words": [
44
"Asciinema",
5+
"Backlink",
6+
"Backlinks",
57
"firstrun",
68
"Minifolio",
79
"superstruct"

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "minifolio",
3-
"version": "1.2.10",
3+
"version": "1.3.0",
44
"private": true,
55
"license": "GPL-3.0-only",
66
"scripts": {

src/components/pickers/ItemPicker.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
portfolio: ItemData,
99
value: ItemId,
1010
id?: string,
11+
onchange?: (() => any) | undefined,
1112
};
1213
13-
let { portfolio, value: value = $bindable(), id }: Props = $props();
14+
let { portfolio, value: value = $bindable(), id, onchange }: Props = $props();
1415
1516
/** Select the item with the given ID fragment at the given depth */
1617
function setSelection(index: number, childId: string | undefined) {
@@ -25,6 +26,7 @@
2526
<Select
2627
bind:value={() => itemId.at(value, index),
2728
newSelection => setSelection(index, newSelection)}
29+
onchange={onchange}
2830
>
2931
<option value={undefined}
3032
>{index === 0 ? '-- Root --' : '-- This item --'}</option

src/lib/server/data/item/section.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ async function validateLinksSection(itemId: ItemId, data: LinksSection) {
5353
await Promise.all(data.items.map(otherItem => validateLinkedItem(otherItem)));
5454
}
5555

56+
/** Backlinks from another group of items to this item */
57+
const BacklinksSectionStruct = type({
58+
/** The type of section (in this case 'backlinks') */
59+
type: literal('backlinks'),
60+
/** The text to display for the section (eg "See also") */
61+
label: string(),
62+
/** The style in which to present the links ('chip' or 'card') */
63+
style: enums(linkDisplayStyles),
64+
/** Item whose children can be potentially shown */
65+
parentItem: ItemIdStruct,
66+
});
67+
68+
/** Backlinks from another group of items to this item */
69+
export type BacklinksSection = Infer<typeof BacklinksSectionStruct>;
70+
71+
async function validateBacklinksSection(itemId: ItemId, data: BacklinksSection) {
72+
validate.name(data.label);
73+
if (!await itemExists(data.parentItem)) {
74+
error(400, `Backlink parent item ${data.parentItem} does not exist`);
75+
}
76+
}
77+
5678
/** Package information section */
5779
const PackageSectionStruct = type({
5880
/** The type of section (in this case 'package') */
@@ -111,6 +133,7 @@ export type DownloadSection = Infer<typeof DownloadSectionStruct>;
111133
export const ItemSectionStruct = union([
112134
HeadingSectionStruct,
113135
LinksSectionStruct,
136+
BacklinksSectionStruct,
114137
PackageSectionStruct,
115138
RepoSectionStruct,
116139
SiteSectionStruct,
@@ -128,7 +151,9 @@ export async function validateSection(itemId: ItemId, data: ItemSection) {
128151
switch (data.type) {
129152
case 'links':
130153
await validateLinksSection(itemId, data);
131-
validate.name(data.label);
154+
break;
155+
case 'backlinks':
156+
await validateBacklinksSection(itemId, data);
132157
break;
133158
case 'heading':
134159
validate.name(data.heading);

src/lib/server/data/migrations/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@ export type PrivateMigrationFunction = (
2020

2121
// Migrations for data
2222
const dataMigrations: Record<string, DataMigrationFunction> = {
23-
// v0.6.x --> v1.2.0
23+
// v0.6.x --> v1.3.0
2424
'~0.6.1': migrateDataV06,
25-
// v1.0.x --> v1.2.0
25+
// v1.0.x --> v1.3.0
2626
'~1.0.0': migrateDataV10,
27-
// v1.1.x --> v1.2.0
27+
// v1.1.x --> v1.3.0
2828
'~1.1.0': migrateDataV11,
29-
// v1.2.x (minor version bumps)
29+
// v1.2.x --> 1.3.0
3030
'~1.2.0': bumpDataVersion,
31+
// v1.3.x (minor version bumps)
32+
'~1.3.0': bumpDataVersion,
3133
};
3234

3335
// Migrations for private data
@@ -38,8 +40,10 @@ const privateMigrations: Record<string, PrivateMigrationFunction> = {
3840
'~1.0.0': migratePrivateV10,
3941
// v1.1.x --> v1.2.0
4042
'~1.1.0': migratePrivateV11,
41-
// v1.1.x (minor version bumps)
43+
// v1.2.x --> 1.3.0 (no data migration required)
4244
'~1.2.0': bumpPrivateDataVersion,
45+
// v1.3.x (minor version bumps)
46+
'~1.3.0': bumpPrivateDataVersion,
4347
};
4448

4549
/** Perform a migration from the given version */
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<script lang="ts">
2+
import { Select, TextInput } from '$components/base';
3+
import ItemCardGrid from '$components/card/ItemCardGrid.svelte';
4+
import { ItemChipList } from '$components/chip';
5+
import { ItemPicker } from '$components/pickers';
6+
import { getDescendant } from '$lib/itemData';
7+
import itemId, { type ItemId } from '$lib/itemId';
8+
import { linkDisplayStyles } from '$lib/links';
9+
import type { ItemData } from '$lib/server/data/item';
10+
import type { BacklinksSection } from '$lib/server/data/item/section';
11+
import { capitalize } from '$lib/util';
12+
13+
type Props = {
14+
item: ItemId,
15+
portfolio: ItemData,
16+
section: BacklinksSection,
17+
editing: boolean,
18+
onchange: () => void,
19+
};
20+
21+
const {
22+
item: item,
23+
portfolio,
24+
editing,
25+
section = $bindable(),
26+
onchange,
27+
}: Props = $props();
28+
29+
const parent = $derived(getDescendant(portfolio, section.parentItem));
30+
const backlinkItems = $derived(
31+
parent.info.children
32+
.map(child => itemId.child(section.parentItem, child))
33+
.filter(childId =>
34+
getDescendant(portfolio, childId).info.sections.find(
35+
sect => sect.type === 'links' && sect.items.includes(item),
36+
) !== undefined));
37+
</script>
38+
39+
{#snippet display()}
40+
{#if section.style === 'chip'}
41+
<div class="link-chips">
42+
<h3>{section.label}</h3>
43+
<ItemChipList
44+
{portfolio}
45+
items={[backlinkItems.map(i => ({ itemId: i, selected: false }))]}
46+
link={!editing}
47+
/>
48+
</div>
49+
{:else}
50+
<h2>{section.label}</h2>
51+
<ItemCardGrid {portfolio} itemIds={backlinkItems} {editing} />
52+
{/if}
53+
{/snippet}
54+
55+
{#if editing}
56+
<div class="edit-outer">
57+
<p>Children of the selected item which link to this item will be shown.</p>
58+
<div class="edit-grid">
59+
<label for="links-label-text">Label text</label>
60+
<TextInput
61+
id="links-label-text"
62+
bind:value={section.label}
63+
oninput={onchange}
64+
placeholder={'See also'}
65+
/>
66+
<label for="links-style">Display style</label>
67+
<Select id="links-style" bind:value={section.style} {onchange}>
68+
{#each linkDisplayStyles as style}
69+
<option value={style}>{capitalize(style)}</option>
70+
{/each}
71+
</Select>
72+
<label for="links-item-picker">Parent item</label>
73+
<div class="item-picker-control">
74+
<ItemPicker
75+
id="links-item-picker"
76+
{portfolio}
77+
bind:value={section.parentItem}
78+
{onchange}
79+
/>
80+
</div>
81+
</div>
82+
{@render display()}
83+
</div>
84+
{:else}
85+
{@render display()}
86+
{/if}
87+
88+
<style>
89+
.link-chips {
90+
display: flex;
91+
align-items: baseline;
92+
gap: 10px;
93+
}
94+
.link-chips h3 {
95+
margin: 0;
96+
}
97+
98+
.edit-outer {
99+
display: flex;
100+
flex-direction: column;
101+
gap: 10px;
102+
align-items: center;
103+
}
104+
105+
.edit-grid {
106+
display: grid;
107+
grid-template-columns: 1fr 1fr;
108+
gap: 10px;
109+
}
110+
111+
label {
112+
display: flex;
113+
align-items: center;
114+
}
115+
116+
.item-picker-control {
117+
display: flex;
118+
gap: 5px;
119+
}
120+
</style>

src/routes/[...item]/sections/CreateSectionForm.svelte

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import { Button, Select } from '$components/base';
3+
import itemId from '$lib/itemId';
34
import type { ItemSection, SectionType } from '$lib/server/data/item/section';
45
import { capitalize } from '$lib/util';
56
@@ -29,6 +30,12 @@
2930
style: 'chip',
3031
items: [],
3132
},
33+
backlinks: {
34+
type: 'backlinks',
35+
label: 'See also',
36+
style: 'chip',
37+
parentItem: itemId.ROOT,
38+
},
3239
package: {
3340
type: 'package',
3441
label: null,

src/routes/[...item]/sections/Links.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
2828
function addNewLink() {
2929
section.items.push(newLinkId);
30-
newLinkId = itemId.ROOT;
30+
// newLinkId = itemId.ROOT;
3131
onchange();
3232
}
3333
</script>
@@ -59,7 +59,7 @@
5959
placeholder={'See also'}
6060
/>
6161
<label for="links-style">Display style</label>
62-
<Select id="links-style" bind:value={section.style}>
62+
<Select id="links-style" bind:value={section.style} {onchange}>
6363
{#each linkDisplayStyles as style}
6464
<option value={style}>{capitalize(style)}</option>
6565
{/each}

src/routes/[...item]/sections/Section.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import type { ItemId } from '$lib/itemId';
44
import type { ItemData } from '$lib/server/data/item';
55
import type { ItemSection } from '$lib/server/data/item/section';
6+
import Backlinks from './Backlinks.svelte';
67
import Download from './Download.svelte';
78
import Heading from './Heading.svelte';
89
import Links from './Links.svelte';
@@ -43,6 +44,8 @@
4344
<Heading bind:section {editing} {onchange} />
4445
{:else if section.type === 'links'}
4546
<Links bind:section {editing} {portfolio} {onchange} />
47+
{:else if section.type === 'backlinks'}
48+
<Backlinks bind:section {editing} {portfolio} {item} {onchange} />
4649
{:else if section.type === 'download'}
4750
<Download bind:section {editing} {portfolio} {item} {onchange} />
4851
{/if}

0 commit comments

Comments
 (0)