diff --git a/.github/workflows/nmc-custom-build.yml b/.github/workflows/nmc-custom-build.yml new file mode 100644 index 0000000000000..e9a3dae3b2222 --- /dev/null +++ b/.github/workflows/nmc-custom-build.yml @@ -0,0 +1,172 @@ +name: MagentaCLOUD custom build strategy + +# Test call: act --container-architecture linux/amd64 --secret-file ../secrets.env --env-file ../nmc-master-build.env -j build-custom + +# we already try to build a custom release as soon as people +# created a customisation PR +on: + workflow_dispatch: + +env: + TARGET_TRUNK: "master" + TARGET_STABLE: "nmcstable/25.0.6" + CUSTOM_REPO: ${{ github.repository }} + CUSTOM_BRANCH: ${{ github.ref }} + BUILD_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + + +jobs: + build-custom: + runs-on: ubuntu-latest + steps: + + - name: "Find customisation candidates" + uses: octokit/graphql-action@v2.x + id: find_customisations + env: + GITHUB_TOKEN: ${{ env.BUILD_TOKEN }} + with: + query: | + query findCustomisations($searchexpr: String!) { + search(query: $searchexpr, type: ISSUE, first: 100) { + edges { + node { + ... on PullRequest { + state + number + title + baseRefName + headRefName + mergeable + isDraft + url + } + } + } + } + } + searchexpr: "type:pr state:open repo:${{ env.CUSTOM_REPO }} base:${{ env.TARGET_TRUNK }} base:${{ env.TARGET_STABLE }} label:custom label:build-ready" + # note that the search has OR semantice for base:, but AND semantice for label: ! + # see: https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests#search-by-label + - id: customisations + run: | + pulls=$(echo '${{ steps.find_customisations.outputs.data }}' | jq -s '.[].search.edges | map(.node) | sort_by(.headRefName)') + echo ::set-output name=pulls::$pulls + + - name: Picking backports + id: pickbackports + uses: actions/github-script@v6 + env: + customisations: ${{ steps.customisations.outputs.pulls }} + target_trunk: ${{ env.TARGET_TRUNK }} + target_stable: ${{ env.TARGET_STABLE }} + with: + script: | + const customisations = JSON.parse(process.env.customisations); + const target_trunk = process.env.target_trunk; + const target_stable = process.env.target_stable; + + function shuffleArray(array) { + return array.reduce((acc, current, index) => { + const randomIndex = Math.floor(Math.random() * (index + 1)); + [acc[index], acc[randomIndex]] = [acc[randomIndex], acc[index]]; + return acc; + }, [...array]); + } + + function isBackportFor(port, master) { + if (( port.baseRefName === target_stable ) && + ( master.baseRefName === target_trunk ) && + ( port.headRefName.startsWith( master.headRefName ))) { + return true; + } else { + return false; + } + } + + if ( target_stable == target_trunk ) { + // only sort out backports it it is not a master build + // which means that the target stable branch is not the name for the trunk branch + core.setOutput('buildparts', JSON.stringify(shuffleArray(customisations)) ); + core.setOutput('newerparts', JSON.stringify([]) ); + return ""; + } + + var buildparts = []; + var newerparts =[]; + + for (cIdx=0; cIdx < customisations.length; cIdx++) { + if (cIdx+1 < customisations.length) { + // detect master - backport pairs + if (isBackportFor( customisations[cIdx], customisations[cIdx+1] )) { + buildparts.push(customisations[cIdx]); + newerparts.push(customisations[cIdx+1]); + cIdx++; + } else if (isBackportFor( customisations[cIdx+1], customisations[cIdx] )) { + buildparts.push(customisations[cIdx+1]); + newerparts.push(customisations[cIdx]); + cIdx++; + } else { + // handle as single entry + buildparts.push(customisations[cIdx]); + } + } else { + // handle as last entry + buildparts.push(customisations[cIdx]); + } + } + core.setOutput('buildparts', JSON.stringify(shuffleArray(buildparts)) ); + core.setOutput('newerparts', JSON.stringify(shuffleArray(newerparts)) ); + return ""; + + # we exclude non-mergeable branches and exclude them from build + - name: Check mergeability + id: checkmergeable + uses: actions/github-script@v6 + env: + buildparts: ${{ steps.pickbackports.outputs.buildparts }} + with: + script: | + const buildparts = JSON.parse(process.env.buildparts); + var mergeableparts = []; + result = 0; + + buildparts.forEach( (buildpart) => { + if ( buildpart.mergeable === 'MERGEABLE' ) { + mergeableparts.push(buildpart); + } else { + notMergeableError = `${buildpart.mergeable} #${buildpart.number}: Incomplete package, lacking '${buildpart.title}'! ` + core.error(notMergeableError); + result++; + } + }); + core.setOutput('mergeableparts', JSON.stringify(mergeableparts) ); + return result; + + - name: Checkout build target branch + uses: actions/checkout@v3 + with: + repository: ${{ env.CUSTOM_REPO }} + ref: ${{ env.CUSTOM_BRANCH }} + fetch-depth: 1 + token: ${{ secrets.BUILD_TOKEN }} + - run: | + git rebase --onto ${{ env.TARGET_STABLE }} ${{ env.CUSTOM_BRANCH }} + + # todo do merge trials for all newerparts + # if one successfully merges with stable branch + # it is a potential candidate to replace a backport + # with a master patch + + - name: Custom merges + id: custommerge + run: + echo ::debug::Merging for '${{ env.TARGET_STABLE }}' on '${{ env.CUSTOM_BRANCH }}' + for mergepull in $(echo '${{ steps.checkmergeable.outputs.mergeableparts }}' | jq -r '.[]') + do + head=$(echo $mergepull | jq -r '.headRefName') + base=$(echo $mergepull | jq -r '.baseRefName') + echo "::group::$head(type:$base)--->$TARGET_STABLE" + echo "::endgroup::" + done \ No newline at end of file diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index e3052ea9fe85b..44b4b7dfbef5c 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -376,6 +376,9 @@ this.$el.on('show', this._onResize); + this.resizeFileActionMenu = _.debounce(_.bind(this.resizeFileActionMenu, this), 250); + $(window).resize(this.resizeFileActionMenu); + // reload files list on share accept $('body').on('OCA.Notification.Action', function(eventObject) { if (eventObject.notification.app === 'files_sharing' && eventObject.action.type === 'POST') { @@ -677,6 +680,7 @@ * @param {boolean} [show=true] whether to open the sidebar if it was closed */ _updateDetailsView: function(fileName, show) { + this.resizeFileActionMenu(); if (!(OCA.Files && OCA.Files.Sidebar)) { console.error('No sidebar available'); return; @@ -1501,6 +1505,12 @@ this.fileMultiSelectMenu.render(); this.$el.find('.selectedActions .filesSelectMenu').remove(); this.$el.find('.selectedActions').append(this.fileMultiSelectMenu.$el); + this.fileMultipleSelectionMenu = new OCA.Files.FileMultipleSelectionMenu(this.multiSelectMenuItems.sort(function(a, b) { + return a.order - b.order + })); + this.fileMultipleSelectionMenu.render(); + this.$el.find('.selectedActions .filesSelectionMenu').remove(); + this.$el.find('.selectedActions').append(this.fileMultipleSelectionMenu.$el); }, /** @@ -3501,6 +3511,72 @@ } }, + /** + * Show or hide file action menu based on the current selection + */ + resizeFileActionMenu: function() { + const appList = this.$el.find('.filesSelectionMenu ul li:not(.hidden-action)'); + const appListWidth = 179; + const checkWidth = Math.ceil(this.$el.find('.column-selection').outerWidth()); + const headerNameWidth = Math.ceil(this.$el.find('.column-name').outerWidth()); + const actionWidth = Math.ceil(this.$el.find('#selectedActionLabel').outerWidth()); + const allLabelWidth = Math.ceil(this.$el.find('#allLabel').not('#allLabel:hidden').outerWidth()); + let headerWidth = Math.ceil(this.$el.find('.files-filestable thead').outerWidth()); + + if($('#app-sidebar-vue').length>0){ + headerWidth = headerWidth - Math.ceil($('#app-sidebar-vue').outerWidth()); + } + + var availableWidth; + if(!allLabelWidth){ + availableWidth = headerWidth - (checkWidth + headerNameWidth); + } + else{ + availableWidth = headerWidth - (checkWidth + allLabelWidth+ headerNameWidth); + } + + let appCount = Math.floor((availableWidth / appListWidth)); + + if(appCount < appList.length) { + + if(!allLabelWidth){ + availableWidth = headerWidth - (checkWidth + headerNameWidth + actionWidth); + } + else{ + availableWidth = headerWidth - (checkWidth + allLabelWidth+ headerNameWidth + actionWidth); + } + appCount = Math.floor((availableWidth / appListWidth)); + } + + var summary = this._selectionSummary.summary; + if (summary.totalFiles === 0 && summary.totalDirs === 0) { + this.$el.find('#selectedActionLabel').css('display','none'); + } + else{ + if(appCount < appList.length) { + this.$el.find('#selectedActionLabel').css('display','block'); + } + else if(appCount == appList.length){ + this.$el.find('#selectedActionLabel').css('display','none'); + } + else if (!isFinite(appCount)) + { + this.$el.find('#selectedActionLabel').css('display','block'); + } + else if(appCount > appList.length){ + this.$el.find('#selectedActionLabel').css('display','none'); + } + } + + for (let k = 0; k < appList.length; k++) { + if (k < appCount) { + $(appList[k]).removeClass('hidden'); + } else { + $(appList[k]).addClass('hidden'); + } + } + }, + /** * Check whether all selected files are copiable */ diff --git a/apps/files/js/filemultipleselectionmenu.js b/apps/files/js/filemultipleselectionmenu.js new file mode 100644 index 0000000000000..fd42a21dbb71b --- /dev/null +++ b/apps/files/js/filemultipleselectionmenu.js @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2018 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + var FileMultipleSelectionMenu = OC.Backbone.View.extend({ + tagName: 'div', + className: 'filesSelectionMenu', + _scopes: null, + initialize: function(menuItems) { + this._scopes = menuItems; + }, + events: { + 'click a.action': '_onClickAction' + }, + + /** + * Renders the menu with the currently set items + */ + render: function() { + this.$el.html(OCA.Files.Templates['filemultiselectmenu']({ + items: this._scopes + })); + }, + /** + * Displays the menu under the given element + * + * @param {OCA.Files.FileActionContext} context context + * @param {Object} $trigger trigger element + */ + show: function(context) { + this._context = context; + return false; + }, + toggleItemVisibility: function (itemName, show) { + var toggle= $('.filesSelectionMenu'); + if (show) { + toggle.find('.item-' + itemName).removeClass('hidden-action'); + } else { + toggle.find('.item-' + itemName).addClass('hidden-action'); + } + }, + updateItemText: function (itemName, translation) { + this.$el.find('.item-' + itemName).find('.label').text(translation); + }, + toggleLoading: function (itemName, showLoading) { + var $actionElement = this.$el.find('.item-' + itemName); + if ($actionElement.length === 0) { + return; + } + var $icon = $actionElement.find('.icon'); + if (showLoading) { + var $loadingIcon = $(''); + $icon.after($loadingIcon); + $icon.addClass('hidden'); + $actionElement.addClass('disabled'); + } else { + $actionElement.find('.icon-loading-small').remove(); + $actionElement.find('.icon').removeClass('hidden'); + $actionElement.removeClass('disabled'); + } + }, + isDisabled: function (itemName) { + var $actionElement = this.$el.find('.item-' + itemName); + return $actionElement.hasClass('disabled'); + }, + /** + * Event handler whenever an action has been clicked within the menu + * + * @param {Object} event event object + */ + _onClickAction: function (event) { + var $target = $(event.currentTarget); + if (!$target.hasClass('menuitem')) { + $target = $target.closest('.menuitem'); + } + + OC.hideMenus(); + this._context.multiSelectMenuClick(event, $target.data('action')); + return false; + } + }); + + OCA.Files.FileMultipleSelectionMenu = FileMultipleSelectionMenu; +})(OC, OCA); diff --git a/apps/files/js/filemultiselectmenu.js b/apps/files/js/filemultiselectmenu.js index d50fe28eaceb9..5474fa8887323 100644 --- a/apps/files/js/filemultiselectmenu.js +++ b/apps/files/js/filemultiselectmenu.js @@ -11,7 +11,7 @@ (function() { var FileMultiSelectMenu = OC.Backbone.View.extend({ tagName: 'div', - className: 'filesSelectMenu popovermenu bubble menu-center', + className: 'filesSelectMenu popovermenu bubble menu-right', _scopes: null, initialize: function(menuItems) { this._scopes = menuItems; @@ -37,11 +37,6 @@ show: function(context) { this._context = context; this.$el.removeClass('hidden'); - if (window.innerWidth < 480) { - this.$el.removeClass('menu-center').addClass('menu-right'); - } else { - this.$el.removeClass('menu-right').addClass('menu-center'); - } OC.showMenu(null, this.$el); return false; },