From 4e6bbe0fd6c5de6e9b1a4f9542510a42f404c33b Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Sun, 6 Jul 2025 21:05:19 +1000
Subject: [PATCH 01/12] feat(pos-rich-link): receive resource if uri is not
present
---
.../pos-rich-link/pos-rich-link.spec.tsx | 51 ++++++++++++++-
.../pos-rich-link/pos-rich-link.tsx | 64 +++++++++++++------
2 files changed, 95 insertions(+), 20 deletions(-)
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx b/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
index 3f0a0dea..30dc3115 100644
--- a/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
+++ b/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
@@ -2,7 +2,7 @@ import { newSpecPage } from '@stencil/core/testing';
import { PosRichLink } from './pos-rich-link';
import { getByText } from '@testing-library/dom';
-describe('pos-rich-link', () => {
+describe('pos-rich-link with uri', () => {
let page;
beforeEach(async () => {
page = await newSpecPage({
@@ -49,3 +49,52 @@ describe('pos-rich-link', () => {
});
});
});
+
+describe('pos-rich-link without uri', () => {
+ it('does not emit pod-os:resource event if uri is present', async () => {
+ const receiveResource = jest.fn();
+ const page = await newSpecPage({
+ components: [PosRichLink],
+ });
+ page.body.addEventListener('pod-os:resource', receiveResource);
+ await page.setContent('');
+ expect(receiveResource).toHaveBeenCalledTimes(0);
+ });
+
+ it('receives resource and sets it as link if uri is not present', async () => {
+ const receiveResource = jest.fn();
+ const page = await newSpecPage({
+ components: [PosRichLink],
+ });
+ page.body.addEventListener('pod-os:resource', receiveResource);
+ await page.setContent('');
+ expect(receiveResource).toHaveBeenCalledTimes(1);
+
+ await page.rootInstance.receiveResource({
+ uri: 'https://pod.example/resource',
+ });
+ await page.waitForChanges();
+ const link = page.root?.shadowRoot?.querySelector('a');
+ expect(link).toEqualAttribute('href', 'https://pod.example/resource');
+ });
+
+ it('is empty if neither uri nor resource are received', async () => {
+ const page = await newSpecPage({
+ components: [PosRichLink],
+ html: ``,
+ });
+ expect(page.root?.innerHTML).toBe('');
+ });
+
+ it('does not use pos-resource if uri is not present', async () => {
+ const page = await newSpecPage({
+ components: [PosRichLink],
+ html: ``,
+ });
+ await page.rootInstance.receiveResource({
+ uri: 'https://pod.example/resource',
+ });
+ await page.waitForChanges();
+ expect(page.root?.shadowRoot?.querySelector('pos-resource')).toBeNull();
+ });
+});
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.tsx b/elements/src/components/pos-rich-link/pos-rich-link.tsx
index fe58f7e4..beb3bf12 100644
--- a/elements/src/components/pos-rich-link/pos-rich-link.tsx
+++ b/elements/src/components/pos-rich-link/pos-rich-link.tsx
@@ -1,32 +1,58 @@
-import { Component, Event, EventEmitter, h, Prop } from '@stencil/core';
+import { Thing } from '@pod-os/core';
+import { Component, Event, EventEmitter, h, Prop, State } from '@stencil/core';
+import { ResourceAware, ResourceEventEmitter, subscribeResource } from '../events/ResourceAware';
@Component({
tag: 'pos-rich-link',
shadow: true,
styleUrl: 'pos-rich-link.css',
})
-export class PosRichLink {
- @Prop() uri: string;
+export class PosRichLink implements ResourceAware {
+ @Prop() uri?: string;
@Event({ eventName: 'pod-os:link' }) linkEmitter: EventEmitter;
+ @Event({ eventName: 'pod-os:resource' })
+ subscribeResource: ResourceEventEmitter;
+
+ @State() resource?: Thing;
+
+ componentWillLoad() {
+ if (!this.uri) subscribeResource(this);
+ }
+
+ receiveResource = (resource: Thing) => {
+ this.resource = resource;
+ };
+
render() {
- return (
-
-
- {
- e.preventDefault();
- this.linkEmitter.emit(this.uri);
- }}
- >
-
-
- {new URL(this.uri).host}
-
-
-
+ const uri = this.uri || this.resource?.uri;
+ if (!uri) return null;
+
+ const content = (
+
+ {
+ e.preventDefault();
+ this.linkEmitter.emit(uri);
+ }}
+ >
+
+
+ {new URL(uri).host}
+
+
);
+
+ if (this.resource) {
+ return content;
+ } else if (this.uri) {
+ return (
+
+ {content}
+
+ );
+ }
}
}
From 27809566df2a493563caa1dcb4d438dd6e1df89e Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Sun, 6 Jul 2025 21:36:10 +1000
Subject: [PATCH 02/12] feat(pos-rich-link): follow rel attribute if present
---
.../pos-rich-link/pos-rich-link.spec.tsx | 42 +++++++++++++++++++
.../pos-rich-link/pos-rich-link.tsx | 23 ++++++++--
2 files changed, 61 insertions(+), 4 deletions(-)
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx b/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
index 30dc3115..21eb77cd 100644
--- a/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
+++ b/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
@@ -97,4 +97,46 @@ describe('pos-rich-link without uri', () => {
await page.waitForChanges();
expect(page.root?.shadowRoot?.querySelector('pos-resource')).toBeNull();
});
+
+ it('uses the matching relation if rel prop is defined', async () => {
+ const page = await newSpecPage({
+ components: [PosRichLink],
+ html: ``,
+ });
+ await page.rootInstance.receiveResource({
+ uri: 'https://pod.example/resource',
+ relations: () => [{ predicate: 'https://schema.org/video', uris: ['https://video.test/video-1'] }],
+ });
+ await page.waitForChanges();
+ const link = page.root?.shadowRoot?.querySelector('a');
+ expect(link).toEqualAttribute('href', 'https://video.test/video-1');
+ });
+
+ it('displays an error if no link is found', async () => {
+ const page = await newSpecPage({
+ components: [PosRichLink],
+ html: ``,
+ });
+ await page.rootInstance.receiveResource({
+ uri: 'https://pod.example/resource',
+ relations: () => [],
+ });
+ await page.waitForChanges();
+ expect(page.root?.shadowRoot?.textContent).toEqual('No matching link found');
+ });
+
+ it('displays an error if more than one link is found', async () => {
+ const page = await newSpecPage({
+ components: [PosRichLink],
+ html: ``,
+ });
+ await page.rootInstance.receiveResource({
+ uri: 'https://pod.example/resource',
+ relations: () => [
+ { predicate: 'https://schema.org/video', uris: ['https://video.test/video-1', 'https://video.test/video-2'] },
+ ],
+ });
+ await page.waitForChanges();
+ expect(page.root?.shadowRoot?.textContent).toEqual('More than one matching link found');
+ });
});
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.tsx b/elements/src/components/pos-rich-link/pos-rich-link.tsx
index beb3bf12..4d958fa9 100644
--- a/elements/src/components/pos-rich-link/pos-rich-link.tsx
+++ b/elements/src/components/pos-rich-link/pos-rich-link.tsx
@@ -9,24 +9,39 @@ import { ResourceAware, ResourceEventEmitter, subscribeResource } from '../event
})
export class PosRichLink implements ResourceAware {
@Prop() uri?: string;
+ @Prop() rel?: string;
@Event({ eventName: 'pod-os:link' }) linkEmitter: EventEmitter;
@Event({ eventName: 'pod-os:resource' })
subscribeResource: ResourceEventEmitter;
- @State() resource?: Thing;
+ @State() link?: string;
+ @State() error: string = null;
componentWillLoad() {
if (!this.uri) subscribeResource(this);
}
receiveResource = (resource: Thing) => {
- this.resource = resource;
+ if (this.rel) {
+ const links = resource.relations(this.rel);
+ if (links.length == 0) {
+ this.error = 'No matching link found';
+ } else if (links[0].uris.length > 1) {
+ this.error = 'More than one matching link found';
+ } else {
+ this.link = links[0].uris[0];
+ }
+ } else {
+ this.link = resource.uri;
+ }
};
render() {
- const uri = this.uri || this.resource?.uri;
+ if (this.error) return this.error;
+
+ const uri = this.uri || this.link;
if (!uri) return null;
const content = (
@@ -45,7 +60,7 @@ export class PosRichLink implements ResourceAware {
);
- if (this.resource) {
+ if (this.link) {
return content;
} else if (this.uri) {
return (
From 9b78cb39e6d15427c5797250ffd3d9c2b6ca5edf Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Thu, 10 Jul 2025 21:02:47 +1000
Subject: [PATCH 03/12] feat(core): thing.reverseRelations takes optional
predicate
---
core/CHANGELOG.md | 6 +++++
core/src/thing/Thing.reverseRelations.spec.ts | 25 +++++++++++++++++++
core/src/thing/Thing.ts | 4 +--
3 files changed, 33 insertions(+), 2 deletions(-)
diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md
index 5856e1fd..decf80e5 100644
--- a/core/CHANGELOG.md
+++ b/core/CHANGELOG.md
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+
+### Changed
+
+- `Thing.reverseRelations()`: Now takes optional predicate to filter by
+
## 0.17.0
### Changed
diff --git a/core/src/thing/Thing.reverseRelations.spec.ts b/core/src/thing/Thing.reverseRelations.spec.ts
index 2109e761..2fa9e639 100644
--- a/core/src/thing/Thing.reverseRelations.spec.ts
+++ b/core/src/thing/Thing.reverseRelations.spec.ts
@@ -142,5 +142,30 @@ describe("Thing", function () {
},
]);
});
+
+ it("only follows the given predicate if provided", () => {
+ const store = graph();
+ const uri = "https://jane.doe.example/container/file.ttl#fragment";
+ store.add(
+ sym("https://pod.example/first"),
+ sym("http://vocab.test/first"),
+
+ sym(uri),
+ );
+ store.add(
+ sym("https://pod.example/second"),
+ sym("http://vocab.test/second"),
+ sym(uri),
+ );
+ const it = new Thing(uri, store);
+ const result = it.reverseRelations("http://vocab.test/first");
+ expect(result).toEqual([
+ {
+ predicate: "http://vocab.test/first",
+ label: "first",
+ uris: ["https://pod.example/first"],
+ },
+ ]);
+ });
});
});
diff --git a/core/src/thing/Thing.ts b/core/src/thing/Thing.ts
index 3cb8d64f..b53b0fb1 100644
--- a/core/src/thing/Thing.ts
+++ b/core/src/thing/Thing.ts
@@ -89,10 +89,10 @@ export class Thing {
}));
}
- reverseRelations(): Relation[] {
+ reverseRelations(predicate?: string): Relation[] {
const statements = this.store.statementsMatching(
undefined,
- undefined,
+ predicate ? sym(predicate) : null,
sym(this.uri),
);
From 3db0d4283acc8359f67ca3276c7b2b98a327b93e Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Thu, 10 Jul 2025 21:05:39 +1000
Subject: [PATCH 04/12] fix(pos-rich-link): clarify test mock function name
---
.../components/pos-rich-link/pos-rich-link.spec.tsx | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx b/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
index 21eb77cd..17ee1aa1 100644
--- a/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
+++ b/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
@@ -52,23 +52,23 @@ describe('pos-rich-link with uri', () => {
describe('pos-rich-link without uri', () => {
it('does not emit pod-os:resource event if uri is present', async () => {
- const receiveResource = jest.fn();
+ const onResource = jest.fn();
const page = await newSpecPage({
components: [PosRichLink],
});
- page.body.addEventListener('pod-os:resource', receiveResource);
+ page.body.addEventListener('pod-os:resource', onResource);
await page.setContent('');
- expect(receiveResource).toHaveBeenCalledTimes(0);
+ expect(onResource).toHaveBeenCalledTimes(0);
});
it('receives resource and sets it as link if uri is not present', async () => {
- const receiveResource = jest.fn();
+ const onResource = jest.fn();
const page = await newSpecPage({
components: [PosRichLink],
});
- page.body.addEventListener('pod-os:resource', receiveResource);
+ page.body.addEventListener('pod-os:resource', onResource);
await page.setContent('');
- expect(receiveResource).toHaveBeenCalledTimes(1);
+ expect(onResource).toHaveBeenCalledTimes(1);
await page.rootInstance.receiveResource({
uri: 'https://pod.example/resource',
From 3e453781bb2b5960038e4da3d2b37205ae0afde0 Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Thu, 10 Jul 2025 21:10:43 +1000
Subject: [PATCH 05/12] refactor(pos-rich-link): use content function
---
.../components/pos-rich-link/pos-rich-link.tsx | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.tsx b/elements/src/components/pos-rich-link/pos-rich-link.tsx
index 4d958fa9..bad97971 100644
--- a/elements/src/components/pos-rich-link/pos-rich-link.tsx
+++ b/elements/src/components/pos-rich-link/pos-rich-link.tsx
@@ -39,12 +39,7 @@ export class PosRichLink implements ResourceAware {
};
render() {
- if (this.error) return this.error;
-
- const uri = this.uri || this.link;
- if (!uri) return null;
-
- const content = (
+ const content = (uri: string) => (
);
- if (this.link) {
- return content;
+ if (this.error) {
+ return this.error;
+ } else if (this.link) {
+ return content(this.link);
} else if (this.uri) {
return (
- {content}
+ {content(this.uri)}
);
+ } else {
+ return null;
}
}
}
From 20ff56212cf9349fb1098cb3f0d706f2f5f36505 Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Thu, 10 Jul 2025 21:21:14 +1000
Subject: [PATCH 06/12] test(pos-rich-link): test Thing.relations is called
with correct rel
---
.../components/pos-rich-link/pos-rich-link.spec.tsx | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx b/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
index 17ee1aa1..9b27e7f1 100644
--- a/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
+++ b/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
@@ -1,6 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { PosRichLink } from './pos-rich-link';
import { getByText } from '@testing-library/dom';
+import { when } from 'jest-when';
describe('pos-rich-link with uri', () => {
let page;
@@ -103,10 +104,15 @@ describe('pos-rich-link without uri', () => {
components: [PosRichLink],
html: ``,
});
- await page.rootInstance.receiveResource({
+ const thing = {
uri: 'https://pod.example/resource',
- relations: () => [{ predicate: 'https://schema.org/video', uris: ['https://video.test/video-1'] }],
- });
+ relations: jest.fn(),
+ };
+ when(thing.relations)
+ .calledWith('https://schema.org/video')
+ .mockReturnValue([{ predicate: 'https://schema.org/video', uris: ['https://video.test/video-1'] }]);
+
+ await page.rootInstance.receiveResource(thing);
await page.waitForChanges();
const link = page.root?.shadowRoot?.querySelector('a');
expect(link).toEqualAttribute('href', 'https://video.test/video-1');
From d8827c10ff3bcc192d6327033388b0ca25760dea Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Thu, 10 Jul 2025 21:43:56 +1000
Subject: [PATCH 07/12] feat(pos-rich-link): follow rev attribute if present
---
.../pos-rich-link/pos-rich-link.spec.tsx | 19 ++++++++++++++++++
.../pos-rich-link/pos-rich-link.tsx | 20 +++++++++++++++++--
2 files changed, 37 insertions(+), 2 deletions(-)
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx b/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
index 9b27e7f1..9e1a8566 100644
--- a/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
+++ b/elements/src/components/pos-rich-link/pos-rich-link.spec.tsx
@@ -118,6 +118,25 @@ describe('pos-rich-link without uri', () => {
expect(link).toEqualAttribute('href', 'https://video.test/video-1');
});
+ it('uses the matching relation if rev prop is defined', async () => {
+ const page = await newSpecPage({
+ components: [PosRichLink],
+ html: ``,
+ });
+ const thing = {
+ uri: 'https://video.test/video-1',
+ reverseRelations: jest.fn(),
+ };
+ when(thing.reverseRelations)
+ .calledWith('https://schema.org/video')
+ .mockReturnValue([{ predicate: 'https://schema.org/video', uris: ['https://pod.example/resource'] }]);
+
+ await page.rootInstance.receiveResource(thing);
+ await page.waitForChanges();
+ const link = page.root?.shadowRoot?.querySelector('a');
+ expect(link).toEqualAttribute('href', 'https://pod.example/resource');
+ });
+
it('displays an error if no link is found', async () => {
const page = await newSpecPage({
components: [PosRichLink],
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.tsx b/elements/src/components/pos-rich-link/pos-rich-link.tsx
index bad97971..06235491 100644
--- a/elements/src/components/pos-rich-link/pos-rich-link.tsx
+++ b/elements/src/components/pos-rich-link/pos-rich-link.tsx
@@ -8,8 +8,18 @@ import { ResourceAware, ResourceEventEmitter, subscribeResource } from '../event
styleUrl: 'pos-rich-link.css',
})
export class PosRichLink implements ResourceAware {
+ /**
+ * Link will use this URI
+ */
@Prop() uri?: string;
+ /**
+ * Link will be obtained by following the predicate with this URI forward from a resource
+ */
@Prop() rel?: string;
+ /**
+ * Link will be obtained by following the predicate with this URI in reverse from a resource
+ */
+ @Prop() rev?: string;
@Event({ eventName: 'pod-os:link' }) linkEmitter: EventEmitter;
@@ -24,8 +34,14 @@ export class PosRichLink implements ResourceAware {
}
receiveResource = (resource: Thing) => {
- if (this.rel) {
- const links = resource.relations(this.rel);
+ if (this.rel || this.rev) {
+ let links = [];
+ if (this.rel) {
+ links = resource.relations(this.rel);
+ } else if (this.rev) {
+ links = resource.reverseRelations(this.rev);
+ }
+
if (links.length == 0) {
this.error = 'No matching link found';
} else if (links[0].uris.length > 1) {
From 83ee599a504445762e57ca45307a77627f8009aa Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Thu, 10 Jul 2025 21:49:24 +1000
Subject: [PATCH 08/12] doc(elements): update generated readmes with
pos-rich-link
---
docs/elements/apps/pos-app-browser/readme.md | 10 +++----
.../pos-example-resources/readme.md | 2 +-
.../elements/apps/pos-app-dashboard/readme.md | 2 +-
docs/elements/apps/pos-app-generic/readme.md | 2 +-
.../apps/pos-app-ldp-container/readme.md | 2 +-
.../apps/pos-app-rdf-document/readme.md | 2 +-
.../components/pos-internal-router/readme.md | 2 +-
.../components/pos-navigation-bar/readme.md | 2 +-
.../components/pos-relations/readme.md | 2 +-
.../pos-reverse-relations/readme.md | 2 +-
.../components/pos-rich-link/readme.md | 19 ++++++++------
.../components/pos-subjects/readme.md | 2 +-
elements/src/components.d.ts | 26 ++++++++++++++++++-
13 files changed, 51 insertions(+), 24 deletions(-)
diff --git a/docs/elements/apps/pos-app-browser/readme.md b/docs/elements/apps/pos-app-browser/readme.md
index 219a2574..3834dc2c 100644
--- a/docs/elements/apps/pos-app-browser/readme.md
+++ b/docs/elements/apps/pos-app-browser/readme.md
@@ -7,10 +7,10 @@
## Properties
-| Property | Attribute | Description | Type | Default |
-| ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- |
-| `mode` | `mode` | The mode the app is running in: - standalone: use this when you deploy it as a standalone web application - pod: use this when you host this app as a default interface for you pod | `"pod" \| "standalone"` | `'standalone'` |
-| `restorePreviousSession` | `restore-previous-session` | | `boolean` | `false` |
+| Property | Attribute | Description | Type | Default |
+| ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | -------------- |
+| `mode` | `mode` | The mode the app is running in: - standalone: use this when you deploy it as a standalone web application - pod: use this when you host this app as a default interface for you pod | `"pod" \| "standalone"` | `'standalone'` |
+| `restorePreviousSession` | `restore-previous-session` | | `boolean` | `false` |
## Dependencies
@@ -55,9 +55,9 @@ graph TD;
pos-make-findable --> pos-label
pos-resource --> ion-progress-bar
ion-searchbar --> ion-icon
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-login --> pos-resource
pos-login --> pos-picture
pos-login --> pos-label
diff --git a/docs/elements/apps/pos-app-dashboard/pos-example-resources/readme.md b/docs/elements/apps/pos-app-dashboard/pos-example-resources/readme.md
index be8580d5..e5129690 100644
--- a/docs/elements/apps/pos-app-dashboard/pos-example-resources/readme.md
+++ b/docs/elements/apps/pos-app-dashboard/pos-example-resources/readme.md
@@ -16,9 +16,9 @@
```mermaid
graph TD;
pos-example-resources --> pos-rich-link
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-resource --> ion-progress-bar
pos-app-dashboard --> pos-example-resources
style pos-example-resources fill:#f9f,stroke:#333,stroke-width:4px
diff --git a/docs/elements/apps/pos-app-dashboard/readme.md b/docs/elements/apps/pos-app-dashboard/readme.md
index bfd7c2ed..6b7ea98b 100644
--- a/docs/elements/apps/pos-app-dashboard/readme.md
+++ b/docs/elements/apps/pos-app-dashboard/readme.md
@@ -19,9 +19,9 @@ graph TD;
pos-app-dashboard --> pos-getting-started
pos-app-dashboard --> pos-example-resources
pos-example-resources --> pos-rich-link
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-resource --> ion-progress-bar
pos-internal-router --> pos-app-dashboard
style pos-app-dashboard fill:#f9f,stroke:#333,stroke-width:4px
diff --git a/docs/elements/apps/pos-app-generic/readme.md b/docs/elements/apps/pos-app-generic/readme.md
index bb20fa58..19fbdd54 100644
--- a/docs/elements/apps/pos-app-generic/readme.md
+++ b/docs/elements/apps/pos-app-generic/readme.md
@@ -39,9 +39,9 @@ graph TD;
pos-add-literal-value --> pos-select-term
pos-relations --> pos-predicate
pos-relations --> pos-rich-link
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-resource --> ion-progress-bar
pos-reverse-relations --> pos-predicate
pos-reverse-relations --> pos-rich-link
diff --git a/docs/elements/apps/pos-app-ldp-container/readme.md b/docs/elements/apps/pos-app-ldp-container/readme.md
index c292a1ab..71e6610b 100644
--- a/docs/elements/apps/pos-app-ldp-container/readme.md
+++ b/docs/elements/apps/pos-app-ldp-container/readme.md
@@ -28,9 +28,9 @@ graph TD;
pos-resource --> ion-progress-bar
pos-container-item --> ion-icon
pos-subjects --> pos-rich-link
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-type-badges --> ion-badge
pos-type-badges --> ion-icon
pos-literals --> pos-predicate
diff --git a/docs/elements/apps/pos-app-rdf-document/readme.md b/docs/elements/apps/pos-app-rdf-document/readme.md
index c144f960..02389038 100644
--- a/docs/elements/apps/pos-app-rdf-document/readme.md
+++ b/docs/elements/apps/pos-app-rdf-document/readme.md
@@ -22,9 +22,9 @@ graph TD;
pos-app-rdf-document --> pos-type-badges
pos-app-rdf-document --> pos-literals
pos-subjects --> pos-rich-link
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-resource --> ion-progress-bar
pos-type-badges --> ion-badge
pos-type-badges --> ion-icon
diff --git a/docs/elements/components/pos-internal-router/readme.md b/docs/elements/components/pos-internal-router/readme.md
index 497c0353..9a069e59 100644
--- a/docs/elements/components/pos-internal-router/readme.md
+++ b/docs/elements/components/pos-internal-router/readme.md
@@ -29,9 +29,9 @@ graph TD;
pos-app-dashboard --> pos-getting-started
pos-app-dashboard --> pos-example-resources
pos-example-resources --> pos-rich-link
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-resource --> ion-progress-bar
pos-app-browser --> pos-internal-router
style pos-internal-router fill:#f9f,stroke:#333,stroke-width:4px
diff --git a/docs/elements/components/pos-navigation-bar/readme.md b/docs/elements/components/pos-navigation-bar/readme.md
index 48ecfdda..f96a8a6a 100644
--- a/docs/elements/components/pos-navigation-bar/readme.md
+++ b/docs/elements/components/pos-navigation-bar/readme.md
@@ -42,9 +42,9 @@ graph TD;
pos-make-findable --> pos-label
pos-resource --> ion-progress-bar
ion-searchbar --> ion-icon
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-app-browser --> pos-navigation-bar
style pos-navigation-bar fill:#f9f,stroke:#333,stroke-width:4px
```
diff --git a/docs/elements/components/pos-relations/readme.md b/docs/elements/components/pos-relations/readme.md
index a7d3807e..3017addb 100644
--- a/docs/elements/components/pos-relations/readme.md
+++ b/docs/elements/components/pos-relations/readme.md
@@ -29,9 +29,9 @@ graph TD;
pos-relations --> pos-predicate
pos-relations --> pos-rich-link
pos-predicate --> ion-icon
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-resource --> ion-progress-bar
pos-app-generic --> pos-relations
style pos-relations fill:#f9f,stroke:#333,stroke-width:4px
diff --git a/docs/elements/components/pos-reverse-relations/readme.md b/docs/elements/components/pos-reverse-relations/readme.md
index 2292c79a..b0f9b1eb 100644
--- a/docs/elements/components/pos-reverse-relations/readme.md
+++ b/docs/elements/components/pos-reverse-relations/readme.md
@@ -29,9 +29,9 @@ graph TD;
pos-reverse-relations --> pos-predicate
pos-reverse-relations --> pos-rich-link
pos-predicate --> ion-icon
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-resource --> ion-progress-bar
pos-app-generic --> pos-reverse-relations
style pos-reverse-relations fill:#f9f,stroke:#333,stroke-width:4px
diff --git a/docs/elements/components/pos-rich-link/readme.md b/docs/elements/components/pos-rich-link/readme.md
index b9563d04..efc40f69 100644
--- a/docs/elements/components/pos-rich-link/readme.md
+++ b/docs/elements/components/pos-rich-link/readme.md
@@ -7,16 +7,19 @@
## Properties
-| Property | Attribute | Description | Type | Default |
-| -------- | --------- | ----------- | -------- | ----------- |
-| `uri` | `uri` | | `string` | `undefined` |
+| Property | Attribute | Description | Type | Default |
+| -------- | --------- | ----------------------------------------------------------------------------------------- | -------- | ----------- |
+| `rel` | `rel` | Link will be obtained by following the predicate with this URI forward from a resource | `string` | `undefined` |
+| `rev` | `rev` | Link will be obtained by following the predicate with this URI in reverse from a resource | `string` | `undefined` |
+| `uri` | `uri` | Link will use this URI | `string` | `undefined` |
## Events
-| Event | Description | Type |
-| ------------- | ----------- | ------------------ |
-| `pod-os:link` | | `CustomEvent` |
+| Event | Description | Type |
+| ----------------- | ----------- | ------------------ |
+| `pod-os:link` | | `CustomEvent` |
+| `pod-os:resource` | | `CustomEvent` |
## Dependencies
@@ -31,16 +34,16 @@
### Depends on
-- [pos-resource](../pos-resource)
- [pos-label](../pos-label)
- [pos-description](../pos-description)
+- [pos-resource](../pos-resource)
### Graph
```mermaid
graph TD;
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-resource --> ion-progress-bar
pos-example-resources --> pos-rich-link
pos-navigation-bar --> pos-rich-link
diff --git a/docs/elements/components/pos-subjects/readme.md b/docs/elements/components/pos-subjects/readme.md
index 8e300153..7d1e2101 100644
--- a/docs/elements/components/pos-subjects/readme.md
+++ b/docs/elements/components/pos-subjects/readme.md
@@ -27,9 +27,9 @@
```mermaid
graph TD;
pos-subjects --> pos-rich-link
- pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
+ pos-rich-link --> pos-resource
pos-resource --> ion-progress-bar
pos-app-ldp-container --> pos-subjects
pos-app-rdf-document --> pos-subjects
diff --git a/elements/src/components.d.ts b/elements/src/components.d.ts
index a2885085..033be9b3 100644
--- a/elements/src/components.d.ts
+++ b/elements/src/components.d.ts
@@ -121,7 +121,18 @@ export namespace Components {
interface PosReverseRelations {
}
interface PosRichLink {
- "uri": string;
+ /**
+ * Link will be obtained by following the predicate with this URI forward from a resource
+ */
+ "rel"?: string;
+ /**
+ * Link will be obtained by following the predicate with this URI in reverse from a resource
+ */
+ "rev"?: string;
+ /**
+ * Link will use this URI
+ */
+ "uri"?: string;
}
/**
* The responsibility of pos-router is to handle the `uri` query param, that specifies the URI of the resource that is currently opened.
@@ -724,6 +735,7 @@ declare global {
};
interface HTMLPosRichLinkElementEventMap {
"pod-os:link": any;
+ "pod-os:resource": any;
}
interface HTMLPosRichLinkElement extends Components.PosRichLink, HTMLStencilElement {
addEventListener(type: K, listener: (this: HTMLPosRichLinkElement, ev: PosRichLinkCustomEvent) => any, options?: boolean | AddEventListenerOptions): void;
@@ -1067,6 +1079,18 @@ declare namespace LocalJSX {
}
interface PosRichLink {
"onPod-os:link"?: (event: PosRichLinkCustomEvent) => void;
+ "onPod-os:resource"?: (event: PosRichLinkCustomEvent) => void;
+ /**
+ * Link will be obtained by following the predicate with this URI forward from a resource
+ */
+ "rel"?: string;
+ /**
+ * Link will be obtained by following the predicate with this URI in reverse from a resource
+ */
+ "rev"?: string;
+ /**
+ * Link will use this URI
+ */
"uri"?: string;
}
/**
From e6972f723be642f93d17b3e97140758866d19a38 Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Thu, 10 Jul 2025 21:53:47 +1000
Subject: [PATCH 09/12] doc(elements): add pos-rich-link changes to changelog
---
elements/CHANGELOG.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/elements/CHANGELOG.md b/elements/CHANGELOG.md
index 1396b84d..6ab9677d 100644
--- a/elements/CHANGELOG.md
+++ b/elements/CHANGELOG.md
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## Unreleased
+### Changed
+
+- [pos-rich-link](../docs/elements/components/pos-rich-link):
+ - can now receive a resource to use for the link
+ - can now follow `rel` and `rev` to discover a resource to use for the link
+
### Fixed
- [pos-app-browser](../docs/elements/apps/pos-app-browser): prevent error message flashing up while uri is unset on hard refresh
From e1abc7dda6e5ab5adf0a5da4afdb97a2bab450d4 Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Sat, 12 Jul 2025 00:23:16 +1000
Subject: [PATCH 10/12] fix(pos-rich-link): rel and rev use nested pos-resource
---
.../pos-rich-link.integration.spec.tsx | 158 ++++++++++++++++++
.../pos-rich-link/pos-rich-link.tsx | 10 +-
2 files changed, 167 insertions(+), 1 deletion(-)
create mode 100644 elements/src/components/pos-rich-link/pos-rich-link.integration.spec.tsx
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.integration.spec.tsx b/elements/src/components/pos-rich-link/pos-rich-link.integration.spec.tsx
new file mode 100644
index 00000000..0784add9
--- /dev/null
+++ b/elements/src/components/pos-rich-link/pos-rich-link.integration.spec.tsx
@@ -0,0 +1,158 @@
+import { newSpecPage } from '@stencil/core/testing';
+import { mockPodOS } from '../../test/mockPodOS';
+import { PosApp } from '../pos-app/pos-app';
+import { PosDescription } from '../pos-description/pos-description';
+import { PosLabel } from '../pos-label/pos-label';
+import { PosResource } from '../pos-resource/pos-resource';
+import { PosRichLink } from './pos-rich-link';
+import { when } from 'jest-when';
+import { Component, h } from '@stencil/core';
+
+describe('pos-rich-link', () => {
+ let os;
+ beforeEach(async () => {
+ os = mockPodOS();
+ when(os.store.get)
+ .calledWith('https://resource.test')
+ .mockReturnValue({
+ uri: 'https://resource.test',
+ label: () => 'Test label',
+ description: () => 'Test description',
+ relations: () => [{ predicate: 'https://schema.org/video', uris: ['https://video.test/video-1'] }],
+ });
+ when(os.store.get)
+ .calledWith('https://video.test/video-1')
+ .mockReturnValue({
+ uri: 'https://video.test/video-1',
+ label: () => 'Video 1',
+ description: () => 'Description of Video 1',
+ reverseRelations: () => [{ predicate: 'https://schema.org/video', uris: ['https://resource.test'] }],
+ });
+ });
+
+ it('can be used outside resource', async () => {
+ const page = await newSpecPage({
+ components: [PosApp, PosDescription, PosLabel, PosResource, PosRichLink],
+ supportsShadowDom: false,
+ html: `
+
+
+ `,
+ });
+
+ const link = page.root?.querySelector('pos-rich-link');
+ expect(link).toEqualHtml(`
+
+
+
+
+
+ Test label
+
+
+ resource.test
+
+ Test description
+
+
+
+
+ `);
+ });
+
+ it('receives and renders resource', async () => {
+ const page = await newSpecPage({
+ components: [PosApp, PosDescription, PosLabel, PosResource, PosRichLink],
+ supportsShadowDom: false,
+ html: `
+
+
+
+
+ `,
+ });
+
+ expect(os.store.get.mock.calls).toHaveLength(1);
+
+ const link = page.root?.querySelector('pos-rich-link');
+ expect(link).toEqualHtml(`
+
+
+
+
+ Test label
+
+
+ resource.test
+
+ Test description
+
+
+
+ `);
+ });
+
+ it('uses label and description of the matching rel', async () => {
+ const page = await newSpecPage({
+ components: [PosApp, PosDescription, PosLabel, PosResource, PosRichLink],
+ supportsShadowDom: false,
+ html: `
+
+
+
+
+ `,
+ });
+
+ const link = page.root?.querySelector('pos-rich-link');
+ expect(link).toEqualHtml(`
+
+
+
+
+
+ Video 1
+
+
+ video.test
+
+ Description of Video 1
+
+
+
+
+ `);
+ });
+
+ it('uses label and description of the matching rev', async () => {
+ const page = await newSpecPage({
+ components: [PosApp, PosDescription, PosLabel, PosResource, PosRichLink],
+ supportsShadowDom: false,
+ html: `
+
+
+
+
+ `,
+ });
+
+ const link = page.root?.querySelector('pos-rich-link');
+ expect(link).toEqualHtml(`
+
+
+
+
+
+ Test label
+
+
+ resource.test
+
+ Test description
+
+
+
+
+ `);
+ });
+});
diff --git a/elements/src/components/pos-rich-link/pos-rich-link.tsx b/elements/src/components/pos-rich-link/pos-rich-link.tsx
index 06235491..9b4aa828 100644
--- a/elements/src/components/pos-rich-link/pos-rich-link.tsx
+++ b/elements/src/components/pos-rich-link/pos-rich-link.tsx
@@ -27,6 +27,7 @@ export class PosRichLink implements ResourceAware {
subscribeResource: ResourceEventEmitter;
@State() link?: string;
+ @State() followPredicate: boolean = false;
@State() error: string = null;
componentWillLoad() {
@@ -34,7 +35,8 @@ export class PosRichLink implements ResourceAware {
}
receiveResource = (resource: Thing) => {
- if (this.rel || this.rev) {
+ this.followPredicate = typeof this.rel != 'undefined' || typeof this.rev != 'undefined';
+ if (this.followPredicate) {
let links = [];
if (this.rel) {
links = resource.relations(this.rel);
@@ -73,6 +75,12 @@ export class PosRichLink implements ResourceAware {
if (this.error) {
return this.error;
+ } else if (this.followPredicate) {
+ return (
+
+ {content(this.link)}
+
+ );
} else if (this.link) {
return content(this.link);
} else if (this.uri) {
From f688cfdb418e79339b4f254abbc1a3340f4c7c8f Mon Sep 17 00:00:00 2001
From: jg10 <181532694+jg10-mastodon-social@users.noreply.github.com>
Date: Sat, 12 Jul 2025 00:27:49 +1000
Subject: [PATCH 11/12] doc(pos-rich-link): add storybook examples
---
.../navigation/0_pos-rich-link.stories.mdx | 87 +++++++++++++++----
1 file changed, 68 insertions(+), 19 deletions(-)
diff --git a/storybook/stories/navigation/0_pos-rich-link.stories.mdx b/storybook/stories/navigation/0_pos-rich-link.stories.mdx
index 201d3acc..d35b3c82 100644
--- a/storybook/stories/navigation/0_pos-rich-link.stories.mdx
+++ b/storybook/stories/navigation/0_pos-rich-link.stories.mdx
@@ -1,35 +1,84 @@
-import {
- html
-} from "lit-html";
+import { html } from "lit-html";
-import {
- Canvas,
- Meta,
- Story
-} from '@storybook/addon-docs/blocks';
+import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
-
## pos-rich-link
-Links to the given URI and shows a label and description of the resource identified by that URI, if available.
+Links to the given URI and shows a label and description of the resource
+identified by that URI, if available.
+
+
+
+Link can be obtained by receiving a resource from an ancestor element
+
+
+
+Link can be obtained by following the predicate forward from a resource
+
+
+
+Link can be obtained by following a predicate in reverse from a resource
+
+
+
+Error is shown if there is no matching link
+
+
+
+Error is shown if there is more than one link
-