From 5a7fe948956e69d2784c1b0b488452c4e2a083a5 Mon Sep 17 00:00:00 2001 From: John Tordoff <> Date: Wed, 18 Dec 2024 09:24:22 -0500 Subject: [PATCH 01/15] add Project request modal with user messaging tab to the institutional dashboard --- app/adapters/node-request.ts | 14 ++ app/adapters/user-message.ts | 13 ++ .../-components/object-list/component.ts | 125 +++++++++++++++++ .../contributors-field/component.ts | 15 +- .../contributors-field/styles.scss | 14 ++ .../contributors-field/template.hbs | 7 + .../-components/object-list/styles.scss | 68 +++++++++ .../-components/object-list/template.hbs | 130 ++++++++++++++++++ app/models/node-request.ts | 13 ++ app/models/user-message.ts | 15 ++ translations/en-us.yml | 23 ++++ 11 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 app/adapters/node-request.ts create mode 100644 app/adapters/user-message.ts create mode 100644 app/institutions/dashboard/-components/object-list/contributors-field/styles.scss create mode 100644 app/models/node-request.ts create mode 100644 app/models/user-message.ts diff --git a/app/adapters/node-request.ts b/app/adapters/node-request.ts new file mode 100644 index 00000000000..32f1d140bc2 --- /dev/null +++ b/app/adapters/node-request.ts @@ -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/`; + } +} diff --git a/app/adapters/user-message.ts b/app/adapters/user-message.ts new file mode 100644 index 00000000000..a92b865c714 --- /dev/null +++ b/app/adapters/user-message.ts @@ -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/`; + } +} diff --git a/app/institutions/dashboard/-components/object-list/component.ts b/app/institutions/dashboard/-components/object-list/component.ts index 53dd0f146d3..c5f5cfb3a6e 100644 --- a/app/institutions/dashboard/-components/object-list/component.ts +++ b/app/institutions/dashboard/-components/object-list/component.ts @@ -9,6 +9,13 @@ 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 Intl from 'ember-intl/services/intl'; +import Store from '@ember-data/store'; +import CurrentUser from 'ember-osf-web/services/current-user'; + import config from 'ember-osf-web/config/environment'; const shareDownloadFlag = config.featureFlagNames.shareDownload; @@ -49,6 +56,20 @@ export default class InstitutionalObjectList extends Component column.name); @tracked dirtyVisibleColumns = [...this.visibleColumns]; // track changes to visible columns before they are saved + @tracked selectedPermission = 'write'; + @tracked projectRequestModalShown = false; + @tracked activeTab = 'request-access'; // Default tab + @tracked messageText = ''; + @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 = { @@ -126,6 +147,34 @@ export default class InstitutionalObjectList extends Component { + 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 toggleFilter(property: Filter) { this.page = ''; @@ -151,4 +200,80 @@ export default class InstitutionalObjectList extends Component { + 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(); + } } diff --git a/app/institutions/dashboard/-components/object-list/contributors-field/component.ts b/app/institutions/dashboard/-components/object-list/contributors-field/component.ts index 1c0a69f484b..3bb153f3339 100644 --- a/app/institutions/dashboard/-components/object-list/contributors-field/component.ts +++ b/app/institutions/dashboard/-components/object-list/contributors-field/component.ts @@ -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'; @@ -10,6 +11,7 @@ import { getOsfmapObjects, getSingleOsfmapValue, hasOsfmapValue } from 'ember-os interface ContributorsFieldArgs { searchResult: SearchResultModel; institution: InstitutionModel; + projectRequestModal: (contributor: any) => void; } const roleIriToTranslationKey: Record = { @@ -47,13 +49,24 @@ 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[]) { diff --git a/app/institutions/dashboard/-components/object-list/contributors-field/styles.scss b/app/institutions/dashboard/-components/object-list/contributors-field/styles.scss new file mode 100644 index 00000000000..da8d8a88a6a --- /dev/null +++ b/app/institutions/dashboard/-components/object-list/contributors-field/styles.scss @@ -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; +} + diff --git a/app/institutions/dashboard/-components/object-list/contributors-field/template.hbs b/app/institutions/dashboard/-components/object-list/contributors-field/template.hbs index 992e6be26a3..e2968eed90a 100644 --- a/app/institutions/dashboard/-components/object-list/contributors-field/template.hbs +++ b/app/institutions/dashboard/-components/object-list/contributors-field/template.hbs @@ -7,6 +7,13 @@ {{t 'institutions.dashboard.object-list.table-items.permission-level' permissionLevel=contributor.permissionLevel}} + {{else}}
{{t 'institutions.dashboard.object-list.table-items.no-contributors'}} diff --git a/app/institutions/dashboard/-components/object-list/styles.scss b/app/institutions/dashboard/-components/object-list/styles.scss index 0677767f4fe..7467ee83e9d 100644 --- a/app/institutions/dashboard/-components/object-list/styles.scss +++ b/app/institutions/dashboard/-components/object-list/styles.scss @@ -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 #ddd; + margin-bottom: 1rem; +} + +.tab-button { + flex: 1; + padding: 10px; + text-align: center; + cursor: pointer; + background: #f9f9f9; + border: 0; + border-bottom: 2px solid transparent; + + &.active { + border-bottom: 2px solid #007bff; + font-weight: bold; + } + + &:hover { + background: #f1f1f1; + } +} + +.tab-pane { + padding: 1rem; + background: #fff; +} + +.request-modal { + max-width: min-content; +} + .download-dropdown-content { display: flex; flex-direction: column; diff --git a/app/institutions/dashboard/-components/object-list/template.hbs b/app/institutions/dashboard/-components/object-list/template.hbs index c4e2ba2c6bb..7ad27732918 100644 --- a/app/institutions/dashboard/-components/object-list/template.hbs +++ b/app/institutions/dashboard/-components/object-list/template.hbs @@ -184,6 +184,7 @@ as |list|> {{else}} {{call (fn column.getValue result)}} @@ -277,3 +278,132 @@ as |list|> {{/if}} + + + {{t 'institutions.dashboard.object-list.request-project-message-modal.title'}} + + +
+ + +
+ + {{#if (eq this.activeTab 'request-access')}} +
+

+ {{t 'institutions.dashboard.object-list.request-project-message-modal.request_label'}} +

+
+ + +