From 2a257a32f04562b1238359333438917e5f78fe8c Mon Sep 17 00:00:00 2001 From: Shane Maglangit Date: Thu, 17 Jul 2025 18:41:12 +0800 Subject: [PATCH 1/2] PoC --- app/assets/javascripts/api/groups_api.js | 14 ++ app/assets/javascripts/api/projects_api.js | 14 ++ .../archive/components/archive_settings.vue | 97 +++++++++++++ .../groups_projects/archive/index.js | 15 ++ .../components/archive_modal.vue | 89 ++++++++++++ .../components/unarchive_modal.vue | 89 ++++++++++++ .../javascripts/groups_projects/constants.js | 2 + .../components/unarchive_settings.vue | 132 ++++++++++++++++++ .../groups_projects/unarchive/index.js | 15 ++ .../javascripts/pages/groups/edit/index.js | 4 + .../javascripts/pages/projects/edit/index.js | 5 + app/helpers/groups_helper.rb | 17 +++ app/helpers/projects_helper.rb | 19 +++ app/views/groups/settings/_advanced.html.haml | 1 + app/views/groups/settings/_archive.html.haml | 6 + .../projects/settings/_archive.html.haml | 24 +--- locale/gitlab.pot | 47 +++++-- 17 files changed, 557 insertions(+), 33 deletions(-) create mode 100644 app/assets/javascripts/groups_projects/archive/components/archive_settings.vue create mode 100644 app/assets/javascripts/groups_projects/archive/index.js create mode 100644 app/assets/javascripts/groups_projects/components/archive_modal.vue create mode 100644 app/assets/javascripts/groups_projects/components/unarchive_modal.vue create mode 100644 app/assets/javascripts/groups_projects/unarchive/components/unarchive_settings.vue create mode 100644 app/assets/javascripts/groups_projects/unarchive/index.js create mode 100644 app/views/groups/settings/_archive.html.haml diff --git a/app/assets/javascripts/api/groups_api.js b/app/assets/javascripts/api/groups_api.js index 01f86fb4dae0c4..e15bcca0c45bd9 100644 --- a/app/assets/javascripts/api/groups_api.js +++ b/app/assets/javascripts/api/groups_api.js @@ -4,6 +4,8 @@ import { buildApiUrl } from './api_utils'; const GROUP_PATH = '/api/:version/groups/:id'; const GROUP_RESTORE_PATH = '/api/:version/groups/:id/restore'; +const GROUP_ARCHIVE_PATH = '/api/:version/groups/:id/archive'; +const GROUP_UNARCHIVE_PATH = '/api/:version/groups/:id/unarchive'; const GROUPS_PATH = '/api/:version/groups.json'; const GROUP_MEMBERS_PATH = '/api/:version/groups/:id/members'; const GROUP_MEMBER_PATH = '/api/:version/groups/:id/members/:user_id'; @@ -66,6 +68,18 @@ export function restoreGroup(groupId) { return axios.post(url); } +export function archiveGroup(groupId) { + const url = buildApiUrl(GROUP_ARCHIVE_PATH).replace(':id', groupId); + + return axios.post(url); +} + +export function unarchiveGroup(groupId) { + const url = buildApiUrl(GROUP_UNARCHIVE_PATH).replace(':id', groupId); + + return axios.post(url); +} + export const getGroupTransferLocations = (groupId, params = {}) => { const url = buildApiUrl(GROUP_TRANSFER_LOCATIONS_PATH).replace(':id', groupId); const defaultParams = { per_page: DEFAULT_PER_PAGE }; diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js index aa3e2f7f59a8d5..bebf4bfc2f61e0 100644 --- a/app/assets/javascripts/api/projects_api.js +++ b/app/assets/javascripts/api/projects_api.js @@ -12,6 +12,8 @@ const PROJECT_TRANSFER_LOCATIONS_PATH = 'api/:version/projects/:id/transfer_loca const PROJECT_SHARE_LOCATIONS_PATH = 'api/:version/projects/:id/share_locations'; const PROJECT_UPLOADS_PATH = '/api/:version/projects/:id/uploads'; const PROJECT_RESTORE_PATH = '/api/:version/projects/:id/restore'; +const PROJECT_ARCHIVE_PATH = '/api/:version/projects/:id/archive'; +const PROJECT_UNARCHIVE_PATH = '/api/:version/projects/:id/unarchive'; export function getProjects(query, options, callback = () => {}) { const url = buildApiUrl(PROJECTS_PATH); @@ -64,6 +66,18 @@ export function restoreProject(projectId) { return axios.post(url); } +export function archiveProject(projectId) { + const url = buildApiUrl(PROJECT_ARCHIVE_PATH).replace(':id', projectId); + + return axios.post(url); +} + +export function unarchiveProject(projectId) { + const url = buildApiUrl(PROJECT_UNARCHIVE_PATH).replace(':id', projectId); + + return axios.post(url); +} + export function importProjectMembers(sourceId, targetId) { const url = buildApiUrl(PROJECT_IMPORT_MEMBERS_PATH) .replace(':id', sourceId) diff --git a/app/assets/javascripts/groups_projects/archive/components/archive_settings.vue b/app/assets/javascripts/groups_projects/archive/components/archive_settings.vue new file mode 100644 index 00000000000000..0723babd3cd987 --- /dev/null +++ b/app/assets/javascripts/groups_projects/archive/components/archive_settings.vue @@ -0,0 +1,97 @@ + + + diff --git a/app/assets/javascripts/groups_projects/archive/index.js b/app/assets/javascripts/groups_projects/archive/index.js new file mode 100644 index 00000000000000..c71fa033be2922 --- /dev/null +++ b/app/assets/javascripts/groups_projects/archive/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import ArchiveSettings from '~/groups_projects/archive/components/archive_settings.vue'; + +export default function initArchiveSettings() { + const el = document.getElementById('js-archive-settings'); + if (!el) return null; + + return new Vue({ + el, + name: 'ArchiveSettingsRoot', + render(createElement) { + return createElement(ArchiveSettings, { props: el.dataset }); + }, + }); +} diff --git a/app/assets/javascripts/groups_projects/components/archive_modal.vue b/app/assets/javascripts/groups_projects/components/archive_modal.vue new file mode 100644 index 00000000000000..ed888dd6a5d3ce --- /dev/null +++ b/app/assets/javascripts/groups_projects/components/archive_modal.vue @@ -0,0 +1,89 @@ + + + diff --git a/app/assets/javascripts/groups_projects/components/unarchive_modal.vue b/app/assets/javascripts/groups_projects/components/unarchive_modal.vue new file mode 100644 index 00000000000000..b6acc317e55883 --- /dev/null +++ b/app/assets/javascripts/groups_projects/components/unarchive_modal.vue @@ -0,0 +1,89 @@ + + + diff --git a/app/assets/javascripts/groups_projects/constants.js b/app/assets/javascripts/groups_projects/constants.js index faad11f8691735..eb6bd0893629ea 100644 --- a/app/assets/javascripts/groups_projects/constants.js +++ b/app/assets/javascripts/groups_projects/constants.js @@ -1,5 +1,7 @@ import { __ } from '~/locale'; +export const RESOURCE_TYPES = ['group', 'project']; + export const SORT_LABEL_NAME = __('Name'); export const SORT_LABEL_CREATED = __('Created date'); export const SORT_LABEL_UPDATED = __('Updated date'); diff --git a/app/assets/javascripts/groups_projects/unarchive/components/unarchive_settings.vue b/app/assets/javascripts/groups_projects/unarchive/components/unarchive_settings.vue new file mode 100644 index 00000000000000..635ba08ee1589f --- /dev/null +++ b/app/assets/javascripts/groups_projects/unarchive/components/unarchive_settings.vue @@ -0,0 +1,132 @@ + + + diff --git a/app/assets/javascripts/groups_projects/unarchive/index.js b/app/assets/javascripts/groups_projects/unarchive/index.js new file mode 100644 index 00000000000000..681cb0cbbb4dbf --- /dev/null +++ b/app/assets/javascripts/groups_projects/unarchive/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import UnarchiveSettings from '~/groups_projects/unarchive/components/unarchive_settings.vue'; + +export default function initUnarchiveSettings() { + const el = document.getElementById('js-unarchive-settings'); + if (!el) return null; + + return new Vue({ + el, + name: 'UnarchiveSettingsRoot', + render(createElement) { + return createElement(UnarchiveSettings, { props: el.dataset }); + }, + }); +} diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 47d1741cf2d6f1..7f22f7f7f0bb75 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -10,6 +10,8 @@ import initSearchSettings from '~/search_settings'; import initSettingsPanels from '~/settings_panels'; import initConfirmDanger from '~/init_confirm_danger'; import { initGroupSettingsReadme } from '~/groups/settings/init_group_settings_readme'; +import initArchiveSettings from '~/groups_projects/archive'; +import initUnarchiveSettings from '~/groups_projects/unarchive'; initFilePickers(); initConfirmDanger(); @@ -28,3 +30,5 @@ initSearchSettings(); initCascadingSettingsLockTooltips(); initGroupSettingsReadme(); +initArchiveSettings(); +initUnarchiveSettings(); diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 235ed07ba53686..eff5716e097719 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -13,6 +13,8 @@ import UserCallout from '~/user_callout'; import initTopicsTokenSelector from '~/projects/settings/topics'; import { initProjectSelects } from '~/vue_shared/components/entity_select/init_project_selects'; import initPruneObjectsButton from '~/projects/prune_objects_button'; +import initArchiveSettings from '~/groups_projects/archive'; +import initUnarchiveSettings from '~/groups_projects/unarchive'; import initProjectPermissionsSettings from '../shared/permissions'; import initGitlabDuoSettings from '../shared/permissions/gitlab_duo_settings'; import initProjectLoadingSpinner from '../shared/save_project_loader'; @@ -38,3 +40,6 @@ dirtySubmitFactory(document.querySelectorAll('.js-general-settings-form, .js-mr- initSearchSettings(); initTopicsTokenSelector(); initProjectSelects(); + +initArchiveSettings(); +initUnarchiveSettings(); diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index a96690afa6fa71..ecd070b068eec8 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -170,6 +170,23 @@ def group_settings_readme_app_data(group) } end + def group_archive_settings_app_data(group) + { + resource_type: 'group', + resource_id: group.id, + resource_path: group_path(group) + } + end + + def group_unarchive_settings_app_data(group) + { + resource_type: 'group', + resource_id: group.id, + resource_path: group_path(group), + ancestors_archived: group.ancestors_archived? + } + end + def enabled_git_access_protocol_options_for_group case ::Gitlab::CurrentSettings.enabled_git_access_protocol when nil, "" diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a47081a6a8608b..ac8710132c825b 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -701,6 +701,25 @@ def issue_manual_ordering_class 'manual-ordering' end + def project_archive_settings_app_data(project) + { + resource_type: 'project', + resource_id: project.id, + resource_path: project_path(project), + help_path: help_page_path('user/project/working_with_projects.md', anchor: 'archive-a-project') + } + end + + def project_unarchive_settings_app_data(project) + { + resource_type: 'project', + resource_id: project.id, + resource_path: project_path(project), + ancestors_archived: project.ancestors_archived?, + help_path: help_page_path('user/project/working_with_projects.md', anchor: 'unarchive-a-project') + } + end + def projects_filtered_search_and_sort_app_data { initial_sort: project_list_sort_by, diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml index efbeaf9ea5b947..7e6d58cc851b3d 100644 --- a/app/views/groups/settings/_advanced.html.haml +++ b/app/views/groups/settings/_advanced.html.haml @@ -2,6 +2,7 @@ .gl-flex.gl-gap-5.gl-flex-col = render 'groups/settings/export', group: @group + = render 'groups/settings/archive', group: @group = render Pajamas::CardComponent.new(header_options: { class: 'gl-px-5 gl-py-4 gl-border-b-1 gl-border-b-solid gl-border-default' }, body_options: { class: 'gl-px-5 gl-py-4' }) do |c| - c.with_header do diff --git a/app/views/groups/settings/_archive.html.haml b/app/views/groups/settings/_archive.html.haml new file mode 100644 index 00000000000000..0126f99ea70aa3 --- /dev/null +++ b/app/views/groups/settings/_archive.html.haml @@ -0,0 +1,6 @@ +- return unless Feature.enabled?(:archive_group, group) + +- if group.self_or_ancestors_archived? + #js-unarchive-settings{ data: group_unarchive_settings_app_data(group) } +- else + #js-archive-settings{ data: group_archive_settings_app_data(group) } diff --git a/app/views/projects/settings/_archive.html.haml b/app/views/projects/settings/_archive.html.haml index cde601533a999e..fea8664ecb4177 100644 --- a/app/views/projects/settings/_archive.html.haml +++ b/app/views/projects/settings/_archive.html.haml @@ -1,22 +1,6 @@ - return unless archiving_available?(@project) -= render Pajamas::CardComponent.new do |c| - - c.with_header do - .gl-flex.gl-grow - %h4.gl-text-base.gl-leading-24.gl-m-0 - - if @project.archived? - = _('Unarchive project') - - else - = _('Archive project') - - - c.with_body do - - if @project.archived? - - link_start = ''.html_safe % { url: help_page_path('user/project/working_with_projects.md', anchor: 'unarchive-a-project') } - %p= _("Unarchiving the project restores its members' ability to make commits, and create issues, comments, and other entities. %{strong_start}After you unarchive the project, it displays in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: ''.html_safe, strong_end: ''.html_safe, link_start: link_start, link_end: ''.html_safe } - = render Pajamas::ButtonComponent.new(method: :post, href: unarchive_project_path(@project), variant: :confirm, button_options: { aria: { label: _('Unarchive project') }, data: { confirm: _("Are you sure that you want to unarchive this project?"), testid: 'unarchive-project-link' } }) do - = _('Unarchive project') - - else - - link_start = ''.html_safe % { url: help_page_path('user/project/working_with_projects.md', anchor: 'archive-a-project') } - %p= _("Archiving the project makes it entirely read-only. It is hidden from the dashboard and doesn't display in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: ''.html_safe, strong_end: ''.html_safe, link_start: link_start, link_end: ''.html_safe } - = render Pajamas::ButtonComponent.new(method: :post, href: archive_project_path(@project), variant: :confirm, button_options: { aria: { label: _('Archive project') }, data: { confirm: _("Are you sure that you want to archive this project?"), testid: 'archive-project-link', 'confirm-btn-variant': 'confirm' } }) do - = _('Archive project') +- if @project.self_or_ancestors_archived? + #js-unarchive-settings{ data: project_unarchive_settings_app_data(@project) } +- else + #js-archive-settings{ data: project_archive_settings_app_data(@project) } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 00202bd4f49e8b..9c90f09dd1c267 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1339,6 +1339,9 @@ msgid_plural "%{resolvedDiscussionsCount} of %{resolvableDiscussionsCount} threa msgstr[0] "" msgstr[1] "" +msgid "%{resourceType} has successfully been restored to an active state. You'll are now able to modify its contents and settings again." +msgstr "" + msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}." msgstr "" @@ -7213,6 +7216,9 @@ msgstr "" msgid "An error occurred while adding formatted title for epic" msgstr "" +msgid "An error occurred while archiving the %{resourceType}. Please refresh the page an try again." +msgstr "" + msgid "An error occurred while checking group path. Please refresh and try again." msgstr "" @@ -7501,6 +7507,9 @@ msgstr "" msgid "An error occurred while trying to update the registry: '%{error_message}'." msgstr "" +msgid "An error occurred while unarchiving the %{resourceType}. Please refresh the page an try again." +msgstr "" + msgid "An error occurred while updating approvers" msgstr "" @@ -8634,10 +8643,10 @@ msgstr "" msgid "Archive" msgstr "" -msgid "Archive pipelines" +msgid "Archive %{resourceType}" msgstr "" -msgid "Archive project" +msgid "Archive pipelines" msgstr "" msgid "Archived" @@ -8652,27 +8661,21 @@ msgstr "" msgid "Archiving projects" msgstr "" -msgid "Archiving the project makes it entirely read-only. It is hidden from the dashboard and doesn't display in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}" -msgstr "" - msgid "Are you absolutely sure?" msgstr "" -msgid "Are you sure that you want to archive this project?" -msgstr "" - msgid "Are you sure that you want to destroy %{application}" msgstr "" -msgid "Are you sure that you want to unarchive this project?" -msgstr "" - msgid "Are you sure you want to %{action} %{name}?" msgstr "" msgid "Are you sure you want to approve %{user}?" msgstr "" +msgid "Are you sure you want to archive this %{resourceType}" +msgstr "" + msgid "Are you sure you want to attempt to merge?" msgstr "" @@ -8831,6 +8834,9 @@ msgstr "" msgid "Are you sure you want to stop this environment?" msgstr "" +msgid "Are you sure you want to unarchive this %{resourceType}" +msgstr "" + msgid "Are you sure you want to unlock %{path_lock_path}?" msgstr "" @@ -31920,6 +31926,9 @@ msgstr "" msgid "How can I make my variables more secure?" msgstr "" +msgid "How do I archive a %{resourceType}?" +msgstr "" + msgid "How do I configure Akismet?" msgstr "" @@ -31950,6 +31959,9 @@ msgstr "" msgid "How do I set up this service?" msgstr "" +msgid "How do I unarchive a %{resourceType}?" +msgstr "" + msgid "How do I use a web terminal?" msgstr "" @@ -37656,6 +37668,9 @@ msgstr "" msgid "Make sure you trust %{strong_start}%{client_name}%{strong_end} before authorizing." msgstr "" +msgid "Make your %{resourceType} read-only while preserving all data and access to its repository, issues, and merge requests." +msgstr "" + msgid "Makes this %{type} confidential." msgstr "" @@ -52765,6 +52780,9 @@ msgstr "" msgid "Restore project" msgstr "" +msgid "Restore your %{resourceType} to an active state. You'll be able to modify its content and settings again." +msgstr "" + msgid "Restoring projects" msgstr "" @@ -65130,6 +65148,9 @@ msgstr "" msgid "To submit your changes in a merge request, switch to one of these forks or create a new fork." msgstr "" +msgid "To unarchive this %{resourceType}, you must unarchive its parent group." +msgstr "" + msgid "To unsubscribe from this issue, please paste the following link into your browser:" msgstr "" @@ -66450,10 +66471,10 @@ msgstr "" msgid "Unapproved the current merge request." msgstr "" -msgid "Unarchive project" +msgid "Unarchive" msgstr "" -msgid "Unarchiving the project restores its members' ability to make commits, and create issues, comments, and other entities. %{strong_start}After you unarchive the project, it displays in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}" +msgid "Unarchive %{resourceType}" msgstr "" msgid "Unassign all" -- GitLab From ccab488272020b6f30953aad58c0d55d59cb0204 Mon Sep 17 00:00:00 2001 From: Shane Maglangit Date: Fri, 18 Jul 2025 09:24:46 +0800 Subject: [PATCH 2/2] PoC --- .../javascripts/groups_projects/components/archive_modal.vue | 1 + .../javascripts/groups_projects/components/unarchive_modal.vue | 1 + 2 files changed, 2 insertions(+) diff --git a/app/assets/javascripts/groups_projects/components/archive_modal.vue b/app/assets/javascripts/groups_projects/components/archive_modal.vue index ed888dd6a5d3ce..bc2eba301091ea 100644 --- a/app/assets/javascripts/groups_projects/components/archive_modal.vue +++ b/app/assets/javascripts/groups_projects/components/archive_modal.vue @@ -78,6 +78,7 @@ export default {