21128: Merge branch 'main' into 21128-toolbar-context-menu
authorLisa Knox <lisaknox83@gmail.com>
Mon, 18 Dec 2023 16:42:25 +0000 (11:42 -0500)
committerLisa Knox <lisaknox83@gmail.com>
Mon, 18 Dec 2023 19:06:57 +0000 (14:06 -0500)
refs #21128

Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii.com>

44 files changed:
.yarn/releases/yarn-3.2.0.cjs
cypress/integration/collection.spec.js
cypress/integration/multiselect-toolbar.spec.js [new file with mode: 0644]
cypress/integration/process.spec.js
cypress/integration/project.spec.js
cypress/integration/workflow.spec.js
src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-table.tsx
src/components/icon/icon.tsx
src/components/multiselect-toolbar/MultiselectToolbar.tsx
src/components/multiselect-toolbar/ms-kind-action-differentiator.ts
src/components/multiselect-toolbar/ms-toolbar-action-filters.ts
src/store/collections/collection-info-actions.ts
src/store/favorites/favorites-actions.ts
src/store/multiselect/multiselect-actions.tsx
src/store/multiselect/multiselect-reducer.tsx
src/store/process-panel/process-panel-actions.ts
src/store/project-panel/project-panel-middleware-service.ts
src/store/projects/project-lock-actions.ts
src/store/public-favorites/public-favorites-actions.ts
src/store/trash-panel/trash-panel-middleware-service.ts
src/store/trash/trash-actions.ts
src/store/workbench/workbench-actions.ts
src/views-components/context-menu/action-sets/process-resource-action-set.ts
src/views-components/context-menu/context-menu.tsx
src/views-components/data-explorer/data-explorer.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/details-panel/details-panel.tsx
src/views-components/multiselect-toolbar/ms-collection-action-set.ts
src/views-components/multiselect-toolbar/ms-menu-actions.ts [new file with mode: 0644]
src/views-components/multiselect-toolbar/ms-process-action-set.ts
src/views-components/multiselect-toolbar/ms-project-action-set.ts
src/views-components/multiselect-toolbar/ms-workflow-action-set.ts [new file with mode: 0644]
src/views/all-processes-panel/all-processes-panel.tsx
src/views/collection-panel/collection-panel.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/process-panel/process-details-attributes.tsx
src/views/project-panel/project-panel.tsx
src/views/public-favorites-panel/public-favorites-panel.tsx
src/views/search-results-panel/search-results-panel.tsx
src/views/shared-with-me-panel/shared-with-me-panel.tsx
src/views/trash-panel/trash-panel.tsx
src/views/workbench/workbench.tsx

index 59267757f98a302a96d4ff8f5ccd3e48916a930a..b30d0655d004fa0f15cda007116aa609eb7353a1 100755 (executable)
@@ -470,7 +470,7 @@ Try running the command again with the package name prefixed: ${ae.pretty(e,"yar
       This command will unset a configuration setting.
     `,examples:[["Unset a simple configuration setting","yarn config unset initScope"],["Unset a complex configuration setting","yarn config unset packageExtensions"],["Unset a nested configuration setting","yarn config unset npmScopes.company.npmRegistryServer"]]});var uae=Am;var KN=ge(require("util")),lm=class extends Le{constructor(){super(...arguments);this.verbose=z.Boolean("-v,--verbose",!1,{description:"Print the setting description on top of the regular key/value information"});this.why=z.Boolean("--why",!1,{description:"Print the reason why a setting is set a particular way"});this.json=z.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins,{strict:!1});return(await Je.start({configuration:e,json:this.json,stdout:this.context.stdout},async i=>{if(e.invalid.size>0&&!this.json){for(let[n,s]of e.invalid)i.reportError($.INVALID_CONFIGURATION_KEY,`Invalid configuration key "${n}" in ${s}`);i.reportSeparator()}if(this.json){let n=Se.sortMap(e.settings.keys(),s=>s);for(let s of n){let o=e.settings.get(s),a=e.getSpecial(s,{hideSecrets:!0,getNativePaths:!0}),l=e.sources.get(s);this.verbose?i.reportJson({key:s,effective:a,source:l}):i.reportJson(N({key:s,effective:a,source:l},o))}}else{let n=Se.sortMap(e.settings.keys(),a=>a),s=n.reduce((a,l)=>Math.max(a,l.length),0),o={breakLength:Infinity,colors:e.get("enableColors"),maxArrayLength:2};if(this.why||this.verbose){let a=n.map(c=>{let u=e.settings.get(c);if(!u)throw new Error(`Assertion failed: This settings ("${c}") should have been registered`);let g=this.why?e.sources.get(c)||"<default>":u.description;return[c,g]}),l=a.reduce((c,[,u])=>Math.max(c,u.length),0);for(let[c,u]of a)i.reportInfo(null,`${c.padEnd(s," ")}   ${u.padEnd(l," ")}   ${(0,KN.inspect)(e.getSpecial(c,{hideSecrets:!0,getNativePaths:!0}),o)}`)}else for(let a of n)i.reportInfo(null,`${a.padEnd(s," ")}   ${(0,KN.inspect)(e.getSpecial(a,{hideSecrets:!0,getNativePaths:!0}),o)}`)}})).exitCode()}};lm.paths=[["config"]],lm.usage=Re.Usage({description:"display the current configuration",details:`
       This command prints the current active configuration settings.
-    `,examples:[["Print the active configuration settings","$0 config"]]});var gae=lm;Es();var HN={};ft(HN,{Strategy:()=>Iu,acceptedStrategies:()=>R8e,dedupe:()=>jN});var fae=ge(ts()),Iu;(function(e){e.HIGHEST="highest"})(Iu||(Iu={}));var R8e=new Set(Object.values(Iu)),F8e={highest:async(t,e,{resolver:r,fetcher:i,resolveOptions:n,fetchOptions:s})=>{let o=new Map;for(let[a,l]of t.storedResolutions){let c=t.storedDescriptors.get(a);if(typeof c=="undefined")throw new Error(`Assertion failed: The descriptor (${a}) should have been registered`);Se.getSetWithDefault(o,c.identHash).add(l)}return Array.from(t.storedDescriptors.values(),async a=>{if(e.length&&!fae.default.isMatch(P.stringifyIdent(a),e))return null;let l=t.storedResolutions.get(a.descriptorHash);if(typeof l=="undefined")throw new Error(`Assertion failed: The resolution (${a.descriptorHash}) should have been registered`);let c=t.originalPackages.get(l);if(typeof c=="undefined"||!r.shouldPersistResolution(c,n))return null;let u=o.get(a.identHash);if(typeof u=="undefined")throw new Error(`Assertion failed: The resolutions (${a.identHash}) should have been registered`);if(u.size===1)return null;let g=[...u].map(y=>{let Q=t.originalPackages.get(y);if(typeof Q=="undefined")throw new Error(`Assertion failed: The package (${y}) should have been registered`);return Q.reference}),f=await r.getSatisfying(a,g,n),h=f==null?void 0:f[0];if(typeof h=="undefined")return null;let p=h.locatorHash,m=t.originalPackages.get(p);if(typeof m=="undefined")throw new Error(`Assertion failed: The package (${p}) should have been registered`);return p===l?null:{descriptor:a,currentPackage:c,updatedPackage:m}})}};async function jN(t,{strategy:e,patterns:r,cache:i,report:n}){let{configuration:s}=t,o=new pi,a=s.makeResolver(),l=s.makeFetcher(),c={cache:i,checksums:t.storedChecksums,fetcher:l,project:t,report:o,skipIntegrityCheck:!0,cacheOptions:{skipIntegrityCheck:!0}},u={project:t,resolver:a,report:o,fetchOptions:c};return await n.startTimerPromise("Deduplication step",async()=>{let f=await F8e[e](t,r,{resolver:a,resolveOptions:u,fetcher:l,fetchOptions:c}),h=Ji.progressViaCounter(f.length);n.reportProgress(h);let p=0;await Promise.all(f.map(Q=>Q.then(S=>{if(S===null)return;p++;let{descriptor:x,currentPackage:M,updatedPackage:Y}=S;n.reportInfo($.UNNAMED,`${P.prettyDescriptor(s,x)} can be deduped from ${P.prettyLocator(s,M)} to ${P.prettyLocator(s,Y)}`),n.reportJson({descriptor:P.stringifyDescriptor(x),currentResolution:P.stringifyLocator(M),updatedResolution:P.stringifyLocator(Y)}),t.storedResolutions.set(x.descriptorHash,Y.locatorHash)}).finally(()=>h.tick())));let m;switch(p){case 0:m="No packages";break;case 1:m="One package";break;default:m=`${p} packages`}let y=ae.pretty(s,e,ae.Type.CODE);return n.reportInfo($.UNNAMED,`${m} can be deduped using the ${y} strategy`),p})}var cm=class extends Le{constructor(){super(...arguments);this.strategy=z.String("-s,--strategy",Iu.HIGHEST,{description:"The strategy to use when deduping dependencies",validator:nn(Iu)});this.check=z.Boolean("-c,--check",!1,{description:"Exit with exit code 1 when duplicates are found, without persisting the dependency tree"});this.json=z.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.mode=z.String("--mode",{description:"Change what artifacts installs generate",validator:nn(di)});this.patterns=z.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:r}=await ze.find(e,this.context.cwd),i=await Nt.find(e);await r.restoreInstallState({restoreResolutions:!1});let n=0,s=await Je.start({configuration:e,includeFooter:!1,stdout:this.context.stdout,json:this.json},async o=>{n=await jN(r,{strategy:this.strategy,patterns:this.patterns,cache:i,report:o})});return s.hasErrors()?s.exitCode():this.check?n?1:0:(await Je.start({configuration:e,stdout:this.context.stdout,json:this.json},async a=>{await r.install({cache:i,report:a,mode:this.mode})})).exitCode()}};cm.paths=[["dedupe"]],cm.usage=Re.Usage({description:"deduplicate dependencies with overlapping ranges",details:"\n      Duplicates are defined as descriptors with overlapping ranges being resolved and locked to different locators. They are a natural consequence of Yarn's deterministic installs, but they can sometimes pile up and unnecessarily increase the size of your project.\n\n      This command dedupes dependencies in the current project using different strategies (only one is implemented at the moment):\n\n      - `highest`: Reuses (where possible) the locators with the highest versions. This means that dependencies can only be upgraded, never downgraded. It's also guaranteed that it never takes more than a single pass to dedupe the entire dependency tree.\n\n      **Note:** Even though it never produces a wrong dependency tree, this command should be used with caution, as it modifies the dependency tree, which can sometimes cause problems when packages don't strictly follow semver recommendations. Because of this, it is recommended to also review the changes manually.\n\n      If set, the `-c,--check` flag will only report the found duplicates, without persisting the modified dependency tree. If changes are found, the command will exit with a non-zero exit code, making it suitable for CI purposes.\n\n      If the `--mode=<mode>` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n      - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the later will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n      - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n      This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n      ### In-depth explanation:\n\n      Yarn doesn't deduplicate dependencies by default, otherwise installs wouldn't be deterministic and the lockfile would be useless. What it actually does is that it tries to not duplicate dependencies in the first place.\n\n      **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@*`will cause Yarn to reuse `foo@2.3.4`, even if the latest `foo` is actually `foo@2.10.14`, thus preventing unnecessary duplication.\n\n      Duplication happens when Yarn can't unlock dependencies that have already been locked inside the lockfile.\n\n      **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@2.10.14` will cause Yarn to install `foo@2.10.14` because the existing resolution doesn't satisfy the range `2.10.14`. This behavior can lead to (sometimes) unwanted duplication, since now the lockfile contains 2 separate resolutions for the 2 `foo` descriptors, even though they have overlapping ranges, which means that the lockfile can be simplified so that both descriptors resolve to `foo@2.10.14`.\n    ",examples:[["Dedupe all packages","$0 dedupe"],["Dedupe all packages using a specific strategy","$0 dedupe --strategy highest"],["Dedupe a specific package","$0 dedupe lodash"],["Dedupe all packages with the `@babel/*` scope","$0 dedupe '@babel/*'"],["Check for duplicates (can be used as a CI step)","$0 dedupe --check"]]});var hae=cm;var ib=class extends Le{async execute(){let{plugins:e}=await ye.find(this.context.cwd,this.context.plugins),r=[];for(let o of e){let{commands:a}=o[1];if(a){let c=Is.from(a).definitions();r.push([o[0],c])}}let i=this.cli.definitions(),n=(o,a)=>o.split(" ").slice(1).join()===a.split(" ").slice(1).join(),s=dae()["@yarnpkg/builder"].bundles.standard;for(let o of r){let a=o[1];for(let l of a)i.find(c=>n(c.path,l.path)).plugin={name:o[0],isDefault:s.includes(o[0])}}this.context.stdout.write(`${JSON.stringify(i,null,2)}
+    `,examples:[["Print the active configuration settings","$0 config"]]});var gae=lm;Es();var HN={};ft(HN,{Strategy:()=>Iu,acceptedStrategies:()=>R8e,dedupe:()=>jN});var fae=ge(ts()),Iu;(function(e){e.HIGHEST="highest"})(Iu||(Iu={}));var R8e=new Set(Object.values(Iu)),F8e={highest:async(t,e,{resolver:r,fetcher:i,resolveOptions:n,fetchOptions:s})=>{let o=new Map;for(let[a,l]of t.storedResolutions){let c=t.storedDescriptors.get(a);if(typeof c=="undefined")throw new Error(`Assertion failed: The descriptor (${a}) should have been registered`);Se.getSetWithDefault(o,c.identHash).add(l)}return Array.from(t.storedDescriptors.values(),async a=>{if(e.length&&!fae.default.isMatch(P.stringifyIdent(a),e))return null;let l=t.storedResolutions.get(a.descriptorHash);if(typeof l=="undefined")throw new Error(`Assertion failed: The resolution (${a.descriptorHash}) should have been registered`);let c=t.originalPackages.get(l);if(typeof c=="undefined"||!r.shouldPersistResolution(c,n))return null;let u=o.get(a.identHash);if(typeof u=="undefined")throw new Error(`Assertion failed: The resolutions (${a.identHash}) should have been registered`);if(u.size===1)return null;let g=[...u].map(y=>{let Q=t.originalPackages.get(y);if(typeof Q=="undefined")throw new Error(`Assertion failed: The package (${y}) should have been registered`);return Q.reference}),f=await r.getSatisfying(a,g,n),h=f==null?void 0:f[0];if(typeof h=="undefined")return null;let p=h.locatorHash,m=t.originalPackages.get(p);if(typeof m=="undefined")throw new Error(`Assertion failed: The package (${p}) should have been registered`);return p===l?null:{descriptor:a,currentPackage:c,updatedPackage:m}})}};async function jN(t,{strategy:e,patterns:r,cache:i,report:n}){let{configuration:s}=t,o=new pi,a=s.makeResolver(),l=s.makeFetcher(),c={cache:i,checksums:t.storedChecksums,fetcher:l,project:t,report:o,skipIntegrityCheck:!0,cacheOptions:{skipIntegrityCheck:!0}},u={project:t,resolver:a,report:o,fetchOptions:c};return await n.startTimerPromise("Deduplication step",async()=>{let f=await F8e[e](t,r,{resolver:a,resolveOptions:u,fetcher:l,fetchOptions:c}),h=Ji.progressViaCounter(f.length);n.reportProgress(h);let p=0;await Promise.all(f.map(Q=>Q.then(S=>{if(S===null)return;p++;let{descriptor:x,currentPackage:M,updatedPackage:Y}=S;n.reportInfo($.UNNAMED,`${P.prettyDescriptor(s,x)} can be deduped from ${P.prettyLocator(s,M)} to ${P.prettyLocator(s,Y)}`),n.reportJson({descriptor:P.stringifyDescriptor(x),currentResolution:P.stringifyLocator(M),updatedResolution:P.stringifyLocator(Y)}),t.storedResolutions.set(x.descriptorHash,Y.locatorHash)}).finally(()=>h.tick())));let m;switch(p){case 0:m="No packages";break;case 1:m="One package";break;default:m=`${p} packages`}let y=ae.pretty(s,e,ae.Type.CODE);return n.reportInfo($.UNNAMED,`${m} can be deduped using the ${y} strategy`),p})}var cm=class extends Le{constructor(){super(...arguments);this.strategy=z.String("-s,--strategy",Iu.HIGHEST,{description:"The strategy to use when deduping dependencies",validator:nn(Iu)});this.check=z.Boolean("-c,--check",!1,{description:"Exit with exit code 1 when duplicates are found, without persisting the dependency tree"});this.json=z.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.mode=z.String("--mode",{description:"Change what artifacts installs generate",validator:nn(di)});this.patterns=z.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:r}=await ze.find(e,this.context.cwd),i=await Nt.find(e);await r.restoreInstallState({restoreResolutions:!1});let n=0,s=await Je.start({configuration:e,includeFooter:!1,stdout:this.context.stdout,json:this.json},async o=>{n=await jN(r,{strategy:this.strategy,patterns:this.patterns,cache:i,report:o})});return s.hasErrors()?s.exitCode():this.check?n?1:0:(await Je.start({configuration:e,stdout:this.context.stdout,json:this.json},async a=>{await r.install({cache:i,report:a,mode:this.mode})})).exitCode()}};cm.paths=[["dedupe"]],cm.usage=Re.Usage({description:"deduplicate dependencies with overlapping ranges",details:"\n      Duplicates are defined as descriptors with overlapping ranges being resolved and locked to different locators. They are a natural consequence of Yarn's deterministic installs, but they can sometimes pile up and unnecessarily increase the size of your project.\n\n      This command dedupes dependencies in the current project using different strategies (only one is implemented at the moment):\n\n      - `highest`: Reuses (where possible) the locators with the highest versions. This means that dependencies can only be upgraded, never downgraded. It's also guaranteed that it never takes more than a single pass to dedupe the entire dependency tree.\n\n      **Note:** Even though it never produces a wrong dependency tree, this command should be used with caution, as it modifies the dependency tree, which can sometimes cause problems when packages don't strictly follow semver recommendations. Because of this, it is recommended to also review the changes manually.\n\n      If set, the `-c,--check` flag will only report the found duplicates, without persisting the modified dependency tree. If changes are found, the command will exit with a non-zero exit code, making it suitable for CI purposes.\n\n      If the `--mode=<mode>` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n      - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the later will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n      - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n      This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n      ### In-depth explanation:\n\n      Yarn doesn't deduplicate dependencies by default, otherwise installs wouldn't be deterministic and the lockfile would be useless. What it actually does is that it tries to not duplicate dependencies in the first place.\n\n      **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@*`will cause Yarn to reuse `foo@2.3.4`, even if the latest `foo` is actually `foo@2.10.14`, thus preventing unnecessary duplication.\n\n      Duplication happens when Yarn can't unlock dependencies that have already been locked inside the lockfile.\n\n      **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@2.10.14` will cause Yarn to install `foo@2.10.14` because the existing resolution doesn't satisfy the range `2.10.14`. This behavior can lead to (sometimes) unwanted duplication, since now the lockfile contains 2 separate resolutions for the 2 `foo` descriptors, even though they have overlapping ranges, which means that the lockfile can be simplified so that both descriptors resolve to `foo@2.10.14`.\n    ",examples:[["Dedupe all packages","$0 dedupe"],["Dedupe all packages using a specific strategy","$0 dedupe --strategy highest"],["Dedupe a specific package","$0 dedupe lodash"],["Dedupe all packages with the `@babel/*` scope","$0 dedupe '@babel/*'"],["Check for duplicates (can be used as a CI step)","$0 dedupe --check"]]});var hae=cm;var ib=class extends Le{async execute(){let{plugins:e}=await ye.find(this.context.cwd,this.context.plugins),r=[];for(let o of e){let{commands:a}=o[1];if(a){let c=Is.from(a).definitions();r.push([o[0],c])}}let i=this.cli.definitions(),n=(o,a)=>o.split(" ").slice(1).join()===a.split(" ").slice(1).join(),s=dae()["@yarnpkg/builder"].bundles.standard;for(let o of r){let a=o[1];for(let l of a)i.find(c=>n(c.path,l.path)).plugin={name:o[0],useAlts:s.includes(o[0])}}this.context.stdout.write(`${JSON.stringify(i,null,2)}
 `)}};ib.paths=[["--clipanion=definitions"]];var Cae=ib;var nb=class extends Le{async execute(){this.context.stdout.write(this.cli.usage(null))}};nb.paths=[["help"],["--help"],["-h"]];var mae=nb;var GN=class extends Le{constructor(){super(...arguments);this.leadingArgument=z.String();this.args=z.Proxy()}async execute(){if(this.leadingArgument.match(/[\\/]/)&&!P.tryParseIdent(this.leadingArgument)){let e=k.resolve(this.context.cwd,j.toPortablePath(this.leadingArgument));return await this.cli.run(this.args,{cwd:e})}else return await this.cli.run(["run",this.leadingArgument,...this.args])}},Eae=GN;var sb=class extends Le{async execute(){this.context.stdout.write(`${Ur||"<unknown>"}
 `)}};sb.paths=[["-v"],["--version"]];var Iae=sb;var um=class extends Le{constructor(){super(...arguments);this.commandName=z.String();this.args=z.Proxy()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:r,locator:i}=await ze.find(e,this.context.cwd);return await r.restoreInstallState(),await Zt.executePackageShellcode(i,this.commandName,this.args,{cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,project:r})}};um.paths=[["exec"]],um.usage=Re.Usage({description:"execute a shell script",details:`
       This command simply executes a shell script within the context of the root directory of the active workspace using the portable shell.
index f83a974193abe98926bb982ce4d731446ebd9bb3..54c570f7c4453fdafa3fe5bd4cd27795eadcb1e1 100644 (file)
@@ -32,6 +32,45 @@ describe("Collection panel tests", function () {
         cy.clearLocalStorage();
     });
 
+    it('shows the appropriate buttons in the toolbar', () => {
+
+        const msButtonTooltips = [
+            'API Details',
+            'Add to Favorites',
+            'Copy to clipboard',
+            'Edit collection',
+            'Make a copy',
+            'Move to',
+            'Move to trash',
+            'Open in new tab',
+            'Open with 3rd party client',
+            'Share',
+            'View details',
+        ];
+
+        cy.loginAs(activeUser);
+        const name = `Test collection ${Math.floor(Math.random() * 999999)}`;
+        cy.get("[data-cy=side-panel-button]").click({force: true});
+        cy.get("[data-cy=side-panel-new-collection]").click();
+        cy.get("[data-cy=form-dialog]")
+            .should("contain", "New collection")
+            .within(() => {
+                cy.get("[data-cy=name-field]").within(() => {
+                    cy.get("input").type(name);
+                });
+                cy.get("[data-cy=form-submit-btn]").click();
+            });
+            cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
+            cy.waitForDom()
+            cy.get('[data-cy=data-table-row]').contains(name).should('exist').parent().parent().parent().parent().click()
+            cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
+            for (let i = 0; i < msButtonTooltips.length; i++) {
+                cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
+                cy.get('body').contains(msButtonTooltips[i]).should('exist')
+                cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
+            }
+    })
+
     it("allows to download mountain duck config for a collection", () => {
         cy.createCollection(adminUser.token, {
             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
@@ -125,6 +164,8 @@ describe("Collection panel tests", function () {
         cy.get("[data-cy=form-dialog]").should("exist").and("contain", "Collection with the same name already exists");
     });
 
+    
+
     it("uses the property editor (from edit dialog) with vocabulary terms", function () {
         cy.createCollection(adminUser.token, {
             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
@@ -168,6 +209,8 @@ describe("Collection panel tests", function () {
             });
     });
 
+    
+
     it("uses the editor (from details panel) with vocabulary terms", function () {
         cy.createCollection(adminUser.token, {
             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
diff --git a/cypress/integration/multiselect-toolbar.spec.js b/cypress/integration/multiselect-toolbar.spec.js
new file mode 100644 (file)
index 0000000..ef503f7
--- /dev/null
@@ -0,0 +1,36 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+describe('Multiselect Toolbar Tests', () => {
+    let activeUser;
+    let adminUser;
+
+    before(function () {
+        // Only set up common users once. These aren't set up as aliases because
+        // aliases are cleaned up after every test. Also it doesn't make sense
+        // to set the same users on beforeEach() over and over again, so we
+        // separate a little from Cypress' 'Best Practices' here.
+        cy.getUser('admin', 'Admin', 'User', true, true)
+            .as('adminUser')
+            .then(function () {
+                adminUser = this.adminUser;
+            });
+        cy.getUser('user', 'Active', 'User', false, true)
+            .as('activeUser')
+            .then(function () {
+                activeUser = this.activeUser;
+            });
+    });
+
+    beforeEach(function () {
+        cy.clearCookies();
+        cy.clearLocalStorage();
+    });
+
+    it('exists in DOM in neutral state', () => {
+        cy.loginAs(activeUser);
+        cy.get('[data-cy=multiselect-toolbar]').should('exist');
+        cy.get('[data-cy=multiselect-button]').should('not.exist');
+    });
+});
index 38d9794b18573a0379a3bbba93f53f0b661abe83..9ea026b9906511c297cb6367370b8c08955f8869 100644 (file)
@@ -90,6 +90,49 @@ describe("Process tests", function () {
         });
     }
 
+    describe('Multiselect Toolbar', () => {
+        it('shows the appropriate buttons in the toolbar', () => {
+
+            const msButtonTooltips = [
+                'API Details',
+                'Add to Favorites',
+                'CANCEL',
+                'Copy and re-run process',
+                'Edit process',
+                'Move to',
+                'Open in new tab',
+                'Outputs',
+                'Remove',
+                'Share',
+                'View details',
+            ];
+    
+            createContainerRequest(
+                activeUser,
+                `test_container_request ${Math.floor(Math.random() * 999999)}`,
+                "arvados/jobs",
+                ["echo", "hello world"],
+                false,
+                "Committed"
+            ).then(function (containerRequest) {
+                cy.loginAs(activeUser);
+                cy.goToPath(`/processes/${containerRequest.uuid}`);
+                cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
+                cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
+                cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
+                cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
+                cy.waitForDom()
+                cy.get('[data-cy=data-table-row]').contains(containerRequest.name).should('exist').parent().parent().parent().parent().click()
+                cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
+                for (let i = 0; i < msButtonTooltips.length; i++) {
+                    cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
+                    cy.get('body').contains(msButtonTooltips[i]).should('exist')
+                    cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
+                }
+            });
+        })
+    })
+
     describe("Details panel", function () {
         it("shows process details", function () {
             createContainerRequest(
index e61138219dcf01558151f2179aec78983c73f6ad..e6185c108e94c454970ad2605d83f3bc8b4637a2 100644 (file)
@@ -180,6 +180,47 @@ describe("Project tests", function () {
         verifyProjectDescription(projName, null);
     });
 
+    it('shows the appropriate buttons in the multiselect toolbar', () => {
+
+        const msButtonTooltips = [
+            'API Details',
+            'Add to Favorites',
+            'Copy to clipboard',
+            'Edit project',
+            'Freeze Project',
+            'Move to',
+            'Move to trash',
+            'New project',
+            'Open in new tab',
+            'Open with 3rd party client',
+            'Share',
+            'View details',
+        ];
+
+        cy.loginAs(activeUser);
+        const projName = `Test project (${Math.floor(999999 * Math.random())})`;
+        cy.get('[data-cy=side-panel-button]').click();
+        cy.get('[data-cy=side-panel-new-project]').click();
+        cy.get('[data-cy=form-dialog]')
+            .should('contain', 'New Project')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(projName);
+                });
+            })
+        cy.get("[data-cy=form-submit-btn]").click();
+        cy.waitForDom()
+        cy.go('back')
+
+        cy.get('[data-cy=data-table-row]').contains(projName).should('exist').parent().parent().parent().click()
+        cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
+        for (let i = 0; i < msButtonTooltips.length; i++) {
+            cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
+            cy.get('body').contains(msButtonTooltips[i]).should('exist')
+            cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
+        }
+    })
+
     it("creates new project on home project and then a subproject inside it", function () {
         const createProject = function (name, parentName) {
             cy.get("[data-cy=side-panel-button]").click();
@@ -473,7 +514,7 @@ describe("Project tests", function () {
             });
         });
 
-        it("should be able to froze own project", () => {
+        it("should be able to freeze own project", () => {
             cy.getAll("@mainProject").then(([mainProject]) => {
                 cy.loginAs(activeUser);
 
@@ -503,7 +544,7 @@ describe("Project tests", function () {
             });
         });
 
-        it("should be able to froze not owned project", () => {
+        it("should be able to freeze not owned project", () => {
             cy.getAll("@adminProject").then(([adminProject]) => {
                 cy.loginAs(activeUser);
 
@@ -515,7 +556,7 @@ describe("Project tests", function () {
             });
         });
 
-        it("should be able to unfroze project if user is an admin", () => {
+        it("should be able to unfreeze project if user is an admin", () => {
             cy.getAll("@adminProject").then(([adminProject]) => {
                 cy.loginAs(adminUser);
 
index 76ad3c631dfe96fb33b836fb83f348b446aa293a..844e87d8deb64b1969014296330a17312623051f 100644 (file)
@@ -265,4 +265,31 @@ describe('Registered workflow panel tests', function() {
                     });
             });
     });
+
+    it('shows the appropriate buttons in the multiselect toolbar', () => {
+
+        const msButtonTooltips = [
+            'API Details',
+            'Copy to clipboard',
+            'Delete Workflow',
+            'Open in new tab',
+            'Run Workflow',
+            'View details',
+        ];
+
+        cy.createResource(activeUser.token, "workflows", {workflow: {name: "Test wf"}})
+            .then(function(workflowResource) {
+                cy.loginAs(activeUser);
+                cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
+                cy.waitForDom()
+                cy.get('[data-cy=data-table-row]').contains(workflowResource.name).should('exist').parent().parent().parent().click()
+                cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
+                for (let i = 0; i < msButtonTooltips.length; i++) {
+                        cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
+                        cy.get('body').contains(msButtonTooltips[i]).should('exist')
+                        cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
+                    }
+                });
+    })
+
 });
index 586bb13b1e8e43b520b9848d16cbc7483ae79709..3ef483dfe03faf63edd24fa3f02ff322e16110c4 100644 (file)
@@ -48,7 +48,7 @@ export const CopyToClipboardSnackbar = connect()(
             render() {
                 const { children, value, classes } = this.props;
                 return (
-                    <Tooltip title='Copy to clipboard'>
+                    <Tooltip title='Copy to clipboard' onClick={(ev) => ev.stopPropagation()}>
                         <span className={classes.copyIcon}>
                             <CopyToClipboard text={value} onCopy={this.onCopy}>
                                 {children || <CopyIcon />}
index b3a93bbb799c9a64cfcee18602271606fc99e912..27e46d584962c8d3e1cb1ca536b21ab1b4577ecf 100644 (file)
@@ -390,7 +390,10 @@ export const DataExplorer = withStyles(styles)(
                 >
                     <IconButton
                         className={this.props.classes.moreOptionsButton}
-                        onClick={event => this.props.onContextMenu(event, item)}
+                        onClick={event => {
+                            event.stopPropagation()
+                            this.props.onContextMenu(event, item)
+                        }}
                     >
                         <MoreVerticalIcon />
                     </IconButton>
index 155d772f85855ddd86a90ecc7842065d543e830f..97e1a3ac488caf6f0b260858b649c79df96a072e 100644 (file)
@@ -155,6 +155,13 @@ export const DataTable = withStyles(styles)(
                 if (items.length) this.initializeCheckedList(items);
                 else setCheckedListOnStore({});
             }
+            if (prevProps.currentRoute !== this.props.currentRoute) {
+                this.initializeCheckedList([])
+            }
+        }
+
+        componentWillUnmount(): void {
+            this.initializeCheckedList([])
         }
 
         checkBoxColumn: DataColumn<any, any> = {
@@ -166,6 +173,7 @@ export const DataTable = withStyles(styles)(
                 const { classes, checkedList } = this.props;
                 return (
                     <input
+                        data-cy={`multiselect-checkbox-${uuid}`}
                         type="checkbox"
                         name={uuid}
                         className={classes.checkBox}
@@ -367,6 +375,7 @@ export const DataTable = withStyles(styles)(
             const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid, currentRoute } = this.props;
             return (
                 <TableRow
+                    data-cy={'data-table-row'}
                     hover
                     key={extractKey ? extractKey(item) : index}
                     onClick={event => onRowClick && onRowClick(event, item)}
index 998cd8059aabe97f1d56e0e9d6c699dea876e06b..2dd97c1663777cc5d64a7540351d8d8dfeef5b52 100644 (file)
@@ -84,13 +84,13 @@ import { faPencilAlt, faSlash, faUsers, faEllipsisH } from "@fortawesome/free-so
 import { FormatAlignLeft } from "@material-ui/icons";
 library.add(faPencilAlt, faSlash, faUsers, faEllipsisH);
 
-export const FreezeIcon = (props: any) => (
+export const FreezeIcon: IconType = (props: any) => (
     <SvgIcon {...props}>
         <path d="M20.79,13.95L18.46,14.57L16.46,13.44V10.56L18.46,9.43L20.79,10.05L21.31,8.12L19.54,7.65L20,5.88L18.07,5.36L17.45,7.69L15.45,8.82L13,7.38V5.12L14.71,3.41L13.29,2L12,3.29L10.71,2L9.29,3.41L11,5.12V7.38L8.5,8.82L6.5,7.69L5.92,5.36L4,5.88L4.47,7.65L2.7,8.12L3.22,10.05L5.55,9.43L7.55,10.56V13.45L5.55,14.58L3.22,13.96L2.7,15.89L4.47,16.36L4,18.12L5.93,18.64L6.55,16.31L8.55,15.18L11,16.62V18.88L9.29,20.59L10.71,22L12,20.71L13.29,22L14.7,20.59L13,18.88V16.62L15.5,15.17L17.5,16.3L18.12,18.63L20,18.12L19.53,16.35L21.3,15.88L20.79,13.95M9.5,10.56L12,9.11L14.5,10.56V13.44L12,14.89L9.5,13.44V10.56Z" />
     </SvgIcon>
 );
 
-export const UnfreezeIcon = (props: any) => (
+export const UnfreezeIcon: IconType = (props: any) => (
     <SvgIcon {...props}>
         <path d="M11 5.12L9.29 3.41L10.71 2L12 3.29L13.29 2L14.71 3.41L13 5.12V7.38L15.45 8.82L17.45 7.69L18.07 5.36L20 5.88L19.54 7.65L21.31 8.12L20.79 10.05L18.46 9.43L16.46 10.56V13.26L14.5 11.3V10.56L12.74 9.54L10.73 7.53L11 7.38V5.12M18.46 14.57L16.87 13.67L19.55 16.35L21.3 15.88L20.79 13.95L18.46 14.57M13 16.62V18.88L14.7 20.59L13.29 22L12 20.71L10.71 22L9.29 20.59L11 18.88V16.62L8.55 15.18L6.55 16.31L5.93 18.64L4 18.12L4.47 16.36L2.7 15.89L3.22 13.96L5.55 14.58L7.55 13.45V10.56L5.55 9.43L3.22 10.05L2.7 8.12L4.47 7.65L4 5.89L1.11 3L2.39 1.73L22.11 21.46L20.84 22.73L14.1 16L13 16.62M12 14.89L12.63 14.5L9.5 11.39V13.44L12 14.89Z" />
     </SvgIcon>
index 4eff8885fcd5e6e7e034eab672588c340bf92f22..30d5bd7912f1d232d7687ea847d842506841fb55 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import React from "react";
+import React, { useEffect, useState } from "react";
 import { connect } from "react-redux";
 import { StyleRulesCallback, withStyles, WithStyles, Toolbar, Tooltip, IconButton } from "@material-ui/core";
 import { ArvadosTheme } from "common/custom-theme";
@@ -10,18 +10,34 @@ import { RootState } from "store/store";
 import { Dispatch } from "redux";
 import { TCheckedList } from "components/data-table/data-table";
 import { ContextMenuResource } from "store/context-menu/context-menu-actions";
-import { Resource, extractUuidKind } from "models/resource";
+import { Resource, ResourceKind, extractUuidKind } from "models/resource";
 import { getResource } from "store/resources/resources";
 import { ResourcesState } from "store/resources/resources";
-import { ContextMenuAction, ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { RestoreFromTrashIcon, TrashIcon } from "components/icon/icon";
-import { multiselectActionsFilters, TMultiselectActionsFilters, contextMenuActionConsts } from "./ms-toolbar-action-filters";
+import { MultiSelectMenuAction, MultiSelectMenuActionSet } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { ContextMenuAction } from "views-components/context-menu/context-menu-action-set";
+import { multiselectActionsFilters, TMultiselectActionsFilters, msMenuResourceKind } from "./ms-toolbar-action-filters";
 import { kindToActionSet, findActionByName } from "./ms-kind-action-differentiator";
 import { msToggleTrashAction } from "views-components/multiselect-toolbar/ms-project-action-set";
 import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
 import { ContainerRequestResource } from "models/container-request";
+import { FavoritesState } from "store/favorites/favorites-reducer";
+import { resourceIsFrozen } from "common/frozen-resources";
+import { getResourceWithEditableStatus } from "store/resources/resources";
+import { GroupResource } from "models/group";
+import { EditableResource } from "models/resource";
+import { User } from "models/user";
+import { GroupClass } from "models/group";
+import { isProcessCancelable } from "store/processes/process";
+import { CollectionResource } from "models/collection";
+import { getProcess } from "store/processes/process";
+import { Process } from "store/processes/process";
+import { PublicFavoritesState } from "store/public-favorites/public-favorites-reducer";
+import { isExactlyOneSelected } from "store/multiselect/multiselect-actions";
 
-type CssRules = "root" | "button";
+const WIDTH_TRANSITION = 150
+
+type CssRules = "root" | "transition" | "button" | "iconContainer";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -29,75 +45,137 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         flexShrink: 0,
         flexDirection: "row",
         width: 0,
+        height: '2.7rem',
         padding: 0,
         margin: "1rem auto auto 0.5rem",
-        overflow: "hidden",
-        transition: "width 150ms",
+        overflowY: 'auto',
+        scrollBehavior: 'smooth',
+        '&::-webkit-scrollbar': {
+            width: 0,
+            height: 2
+        },
+        '&::-webkit-scrollbar-track': {
+            width: 0,
+            height: 2
+        },
+        '&::-webkit-scrollbar-thumb': {
+            backgroundColor: '#757575',
+            borderRadius: 2
+        }
+    },
+    transition: {
+        display: "flex",
+        flexDirection: "row",
+        width: 0,
+        height: '2.7rem',
+        padding: 0,
+        margin: "1rem auto auto 0.5rem",
+        overflow: 'hidden',
+        transition: `width ${WIDTH_TRANSITION}ms`,
     },
     button: {
         width: "2.5rem",
         height: "2.5rem ",
     },
+    iconContainer: {
+        height: '100%'
+    }
 });
 
 export type MultiselectToolbarProps = {
     checkedList: TCheckedList;
-    resources: ResourcesState;
+    singleSelectedUuid: string | null
+    iconProps: IconProps
+    user: User | null
+    disabledButtons: Set<string>
     executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void;
 };
 
+type IconProps = {
+    resources: ResourcesState;
+    favorites: FavoritesState;
+    publicFavorites: PublicFavoritesState;
+}
+
 export const MultiselectToolbar = connect(
     mapStateToProps,
     mapDispatchToProps
 )(
     withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
-        const { classes, checkedList } = props;
-        const currentResourceKinds = Array.from(selectedToKindSet(checkedList));
-
+        const { classes, checkedList, singleSelectedUuid, iconProps, user, disabledButtons } = props;
+        const singleResourceKind = singleSelectedUuid ? [resourceToMsResourceKind(singleSelectedUuid, iconProps.resources, user)] : null
+        const currentResourceKinds = singleResourceKind ? singleResourceKind : Array.from(selectedToKindSet(checkedList));
         const currentPathIsTrash = window.location.pathname === "/trash";
-        const buttons =
-            currentPathIsTrash && selectedToKindSet(checkedList).size
-                ? [msToggleTrashAction]
-                : selectActionsByKind(currentResourceKinds, multiselectActionsFilters);
+        const [isTransitioning, setIsTransitioning] = useState(false);
+        
+        const actions =
+        currentPathIsTrash && selectedToKindSet(checkedList).size
+        ? [msToggleTrashAction]
+        : selectActionsByKind(currentResourceKinds as string[], multiselectActionsFilters)
+        .filter((action) => (singleSelectedUuid === null ? action.isForMulti : true));
+        
+        const handleTransition = () => {
+            setIsTransitioning(true)
+            setTimeout(() => {
+                setIsTransitioning(false)
+            }, WIDTH_TRANSITION);
+        }
+
+        useEffect(()=>{
+            handleTransition()
+        }, [checkedList])
 
         return (
             <React.Fragment>
                 <Toolbar
-                    className={classes.root}
-                    style={{ width: `${buttons.length * 2.5}rem` }}
-                >
-                    {buttons.length ? (
-                        buttons.map((btn, i) =>
-                            btn.name === "ToggleTrashAction" ? (
-                                <Tooltip
-                                    className={classes.button}
-                                    title={currentPathIsTrash ? "Restore selected" : "Move to trash"}
-                                    key={i}
-                                    disableFocusListener
-                                >
-                                    <IconButton onClick={() => props.executeMulti(btn, checkedList, props.resources)}>
-                                        {currentPathIsTrash ? <RestoreFromTrashIcon /> : <TrashIcon />}
+                    className={isTransitioning ? classes.transition: classes.root}
+                    style={{ width: `${(actions.length * 2.5) + 1}rem` }}
+                    data-cy='multiselect-toolbar'
+                    >
+                    {actions.length ? (
+                        actions.map((action, i) =>{
+                            const { hasAlts, useAlts, name, altName, icon, altIcon } = action;
+                        return hasAlts ? (
+                            <Tooltip
+                                className={classes.button}
+                                title={currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altName : name}
+                                key={i}
+                                disableFocusListener
+                            >
+                                <span className={classes.iconContainer}>
+                                    <IconButton
+                                        data-cy='multiselect-button'
+                                        disabled={disabledButtons.has(name)}
+                                        onClick={() => props.executeMulti(action, checkedList, iconProps.resources)}
+                                    >
+                                        {currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altIcon && altIcon({}) : icon({})}
                                     </IconButton>
-                                </Tooltip>
-                            ) : (
-                                <Tooltip
-                                    className={classes.button}
-                                    title={btn.name}
-                                    key={i}
-                                    disableFocusListener
-                                >
-                                    <IconButton onClick={() => props.executeMulti(btn, checkedList, props.resources)}>
-                                        {btn.icon ? btn.icon({}) : <></>}
+                                </span>
+                            </Tooltip>
+                        ) : (
+                            <Tooltip
+                                className={classes.button}
+                                title={action.name}
+                                key={i}
+                                disableFocusListener
+                            >
+                                <span className={classes.iconContainer}>
+                                    <IconButton
+                                        data-cy='multiselect-button'
+                                        onClick={() => props.executeMulti(action, checkedList, iconProps.resources)}
+                                    >
+                                        {action.icon({})}
                                     </IconButton>
-                                </Tooltip>
-                            )
-                        )
+                                </span>
+                            </Tooltip>
+                        );
+                        })
                     ) : (
                         <></>
                     )}
                 </Toolbar>
             </React.Fragment>
-        );
+        )
     })
 );
 
@@ -131,14 +209,75 @@ function groupByKind(checkedList: TCheckedList, resources: ResourcesState): Reco
     return result;
 }
 
-function filterActions(actionArray: ContextMenuActionSet, filters: Set<string>): Array<ContextMenuAction> {
+function filterActions(actionArray: MultiSelectMenuActionSet, filters: Set<string>): Array<MultiSelectMenuAction> {
     return actionArray[0].filter(action => filters.has(action.name as string));
 }
 
+const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: User | null, readonly = false): (msMenuResourceKind | ResourceKind) | undefined => {
+    if (!user) return;
+    const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, user.uuid)(resources);
+    const { isAdmin } = user;
+    const kind = extractUuidKind(uuid);
+
+    const isFrozen = resourceIsFrozen(resource, resources);
+    const isEditable = (user.isAdmin || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
+
+    switch (kind) {
+        case ResourceKind.PROJECT:
+            if (isFrozen) {
+                return isAdmin ? msMenuResourceKind.FROZEN_PROJECT_ADMIN : msMenuResourceKind.FROZEN_PROJECT;
+            }
+
+            return isAdmin && !readonly
+                ? resource && resource.groupClass !== GroupClass.FILTER
+                    ? msMenuResourceKind.PROJECT_ADMIN
+                    : msMenuResourceKind.FILTER_GROUP_ADMIN
+                : isEditable
+                ? resource && resource.groupClass !== GroupClass.FILTER
+                    ? msMenuResourceKind.PROJECT
+                    : msMenuResourceKind.FILTER_GROUP
+                : msMenuResourceKind.READONLY_PROJECT;
+        case ResourceKind.COLLECTION:
+            const c = getResource<CollectionResource>(uuid)(resources);
+            if (c === undefined) {
+                return;
+            }
+            const isOldVersion = c.uuid !== c.currentVersionUuid;
+            const isTrashed = c.isTrashed;
+            return isOldVersion
+                ? msMenuResourceKind.OLD_VERSION_COLLECTION
+                : isTrashed && isEditable
+                ? msMenuResourceKind.TRASHED_COLLECTION
+                : isAdmin && isEditable
+                ? msMenuResourceKind.COLLECTION_ADMIN
+                : isEditable
+                ? msMenuResourceKind.COLLECTION
+                : msMenuResourceKind.READONLY_COLLECTION;
+        case ResourceKind.PROCESS:
+            return isAdmin && isEditable
+                ? resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
+                    ? msMenuResourceKind.RUNNING_PROCESS_ADMIN
+                    : msMenuResourceKind.PROCESS_ADMIN
+                : readonly
+                ? msMenuResourceKind.READONLY_PROCESS_RESOURCE
+                : resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
+                ? msMenuResourceKind.RUNNING_PROCESS_RESOURCE
+                : msMenuResourceKind.PROCESS_RESOURCE;
+        case ResourceKind.USER:
+            return msMenuResourceKind.ROOT_PROJECT;
+        case ResourceKind.LINK:
+            return msMenuResourceKind.LINK;
+        case ResourceKind.WORKFLOW:
+            return isEditable ? msMenuResourceKind.WORKFLOW : msMenuResourceKind.READONLY_WORKFLOW;
+        default:
+            return;
+    }
+}; 
+
 function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters) {
-    const rawResult: Set<ContextMenuAction> = new Set();
+    const rawResult: Set<MultiSelectMenuAction> = new Set();
     const resultNames = new Set();
-    const allFiltersArray: ContextMenuAction[][] = [];
+    const allFiltersArray: MultiSelectMenuAction[][] = []
     currentResourceKinds.forEach(kind => {
         if (filterSet[kind]) {
             const actions = filterActions(...filterSet[kind]);
@@ -153,14 +292,14 @@ function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMu
     });
 
     const filteredNameSet = allFiltersArray.map(filterArray => {
-        const resultSet = new Set();
-        filterArray.forEach(action => resultSet.add(action.name || ""));
+        const resultSet = new Set<string>();
+        filterArray.forEach(action => resultSet.add(action.name as string || ""));
         return resultSet;
     });
 
     const filteredResult = Array.from(rawResult).filter(action => {
         for (let i = 0; i < filteredNameSet.length; i++) {
-            if (!filteredNameSet[i].has(action.name)) return false;
+            if (!filteredNameSet[i].has(action.name as string)) return false;
         }
         return true;
     });
@@ -178,13 +317,21 @@ function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMu
     });
 }
 
+
 //--------------------------------------------------//
 
-function mapStateToProps(state: RootState) {
+function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites}: RootState) {
     return {
-        checkedList: state.multiselect.checkedList as TCheckedList,
-        resources: state.resources,
-    };
+        checkedList: multiselect.checkedList as TCheckedList,
+        singleSelectedUuid: isExactlyOneSelected(multiselect.checkedList),
+        user: auth && auth.user ? auth.user : null,
+        disabledButtons: new Set<string>(multiselect.disabledButtons),
+        iconProps: {
+            resources,
+            favorites,
+            publicFavorites
+        }
+    }
 }
 
 function mapDispatchToProps(dispatch: Dispatch) {
@@ -192,13 +339,13 @@ function mapDispatchToProps(dispatch: Dispatch) {
         executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => {
             const kindGroups = groupByKind(checkedList, resources);
             switch (selectedAction.name) {
-                case contextMenuActionConsts.MOVE_TO:
-                case contextMenuActionConsts.REMOVE:
+                case MultiSelectMenuActionNames.MOVE_TO:
+                case MultiSelectMenuActionNames.REMOVE:
                     const firstResource = getResource(selectedToArray(checkedList)[0])(resources) as ContainerRequestResource | Resource;
                     const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]);
                     if (action) action.execute(dispatch, kindGroups[firstResource.kind]);
                     break;
-                case contextMenuActionConsts.COPY_TO_CLIPBOARD:
+                case MultiSelectMenuActionNames.COPY_TO_CLIPBOARD:
                     const selectedResources = selectedToArray(checkedList).map(uuid => getResource(uuid)(resources));
                     dispatch<any>(copyToClipboardAction(selectedResources));
                     break;
index e2f643b6870e76d8f9e56b4e77ce49e679f29f80..5a84d4c573f711a46a4a4665b9acbcff5bf2f18f 100644 (file)
@@ -3,19 +3,21 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ResourceKind } from "models/resource";
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+import { MultiSelectMenuActionSet} from "views-components/multiselect-toolbar/ms-menu-actions";
 import { msCollectionActionSet } from "views-components/multiselect-toolbar/ms-collection-action-set";
 import { msProjectActionSet } from "views-components/multiselect-toolbar/ms-project-action-set";
 import { msProcessActionSet } from "views-components/multiselect-toolbar/ms-process-action-set";
+import { msWorkflowActionSet } from "views-components/multiselect-toolbar/ms-workflow-action-set";
 
-export function findActionByName(name: string, actionSet: ContextMenuActionSet) {
+export function findActionByName(name: string, actionSet: MultiSelectMenuActionSet) {
     return actionSet[0].find(action => action.name === name);
 }
 
-const { COLLECTION, PROJECT, PROCESS } = ResourceKind;
+const { COLLECTION, PROCESS, PROJECT, WORKFLOW } = ResourceKind;
 
-export const kindToActionSet: Record<string, ContextMenuActionSet> = {
+export const kindToActionSet: Record<string, MultiSelectMenuActionSet> = {
     [COLLECTION]: msCollectionActionSet,
-    [PROJECT]: msProjectActionSet,
     [PROCESS]: msProcessActionSet,
+    [PROJECT]: msProjectActionSet,
+    [WORKFLOW]: msWorkflowActionSet,
 };
index 9145a820694c7196c2676209e47cff43e327ae2d..b34cc22cb9ab05934e4ff6568527091ee9dce45a 100644 (file)
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ResourceKind } from "models/resource";
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { msCollectionActionSet } from "views-components/multiselect-toolbar/ms-collection-action-set";
-import { msProjectActionSet } from "views-components/multiselect-toolbar/ms-project-action-set";
-import { msProcessActionSet } from "views-components/multiselect-toolbar/ms-process-action-set";
+import { MultiSelectMenuActionSet } from 'views-components/multiselect-toolbar/ms-menu-actions';
+import { msCollectionActionSet, msCommonCollectionActionFilter, msReadOnlyCollectionActionFilter } from 'views-components/multiselect-toolbar/ms-collection-action-set';
+import {
+    msProjectActionSet,
+    msCommonProjectActionFilter,
+    msReadOnlyProjectActionFilter,
+    msFilterGroupActionFilter,
+    msAdminFilterGroupActionFilter,
+    msFrozenProjectActionFilter,
+    msAdminFrozenProjectActionFilter
+} from 'views-components/multiselect-toolbar/ms-project-action-set';
+import { msProcessActionSet, msCommonProcessActionFilter, msAdminProcessActionFilter, msRunningProcessActionFilter } from 'views-components/multiselect-toolbar/ms-process-action-set';
+import { msWorkflowActionSet, msWorkflowActionFilter, msReadOnlyWorkflowActionFilter } from 'views-components/multiselect-toolbar/ms-workflow-action-set';
+import { ResourceKind } from 'models/resource';
 
-export type TMultiselectActionsFilters = Record<string, [ContextMenuActionSet, Set<string>]>;
+export enum msMenuResourceKind {
+    API_CLIENT_AUTHORIZATION = 'ApiClientAuthorization',
+    ROOT_PROJECT = 'RootProject',
+    PROJECT = 'Project',
+    FILTER_GROUP = 'FilterGroup',
+    READONLY_PROJECT = 'ReadOnlyProject',
+    FROZEN_PROJECT = 'FrozenProject',
+    FROZEN_PROJECT_ADMIN = 'FrozenProjectAdmin',
+    PROJECT_ADMIN = 'ProjectAdmin',
+    FILTER_GROUP_ADMIN = 'FilterGroupAdmin',
+    RESOURCE = 'Resource',
+    FAVORITE = 'Favorite',
+    TRASH = 'Trash',
+    COLLECTION_FILES = 'CollectionFiles',
+    COLLECTION_FILES_MULTIPLE = 'CollectionFilesMultiple',
+    READONLY_COLLECTION_FILES = 'ReadOnlyCollectionFiles',
+    READONLY_COLLECTION_FILES_MULTIPLE = 'ReadOnlyCollectionFilesMultiple',
+    COLLECTION_FILES_NOT_SELECTED = 'CollectionFilesNotSelected',
+    COLLECTION_FILE_ITEM = 'CollectionFileItem',
+    COLLECTION_DIRECTORY_ITEM = 'CollectionDirectoryItem',
+    READONLY_COLLECTION_FILE_ITEM = 'ReadOnlyCollectionFileItem',
+    READONLY_COLLECTION_DIRECTORY_ITEM = 'ReadOnlyCollectionDirectoryItem',
+    COLLECTION = 'Collection',
+    COLLECTION_ADMIN = 'CollectionAdmin',
+    READONLY_COLLECTION = 'ReadOnlyCollection',
+    OLD_VERSION_COLLECTION = 'OldVersionCollection',
+    TRASHED_COLLECTION = 'TrashedCollection',
+    PROCESS = 'Process',
+    RUNNING_PROCESS_ADMIN = 'RunningProcessAdmin',
+    PROCESS_ADMIN = 'ProcessAdmin',
+    RUNNING_PROCESS_RESOURCE = 'RunningProcessResource',
+    PROCESS_RESOURCE = 'ProcessResource',
+    READONLY_PROCESS_RESOURCE = 'ReadOnlyProcessResource',
+    PROCESS_LOGS = 'ProcessLogs',
+    REPOSITORY = 'Repository',
+    SSH_KEY = 'SshKey',
+    VIRTUAL_MACHINE = 'VirtualMachine',
+    KEEP_SERVICE = 'KeepService',
+    USER = 'User',
+    GROUPS = 'Group',
+    GROUP_MEMBER = 'GroupMember',
+    PERMISSION_EDIT = 'PermissionEdit',
+    LINK = 'Link',
+    WORKFLOW = 'Workflow',
+    READONLY_WORKFLOW = 'ReadOnlyWorkflow',
+    SEARCH_RESULTS = 'SearchResults',
+}
 
-export const contextMenuActionConsts = {
-    MAKE_A_COPY: "Make a copy",
-    MOVE_TO: "Move to",
-    TOGGLE_TRASH_ACTION: "ToggleTrashAction",
-    COPY_TO_CLIPBOARD: "Copy to clipboard",
-    COPY_AND_RERUN_PROCESS: "Copy and re-run process",
-    REMOVE: "Remove",
-};
-
-const { MOVE_TO, TOGGLE_TRASH_ACTION, REMOVE, MAKE_A_COPY } = contextMenuActionConsts;
+const {
+    COLLECTION,
+    COLLECTION_ADMIN,
+    READONLY_COLLECTION,
+    PROCESS_RESOURCE,
+    RUNNING_PROCESS_RESOURCE,
+    RUNNING_PROCESS_ADMIN,
+    PROCESS_ADMIN,
+    PROJECT,
+    PROJECT_ADMIN,
+    FROZEN_PROJECT,
+    FROZEN_PROJECT_ADMIN,
+    READONLY_PROJECT,
+    FILTER_GROUP,
+    FILTER_GROUP_ADMIN,
+    WORKFLOW,
+    READONLY_WORKFLOW,
+} = msMenuResourceKind;
 
-//these sets govern what actions are on the ms toolbar for each resource kind
-const projectMSActionsFilter = new Set([MOVE_TO, TOGGLE_TRASH_ACTION]);
-const processResourceMSActionsFilter = new Set([MOVE_TO, REMOVE]);
-const collectionMSActionsFilter = new Set([MAKE_A_COPY, MOVE_TO, TOGGLE_TRASH_ACTION]);
+export type TMultiselectActionsFilters = Record<string, [MultiSelectMenuActionSet, Set<string>]>;
 
-const { COLLECTION, PROJECT, PROCESS } = ResourceKind;
+const allActionNames = (actionSet: MultiSelectMenuActionSet): Set<string> => new Set(actionSet[0].map((action) => action.name));
 
 export const multiselectActionsFilters: TMultiselectActionsFilters = {
-    [PROJECT]: [msProjectActionSet, projectMSActionsFilter],
-    [PROCESS]: [msProcessActionSet, processResourceMSActionsFilter],
-    [COLLECTION]: [msCollectionActionSet, collectionMSActionsFilter],
+    [COLLECTION]: [msCollectionActionSet, msCommonCollectionActionFilter],
+    [COLLECTION_ADMIN]: [msCollectionActionSet, allActionNames(msCollectionActionSet)],
+    [READONLY_COLLECTION]: [msCollectionActionSet, msReadOnlyCollectionActionFilter],
+    [ResourceKind.COLLECTION]: [msCollectionActionSet, msCommonCollectionActionFilter],
+
+    [PROCESS_RESOURCE]: [msProcessActionSet, msCommonProcessActionFilter],
+    [PROCESS_ADMIN]: [msProcessActionSet, msAdminProcessActionFilter],
+    [RUNNING_PROCESS_RESOURCE]: [msProcessActionSet, msRunningProcessActionFilter],
+    [RUNNING_PROCESS_ADMIN]: [msProcessActionSet, allActionNames(msProcessActionSet)],
+    [ResourceKind.PROCESS]: [msProcessActionSet, msCommonProcessActionFilter],
+    
+    [PROJECT]: [msProjectActionSet, msCommonProjectActionFilter],
+    [PROJECT_ADMIN]: [msProjectActionSet, allActionNames(msProjectActionSet)],
+    [FROZEN_PROJECT]: [msProjectActionSet, msFrozenProjectActionFilter],
+    [FROZEN_PROJECT_ADMIN]: [msProjectActionSet, msAdminFrozenProjectActionFilter], 
+    [READONLY_PROJECT]: [msProjectActionSet, msReadOnlyProjectActionFilter],
+    [ResourceKind.PROJECT]: [msProjectActionSet, msCommonProjectActionFilter],
+    
+    [FILTER_GROUP]: [msProjectActionSet, msFilterGroupActionFilter],
+    [FILTER_GROUP_ADMIN]: [msProjectActionSet, msAdminFilterGroupActionFilter],
+    
+    [WORKFLOW]: [msWorkflowActionSet, msWorkflowActionFilter],
+    [READONLY_WORKFLOW]: [msWorkflowActionSet, msReadOnlyWorkflowActionFilter],
 };
index 6107c40972d690df30ce36a02ed589f24d67fc13..772def29d05e9610ec53561234e8de523af3a18d 100644 (file)
@@ -2,12 +2,18 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { ofType, unionize } from 'common/unionize';
 import { Dispatch } from "redux";
 import { RootState } from "store/store";
 import { ServiceRepository } from "services/services";
 import { dialogActions } from 'store/dialog/dialog-actions';
-import { getNewExtraToken } from "../auth/auth-action";
 import { CollectionResource } from "models/collection";
+import { SshKeyResource } from 'models/ssh-key';
+import { User } from "models/user";
+import { Session } from "models/session";
+import { Config } from 'common/config';
+import { createServices, setAuthorizationHeader } from "services/services";
+import { getTokenV2 } from 'models/api-client-authorization';
 
 export const COLLECTION_WEBDAV_S3_DIALOG_NAME = 'collectionWebdavS3Dialog';
 
@@ -42,3 +48,77 @@ export const openWebDavS3InfoDialog = (uuid: string, activeTab?: number) =>
             }
         }));
     };
+
+const authActions = unionize({
+    LOGIN: {},
+    LOGOUT: ofType<{ deleteLinkData: boolean, preservePath: boolean }>(),
+    SET_CONFIG: ofType<{ config: Config }>(),
+    SET_EXTRA_TOKEN: ofType<{ extraApiToken: string, extraApiTokenExpiration?: Date }>(),
+    RESET_EXTRA_TOKEN: {},
+    INIT_USER: ofType<{ user: User, token: string, tokenExpiration?: Date, tokenLocation?: string }>(),
+    USER_DETAILS_REQUEST: {},
+    USER_DETAILS_SUCCESS: ofType<User>(),
+    SET_SSH_KEYS: ofType<SshKeyResource[]>(),
+    ADD_SSH_KEY: ofType<SshKeyResource>(),
+    REMOVE_SSH_KEY: ofType<string>(),
+    SET_HOME_CLUSTER: ofType<string>(),
+    SET_SESSIONS: ofType<Session[]>(),
+    ADD_SESSION: ofType<Session>(),
+    REMOVE_SESSION: ofType<string>(),
+    UPDATE_SESSION: ofType<Session>(),
+    REMOTE_CLUSTER_CONFIG: ofType<{ config: Config }>(),
+});
+
+const getConfig = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Config => {
+    const state = getState().auth;
+    return state.remoteHostsConfig[state.localCluster];
+};
+
+const getNewExtraToken =
+    (reuseStored: boolean = false) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const extraToken = getState().auth.extraApiToken;
+        if (reuseStored && extraToken !== undefined) {
+            const config = dispatch<any>(getConfig);
+            const svc = createServices(config, { progressFn: () => {}, errorFn: () => {} });
+            setAuthorizationHeader(svc, extraToken);
+            try {
+                // Check the extra token's validity before using it. Refresh its
+                // expiration date just in case it changed.
+                const client = await svc.apiClientAuthorizationService.get('current');
+                dispatch(
+                    authActions.SET_EXTRA_TOKEN({
+                        extraApiToken: extraToken,
+                        extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt) : undefined,
+                    })
+                );
+                return extraToken;
+            } catch (e) {
+                dispatch(authActions.RESET_EXTRA_TOKEN());
+            }
+        }
+        const user = getState().auth.user;
+        const loginCluster = getState().auth.config.clusterConfig.Login.LoginCluster;
+        if (user === undefined) {
+            return;
+        }
+        if (loginCluster !== '' && getState().auth.homeCluster !== loginCluster) {
+            return;
+        }
+        try {
+            // Do not show errors on the create call, cluster security configuration may not
+            // allow token creation and there's no way to know that from workbench2 side in advance.
+            const client = await services.apiClientAuthorizationService.create(undefined, false);
+            const newExtraToken = getTokenV2(client);
+            dispatch(
+                authActions.SET_EXTRA_TOKEN({
+                    extraApiToken: newExtraToken,
+                    extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt) : undefined,
+                })
+            );
+            return newExtraToken;
+        } catch {
+            console.warn("Cannot create new tokens with the current token, probably because of cluster's security settings.");
+            return;
+        }
+    };
\ No newline at end of file
index 1e23f35cbfd8b4d1beced3aae5ee043c28875a53..da454ed77dbc9561116cf43c6de5fc25ae8edc95 100644 (file)
@@ -10,6 +10,8 @@ import { checkFavorite } from "./favorites-reducer";
 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
 import { ServiceRepository } from "services/services";
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions";
 import { loadFavoritesTree} from "store/side-panel-tree/side-panel-tree-actions";
 
 export const favoritesActions = unionize({
@@ -27,6 +29,7 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) =>
             return Promise.reject("No user");
         }
         dispatch(progressIndicatorActions.START_WORKING("toggleFavorite"));
+        dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.ADD_TO_FAVORITES))
         dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
         const isFavorite = checkFavorite(resource.uuid, getState().favorites);
         dispatch(snackbarActions.OPEN_SNACKBAR({
@@ -51,6 +54,7 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) =>
                     hideDuration: 2000,
                     kind: SnackbarKind.SUCCESS
                 }));
+                dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.ADD_TO_FAVORITES))
                 dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
                 dispatch<any>(loadFavoritesTree())
             })
index 1c329a9e90496bb87b168e55b8302c9808f6c555..a246ddbcc02a597c05e293a7b2c79f5e2d04e641 100644 (file)
@@ -3,11 +3,45 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { TCheckedList } from "components/data-table/data-table";
+import { ContainerRequestResource } from "models/container-request";
+import { Dispatch } from "redux";
+import { navigateTo } from "store/navigation/navigation-action";
+import { snackbarActions } from "store/snackbar/snackbar-actions";
+import { RootState } from "store/store";
+import { ServiceRepository } from "services/services";
+import { SnackbarKind } from "store/snackbar/snackbar-actions";
+import { ContextMenuResource } from 'store/context-menu/context-menu-actions';
 
 export const multiselectActionContants = {
     TOGGLE_VISIBLITY: "TOGGLE_VISIBLITY",
     SET_CHECKEDLIST: "SET_CHECKEDLIST",
+    SELECT_ONE: 'SELECT_ONE',
     DESELECT_ONE: "DESELECT_ONE",
+    TOGGLE_ONE: 'TOGGLE_ONE',
+    SET_SELECTED_UUID: 'SET_SELECTED_UUID',
+    ADD_DISABLED: 'ADD_DISABLED',
+    REMOVE_DISABLED: 'REMOVE_DISABLED',
+};
+
+export const msNavigateToOutput = (resource: ContextMenuResource | ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    try {
+        await services.collectionService.get(resource.outputUuid || '');
+        dispatch<any>(navigateTo(resource.outputUuid || ''));
+    } catch {
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Output collection was trashed or deleted.", hideDuration: 4000, kind: SnackbarKind.WARNING }));
+    }
+};
+
+export const isExactlyOneSelected = (checkedList: TCheckedList) => {
+    let tally = 0;
+    let current = '';
+    for (const uuid in checkedList) {
+        if (checkedList[uuid] === true) {
+            tally++;
+            current = uuid;
+        }
+    }
+    return tally === 1 ? current : null
 };
 
 export const toggleMSToolbar = (isVisible: boolean) => {
@@ -18,18 +52,54 @@ export const toggleMSToolbar = (isVisible: boolean) => {
 
 export const setCheckedListOnStore = (checkedList: TCheckedList) => {
     return dispatch => {
+        dispatch(setSelectedUuid(isExactlyOneSelected(checkedList)))
         dispatch({ type: multiselectActionContants.SET_CHECKEDLIST, payload: checkedList });
     };
 };
 
+export const selectOne = (uuid: string) => {
+    return dispatch => {
+        dispatch({ type: multiselectActionContants.SELECT_ONE, payload: uuid });
+    };
+};
+
 export const deselectOne = (uuid: string) => {
     return dispatch => {
         dispatch({ type: multiselectActionContants.DESELECT_ONE, payload: uuid });
     };
 };
 
+export const toggleOne = (uuid: string) => {
+    return dispatch => {
+        dispatch({ type: multiselectActionContants.TOGGLE_ONE, payload: uuid });
+    };
+};
+
+export const setSelectedUuid = (uuid: string | null) => {
+    return dispatch => {
+        dispatch({ type: multiselectActionContants.SET_SELECTED_UUID, payload: uuid });
+    };
+};
+
+export const addDisabledButton = (buttonName: string) => {
+    return dispatch => {
+        dispatch({ type: multiselectActionContants.ADD_DISABLED, payload: buttonName });
+    };
+};
+
+export const removeDisabledButton = (buttonName: string) => {
+    return dispatch => {
+        dispatch({ type: multiselectActionContants.REMOVE_DISABLED, payload: buttonName });
+    };
+};
+
 export const multiselectActions = {
     toggleMSToolbar,
     setCheckedListOnStore,
+    selectOne,
     deselectOne,
+    toggleOne,
+    setSelectedUuid,
+    addDisabledButton,
+    removeDisabledButton,
 };
index 75c4b1f99388d5567b98cf7955c59db297355fd1..26b853933c066abd2e1629af203a6a4327d7b6bc 100644 (file)
@@ -8,14 +8,18 @@ import { TCheckedList } from "components/data-table/data-table";
 type MultiselectToolbarState = {
     isVisible: boolean;
     checkedList: TCheckedList;
+    selectedUuid: string;
+    disabledButtons: string[]
 };
 
 const multiselectToolbarInitialState = {
     isVisible: false,
     checkedList: {},
+    selectedUuid: '',
+    disabledButtons: []
 };
 
-const { TOGGLE_VISIBLITY, SET_CHECKEDLIST, DESELECT_ONE } = multiselectActionContants;
+const { TOGGLE_VISIBLITY, SET_CHECKEDLIST, SELECT_ONE, DESELECT_ONE, TOGGLE_ONE, SET_SELECTED_UUID, ADD_DISABLED, REMOVE_DISABLED } = multiselectActionContants;
 
 export const multiselectReducer = (state: MultiselectToolbarState = multiselectToolbarInitialState, action) => {
     switch (action.type) {
@@ -23,8 +27,18 @@ export const multiselectReducer = (state: MultiselectToolbarState = multiselectT
             return { ...state, isVisible: action.payload };
         case SET_CHECKEDLIST:
             return { ...state, checkedList: action.payload };
+        case SELECT_ONE:
+            return { ...state, checkedList: { ...state.checkedList, [action.payload]: true } };
         case DESELECT_ONE:
             return { ...state, checkedList: { ...state.checkedList, [action.payload]: false } };
+        case TOGGLE_ONE:
+            return { ...state, checkedList: { ...state.checkedList, [action.payload]: !state.checkedList[action.payload] } };
+        case SET_SELECTED_UUID:
+            return {...state, selectedUuid: action.payload || ''}
+        case ADD_DISABLED:
+            return { ...state, disabledButtons: [...state.disabledButtons, action.payload]}
+        case REMOVE_DISABLED:
+            return { ...state, disabledButtons: state.disabledButtons.filter((button) => button !== action.payload) };
         default:
             return state;
     }
index 81f8dd6ba0dc456f980ffaec17bf25000c5afa3d..2111afdb2fc89d05eaba56ad46add0c43beccf19 100644 (file)
@@ -20,6 +20,7 @@ import { CommandInputParameter, getIOParamId, WorkflowInputsData } from "models/
 import { getIOParamDisplayValue, ProcessIOParameter } from "views/process-panel/process-io-card";
 import { OutputDetails, NodeInstanceType, NodeInfo } from "./process-panel";
 import { AuthState } from "store/auth/auth-reducer";
+import { ContextMenuResource } from "store/context-menu/context-menu-actions";
 
 export const processPanelActions = unionize({
     RESET_PROCESS_PANEL: ofType<{}>(),
@@ -53,10 +54,10 @@ export const loadProcessPanel = (uuid: string) => async (dispatch: Dispatch, get
     dispatch<any>(loadSubprocessPanel());
 };
 
-export const navigateToOutput = (uuid: string) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+export const navigateToOutput = (resource: ContextMenuResource | ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
     try {
-        await services.collectionService.get(uuid);
-        dispatch<any>(navigateTo(uuid));
+        await services.collectionService.get(resource.outputUuid || '');
+        dispatch<any>(navigateTo(resource.outputUuid || ''));
     } catch {
         dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Output collection was trashed or deleted.", hideDuration: 4000, kind: SnackbarKind.WARNING }));
     }
index b72058d56e81dd0ed4a42dce663357ffcaea4dd9..366e15ae04759e2a774563d8307fa8c4449eb8fe 100644 (file)
@@ -35,6 +35,8 @@ import { updatePublicFavorites } from "store/public-favorites/public-favorites-a
 import { selectedFieldsOfGroup } from "models/group";
 import { defaultCollectionSelectedFields } from "models/collection";
 import { containerRequestFieldsNoMounts } from "models/container-request";
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { removeDisabledButton } from "store/multiselect/multiselect-actions";
 
 export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -76,7 +78,10 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService
                     api.dispatch(couldNotFetchProjectContents());
                 }
             } finally {
-                if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
+                if (!background) { 
+                    api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+                    api.dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.MOVE_TO_TRASH))
+                }
             }
         }
     }
index 84f886d6db6026a9f3ce3636d75044451d4567d0..28e934d1f85ff988f5d5fc73df6d2faadca3d450 100644 (file)
@@ -7,25 +7,31 @@ import { ServiceRepository } from "services/services";
 import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
 import { loadResource } from "store/resources/resources-actions";
 import { RootState } from "store/store";
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions";
 
 export const freezeProject = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.FREEZE_PROJECT))
     const userUUID = getState().auth.user!.uuid;
-
+    
     const updatedProject = await services.projectService.update(uuid, {
         frozenByUuid: userUUID,
     });
-
+    
     dispatch(projectPanelActions.REQUEST_ITEMS());
     dispatch<any>(loadResource(uuid, false));
+    dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.FREEZE_PROJECT))
     return updatedProject;
 };
 
 export const unfreezeProject = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.FREEZE_PROJECT))
     const updatedProject = await services.projectService.update(uuid, {
         frozenByUuid: null,
     });
 
     dispatch(projectPanelActions.REQUEST_ITEMS());
     dispatch<any>(loadResource(uuid, false));
+    dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.FREEZE_PROJECT))
     return updatedProject;
 };
index 363b0b44a97018b6885f68ca1ac0d589ef410f52..0f8ed6c2611c72e55fc769a2d5f4b7ab3d4c6408 100644 (file)
@@ -9,6 +9,8 @@ import { checkPublicFavorite } from "./public-favorites-reducer";
 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 import { ServiceRepository } from "services/services";
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions";
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
 import { loadPublicFavoritesTree } from "store/side-panel-tree/side-panel-tree-actions";
 
 export const publicFavoritesActions = unionize({
@@ -22,6 +24,7 @@ export type PublicFavoritesAction = UnionOf<typeof publicFavoritesActions>;
 export const togglePublicFavorite = (resource: { uuid: string; name: string }) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
         dispatch(progressIndicatorActions.START_WORKING("togglePublicFavorite"));
+        dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.ADD_TO_PUBLIC_FAVORITES))
         const uuidPrefix = getState().auth.config.uuidPrefix;
         const uuid = `${uuidPrefix}-j7d0g-publicfavorites`;
         dispatch(publicFavoritesActions.TOGGLE_PUBLIC_FAVORITE({ resourceUuid: resource.uuid }));
@@ -48,6 +51,7 @@ export const togglePublicFavorite = (resource: { uuid: string; name: string }) =
                     hideDuration: 2000,
                     kind: SnackbarKind.SUCCESS
                 }));
+                dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.ADD_TO_PUBLIC_FAVORITES))
                 dispatch(progressIndicatorActions.STOP_WORKING("togglePublicFavorite"));
                 dispatch<any>(loadPublicFavoritesTree())
             })
index d72b6ad7a1ab62ee59c83ce5e765ff6d5177de44..c822cece8742856330a7d7734cd655cae923aec7 100644 (file)
@@ -27,7 +27,8 @@ import { serializeResourceTypeFilters } from 'store//resource-type-filters/resou
 import { getDataExplorerColumnFilters } from 'store/data-explorer/data-explorer-middleware-service';
 import { joinFilters } from 'services/api/filter-builder';
 import { CollectionResource } from "models/collection";
-
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { removeDisabledButton } from "store/multiselect/multiselect-actions";
 export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
         super(id);
@@ -84,6 +85,7 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
             }));
             api.dispatch(couldNotFetchTrashContents());
         }
+        api.dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.MOVE_TO_TRASH))
     }
 }
 
index 62b669220e68e2e6d3089f878f7fe4e73f29b962..f4e3d3f0c4de225406cff2a8c4b6e1c9eed61fe9 100644 (file)
@@ -13,6 +13,8 @@ import { sharedWithMePanelActions } from "store/shared-with-me-panel/shared-with
 import { ResourceKind } from "models/resource";
 import { navigateTo, navigateToTrash } from "store/navigation/navigation-action";
 import { matchCollectionRoute, matchSharedWithMeRoute } from "routes/routes";
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { addDisabledButton } from "store/multiselect/multiselect-actions";
 
 export const toggleProjectTrashed =
     (uuid: string, ownerUuid: string, isTrashed: boolean, isMulti: boolean) =>
@@ -20,11 +22,12 @@ export const toggleProjectTrashed =
             let errorMessage = "";
             let successMessage = "";
             let untrashedResource;
+            dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.MOVE_TO_TRASH))
             try {
                 if (isTrashed) {
                     errorMessage = "Could not restore project from trash";
                     successMessage = "Restored project from trash";
-                    untrashedResource = await services.groupsService.untrash(uuid);
+                     untrashedResource = await services.groupsService.untrash(uuid);
                     dispatch<any>(isMulti || !untrashedResource ? navigateToTrash : navigateTo(uuid));
                     dispatch<any>(activateSidePanelTreeItem(uuid));
                 } else {
@@ -32,7 +35,7 @@ export const toggleProjectTrashed =
                     successMessage = "Added project to trash";
                     await services.groupsService.trash(uuid);
                     dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
-
+                    
                     const { location } = getState().router;
                     if (matchSharedWithMeRoute(location ? location.pathname : "")) {
                         dispatch(sharedWithMePanelActions.REQUEST_ITEMS());
@@ -42,7 +45,7 @@ export const toggleProjectTrashed =
                     }
                 }
                 if (untrashedResource) {
-                    dispatch(
+                        dispatch(
                         snackbarActions.OPEN_SNACKBAR({
                             message: successMessage,
                             hideDuration: 2000,
@@ -74,6 +77,7 @@ export const toggleCollectionTrashed =
         async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
             let errorMessage = "";
             let successMessage = "";
+            dispatch<any>(addDisabledButton(MultiSelectMenuActionNames.MOVE_TO_TRASH))
             try {
                 if (isTrashed) {
                     const { location } = getState().router;
index e89a95e039df04e19abbcf47e00237643b24998a..188dba05689edf9be63c7da67ed6c35c2db1df55 100644 (file)
@@ -335,7 +335,7 @@ export const moveProject =
                 } catch (e) {
                     dispatch(
                         snackbarActions.OPEN_SNACKBAR({
-                            message: e.message,
+                            message: !!(project as any).frozenByUuid ? 'Could not move frozen project.' : e.message,
                             hideDuration: 2000,
                             kind: SnackbarKind.ERROR,
                         })
index 64b90ff45c5d84a57b7eb832831b5bf0667dee45..2aa7faa1242369be4ea985bad80805b94529b72f 100644 (file)
@@ -60,8 +60,8 @@ export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [
             icon: OutputIcon,
             name: "Outputs",
             execute: (dispatch, resources) => {
-                if (resources[0].outputUuid) {
-                    dispatch<any>(navigateToOutput(resources[0].outputUuid));
+                if (resources[0]) {
+                    dispatch<any>(navigateToOutput(resources[0]));
                 }
             },
         },
index 2a5cccc0a549231d2860ec93bb5beec9d5435c1e..aeb69de7624bf3e27a82f7a911f7de1d57e03d4c 100644 (file)
@@ -16,7 +16,6 @@ type DataProps = Pick<ContextMenuProps, "anchorEl" | "items" | "open"> & { resou
 
 const mapStateToProps = (state: RootState): DataProps => {
     const { open, position, resource } = state.contextMenu;
-
     const filteredItems = getMenuActionSet(resource).map(group =>
         group.filter(item => {
             if (resource && item.filters) {
index d5a9977a71b4caad93d464d43d580ca457399e6c..2e316f68598b87b923122c905e633a1ad09ebba3 100644 (file)
@@ -22,13 +22,14 @@ interface Props {
     extractKey?: (item: any) => React.Key;
 }
 
-const mapStateToProps = (state: RootState, { id }: Props) => {
-    const progress = state.progressIndicator.find(p => p.id === id);
-    const dataExplorerState = getDataExplorer(state.dataExplorer, id);
-    const currentRoute = state.router.location ? state.router.location.pathname : "";
+const mapStateToProps = ({ progressIndicator, dataExplorer, router, multiselect, detailsPanel, properties}: RootState, { id }: Props) => {
+    const progress = progressIndicator.find(p => p.id === id);
+    const dataExplorerState = getDataExplorer(dataExplorer, id);
+    const currentRoute = router.location ? router.location.pathname : "";
     const currentRefresh = localStorage.getItem(LAST_REFRESH_TIMESTAMP) || "";
-    const currentItemUuid = currentRoute === "/workflows" ? state.properties.workflowPanelDetailsUuid : state.detailsPanel.resourceUuid;
-    const isMSToolbarVisible = state.multiselect.isVisible;
+    const isDetailsResourceChecked = multiselect.checkedList[detailsPanel.resourceUuid]
+    const currentItemUuid = currentRoute === "/workflows" ? properties.workflowPanelDetailsUuid : isDetailsResourceChecked ? detailsPanel.resourceUuid : multiselect.selectedUuid;
+    const isMSToolbarVisible = multiselect.isVisible;
     return {
         ...dataExplorerState,
         working: !!progress?.working,
@@ -37,11 +38,11 @@ const mapStateToProps = (state: RootState, { id }: Props) => {
         paperKey: currentRoute,
         currentItemUuid,
         isMSToolbarVisible,
-        checkedList: state.multiselect.checkedList,
+        checkedList: multiselect.checkedList,
     };
 };
 
-const mapDispatchToProps = dispatchFn => {
+const mapDispatchToProps = () => {
     return (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
         onSetColumns: (columns: DataColumns<any, any>) => {
             dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
index 2cbf038abb006bdc69e84b89b1ff21dae147a4c5..059aad4344fad0f49d55349f79e076a3f92d92e9 100644 (file)
@@ -68,7 +68,10 @@ const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
                 <Typography
                     color="primary"
                     style={{ width: "auto", cursor: "pointer" }}
-                    onClick={() => dispatch<any>(navFunc(item.uuid))}
+                    onClick={(ev) => {
+                        ev.stopPropagation()
+                        dispatch<any>(navFunc(item.uuid))
+                    }}
                 >
                     {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION ? <IllegalNamingWarning name={item.name} /> : null}
                     {item.name}
index e9175f57ba423e5069064db69876ddef15c97e1c..2653a2103345fe40a99cf9deb467e162efb05572 100644 (file)
@@ -83,8 +83,11 @@ const getItem = (res: DetailsResource): DetailsData => {
     }
 };
 
-const mapStateToProps = ({ auth, detailsPanel, resources, collectionPanelFiles }: RootState) => {
-    const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource | undefined;
+const mapStateToProps = ({ auth, detailsPanel, resources, collectionPanelFiles, multiselect, router }: RootState) => {
+    const isDetailsResourceChecked = multiselect.checkedList[detailsPanel.resourceUuid]
+    const currentRoute = router.location ? router.location.pathname : "";
+    const currentItemUuid = isDetailsResourceChecked || currentRoute.includes('collections') ? detailsPanel.resourceUuid : multiselect.selectedUuid ? multiselect.selectedUuid : currentRoute.split('/')[2];
+    const resource = getResource(currentItemUuid)(resources) as DetailsResource | undefined;
     const file = resource
         ? undefined
         : getNode(detailsPanel.resourceUuid)(collectionPanelFiles);
index b0a2a1b024793aafc8e86d269f468c7a49324862..a8a8f45748276dfd6a4578d6c54de7534c0ba13b 100644 (file)
@@ -2,37 +2,93 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { MoveToIcon, CopyIcon } from "components/icon/icon";
+import { MoveToIcon, CopyIcon, RenameIcon } from "components/icon/icon";
 import { openMoveCollectionDialog } from "store/collections/collection-move-actions";
 import { openCollectionCopyDialog, openMultiCollectionCopyDialog } from "store/collections/collection-copy-actions";
-import { ToggleTrashAction } from "views-components/context-menu/actions/trash-action";
 import { toggleCollectionTrashed } from "store/trash/trash-actions";
 import { ContextMenuResource } from "store/context-menu/context-menu-actions";
+import { msCommonActionSet, MultiSelectMenuActionSet, MultiSelectMenuAction } from "./ms-menu-actions";
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { TrashIcon, Link, FolderSharedIcon } from "components/icon/icon";
+import { openCollectionUpdateDialog } from "store/collections/collection-update-actions";
+import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
+import { openWebDavS3InfoDialog } from "store/collections/collection-info-actions";
 
-export const msCollectionActionSet: ContextMenuActionSet = [
+const { MAKE_A_COPY, MOVE_TO, MOVE_TO_TRASH, EDIT_COLLECTION, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, COPY_TO_CLIPBOARD, VIEW_DETAILS, API_DETAILS, ADD_TO_FAVORITES, SHARE} = MultiSelectMenuActionNames;
+
+const msCopyCollection: MultiSelectMenuAction = {
+    name: MAKE_A_COPY,
+    icon: CopyIcon,
+    hasAlts: false,
+    isForMulti: true,
+    execute: (dispatch, [...resources]) => {
+        if (resources[0].fromContextMenu || resources.length === 1) dispatch<any>(openCollectionCopyDialog(resources[0]));
+        else dispatch<any>(openMultiCollectionCopyDialog(resources[0]));
+    },
+}
+
+const msMoveCollection: MultiSelectMenuAction = {
+    name: MOVE_TO,
+    icon: MoveToIcon,
+    hasAlts: false,
+    isForMulti: true,
+    execute: (dispatch, resources) => dispatch<any>(openMoveCollectionDialog(resources[0])),
+}
+
+const msToggleTrashAction: MultiSelectMenuAction = {
+    name: MOVE_TO_TRASH,
+    icon: TrashIcon,
+    isForMulti: true,
+    hasAlts: false,
+    execute: (dispatch, resources: ContextMenuResource[]) => {
+        for (const resource of [...resources]) {
+            dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
+        }
+    },
+}
+
+const msEditCollection: MultiSelectMenuAction = {
+    name: MultiSelectMenuActionNames.EDIT_COLLECTION,
+    icon: RenameIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(openCollectionUpdateDialog(resources[0]));
+    },
+}
+
+const msCopyToClipboardMenuAction: MultiSelectMenuAction  = {
+    name: COPY_TO_CLIPBOARD,
+    icon: Link,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(copyToClipboardAction(resources));
+    },
+};
+
+const msOpenWith3rdPartyClientAction: MultiSelectMenuAction  = {
+    name: OPEN_W_3RD_PARTY_CLIENT,
+    icon: FolderSharedIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(openWebDavS3InfoDialog(resources[0].uuid));
+    },
+};
+
+export const msCollectionActionSet: MultiSelectMenuActionSet = [
     [
-        {
-            icon: CopyIcon,
-            name: "Make a copy",
-            execute: (dispatch, [...resources]) => {
-                if (resources[0].fromContextMenu || resources.length === 1) dispatch<any>(openCollectionCopyDialog(resources[0]));
-                else dispatch<any>(openMultiCollectionCopyDialog(resources[0]));
-            },
-        },
-        {
-            icon: MoveToIcon,
-            name: "Move to",
-            execute: (dispatch, resources) => dispatch<any>(openMoveCollectionDialog(resources[0])),
-        },
-        {
-            component: ToggleTrashAction,
-            name: "ToggleTrashAction",
-            execute: (dispatch, resources: ContextMenuResource[]) => {
-                for (const resource of [...resources]) {
-                    dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
-                }
-            },
-        },
+        ...msCommonActionSet,
+        msCopyCollection,
+        msMoveCollection,
+        msToggleTrashAction,
+        msEditCollection,
+        msCopyToClipboardMenuAction,
+        msOpenWith3rdPartyClientAction
     ],
 ];
+
+export const msReadOnlyCollectionActionFilter = new Set([OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, MAKE_A_COPY, VIEW_DETAILS, API_DETAILS, ADD_TO_FAVORITES, OPEN_W_3RD_PARTY_CLIENT]);
+export const msCommonCollectionActionFilter = new Set([OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, MAKE_A_COPY, VIEW_DETAILS, API_DETAILS, OPEN_W_3RD_PARTY_CLIENT, EDIT_COLLECTION, SHARE, MOVE_TO, ADD_TO_FAVORITES, MOVE_TO_TRASH])
+export const msOldCollectionActionFilter = new Set([OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, MAKE_A_COPY, VIEW_DETAILS, API_DETAILS, OPEN_W_3RD_PARTY_CLIENT, EDIT_COLLECTION, SHARE, MOVE_TO, ADD_TO_FAVORITES, MOVE_TO_TRASH])
\ No newline at end of file
diff --git a/src/views-components/multiselect-toolbar/ms-menu-actions.ts b/src/views-components/multiselect-toolbar/ms-menu-actions.ts
new file mode 100644 (file)
index 0000000..91e96d9
--- /dev/null
@@ -0,0 +1,144 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { IconType } from 'components/icon/icon';
+import { ResourcesState } from 'store/resources/resources';
+import { FavoritesState } from 'store/favorites/favorites-reducer';
+import { ContextMenuResource } from 'store/context-menu/context-menu-actions';
+import { AddFavoriteIcon, AdvancedIcon, DetailsIcon, OpenIcon, PublicFavoriteIcon, RemoveFavoriteIcon, ShareIcon } from 'components/icon/icon';
+import { checkFavorite } from 'store/favorites/favorites-reducer';
+import { toggleFavorite } from 'store/favorites/favorites-actions';
+import { favoritePanelActions } from 'store/favorite-panel/favorite-panel-action';
+import { openInNewTabAction } from 'store/open-in-new-tab/open-in-new-tab.actions';
+import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
+import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
+import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
+import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
+import { publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
+import { PublicFavoritesState } from 'store/public-favorites/public-favorites-reducer';
+
+export enum MultiSelectMenuActionNames {
+    ADD_TO_FAVORITES = 'Add to Favorites',
+    MOVE_TO_TRASH = 'Move to trash',
+    ADD_TO_PUBLIC_FAVORITES = 'Add to public favorites',
+    API_DETAILS = 'API Details',
+    CANCEL = 'CANCEL',
+    COPY_AND_RERUN_PROCESS = 'Copy and re-run process',
+    COPY_TO_CLIPBOARD = 'Copy to clipboard',
+    DELETE_WORKFLOW = 'Delete Workflow',
+    EDIT_COLLECTION = 'Edit collection',
+    EDIT_PROJECT = 'Edit project',
+    EDIT_PROCESS = 'Edit process',
+    FREEZE_PROJECT = 'Freeze Project',
+    MAKE_A_COPY = 'Make a copy',
+    MOVE_TO = 'Move to',
+    NEW_PROJECT = 'New project',
+    OPEN_IN_NEW_TAB = 'Open in new tab',
+    OPEN_W_3RD_PARTY_CLIENT = 'Open with 3rd party client',
+    OUTPUTS = 'Outputs',
+    REMOVE = 'Remove',
+    RUN_WORKFLOW = 'Run Workflow',
+    SHARE = 'Share',
+    VIEW_DETAILS = 'View details',
+};
+
+export type MultiSelectMenuAction = {
+    name: string;
+    icon: IconType;
+    hasAlts: boolean;
+    altName?: string;
+    altIcon?: IconType;
+    isForMulti: boolean;
+    useAlts?: (uuid: string | null, iconProps: {resources: ResourcesState, favorites: FavoritesState, publicFavorites: PublicFavoritesState}) => boolean;
+    execute(dispatch: Dispatch, resources: ContextMenuResource[], state?: any): void;
+    adminOnly?: boolean;
+};
+
+export type MultiSelectMenuActionSet = MultiSelectMenuAction[][];
+
+const { ADD_TO_FAVORITES, ADD_TO_PUBLIC_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, SHARE } = MultiSelectMenuActionNames;
+
+const msToggleFavoriteAction: MultiSelectMenuAction = {
+    name: ADD_TO_FAVORITES,
+    icon: AddFavoriteIcon,
+    hasAlts: true,
+    altName: 'Remove from Favorites',
+    altIcon: RemoveFavoriteIcon,
+    isForMulti: false,
+    useAlts: (uuid: string, iconProps) => {
+        return checkFavorite(uuid, iconProps.favorites);
+    },
+    execute: (dispatch, resources) => {
+        dispatch<any>(toggleFavorite(resources[0])).then(() => {
+            dispatch(favoritePanelActions.REQUEST_ITEMS());
+        });
+    },
+};
+
+const msOpenInNewTabMenuAction: MultiSelectMenuAction  = {
+    name: OPEN_IN_NEW_TAB,
+    icon: OpenIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(openInNewTabAction(resources[0]));
+    },
+};
+
+const msViewDetailsAction: MultiSelectMenuAction  = {
+    name: VIEW_DETAILS,
+    icon: DetailsIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch) => {
+        dispatch<any>(toggleDetailsPanel());
+    },
+};
+
+const msAdvancedAction: MultiSelectMenuAction  = {
+    name: API_DETAILS,
+    icon: AdvancedIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+    },
+};
+
+const msShareAction: MultiSelectMenuAction  = {
+    name: SHARE,
+    icon: ShareIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(openSharingDialog(resources[0].uuid));
+    },
+};
+
+const msTogglePublicFavoriteAction: MultiSelectMenuAction = {
+    name: ADD_TO_PUBLIC_FAVORITES,
+    icon: PublicFavoriteIcon,
+    hasAlts: true,
+    altName: 'Remove from public favorites',
+    altIcon: PublicFavoriteIcon,
+    isForMulti: false,
+    useAlts: (uuid: string, iconProps) => {
+        return iconProps.publicFavorites[uuid] === true
+    },
+    execute: (dispatch, resources) => {
+        dispatch<any>(togglePublicFavorite(resources[0])).then(() => {
+            dispatch(publicFavoritePanelActions.REQUEST_ITEMS());
+        });
+    },
+};
+
+export const msCommonActionSet = [
+    msToggleFavoriteAction,
+    msOpenInNewTabMenuAction,
+    msViewDetailsAction,
+    msAdvancedAction,
+    msShareAction,
+    msTogglePublicFavoriteAction
+];
index 820fc7999eeb0595c22fba64a7f64cccfd8e61f0..7802ad81f12cb303948c8dc7de82655478f21527 100644 (file)
@@ -2,36 +2,97 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { MoveToIcon, RemoveIcon, ReRunProcessIcon } from "components/icon/icon";
+import { MoveToIcon, RemoveIcon, ReRunProcessIcon, OutputIcon, RenameIcon, StopIcon } from "components/icon/icon";
 import { openMoveProcessDialog } from "store/processes/process-move-actions";
 import { openCopyProcessDialog } from "store/processes/process-copy-actions";
 import { openRemoveProcessDialog } from "store/processes/processes-actions";
+import { MultiSelectMenuAction, MultiSelectMenuActionSet, msCommonActionSet } from "./ms-menu-actions";
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { openProcessUpdateDialog } from "store/processes/process-update-actions";
+import { msNavigateToOutput } from "store/multiselect/multiselect-actions";
+import { cancelRunningWorkflow } from "store/processes/processes-actions";
 
-export const msProcessActionSet: ContextMenuActionSet = [
+const msCopyAndRerunProcess: MultiSelectMenuAction = {
+    name: MultiSelectMenuActionNames.COPY_AND_RERUN_PROCESS,
+    icon: ReRunProcessIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        for (const resource of [...resources]) {
+            dispatch<any>(openCopyProcessDialog(resource));
+        }
+    },
+}
+
+const msRemoveProcess: MultiSelectMenuAction = {
+    name: MultiSelectMenuActionNames.REMOVE,
+    icon: RemoveIcon,
+    hasAlts: false,
+    isForMulti: true,
+    execute: (dispatch, resources) => {
+        dispatch<any>(openRemoveProcessDialog(resources[0], resources.length));
+    },
+}
+
+const msMoveTo: MultiSelectMenuAction = {
+    name: MultiSelectMenuActionNames.MOVE_TO,
+    icon: MoveToIcon,
+    hasAlts: false,
+    isForMulti: true,
+    execute: (dispatch, resources) => {
+        dispatch<any>(openMoveProcessDialog(resources[0]));
+    },
+}
+
+const msViewOutputs: MultiSelectMenuAction = {
+    name: MultiSelectMenuActionNames.OUTPUTS,
+    icon: OutputIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+                if (resources[0]) {
+            dispatch<any>(msNavigateToOutput(resources[0]));
+        }
+    },
+}
+
+const msEditProcess: MultiSelectMenuAction = {
+    name: MultiSelectMenuActionNames.EDIT_PROCESS,
+    icon: RenameIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(openProcessUpdateDialog(resources[0]));
+    },
+}
+
+const msCancelProcess: MultiSelectMenuAction = {
+    name: MultiSelectMenuActionNames.CANCEL,
+    icon: StopIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(cancelRunningWorkflow(resources[0].uuid));
+    },
+}
+
+export const msProcessActionSet: MultiSelectMenuActionSet = [
     [
-        {
-            icon: ReRunProcessIcon,
-            name: "Copy and re-run process",
-            execute: (dispatch, resources) => {
-                for (const resource of [...resources]) {
-                    dispatch<any>(openCopyProcessDialog(resource));
-                }
-            },
-        },
-        {
-            icon: MoveToIcon,
-            name: "Move to",
-            execute: (dispatch, resources) => {
-                dispatch<any>(openMoveProcessDialog(resources[0]));
-            },
-        },
-        {
-            name: "Remove",
-            icon: RemoveIcon,
-            execute: (dispatch, resources) => {
-                dispatch<any>(openRemoveProcessDialog(resources[0], resources.length));
-            },
-        },
-    ],
+        ...msCommonActionSet,
+        msCopyAndRerunProcess,
+        msRemoveProcess,
+        msMoveTo,
+        msViewOutputs,
+        msEditProcess,
+        msCancelProcess
+    ]
 ];
+
+const { MOVE_TO, REMOVE, COPY_AND_RERUN_PROCESS, ADD_TO_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, SHARE, ADD_TO_PUBLIC_FAVORITES, OUTPUTS, EDIT_PROCESS, CANCEL } = MultiSelectMenuActionNames
+
+export const msCommonProcessActionFilter = new Set([MOVE_TO, REMOVE, COPY_AND_RERUN_PROCESS, ADD_TO_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, SHARE, OUTPUTS, EDIT_PROCESS ]);
+export const msRunningProcessActionFilter = new Set([MOVE_TO, REMOVE, COPY_AND_RERUN_PROCESS, ADD_TO_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, SHARE, OUTPUTS, EDIT_PROCESS, CANCEL ]);
+
+export const msReadOnlyProcessActionFilter = new Set([COPY_AND_RERUN_PROCESS, ADD_TO_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, OUTPUTS ]);
+export const msAdminProcessActionFilter = new Set([MOVE_TO, REMOVE, COPY_AND_RERUN_PROCESS, ADD_TO_FAVORITES, OPEN_IN_NEW_TAB, VIEW_DETAILS, API_DETAILS, SHARE, ADD_TO_PUBLIC_FAVORITES, OUTPUTS, EDIT_PROCESS ]);
+
index 2b5dfa22a5d9a0c16fd1f8fdf07534c91c87ce6f..ee1ea1d1792911ad850791caba0789122441d5fd 100644 (file)
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { MoveToIcon, Link } from "components/icon/icon";
-import { openMoveProjectDialog } from "store/projects/project-move-actions";
-import { ToggleTrashAction } from "views-components/context-menu/actions/trash-action";
-import { toggleProjectTrashed } from "store/trash/trash-actions";
-import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
-
-export const msCopyToClipboardMenuAction = {
+import { MultiSelectMenuAction, MultiSelectMenuActionSet, msCommonActionSet } from 'views-components/multiselect-toolbar/ms-menu-actions';
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { openMoveProjectDialog } from 'store/projects/project-move-actions';
+import { toggleProjectTrashed } from 'store/trash/trash-actions';
+import {
+    FreezeIcon,
+    MoveToIcon,
+    NewProjectIcon,
+    RenameIcon,
+    UnfreezeIcon,
+} from 'components/icon/icon';
+import { RestoreFromTrashIcon, TrashIcon, FolderSharedIcon, Link } from 'components/icon/icon';
+import { getResource } from 'store/resources/resources';
+import { openProjectCreateDialog } from 'store/projects/project-create-actions';
+import { openProjectUpdateDialog } from 'store/projects/project-update-actions';
+import { freezeProject, unfreezeProject } from 'store/projects/project-lock-actions';
+import { openWebDavS3InfoDialog } from 'store/collections/collection-info-actions';
+import { copyToClipboardAction } from 'store/open-in-new-tab/open-in-new-tab.actions';
+
+const {
+    ADD_TO_FAVORITES,
+    ADD_TO_PUBLIC_FAVORITES,
+    OPEN_IN_NEW_TAB,
+    COPY_TO_CLIPBOARD,
+    VIEW_DETAILS,
+    API_DETAILS,
+    OPEN_W_3RD_PARTY_CLIENT,
+    EDIT_PROJECT,
+    SHARE,
+    MOVE_TO,
+    MOVE_TO_TRASH,
+    FREEZE_PROJECT,
+    NEW_PROJECT,
+} = MultiSelectMenuActionNames;
+
+const msCopyToClipboardMenuAction: MultiSelectMenuAction  = {
+    name: COPY_TO_CLIPBOARD,
     icon: Link,
-    name: "Copy to clipboard",
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(copyToClipboardAction(resources));
+    },
+};
+
+const msEditProjectAction: MultiSelectMenuAction = {
+    name: EDIT_PROJECT,
+    icon: RenameIcon,
+    hasAlts: false,
+    isForMulti: false,
     execute: (dispatch, resources) => {
-        dispatch(copyToClipboardAction(resources));
+        dispatch<any>(openProjectUpdateDialog(resources[0]));
     },
 };
 
-export const msMoveToAction = {
+const msMoveToAction: MultiSelectMenuAction = {
+    name: MOVE_TO,
     icon: MoveToIcon,
-    name: "Move to",
+    hasAlts: false,
+    isForMulti: true,
     execute: (dispatch, resource) => {
-        dispatch(openMoveProjectDialog(resource[0]));
+        dispatch<any>(openMoveProjectDialog(resource[0]));
     },
 };
 
-export const msToggleTrashAction = {
-    component: ToggleTrashAction,
-    name: "ToggleTrashAction",
+const msOpenWith3rdPartyClientAction: MultiSelectMenuAction  = {
+    name: OPEN_W_3RD_PARTY_CLIENT,
+    icon: FolderSharedIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(openWebDavS3InfoDialog(resources[0].uuid));
+    },
+};
+
+export const msToggleTrashAction: MultiSelectMenuAction = {
+    name: MOVE_TO_TRASH,
+    icon: TrashIcon,
+    hasAlts: true,
+    altName: 'Restore from Trash',
+    altIcon: RestoreFromTrashIcon,
+    isForMulti: true,
+    useAlts: (uuid, iconProps) => {
+        return uuid ? (getResource(uuid)(iconProps.resources) as any).isTrashed : false;
+    },
     execute: (dispatch, resources) => {
         for (const resource of [...resources]) {
-            dispatch(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!, resources.length > 1));
+            dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!, resources.length > 1));
         }
     },
 };
 
-export const msProjectActionSet: ContextMenuActionSet = [[msCopyToClipboardMenuAction, msMoveToAction, msToggleTrashAction]];
+const msFreezeProjectAction: MultiSelectMenuAction = {
+    name: FREEZE_PROJECT,
+    icon: FreezeIcon,
+    hasAlts: true,
+    altName: 'Unfreeze Project',
+    altIcon: UnfreezeIcon,
+    isForMulti: false,
+    useAlts: (uuid, iconProps) => {
+        return uuid ? !!(getResource(uuid)(iconProps.resources) as any).frozenByUuid : false;
+    },
+    execute: (dispatch, resources) => {
+        if ((resources[0] as any).frozenByUuid) {
+            dispatch<any>(unfreezeProject(resources[0].uuid));
+        } else {
+            dispatch<any>(freezeProject(resources[0].uuid));
+        }
+    },
+};
+
+const msNewProjectAction: MultiSelectMenuAction = {
+    name: NEW_PROJECT,
+    icon: NewProjectIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources): void => {
+        dispatch<any>(openProjectCreateDialog(resources[0].uuid));
+    },
+};
+
+export const msProjectActionSet: MultiSelectMenuActionSet = [
+    [
+        ...msCommonActionSet,
+        msEditProjectAction,
+        msMoveToAction,
+        msToggleTrashAction,
+        msNewProjectAction,
+        msFreezeProjectAction,
+        msOpenWith3rdPartyClientAction,
+        msCopyToClipboardMenuAction
+    ],
+];
+
+export const msCommonProjectActionFilter = new Set<string>([
+    ADD_TO_FAVORITES,
+    MOVE_TO_TRASH,
+    API_DETAILS,
+    COPY_TO_CLIPBOARD,
+    EDIT_PROJECT,
+    FREEZE_PROJECT,
+    MOVE_TO,
+    NEW_PROJECT,
+    OPEN_IN_NEW_TAB,
+    OPEN_W_3RD_PARTY_CLIENT,
+    SHARE,
+    VIEW_DETAILS,
+]);
+export const msReadOnlyProjectActionFilter = new Set<string>([ADD_TO_FAVORITES, API_DETAILS, COPY_TO_CLIPBOARD, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, VIEW_DETAILS,]);
+export const msFrozenProjectActionFilter = new Set<string>([ADD_TO_FAVORITES, API_DETAILS, COPY_TO_CLIPBOARD, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, VIEW_DETAILS, SHARE, FREEZE_PROJECT])
+export const msAdminFrozenProjectActionFilter = new Set<string>([ADD_TO_FAVORITES, API_DETAILS, COPY_TO_CLIPBOARD, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, VIEW_DETAILS, SHARE, FREEZE_PROJECT, ADD_TO_PUBLIC_FAVORITES])
+
+export const msFilterGroupActionFilter = new Set<string>([ADD_TO_FAVORITES, API_DETAILS, COPY_TO_CLIPBOARD, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, VIEW_DETAILS, SHARE, MOVE_TO_TRASH, EDIT_PROJECT, MOVE_TO])
+export const msAdminFilterGroupActionFilter = new Set<string>([ADD_TO_FAVORITES, API_DETAILS, COPY_TO_CLIPBOARD, OPEN_IN_NEW_TAB, OPEN_W_3RD_PARTY_CLIENT, VIEW_DETAILS, SHARE, MOVE_TO_TRASH, EDIT_PROJECT, MOVE_TO, ADD_TO_PUBLIC_FAVORITES])
\ No newline at end of file
diff --git a/src/views-components/multiselect-toolbar/ms-workflow-action-set.ts b/src/views-components/multiselect-toolbar/ms-workflow-action-set.ts
new file mode 100644 (file)
index 0000000..ab819df
--- /dev/null
@@ -0,0 +1,46 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { openRunProcess, deleteWorkflow } from 'store/workflow-panel/workflow-panel-actions';
+import { StartIcon, TrashIcon, Link } from 'components/icon/icon';
+import { MultiSelectMenuAction, MultiSelectMenuActionSet, msCommonActionSet } from './ms-menu-actions';
+import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { copyToClipboardAction } from 'store/open-in-new-tab/open-in-new-tab.actions';
+
+const { OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, VIEW_DETAILS, API_DETAILS, RUN_WORKFLOW, DELETE_WORKFLOW } = MultiSelectMenuActionNames;
+
+const msRunWorkflow: MultiSelectMenuAction = {
+    name: RUN_WORKFLOW,
+    icon: StartIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(openRunProcess(resources[0].uuid, resources[0].ownerUuid, resources[0].name));
+    },
+};
+
+const msDeleteWorkflow: MultiSelectMenuAction = {
+    name: DELETE_WORKFLOW,
+    icon: TrashIcon,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(deleteWorkflow(resources[0].uuid, resources[0].ownerUuid));
+    },
+};
+
+const msCopyToClipboardMenuAction: MultiSelectMenuAction  = {
+    name: COPY_TO_CLIPBOARD,
+    icon: Link,
+    hasAlts: false,
+    isForMulti: false,
+    execute: (dispatch, resources) => {
+        dispatch<any>(copyToClipboardAction(resources));
+    },
+};
+
+export const msWorkflowActionSet: MultiSelectMenuActionSet = [[...msCommonActionSet, msRunWorkflow, msDeleteWorkflow, msCopyToClipboardMenuAction]];
+
+export const msReadOnlyWorkflowActionFilter = new Set([OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, VIEW_DETAILS, API_DETAILS, RUN_WORKFLOW ]);
+export const msWorkflowActionFilter = new Set([OPEN_IN_NEW_TAB, COPY_TO_CLIPBOARD, VIEW_DETAILS, API_DETAILS, RUN_WORKFLOW, DELETE_WORKFLOW]);
index ee53f99c3fe94e39c4fa2da096d8e7175d774223..0ccb0502cb28faabe774b4f7b4aba64c2a7b1313 100644 (file)
@@ -31,6 +31,7 @@ import { createTree } from "models/tree";
 import { getInitialProcessStatusFilters, getInitialProcessTypeFilters } from "store/resource-type-filters/resource-type-filters";
 import { getProcess } from "store/processes/process";
 import { ResourcesState } from "store/resources/resources";
+import { toggleOne } from "store/multiselect/multiselect-actions";
 
 type CssRules = "toolbar" | "button" | "root";
 
@@ -143,6 +144,7 @@ export const AllProcessesPanel = withStyles(styles)(
             };
 
             handleRowClick = (uuid: string) => {
+                this.props.dispatch<any>(toggleOne(uuid))
                 this.props.dispatch<any>(loadDetailsPanel(uuid));
             };
 
index d93d6e9258673e5dc49979a6262f9a9bef47570d..28983457e6e7c5a5d2821bad5cc782ecd37ee216 100644 (file)
@@ -350,7 +350,7 @@ export const CollectionDetailsAttributes = (props: CollectionDetailsProps) => {
         </Grid>
         <Grid item xs={12} md={mdSize}>
             <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                label='Storage classes' value={item.storageClassesDesired.join(', ')} />
+                label='Storage classes' value={item.storageClassesDesired ? item.storageClassesDesired.join(', ') : ["default"]} />
         </Grid>
 
         {/*
index 2392d6fda0380cc2e0108fb8101ae952b18bcf31..aa4f2c1a20a0637d1c3effbf839b6b1219e99974 100644 (file)
@@ -38,6 +38,7 @@ import { GroupClass, GroupResource } from 'models/group';
 import { getProperty } from 'store/properties/properties';
 import { PROJECT_PANEL_CURRENT_UUID } from 'store/project-panel/project-panel-action';
 import { CollectionResource } from 'models/collection';
+import { toggleOne } from 'store/multiselect/multiselect-actions';
 
 type CssRules = "toolbar" | "button" | "root";
 
@@ -171,6 +172,7 @@ export const FavoritePanel = withStyles(styles)(
             }
 
             handleRowClick = (uuid: string) => {
+                this.props.dispatch<any>(toggleOne(uuid))
                 this.props.dispatch<any>(loadDetailsPanel(uuid));
             }
 
index ffacd967f4d8e289edf6f9bf66bf31a3c59d2ea1..1f3a73a510f97c0e7f903a2bbe5c20b2feca44a0 100644 (file)
@@ -75,12 +75,12 @@ const mapStateToProps = (state: RootState, props: { request: ProcessResource })
 };
 
 interface ProcessDetailsAttributesActionProps {
-    navigateToOutput: (uuid: string) => void;
+    navigateToOutput: (resource: ContainerRequestResource) => void;
     openWorkflow: (uuid: string) => void;
 }
 
 const mapDispatchToProps = (dispatch: Dispatch): ProcessDetailsAttributesActionProps => ({
-    navigateToOutput: (uuid) => dispatch<any>(navigateToOutput(uuid)),
+    navigateToOutput: (resource) => dispatch<any>(navigateToOutput(resource)),
     openWorkflow: (uuid) => dispatch<any>(openWorkflow(uuid)),
 });
 
@@ -156,7 +156,7 @@ export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
                 </Grid>
                 <Grid item xs={6}>
                     <DetailsAttribute label='Output collection' />
-                    {containerRequest.outputUuid && <span onClick={() => props.navigateToOutput(containerRequest.outputUuid!)}>
+                    {containerRequest.outputUuid && <span onClick={() => props.navigateToOutput(containerRequest!)}>
                         <CollectionName className={classes.link} uuid={containerRequest.outputUuid} />
                     </span>}
                 </Grid>
index 2cc751bffd6e78526b38ea07e8d0a5ca4d0683f2..efaf53eb49b21334d87227740cfcaabf9ceaaa33 100644 (file)
@@ -52,6 +52,7 @@ import { CollectionResource } from 'models/collection';
 import { resourceIsFrozen } from 'common/frozen-resources';
 import { ProjectResource } from 'models/project';
 import { NotFoundView } from 'views/not-found-panel/not-found-panel';
+import { toggleOne } from 'store/multiselect/multiselect-actions';
 
 type CssRules = 'root' | 'button';
 
@@ -323,6 +324,7 @@ export const ProjectPanel = withStyles(styles)(
             };
 
             handleRowClick = (uuid: string) => {
+                this.props.dispatch<any>(toggleOne(uuid))
                 this.props.dispatch<any>(loadDetailsPanel(uuid));
             };
         }
index 47c8aedebfc7645ca4cf451c749ce60418bcd1eb..5cb10c4c66b9af0fdf5b71317c4aad9384b2b0af 100644 (file)
@@ -36,6 +36,7 @@ import { PublicFavoritesState } from 'store/public-favorites/public-favorites-re
 import { getResource, ResourcesState } from 'store/resources/resources';
 import { GroupContentsResource } from 'services/groups-service/groups-service';
 import { CollectionResource } from 'models/collection';
+import { toggleOne } from 'store/multiselect/multiselect-actions';
 
 type CssRules = "toolbar" | "button" | "root";
 
@@ -145,7 +146,8 @@ const mapDispatchToProps = (dispatch: Dispatch): PublicFavoritePanelActionProps
     },
     onDialogOpen: (ownerUuid: string) => { return; },
     onItemClick: (uuid: string) => {
-        dispatch<any>(loadDetailsPanel(uuid));
+                dispatch<any>(toggleOne(uuid))
+                dispatch<any>(loadDetailsPanel(uuid));
     },
     onItemDoubleClick: uuid => {
         dispatch<any>(navigateTo(uuid));
index 0902f15bdcca963a77369b970ff0e2d959cb31f0..320e85cb997afcdf160895e94c17c43b850b2d8f 100644 (file)
@@ -13,6 +13,7 @@ import { SearchBarAdvancedFormData } from 'models/search-bar';
 import { User } from "models/user";
 import { Config } from 'common/config';
 import { Session } from "models/session";
+import { toggleOne } from "store/multiselect/multiselect-actions";
 
 export interface SearchResultsPanelDataProps {
     data: SearchBarAdvancedFormData;
@@ -46,6 +47,7 @@ const mapDispatchToProps = (dispatch: Dispatch): SearchResultsPanelActionProps =
     },
     onDialogOpen: (ownerUuid: string) => { return; },
     onItemClick: (resourceUuid: string) => {
+        dispatch<any>(toggleOne(resourceUuid))
         dispatch<any>(loadDetailsPanel(resourceUuid));
     },
     onItemDoubleClick: uuid => {
index 250447ea95d10cc927f78ee85aae0df5c5bfe740..f3f827d1469fc27fe8c90d8f543123ba4755698d 100644 (file)
@@ -41,6 +41,7 @@ import {
 } from 'views-components/data-explorer/renderers';
 import { DataTableFilterItem } from 'components/data-table-filters/data-table-filters';
 import { GroupContentsResource } from 'services/groups-service/groups-service';
+import { toggleOne } from 'store/multiselect/multiselect-actions';
 import { DataColumns } from 'components/data-table/data-table';
 import { ContainerRequestState } from 'models/container-request';
 import { ProjectResource } from 'models/project';
@@ -280,6 +281,7 @@ export const SharedWithMePanel = withStyles(styles)(
             }
 
             handleRowClick = (uuid: string) => {
+                this.props.dispatch<any>(toggleOne(uuid))
                 this.props.dispatch<any>(loadDetailsPanel(uuid));
             }
         }
index 350207510555ac30870e21dd19916e9399c27534..2a96ffe0d7cf76f2b34c500dfecde6e6e9f8a071 100644 (file)
@@ -35,6 +35,7 @@ import {
     getTrashPanelTypeFilters
 } from 'store/resource-type-filters/resource-type-filters';
 import { CollectionResource } from 'models/collection';
+import { toggleOne } from 'store/multiselect/multiselect-actions';
 
 type CssRules = "toolbar" | "button" | "root";
 
@@ -178,6 +179,7 @@ export const TrashPanel = withStyles(styles)(
             }
 
             handleRowClick = (uuid: string) => {
+                this.props.dispatch<any>(toggleOne(uuid))
                 this.props.dispatch<any>(loadDetailsPanel(uuid));
             }
         }
index 4a2cd7009804335bfb5534946cc7cbbf529d46e9..bc2396f7cf8c2930444d6ecb3bb80324a9d5326d 100644 (file)
@@ -300,7 +300,7 @@ const applyCollapsedState = isCollapsed => {
 
 export const WorkbenchPanel = withStyles(styles)((props: WorkbenchPanelProps) => {
     //panel size will not scale automatically on window resize, so we do it manually
-    window.addEventListener("resize", () => applyCollapsedState(props.sidePanelIsCollapsed));
+    if (props && props.sidePanelIsCollapsed) window.addEventListener("resize", () => applyCollapsedState(props.sidePanelIsCollapsed));
     applyCollapsedState(props.sidePanelIsCollapsed);
 
     return (