Skip to content

Commit ade1ea6

Browse files
committed
transfer ownership of a form
Signed-off-by: hamza221 <hamzamahjoubi221@gmail.com>
1 parent 674d340 commit ade1ea6

File tree

9 files changed

+225
-7
lines changed

9 files changed

+225
-7
lines changed

appinfo/routes.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@
131131
'apiVersion' => 'v2'
132132
]
133133
],
134+
[
135+
'name' => 'api#ownerTransfer',
136+
'url' => '/api/{apiVersion}/form/transfer',
137+
'verb' => 'POST',
138+
'requirements' => [
139+
'apiVersion' => 'v2'
140+
]
141+
],
134142

135143
// Questions
136144
[

lib/Controller/ApiController.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,4 +1136,49 @@ public function exportSubmissionsToCloud(string $hash, string $path) {
11361136

11371137
return new DataResponse($fileName);
11381138
}
1139+
/**
1140+
* @NoAdminRequired
1141+
*
1142+
* Transfer ownership of a form to another user
1143+
*
1144+
* @param int $formId id of the form to update
1145+
* @param string $uid id of the new owner
1146+
* @return DataResponse
1147+
* @throws OCSBadRequestException
1148+
* @throws OCSForbiddenException
1149+
*/
1150+
public function ownerTransfer(int $formId, string $uid): DataResponse {
1151+
$this->logger->debug('Updating owner: formId: {formId}, userId: {uid}', [
1152+
'formId' => $formId,
1153+
'uid' => $uid
1154+
]);
1155+
1156+
try {
1157+
$form = $this->formMapper->findById($formId);
1158+
} catch (IMapperException $e) {
1159+
$this->logger->debug('Could not find form');
1160+
throw new OCSBadRequestException('Could not find form');
1161+
}
1162+
1163+
if ($form->getOwnerId() !== $this->currentUser->getUID()) {
1164+
$this->logger->debug('This form is not owned by the current user');
1165+
throw new OCSForbiddenException();
1166+
}
1167+
1168+
// update form owner
1169+
$form->setOwnerId($uid);
1170+
1171+
// Update changed Columns in Db.
1172+
$this->formMapper->update($form);
1173+
1174+
//delete this form from shares for the new owner
1175+
try {
1176+
$share = $this->shareMapper->findPublicShareByFormIdAndUid($formId, $uid);
1177+
$this->shareMapper->deleteById($share->getId());
1178+
} catch (IMapperException $e) {
1179+
$this->logger->debug('No shares found');
1180+
}
1181+
1182+
return new DataResponse($form->getOwnerId());
1183+
}
11391184
}

lib/Db/ShareMapper.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,28 @@ public function findPublicShareByHash(string $hash): Share {
100100

101101
return $this->findEntity($qb);
102102
}
103+
/**
104+
* Find Share by formId and user id
105+
* @param int $formId
106+
* @param string $uid
107+
* @return Share
108+
* @throws MultipleObjectsReturnedException if more than one result
109+
* @throws DoesNotExistException if not found
110+
*/
111+
public function findPublicShareByFormIdAndUid(int $formId, string $uid): Share {
112+
$qb = $this->db->getQueryBuilder();
103113

114+
$qb->select('*')
115+
->from($this->getTableName())
116+
->where(
117+
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
118+
)
119+
->andWhere(
120+
$qb->expr()->eq('share_with', $qb->createNamedParameter($uid, IQueryBuilder::PARAM_STR))
121+
);
122+
123+
return $this->findEntity($qb);
124+
}
104125
/**
105126
* Delete a share
106127
* @param int $id of the share.

package-lock.json

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@nextcloud/browserslist-config": "^2.3.0",
5959
"@nextcloud/eslint-config": "^8.1.4",
6060
"@nextcloud/stylelint-config": "^2.3.0",
61-
"@nextcloud/webpack-vue-config": "^5.4.0"
61+
"@nextcloud/webpack-vue-config": "^5.4.0",
62+
"@types/debounce": "^1.2.1"
6263
}
6364
}

src/Forms.vue

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,27 +96,62 @@
9696
:form="selectedForm"
9797
:opened.sync="sidebarOpened"
9898
:active.sync="sidebarActive"
99-
name="sidebar" />
99+
name="sidebar"
100+
@transfer:ownership="openModal" />
100101
</template>
102+
<NcModal v-if="modal"
103+
ref="modal"
104+
size="normal"
105+
:title="'Transfer '+selectedForm.title"
106+
name="NcModal"
107+
:outTransition="true"
108+
@close="closeModal">
109+
<div class="modal__content">
110+
<h1 class="modal_section">{{ t('forms', 'Transfer ') }} {{ selectedForm.title}}</h1>
111+
<div class="modal_section">
112+
<p class="modal_text">{{ t('forms', 'Account to transfer to') }}</p>
113+
<SharingSearchDiv :isOwnershipTransfer="true" @add-share="setNewOwner" />
114+
<div class="selected_user" v-if="transferData.displayName.length>0">
115+
<p>{{transferData.displayName}}</p>
116+
<NcButton type="tertiary-no-background" @click="clearSelected">
117+
X
118+
</NcButton>
119+
</div>
120+
</div>
121+
<div class="modal_section">
122+
<p class="modal_text">{{ t('forms', 'Type ') }} {{confirmationString}} {{ t('forms', ' to confirm') }}</p>
123+
<NcTextField :value.sync="confirmation" :success="confirmation===confirmationString" :show-trailing-button="confirmation !== ''"
124+
@trailing-button-click="clearText" >
125+
</NcTextField>
126+
</div>
127+
<NcButton :disabled="confirmation!=confirmationString" type="error" @click="onOwnershipTransfer">
128+
{{ t('forms', 'I understand, transfer this form') }}
129+
</NcButton>
130+
</div>
131+
</NcModal>
101132
</NcContent>
102133
</template>
103134

104135
<script>
105136
import { emit } from '@nextcloud/event-bus'
106137
import { generateOcsUrl } from '@nextcloud/router'
107138
import { loadState } from '@nextcloud/initial-state'
108-
import { showError } from '@nextcloud/dialogs'
139+
import { showError, showSuccess } from '@nextcloud/dialogs'
109140
import axios from '@nextcloud/axios'
110141
111142
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
112143
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
113144
import NcAppNavigationCaption from '@nextcloud/vue/dist/Components/NcAppNavigationCaption.js'
114145
import NcAppNavigationNew from '@nextcloud/vue/dist/Components/NcAppNavigationNew.js'
115146
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
147+
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
116148
import NcContent from '@nextcloud/vue/dist/Components/NcContent.js'
117149
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
118150
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
119151
import isMobile from '@nextcloud/vue/dist/Mixins/isMobile.js'
152+
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
153+
import SharingSearchDiv from './components/SidebarTabs/SharingSearchDiv.vue'
154+
120155
121156
import IconPlus from 'vue-material-design-icons/Plus.vue'
122157
@@ -141,23 +176,34 @@ export default {
141176
NcContent,
142177
NcEmptyContent,
143178
NcLoadingIcon,
179+
NcModal,
180+
SharingSearchDiv,
181+
NcTextField,
144182
},
145183
146184
mixins: [isMobile, PermissionTypes],
147185
148186
data() {
149187
return {
150188
loading: true,
189+
modal: false,
151190
sidebarOpened: false,
152191
sidebarActive: 'forms-sharing',
153192
forms: [],
154193
sharedForms: [],
155-
194+
transferData: { formId: null, userId: null ,displayName: ''},
195+
confirmation:'',
156196
canCreateForms: loadState(appName, 'appConfig').canCreateForms,
157197
}
158198
},
159199
160200
computed: {
201+
canEdit() {
202+
return this.selectedForm.permissions.includes(this.PERMISSION_TYPES.PERMISSION_EDIT)
203+
},
204+
confirmationString(){
205+
return `${this.selectedForm.ownerId}/${this.selectedForm.title}`
206+
},
161207
hasForms() {
162208
return !this.noOwnedForms || !this.noSharedForms
163209
},
@@ -215,12 +261,54 @@ export default {
215261
},
216262
},
217263
},
264+
218265
219266
beforeMount() {
220267
this.loadForms()
221268
},
222269
223270
methods: {
271+
clearSelected(){
272+
this.transferData= { formId: null, userId: null ,displayName: ''}
273+
},
274+
clearText() {
275+
this.confirmation = ''
276+
},
277+
setNewOwner(share){
278+
console.log(share)
279+
this.transferData.userId=share.shareWith
280+
this.transferData.formId=this.selectedForm.id
281+
this.transferData.displayName=share.displayName
282+
283+
},
284+
closeModal() {
285+
this.modal = false
286+
showError(t('forms', 'Ownership transfer was Cancelled'))
287+
},
288+
openModal() {
289+
this.modal = true
290+
},
291+
async onOwnershipTransfer() {
292+
this.modal = false
293+
if (this.transferData.formId && this.transferData.userId) {
294+
try {
295+
await axios.post(generateOcsUrl('apps/forms/api/v2/form/transfer'), {
296+
formId: this.transferData.formId,
297+
uid: this.transferData.userId,
298+
})
299+
showSuccess(`${t('forms', 'This form is now owned by')} ${this.transferData.displayName}`)
300+
this.$router.push({ name: 'root' })
301+
302+
} catch (error) {
303+
logger.error('Error while transfering form ownership', { error })
304+
showError(t('forms', 'An error occurred while transfering ownership'))
305+
}
306+
307+
} else {
308+
logger.error('Null parameters while transfering form ownership', { transferData: this.tranferData })
309+
showError(t('forms', 'An error occurred while transfering ownership'))
310+
}
311+
},
224312
/**
225313
* Closes the App-Navigation on mobile-devices
226314
*/
@@ -348,3 +436,21 @@ export default {
348436
},
349437
}
350438
</script>
439+
<style scoped>
440+
.modal__content {
441+
margin: 50px;
442+
}
443+
.modal_text{
444+
text-align: start;
445+
margin-bottom: 10px;
446+
}
447+
.modal_section{
448+
margin-bottom: 20px;
449+
}
450+
451+
.selected_user{
452+
display: flex;
453+
align-items: center;
454+
}
455+
456+
</style>

src/components/SidebarTabs/SettingsSidebarTab.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,29 @@
5555
type="datetime"
5656
@change="onExpirationDateChange" />
5757
</div>
58+
<div>
59+
<NcButton @click="onChangeOwner">
60+
{{ t('forms', 'Transfer ownership') }}
61+
</NcButton>
62+
</div>
5863
</div>
5964
</template>
6065

6166
<script>
6267
import moment from '@nextcloud/moment'
6368
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
69+
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
6470
import NcDatetimePicker from '@nextcloud/vue/dist/Components/NcDatetimePicker.js'
6571
import ShareTypes from '../../mixins/ShareTypes.js'
72+
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
73+
6674
6775
export default {
6876
components: {
6977
NcCheckboxRadioSwitch,
7078
NcDatetimePicker,
79+
NcButton,
80+
NcEmptyContent,
7181
},
7282
7383
mixins: [ShareTypes],
@@ -85,6 +95,7 @@ export default {
8595
stringify: this.stringifyDate,
8696
parse: this.parseTimestampToDate,
8797
},
98+
8899
}
89100
},
90101
@@ -196,6 +207,10 @@ export default {
196207
notBeforeNow(datetime) {
197208
return datetime < moment().toDate()
198209
},
210+
onChangeOwner() {
211+
this.localLoading=true
212+
this.$emit('transfer:ownership')
213+
},
199214
},
200215
}
201216
</script>

src/components/SidebarTabs/SharingSearchDiv.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<template>
2525
<div>
2626
<NcMultiselect :clear-on-select="false"
27-
:close-on-select="false"
27+
:close-on-select="isOwnershipTransfer"
2828
:hide-selected="true"
2929
:internal-search="false"
3030
:loading="showLoadingCircle"
@@ -74,6 +74,11 @@ export default {
7474
type: Boolean,
7575
default: false,
7676
},
77+
isOwnershipTransfer: {
78+
type:Boolean,
79+
default: false,
80+
required:false
81+
}
7782
},
7883
7984
data() {

0 commit comments

Comments
 (0)