Skip to content

Commit

Permalink
add Project request modal with user messaging tab to the institutiona…
Browse files Browse the repository at this point in the history
…l dashboard
  • Loading branch information
John Tordoff committed Dec 17, 2024
1 parent 83e11db commit 25dab4c
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 110 deletions.
14 changes: 14 additions & 0 deletions app/adapters/node-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// app/adapters/user-message.js
import { inject as service } from '@ember/service';
import config from 'ember-osf-web/config/environment';
const { OSF: { apiUrl } } = config;
import OsfAdapter from './osf-adapter';

export default class NodeRequestAdapter extends OsfAdapter {
@service session;

urlForCreateRecord(modelName, snapshot) {
const nodeId = snapshot.record.target;
return `${apiUrl}/v2/nodes/${nodeId}/request/`;
}
}
13 changes: 13 additions & 0 deletions app/adapters/user-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// app/adapters/user-message.js
import { inject as service } from '@ember/service';
import config from 'ember-osf-web/config/environment';
const { OSF: { apiUrl } } = config;
import OsfAdapter from './osf-adapter';

export default class UserMessageAdapter extends OsfAdapter {
@service session;
urlForCreateRecord(modelName, snapshot) {
const userId = snapshot.record.user;
return `${apiUrl}/v2/users/${userId}/messages/`;
}
}
124 changes: 89 additions & 35 deletions app/institutions/dashboard/-components/object-list/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import InstitutionModel from 'ember-osf-web/models/institution';
import { SuggestedFilterOperators } from 'ember-osf-web/models/related-property-path';
import SearchResultModel from 'ember-osf-web/models/search-result';
import { Filter } from 'osf-components/components/search-page/component';
import { waitFor } from '@ember/test-waiters';
import { task } from 'ember-concurrency';
import Toast from 'ember-toastr/services/toast';
import { inject as service } from '@ember/service';
import Intl from 'ember-intl/services/intl';
import Store from '@ember-data/store';
import CurrentUser from 'ember-osf-web/services/current-user';


interface Column {
name: string;
Expand Down Expand Up @@ -46,8 +54,16 @@ export default class InstitutionalObjectList extends Component<InstitutionalObje
@tracked projectRequestModalShown = false;
@tracked activeTab = 'request-access'; // Default tab
@tracked messageText = '';
@tracked cc = false;
@tracked bcc_sender = false;
@tracked replyTo = false;
@tracked selectedUserId = '';
@tracked selectedNodeId = '';
@tracked showSendMessagePrompt = false;
@service toast!: Toast;
@service intl!: Intl;
@service store!: Store;
@service currentUser!: CurrentUser;


get queryOptions() {
const options = {
Expand Down Expand Up @@ -98,49 +114,31 @@ export default class InstitutionalObjectList extends Component<InstitutionalObje
}
}


@action
openProjectRequestModal() {
openProjectRequestModal(contributor: any) {
this.selectedUserId = contributor.user_id;
this.selectedNodeId = contributor.node_id;
this.projectRequestModalShown = true;
}

@action
closeProjectRequestModal() {
this.projectRequestModalShown = false;
}

@action
toggleProjectRequestModal() {
this.projectRequestModalShown = !this.projectRequestModalShown;
if (!this.projectRequestModalShown) {
this.messageText = ''; // Reset message text on close
}
}

@action
toggleCc() {
this.cc = !this.cc;
}

@action
toggleReplyTo() {
this.replyTo = !this.replyTo;
}
handleBackToSendMessage() {
this.activeTab = 'send-message';
this.showSendMessagePrompt = false;
setTimeout(() => {
this.projectRequestModalShown = true; // Reopen the main modal
}, 200);

@action
updateMessageText(event: Event) {
this.messageText = (event.target as HTMLTextAreaElement).value;
}

@action
toggleMessageModal() {
this.messageModalShown = !this.messageModalShown;
closeSendMessagePrompt() {
this.showSendMessagePrompt = false; // Hide confirmation modal without reopening
}

@action
sendMessage() {
console.log('Message sent:', this.messageText);
this.toggleMessageModal(); // Close modal after sending
toggleProjectRequestModal() {
this.projectRequestModalShown = !this.projectRequestModalShown;
}


Expand Down Expand Up @@ -176,17 +174,73 @@ export default class InstitutionalObjectList extends Component<InstitutionalObje
}

@action
setActiveTab(tabName) {
setActiveTab(tabName: string) {
this.activeTab = tabName;
}


@action
resetFields() {
this.messageText = '';
this.selectedPermission = 'read';
this.cc = false;
this.bcc_sender = false;
this.replyTo = false;
}

@task
@waitFor
async handleSend() {
try {
if (this.activeTab === 'send-message') {
await this._sendUserMessage();
} else if (this.activeTab === 'request-access') {
await this._sendNodeRequest();
}

this.toast.success(
this.intl.t('institutions.dashboard.object-list.request-project-message-modal.message_sent_success'),
);
this.resetFields();
} catch (error) {
const errorDetail = error?.errors?.[0]?.detail || '';

// Check for the specific error where access requests are disabled
if (error.status === 400 && errorDetail.includes('does not have Access Requests enabled')) {
setTimeout(() => {
this.showSendMessagePrompt = true; // timeout to allow the other to exit
}, 200);
} else {
this.toast.error(
this.intl.t('institutions.dashboard.object-list.request-project-message-modal.message_sent_failed'),
);
}
} finally {
this.projectRequestModalShown = false; // Close the main modal
}
}

async _sendUserMessage() {
const userMessage = this.store.createRecord('user-message', {
messageText: this.messageText.trim(),
messageType: 'institutional_request',
bcc_sender: this.bcc_sender,
replyTo: this.replyTo,
institution: this.args.institution,
user: this.selectedUserId,
});
await userMessage.save();
}

async _sendNodeRequest() {
const nodeRequest = this.store.createRecord('node-request', {
comment: this.messageText.trim(),
requestType: 'institutional_access',
requestedPermission: this.selectedPermission,
bcc_sender: this.bcc_sender,
replyTo: this.replyTo,
institution: this.args.institution,
message_recipent: this.selectedUserId,
target: this.selectedNodeId,
});
await nodeRequest.save();
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import Intl from 'ember-intl/services/intl';
import { action } from '@ember/object';
import { getOsfmapObjects, getSingleOsfmapValue, hasOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld';

import InstitutionModel from 'ember-osf-web/models/institution';
import SearchResultModel from 'ember-osf-web/models/search-result';
import { AttributionRoleIris } from 'ember-osf-web/models/index-card';
import { getOsfmapObjects, getSingleOsfmapValue, hasOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld';

interface ContributorsFieldArgs {
searchResult: SearchResultModel;
institution: InstitutionModel;
projectRequestModal: (contributor: any) => void;
}

const roleIriToTranslationKey: Record<AttributionRoleIris, string> = {
Expand All @@ -18,20 +20,20 @@ const roleIriToTranslationKey: Record<AttributionRoleIris, string> = {
[AttributionRoleIris.Read]: 'general.permissions.read',
};


export default class InstitutionalObjectListContributorsField extends Component<ContributorsFieldArgs> {
@service intl!: Intl;

// Return two contributors affiliated with the institution given with highest permission levels
get topInstitutionAffiliatedContributors() {
const { searchResult, institution } = this.args;
const {resourceMetadata} = searchResult;
const { resourceMetadata } = searchResult;

const attributions: any[] = getOsfmapObjects(resourceMetadata, ['qualifiedAttribution']);
const contributors = getOsfmapObjects(resourceMetadata, ['creator']);
const institutionIris = institution.iris;

const affiliatedAttributions = attributions
.filter((attribution: any) => hasInstitutionAffiliation(contributors, attribution, institutionIris));
const affiliatedAttributions = attributions.filter((attribution: any) =>
hasInstitutionAffiliation(contributors, attribution, institutionIris));

const adminAttributions = affiliatedAttributions.filter(
attribution => hasOsfmapValue(attribution, ['hadRole'], AttributionRoleIris.Admin),
);
Expand All @@ -47,32 +49,40 @@ export default class InstitutionalObjectListContributorsField extends Component<
return prioritizedAttributions.slice(0, 2).map(attribution => {
const contributor = getContributorById(contributors, getSingleOsfmapValue(attribution, ['agent']));
const roleIri: AttributionRoleIris = getSingleOsfmapValue(attribution, ['hadRole']);

const regex = /([^?#]+)$/; // Regex to extract the final part after the last slash
const contributorId = contributor?.['@id']?.match(regex)?.[1] || '';

return {
name: getSingleOsfmapValue(contributor,['name']),
name: getSingleOsfmapValue(contributor, ['name']) || 'Unknown Contributor',
user_id: contributorId,
node_id: searchResult.indexCard.get('osfGuid'),
url: getSingleOsfmapValue(contributor, ['identifier']),
permissionLevel: this.intl.t(roleIriToTranslationKey[roleIri]),
};
});
}

@action
handleOpenModal(contributor: any) {
this.args.projectRequestModal(contributor);
}
}

function hasInstitutionAffiliation(contributors: any[], attribution: any, institutionIris: string[]) {
const attributedContributor = getContributorById(contributors, getSingleOsfmapValue(attribution, ['agent']));

if (!attributedContributor.affiliation) {
if (!attributedContributor?.affiliation) {
return false;
}

return attributedContributor.affiliation.some(
(affiliation: any) => {
if (affiliation.identifier) {
return affiliation.identifier.some(
(affiliationIdentifier: any) => institutionIris.includes(affiliationIdentifier['@value']),
);
}
return institutionIris.includes(affiliation['@id']);
},
);
return attributedContributor.affiliation.some((affiliation: any) => {
if (affiliation.identifier) {
return affiliation.identifier.some((affiliationIdentifier: any) =>
institutionIris.includes(affiliationIdentifier['@value']));
}
return institutionIris.includes(affiliation['@id']);
});
}

function getContributorById(contributors: any[], contributorId: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

.icon-message {
opacity: 0;
color: $color-text-blue-dark;
background-color: inherit !important;
border: 0 !important;
box-shadow: 0 !important;
}

.icon-message:hover {
opacity: 1;
background-color: inherit !important;
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
</OsfLink>
{{t 'institutions.dashboard.object-list.table-items.permission-level' permissionLevel=contributor.permissionLevel}}
</div>
<Button
local-class='icon-message'
aria-label={{t 'institutions.dashboard.request_project_message_modal.open_aira_label'}}
{{on 'click' (fn this.handleOpenModal contributor)}}
>
<FaIcon @icon='comment' />
</Button>
{{else}}
<div>
{{t 'institutions.dashboard.object-list.table-items.no-contributors'}}
Expand Down
15 changes: 1 addition & 14 deletions app/institutions/dashboard/-components/object-list/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,6 @@
padding-left: 10px;
}

.icon-message {
opacity: 0;
color: $color-text-blue-dark;
background-color: inherit !important;
border: 0 !important;
box-shadow: 0 !important;
}

.icon-message:hover {
opacity: 1;
background-color: inherit !important;
}

.message-textarea {
min-width: 450px;
min-height: 280px;
Expand Down Expand Up @@ -188,7 +175,7 @@
text-align: center;
cursor: pointer;
background: #f9f9f9;
border: none;
border: 0;
border-bottom: 2px solid transparent;

&.active {
Expand Down
Loading

0 comments on commit 25dab4c

Please sign in to comment.