From c0043fc075b5c61c133e91fb1bf28519fa63177d Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 20 Jul 2023 13:55:12 +0900 Subject: [PATCH] MOBILE-4374 user: Implement social profile fields --- .../addon-user-profile-field-social.html | 28 +++++++ .../social/component/social.ts | 28 +++++++ .../social/services/handlers/social.ts | 79 ++++++++++++++++++ .../userprofilefield/social/social.module.ts | 42 ++++++++++ .../userprofilefield.module.ts | 2 + .../user/tests/behat/basic_usage.feature | 28 ++++++- ...sage-of-user-features-view-profile_21.png} | Bin 19824 -> 28324 bytes 7 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 src/addons/userprofilefield/social/component/addon-user-profile-field-social.html create mode 100644 src/addons/userprofilefield/social/component/social.ts create mode 100644 src/addons/userprofilefield/social/services/handlers/social.ts create mode 100644 src/addons/userprofilefield/social/social.module.ts rename src/core/features/user/tests/behat/snapshots/{test-basic-usage-of-user-features-view-profile_7.png => test-basic-usage-of-user-features-view-profile_21.png} (60%) diff --git a/src/addons/userprofilefield/social/component/addon-user-profile-field-social.html b/src/addons/userprofilefield/social/component/addon-user-profile-field-social.html new file mode 100644 index 00000000000..f6a65df6659 --- /dev/null +++ b/src/addons/userprofilefield/social/component/addon-user-profile-field-social.html @@ -0,0 +1,28 @@ + + + +

+ + +

+

+ + +

+
+
+ + + + + + + + + + + + diff --git a/src/addons/userprofilefield/social/component/social.ts b/src/addons/userprofilefield/social/component/social.ts new file mode 100644 index 00000000000..91226ab8ec8 --- /dev/null +++ b/src/addons/userprofilefield/social/component/social.ts @@ -0,0 +1,28 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component } from '@angular/core'; + +import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component'; + +/** + * Directive to render a social user profile field. + */ +@Component({ + selector: 'addon-user-profile-field-social', + templateUrl: 'addon-user-profile-field-social.html', +}) +export class AddonUserProfileFieldSocialComponent extends CoreUserProfileFieldBaseComponent { + +} diff --git a/src/addons/userprofilefield/social/services/handlers/social.ts b/src/addons/userprofilefield/social/services/handlers/social.ts new file mode 100644 index 00000000000..ca7335ef58f --- /dev/null +++ b/src/addons/userprofilefield/social/services/handlers/social.ts @@ -0,0 +1,79 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Type } from '@angular/core'; + +import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate'; +import { AddonUserProfileFieldSocialComponent } from '../../component/social'; +import { CoreTextUtils } from '@services/utils/text'; +import { AuthEmailSignupProfileField } from '@features/login/services/login-helper'; +import { CoreUserProfileField } from '@features/user/services/user'; +import { makeSingleton } from '@singletons'; +import { CoreFormFields } from '@singletons/form'; + +/** + * Social user profile field handlers. + */ +@Injectable({ providedIn: 'root' }) +export class AddonUserProfileFieldSocialHandlerService implements CoreUserProfileFieldHandler { + + name = 'AddonUserProfileFieldSocial'; + type = 'social'; + + /** + * Whether or not the handler is enabled on a site level. + * + * @returns True or promise resolved with true if enabled. + */ + async isEnabled(): Promise { + return true; + } + + /** + * Get the data to send for the field based on the input data. + * + * @param field User field to get the data for. + * @param signup True if user is in signup page. + * @param registerAuth Register auth method. E.g. 'email'. + * @param formValues Form Values. + * @returns Data to send for the field. + */ + async getData( + field: AuthEmailSignupProfileField | CoreUserProfileField, + signup: boolean, + registerAuth: string, + formValues: CoreFormFields, + ): Promise { + const name = 'profile_field_' + field.shortname; + + return { + type: 'social', + name: name, + value: CoreTextUtils.cleanTags( formValues[name]), + }; + } + + /** + * Return the Component to use to display the user profile field. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @returns The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(): Type | Promise> { + return AddonUserProfileFieldSocialComponent; + } + +} + +export const AddonUserProfileFieldSocialHandler = makeSingleton(AddonUserProfileFieldSocialHandlerService); diff --git a/src/addons/userprofilefield/social/social.module.ts b/src/addons/userprofilefield/social/social.module.ts new file mode 100644 index 00000000000..f49a1169dd5 --- /dev/null +++ b/src/addons/userprofilefield/social/social.module.ts @@ -0,0 +1,42 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; + +import { AddonUserProfileFieldSocialHandler } from './services/handlers/social'; +import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate'; +import { AddonUserProfileFieldSocialComponent } from './component/social'; +import { CoreSharedModule } from '@/core/shared.module'; + +@NgModule({ + declarations: [ + AddonUserProfileFieldSocialComponent, + ], + imports: [ + CoreSharedModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreUserProfileFieldDelegate.registerHandler(AddonUserProfileFieldSocialHandler.instance); + }, + }, + ], + exports: [ + AddonUserProfileFieldSocialComponent, + ], +}) +export class AddonUserProfileFieldSocialModule {} diff --git a/src/addons/userprofilefield/userprofilefield.module.ts b/src/addons/userprofilefield/userprofilefield.module.ts index 90769bc4afa..7b178bbf841 100644 --- a/src/addons/userprofilefield/userprofilefield.module.ts +++ b/src/addons/userprofilefield/userprofilefield.module.ts @@ -17,6 +17,7 @@ import { NgModule } from '@angular/core'; import { AddonUserProfileFieldCheckboxModule } from './checkbox/checkbox.module'; import { AddonUserProfileFieldDatetimeModule } from './datetime/datetime.module'; import { AddonUserProfileFieldMenuModule } from './menu/menu.module'; +import { AddonUserProfileFieldSocialModule } from './social/social.module'; import { AddonUserProfileFieldTextareaModule } from './textarea/textarea.module'; import { AddonUserProfileFieldTextModule } from './text/text.module'; @@ -25,6 +26,7 @@ import { AddonUserProfileFieldTextModule } from './text/text.module'; AddonUserProfileFieldCheckboxModule, AddonUserProfileFieldDatetimeModule, AddonUserProfileFieldMenuModule, + AddonUserProfileFieldSocialModule, AddonUserProfileFieldTextareaModule, AddonUserProfileFieldTextModule, ], diff --git a/src/core/features/user/tests/behat/basic_usage.feature b/src/core/features/user/tests/behat/basic_usage.feature index 01b1f25d3fb..8fc5697a427 100755 --- a/src/core/features/user/tests/behat/basic_usage.feature +++ b/src/core/features/user/tests/behat/basic_usage.feature @@ -8,8 +8,9 @@ Feature: Test basic usage of user features Scenario: Complete missing fields Given the following "custom profile fields" exist: - | datatype | shortname | name | required | - | text | food | Favourite food | 1 | + | datatype | shortname | name | required | param1 | + | text | food | Favourite food | 1 | | + | social | website | url | 1 | url | When I enter the app And I log in as "student1" Then I should find "Complete your profile" in the app @@ -42,6 +43,7 @@ Feature: Test basic usage of user features And I set the field "password" to "student1" And I click on "Log in" "button" And I set the field "Favourite food" to "Pasta" + And I set the field "Web page" to "https://moodle.com" And I click on "Update profile" "button" Then I should see "Changes saved" @@ -53,11 +55,29 @@ Feature: Test basic usage of user features Then I should find "Acceptance test site" in the app Scenario: View profile - Given I entered the app as "student1" - When I press the user menu button in the app + Given the following "custom profile fields" exist: + | datatype | shortname | name | required | param1 | + | text | food | Favourite food | 1 | | + | social | website | url | 1 | url | + And I entered the app as "student1" + And I press "Complete profile" in the app + And I switch to the browser tab opened by the app + And I set the field "username" to "student1" + And I set the field "password" to "student1" + And I click on "Log in" "button" + And I set the field "Favourite food" to "Pasta" + And I set the field "Web page" to "https://moodle.com" + When I click on "Update profile" "button" + Then I should see "Changes saved" + + When I close the browser tab opened by the app + And I press "Reconnect" in the app + And I press the user menu button in the app And I press "Student" in the app Then I should find "student1@example.com" in the app And I should find "Student Student" in the app + And I should find "Pasta" in the app + And I should find "https://moodle.com" in the app And the UI should match the snapshot @lms_from4.2 diff --git a/src/core/features/user/tests/behat/snapshots/test-basic-usage-of-user-features-view-profile_7.png b/src/core/features/user/tests/behat/snapshots/test-basic-usage-of-user-features-view-profile_21.png similarity index 60% rename from src/core/features/user/tests/behat/snapshots/test-basic-usage-of-user-features-view-profile_7.png rename to src/core/features/user/tests/behat/snapshots/test-basic-usage-of-user-features-view-profile_21.png index 67a6beb33a560d3c3ec0325d7535c3eded8f435c..0b2e6eb1df355f8097407effc55f64f18b0f9a4e 100644 GIT binary patch delta 11241 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

4Nx)6I?5~|pjQkB)XH9$Bl>5qGj93dwGi~YV zTuSDx5fYeJw!#ke`CZntI8%gYRd#-X1&Q@gg%YaC7u;Oq5hwiO5}-qeMoSF5Y}8rI z1Tc^D)p}FA9vT`WYG1d8P40*MZue>^xGst0p~D~1pU2+h_q&3>r?>H%SO_VE(SR<%xSf!cf(-1`H4t z+P_ms2wi^Fnz2Mr&)YXb1V5X%R~c}*sTOvitKZ-#K7l^8=%tOYvc^>PTlD9&`I&|( z-sPlx3Io?u4)tjs-nSJie52I@{XOV*X=gjRN!gy~BMo^*_i||%C_GxX7`L1ovJ3LM zwOMgycbTEz@}-zi#ZxX9cJ*u`&B60Yph7x2#`X48i-=Eq%B@r^E42hFXvdxH{832) zq{R-1KUzZ&mRG0#nYlCr#Vm~Zro6zRe=mTVE=#p6(Qi?bnob&f(Y>Mx%TwW z=^?;>y-WYEJPQ0fE`9yS>aXnvApiY$&ig&AQ6v`V*LP{jK#}Tgy7E>bTcM7Oi14Zo zsXfzgsVxU$7nj)4;#d+I9bxC})8fFy;MnJ4R7Q=XCJ%XaOxxFIY<5r{Xf_U4RuLqH zyb>UlfkGQd==k5d*g2Cs6bCXs^%e1Tj=km*4NM4k!_$UtuSX_PJGoqLd9?AAdz-0~ zo%I;F0fKOg?xK07z`ka?1Xqix4147NArxHfCDL}1Q*{r^v$bK=5Wj|eJ5-yhscbhn zgXy9q99g>wY9_IuC!Wxx9n?CU7R~}nk4c0f0)}xUTjWK;uwaHNeh)~R&7;vJe-K0& zNoX{hGVPc$pRJDqs4UOSJND)0L($%w;Ihp521$atn2e85RO3{N<2c&gon&J&Pdo?r z5X^Ef+F-kv(9E}Bl1Him)2NUkGedEuJty#lDf zI;UiNn;z#X>*smggr46E446!n*V~H2%dsLasZ?r!dt?M*F2m5CFky>O3(7hV6I*I3#yLQYswG|*ZR3BmkEnNLUVIaP?Jbb|-)tse`NPdVjLoBZeElVJ66YFs~v$3tN4 zJ2@it2_b};5~Y-7z(KTm35ySc`x{w;lH|^+XB!K0Z-G0aM*fP6)ym>(_*75+HCJPu zXO5MMe)0v{(CC;9#vUn1q)#)Xw9cWENFaoLB5$pB_G9EerW^coNisC1PRq92 zzPlyP$-7Rr&+@~xtdEjXtFz1m(zKV0hCBi5Hz>0Yq6VbEsX^-yE2fMqV`<0>>6h#g zHfvA_+jR#JYc{7N*1b20*c>om^WKGuHv$lckQWfo34ni9tgZlfIm}gq0A68Ns{sML z>cL(!1n_rH@tQqeWC8r+XZ}ABz>DLt>JY$7WOOwlfR`2#D~M+kbTs%Q=y1%XKLG_D(y{;m