Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion components/check-permissions/checkPermissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
import TPEN from '../../api/TPEN.js'
import { getUserFromToken } from '../../components/iiif-tools/index.js'
import { permissionMatch } from "../../components/check-permissions/permission-match.js"
import { whenProjectReady } from "../../utilities/projectReady.js"

class checkPermissions {
#project
#userId

constructor() {
TPEN.eventDispatcher.on('tpen-project-loaded', (ev) => {
whenProjectReady((ev) => {
this.#project = ev.detail
this.#userId = getUserFromToken(TPEN.getAuthorization())
})
Expand Down
6 changes: 5 additions & 1 deletion components/check-permissions/permission-match.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import TPEN from '../../api/TPEN.js'
import { getUserFromToken } from "../../components/iiif-tools/index.js"
import { whenProjectReady } from "../../utilities/projectReady.js"
// TODO use these from a central location, such as a Permission Class.
const ENTITIES = [
"PROJECT",
Expand All @@ -39,7 +40,10 @@ const ENTITIES = [
"*",
"ANY"
]
TPEN.eventDispatcher.on("tpen-project-loaded", ev => checkElements(ev.detail))
// Defer one microtask so any tpen-view/tpen-edit elements added by
// sibling modules during their own evaluation are present in the DOM
// before checkElements scans for them.
queueMicrotask(() => whenProjectReady(ev => checkElements(ev.detail)))

/**
* Gather all elements with the tpen-view or tpen-edit attributes.
Expand Down
4 changes: 2 additions & 2 deletions components/update-metadata/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ <h3>Edit Metadata</h3>
</tpen-page>
<script type="module">
import CheckPermissions from '../check-permissions/checkPermissions.js'
import TPEN from '../../api/TPEN.js'
import { whenProjectReady } from '../../utilities/projectReady.js'

TPEN.eventDispatcher.on('tpen-project-loaded', () => {
whenProjectReady(() => {
const divMetadata = document.querySelector('.tpen-metadata')
const permissionMsg = document.querySelector('.permission-msg')
const scope = divMetadata.getAttribute('tpen-scope')
Expand Down
4 changes: 2 additions & 2 deletions interfaces/manage-layers/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ <h2 slot="header">Layers and Pages</h2>
</tpen-page>
</body>
<script type="module">
import TPEN from '../../api/TPEN.js'
import CheckPermissions from '../../components/check-permissions/checkPermissions.js'
import { whenProjectReady } from '../../utilities/projectReady.js'

TPEN.eventDispatcher.on('tpen-project-loaded', (ev) => {
whenProjectReady((ev) => {
if (CheckPermissions.checkEditAccess('PROJECT')) {
const goManage = document.getElementById("projectManagementBtn")
if (goManage) {
Expand Down
5 changes: 2 additions & 3 deletions interfaces/manage-project/collaborators.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ <h2 slot="header">Manage Collaborators</h2>
<tpen-feedback-button></tpen-feedback-button>
</body>
<script type="module">
import TPEN from '../../api/TPEN.js'
import CheckPermissions from '../../components/check-permissions/checkPermissions.js'
import { whenProjectReady } from '../../utilities/projectReady.js'

TPEN.eventDispatcher.on('tpen-project-loaded', (ev) => {
whenProjectReady((ev) => {
if (CheckPermissions.checkEditAccess('PROJECT')) {
const goManage = document.getElementById("projectManagementBtn")
if (goManage) {
Expand All @@ -87,6 +87,5 @@ <h2 slot="header">Manage Collaborators</h2>
}
}
})

</script>
</html>
4 changes: 2 additions & 2 deletions interfaces/manage-project/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ <h2 slot="header">Export & Links</h2>
</tpen-page>
<script type="module">
import CheckPermissions from '../../components/check-permissions/checkPermissions.js'
import TPEN from '../../api/TPEN.js'
import { whenProjectReady } from '../../utilities/projectReady.js'

TPEN.eventDispatcher.on('tpen-project-loaded', ev => {
whenProjectReady(ev => {
const goParse = document.getElementById("goParse")
const goTranscribe = document.getElementById("goTranscribe")
if (goParse
Expand Down
3 changes: 2 additions & 1 deletion interfaces/manage-project/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import "../../components/project-layers/index.js"
import "../../components/project-tools/index.js"
import CheckPermissions from "../../components/check-permissions/checkPermissions.js"
import { confirmAction } from "../../utilities/confirmAction.js"
import { whenProjectReady } from "../../utilities/projectReady.js"

const container = document.body
TPEN.attachAuthentication(container)

// Single consolidated listener for project loaded event
TPEN.eventDispatcher.on('tpen-project-loaded', () => {
whenProjectReady(() => {
const projectID = TPEN.screen.projectInQuery

// Set href for navigation links
Expand Down
9 changes: 4 additions & 5 deletions interfaces/project/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,18 @@ <h2 slot="header">Project Details <a id="projectManagementBtn" title="Manage Pro
</tpen-page>
</body>
<script type="module">
import TPEN from '../../api/TPEN.js'
import CheckPermissions from '../../components/check-permissions/checkPermissions.js'
import { whenProjectReady } from '../../utilities/projectReady.js'

TPEN.eventDispatcher.on('tpen-project-loaded', (ev) => {
whenProjectReady((ev) => {
const goParse = document.getElementById("goParse")
const goTranscribe = document.getElementById("goTranscribe")
const leaveProject = document.getElementById("leaveProject")
if (leaveProject) {
leaveProject.href = `/project/leave?projectID=${ev.detail._id}`
}
if (goParse &&
CheckPermissions.checkEditAccess("LINE", "SELECTOR") &&
if (goParse &&
CheckPermissions.checkEditAccess("LINE", "SELECTOR") &&
CheckPermissions.checkCreateAccess("LINE", "SELECTOR")) {
goParse.setAttribute("href", `/annotator?projectID=${ev.detail._id}`)
goParse.hidden = false
Expand All @@ -119,6 +119,5 @@ <h2 slot="header">Project Details <a id="projectManagementBtn" title="Manage Pro
}
}
})

</script>
</html>
4 changes: 2 additions & 2 deletions interfaces/project/metadata.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ <h3>Edit Project Metadata</h3>
<a id="projectManagementBtn"><span aria-hidden="true">↪</span> Go to Project Management</a>
</tpen-page>
<script type="module">
import TPEN from '../../api/TPEN.js'
import CheckPermissions from '../../components/check-permissions/checkPermissions.js'
import { whenProjectReady } from '../../utilities/projectReady.js'

TPEN.eventDispatcher.on('tpen-project-loaded', (ev) => {
whenProjectReady((ev) => {
if (CheckPermissions.checkEditAccess('PROJECT')) {
const goManage = document.getElementById("projectManagementBtn")
if (goManage) {
Expand Down
4 changes: 2 additions & 2 deletions interfaces/project/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ <h2 slot="header">Project Configuration</h2>
</body>

<script type="module">
import TPEN from '../../api/TPEN.js'
import CheckPermissions from '../../components/check-permissions/checkPermissions.js'
import { whenProjectReady } from '../../utilities/projectReady.js'

TPEN.eventDispatcher.on('tpen-project-loaded', (ev) => {
whenProjectReady((ev) => {
if (CheckPermissions.checkEditAccess('PROJECT')) {
const goManage = document.getElementById("projectManagementBtn")
if (goManage) {
Expand Down
5 changes: 2 additions & 3 deletions interfaces/quicktype/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@
</body>

<script type="module">
import TPEN from '../../api/TPEN.js'
import CheckPermissions from '../../components/check-permissions/checkPermissions.js'
import { whenProjectReady } from '../../utilities/projectReady.js'

TPEN.eventDispatcher.on('tpen-project-loaded', (ev) => {
whenProjectReady((ev) => {
if (CheckPermissions.checkEditAccess('PROJECT')) {
const goManage = document.getElementById("projectManagementBtn")
if (goManage) {
Expand All @@ -72,7 +72,6 @@
}
}
})

</script>

</html>
65 changes: 64 additions & 1 deletion utilities/__tests__/projectReady.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const mockDispatcher = {
const { default: TPEN } = await import('../../api/TPEN.js')
TPEN.eventDispatcher = mockDispatcher

const { onProjectReady } = await import('../projectReady.js')
const { onProjectReady, whenProjectReady } = await import('../projectReady.js')

describe('onProjectReady', () => {
afterEach(() => {
Expand Down Expand Up @@ -63,3 +63,66 @@ describe('onProjectReady', () => {
})
})
})

describe('whenProjectReady', () => {
afterEach(() => {
mockDispatcher._handlers.clear()
TPEN.activeProject = undefined
})

it('subscribes handler to tpen-project-loaded event', () => {
let fired = 0
whenProjectReady(() => { fired++ })
mockDispatcher.dispatch('tpen-project-loaded', { _id: 'p1' })
assert.equal(fired, 1)
})

it('invokes synchronously with synthetic event when project already loaded', () => {
const project = { _createdAt: Date.now(), _id: 'p1' }
TPEN.activeProject = project
let received
whenProjectReady(ev => { received = ev })
assert.deepEqual(received, { detail: project })
})

it('does not invoke synchronously when project is not loaded', () => {
let fired = 0
whenProjectReady(() => { fired++ })
assert.equal(fired, 0)
})

it('returns an unsubscribe function', () => {
let fired = 0
const unsub = whenProjectReady(() => { fired++ })
unsub()
mockDispatcher.dispatch('tpen-project-loaded', { _id: 'p1' })
assert.equal(fired, 0)
})

it('returns a no-op when handler is missing', () => {
assert.doesNotThrow(() => { whenProjectReady(null)?.() })
})

it('does not also receive future dispatches after a sync fire', () => {
TPEN.activeProject = { _createdAt: Date.now(), _id: 'p1' }
let fired = 0
whenProjectReady(() => { fired++ })
mockDispatcher.dispatch('tpen-project-loaded', { _id: 'p2' })
assert.equal(fired, 1)
})

it('logs and recovers when sync handler throws', () => {
TPEN.activeProject = { _createdAt: Date.now(), _id: 'p1' }
const originalError = console.error
let logged
console.error = (...args) => { logged = args }
try {
assert.doesNotThrow(() => {
whenProjectReady(() => { throw new Error('boom') })
})
assert.ok(logged?.[0]?.includes('[whenProjectReady]'))
} finally {
console.error = originalError
}
})
})
48 changes: 45 additions & 3 deletions utilities/projectReady.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,55 @@
import TPEN from "../api/TPEN.js"

/**
* Bind `handler` to `ctx` and invoke it when the active project is ready.
* Intended for component lifecycle use where the handler needs a `this`
* context (typically a custom element). If the project is already loaded
* when called, the bound handler is invoked synchronously and no listener
* is registered. Otherwise, subscribes for the next `tpen-project-loaded`
* dispatch. The sync path and the listener are mutually exclusive — the
* handler is invoked exactly once for the load.
* @param {object} ctx - Object to bind the handler to (e.g. a component instance).
* @param {(this: object, ev?: { detail: any }) => void} handler
* @param {string} [eventName='tpen-project-loaded']
* @returns {() => void} unsubscribe (no-op when invoked synchronously or when ctx/handler is missing)
*/
export const onProjectReady = (ctx, handler, eventName = 'tpen-project-loaded') => {
if (!ctx || typeof handler !== 'function') return () => {}
const bound = handler.bind(ctx)
try {
if (TPEN.activeProject?._createdAt) {
if (TPEN.activeProject?._createdAt) {
try {
bound()
} catch (err) {
console.error('[onProjectReady] handler threw during sync invocation:', err)
}
} catch (_) {}
return () => {}
}
TPEN.eventDispatcher.on(eventName, bound)
return () => TPEN.eventDispatcher.off(eventName, bound)
}

/**
* Context-free variant of {@link onProjectReady} for inline `<script type="module">`
* blocks that have no element to bind to. If the project is already loaded when
* called, the handler is invoked synchronously with a synthetic
* `{ detail: TPEN.activeProject }` event so existing `ev.detail.*` handler bodies
* keep working, and no listener is registered. Otherwise, subscribes for the
* next `tpen-project-loaded` dispatch. The sync path and the listener are
* mutually exclusive — the handler is invoked exactly once for the load.
* @param {(ev: { detail: any }) => void} handler
* @param {string} [eventName='tpen-project-loaded']
* @returns {() => void} unsubscribe (no-op when invoked synchronously)
*/
export const whenProjectReady = (handler, eventName = 'tpen-project-loaded') => {
if (typeof handler !== 'function') return () => {}
if (TPEN.activeProject?._createdAt) {
try {
handler({ detail: TPEN.activeProject })
} catch (err) {
console.error('[whenProjectReady] handler threw during sync invocation:', err)
}
return () => {}
}
TPEN.eventDispatcher.on(eventName, handler)
return () => TPEN.eventDispatcher.off(eventName, handler)
}
Loading