Skip to content

Commit c1e2b05

Browse files
committed
[WIP] Vuetiful emails
Signed-off-by: Christopher Ng <chrng8@gmail.com>
1 parent fabcf86 commit c1e2b05

11 files changed

Lines changed: 243 additions & 11 deletions

File tree

apps/provisioning_api/lib/Controller/UsersController.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -740,12 +740,19 @@ public function editUser(string $userId, string $key, string $value): DataRespon
740740
$this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value);
741741
break;
742742
case IAccountManager::PROPERTY_EMAIL:
743-
if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') {
744-
$targetUser->setEMailAddress($value);
745-
} else {
746-
throw new OCSException('', 102);
743+
$userAccount = $this->accountManager->getAccount($targetUser);
744+
$userProperty = $userAccount->getProperty($key);
745+
if ($userProperty->getValue() !== $value) {
746+
try {
747+
$userProperty->setValue($value);
748+
$this->accountManager->updateAccount($userAccount);
749+
} catch (\InvalidArgumentException $e) {
750+
throw new OCSException('Invalid ' . $e->getMessage(), 102);
751+
}
747752
}
748753
break;
754+
case IAccountManager::COLLECTION_EMAIL:
755+
break;
749756
case IAccountManager::PROPERTY_PHONE:
750757
case IAccountManager::PROPERTY_ADDRESS:
751758
case IAccountManager::PROPERTY_WEBSITE:

apps/settings/js/federationsettingsview.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
this._inputFields = [
3737
'displayname',
3838
'phone',
39-
'email',
39+
// 'email',
4040
'website',
4141
'twitter',
4242
'address',

apps/settings/lib/Settings/Personal/PersonalInfo.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
use OCP\IUserManager;
5050
use OCP\L10N\IFactory;
5151
use OCP\Settings\ISettings;
52+
use OCP\Accounts\IAccountProperty;
53+
use OCP\AppFramework\Services\IInitialState;
5254

5355
class PersonalInfo implements ISettings {
5456

@@ -66,6 +68,8 @@ class PersonalInfo implements ISettings {
6668
private $l10nFactory;
6769
/** @var IL10N */
6870
private $l;
71+
/** @var IInitialState */
72+
private $initialStateService;
6973

7074
public function __construct(
7175
IConfig $config,
@@ -74,7 +78,8 @@ public function __construct(
7478
IAccountManager $accountManager,
7579
IAppManager $appManager,
7680
IFactory $l10nFactory,
77-
IL10N $l
81+
IL10N $l,
82+
IInitialState $initialStateService
7883
) {
7984
$this->config = $config;
8085
$this->userManager = $userManager;
@@ -83,6 +88,7 @@ public function __construct(
8388
$this->appManager = $appManager;
8489
$this->l10nFactory = $l10nFactory;
8590
$this->l = $l;
91+
$this->initialStateService = $initialStateService;
8692
}
8793

8894
public function getForm(): TemplateResponse {
@@ -139,6 +145,24 @@ public function getForm(): TemplateResponse {
139145
'groups' => $this->getGroups($user),
140146
] + $messageParameters + $languageParameters + $localeParameters;
141147

148+
$this->initialStateService->provideInitialState(
149+
'emails',
150+
[
151+
[
152+
'value' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue(),
153+
'scope' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope(),
154+
'verified' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getVerified(),
155+
],
156+
...array_map(
157+
fn(IAccountProperty $property) => [
158+
'value' => $property->getValue(),
159+
'scope' => $property->getScope(),
160+
'verified' => $property->getVerified(),
161+
],
162+
$account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties()
163+
)
164+
]
165+
);
142166

143167
return new TemplateResponse('settings', 'settings/personal/personal.info', $parameters, '');
144168
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<!--
2+
- @copyright 2021 Christopher Ng <chrng8@gmail.com>
3+
-
4+
- @author 2021 Christopher Ng <chrng8@gmail.com>
5+
-
6+
- @license GNU AGPL version 3 or any later version
7+
-
8+
- This program is free software: you can redistribute it and/or modify
9+
- it under the terms of the GNU Affero General Public License as
10+
- published by the Free Software Foundation, either version 3 of the
11+
- License, or (at your option) any later version.
12+
-
13+
- This program is distributed in the hope that it will be useful,
14+
- but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
- GNU Affero General Public License for more details.
17+
-
18+
- You should have received a copy of the GNU Affero General Public License
19+
- along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
-->
21+
22+
<template>
23+
<form id="emailform" class="section">
24+
<h3>
25+
<label for="email">{{ t('settings', 'Emails') }}</label>
26+
<a href="#" class="federation-menu" :aria-label="t('settings', 'Change privacy level of email')">
27+
<span class="icon-federation-menu icon-password">
28+
<span class="icon-triangle-s"></span>
29+
</span>
30+
</a>
31+
<Actions>
32+
<ActionButton icon="icon-add" @click.stop.prevent="addEmails(email, additionalEmails)">
33+
{{ t('settings', 'Add email address') }}
34+
</ActionButton>
35+
</Actions>
36+
</h3>
37+
<input
38+
type="email"
39+
name="email"
40+
id="email"
41+
v-model="email"
42+
:placeholder="t('settings', 'Your email address')" />
43+
<input
44+
v-for="(email, index) in additionalEmails"
45+
type="email"
46+
name="additionalEmail[]"
47+
:id="`additionalEmail-${index}`"
48+
v-model="email.value"
49+
:placeholder="t('settings', `Additional email address ${index+1}`)"
50+
:key="index" />
51+
</form>
52+
</template>
53+
54+
<script>
55+
import { Actions, ActionButton } from '@nextcloud/vue'
56+
import axios from '@nextcloud/axios'
57+
import * as auth from '@nextcloud/auth'
58+
import * as router from '@nextcloud/router'
59+
import ErrorHandler from '../../mixins/ErrorHandler'
60+
61+
export default {
62+
name: 'EmailSection',
63+
components: {
64+
Actions,
65+
ActionButton,
66+
},
67+
mixins: [ErrorHandler],
68+
props: {
69+
initialEmails: {
70+
type: Array,
71+
required: true,
72+
},
73+
},
74+
data() {
75+
/* eslint-disable */
76+
console.log(this.initialEmails)
77+
return {
78+
email: this.initialEmails[0].value,
79+
additionalEmails: this.initialEmails.slice(1),
80+
}
81+
},
82+
methods: {
83+
async addEmails(email, additionalEmails) {
84+
const userId = auth.getCurrentUser().uid
85+
// TODO upgrade @nextcloud/router to v2.0 so we can remove the .slice() trailing slash hack
86+
const url = router.generateOcsUrl(`cloud/users/${userId}`, 2).slice(0, -1)
87+
88+
// Set the primary email
89+
const res = await this.handleError(axios.put(url, {
90+
key: 'email',
91+
value: email,
92+
}))
93+
// console.log(res.data)
94+
95+
// Set additional emails
96+
const resp = await this.handleError(axios.put(url, {
97+
key: 'additional_mail',
98+
value: additionalEmails.map(({ value }) => value),
99+
}))
100+
// console.log(res.data)
101+
102+
additionalEmails.push({ value: '' })
103+
}
104+
}
105+
}
106+
</script>
107+
108+
<style scoped>
109+
</style>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @copyright 2021, Christopher Ng <chrng8@gmail.com>
3+
*
4+
* @author Christopher Ng <chrng8@gmail.com>
5+
*
6+
* @license GNU AGPL version 3 or any later version
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
import Vue from 'vue'
24+
import { loadState } from '@nextcloud/initial-state'
25+
26+
import EmailSection from './components/PersonalInfo/EmailSection'
27+
28+
// eslint-disable-next-line camelcase
29+
__webpack_nonce__ = btoa(OC.requestToken)
30+
31+
Vue.prototype.t = t
32+
33+
const View = Vue.extend(EmailSection)
34+
new View({
35+
propsData: {
36+
initialEmails: loadState('settings', 'emails'),
37+
// ...more initial props
38+
},
39+
}).$mount('#vue-emailform')
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @copyright Copyright (c) 2021 Christopher Ng <chrng8@gmail.com>
3+
*
4+
* @author Christopher Ng <chrng8@gmail.com>
5+
*
6+
* @license GNU AGPL version 3 or any later version
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
export default {
24+
methods: {
25+
handleError(promise) {
26+
promise
27+
.then(response => response)
28+
.catch(error => ({ data: null, error }))
29+
}
30+
}
31+
}

apps/settings/templates/settings/personal/personal.info.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
'federationsettingsview',
3232
'federationscopemenu',
3333
'settings/personalInfo',
34+
'vue-settings-personal-info',
3435
]);
3536
?>
3637

@@ -125,6 +126,9 @@
125126
<input type="hidden" id="displaynamescope" value="<?php p($_['displayNameScope']) ?>">
126127
</form>
127128
</div>
129+
<div class="personal-settings-setting-box">
130+
<div id="vue-emailform" class="section"></div>
131+
</div>
128132
<div class="personal-settings-setting-box">
129133
<form id="emailform" class="section">
130134
<h3>

apps/settings/webpack.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module.exports = {
3232
'settings-personal-security': path.join(__dirname, 'src', 'main-personal-security'),
3333
'settings-personal-webauthn': path.join(__dirname, 'src', 'main-personal-webauth'),
3434
'settings-nextcloud-pdf': path.join(__dirname, 'src', 'main-nextcloud-pdf'),
35+
'settings-personal-info': path.join(__dirname, 'src', 'main-personal-info'),
3536
},
3637
output: {
3738
path: path.resolve(__dirname, './js'),

lib/private/Accounts/AccountManager.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,12 @@ protected function addMissingDefaultValues(array $userData) {
403403
}
404404
}
405405

406+
foreach ($this::COLLECTION_PROPERTIES as $property) {
407+
if (!isset($userData[$property])) {
408+
$userData[$property] = new AccountPropertyCollection($property);
409+
}
410+
}
411+
406412
return $userData;
407413
}
408414

@@ -564,6 +570,11 @@ protected function buildDefaultUserRecord(IUser $user) {
564570
'scope' => self::SCOPE_FEDERATED,
565571
'verified' => self::NOT_VERIFIED,
566572
],
573+
self::COLLECTION_EMAIL =>
574+
[
575+
// TODO implement the correct way
576+
'properties' => [],
577+
],
567578
self::PROPERTY_AVATAR =>
568579
[
569580
'scope' => self::SCOPE_FEDERATED
@@ -586,7 +597,11 @@ protected function buildDefaultUserRecord(IUser $user) {
586597
private function parseAccountData(IUser $user, $data): Account {
587598
$account = new Account($user);
588599
foreach ($data as $property => $accountData) {
589-
$account->setProperty($property, $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
600+
if (!$this->isCollection($property)) {
601+
$account->setProperty($property, $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
602+
} else {
603+
$account->setPropertyCollection(new AccountPropertyCollection($property));
604+
}
590605
}
591606
return $account;
592607
}
@@ -598,7 +613,7 @@ public function getAccount(IUser $user): IAccount {
598613
public function updateAccount(IAccount $account): void {
599614
$data = [];
600615

601-
foreach ($account->getProperties() as $property) {
616+
foreach ($account->getAllProperties() as $property) {
602617
$data[$property->getName()] = [
603618
'value' => $property->getValue(),
604619
'scope' => $property->getScope(),

lib/private/Accounts/TAccountsHelper.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@
3131
trait TAccountsHelper {
3232
protected function isCollection(string $propertyName): bool {
3333
return in_array($propertyName,
34-
[
35-
IAccountManager::COLLECTION_EMAIL,
36-
],
34+
IAccountManager::COLLECTION_PROPERTIES,
3735
true
3836
);
3937
}

0 commit comments

Comments
 (0)