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

[ENG-6669] Institutional Access project Request Modal #2433

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
51fd954
Merge branch 'hotfix/24.09.1'
mfraezz Nov 21, 2024
3a29a83
Merge branch 'release/24.10.0'
mfraezz Dec 11, 2024
5a7fe94
add Project request modal with user messaging tab to the institution…
Dec 18, 2024
13f0699
Merge branch 'feature/institutional-access' of https://github.com/Cen…
Dec 18, 2024
0039fc6
Feature/share download integration (#2457)
futa-ikeda Jan 6, 2025
660cee2
Bump version no. Add CHANGELOG.
adlius Jan 6, 2025
2fe05c7
Merge branch 'release/25.01.0'
adlius Jan 6, 2025
5300c97
Merge tag '25.01.0' into develop
adlius Jan 6, 2025
002ccc8
Feature/share download integration (#2457)
futa-ikeda Jan 6, 2025
bf8ab2c
Bump version no. Add CHANGELOG.
adlius Jan 6, 2025
1abd4e4
Merge branch 'develop' of https://github.com/CenterForOpenScience/emb…
Jan 6, 2025
d54a71d
Fix funder column propertyPathKey
futa-ikeda Jan 6, 2025
dae4684
Update CHANGELOG, bump version
mfraezz Jan 6, 2025
6fb0bee
Merge branch 'hotfix/25.01.1' into develop
mfraezz Jan 6, 2025
9b21ce4
Merge branch 'develop' of https://github.com/CenterForOpenScience/emb…
Jan 6, 2025
524eef0
integrations fixes and refactors
Jan 7, 2025
4057df3
add project request and messaging modal to institution dashboard
Jan 7, 2025
19fb416
Merge branch 'institutional-access-project-request-modal' of https://…
Jan 9, 2025
4b74341
Code review cleanup and typos
Jan 9, 2025
983ac69
clean-up user message pane and make tasks
Jan 9, 2025
e7f569d
use variables for colors
Jan 9, 2025
715bee8
clean-up exception handling and include notes
Jan 9, 2025
a92edbf
add translation to default value for unknown contributor
Jan 9, 2025
9a3e056
clean-up translation keys
Jan 10, 2025
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
13 changes: 13 additions & 0 deletions app/adapters/node-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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}/requests/`;
}
}
3 changes: 1 addition & 2 deletions app/adapters/user-message.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// app/adapters/user-message.js
import { inject as service } from '@ember/service';
import config from 'ember-osf-web/config/environment';
const { OSF: { apiUrl } } = config;
Expand All @@ -7,7 +6,7 @@ import OsfAdapter from './osf-adapter';
export default class UserMessageAdapter extends OsfAdapter {
@service session;
urlForCreateRecord(modelName, snapshot) {
const userId = snapshot.record.user;
const userId = snapshot.record.messageRecipient;
return `${apiUrl}/v2/users/${userId}/messages/`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class InstitutionalUsersList extends Component<InstitutionalUsers
@tracked filteredUsers = [];
@tracked messageModalShown = false;
@tracked messageText = '';
@tracked bcc_sender = false;
@tracked bccSender = false;
@tracked replyTo = false;
@tracked selectedUserId = null;
@service toast!: Toast;
Expand Down Expand Up @@ -291,7 +291,7 @@ export default class InstitutionalUsersList extends Component<InstitutionalUsers
@action
resetModalFields() {
this.messageText = '';
this.bcc_sender = false;
this.bccSender = false;
this.replyTo = false;
this.selectedUserId = null;
}
Expand All @@ -308,7 +308,7 @@ export default class InstitutionalUsersList extends Component<InstitutionalUsers
const userMessage = this.store.createRecord('user-message', {
messageText: this.messageText.trim(),
messageType: MessageTypeChoices.InstitutionalRequest,
bcc_sender: this.bcc_sender,
bccSender: this.bccSender,
replyTo: this.replyTo,
institution: this.args.institution,
user: this.selectedUserId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@
</div>
<div local-class='checkbox-container'>
<label local-class='checkbox-item'>
<Input @type='checkbox' @checked={{this.bcc_sender}} />
<Input @type='checkbox' @checked={{this.bccSender}} />
{{t 'institutions.dashboard.send_message_modal.cc_label'}}
</label>
<label local-class='checkbox-item'>
Expand Down
145 changes: 145 additions & 0 deletions app/institutions/dashboard/-components/object-list/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ 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 { taskFor } from 'ember-concurrency-ts';
import Toast from 'ember-toastr/services/toast';
import Intl from 'ember-intl/services/intl';
import Store from '@ember-data/store';
import CurrentUser from 'ember-osf-web/services/current-user';
import {MessageTypeChoices} from 'ember-osf-web/models/user-message';
import {RequestTypeChoices} from 'ember-osf-web/models/node-request';

import config from 'ember-osf-web/config/environment';

const shareDownloadFlag = config.featureFlagNames.shareDownload;
Expand Down Expand Up @@ -49,6 +59,20 @@ export default class InstitutionalObjectList extends Component<InstitutionalObje
@tracked sortParam?: string;
@tracked visibleColumns = this.args.columns.map(column => column.name);
@tracked dirtyVisibleColumns = [...this.visibleColumns]; // track changes to visible columns before they are saved
@tracked selectedPermissions = 'write';
@tracked projectRequestModalShown = false;
@tracked activeTab = 'request-access'; // Default tab
@tracked messageText = '';
@tracked bccSender = 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 @@ -151,4 +175,125 @@ export default class InstitutionalObjectList extends Component<InstitutionalObje
updatePage(newPage: string) {
this.page = newPage;
}

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

@action
handleBackToSendMessage() {
this.activeTab = 'send-message';
this.showSendMessagePrompt = false;
setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why we have a setTimeout here? I'd like to avoid any unneeded timeouts if possible

Copy link
Collaborator Author

@Johnetordoff Johnetordoff Jan 9, 2025

Choose a reason for hiding this comment

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

Yes, I'll leave a comment here, I clarified this but only later on line 243, and not very well. The reason for the timeout is that ember bans having two dialogue modals open at the same time and the modal closing animation happens too slowly, so just

modal1.close()
modal2.open()

opens modal2 before the modal1 close animation is complete. Causing the framework to produce an error because there are technically two open modals. Product wanted a modal on this, because the toast message is not enough.

this.projectRequestModalShown = true; // Reopen the main modal
}, 200);

}

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

@action
toggleProjectRequestModal() {
this.projectRequestModalShown = !this.projectRequestModalShown;
}

@action
updateselectedPermissions(permission: string) {
this.selectedPermissions = permission;
}

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


@action
resetFields() {
this.selectedPermissions = 'write';
this.bccSender = false;
this.replyTo = false;
}

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

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.user || error?.errors?.[0]?.detail || '';
const errorCode = parseInt(error?.errors?.[0]?.status, 10);

if (errorCode === 400 && errorDetail.includes('does not have Access Requests enabled')) {
// Product wanted special handling for this error that involve a second pop-up modal
// Timeout to allow the modal to exit, can't have two OSFDialogs open at same time
setTimeout(() => {
this.showSendMessagePrompt = true; // Timeout to allow the modal to exit
}, 200);
Comment on lines +245 to +247
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to a comment above, I would avoid using setTimeout to set page states if possible. In this case, I think you can just add this bit of logic in to the finally block like:

} finally {
  this.projectRequestModalShown = false; // Close the main modal
  if (relevantCondition) {
     this.showSendMessagePrompt = true;
  }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As mentioned in the comment above this actual will temporarily cause where both modals are open simultaneously.

} else if ([400, 403].includes(errorCode)) {
// Handle more specific errors 403s could result due if a project quickly switches it's institution
this.toast.error(errorDetail);
} else if (errorDetail.includes('Request was throttled')) { // 429 response not in JSON payload.
this.toast.error(errorDetail);
} 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
}
}

@task
@waitFor
async _sendUserMessage() {
Johnetordoff marked this conversation as resolved.
Show resolved Hide resolved
const userMessage = this.store.createRecord('user-message', {
messageText: this.messageText.trim(),
messageType: MessageTypeChoices.InstitutionalRequest,
bccSender: this.bccSender,
replyTo: this.replyTo,
institution: this.args.institution,
user: this.selectedUserOsfGuid,
});
await userMessage.save();
}

@task
@waitFor
async _sendNodeRequest() {
Johnetordoff marked this conversation as resolved.
Show resolved Hide resolved
const nodeRequest = this.store.createRecord('node-request', {
comment: this.messageText.trim(),
requestType: RequestTypeChoices.InstitutionalRequest,
requestedPermissions: this.selectedPermissions,
bccSender: this.bccSender,
replyTo: this.replyTo,
institution: this.args.institution,
messageRecipient: this.selectedUserOsfGuid,
target: this.selectedNodeId,
});
await nodeRequest.save();
}

get selectedUserOsfGuid() {
const url = new URL(this.selectedUserId);
const pathSegments = url.pathname.split('/').filter(Boolean);
return pathSegments[pathSegments.length - 1] || ''; // Last non-empty segment
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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 InstitutionModel from 'ember-osf-web/models/institution';
import SearchResultModel from 'ember-osf-web/models/search-result';
Expand All @@ -10,6 +11,7 @@ import { getOsfmapObjects, getSingleOsfmapValue, hasOsfmapValue } from 'ember-os
interface ContributorsFieldArgs {
searchResult: SearchResultModel;
institution: InstitutionModel;
projectRequestModal: (contributor: any) => void;
}

const roleIriToTranslationKey: Record<AttributionRoleIris, string> = {
Expand Down Expand Up @@ -48,12 +50,19 @@ export default class InstitutionalObjectListContributorsField extends Component<
const contributor = getContributorById(contributors, getSingleOsfmapValue(attribution, ['agent']));
const roleIri: AttributionRoleIris = getSingleOsfmapValue(attribution, ['hadRole']);
return {
name: getSingleOsfmapValue(contributor,['name']),
name: getSingleOsfmapValue(contributor, ['name']) || this.intl.t('general.unknownContributor'),
userId: contributor['@id'],
nodeId: 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[]) {
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_aria_label'}}
{{on 'click' (fn this.handleOpenModal contributor)}}
>
<FaIcon @icon='comment' />
</Button>
{{else}}
<div>
{{t 'institutions.dashboard.object-list.table-items.no-contributors'}}
Expand Down
68 changes: 68 additions & 0 deletions app/institutions/dashboard/-components/object-list/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,74 @@
padding-left: 10px;
}

.message-textarea {
min-width: 450px;
min-height: 280px;
}

.message-label {
display: block;
}

.checkbox-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
}

.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}

.radiobox-container {
gap: 20px;
}

.radiobox-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}

.tab-container {
display: flex;
border-bottom: 1px solid $color-border-gray;
margin-bottom: 1rem;
}

.tab-button {
flex: 1;
padding: 10px;
text-align: center;
cursor: pointer;
background: $color-bg-gray;
border: 0;
border-bottom: 2px solid transparent;

&.active {
border-bottom: 2px solid $color-border-gray;
font-weight: bold;
}

&:hover {
background: $color-bg-gray;
}
}

.tab-pane {
padding: 1rem;
background: $color-border-white;
}

.request-modal {
max-width: min-content;
}

.download-dropdown-content {
display: flex;
flex-direction: column;
Expand Down
Loading
Loading