Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage collaborators #24

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
fd228a8
add manage collaborators
git-voo Sep 4, 2024
e01c768
fix PR comments
git-voo Sep 11, 2024
4370ee2
Refactor permision-based visibility of elements
git-voo Sep 11, 2024
b92f241
Add live data for project and collaborators details on /manage/*
git-voo Sep 13, 2024
4af35d4
Add auth button and script
git-voo Oct 6, 2024
921416c
authenticate projectsHighlights and projects listing
git-voo Oct 18, 2024
2e344d8
get authorization from TPEN_User
git-voo Oct 18, 2024
a6e3b6f
main merge
thehabes Oct 23, 2024
23a289e
Merge branch 'main' into manage-collaborators
thehabes Oct 23, 2024
37025cb
Bring in development + suggestions (#38)
cubap Nov 5, 2024
f8535d3
fix import path error
git-voo Nov 11, 2024
9d3c0f0
Merge branch 'main' of https://github.com/CenterForDigitalHumanities/…
git-voo Nov 11, 2024
7bb1da4
Merge branch 'main' of https://github.com/CenterForDigitalHumanities/…
git-voo Nov 12, 2024
9604626
quick fix
git-voo Nov 12, 2024
522ecf5
use the new public-login component for the auth-button.
thehabes Nov 13, 2024
8274279
new login button on /projects
thehabes Nov 14, 2024
b8f9d01
Merge branch 'manage-collaborators' of https://github.com/CenterForDi…
cubap Nov 14, 2024
d111221
Merge branch 'manage-collaborators' of https://github.com/CenterForDi…
cubap Nov 14, 2024
7146b7b
listing
cubap Nov 14, 2024
cacd407
rough - in progress
git-voo Nov 14, 2024
a787704
up and running
cubap Nov 14, 2024
9050ec6
Update index.html
cubap Nov 14, 2024
9ffe8c0
helpful cheat
cubap Nov 14, 2024
1b5d369
bad User error fixed
cubap Nov 15, 2024
782912f
contributer > collaborator
cubap Nov 15, 2024
81a4d99
working if you want it.
cubap Nov 15, 2024
e12e7ed
null check
git-voo Nov 15, 2024
4bd006b
This is definitely wrong
cubap Nov 19, 2024
3349686
touch up loading behavior
thehabes Nov 19, 2024
3a76a16
Watch for user-id changes
cubap Nov 19, 2024
97317d8
fix add/remove contributor
git-voo Nov 20, 2024
85a90c7
hotfix remove error
cubap Nov 25, 2024
0a5092f
update UI after removal
git-voo Nov 25, 2024
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
84 changes: 84 additions & 0 deletions Project/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import TPEN from "../TPEN/index.mjs"
import { userMessage } from "../components/iiif-tools/index.mjs"

export default class Project {

TPEN = new TPEN()

constructor(_id) {
this._id = _id
}

async fetch() {
const AUTH_TOKEN = TPEN.getAuthorization() ?? TPEN.login()
try {
return await fetch(`${this.TPEN.servicesURL}/project/${this._id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${AUTH_TOKEN}`
}
})
.then(response => response.ok ? response : Promise.reject(response))
.then(response => response.json())
.then(data => Object.assign(this, data))
.catch(error => { throw error })
} catch (error) {
return userMessage(`${error.status}: ${error.statusText}`)
}
}

/**
* Add a member to the project by email.
* @param {String} email The email of the member to add.
*/
async addMember(email) {
try {
const AUTH_TOKEN = TPEN.getAuthorization() ?? TPEN.login()
const response = await fetch(`${this.TPEN.servicesURL}/project/${this._id}/invite-member`, {
headers: {
Authorization: `Bearer ${AUTH_TOKEN}`,
'Content-Type': 'application/json',
},
method: "POST",
body: JSON.stringify({ email }),
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || `Failed to invite collaborator: ${response.statusText}`)
}

return await response.json()
} catch (error) {
userMessage(error.message)
}
}

/**
* Remove a member from the project by userId.
* @param {String} userId The ID of the member to remove.
*/
async removeMember(userId) {
try {
const token = TPEN.getAuthorization() ?? TPEN.login()
const response = await fetch(`${this.TPEN.servicesURL}/project/${this._id}/remove-member`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId }),
})
if (!response.ok) {
throw new Error(`Error removing member: ${response.status}`)
}

return await response
} catch (error) {
userMessage(error.message)
}
}
getLabel() {
return this.label ?? this.data?.label ?? this.metadata?.find(m => m.label === "title")?.value ?? "Untitled"
}
}
1 change: 1 addition & 0 deletions TPEN/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,6 @@ function updateUser(element, token) {
if(window?.location){
console.log("TPEN module loaded")
window.TPEN = TPEN
window.TPEN.eventDispatcher = eventDispatcher
document?.dispatchEvent(new CustomEvent("tpen-loaded"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still needed for non-module TPEN usage.

}
61 changes: 34 additions & 27 deletions User/index.mjs
Original file line number Diff line number Diff line change
@@ -1,46 +1,37 @@
import { getUserFromToken } from "../components/iiif-tools/index.mjs"
import { eventDispatcher } from "../TPEN/events.mjs"
import TPEN from "../TPEN/index.mjs"
/** Description: to use this class, initialize new class, set authentication token, then call required methods
*
*/
export default class User {
#authentication
baseURL = "https://dev.api.t-pen.org"
TPEN = new TPEN()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be #TPEN, unless we need it exposed outside this.


#isTheAuthenticatedUser() {
return this._id === getUserFromToken(TPEN.getAuthorization())
}
constructor(_id) {
this._id = _id
// if (this.#authentication || this._id) this.getProfile()
}

/**
* @param {any} token
*/
set authentication(token) {
let isNewToken = false;
if (token != this.#authentication) {
isNewToken = true
}
this.#authentication = token
if (isNewToken) this.getProfile()
}

async getProfile() {
if (!this.#authentication && !this._id)
if (!this._id)
throw Error("User ID is required")

const serviceAPI = `${this.baseURL}/${this.#authentication ? "my/profile" : `user/:${this._id}`
const serviceAPI = `${this.TPEN.servicesURL}/${this.#isTheAuthenticatedUser() ? "my/profile" : `user/:${this._id}`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this winds up calling TPEN.getAuthorization a lot, but it isn't broken, so....
That's why #authentication existed, fwiw.

}`
const headers = this.#authentication
? new Headers({ Authorization: `Bearer ${this.#authentication}` })
const headers = this.#isTheAuthenticatedUser()
? new Headers({ Authorization: `Bearer ${TPEN.getAuthorization()}` })
: new Headers()
await fetch(serviceAPI, { headers })
fetch(serviceAPI, { headers })
.then((response) => {
if (!response.ok) Promise.reject(response)
return response.json()
})
.then((data) => {
Object.assign(this, data)
// the public user object has no displayName tag, it has a name instead, hence the check below
this.displayName = data.profile.displayName ?? data.name ?? "Anonymous"
this.displayName = data.profile?.displayName ?? data.name ?? "Anonymous"
if (data._sub) {
eventDispatcher.dispatch("tpen-user-loaded", this)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line (and the import above) is still needed and how #currentUser gets set in other modules, at the moment.

}
Expand All @@ -50,10 +41,10 @@ export default class User {

async getProjects() {
const headers = new Headers({
Authorization: `Bearer ${this.#authentication}`
Authorization: `Bearer ${TPEN.getAuthorization()}`
})

return fetch(`${this.baseURL}/my/projects`, { headers })
return fetch(`${this.TPEN.servicesURL}/my/projects`, { headers })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good change, I like getting baseURL out of there.

.then((response) => {
if (!response.ok) {
return Promise.reject(response)
Expand Down Expand Up @@ -91,14 +82,25 @@ export default class User {
<li>
${project.title}
<div class="manage">
<span>Resume</span>
<span>Manage</span>
<span class="resume-btn">Resume</span>
<span class="manage-btn" data-project-id="${project._id}">Manage</span>
</div>
</li>
`

projectsList.insertAdjacentHTML("beforeend", projectTemplate)
})

const manageButtons = document.querySelectorAll(".manage-btn")
manageButtons.forEach((button) => {
button.addEventListener("click", (event) => {

const projectId = event.target.getAttribute("data-project-id")

window.location.href = `/manage/?projectID=${projectId}`
})
})

} else {
projectsList.innerHTML = "No projects yet. Create one to get started"
}
Expand All @@ -107,6 +109,7 @@ export default class User {
const errorTemplate = `
<li>
Error ${error.status ?? 500}: ${error.statusText ?? "Unknown Server error"}
Error ${error.status ?? 500}: ${error.statusText ?? "Unknown Server error"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doubled line

</li>
`
projectsList.insertAdjacentHTML("beforeend", errorTemplate)
Expand All @@ -116,11 +119,11 @@ export default class User {

async updateRecord(data) {
try {
const response = await fetch(`${this.baseURL}/my/profile/update`, {
const response = await fetch(`${this.TPEN.servicesURL}/my/profile/update`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.#authentication}`
Authorization: `Bearer ${TPEN.getAuthorization()}`
},
body: JSON.stringify(data)
})
Expand Down Expand Up @@ -152,4 +155,8 @@ export default class User {
const response = await this.updateRecord(data)
return response
}

static fromToken(token) {
return new User(getUserFromToken(token))
}
}
124 changes: 105 additions & 19 deletions _site/api/project.mjs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This directory shouldn't be versioned

cubap marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,38 +1,124 @@
import checkUserAuthentication from "../utilities/checkUserAuthentication.mjs"


/**
* Project API Link for user interfaces to import.
*
* @author cubap
* @type module
* @version 0.0.1
*/
const baseURL = "https://dev.api.t-pen.org"

export default class Project {
constructor(projectID) {

if (typeof projectID === "object") {
// Assume configuration object
this.data = projectID
return
}

if (typeof projectID === "string") {
this.id = projectID
return
}

throw new Error("Project ID or configuration is required")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preserving some sort of error may be great so we don't wind up with an empty Project.

this.id = projectID
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe _id is better for consistency.

}

// new private function getById()

/**
* Load project information from TPEN Services about the project
* for rendering user interface.
* @param {String} projectID short hash id for TPEN project
*/

async loadData() {
return await fetch(`/project/${this.id}`)
.then(response => response.json())
.then(project => this.data = project)
.catch(err => new Promise.reject(err))
const TPEN_USER = await checkUserAuthentication()
let token = TPEN_USER.authorization
return await fetch(`${baseURL}/project/${this.id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then(response => {
if (!response.ok) {
return Promise.reject(response)
}
const data = response.json()
return this.data = data
})
.catch(err => { throw err })
}

async addMember(email) {
let url = `${baseURL}/project/${this.id}/invite-member`

try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
method: "POST",
body: JSON.stringify({ email }),
})

if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || `Failed to invite collaborator: ${response.statusText}`)
}

return await response.json()
} catch (error) {
throw error
}
}

async removeMember(userId) {

const url = `${baseURL}/project/${this.id}/remove-member`

try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId })
})

if (!response.ok) {
throw new Error(`Error removing member: ${response.status}`)
}

const data = await response.json()
return data


} catch (error) {
console.error('Error removing member:', error)
throw error
}








const removeButtons = document.querySelectorAll('.remove-button')

removeButtons.forEach((button) => {


button.addEventListener('click', async () => {
const memberID = button.getAttribute("data-member-id")
const memberName = button.getAttribute("data-member-name")

const confirmed = confirm(`This action will remove ${memberName} from your project. Click 'OK' to continue?`)
if (!confirmed) {
return
}


})
})
}

}

// let projectID = "6602dd2314cd575343f513ba"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove these comments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is already updated in Project/index.js. we may have to remove this completely

// let projectObj = new Project(projectID)
// const projectData = await projectObj.loadData()
// console.log(projectData)
Loading