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.
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)}`,
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)}`,
});
});
+
+
it("uses the editor (from details panel) with vocabulary terms", function () {
cy.createCollection(adminUser.token, {
name: `Test collection ${Math.floor(Math.random() * 999999)}`,
--- /dev/null
+// 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');
+ });
+});
});
}
+ 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(
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();
});
});
- it("should be able to froze own project", () => {
+ it("should be able to freeze own project", () => {
cy.getAll("@mainProject").then(([mainProject]) => {
cy.loginAs(activeUser);
});
});
- 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);
});
});
- 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);
});
});
});
+
+ 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');
+ }
+ });
+ })
+
});
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 />}
>
<IconButton
className={this.props.classes.moreOptionsButton}
- onClick={event => this.props.onContextMenu(event, item)}
+ onClick={event => {
+ event.stopPropagation()
+ this.props.onContextMenu(event, item)
+ }}
>
<MoreVerticalIcon />
</IconButton>
if (items.length) this.initializeCheckedList(items);
else setCheckedListOnStore({});
}
+ if (prevProps.currentRoute !== this.props.currentRoute) {
+ this.initializeCheckedList([])
+ }
+ }
+
+ componentWillUnmount(): void {
+ this.initializeCheckedList([])
}
checkBoxColumn: DataColumn<any, any> = {
const { classes, checkedList } = this.props;
return (
<input
+ data-cy={`multiselect-checkbox-${uuid}`}
type="checkbox"
name={uuid}
className={classes.checkBox}
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)}
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>
//
// 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";
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: {
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>
- );
+ )
})
);
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]);
});
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;
});
});
}
+
//--------------------------------------------------//
-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) {
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;
// 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,
};
//
// 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],
};
//
// 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';
}
}));
};
+
+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
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({
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({
hideDuration: 2000,
kind: SnackbarKind.SUCCESS
}));
+ dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.ADD_TO_FAVORITES))
dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
dispatch<any>(loadFavoritesTree())
})
// 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) => {
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,
};
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) {
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;
}
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<{}>(),
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 }));
}
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) {
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))
+ }
}
}
}
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;
};
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({
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 }));
hideDuration: 2000,
kind: SnackbarKind.SUCCESS
}));
+ dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.ADD_TO_PUBLIC_FAVORITES))
dispatch(progressIndicatorActions.STOP_WORKING("togglePublicFavorite"));
dispatch<any>(loadPublicFavoritesTree())
})
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);
}));
api.dispatch(couldNotFetchTrashContents());
}
+ api.dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.MOVE_TO_TRASH))
}
}
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) =>
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 {
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());
}
}
if (untrashedResource) {
- dispatch(
+ dispatch(
snackbarActions.OPEN_SNACKBAR({
message: successMessage,
hideDuration: 2000,
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;
} 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,
})
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]));
}
},
},
const mapStateToProps = (state: RootState): DataProps => {
const { open, position, resource } = state.contextMenu;
-
const filteredItems = getMenuActionSet(resource).map(group =>
group.filter(item => {
if (resource && item.filters) {
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,
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 }));
<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}
}
};
-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);
//
// 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
--- /dev/null
+// 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
+];
//
// 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 ]);
+
//
// 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
--- /dev/null
+// 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]);
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";
};
handleRowClick = (uuid: string) => {
+ this.props.dispatch<any>(toggleOne(uuid))
this.props.dispatch<any>(loadDetailsPanel(uuid));
};
</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>
{/*
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";
}
handleRowClick = (uuid: string) => {
+ this.props.dispatch<any>(toggleOne(uuid))
this.props.dispatch<any>(loadDetailsPanel(uuid));
}
};
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)),
});
</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>
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';
};
handleRowClick = (uuid: string) => {
+ this.props.dispatch<any>(toggleOne(uuid))
this.props.dispatch<any>(loadDetailsPanel(uuid));
};
}
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";
},
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));
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;
},
onDialogOpen: (ownerUuid: string) => { return; },
onItemClick: (resourceUuid: string) => {
+ dispatch<any>(toggleOne(resourceUuid))
dispatch<any>(loadDetailsPanel(resourceUuid));
},
onItemDoubleClick: uuid => {
} 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';
}
handleRowClick = (uuid: string) => {
+ this.props.dispatch<any>(toggleOne(uuid))
this.props.dispatch<any>(loadDetailsPanel(uuid));
}
}
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";
}
handleRowClick = (uuid: string) => {
+ this.props.dispatch<any>(toggleOne(uuid))
this.props.dispatch<any>(loadDetailsPanel(uuid));
}
}
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 (