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

Feature/institutional access #2471

Merged
merged 52 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
8375eb7
Add user messaging modal to user's tab on institutional dashboard
Dec 3, 2024
975af66
fix en-us typo
Dec 6, 2024
d09ca7d
add aira-label.
Dec 6, 2024
0627e1b
fix label for message box
Dec 9, 2024
7639ea1
update sendMessage to task
Dec 9, 2024
9eb5df3
update text area for Ember type area
Dec 9, 2024
abe2852
Add type annotations for belongs to
Dec 9, 2024
0d8cd74
add checkboxes for reply-to and ccing
Dec 9, 2024
fa4ae5e
update to fit Product language and styling
Dec 10, 2024
28aadaf
update language and fix modal closing bug.
Dec 12, 2024
777dd66
Merge branch 'develop' of https://github.com/CenterForOpenScience/emb…
Dec 12, 2024
d5803a6
make message types an enum
Dec 16, 2024
434288e
use proper Ember input tags, remove extra close modal command
Dec 16, 2024
5a7fe94
add Project request modal with user messaging tab to the institution…
Dec 18, 2024
5bbb5a3
Merge pull request #2421 from Johnetordoff/institutional-access-user-…
Johnetordoff Dec 18, 2024
13f0699
Merge branch 'feature/institutional-access' of https://github.com/Cen…
Dec 18, 2024
1abd4e4
Merge branch 'develop' of https://github.com/CenterForOpenScience/emb…
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
497b301
Merge pull request #2433 from Johnetordoff/institutional-access-proje…
Johnetordoff Jan 10, 2025
6d86462
fix user messaging bug due to out of sync adapter
Jan 13, 2025
6c972a2
Merge pull request #2467 from Johnetordoff/fix-user-messaging-tab
Johnetordoff Jan 13, 2025
239f9e1
use FE feature flipping
Jan 13, 2025
c001e63
Merge pull request #2469 from Johnetordoff/feature-flipper
Johnetordoff Jan 13, 2025
c0e7469
fix feature flip bug (#2470)
Johnetordoff Jan 14, 2025
c3afad6
Merge https://github.com/centerforopenscience/ember-osf-web into feat…
Johnetordoff Jan 16, 2025
b0e1cbe
Merge branch 'feature/institutional-access' of https://github.com/Cen…
Johnetordoff Jan 16, 2025
9108857
Merge pull request #2473 from Johnetordoff/feature/institutional-access
Johnetordoff Jan 16, 2025
237a0ec
surface 409s as error messages
Johnetordoff Jan 16, 2025
b735ed3
fix typo stopping emails
Johnetordoff Jan 16, 2025
a759326
make buttons display properly
Johnetordoff Jan 16, 2025
e0f63f8
fix messageRecipient problem by using real user data not guid
Johnetordoff Jan 16, 2025
5073c59
fix syntax on brackets
Johnetordoff Jan 16, 2025
bff0b1e
revert/restore old code
Johnetordoff Jan 16, 2025
c45d417
Merge pull request #2476 from CenterForOpenScience/IA-fixes
Johnetordoff Jan 16, 2025
5551223
fix typo and issue preventing pop-up modal from popping up
Johnetordoff Jan 17, 2025
9c52b2a
Merge pull request #2478 from Johnetordoff/IA-fixes
Johnetordoff Jan 17, 2025
75f03ba
fix issue with not including error response codes in payload
Johnetordoff Jan 17, 2025
07aef1a
Merge pull request #2479 from Johnetordoff/IA-fixes
Johnetordoff Jan 17, 2025
d550aaf
update translation to remove html tag
Johnetordoff Jan 17, 2025
56c156b
Merge pull request #2480 from Johnetordoff/IA-fixes
Johnetordoff Jan 17, 2025
af22ef9
remove debugging return
Johnetordoff Jan 17, 2025
2da6676
remove superfluous import and type Institution model correctly
Johnetordoff Jan 17, 2025
c26aa4f
Merge pull request #2481 from Johnetordoff/IA-fixes
Johnetordoff Jan 17, 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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: doesn't look like you need this line and any imports for it

Copy link
Contributor

Choose a reason for hiding this comment

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

@Johnetordoff This looks like it's still there, but you marked it as resolved without additional comment. What's going on with this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I must have missed it sorry, let me remove it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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


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

export default class UserMessageAdapter extends OsfAdapter {
urlForCreateRecord(modelName, snapshot) {
const userId = snapshot.record.messageRecipient;
return `${apiUrl}/v2/users/${userId}/messages/`;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { task } from 'ember-concurrency';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
Expand All @@ -10,6 +11,7 @@ import InstitutionModel from 'ember-osf-web/models/institution';
import InstitutionDepartmentsModel from 'ember-osf-web/models/institution-department';
import Analytics from 'ember-osf-web/services/analytics';
import { RelationshipWithLinks } from 'osf-api';
import { MessageTypeChoices } from 'ember-osf-web/models/user-message';

interface Column {
key: string;
Expand All @@ -27,6 +29,8 @@ interface InstitutionalUsersListArgs {
export default class InstitutionalUsersList extends Component<InstitutionalUsersListArgs> {
@service analytics!: Analytics;
@service intl!: Intl;
@service store;
@service currentUser!: CurrentUser;

institution?: InstitutionModel;

Expand All @@ -37,6 +41,12 @@ export default class InstitutionalUsersList extends Component<InstitutionalUsers
@tracked sort = 'user_name';
@tracked selectedDepartments: string[] = [];
@tracked filteredUsers = [];
@tracked messageModalShown = false;
@tracked messageText = '';
@tracked bccSender = false;
@tracked replyTo = false;
@tracked selectedUserId = null;
@service toast!: Toast;

@tracked columns: Column[] = [
{
Expand Down Expand Up @@ -262,4 +272,54 @@ export default class InstitutionalUsersList extends Component<InstitutionalUsers
clickToggleOrcidFilter(hasOrcid: boolean) {
this.hasOrcid = !hasOrcid;
}

@action
openMessageModal(userId: string) {
this.selectedUserId = userId;
this.messageModalShown = true;
}

@action
toggleMessageModal(userId: string | null = null) {
this.messageModalShown = !this.messageModalShown;
this.selectedUserId = userId;
if (!this.messageModalShown) {
this.resetModalFields();
}
}

@action
resetModalFields() {
this.messageText = '';
this.bccSender = false;
this.replyTo = false;
this.selectedUserId = null;
}

@task
@waitFor
async sendMessage() {
if (!this.messageText.trim()) {
this.toast.error(this.intl.t('error.empty_message'));
return;
}

try {
const userMessage = this.store.createRecord('user-message', {
messageText: this.messageText.trim(),
messageType: MessageTypeChoices.InstitutionalRequest,
bccSender: this.bccSender,
replyTo: this.replyTo,
institution: this.args.institution,
messageRecipient: this.selectedUserId,
});

await userMessage.save();
this.toast.success(this.intl.t('institutions.dashboard.send_message_modal.message_sent_success'));
} catch (error) {
this.toast.error(this.intl.t('institutions.dashboard.send_message_modal.message_sent_failed'));
} finally {
this.messageModalShown = false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,41 @@ input:checked + .slider::before {
justify-content: flex-end;
margin-right: 15px;
}


.icon-message {
opacity: 0;
color: $color-text-blue-dark;
/* !important used to override ember Button border scripting */
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;
}

.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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@
<OsfLink @href={{concat '/' institutionalUser.userGuid '/'}}>
{{institutionalUser.userName}}
</OsfLink>
{{#if @institution.institutionalRequestAccessEnabled}}
<Button
local-class='icon-message'
aria-label={{t 'institutions.dashboard.send_message_modal.open_aira_label'}}
{{on 'click' (fn this.openMessageModal institutionalUser.userGuid)}}
>
<FaIcon @icon='comment' />
</Button>
{{/if}}
{{else if (eq column.type 'osf_link')}}
<OsfLink @href={{concat '/' institutionalUser.userGuid '/'}}>
{{institutionalUser.userGuid}}
Expand Down Expand Up @@ -204,4 +213,50 @@
{{t 'institutions.dashboard.users_list.empty'}}
</list.empty>
</PaginatedList::HasMany>
<OsfDialog @isOpen={{this.messageModalShown}} @onClose={{this.toggleMessageModal}} as |dialog|>
<dialog.heading>
{{t 'institutions.dashboard.send_message_modal.title'}}
</dialog.heading>
<dialog.main>
<div>
<label for='message-text' local-class='message-label'>
{{t 'institutions.dashboard.send_message_modal.opening_message_label'}}
</label>
<Textarea
id='message-text'
local-class='message-textarea'
@value={{this.messageText}}
/>
<div>
{{t 'institutions.dashboard.send_message_modal.closing_message_label' adminName=this.currentUser.user.fullName htmlSafe=true}}
</div>
<div local-class='checkbox-container'>
<label local-class='checkbox-item'>
<Input @type='checkbox' @checked={{this.bccSender}} />
{{t 'institutions.dashboard.send_message_modal.cc_label'}}
</label>
<label local-class='checkbox-item'>
<Input @type='checkbox' @checked={{this.replyTo}} />
{{t 'institutions.dashboard.send_message_modal.reply_to_label'}}
</label>
</div>
</div>
</dialog.main>
<dialog.footer>
<Button
@type='secondary'
{{on 'click' this.toggleMessageModal}}
>
{{t 'general.cancel'}}
</Button>
<Button
@type='primary'
@disabled={{not this.messageText.trim}}
{{on 'click' (queue (perform this.sendMessage))}}

>
{{t 'institutions.dashboard.send_message_modal.send'}}
</Button>
</dialog.footer>
</OsfDialog>
{{/if}}
147 changes: 147 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 @@ -50,6 +60,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 @@ -162,4 +186,127 @@ 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(() => {
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 (errorDetail.includes('does not have Access Requests enabled')) {
// This error does not include HTTP code in payload, but it returns response header of 403
// 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);
} else if ([400, 403, 409].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() {
const userMessage = this.store.createRecord('user-message', {
messageText: this.messageText.trim(),
messageType: MessageTypeChoices.InstitutionalRequest,
bccSender: this.bccSender,
replyTo: this.replyTo,
institution: this.args.institution,
messageRecipient: this.selectedUserOsfGuid,
});
await userMessage.save();
}

@task
@waitFor
async _sendNodeRequest() {
const userRecord = await this.store.findRecord('user', this.selectedUserOsfGuid);
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: userRecord,
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
}

}
Loading
Loading