Commit 7c378891 authored by Jiaan Louw's avatar Jiaan Louw Committed by Markus Koller

Update project delete modals

- Update remove project setting section
- Add a unique modal for regular project deletions
- Add a unique modal for adjourned project deletions
parent ecb23143
...@@ -7,7 +7,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; ...@@ -7,7 +7,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers'; import initFilePickers from '~/file_pickers';
import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectLoadingSpinner from '../shared/save_project_loader';
import initProjectPermissionsSettings from '../shared/permissions'; import initProjectPermissionsSettings from '../shared/permissions';
import initProjectRemoveModal from '~/projects/project_remove_modal'; import initProjectDeleteButton from '~/projects/project_delete_button';
import UserCallout from '~/user_callout'; import UserCallout from '~/user_callout';
import initServiceDesk from '~/projects/settings_service_desk'; import initServiceDesk from '~/projects/settings_service_desk';
...@@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFilePickers(); initFilePickers();
initConfirmDangerModal(); initConfirmDangerModal();
initSettingsPanels(); initSettingsPanels();
initProjectRemoveModal(); initProjectDeleteButton();
mountBadgeSettings(PROJECT_BADGE); mountBadgeSettings(PROJECT_BADGE);
new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new
......
<script>
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import SharedDeleteButton from './shared/delete_button.vue';
export default {
components: {
GlSprintf,
GlAlert,
SharedDeleteButton,
},
props: {
confirmPhrase: {
type: String,
required: true,
},
formPath: {
type: String,
required: true,
},
},
strings: {
alertTitle: __('You are about to permanently delete this project'),
alertBody: __(
'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its respositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.',
),
modalBody: __(
"This action cannot be undone. You will lose the project's respository and all conent: issues, merge requests, etc.",
),
},
};
</script>
<template>
<shared-delete-button v-bind="{ confirmPhrase, formPath }">
<template #modal-body>
<gl-alert
class="gl-mb-5"
variant="danger"
:title="$options.strings.alertTitle"
:dismissible="false"
>
<gl-sprintf :message="$options.strings.alertBody">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</gl-alert>
<p>{{ $options.strings.modalBody }}</p>
</template>
</shared-delete-button>
</template>
<script> <script>
import { GlModal, GlModalDirective, GlSprintf, GlFormInput, GlButton } from '@gitlab/ui'; import { uniqueId } from 'lodash';
import { GlModal, GlModalDirective, GlFormInput, GlButton } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { rstrip } from '~/lib/utils/common_utils';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
export default { export default {
components: { components: {
GlModal, GlModal,
GlSprintf,
GlFormInput, GlFormInput,
GlButton, GlButton,
}, },
...@@ -19,10 +18,6 @@ export default { ...@@ -19,10 +18,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
warningMessage: {
type: String,
required: true,
},
formPath: { formPath: {
type: String, type: String,
required: true, required: true,
...@@ -31,15 +26,27 @@ export default { ...@@ -31,15 +26,27 @@ export default {
data() { data() {
return { return {
userInput: null, userInput: null,
modalId: uniqueId('delete-project-modal-'),
}; };
}, },
computed: { computed: {
buttonDisabled() { confirmDisabled() {
return rstrip(this.userInput) !== this.confirmPhrase; return this.userInput !== this.confirmPhrase;
}, },
csrfToken() { csrfToken() {
return csrf.token; return csrf.token;
}, },
modalActionProps() {
return {
primary: {
text: __('Yes, delete project'),
attributes: [{ variant: 'danger' }, { disabled: this.confirmDisabled }],
},
cancel: {
text: __('Cancel, keep project'),
},
};
},
}, },
methods: { methods: {
submitForm() { submitForm() {
...@@ -47,15 +54,10 @@ export default { ...@@ -47,15 +54,10 @@ export default {
}, },
}, },
strings: { strings: {
removeProject: __('Remove project'), deleteProject: __('Remove project'),
title: __('Confirmation required'), title: __('Delete project. Are you ABSOLUTELY SURE?'),
confirm: __('Confirm'), confirmText: __('Please type the following to confirm:'),
dataLoss: __(
'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.',
),
confirmText: __('Please type %{phrase_code} to proceed or close this modal to cancel.'),
}, },
modalId: 'remove-project-modal',
}; };
</script> </script>
...@@ -63,38 +65,28 @@ export default { ...@@ -63,38 +65,28 @@ export default {
<form ref="form" :action="formPath" method="post"> <form ref="form" :action="formPath" method="post">
<input type="hidden" name="_method" value="delete" /> <input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" /> <input :value="csrfToken" type="hidden" name="authenticity_token" />
<gl-button v-gl-modal="$options.modalId" category="primary" variant="danger">{{
$options.strings.removeProject <gl-button v-gl-modal="modalId" category="primary" variant="danger">{{
$options.strings.deleteProject
}}</gl-button> }}</gl-button>
<gl-modal <gl-modal
ref="removeModal" ref="removeModal"
:modal-id="$options.modalId" :modal-id="modalId"
size="sm" size="sm"
ok-variant="danger" ok-variant="danger"
footer-class="bg-gray-light gl-p-5" footer-class="gl-bg-gray-10 gl-p-5"
title-class="gl-text-red-500"
:action-primary="modalActionProps.primary"
:action-cancel="modalActionProps.cancel"
@ok="submitForm"
> >
<template #modal-title>{{ $options.strings.title }}</template> <template #modal-title>{{ $options.strings.title }}</template>
<template #modal-footer>
<div class="gl-w-full gl-display-flex gl-just-content-start gl-m-0">
<gl-button
:disabled="buttonDisabled"
category="primary"
variant="danger"
@click="submitForm"
>
{{ $options.strings.confirm }}
</gl-button>
</div>
</template>
<div> <div>
<p class="gl-text-red-500 gl-font-weight-bold">{{ warningMessage }}</p> <slot name="modal-body"></slot>
<p class="gl-mb-0">{{ $options.strings.dataLoss }}</p> <p class="gl-mb-1">{{ $options.strings.confirmText }}</p>
<p> <p>
<gl-sprintf :message="$options.strings.confirmText"> <code>{{ confirmPhrase }}</code>
<template #phrase_code>
<code>{{ confirmPhrase }}</code>
</template>
</gl-sprintf>
</p> </p>
<gl-form-input <gl-form-input
id="confirm_name_input" id="confirm_name_input"
...@@ -102,6 +94,7 @@ export default { ...@@ -102,6 +94,7 @@ export default {
name="confirm_name_input" name="confirm_name_input"
type="text" type="text"
/> />
<slot name="modal-footer"></slot>
</div> </div>
</gl-modal> </gl-modal>
</form> </form>
......
import Vue from 'vue'; import Vue from 'vue';
import RemoveProjectModal from './components/remove_modal.vue'; import ProjectDeleteButton from './components/project_delete_button.vue';
export default (selector = '#js-confirm-project-remove') => { export default (selector = '#js-project-delete-button') => {
const el = document.querySelector(selector); const el = document.querySelector(selector);
if (!el) return; if (!el) return;
const { formPath, confirmPhrase, warningMessage } = el.dataset; const { confirmPhrase, formPath } = el.dataset;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
render(createElement) { render(createElement) {
return createElement(RemoveProjectModal, { return createElement(ProjectDeleteButton, {
props: { props: {
confirmPhrase, confirmPhrase,
warningMessage,
formPath, formPath,
}, },
}); });
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
%strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.') %strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
%p %p
%strong= _('Removed projects cannot be restored!') %strong= _('Removed projects cannot be restored!')
#js-confirm-project-remove{ data: { form_path: project_path(project), confirm_phrase: project.path, warning_message: remove_project_message(project) } } #js-project-delete-button{ data: { form_path: project_path(project), confirm_phrase: project.path } }
---
title: Update project remove modal to add additional warnings
merge_request: 36962
author:
type: changed
...@@ -5,6 +5,7 @@ import mountApprovals from 'ee/approvals/mount_project_settings'; ...@@ -5,6 +5,7 @@ import mountApprovals from 'ee/approvals/mount_project_settings';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import UserCallout from '~/user_callout'; import UserCallout from '~/user_callout';
import groupsSelect from '~/groups_select'; import groupsSelect from '~/groups_select';
import initProjectAdjournedDeleteButton from 'ee/projects/project_adjourned_delete_button';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); new UsersSelect();
...@@ -13,4 +14,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -13,4 +14,6 @@ document.addEventListener('DOMContentLoaded', () => {
new UserCallout({ className: 'js-mr-approval-callout' }); new UserCallout({ className: 'js-mr-approval-callout' });
mountApprovals(document.getElementById('js-mr-approvals-settings')); mountApprovals(document.getElementById('js-mr-approvals-settings'));
initProjectAdjournedDeleteButton();
}); });
<script>
import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import SharedDeleteButton from '~/projects/components/shared/delete_button.vue';
export default {
components: {
GlSprintf,
GlIcon,
GlLink,
SharedDeleteButton,
},
props: {
confirmPhrase: {
type: String,
required: true,
},
formPath: {
type: String,
required: true,
},
adjournedRemovalDate: {
type: String,
required: true,
},
recoveryHelpPath: {
type: String,
required: true,
},
},
strings: {
modalBody: __(
"Once a project is permanently deleted it cannot be recovered. You will lose this project's repository and all content: issues, merge requests etc.",
),
helpLabel: __('Recovering projects'),
recoveryMessage: __('You can recover this project until %{date}'),
},
};
</script>
<template>
<shared-delete-button v-bind="{ confirmPhrase, formPath }">
<template #modal-body>
<p>{{ $options.strings.modalBody }}</p>
</template>
<template #modal-footer>
<p
class="gl-display-flex gl-display-flex gl-align-items-center gl-mt-3 gl-mb-0 gl-text-gray-500"
>
<gl-sprintf :message="$options.strings.recoveryMessage">
<template #date>
{{ adjournedRemovalDate }}
</template>
</gl-sprintf>
<gl-link
:aria-label="$options.strings.helpLabel"
class="gl-display-flex gl-ml-2 gl-text-gray-500"
:href="recoveryHelpPath"
>
<gl-icon name="question" />
</gl-link>
</p>
</template>
</shared-delete-button>
</template>
import Vue from 'vue';
import ProjectAdjournedDeleteButton from './components/project_adjourned_delete_button.vue';
export default (selector = '#js-project-adjourned-delete-button') => {
const el = document.querySelector(selector);
if (!el) return;
const { adjournedRemovalDate, confirmPhrase, formPath, recoveryHelpPath } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
render(createElement) {
return createElement(ProjectAdjournedDeleteButton, {
props: {
adjournedRemovalDate,
confirmPhrase,
formPath,
recoveryHelpPath,
},
});
},
});
};
...@@ -94,6 +94,17 @@ module EE ...@@ -94,6 +94,17 @@ module EE
{ date: date } { date: date }
end end
def permanent_delete_message(project)
message = _('This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all content: issues, merge requests, etc.')
html_escape(message) % remove_message_data(project)
end
def marked_for_removal_message(project)
date = permanent_deletion_date(Time.now.utc)
message = _('This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}on %{date}%{strongClose}, including its repositories and all content: issues, merge requests, etc.')
html_escape(message) % remove_message_data(project).merge(date: date)
end
def permanent_deletion_date(date) def permanent_deletion_date(date)
(date + ::Gitlab::CurrentSettings.deletion_adjourned_period.days).strftime('%F') (date + ::Gitlab::CurrentSettings.deletion_adjourned_period.days).strftime('%F')
end end
...@@ -282,5 +293,15 @@ module EE ...@@ -282,5 +293,15 @@ module EE
nav_tabs nav_tabs
end end
def remove_message_data(project)
{
project: project.path,
strongOpen: '<strong>'.html_safe,
strongClose: '</strong>'.html_safe,
codeOpen: '<code>'.html_safe,
codeClose: '</code>'.html_safe
}
end
end end
end end
- return unless can?(current_user, :remove_project, project) - return unless can?(current_user, :remove_project, project)
- adjourned_deletion = project.adjourned_deletion? - adjourned_deletion = project.adjourned_deletion?
- adjourned_date = adjourned_deletion ? permanent_deletion_date(Time.now.utc).to_s : nil;
- admin_help_path = help_page_path('user/admin_area/settings/visibility_and_access_controls', anchor: 'default-deletion-adjourned-period-premium-only')
- recovery_help_path = help_page_path('user/project/settings/index', anchor: 'remove-a-project')
- unless project.marked_for_deletion? - unless project.marked_for_deletion?
.sub-section .sub-section
...@@ -8,14 +11,15 @@ ...@@ -8,14 +11,15 @@
%strong= s_('Delayed Project Deletion (%{adjourned_deletion})') % { adjourned_deletion: adjourned_deletion ? 'Enabled' : 'Disabled' } %strong= s_('Delayed Project Deletion (%{adjourned_deletion})') % { adjourned_deletion: adjourned_deletion ? 'Enabled' : 'Disabled' }
- if adjourned_deletion - if adjourned_deletion
= render 'projects/settings/marked_for_removal' = render 'projects/settings/marked_for_removal'
#js-project-adjourned-delete-button{ data: { recovery_help_path: recovery_help_path, adjourned_removal_date: adjourned_date, form_path: project_path(project), confirm_phrase: project.path } }
- else - else
%p %p
= _("Removing a project deletes it immediately, there will be no delay before the project is permanently removed.") %span.gl-text-gray-500= _('Projects will be permanently deleted immediately.')
%p = link_to(_('Customizable by an administrator.'), admin_help_path)
%strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.') %p= permanent_delete_message(project)
%p %p
%strong= _('Removed projects cannot be restored!') %strong= _('Are you ABSOLUTELY SURE you wish to delete this project?')
#js-confirm-project-remove{ data: { form_path: project_path(project), confirm_phrase: project.path, warning_message: remove_project_message(project) } } #js-project-delete-button{ data: { form_path: project_path(project), confirm_phrase: project.path } }
- else - else
= render 'projects/settings/restore', project: project = render 'projects/settings/restore', project: project
- return unless @project.feature_available?(:adjourned_deletion_for_projects_and_groups) - return unless @project.feature_available?(:adjourned_deletion_for_projects_and_groups)
- date = permanent_deletion_date(Time.now.utc)
%p %p
= _("Removing a project places it into a read-only state until %{date}, at which point the project will be permanently removed.") %{ date: date } %span.gl-text-gray-500= _('Projects will be permanently deleted after a 7-day waiting period.')
%br = link_to(_('Customizable by an administrator.'), help_page_path('user/admin_area/settings/visibility_and_access_controls', anchor: 'default-deletion-adjourned-period-premium-only'))
= _("Until that time, the project can be restored.") %p= marked_for_removal_message(@project)
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Project remove modal initialized matches the snapshot 1`] = `
<form
action="some/path"
method="post"
>
<input
name="_method"
type="hidden"
value="delete"
/>
<input
name="authenticity_token"
type="hidden"
/>
<gl-button-stub
category="primary"
icon=""
role="button"
size="medium"
tabindex="0"
variant="danger"
>
Remove project
</gl-button-stub>
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
footer-class="gl-bg-gray-10 gl-p-5"
modalclass=""
modalid="fakeUniqueId"
ok-variant="danger"
size="sm"
title-class="gl-text-red-500"
titletag="h4"
>
<div>
<p>
Once a project is permanently deleted it cannot be recovered. You will lose this project's repository and all content: issues, merge requests etc.
</p>
<p
class="gl-mb-1"
>
Please type the following to confirm:
</p>
<p>
<code>
foo
</code>
</p>
<gl-form-input-stub
id="confirm_name_input"
name="confirm_name_input"
type="text"
/>
<p
class="gl-display-flex gl-display-flex gl-align-items-center gl-mt-3 gl-mb-0 gl-text-gray-500"
>
<gl-sprintf-stub
message="You can recover this project until %{date}"
/>
<gl-link-stub
aria-label="Recovering projects"
class="gl-display-flex gl-ml-2 gl-text-gray-500"
href="recovery/help/path"
>
<gl-icon-stub
name="question"
size="16"
/>
</gl-link-stub>
</p>
</div>
</gl-modal-stub>
</form>
`;
import { shallowMount } from '@vue/test-utils';
import ProjectAdjournedDeleteButton from 'ee/projects/components/project_adjourned_delete_button.vue';
import SharedDeleteButton from '~/projects/components/shared/delete_button.vue';
jest.mock('lodash/uniqueId', () => () => 'fakeUniqueId');
describe('Project remove modal', () => {
let wrapper;
const findSharedDeleteButton = () => wrapper.find(SharedDeleteButton);
const defaultProps = {
adjournedRemovalDate: '2020-12-12',
confirmPhrase: 'foo',
formPath: 'some/path',
recoveryHelpPath: 'recovery/help/path',
};
const createComponent = (props = {}) => {
wrapper = shallowMount(ProjectAdjournedDeleteButton, {
propsData: {
...defaultProps,
...props,
},
stubs: {
SharedDeleteButton,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('initialized', () => {
beforeEach(() => {
createComponent();
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('passes confirmPhrase and formPath props to the shared delete button', () => {
expect(findSharedDeleteButton().props()).toEqual({
confirmPhrase: defaultProps.confirmPhrase,
formPath: defaultProps.formPath,
});
});
});
});
...@@ -3083,6 +3083,9 @@ msgstr "" ...@@ -3083,6 +3083,9 @@ msgstr ""
msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}" msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}"
msgstr "" msgstr ""
msgid "Are you ABSOLUTELY SURE you wish to delete this project?"
msgstr ""
msgid "Are you setting up GitLab for a company?" msgid "Are you setting up GitLab for a company?"
msgstr "" msgstr ""
...@@ -4245,6 +4248,9 @@ msgstr "" ...@@ -4245,6 +4248,9 @@ msgstr ""
msgid "Cancel this job" msgid "Cancel this job"
msgstr "" msgstr ""
msgid "Cancel, keep project"
msgstr ""
msgid "Canceled deployment to" msgid "Canceled deployment to"
msgstr "" msgstr ""
...@@ -7283,6 +7289,9 @@ msgstr "" ...@@ -7283,6 +7289,9 @@ msgstr ""
msgid "Customer Portal" msgid "Customer Portal"
msgstr "" msgstr ""
msgid "Customizable by an administrator."
msgstr ""
msgid "Customize colors" msgid "Customize colors"
msgstr "" msgstr ""
...@@ -7729,6 +7738,9 @@ msgstr "" ...@@ -7729,6 +7738,9 @@ msgstr ""
msgid "Delete project" msgid "Delete project"
msgstr "" msgstr ""
msgid "Delete project. Are you ABSOLUTELY SURE?"
msgstr ""
msgid "Delete serverless domain?" msgid "Delete serverless domain?"
msgstr "" msgstr ""
...@@ -16547,6 +16559,12 @@ msgstr "" ...@@ -16547,6 +16559,12 @@ msgstr ""
msgid "OnDemandScans|Target URL" msgid "OnDemandScans|Target URL"
msgstr "" msgstr ""
msgid "Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its respositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc."
msgstr ""
msgid "Once a project is permanently deleted it cannot be recovered. You will lose this project's repository and all content: issues, merge requests etc."
msgstr ""
msgid "Once imported, repositories can be mirrored over SSH. Read more %{link_start}here%{link_end}." msgid "Once imported, repositories can be mirrored over SSH. Read more %{link_start}here%{link_end}."
msgstr "" msgstr ""
...@@ -17773,6 +17791,9 @@ msgstr "" ...@@ -17773,6 +17791,9 @@ msgstr ""
msgid "Please type %{phrase_code} to proceed or close this modal to cancel." msgid "Please type %{phrase_code} to proceed or close this modal to cancel."
msgstr "" msgstr ""
msgid "Please type the following to confirm:"
msgstr ""
msgid "Please use this form to report to the admin users who create spam issues, comments or behave inappropriately." msgid "Please use this form to report to the admin users who create spam issues, comments or behave inappropriately."
msgstr "" msgstr ""
...@@ -18991,6 +19012,12 @@ msgstr "" ...@@ -18991,6 +19012,12 @@ msgstr ""
msgid "Projects to index" msgid "Projects to index"
msgstr "" msgstr ""
msgid "Projects will be permanently deleted after a 7-day waiting period."
msgstr ""
msgid "Projects will be permanently deleted immediately."
msgstr ""
msgid "Projects with critical vulnerabilities" msgid "Projects with critical vulnerabilities"
msgstr "" msgstr ""
...@@ -19675,6 +19702,9 @@ msgstr "" ...@@ -19675,6 +19702,9 @@ msgstr ""
msgid "Recover hidden stage" msgid "Recover hidden stage"
msgstr "" msgstr ""
msgid "Recovering projects"
msgstr ""
msgid "Recovery Codes" msgid "Recovery Codes"
msgstr "" msgstr ""
...@@ -20069,12 +20099,6 @@ msgstr "" ...@@ -20069,12 +20099,6 @@ msgstr ""
msgid "Removes time estimate." msgid "Removes time estimate."
msgstr "" msgstr ""
msgid "Removing a project deletes it immediately, there will be no delay before the project is permanently removed."
msgstr ""
msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanently removed."
msgstr ""
msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanently removed. Are you ABSOLUTELY sure?" msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanently removed. Are you ABSOLUTELY sure?"
msgstr "" msgstr ""
...@@ -24392,6 +24416,15 @@ msgstr "" ...@@ -24392,6 +24416,15 @@ msgstr ""
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention." msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
msgstr "" msgstr ""
msgid "This action cannot be undone. You will lose the project's respository and all conent: issues, merge requests, etc."
msgstr ""
msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all content: issues, merge requests, etc."
msgstr ""
msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}on %{date}%{strongClose}, including its repositories and all content: issues, merge requests, etc."
msgstr ""
msgid "This also resolves all related threads" msgid "This also resolves all related threads"
msgstr "" msgstr ""
...@@ -25851,9 +25884,6 @@ msgstr "" ...@@ -25851,9 +25884,6 @@ msgstr ""
msgid "Until" msgid "Until"
msgstr "" msgstr ""
msgid "Until that time, the project can be restored."
msgstr ""
msgid "Unverified" msgid "Unverified"
msgstr "" msgstr ""
...@@ -27315,6 +27345,9 @@ msgstr "" ...@@ -27315,6 +27345,9 @@ msgstr ""
msgid "Yes, close issue" msgid "Yes, close issue"
msgstr "" msgstr ""
msgid "Yes, delete project"
msgstr ""
msgid "Yes, let me map Google Code users to full names or GitLab users." msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr "" msgstr ""
...@@ -27330,6 +27363,9 @@ msgstr "" ...@@ -27330,6 +27363,9 @@ msgstr ""
msgid "You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application." msgid "You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application."
msgstr "" msgstr ""
msgid "You are about to permanently delete this project"
msgstr ""
msgid "You are about to transfer the control of your account to %{group_name} group. This action is NOT reversible, you won't be able to access any of your groups and projects outside of %{group_name} once this transfer is complete." msgid "You are about to transfer the control of your account to %{group_name} group. This action is NOT reversible, you won't be able to access any of your groups and projects outside of %{group_name} once this transfer is complete."
msgstr "" msgstr ""
...@@ -27480,6 +27516,9 @@ msgstr "" ...@@ -27480,6 +27516,9 @@ msgstr ""
msgid "You can only upload one design when dropping onto an existing design." msgid "You can only upload one design when dropping onto an existing design."
msgstr "" msgstr ""
msgid "You can recover this project until %{date}"
msgstr ""
msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr "" msgstr ""
......
...@@ -260,7 +260,7 @@ RSpec.describe 'Project' do ...@@ -260,7 +260,7 @@ RSpec.describe 'Project' do
end end
it 'removes a project', :sidekiq_might_not_need_inline do it 'removes a project', :sidekiq_might_not_need_inline do
expect { remove_with_confirm('Remove project', project.path) }.to change { Project.count }.by(-1) expect { remove_with_confirm('Remove project', project.path, 'Yes, delete project') }.to change { Project.count }.by(-1)
expect(page).to have_content "Project '#{project.full_name}' is in the process of being deleted." expect(page).to have_content "Project '#{project.full_name}' is in the process of being deleted."
expect(Project.all.count).to be_zero expect(Project.all.count).to be_zero
expect(project.issues).to be_empty expect(project.issues).to be_empty
...@@ -386,9 +386,9 @@ RSpec.describe 'Project' do ...@@ -386,9 +386,9 @@ RSpec.describe 'Project' do
{ form: '.rspec-merge-request-settings', input: '#project_printing_merge_request_link_enabled' }] { form: '.rspec-merge-request-settings', input: '#project_printing_merge_request_link_enabled' }]
end end
def remove_with_confirm(button_text, confirm_with) def remove_with_confirm(button_text, confirm_with, confirm_button_text = 'Confirm')
click_button button_text click_button button_text
fill_in 'confirm_name_input', with: confirm_with fill_in 'confirm_name_input', with: confirm_with
click_button 'Confirm' click_button confirm_button_text
end end
end end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Project remove modal initialized matches the snapshot 1`] = `
<form
action="some/path"
method="post"
>
<input
name="_method"
type="hidden"
value="delete"
/>
<input
name="authenticity_token"
type="hidden"
/>
<gl-button-stub
category="primary"
icon=""
role="button"
size="medium"
tabindex="0"
variant="danger"
>
Remove project
</gl-button-stub>
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
footer-class="gl-bg-gray-10 gl-p-5"
modalclass=""
modalid="fakeUniqueId"
ok-variant="danger"
size="sm"
title-class="gl-text-red-500"
titletag="h4"
>
<div>
<gl-alert-stub
class="gl-mb-5"
dismisslabel="Dismiss"
primarybuttonlink=""
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
title="You are about to permanently delete this project"
variant="danger"
>
<gl-sprintf-stub
message="Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its respositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc."
/>
</gl-alert-stub>
<p>
This action cannot be undone. You will lose the project's respository and all conent: issues, merge requests, etc.
</p>
<p
class="gl-mb-1"
>
Please type the following to confirm:
</p>
<p>
<code>
foo
</code>
</p>
<gl-form-input-stub
id="confirm_name_input"
name="confirm_name_input"
type="text"
/>
</div>
</gl-modal-stub>
</form>
`;
import { shallowMount } from '@vue/test-utils';
import ProjectDeleteButton from '~/projects/components/project_delete_button.vue';
import SharedDeleteButton from '~/projects/components/shared/delete_button.vue';
jest.mock('lodash/uniqueId', () => () => 'fakeUniqueId');
describe('Project remove modal', () => {
let wrapper;
const findSharedDeleteButton = () => wrapper.find(SharedDeleteButton);
const defaultProps = {
confirmPhrase: 'foo',
formPath: 'some/path',
};
const createComponent = (props = {}) => {
wrapper = shallowMount(ProjectDeleteButton, {
propsData: {
...defaultProps,
...props,
},
stubs: {
SharedDeleteButton,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('initialized', () => {
beforeEach(() => {
createComponent();
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('passes confirmPhrase and formPath props to the shared delete button', () => {
expect(findSharedDeleteButton().props()).toEqual(defaultProps);
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Project remove modal initialized matches the snapshot 1`] = ` exports[`Project remove modal intialized matches the snapshot 1`] = `
<form <form
action="some/path" action="some/path"
method="post" method="post"
...@@ -14,37 +14,27 @@ exports[`Project remove modal initialized matches the snapshot 1`] = ` ...@@ -14,37 +14,27 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
<input <input
name="authenticity_token" name="authenticity_token"
type="hidden" type="hidden"
value="test-csrf-token"
/> />
<b-button-stub <gl-button-stub
class="[object Object]" category="primary"
event="click" icon=""
role="button" role="button"
routertag="a" size="medium"
size="md"
tabindex="0" tabindex="0"
tag="button"
type="button"
variant="danger" variant="danger"
> >
<!----> Remove project
</gl-button-stub>
<!---->
<span
class="gl-button-text"
>
Remove project
</span>
</b-button-stub>
<b-modal-stub <b-modal-stub
canceltitle="Cancel" canceltitle="Cancel"
cancelvariant="secondary" cancelvariant="secondary"
footerclass="bg-gray-light gl-p-5" footerclass="gl-bg-gray-10 gl-p-5"
headerclosecontent="&times;" headerclosecontent="&times;"
headercloselabel="Close" headercloselabel="Close"
id="remove-project-modal" id="delete-project-modal-2"
ignoreenforcefocusselector="" ignoreenforcefocusselector=""
lazy="true" lazy="true"
modalclass="gl-modal," modalclass="gl-modal,"
...@@ -52,26 +42,22 @@ exports[`Project remove modal initialized matches the snapshot 1`] = ` ...@@ -52,26 +42,22 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
okvariant="danger" okvariant="danger"
size="sm" size="sm"
title="" title=""
titleclass="gl-text-red-500"
titletag="h4" titletag="h4"
> >
<div> <div>
<p
class="gl-text-red-500 gl-font-weight-bold"
>
This can lead to data loss.
</p>
<p <p
class="gl-mb-0" class="gl-mb-1"
> >
This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention. Please type the following to confirm:
</p> </p>
<p> <p>
<gl-sprintf-stub <code>
message="Please type %{phrase_code} to proceed or close this modal to cancel." foo
/> </code>
</p> </p>
<gl-form-input-stub <gl-form-input-stub
...@@ -79,12 +65,13 @@ exports[`Project remove modal initialized matches the snapshot 1`] = ` ...@@ -79,12 +65,13 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
name="confirm_name_input" name="confirm_name_input"
type="text" type="text"
/> />
</div> </div>
<template /> <template />
<template> <template>
Confirmation required Delete project. Are you ABSOLUTELY SURE?
</template> </template>
<template /> <template />
...@@ -94,32 +81,32 @@ exports[`Project remove modal initialized matches the snapshot 1`] = ` ...@@ -94,32 +81,32 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
<template /> <template />
<template> <template>
<div <gl-button-stub
class="gl-w-full gl-display-flex gl-just-content-start gl-m-0" category="tertiary"
class="js-modal-action-cancel"
icon=""
size="medium"
variant="default"
>
Cancel, keep project
</gl-button-stub>
<!---->
<gl-button-stub
category="tertiary"
class="js-modal-action-primary"
disabled="true"
icon=""
size="medium"
variant="danger"
> >
<b-button-stub
class="[object Object]"
disabled="true"
event="click"
routertag="a"
size="md"
tag="button"
type="button"
variant="danger"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Confirm
</span> Yes, delete project
</b-button-stub>
</div> </gl-button-stub>
</template> </template>
</b-modal-stub> </b-modal-stub>
</form> </form>
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlButton, GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import ProjectRemoveModal from '~/projects/components/remove_modal.vue'; import SharedDeleteButton from '~/projects/components/shared/delete_button.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' }));
describe('Project remove modal', () => { describe('Project remove modal', () => {
let wrapper; let wrapper;
const findFormElement = () => wrapper.find('form').element; const findFormElement = () => wrapper.find('form');
const findConfirmButton = () => wrapper.find(GlModal).find(GlButton); const findConfirmButton = () => wrapper.find('.js-modal-action-primary');
const findAuthenticityTokenInput = () => findFormElement().find('input[name=authenticity_token]');
const findModal = () => wrapper.find(GlModal);
const defaultProps = { const defaultProps = {
formPath: 'some/path',
confirmPhrase: 'foo', confirmPhrase: 'foo',
warningMessage: 'This can lead to data loss.', formPath: 'some/path',
}; };
const createComponent = (data = {}) => { const createComponent = (data = {}) => {
wrapper = shallowMount(ProjectRemoveModal, { wrapper = shallowMount(SharedDeleteButton, {
propsData: defaultProps, propsData: defaultProps,
data: () => data, data: () => data,
stubs: { stubs: {
GlButton,
GlModal, GlModal,
}, },
}); });
...@@ -30,7 +32,7 @@ describe('Project remove modal', () => { ...@@ -30,7 +32,7 @@ describe('Project remove modal', () => {
wrapper = null; wrapper = null;
}); });
describe('initialized', () => { describe('intialized', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
}); });
...@@ -38,25 +40,44 @@ describe('Project remove modal', () => { ...@@ -38,25 +40,44 @@ describe('Project remove modal', () => {
it('matches the snapshot', () => { it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it('sets a csrf token on the authenticity form input', () => {
expect(findAuthenticityTokenInput().element.value).toEqual('test-csrf-token');
});
it('sets the form action to the provided path', () => {
expect(findFormElement().attributes('action')).toEqual(defaultProps.formPath);
});
}); });
describe('user input matches the confirmPhrase', () => { describe('when the user input does not match the confirmPhrase', () => {
beforeEach(() => {
createComponent({ userInput: 'bar' });
});
it('the confirm button is disabled', () => {
expect(findConfirmButton().attributes('disabled')).toBe('true');
});
});
describe('when the user input matches the confirmPhrase', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ userInput: defaultProps.confirmPhrase }); createComponent({ userInput: defaultProps.confirmPhrase });
}); });
it('the confirm button is not dislabled', () => { it('the confirm button is not disabled', () => {
expect(findConfirmButton().attributes('disabled')).toBe(undefined); expect(findConfirmButton().attributes('disabled')).toBe(undefined);
}); });
});
describe('and when the confirmation button is clicked', () => { describe('when the modal is confirmed', () => {
beforeEach(() => { beforeEach(() => {
findConfirmButton().vm.$emit('click'); createComponent();
}); findModal().vm.$emit('ok');
});
it('submits the form element', () => { it('submits the form element', () => {
expect(findFormElement().submit).toHaveBeenCalled(); expect(findFormElement().element.submit).toHaveBeenCalled();
});
}); });
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment