diff --git a/lint/dist/generate-docs.js b/lint/dist/generate-docs.js new file mode 100644 index 00000000000..98eb5b74cbc --- /dev/null +++ b/lint/dist/generate-docs.js @@ -0,0 +1,59 @@ +"use strict"; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = require("fs"); +const path_1 = require("path"); +const html_1 = __importDefault(require("./src/rules/html")); +const ts_1 = __importDefault(require("./src/rules/ts")); +const templates = new Map(); +function lazyEJS(path, data) { + if (!templates.has(path)) { + templates.set(path, require('ejs').compile((0, fs_1.readFileSync)(path).toString())); + } + return templates.get(path)(data).replace(/\r\n/g, '\n'); +} +const docsDir = (0, path_1.join)('docs', 'lint'); +const tsDir = (0, path_1.join)(docsDir, 'ts'); +const htmlDir = (0, path_1.join)(docsDir, 'html'); +if ((0, fs_1.existsSync)(docsDir)) { + (0, fs_1.rmSync)(docsDir, { recursive: true }); +} +(0, fs_1.mkdirSync)((0, path_1.join)(tsDir, 'rules'), { recursive: true }); +(0, fs_1.mkdirSync)((0, path_1.join)(htmlDir, 'rules'), { recursive: true }); +function template(name) { + return (0, path_1.join)('lint', 'src', 'util', 'templates', name); +} +// TypeScript docs +(0, fs_1.writeFileSync)((0, path_1.join)(tsDir, 'index.md'), lazyEJS(template('index.ejs'), { + plugin: ts_1.default, + rules: ts_1.default.index.map(rule => rule.info), +})); +for (const rule of ts_1.default.index) { + (0, fs_1.writeFileSync)((0, path_1.join)(tsDir, 'rules', rule.info.name + '.md'), lazyEJS(template('rule.ejs'), { + plugin: ts_1.default, + rule: rule.info, + tests: rule.tests, + })); +} +// HTML docs +(0, fs_1.writeFileSync)((0, path_1.join)(htmlDir, 'index.md'), lazyEJS(template('index.ejs'), { + plugin: html_1.default, + rules: html_1.default.index.map(rule => rule.info), +})); +for (const rule of html_1.default.index) { + (0, fs_1.writeFileSync)((0, path_1.join)(htmlDir, 'rules', rule.info.name + '.md'), lazyEJS(template('rule.ejs'), { + plugin: html_1.default, + rule: rule.info, + tests: rule.tests, + })); +} +//# sourceMappingURL=generate-docs.js.map \ No newline at end of file diff --git a/lint/dist/generate-docs.js.map b/lint/dist/generate-docs.js.map new file mode 100644 index 00000000000..d05036c7b2d --- /dev/null +++ b/lint/dist/generate-docs.js.map @@ -0,0 +1 @@ +{"version":3,"file":"generate-docs.js","sourceRoot":"","sources":["../generate-docs.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lint/dist/src/rules/html/index.js b/lint/dist/src/rules/html/index.js new file mode 100644 index 00000000000..587e1ad442e --- /dev/null +++ b/lint/dist/src/rules/html/index.js @@ -0,0 +1,42 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +/* eslint-disable import/no-namespace */ +const structure_1 = require("../../util/structure"); +const themedComponentUsages = __importStar(require("./themed-component-usages")); +const index = [ + themedComponentUsages, +]; +module.exports = { + parser: require('@angular-eslint/template-parser'), + ...(0, structure_1.bundle)('dspace-angular-html', 'HTML', index), +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lint/dist/src/rules/html/index.js.map b/lint/dist/src/rules/html/index.js.map new file mode 100644 index 00000000000..a8a763cb114 --- /dev/null +++ b/lint/dist/src/rules/html/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/rules/html/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;GAMG;AACH,wCAAwC;AACxC,oDAG8B;AAC9B,iFAAmE;AAEnE,MAAM,KAAK,GAAG;IACZ,qBAAqB;CACM,CAAC;AAE9B,iBAAS;IACP,MAAM,EAAE,OAAO,CAAC,iCAAiC,CAAC;IAClD,GAAG,IAAA,kBAAM,EAAC,qBAAqB,EAAE,MAAM,EAAE,KAAK,CAAC;CAChD,CAAC"} \ No newline at end of file diff --git a/lint/dist/src/rules/html/themed-component-usages.js b/lint/dist/src/rules/html/themed-component-usages.js new file mode 100644 index 00000000000..87f3909a4ea --- /dev/null +++ b/lint/dist/src/rules/html/themed-component-usages.js @@ -0,0 +1,161 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.tests = exports.rule = exports.info = exports.Message = void 0; +const utils_1 = require("@typescript-eslint/utils"); +const fixture_1 = require("../../../test/fixture"); +const theme_support_1 = require("../../util/theme-support"); +const typescript_1 = require("../../util/typescript"); +var Message; +(function (Message) { + Message["WRONG_SELECTOR"] = "mustUseThemedWrapperSelector"; +})(Message || (exports.Message = Message = {})); +exports.info = { + name: 'themed-component-usages', + meta: { + docs: { + description: `Themeable components should be used via the selector of their \`ThemedComponent\` wrapper class + +This ensures that custom themes can correctly override _all_ instances of this component. +The only exception to this rule are unit tests, where we may want to use the base component in order to keep the test setup simple. + `, + }, + type: 'problem', + fixable: 'code', + schema: [], + messages: { + [Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper\'s selector', + }, + }, + defaultOptions: [], +}; +exports.rule = utils_1.ESLintUtils.RuleCreator.withoutDocs({ + ...exports.info, + create(context) { + if ((0, typescript_1.getFilename)(context).includes('.spec.ts')) { + // skip inline templates in unit tests + return {}; + } + const parserServices = (0, typescript_1.getSourceCode)(context).parserServices; + return { + [`Element$1[name = /^${theme_support_1.DISALLOWED_THEME_SELECTORS}/]`](node) { + const { startSourceSpan, endSourceSpan } = node; + const openStart = startSourceSpan.start.offset; + context.report({ + messageId: Message.WRONG_SELECTOR, + loc: parserServices.convertNodeSourceSpanToLoc(startSourceSpan), + fix(fixer) { + const oldSelector = node.name; + const newSelector = (0, theme_support_1.fixSelectors)(oldSelector); + const ops = [ + fixer.replaceTextRange([openStart + 1, openStart + 1 + oldSelector.length], newSelector), + ]; + // make sure we don't mangle self-closing tags + if (endSourceSpan !== null && startSourceSpan.end.offset !== endSourceSpan.end.offset) { + const closeStart = endSourceSpan.start.offset; + const closeEnd = endSourceSpan.end.offset; + ops.push(fixer.replaceTextRange([closeStart + 2, closeEnd - 1], newSelector)); + } + return ops; + }, + }); + }, + }; + }, +}); +exports.tests = { + plugin: exports.info.name, + valid: [ + { + name: 'use no-prefix selectors in HTML templates', + code: ` + + + + `, + }, + { + name: 'use no-prefix selectors in TypeScript templates', + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + { + name: 'use no-prefix selectors in TypeScript test templates', + filename: (0, fixture_1.fixture)('src/test.spec.ts'), + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + { + name: 'base selectors are also allowed in TypeScript test templates', + filename: (0, fixture_1.fixture)('src/test.spec.ts'), + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + ], + invalid: [ + { + name: 'themed override selectors are not allowed in HTML templates', + code: ` + + + + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` + + + + `, + }, + { + name: 'base selectors are not allowed in HTML templates', + code: ` + + + + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` + + + + `, + }, + ], +}; +exports.default = exports.rule; +//# sourceMappingURL=themed-component-usages.js.map \ No newline at end of file diff --git a/lint/dist/src/rules/html/themed-component-usages.js.map b/lint/dist/src/rules/html/themed-component-usages.js.map new file mode 100644 index 00000000000..afad58c671f --- /dev/null +++ b/lint/dist/src/rules/html/themed-component-usages.js.map @@ -0,0 +1 @@ +{"version":3,"file":"themed-component-usages.js","sourceRoot":"","sources":["../../../../src/rules/html/themed-component-usages.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lint/dist/src/rules/ts/index.js b/lint/dist/src/rules/ts/index.js new file mode 100644 index 00000000000..8ff3f21d0a8 --- /dev/null +++ b/lint/dist/src/rules/ts/index.js @@ -0,0 +1,45 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +const structure_1 = require("../../util/structure"); +/* eslint-disable import/no-namespace */ +const themedComponentClasses = __importStar(require("./themed-component-classes")); +const themedComponentSelectors = __importStar(require("./themed-component-selectors")); +const themedComponentUsages = __importStar(require("./themed-component-usages")); +const index = [ + themedComponentClasses, + themedComponentSelectors, + themedComponentUsages, +]; +module.exports = { + ...(0, structure_1.bundle)('dspace-angular-ts', 'TypeScript', index), +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lint/dist/src/rules/ts/index.js.map b/lint/dist/src/rules/ts/index.js.map new file mode 100644 index 00000000000..3f6694acc19 --- /dev/null +++ b/lint/dist/src/rules/ts/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/rules/ts/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;GAMG;AACH,oDAG8B;AAC9B,wCAAwC;AACxC,mFAAqE;AACrE,uFAAyE;AACzE,iFAAmE;AAEnE,MAAM,KAAK,GAAG;IACZ,sBAAsB;IACtB,wBAAwB;IACxB,qBAAqB;CACM,CAAC;AAE9B,iBAAS;IACP,GAAG,IAAA,kBAAM,EAAC,mBAAmB,EAAE,YAAY,EAAG,KAAK,CAAC;CACrD,CAAC"} \ No newline at end of file diff --git a/lint/dist/src/rules/ts/themed-component-classes.js b/lint/dist/src/rules/ts/themed-component-classes.js new file mode 100644 index 00000000000..53d9913c826 --- /dev/null +++ b/lint/dist/src/rules/ts/themed-component-classes.js @@ -0,0 +1,360 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.tests = exports.rule = exports.info = exports.Message = void 0; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +const utils_1 = require("@typescript-eslint/utils"); +const fixture_1 = require("../../../test/fixture"); +const angular_1 = require("../../util/angular"); +const fix_1 = require("../../util/fix"); +const theme_support_1 = require("../../util/theme-support"); +const typescript_1 = require("../../util/typescript"); +var Message; +(function (Message) { + Message["NOT_STANDALONE"] = "mustBeStandalone"; + Message["NOT_STANDALONE_IMPORTS_BASE"] = "mustBeStandaloneAndImportBase"; + Message["WRAPPER_IMPORTS_BASE"] = "wrapperShouldImportBase"; +})(Message || (exports.Message = Message = {})); +exports.info = { + name: 'themed-component-classes', + meta: { + docs: { + description: `Formatting rules for themeable component classes + +- All themeable components must be standalone. +- The base component must always be imported in the \`ThemedComponent\` wrapper. This ensures that it is always sufficient to import just the wrapper whenever we use the component. + `, + }, + type: 'problem', + fixable: 'code', + schema: [], + messages: { + [Message.NOT_STANDALONE]: 'Themeable components must be standalone', + [Message.NOT_STANDALONE_IMPORTS_BASE]: 'Themeable component wrapper classes must be standalone and import the base class', + [Message.WRAPPER_IMPORTS_BASE]: 'Themed component wrapper classes must only import the base class', + }, + }, + defaultOptions: [], +}; +exports.rule = utils_1.ESLintUtils.RuleCreator.withoutDocs({ + ...exports.info, + create(context) { + const filename = (0, typescript_1.getFilename)(context); + if (filename.endsWith('.spec.ts')) { + return {}; + } + function enforceStandalone(decoratorNode, withBaseImport = false) { + const standaloneNode = (0, angular_1.getComponentStandaloneNode)(decoratorNode); + if (standaloneNode === undefined) { + // We may need to add these properties in one go + if (!withBaseImport) { + context.report({ + messageId: Message.NOT_STANDALONE, + node: decoratorNode, + fix(fixer) { + const initializer = (0, angular_1.getComponentInitializer)(decoratorNode); + return (0, fix_1.appendObjectProperties)(context, fixer, initializer, ['standalone: true']); + }, + }); + } + } + else if (!standaloneNode.value) { + context.report({ + messageId: Message.NOT_STANDALONE, + node: standaloneNode, + fix(fixer) { + return fixer.replaceText(standaloneNode, 'true'); + }, + }); + } + if (withBaseImport) { + const baseClass = (0, theme_support_1.getBaseComponentClassName)(decoratorNode); + if (baseClass === undefined) { + return; + } + const importsNode = (0, angular_1.getComponentImportNode)(decoratorNode); + if (importsNode === undefined) { + if (standaloneNode === undefined) { + context.report({ + messageId: Message.NOT_STANDALONE_IMPORTS_BASE, + node: decoratorNode, + fix(fixer) { + const initializer = (0, angular_1.getComponentInitializer)(decoratorNode); + return (0, fix_1.appendObjectProperties)(context, fixer, initializer, ['standalone: true', `imports: [${baseClass}]`]); + }, + }); + } + else { + context.report({ + messageId: Message.WRAPPER_IMPORTS_BASE, + node: decoratorNode, + fix(fixer) { + const initializer = (0, angular_1.getComponentInitializer)(decoratorNode); + return (0, fix_1.appendObjectProperties)(context, fixer, initializer, [`imports: [${baseClass}]`]); + }, + }); + } + } + else { + // If we have an imports node, standalone: true will be enforced by another rule + const imports = importsNode.elements.map(e => e.name); + if (!imports.includes(baseClass) || imports.length > 1) { + // The wrapper should _only_ import the base component + context.report({ + messageId: Message.WRAPPER_IMPORTS_BASE, + node: importsNode, + fix(fixer) { + // todo: this may leave unused imports, but that's better than mangling things + return fixer.replaceText(importsNode, `[${baseClass}]`); + }, + }); + } + } + } + } + return { + 'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node) { + const classNode = node.parent; + const className = classNode.id?.name; + if (className === undefined) { + return; + } + if ((0, theme_support_1.isThemedComponentWrapper)(node)) { + enforceStandalone(node, true); + } + else if ((0, theme_support_1.inThemedComponentOverrideFile)(filename)) { + enforceStandalone(node); + } + else if ((0, theme_support_1.isThemeableComponent)(className)) { + enforceStandalone(node); + } + }, + }; + }, +}); +exports.tests = { + plugin: exports.info.name, + valid: [ + { + name: 'Regular non-themeable component', + code: ` +@Component({ + selector: 'ds-something', + standalone: true, +}) +class Something { +} + `, + }, + { + name: 'Base component', + code: ` +@Component({ + selector: 'ds-base-test-themable', + standalone: true, +}) +class TestThemeableTomponent { +} + `, + }, + { + name: 'Wrapper component', + filename: (0, fixture_1.fixture)('src/app/test/themed-test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [ + TestThemeableComponent, + ], +}) +class ThemedTestThemeableTomponent extends ThemedComponent { +} + `, + }, + { + name: 'Override component', + filename: (0, fixture_1.fixture)('src/themes/test/app/test/test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-themed-test-themable', + standalone: true, +}) +class Override extends BaseComponent { +} + `, + }, + ], + invalid: [ + { + name: 'Base component must be standalone', + code: ` +@Component({ + selector: 'ds-base-test-themable', +}) +class TestThemeableComponent { +} + `, + errors: [ + { + messageId: Message.NOT_STANDALONE, + }, + ], + output: ` +@Component({ + selector: 'ds-base-test-themable', + standalone: true, +}) +class TestThemeableComponent { +} + `, + }, + { + name: 'Wrapper component must be standalone and import base component', + filename: (0, fixture_1.fixture)('src/app/test/themed-test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-test-themable', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors: [ + { + messageId: Message.NOT_STANDALONE_IMPORTS_BASE, + }, + ], + output: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [TestThemeableComponent], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + name: 'Wrapper component must import base component (array present but empty)', + filename: (0, fixture_1.fixture)('src/app/test/themed-test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors: [ + { + messageId: Message.WRAPPER_IMPORTS_BASE, + }, + ], + output: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [TestThemeableComponent], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + name: 'Wrapper component must import base component (array is wrong)', + filename: (0, fixture_1.fixture)('src/app/test/themed-test-themeable.component.ts'), + code: ` +import { SomethingElse } from './somewhere-else'; + +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [ + SomethingElse, + ], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors: [ + { + messageId: Message.WRAPPER_IMPORTS_BASE, + }, + ], + output: ` +import { SomethingElse } from './somewhere-else'; + +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [TestThemeableComponent], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, { + name: 'Wrapper component must import base component (array is wrong)', + filename: (0, fixture_1.fixture)('src/app/test/themed-test-themeable.component.ts'), + code: ` +import { Something, SomethingElse } from './somewhere-else'; + +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [ + SomethingElse, + ], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors: [ + { + messageId: Message.WRAPPER_IMPORTS_BASE, + }, + ], + output: ` +import { Something, SomethingElse } from './somewhere-else'; + +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [TestThemeableComponent], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + name: 'Override component must be standalone', + filename: (0, fixture_1.fixture)('src/themes/test/app/test/test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-themed-test-themable', +}) +class Override extends BaseComponent { +} + `, + errors: [ + { + messageId: Message.NOT_STANDALONE, + }, + ], + output: ` +@Component({ + selector: 'ds-themed-test-themable', + standalone: true, +}) +class Override extends BaseComponent { +} + `, + }, + ], +}; +//# sourceMappingURL=themed-component-classes.js.map \ No newline at end of file diff --git a/lint/dist/src/rules/ts/themed-component-classes.js.map b/lint/dist/src/rules/ts/themed-component-classes.js.map new file mode 100644 index 00000000000..e96d346b0eb --- /dev/null +++ b/lint/dist/src/rules/ts/themed-component-classes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"themed-component-classes.js","sourceRoot":"","sources":["../../../../src/rules/ts/themed-component-classes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lint/dist/src/rules/ts/themed-component-selectors.js b/lint/dist/src/rules/ts/themed-component-selectors.js new file mode 100644 index 00000000000..0d3a238677d --- /dev/null +++ b/lint/dist/src/rules/ts/themed-component-selectors.js @@ -0,0 +1,240 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.tests = exports.rule = exports.info = exports.Message = void 0; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +const utils_1 = require("@typescript-eslint/utils"); +const fixture_1 = require("../../../test/fixture"); +const angular_1 = require("../../util/angular"); +const misc_1 = require("../../util/misc"); +const theme_support_1 = require("../../util/theme-support"); +const typescript_1 = require("../../util/typescript"); +var Message; +(function (Message) { + Message["BASE"] = "wrongSelectorUnthemedComponent"; + Message["WRAPPER"] = "wrongSelectorThemedComponentWrapper"; + Message["THEMED"] = "wrongSelectorThemedComponentOverride"; +})(Message || (exports.Message = Message = {})); +exports.info = { + name: 'themed-component-selectors', + meta: { + docs: { + description: `Themeable component selectors should follow the DSpace convention + +Each themeable component is comprised of a base component, a wrapper component and any number of themed components +- Base components should have a selector starting with \`ds-base-\` +- Themed components should have a selector starting with \`ds-themed-\` +- Wrapper components should have a selector starting with \`ds-\`, but not \`ds-base-\` or \`ds-themed-\` + - This is the regular DSpace selector prefix + - **When making a regular component themeable, its selector prefix should be changed to \`ds-base-\`, and the new wrapper's component should reuse the previous selector** + +Unit tests are exempt from this rule, because they may redefine components using the same class name as other themeable components elsewhere in the source. + `, + }, + type: 'problem', + schema: [], + fixable: 'code', + messages: { + [Message.BASE]: 'Unthemed version of themeable component should have a selector starting with \'ds-base-\'', + [Message.WRAPPER]: 'Themed component wrapper of themeable component shouldn\'t have a selector starting with \'ds-themed-\'', + [Message.THEMED]: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'', + }, + }, + defaultOptions: [], +}; +exports.rule = utils_1.ESLintUtils.RuleCreator.withoutDocs({ + ...exports.info, + create(context) { + const filename = (0, typescript_1.getFilename)(context); + if (filename.endsWith('.spec.ts')) { + return {}; + } + function enforceWrapperSelector(selectorNode) { + if (selectorNode?.value.startsWith('ds-themed-')) { + context.report({ + messageId: Message.WRAPPER, + node: selectorNode, + fix(fixer) { + return fixer.replaceText(selectorNode, (0, misc_1.stringLiteral)(selectorNode.value.replace('ds-themed-', 'ds-'))); + }, + }); + } + } + function enforceBaseSelector(selectorNode) { + if (!selectorNode?.value.startsWith('ds-base-')) { + context.report({ + messageId: Message.BASE, + node: selectorNode, + fix(fixer) { + return fixer.replaceText(selectorNode, (0, misc_1.stringLiteral)(selectorNode.value.replace('ds-', 'ds-base-'))); + }, + }); + } + } + function enforceThemedSelector(selectorNode) { + if (!selectorNode?.value.startsWith('ds-themed-')) { + context.report({ + messageId: Message.THEMED, + node: selectorNode, + fix(fixer) { + return fixer.replaceText(selectorNode, (0, misc_1.stringLiteral)(selectorNode.value.replace('ds-', 'ds-themed-'))); + }, + }); + } + } + return { + 'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node) { + const selectorNode = (0, angular_1.getComponentSelectorNode)(node); + if (selectorNode === undefined) { + return; + } + const selector = selectorNode?.value; + const classNode = node.parent; + const className = classNode.id?.name; + if (selector === undefined || className === undefined) { + return; + } + if ((0, theme_support_1.isThemedComponentWrapper)(node)) { + enforceWrapperSelector(selectorNode); + } + else if ((0, theme_support_1.inThemedComponentOverrideFile)(filename)) { + enforceThemedSelector(selectorNode); + } + else if ((0, theme_support_1.isThemeableComponent)(className)) { + enforceBaseSelector(selectorNode); + } + }, + }; + }, +}); +exports.tests = { + plugin: exports.info.name, + valid: [ + { + name: 'Regular non-themeable component selector', + code: ` +@Component({ + selector: 'ds-something', +}) +class Something { +} + `, + }, + { + name: 'Themeable component selector should replace the original version, unthemed version should be changed to ds-base-', + code: ` +@Component({ + selector: 'ds-base-something', +}) +class Something { +} + +@Component({ + selector: 'ds-something', +}) +class ThemedSomething extends ThemedComponent { +} + +@Component({ + selector: 'ds-themed-something', +}) +class OverrideSomething extends Something { +} + `, + }, + { + name: 'Other themed component wrappers should not interfere', + code: ` +@Component({ + selector: 'ds-something', +}) +class Something { +} + +@Component({ + selector: 'ds-something-else', +}) +class ThemedSomethingElse extends ThemedComponent { +} + `, + }, + ], + invalid: [ + { + name: 'Wrong selector for base component', + filename: (0, fixture_1.fixture)('src/app/test/test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-something', +}) +class TestThemeableComponent { +} + `, + errors: [ + { + messageId: Message.BASE, + }, + ], + output: ` +@Component({ + selector: 'ds-base-something', +}) +class TestThemeableComponent { +} + `, + }, + { + name: 'Wrong selector for wrapper component', + filename: (0, fixture_1.fixture)('src/app/test/themed-test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-themed-something', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors: [ + { + messageId: Message.WRAPPER, + }, + ], + output: ` +@Component({ + selector: 'ds-something', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + name: 'Wrong selector for theme override', + filename: (0, fixture_1.fixture)('src/themes/test/app/test/test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-something', +}) +class TestThememeableComponent extends BaseComponent { +} + `, + errors: [ + { + messageId: Message.THEMED, + }, + ], + output: ` +@Component({ + selector: 'ds-themed-something', +}) +class TestThememeableComponent extends BaseComponent { +} + `, + }, + ], +}; +exports.default = exports.rule; +//# sourceMappingURL=themed-component-selectors.js.map \ No newline at end of file diff --git a/lint/dist/src/rules/ts/themed-component-selectors.js.map b/lint/dist/src/rules/ts/themed-component-selectors.js.map new file mode 100644 index 00000000000..9522212e7b4 --- /dev/null +++ b/lint/dist/src/rules/ts/themed-component-selectors.js.map @@ -0,0 +1 @@ +{"version":3,"file":"themed-component-selectors.js","sourceRoot":"","sources":["../../../../src/rules/ts/themed-component-selectors.ts"],"names":[],"mappings":";;;AAAA;;;;;;GAMG;AACH,oDAIkC;AAElC,mDAAgD;AAChD,gDAA8D;AAC9D,0CAAgD;AAEhD,4DAIkC;AAClC,sDAAoD;AAEpD,IAAY,OAIX;AAJD,WAAY,OAAO;IACjB,kDAAuC,CAAA;IACvC,0DAA+C,CAAA;IAC/C,0DAA+C,CAAA;AACjD,CAAC,EAJW,OAAO,uBAAP,OAAO,QAIlB;AAEY,QAAA,IAAI,GAAG;IAClB,IAAI,EAAE,4BAA4B;IAClC,IAAI,EAAE;QACJ,IAAI,EAAE;YACJ,WAAW,EAAE;;;;;;;;;;OAUZ;SACF;QACD,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,EAAE;QACV,OAAO,EAAE,MAAM;QACf,QAAQ,EAAE;YACR,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,2FAA2F;YAC3G,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,yGAAyG;YAC5H,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,2FAA2F;SAC9G;KACF;IACD,cAAc,EAAE,EAAE;CACK,CAAC;AAEb,QAAA,IAAI,GAAG,mBAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IACtD,GAAG,YAAI;IACP,MAAM,CAAC,OAAiD;QACtD,MAAM,QAAQ,GAAG,IAAA,wBAAW,EAAC,OAAO,CAAC,CAAC;QAEtC,IAAI,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAClC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,SAAS,sBAAsB,CAAC,YAAoC;YAClE,IAAI,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;gBACjD,OAAO,CAAC,MAAM,CAAC;oBACb,SAAS,EAAE,OAAO,CAAC,OAAO;oBAC1B,IAAI,EAAE,YAAY;oBAClB,GAAG,CAAC,KAAK;wBACP,OAAO,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,IAAA,oBAAa,EAAC,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;oBACzG,CAAC;iBACF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,SAAS,mBAAmB,CAAC,YAAoC;YAC/D,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;gBAChD,OAAO,CAAC,MAAM,CAAC;oBACb,SAAS,EAAE,OAAO,CAAC,IAAI;oBACvB,IAAI,EAAE,YAAY;oBAClB,GAAG,CAAC,KAAK;wBACP,OAAO,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,IAAA,oBAAa,EAAC,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;oBACvG,CAAC;iBACF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,SAAS,qBAAqB,CAAC,YAAoC;YACjE,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;gBAClD,OAAO,CAAC,MAAM,CAAC;oBACb,SAAS,EAAE,OAAO,CAAC,MAAM;oBACzB,IAAI,EAAE,YAAY;oBAClB,GAAG,CAAC,KAAK;wBACP,OAAO,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,IAAA,oBAAa,EAAC,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;oBACzG,CAAC;iBACF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO;YACL,oEAAoE,CAAC,IAAwB;gBAC3F,MAAM,YAAY,GAAG,IAAA,kCAAwB,EAAC,IAAI,CAAC,CAAC;gBAEpD,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;oBAC/B,OAAO;gBACT,CAAC;gBAED,MAAM,QAAQ,GAAG,YAAY,EAAE,KAAK,CAAC;gBACrC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAmC,CAAC;gBAC3D,MAAM,SAAS,GAAG,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC;gBAErC,IAAI,QAAQ,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;oBACtD,OAAO;gBACT,CAAC;gBAED,IAAI,IAAA,wCAAwB,EAAC,IAAI,CAAC,EAAE,CAAC;oBACnC,sBAAsB,CAAC,YAAY,CAAC,CAAC;gBACvC,CAAC;qBAAM,IAAI,IAAA,6CAA6B,EAAC,QAAQ,CAAC,EAAE,CAAC;oBACnD,qBAAqB,CAAC,YAAY,CAAC,CAAC;gBACtC,CAAC;qBAAM,IAAI,IAAA,oCAAoB,EAAC,SAAS,CAAC,EAAE,CAAC;oBAC3C,mBAAmB,CAAC,YAAY,CAAC,CAAC;gBACpC,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC;AAEU,QAAA,KAAK,GAAG;IACnB,MAAM,EAAE,YAAI,CAAC,IAAI;IACjB,KAAK,EAAE;QACL;YACE,IAAI,EAAE,0CAA0C;YAChD,IAAI,EAAE;;;;;;OAML;SACF;QACD;YACE,IAAI,EAAE,kHAAkH;YACxH,IAAI,EAAE;;;;;;;;;;;;;;;;;;OAkBL;SACF;QACD;YACE,IAAI,EAAE,sDAAsD;YAC5D,IAAI,EAAE;;;;;;;;;;;;OAYL;SACF;KACF;IACD,OAAO,EAAE;QACP;YACE,IAAI,EAAE,mCAAmC;YACzC,QAAQ,EAAE,IAAA,iBAAO,EAAC,0CAA0C,CAAC;YAC7D,IAAI,EAAE;;;;;;SAMH;YACH,MAAM,EAAE;gBACN;oBACE,SAAS,EAAE,OAAO,CAAC,IAAI;iBACxB;aACF;YACD,MAAM,EAAE;;;;;;SAML;SACJ;QACD;YACE,IAAI,EAAE,sCAAsC;YAC5C,QAAQ,EAAE,IAAA,iBAAO,EAAC,iDAAiD,CAAC;YACpE,IAAI,EAAE;;;;;;SAMH;YACH,MAAM,EAAE;gBACN;oBACE,SAAS,EAAE,OAAO,CAAC,OAAO;iBAC3B;aACF;YACD,MAAM,EAAE;;;;;;SAML;SACJ;QACD;YACE,IAAI,EAAE,mCAAmC;YACzC,QAAQ,EAAE,IAAA,iBAAO,EAAC,sDAAsD,CAAC;YACzE,IAAI,EAAE;;;;;;SAMH;YACH,MAAM,EAAE;gBACN;oBACE,SAAS,EAAE,OAAO,CAAC,MAAM;iBAC1B;aACF;YACD,MAAM,EAAE;;;;;;SAML;SACJ;KACF;CACF,CAAC;AAEF,kBAAe,YAAI,CAAC"} \ No newline at end of file diff --git a/lint/dist/src/rules/ts/themed-component-usages.js b/lint/dist/src/rules/ts/themed-component-usages.js new file mode 100644 index 00000000000..d7f418b5d6c --- /dev/null +++ b/lint/dist/src/rules/ts/themed-component-usages.js @@ -0,0 +1,467 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.tests = exports.rule = exports.info = exports.Message = void 0; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +const utils_1 = require("@typescript-eslint/utils"); +const fixture_1 = require("../../../test/fixture"); +const fix_1 = require("../../util/fix"); +const theme_support_1 = require("../../util/theme-support"); +const typescript_1 = require("../../util/typescript"); +var Message; +(function (Message) { + Message["WRONG_CLASS"] = "mustUseThemedWrapperClass"; + Message["WRONG_IMPORT"] = "mustImportThemedWrapper"; + Message["WRONG_SELECTOR"] = "mustUseThemedWrapperSelector"; + Message["BASE_IN_MODULE"] = "baseComponentNotNeededInModule"; +})(Message || (exports.Message = Message = {})); +exports.info = { + name: 'themed-component-usages', + meta: { + docs: { + description: `Themeable components should be used via their \`ThemedComponent\` wrapper class + +This ensures that custom themes can correctly override _all_ instances of this component. +There are a few exceptions where the base class can still be used: +- Class declaration expressions (otherwise we can't declare, extend or override the class in the first place) +- Angular modules (except for routing modules) +- Angular \`@ViewChild\` decorators +- Type annotations + `, + }, + type: 'problem', + schema: [], + fixable: 'code', + messages: { + [Message.WRONG_CLASS]: 'Themeable components should be used via their ThemedComponent wrapper', + [Message.WRONG_IMPORT]: 'Themeable components should be used via their ThemedComponent wrapper', + [Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper', + [Message.BASE_IN_MODULE]: 'Base themeable components shouldn\'t be declared in modules', + }, + }, + defaultOptions: [], +}; +exports.rule = utils_1.ESLintUtils.RuleCreator.withoutDocs({ + ...exports.info, + create(context) { + const filename = (0, typescript_1.getFilename)(context); + function handleUnthemedUsagesInTypescript(node) { + if ((0, theme_support_1.isAllowedUnthemedUsage)(node)) { + return; + } + const entry = (0, theme_support_1.getThemeableComponentByBaseClass)(node.name); + if (entry === undefined) { + // this should never happen + throw new Error(`No such themeable component in registry: '${node.name}'`); + } + context.report({ + messageId: Message.WRONG_CLASS, + node: node, + fix(fixer) { + if (node.parent.type === utils_1.TSESTree.AST_NODE_TYPES.ArrayExpression) { + return (0, fix_1.replaceOrRemoveArrayIdentifier)(context, fixer, node, entry.wrapperClass); + } + else { + return fixer.replaceText(node, entry.wrapperClass); + } + }, + }); + } + function handleThemedSelectorQueriesInTests(node) { + context.report({ + node, + messageId: Message.WRONG_SELECTOR, + fix(fixer) { + const newSelector = (0, theme_support_1.fixSelectors)(node.raw); + return fixer.replaceText(node, newSelector); + }, + }); + } + function handleUnthemedImportsInTypescript(specifierNode) { + const allUsages = (0, typescript_1.findUsages)(context, specifierNode.local); + const badUsages = allUsages.filter(usage => !(0, theme_support_1.isAllowedUnthemedUsage)(usage)); + if (badUsages.length === 0) { + return; + } + const importedNode = specifierNode.imported; + const declarationNode = specifierNode.parent; + const entry = (0, theme_support_1.getThemeableComponentByBaseClass)(importedNode.name); + if (entry === undefined) { + // this should never happen + throw new Error(`No such themeable component in registry: '${importedNode.name}'`); + } + context.report({ + messageId: Message.WRONG_IMPORT, + node: importedNode, + fix(fixer) { + const ops = []; + const wrapperImport = (0, typescript_1.findImportSpecifier)(context, entry.wrapperClass); + if ((0, typescript_1.findUsagesByName)(context, entry.wrapperClass).length === 0) { + // Wrapper is not present in this file, safe to add import + const newImportLine = `import { ${entry.wrapperClass} } from '${(0, typescript_1.relativePath)(filename, entry.wrapperPath)}';`; + if (declarationNode.specifiers.length === 1) { + if (allUsages.length === badUsages.length) { + ops.push(fixer.replaceText(declarationNode, newImportLine)); + } + else if (wrapperImport === undefined) { + ops.push(fixer.insertTextAfter(declarationNode, '\n' + newImportLine)); + } + } + else { + ops.push(...(0, fix_1.removeWithCommas)(context, fixer, specifierNode)); + if (wrapperImport === undefined) { + ops.push(fixer.insertTextAfter(declarationNode, '\n' + newImportLine)); + } + } + } + else { + // Wrapper already present in the file, remove import instead + if (allUsages.length === badUsages.length) { + if (declarationNode.specifiers.length === 1) { + // Make sure we remove the newline as well + ops.push(fixer.removeRange([declarationNode.range[0], declarationNode.range[1] + 1])); + } + else { + ops.push(...(0, fix_1.removeWithCommas)(context, fixer, specifierNode)); + } + } + } + return ops; + }, + }); + } + // ignore tests and non-routing modules + if (filename.endsWith('.spec.ts')) { + return { + [`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal:first-child[value = /.*${theme_support_1.DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests, + }; + } + else if (filename.endsWith('.cy.ts')) { + return { + [`CallExpression[callee.object.name = "cy"][callee.property.name = "get"] > Literal:first-child[value = /.*${theme_support_1.DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests, + }; + } + else if (filename.match(/(?!src\/themes\/).*(?!routing).module.ts$/) + || filename.match(/themed-.+\.component\.ts$/)) { + // do nothing + return {}; + } + else { + return (0, theme_support_1.allThemeableComponents)().reduce((rules, entry) => { + return { + ...rules, + [`:not(:matches(ClassDeclaration, ImportSpecifier)) > Identifier[name = "${entry.baseClass}"]`]: handleUnthemedUsagesInTypescript, + [`ImportSpecifier[imported.name = "${entry.baseClass}"]`]: handleUnthemedImportsInTypescript, + }; + }, {}); + } + }, +}); +exports.tests = { + plugin: exports.info.name, + valid: [ + { + name: 'allow wrapper class usages', + code: ` +import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component'; + +const config = { + a: ThemedTestThemeableComponent, + b: ChipsComponent, +} + `, + }, + { + name: 'allow base class in class declaration', + code: ` +export class TestThemeableComponent { +} + `, + }, + { + name: 'allow inheriting from base class', + code: ` +import { TestThemeableComponent } from './app/test/test-themeable.component'; + +export class ThemedAdminSidebarComponent extends ThemedComponent { +} + `, + }, + { + name: 'allow base class in ViewChild', + code: ` +import { TestThemeableComponent } from './app/test/test-themeable.component'; + +export class Something { + @ViewChild(TestThemeableComponent) test: TestThemeableComponent; +} + `, + }, + { + name: 'allow wrapper selectors in test queries', + filename: (0, fixture_1.fixture)('src/app/test/test.component.spec.ts'), + code: ` +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); + `, + }, + { + name: 'allow wrapper selectors in cypress queries', + filename: (0, fixture_1.fixture)('src/app/test/test.component.cy.ts'), + code: ` +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); + `, + }, + ], + invalid: [ + { + name: 'disallow direct usages of base class', + code: ` +import { TestThemeableComponent } from './app/test/test-themeable.component'; +import { TestComponent } from './app/test/test.component'; + +const config = { + a: TestThemeableComponent, + b: TestComponent, +} + `, + errors: [ + { + messageId: Message.WRONG_IMPORT, + }, + { + messageId: Message.WRONG_CLASS, + }, + ], + output: ` +import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component'; +import { TestComponent } from './app/test/test.component'; + +const config = { + a: ThemedTestThemeableComponent, + b: TestComponent, +} + `, + }, + { + name: 'disallow direct usages of base class, keep other imports', + code: ` +import { Something, TestThemeableComponent } from './app/test/test-themeable.component'; +import { TestComponent } from './app/test/test.component'; + +const config = { + a: TestThemeableComponent, + b: TestComponent, + c: Something, +} + `, + errors: [ + { + messageId: Message.WRONG_IMPORT, + }, + { + messageId: Message.WRONG_CLASS, + }, + ], + output: ` +import { Something } from './app/test/test-themeable.component'; +import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component'; +import { TestComponent } from './app/test/test.component'; + +const config = { + a: ThemedTestThemeableComponent, + b: TestComponent, + c: Something, +} + `, + }, + { + name: 'handle array replacements correctly', + code: ` +const DECLARATIONS = [ + Something, + TestThemeableComponent, + Something, + ThemedTestThemeableComponent, +]; + `, + errors: [ + { + messageId: Message.WRONG_CLASS, + }, + ], + output: ` +const DECLARATIONS = [ + Something, + Something, + ThemedTestThemeableComponent, +]; + `, + }, + { + name: 'disallow override selector in test queries', + filename: (0, fixture_1.fixture)('src/app/test/test.component.spec.ts'), + code: ` +By.css('ds-themed-themeable'); +By.css('#test > ds-themed-themeable > #nest'); + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); + `, + }, + { + name: 'disallow base selector in test queries', + filename: (0, fixture_1.fixture)('src/app/test/test.component.spec.ts'), + code: ` +By.css('ds-base-themeable'); +By.css('#test > ds-base-themeable > #nest'); + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); + `, + }, + { + name: 'disallow override selector in cypress queries', + filename: (0, fixture_1.fixture)('src/app/test/test.component.cy.ts'), + code: ` +cy.get('ds-themed-themeable'); +cy.get('#test > ds-themed-themeable > #nest'); + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` +cy.get('ds-themeable'); +cy.get('#test > ds-themeable > #nest'); + `, + }, + { + name: 'disallow base selector in cypress queries', + filename: (0, fixture_1.fixture)('src/app/test/test.component.cy.ts'), + code: ` +cy.get('ds-base-themeable'); +cy.get('#test > ds-base-themeable > #nest'); + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` +cy.get('ds-themeable'); +cy.get('#test > ds-themeable > #nest'); + `, + }, + { + name: 'edge case: unable to find usage node through usage token, but import is still flagged and fixed', + filename: (0, fixture_1.fixture)('src/themes/test/app/test/other-themeable.component.ts'), + code: ` +import { Component } from '@angular/core'; + +import { Context } from './app/core/shared/context.model'; +import { TestThemeableComponent } from '../../../../app/test/test-themeable.component'; + +@Component({ + standalone: true, + imports: [TestThemeableComponent], +}) +export class UsageComponent { +} + `, + errors: [ + { + messageId: Message.WRONG_IMPORT, + }, + { + messageId: Message.WRONG_CLASS, + }, + ], + output: ` +import { Component } from '@angular/core'; + +import { Context } from './app/core/shared/context.model'; +import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component'; + +@Component({ + standalone: true, + imports: [ThemedTestThemeableComponent], +}) +export class UsageComponent { +} + `, + }, + { + name: 'edge case edge case: both are imported, only wrapper is retained', + filename: (0, fixture_1.fixture)('src/themes/test/app/test/other-themeable.component.ts'), + code: ` +import { Component } from '@angular/core'; + +import { Context } from './app/core/shared/context.model'; +import { TestThemeableComponent } from '../../../../app/test/test-themeable.component'; +import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component'; + +@Component({ + standalone: true, + imports: [TestThemeableComponent, ThemedTestThemeableComponent], +}) +export class UsageComponent { +} + `, + errors: [ + { + messageId: Message.WRONG_IMPORT, + }, + { + messageId: Message.WRONG_CLASS, + }, + ], + output: ` +import { Component } from '@angular/core'; + +import { Context } from './app/core/shared/context.model'; +import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component'; + +@Component({ + standalone: true, + imports: [ThemedTestThemeableComponent], +}) +export class UsageComponent { +} + `, + }, + ], +}; +exports.default = exports.rule; +//# sourceMappingURL=themed-component-usages.js.map \ No newline at end of file diff --git a/lint/dist/src/rules/ts/themed-component-usages.js.map b/lint/dist/src/rules/ts/themed-component-usages.js.map new file mode 100644 index 00000000000..49589ac9af5 --- /dev/null +++ b/lint/dist/src/rules/ts/themed-component-usages.js.map @@ -0,0 +1 @@ +{"version":3,"file":"themed-component-usages.js","sourceRoot":"","sources":["../../../../src/rules/ts/themed-component-usages.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lint/dist/src/util/angular.js b/lint/dist/src/util/angular.js new file mode 100644 index 00000000000..d553cfef4c7 --- /dev/null +++ b/lint/dist/src/util/angular.js @@ -0,0 +1,77 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isPartOfViewChild = exports.getComponentInitializerNodeByName = exports.getComponentInitializer = exports.getComponentSuperClassName = exports.getComponentClassName = exports.getComponentImportNode = exports.getComponentStandaloneNode = exports.getComponentSelectorNode = void 0; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +const utils_1 = require("@typescript-eslint/utils"); +const typescript_1 = require("./typescript"); +function getComponentSelectorNode(componentDecoratorNode) { + const property = getComponentInitializerNodeByName(componentDecoratorNode, 'selector'); + if (property !== undefined) { + // todo: support template literals as well + if (property.type === utils_1.TSESTree.AST_NODE_TYPES.Literal && typeof property.value === 'string') { + return property; + } + } + return undefined; +} +exports.getComponentSelectorNode = getComponentSelectorNode; +function getComponentStandaloneNode(componentDecoratorNode) { + const property = getComponentInitializerNodeByName(componentDecoratorNode, 'standalone'); + if (property !== undefined) { + if (property.type === utils_1.TSESTree.AST_NODE_TYPES.Literal && typeof property.value === 'boolean') { + return property; + } + } + return undefined; +} +exports.getComponentStandaloneNode = getComponentStandaloneNode; +function getComponentImportNode(componentDecoratorNode) { + const property = getComponentInitializerNodeByName(componentDecoratorNode, 'imports'); + if (property !== undefined) { + if (property.type === utils_1.TSESTree.AST_NODE_TYPES.ArrayExpression) { + return property; + } + } + return undefined; +} +exports.getComponentImportNode = getComponentImportNode; +function getComponentClassName(decoratorNode) { + if (decoratorNode.parent.type !== utils_1.TSESTree.AST_NODE_TYPES.ClassDeclaration) { + return undefined; + } + if (decoratorNode.parent.id?.type !== utils_1.TSESTree.AST_NODE_TYPES.Identifier) { + return undefined; + } + return decoratorNode.parent.id.name; +} +exports.getComponentClassName = getComponentClassName; +function getComponentSuperClassName(decoratorNode) { + if (decoratorNode.parent.type !== utils_1.TSESTree.AST_NODE_TYPES.ClassDeclaration) { + return undefined; + } + if (decoratorNode.parent.superClass?.type !== utils_1.TSESTree.AST_NODE_TYPES.Identifier) { + return undefined; + } + return decoratorNode.parent.superClass.name; +} +exports.getComponentSuperClassName = getComponentSuperClassName; +function getComponentInitializer(componentDecoratorNode) { + return componentDecoratorNode.expression.arguments[0]; +} +exports.getComponentInitializer = getComponentInitializer; +function getComponentInitializerNodeByName(componentDecoratorNode, name) { + const initializer = getComponentInitializer(componentDecoratorNode); + return (0, typescript_1.getObjectPropertyNodeByName)(initializer, name); +} +exports.getComponentInitializerNodeByName = getComponentInitializerNodeByName; +function isPartOfViewChild(node) { + return node.parent?.callee?.name === 'ViewChild'; +} +exports.isPartOfViewChild = isPartOfViewChild; +//# sourceMappingURL=angular.js.map \ No newline at end of file diff --git a/lint/dist/src/util/angular.js.map b/lint/dist/src/util/angular.js.map new file mode 100644 index 00000000000..b033e5d99df --- /dev/null +++ b/lint/dist/src/util/angular.js.map @@ -0,0 +1 @@ +{"version":3,"file":"angular.js","sourceRoot":"","sources":["../../../src/util/angular.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lint/dist/src/util/fix.js b/lint/dist/src/util/fix.js new file mode 100644 index 00000000000..52344f81a7b --- /dev/null +++ b/lint/dist/src/util/fix.js @@ -0,0 +1,109 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.replaceOrRemoveArrayIdentifier = exports.removeWithCommas = exports.isLast = exports.appendArrayElement = exports.appendObjectProperties = void 0; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +const utils_1 = require("@typescript-eslint/utils"); +const typescript_1 = require("./typescript"); +function appendObjectProperties(context, fixer, objectNode, properties) { + // todo: may not handle empty objects too well + const lastProperty = objectNode.properties[objectNode.properties.length - 1]; + const source = (0, typescript_1.getSourceCode)(context); + const nextToken = source.getTokenAfter(lastProperty); + // todo: newline & indentation are hardcoded for @Component({}) + // todo: we're assuming that we need trailing commas, what if we don't? + const newPart = '\n' + properties.map(p => ` ${p},`).join('\n'); + if (nextToken !== null && nextToken.value === ',') { + return fixer.insertTextAfter(nextToken, newPart); + } + else { + return fixer.insertTextAfter(lastProperty, ',' + newPart); + } +} +exports.appendObjectProperties = appendObjectProperties; +function appendArrayElement(context, fixer, arrayNode, value) { + const source = (0, typescript_1.getSourceCode)(context); + if (arrayNode.elements.length === 0) { + // This is the first element + const openArray = source.getTokenByRangeStart(arrayNode.range[0]); + if (openArray == null) { + throw new Error('Unexpected null token for opening square bracket'); + } + // safe to assume the list is single-line + return fixer.insertTextAfter(openArray, `${value}`); + } + else { + const lastElement = arrayNode.elements[arrayNode.elements.length - 1]; + if (lastElement == null) { + throw new Error('Unexpected null node in array'); + } + const nextToken = source.getTokenAfter(lastElement); + // todo: we don't know if the list is chopped or not, so we can't make any assumptions -- may produce output that will be flagged by other rules on the next run! + // todo: we're assuming that we need trailing commas, what if we don't? + if (nextToken !== null && nextToken.value === ',') { + return fixer.insertTextAfter(nextToken, ` ${value},`); + } + else { + return fixer.insertTextAfter(lastElement, `, ${value},`); + } + } +} +exports.appendArrayElement = appendArrayElement; +function isLast(elementNode) { + if (!elementNode.parent) { + return false; + } + let siblingNodes = [null]; + if (elementNode.parent.type === utils_1.TSESTree.AST_NODE_TYPES.ArrayExpression) { + siblingNodes = elementNode.parent.elements; + } + else if (elementNode.parent.type === utils_1.TSESTree.AST_NODE_TYPES.ImportDeclaration) { + siblingNodes = elementNode.parent.specifiers; + } + return elementNode === siblingNodes[siblingNodes.length - 1]; +} +exports.isLast = isLast; +function removeWithCommas(context, fixer, elementNode) { + const ops = []; + const source = (0, typescript_1.getSourceCode)(context); + let nextToken = source.getTokenAfter(elementNode); + let prevToken = source.getTokenBefore(elementNode); + if (nextToken !== null && prevToken !== null) { + if (nextToken.value === ',') { + nextToken = source.getTokenAfter(nextToken); + if (nextToken !== null) { + ops.push(fixer.removeRange([elementNode.range[0], nextToken.range[0]])); + } + } + if (isLast(elementNode) && prevToken.value === ',') { + prevToken = source.getTokenBefore(prevToken); + if (prevToken !== null) { + ops.push(fixer.removeRange([prevToken.range[1], elementNode.range[1]])); + } + } + } + else if (nextToken !== null) { + ops.push(fixer.removeRange([elementNode.range[0], nextToken.range[0]])); + } + return ops; +} +exports.removeWithCommas = removeWithCommas; +function replaceOrRemoveArrayIdentifier(context, fixer, identifierNode, newValue) { + if (identifierNode.parent.type !== utils_1.TSESTree.AST_NODE_TYPES.ArrayExpression) { + throw new Error('Parent node is not an array expression!'); + } + const array = identifierNode.parent; + for (const element of array.elements) { + if (element !== null && element.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier && element.name === newValue) { + return removeWithCommas(context, fixer, identifierNode); + } + } + return [fixer.replaceText(identifierNode, newValue)]; +} +exports.replaceOrRemoveArrayIdentifier = replaceOrRemoveArrayIdentifier; +//# sourceMappingURL=fix.js.map \ No newline at end of file diff --git a/lint/dist/src/util/fix.js.map b/lint/dist/src/util/fix.js.map new file mode 100644 index 00000000000..47fb530d679 --- /dev/null +++ b/lint/dist/src/util/fix.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fix.js","sourceRoot":"","sources":["../../../src/util/fix.ts"],"names":[],"mappings":";;;AAAA;;;;;;GAMG;AACH,oDAAoD;AAOpD,6CAA6C;AAI7C,SAAgB,sBAAsB,CAAC,OAA8B,EAAE,KAAgB,EAAE,UAAqC,EAAE,UAAoB;IAClJ,8CAA8C;IAC9C,MAAM,YAAY,GAAG,UAAU,CAAC,UAAU,CAAC,UAAU,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC7E,MAAM,MAAM,GAAG,IAAA,0BAAa,EAAC,OAAO,CAAC,CAAC;IACtC,MAAM,SAAS,GAAG,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IAErD,+DAA+D;IAC/D,uEAAuE;IACvE,MAAM,OAAO,GAAG,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEjE,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,CAAC,KAAK,KAAK,GAAG,EAAE,CAAC;QAClD,OAAO,KAAK,CAAC,eAAe,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;SAAM,CAAC;QACN,OAAO,KAAK,CAAC,eAAe,CAAC,YAAY,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAfD,wDAeC;AAED,SAAgB,kBAAkB,CAAC,OAA8B,EAAE,KAAgB,EAAE,SAAmC,EAAE,KAAa;IACrI,MAAM,MAAM,GAAG,IAAA,0BAAa,EAAC,OAAO,CAAC,CAAC;IAEtC,IAAI,SAAS,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpC,4BAA4B;QAC5B,MAAM,SAAS,GAAG,MAAM,CAAC,oBAAoB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAElE,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QACtE,CAAC;QAED,yCAAyC;QACzC,OAAO,KAAK,CAAC,eAAe,CAAC,SAAS,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IACtD,CAAC;SAAM,CAAC;QACN,MAAM,WAAW,GAAG,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAEtE,IAAI,WAAW,IAAI,IAAI,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;QAEpD,iKAAiK;QACjK,uEAAuE;QACvE,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,CAAC,KAAK,KAAK,GAAG,EAAE,CAAC;YAClD,OAAO,KAAK,CAAC,eAAe,CAAC,SAAS,EAAE,IAAI,KAAK,GAAG,CAAC,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,OAAO,KAAK,CAAC,eAAe,CAAC,WAAW,EAAE,KAAK,KAAK,GAAG,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;AAEH,CAAC;AA/BD,gDA+BC;AAED,SAAgB,MAAM,CAAC,WAA0B;IAC/C,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC;QACxB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,YAAY,GAA6B,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,WAAW,CAAC,MAAM,CAAC,IAAI,KAAK,gBAAQ,CAAC,cAAc,CAAC,eAAe,EAAE,CAAC;QACxE,YAAY,GAAG,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC;IAC7C,CAAC;SAAM,IAAI,WAAW,CAAC,MAAM,CAAC,IAAI,KAAK,gBAAQ,CAAC,cAAc,CAAC,iBAAiB,EAAE,CAAC;QACjF,YAAY,GAAG,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC;IAC/C,CAAC;IAED,OAAO,WAAW,KAAK,YAAY,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC/D,CAAC;AAbD,wBAaC;AAED,SAAgB,gBAAgB,CAAC,OAA8B,EAAE,KAAgB,EAAE,WAA0B;IAC3G,MAAM,GAAG,GAAG,EAAE,CAAC;IAEf,MAAM,MAAM,GAAG,IAAA,0BAAa,EAAC,OAAO,CAAC,CAAC;IACtC,IAAI,SAAS,GAAG,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;IAClD,IAAI,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;IAEnD,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QAC7C,IAAI,SAAS,CAAC,KAAK,KAAK,GAAG,EAAE,CAAC;YAC5B,SAAS,GAAG,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;YAC5C,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;gBACvB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1E,CAAC;QACH,CAAC;QACD,IAAI,MAAM,CAAC,WAAW,CAAC,IAAI,SAAS,CAAC,KAAK,KAAK,GAAG,EAAE,CAAC;YACnD,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;YAC7C,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;gBACvB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1E,CAAC;QACH,CAAC;IACH,CAAC;SAAM,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QAC9B,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAzBD,4CAyBC;AAED,SAAgB,8BAA8B,CAAC,OAA8B,EAAE,KAAgB,EAAE,cAAmC,EAAE,QAAgB;IACpJ,IAAI,cAAc,CAAC,MAAM,CAAC,IAAI,KAAK,gBAAQ,CAAC,cAAc,CAAC,eAAe,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IAED,MAAM,KAAK,GAAG,cAAc,CAAC,MAAkC,CAAC;IAEhE,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACrC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,IAAI,KAAK,gBAAQ,CAAC,cAAc,CAAC,UAAU,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACzG,OAAO,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC,CAAC;AACvD,CAAC;AAdD,wEAcC"} \ No newline at end of file diff --git a/lint/dist/src/util/misc.js b/lint/dist/src/util/misc.js new file mode 100644 index 00000000000..1062e111b0e --- /dev/null +++ b/lint/dist/src/util/misc.js @@ -0,0 +1,31 @@ +"use strict"; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.toUnixStylePath = exports.stringLiteral = exports.match = void 0; +function match(rangeA, rangeB) { + return rangeA[0] === rangeB[0] && rangeA[1] === rangeB[1]; +} +exports.match = match; +function stringLiteral(value) { + return `'${value}'`; +} +exports.stringLiteral = stringLiteral; +/** + * Transform Windows-style paths into Unix-style paths + */ +function toUnixStylePath(path) { + // note: we're assuming that none of the directory/file names contain '\' or '/' characters. + // using these characters in paths is very bad practice in general, so this should be a safe assumption. + if (path.includes('\\')) { + return path.replace(/^[A-Z]:\\/, '/').replaceAll('\\', '/'); + } + return path; +} +exports.toUnixStylePath = toUnixStylePath; +//# sourceMappingURL=misc.js.map \ No newline at end of file diff --git a/lint/dist/src/util/misc.js.map b/lint/dist/src/util/misc.js.map new file mode 100644 index 00000000000..9643c71376d --- /dev/null +++ b/lint/dist/src/util/misc.js.map @@ -0,0 +1 @@ +{"version":3,"file":"misc.js","sourceRoot":"","sources":["../../../src/util/misc.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH,SAAgB,KAAK,CAAC,MAAgB,EAAE,MAAgB;IACtD,OAAO,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC;AAC5D,CAAC;AAFD,sBAEC;AAGD,SAAgB,aAAa,CAAC,KAAa;IACzC,OAAO,IAAI,KAAK,GAAG,CAAC;AACtB,CAAC;AAFD,sCAEC;AAED;;GAEG;AACH,SAAgB,eAAe,CAAC,IAAY;IAC1C,4FAA4F;IAC5F,8GAA8G;IAC9G,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAPD,0CAOC"} \ No newline at end of file diff --git a/lint/dist/src/util/structure.js b/lint/dist/src/util/structure.js new file mode 100644 index 00000000000..485c5f24128 --- /dev/null +++ b/lint/dist/src/util/structure.js @@ -0,0 +1,16 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.bundle = void 0; +function bundle(name, language, index) { + return index.reduce((o, i) => { + o.rules[i.info.name] = i.rule; + return o; + }, { + name, + language, + rules: {}, + index, + }); +} +exports.bundle = bundle; +//# sourceMappingURL=structure.js.map \ No newline at end of file diff --git a/lint/dist/src/util/structure.js.map b/lint/dist/src/util/structure.js.map new file mode 100644 index 00000000000..8ae94c6a9e6 --- /dev/null +++ b/lint/dist/src/util/structure.js.map @@ -0,0 +1 @@ +{"version":3,"file":"structure.js","sourceRoot":"","sources":["../../../src/util/structure.ts"],"names":[],"mappings":";;;AA0CA,SAAgB,MAAM,CACpB,IAAY,EACZ,QAAgB,EAChB,KAAoB;IAEpB,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAgB,EAAE,CAAc,EAAE,EAAE;QACvD,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QAC9B,OAAO,CAAC,CAAC;IACX,CAAC,EAAE;QACD,IAAI;QACJ,QAAQ;QACR,KAAK,EAAE,EAAE;QACT,KAAK;KACN,CAAC,CAAC;AACL,CAAC;AAdD,wBAcC"} \ No newline at end of file diff --git a/lint/dist/src/util/theme-support.js b/lint/dist/src/util/theme-support.js new file mode 100644 index 00000000000..5b7b09eb43e --- /dev/null +++ b/lint/dist/src/util/theme-support.js @@ -0,0 +1,206 @@ +"use strict"; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.fixSelectors = exports.DISALLOWED_THEME_SELECTORS = exports.isAllowedUnthemedUsage = exports.getThemeableComponentByBaseClass = exports.allThemeableComponents = exports.inThemedComponentOverrideFile = exports.isThemeableComponent = exports.getBaseComponentClassName = exports.isThemedComponentWrapper = exports.themeableComponents = void 0; +const utils_1 = require("@typescript-eslint/utils"); +const fs_1 = require("fs"); +const path_1 = require("path"); +const typescript_1 = __importDefault(require("typescript")); +const angular_1 = require("./angular"); +const typescript_2 = require("./typescript"); +function isAngularComponentDecorator(node) { + if (node.kind === typescript_1.default.SyntaxKind.Decorator && node.parent.kind === typescript_1.default.SyntaxKind.ClassDeclaration) { + const decorator = node; + if (decorator.expression.kind === typescript_1.default.SyntaxKind.CallExpression) { + const method = decorator.expression; + if (method.expression.kind === typescript_1.default.SyntaxKind.Identifier) { + return method.expression.text === 'Component'; + } + } + } + return false; +} +function findImportDeclaration(source, identifierName) { + return typescript_1.default.forEachChild(source, (topNode) => { + if (topNode.kind === typescript_1.default.SyntaxKind.ImportDeclaration) { + const importDeclaration = topNode; + if (importDeclaration.importClause?.namedBindings?.kind === typescript_1.default.SyntaxKind.NamedImports) { + const namedImports = importDeclaration.importClause?.namedBindings; + for (const element of namedImports.elements) { + if (element.name.text === identifierName) { + return importDeclaration; + } + } + } + } + return undefined; + }); +} +/** + * Listing of all themeable Components + */ +class ThemeableComponentRegistry { + constructor() { + this.entries = new Set(); + this.byBaseClass = new Map(); + this.byWrapperClass = new Map(); + this.byBasePath = new Map(); + this.byWrapperPath = new Map(); + } + initialize(prefix = '') { + if (this.entries.size > 0) { + return; + } + function registerWrapper(path) { + const source = getSource(path); + function traverse(node) { + if (node.parent !== undefined && isAngularComponentDecorator(node)) { + const classNode = node.parent; + if (classNode.name === undefined || classNode.heritageClauses === undefined) { + return; + } + const wrapperClass = classNode.name?.escapedText; + for (const heritageClause of classNode.heritageClauses) { + for (const type of heritageClause.types) { + if (type.expression.escapedText === 'ThemedComponent') { + if (type.kind !== typescript_1.default.SyntaxKind.ExpressionWithTypeArguments || type.typeArguments === undefined) { + continue; + } + const firstTypeArg = type.typeArguments[0]; + const baseClass = firstTypeArg.typeName?.escapedText; + if (baseClass === undefined) { + continue; + } + const importDeclaration = findImportDeclaration(source, baseClass); + if (importDeclaration === undefined) { + continue; + } + const basePath = resolveLocalPath(importDeclaration.moduleSpecifier.text, path); + exports.themeableComponents.add({ + baseClass, + basePath: basePath.replace(new RegExp(`^${prefix}`), ''), + baseFileName: (0, path_1.basename)(basePath).replace(/\.ts$/, ''), + wrapperClass, + wrapperPath: path.replace(new RegExp(`^${prefix}`), ''), + wrapperFileName: (0, path_1.basename)(path).replace(/\.ts$/, ''), + }); + } + } + } + return; + } + else { + typescript_1.default.forEachChild(node, traverse); + } + } + traverse(source); + } + const glob = require('glob'); + // note: this outputs Unix-style paths on Windows + const wrappers = glob.GlobSync(prefix + 'src/app/**/themed-*.component.ts', { ignore: 'node_modules/**' }).found; + for (const wrapper of wrappers) { + registerWrapper(wrapper); + } + } + add(entry) { + this.entries.add(entry); + this.byBaseClass.set(entry.baseClass, entry); + this.byWrapperClass.set(entry.wrapperClass, entry); + this.byBasePath.set(entry.basePath, entry); + this.byWrapperPath.set(entry.wrapperPath, entry); + } +} +exports.themeableComponents = new ThemeableComponentRegistry(); +/** + * Construct the AST of a TypeScript source file + * @param file + */ +function getSource(file) { + return typescript_1.default.createSourceFile(file, (0, fs_1.readFileSync)(file).toString(), typescript_1.default.ScriptTarget.ES2020, // todo: actually use tsconfig.json? + /*setParentNodes */ true); +} +/** + * Resolve a possibly relative local path into an absolute path starting from the root directory of the project + */ +function resolveLocalPath(path, relativeTo) { + if (path.startsWith('src/')) { + return path; + } + else if (path.startsWith('./')) { + const parts = relativeTo.split('/'); + return [ + ...parts.slice(0, parts.length - 1), + path.replace(/^.\//, ''), + ].join('/') + '.ts'; + } + else { + throw new Error(`Unsupported local path: ${path}`); + } +} +function isThemedComponentWrapper(decoratorNode) { + if (decoratorNode.parent.type !== utils_1.TSESTree.AST_NODE_TYPES.ClassDeclaration) { + return false; + } + if (decoratorNode.parent.superClass?.type !== utils_1.TSESTree.AST_NODE_TYPES.Identifier) { + return false; + } + return decoratorNode.parent.superClass?.name === 'ThemedComponent'; +} +exports.isThemedComponentWrapper = isThemedComponentWrapper; +function getBaseComponentClassName(decoratorNode) { + const wrapperClass = (0, angular_1.getComponentClassName)(decoratorNode); + if (wrapperClass === undefined) { + return; + } + exports.themeableComponents.initialize(); + const entry = exports.themeableComponents.byWrapperClass.get(wrapperClass); + if (entry === undefined) { + return undefined; + } + return entry.baseClass; +} +exports.getBaseComponentClassName = getBaseComponentClassName; +function isThemeableComponent(className) { + exports.themeableComponents.initialize(); + return exports.themeableComponents.byBaseClass.has(className); +} +exports.isThemeableComponent = isThemeableComponent; +function inThemedComponentOverrideFile(filename) { + const match = filename.match(/src\/themes\/[^\/]+\/(app\/.*)/); + if (!match) { + return false; + } + exports.themeableComponents.initialize(); + // todo: this is fragile! + return exports.themeableComponents.byBasePath.has(`src/${match[1]}`); +} +exports.inThemedComponentOverrideFile = inThemedComponentOverrideFile; +function allThemeableComponents() { + exports.themeableComponents.initialize(); + return [...exports.themeableComponents.entries]; +} +exports.allThemeableComponents = allThemeableComponents; +function getThemeableComponentByBaseClass(baseClass) { + exports.themeableComponents.initialize(); + return exports.themeableComponents.byBaseClass.get(baseClass); +} +exports.getThemeableComponentByBaseClass = getThemeableComponentByBaseClass; +function isAllowedUnthemedUsage(usageNode) { + return (0, typescript_2.isPartOfClassDeclaration)(usageNode) || (0, typescript_2.isPartOfTypeExpression)(usageNode) || (0, angular_1.isPartOfViewChild)(usageNode); +} +exports.isAllowedUnthemedUsage = isAllowedUnthemedUsage; +exports.DISALLOWED_THEME_SELECTORS = 'ds-(base|themed)-'; +function fixSelectors(text) { + return text.replaceAll(/ds-(base|themed)-/g, 'ds-'); +} +exports.fixSelectors = fixSelectors; +//# sourceMappingURL=theme-support.js.map \ No newline at end of file diff --git a/lint/dist/src/util/theme-support.js.map b/lint/dist/src/util/theme-support.js.map new file mode 100644 index 00000000000..d8f0bc4905a --- /dev/null +++ b/lint/dist/src/util/theme-support.js.map @@ -0,0 +1 @@ +{"version":3,"file":"theme-support.js","sourceRoot":"","sources":["../../../src/util/theme-support.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lint/dist/src/util/typescript.js b/lint/dist/src/util/typescript.js new file mode 100644 index 00000000000..b88fc31aee5 --- /dev/null +++ b/lint/dist/src/util/typescript.js @@ -0,0 +1,127 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.findImportSpecifier = exports.relativePath = exports.isPartOfClassDeclaration = exports.isPartOfTypeExpression = exports.findUsagesByName = exports.findUsages = exports.getObjectPropertyNodeByName = exports.getSourceCode = exports.getFilename = void 0; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +const utils_1 = require("@typescript-eslint/utils"); +const misc_1 = require("./misc"); +/** + * Return the current filename based on the ESLint rule context as a Unix-style path. + * This is easier for regex and comparisons to glob paths. + */ +function getFilename(context) { + // TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?) + // eslint-disable-next-line deprecation/deprecation + return (0, misc_1.toUnixStylePath)(context.getFilename()); +} +exports.getFilename = getFilename; +function getSourceCode(context) { + // TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?) + // eslint-disable-next-line deprecation/deprecation + return context.getSourceCode(); +} +exports.getSourceCode = getSourceCode; +function getObjectPropertyNodeByName(objectNode, propertyName) { + for (const propertyNode of objectNode.properties) { + if (propertyNode.type === utils_1.TSESTree.AST_NODE_TYPES.Property + && ((propertyNode.key?.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier + && propertyNode.key?.name === propertyName) || (propertyNode.key?.type === utils_1.TSESTree.AST_NODE_TYPES.Literal + && propertyNode.key?.value === propertyName))) { + return propertyNode.value; + } + } + return undefined; +} +exports.getObjectPropertyNodeByName = getObjectPropertyNodeByName; +function findUsages(context, localNode) { + const source = getSourceCode(context); + const usages = []; + for (const token of source.ast.tokens) { + if (token.type === utils_1.TSESTree.AST_TOKEN_TYPES.Identifier && token.value === localNode.name && !(0, misc_1.match)(token.range, localNode.range)) { + const node = source.getNodeByRangeIndex(token.range[0]); + // todo: in some cases, the resulting node can actually be the whole program (!) + if (node !== null) { + usages.push(node); + } + } + } + return usages; +} +exports.findUsages = findUsages; +function findUsagesByName(context, identifier) { + const source = getSourceCode(context); + const usages = []; + for (const token of source.ast.tokens) { + if (token.type === utils_1.TSESTree.AST_TOKEN_TYPES.Identifier && token.value === identifier) { + const node = source.getNodeByRangeIndex(token.range[0]); + // todo: in some cases, the resulting node can actually be the whole program (!) + if (node !== null) { + usages.push(node); + } + } + } + return usages; +} +exports.findUsagesByName = findUsagesByName; +function isPartOfTypeExpression(node) { + return node.parent?.type?.valueOf().startsWith('TSType'); +} +exports.isPartOfTypeExpression = isPartOfTypeExpression; +function isPartOfClassDeclaration(node) { + return node.parent?.type === utils_1.TSESTree.AST_NODE_TYPES.ClassDeclaration; +} +exports.isPartOfClassDeclaration = isPartOfClassDeclaration; +function fromSrc(path) { + const m = path.match(/^.*(src\/.+)(\.(ts|json|js)?)$/); + if (m) { + return m[1]; + } + else { + throw new Error(`Can't infer project-absolute TS/resource path from: ${path}`); + } +} +function relativePath(thisFile, importFile) { + const fromParts = fromSrc(thisFile).split('/'); + const toParts = fromSrc(importFile).split('/'); + let lastCommon = 0; + for (let i = 0; i < fromParts.length - 1; i++) { + if (fromParts[i] === toParts[i]) { + lastCommon++; + } + else { + break; + } + } + const path = toParts.slice(lastCommon, toParts.length).join('/'); + const backtrack = fromParts.length - lastCommon - 1; + let prefix; + if (backtrack > 0) { + prefix = '../'.repeat(backtrack); + } + else { + prefix = './'; + } + return prefix + path; +} +exports.relativePath = relativePath; +function findImportSpecifier(context, identifier) { + const source = getSourceCode(context); + const usages = []; + for (const token of source.ast.tokens) { + if (token.type === utils_1.TSESTree.AST_TOKEN_TYPES.Identifier && token.value === identifier) { + const node = source.getNodeByRangeIndex(token.range[0]); + // todo: in some cases, the resulting node can actually be the whole program (!) + if (node && node.parent && node.parent.type === utils_1.TSESTree.AST_NODE_TYPES.ImportSpecifier) { + return node.parent; + } + } + } + return undefined; +} +exports.findImportSpecifier = findImportSpecifier; +//# sourceMappingURL=typescript.js.map \ No newline at end of file diff --git a/lint/dist/src/util/typescript.js.map b/lint/dist/src/util/typescript.js.map new file mode 100644 index 00000000000..0c4c92c898c --- /dev/null +++ b/lint/dist/src/util/typescript.js.map @@ -0,0 +1 @@ +{"version":3,"file":"typescript.js","sourceRoot":"","sources":["../../../src/util/typescript.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lint/dist/test/fixture/index.js b/lint/dist/test/fixture/index.js new file mode 100644 index 00000000000..df81b645479 --- /dev/null +++ b/lint/dist/test/fixture/index.js @@ -0,0 +1,16 @@ +"use strict"; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.fixture = exports.FIXTURE = void 0; +exports.FIXTURE = 'lint/test/fixture/'; +function fixture(path) { + return exports.FIXTURE + path; +} +exports.fixture = fixture; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lint/dist/test/fixture/index.js.map b/lint/dist/test/fixture/index.js.map new file mode 100644 index 00000000000..a8024ec6063 --- /dev/null +++ b/lint/dist/test/fixture/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../test/fixture/index.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEU,QAAA,OAAO,GAAG,oBAAoB,CAAC;AAE5C,SAAgB,OAAO,CAAC,IAAY;IAClC,OAAO,eAAO,GAAG,IAAI,CAAC;AACxB,CAAC;AAFD,0BAEC"} \ No newline at end of file diff --git a/lint/dist/test/rules.spec.js b/lint/dist/test/rules.spec.js new file mode 100644 index 00000000000..02c9d650ead --- /dev/null +++ b/lint/dist/test/rules.spec.js @@ -0,0 +1,26 @@ +"use strict"; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const html_1 = __importDefault(require("../src/rules/html")); +const ts_1 = __importDefault(require("../src/rules/ts")); +const testing_1 = require("./testing"); +describe('TypeScript rules', () => { + for (const { info, rule, tests } of ts_1.default.index) { + testing_1.tsRuleTester.run(info.name, rule, tests); + } +}); +describe('HTML rules', () => { + for (const { info, rule, tests } of html_1.default.index) { + testing_1.htmlRuleTester.run(info.name, rule, tests); + } +}); +//# sourceMappingURL=rules.spec.js.map \ No newline at end of file diff --git a/lint/dist/test/rules.spec.js.map b/lint/dist/test/rules.spec.js.map new file mode 100644 index 00000000000..12a7771b974 --- /dev/null +++ b/lint/dist/test/rules.spec.js.map @@ -0,0 +1 @@ +{"version":3,"file":"rules.spec.js","sourceRoot":"","sources":["../../test/rules.spec.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;;;AAEH,6DAA0D;AAC1D,yDAAsD;AACtD,uCAGmB;AAEnB,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,YAAQ,CAAC,KAAK,EAAE,CAAC;QACnD,sBAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAY,CAAC,CAAC;IAClD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,cAAU,CAAC,KAAK,EAAE,CAAC;QACrD,wBAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/lint/dist/test/structure.spec.js b/lint/dist/test/structure.spec.js new file mode 100644 index 00000000000..a7522e0b95b --- /dev/null +++ b/lint/dist/test/structure.spec.js @@ -0,0 +1,69 @@ +"use strict"; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const html_1 = __importDefault(require("../src/rules/html")); +const ts_1 = __importDefault(require("../src/rules/ts")); +describe('plugin structure', () => { + for (const pluginExports of [ts_1.default, html_1.default]) { + const pluginName = pluginExports.name ?? 'UNNAMED PLUGIN'; + describe(pluginName, () => { + it('should have a name', () => { + expect(pluginExports.name).toBeTruthy(); + }); + it('should have rules', () => { + expect(pluginExports.index).toBeTruthy(); + expect(pluginExports.rules).toBeTruthy(); + expect(pluginExports.index.length).toBeGreaterThan(0); + }); + for (const ruleExports of pluginExports.index) { + const ruleName = ruleExports.info.name ?? 'UNNAMED RULE'; + describe(ruleName, () => { + it('should have a name', () => { + expect(ruleExports.info.name).toBeTruthy(); + }); + it('should be included under the right name in the plugin', () => { + expect(pluginExports.rules[ruleExports.info.name]).toBe(ruleExports.rule); + }); + it('should contain metadata', () => { + expect(ruleExports.info).toBeTruthy(); + expect(ruleExports.info.name).toBeTruthy(); + expect(ruleExports.info.meta).toBeTruthy(); + expect(ruleExports.info.defaultOptions).toBeTruthy(); + }); + it('should contain messages', () => { + expect(ruleExports.Message).toBeTruthy(); + expect(ruleExports.info.meta.messages).toBeTruthy(); + }); + describe('messages', () => { + for (const member of Object.keys(ruleExports.Message)) { + describe(member, () => { + const id = ruleExports.Message[member]; + it('should have a valid ID', () => { + expect(id).toBeTruthy(); + }); + it('should have valid metadata', () => { + expect(ruleExports.info.meta.messages[id]).toBeTruthy(); + }); + }); + } + }); + it('should contain tests', () => { + expect(ruleExports.tests).toBeTruthy(); + expect(ruleExports.tests.valid.length).toBeGreaterThan(0); + expect(ruleExports.tests.invalid.length).toBeGreaterThan(0); + }); + }); + } + }); + } +}); +//# sourceMappingURL=structure.spec.js.map \ No newline at end of file diff --git a/lint/dist/test/structure.spec.js.map b/lint/dist/test/structure.spec.js.map new file mode 100644 index 00000000000..b037a83e6aa --- /dev/null +++ b/lint/dist/test/structure.spec.js.map @@ -0,0 +1 @@ +{"version":3,"file":"structure.spec.js","sourceRoot":"","sources":["../../test/structure.spec.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lint/dist/test/testing.js b/lint/dist/test/testing.js new file mode 100644 index 00000000000..4e10d40bf93 --- /dev/null +++ b/lint/dist/test/testing.js @@ -0,0 +1,46 @@ +"use strict"; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.htmlRuleTester = exports.tsRuleTester = void 0; +const rule_tester_1 = require("@typescript-eslint/rule-tester"); +const eslint_1 = require("eslint"); +const theme_support_1 = require("../src/util/theme-support"); +const fixture_1 = require("./fixture"); +// Register themed components from test fixture +theme_support_1.themeableComponents.initialize(fixture_1.FIXTURE); +rule_tester_1.RuleTester.itOnly = fit; +rule_tester_1.RuleTester.itSkip = xit; +exports.tsRuleTester = new rule_tester_1.RuleTester({ + parser: '@typescript-eslint/parser', + defaultFilenames: { + ts: (0, fixture_1.fixture)('src/test.ts'), + tsx: 'n/a', + }, + parserOptions: { + project: (0, fixture_1.fixture)('tsconfig.json'), + }, +}); +class HtmlRuleTester extends eslint_1.RuleTester { + run(name, rule, tests) { + super.run(name, rule, { + valid: tests.valid.map((test) => ({ + filename: (0, fixture_1.fixture)('test.html'), + ...test, + })), + invalid: tests.invalid.map((test) => ({ + filename: (0, fixture_1.fixture)('test.html'), + ...test, + })), + }); + } +} +exports.htmlRuleTester = new HtmlRuleTester({ + parser: require.resolve('@angular-eslint/template-parser'), +}); +//# sourceMappingURL=testing.js.map \ No newline at end of file diff --git a/lint/dist/test/testing.js.map b/lint/dist/test/testing.js.map new file mode 100644 index 00000000000..1585f3f8bfb --- /dev/null +++ b/lint/dist/test/testing.js.map @@ -0,0 +1 @@ +{"version":3,"file":"testing.js","sourceRoot":"","sources":["../../test/testing.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH,gEAAoF;AACpF,mCAAoC;AAEpC,6DAAgE;AAChE,uCAGmB;AAGnB,+CAA+C;AAC/C,mCAAmB,CAAC,UAAU,CAAC,iBAAO,CAAC,CAAC;AAExC,wBAAoB,CAAC,MAAM,GAAG,GAAG,CAAC;AAClC,wBAAoB,CAAC,MAAM,GAAG,GAAG,CAAC;AAErB,QAAA,YAAY,GAAG,IAAI,wBAAoB,CAAC;IACnD,MAAM,EAAE,2BAA2B;IACnC,gBAAgB,EAAE;QAChB,EAAE,EAAE,IAAA,iBAAO,EAAC,aAAa,CAAC;QAC1B,GAAG,EAAE,KAAK;KACX;IACD,aAAa,EAAE;QACb,OAAO,EAAE,IAAA,iBAAO,EAAC,eAAe,CAAC;KAClC;CACF,CAAC,CAAC;AAEH,MAAM,cAAe,SAAQ,mBAAU;IACrC,GAAG,CAAC,IAAY,EAAE,IAAS,EAAE,KAAuC;QAClE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE;YACpB,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBAChC,QAAQ,EAAE,IAAA,iBAAO,EAAC,WAAW,CAAC;gBAC9B,GAAG,IAAI;aACR,CAAC,CAAC;YACH,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBACpC,QAAQ,EAAE,IAAA,iBAAO,EAAC,WAAW,CAAC;gBAC9B,GAAG,IAAI;aACR,CAAC,CAAC;SACJ,CAAC,CAAC;IACL,CAAC;CACF;AAEY,QAAA,cAAc,GAAG,IAAI,cAAc,CAAC;IAC/C,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,iCAAiC,CAAC;CAC3D,CAAC,CAAC"} \ No newline at end of file diff --git a/lint/dist/test/theme-support.spec.js b/lint/dist/test/theme-support.spec.js new file mode 100644 index 00000000000..f32b9ecfe8f --- /dev/null +++ b/lint/dist/test/theme-support.spec.js @@ -0,0 +1,24 @@ +"use strict"; +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const theme_support_1 = require("../src/util/theme-support"); +describe('theme-support', () => { + describe('themeable component registry', () => { + it('should contain all themeable components from the fixture', () => { + expect(theme_support_1.themeableComponents.entries.size).toBe(1); + expect(theme_support_1.themeableComponents.byBasePath.size).toBe(1); + expect(theme_support_1.themeableComponents.byWrapperPath.size).toBe(1); + expect(theme_support_1.themeableComponents.byBaseClass.size).toBe(1); + expect(theme_support_1.themeableComponents.byBaseClass.get('TestThemeableComponent')).toBeTruthy(); + expect(theme_support_1.themeableComponents.byBasePath.get('src/app/test/test-themeable.component.ts')).toBeTruthy(); + expect(theme_support_1.themeableComponents.byWrapperPath.get('src/app/test/themed-test-themeable.component.ts')).toBeTruthy(); + }); + }); +}); +//# sourceMappingURL=theme-support.spec.js.map \ No newline at end of file diff --git a/lint/dist/test/theme-support.spec.js.map b/lint/dist/test/theme-support.spec.js.map new file mode 100644 index 00000000000..e36dd7525f8 --- /dev/null +++ b/lint/dist/test/theme-support.spec.js.map @@ -0,0 +1 @@ +{"version":3,"file":"theme-support.spec.js","sourceRoot":"","sources":["../../test/theme-support.spec.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAEH,6DAAgE;AAEhE,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,QAAQ,CAAC,8BAA8B,EAAE,GAAG,EAAE;QAC5C,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;YAClE,MAAM,CAAC,mCAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjD,MAAM,CAAC,mCAAmB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACpD,MAAM,CAAC,mCAAmB,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACvD,MAAM,CAAC,mCAAmB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAErD,MAAM,CAAC,mCAAmB,CAAC,WAAW,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;YACnF,MAAM,CAAC,mCAAmB,CAAC,UAAU,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;YACpG,MAAM,CAAC,mCAAmB,CAAC,aAAa,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;QAChH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/src/app/core/data/relationship-data.service.spec.ts b/src/app/core/data/relationship-data.service.spec.ts index 4432d5213ae..625e0d62f4d 100644 --- a/src/app/core/data/relationship-data.service.spec.ts +++ b/src/app/core/data/relationship-data.service.spec.ts @@ -125,7 +125,8 @@ describe('RelationshipDataService', () => { const itemService = jasmine.createSpyObj('itemService', { findById: (uuid) => createSuccessfulRemoteDataObject(relatedItems.find((relatedItem) => relatedItem.id === uuid)), - findByHref: createSuccessfulRemoteDataObject$(relatedItems[0]) + findByHref: createSuccessfulRemoteDataObject$(relatedItems[0]), + getIDHrefObs: (uuid: string) => observableOf(`https://demo.dspace.org/server/api/core/items/${uuid}`), }); function initTestService() { @@ -240,6 +241,16 @@ describe('RelationshipDataService', () => { }); }); + describe('searchByItemsAndType', () => { + it('should call addDependency for each item to invalidate the request when one of the items is update', () => { + spyOn(service as any, 'addDependency'); + + service.searchByItemsAndType(relationshipType.id, item.id, relationshipType.leftwardType, ['item-id-1', 'item-id-2']); + + expect((service as any).addDependency).toHaveBeenCalledTimes(2); + }); + }); + describe('resolveMetadataRepresentation', () => { const parentItem: Item = Object.assign(new Item(), { id: 'parent-item', diff --git a/src/app/core/data/relationship-data.service.ts b/src/app/core/data/relationship-data.service.ts index a4c5c0aba15..508d6553ca9 100644 --- a/src/app/core/data/relationship-data.service.ts +++ b/src/app/core/data/relationship-data.service.ts @@ -533,13 +533,18 @@ export class RelationshipDataService extends IdentifiableDataService>> = this.searchBy( 'byItemsAndType', { searchParams: searchParams }, ) as Observable>>; + arrayOfItemIds.forEach((itemId: string) => { + this.addDependency(searchRD$, this.itemService.getIDHrefObs(encodeURIComponent(itemId))); + }); + + return searchRD$; } /** diff --git a/src/app/item-page/edit-item-page/edit-item-page.module.ts b/src/app/item-page/edit-item-page/edit-item-page.module.ts index 0a75394dddc..60aa2828bd8 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.module.ts @@ -46,6 +46,9 @@ import { ResultsBackButtonModule } from '../../shared/results-back-button/result import { AccessControlFormModule } from '../../shared/access-control-form-container/access-control-form.module'; +import { + EditRelationshipListWrapperComponent +} from './item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component'; /** * Module that contains all components related to the Edit Item page administrator functionality @@ -94,6 +97,7 @@ import { ItemRegisterDoiComponent, ItemCurateComponent, ItemAccessControlComponent, + EditRelationshipListWrapperComponent, ], providers: [ BundleDataService, diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.spec.ts index f9694167838..ebc2a82d84c 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.spec.ts @@ -185,6 +185,7 @@ describe('EditItemRelationshipsService', () => { expect(itemService.invalidateByHref).toHaveBeenCalledWith(currentItem.self); expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem1.self); + expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem2.self); expect(notificationsService.success).toHaveBeenCalledTimes(1); }); @@ -265,6 +266,116 @@ describe('EditItemRelationshipsService', () => { }); }); + describe('isProvidedItemTypeLeftType', () => { + it('should return true if the provided item corresponds to the left type of the relationship', (done) => { + const relationshipType = Object.assign(new RelationshipType(), { + leftType: createSuccessfulRemoteDataObject$({id: 'leftType'}), + rightType: createSuccessfulRemoteDataObject$({id: 'rightType'}), + }); + const itemType = Object.assign(new ItemType(), {id: 'leftType'} ); + const item = Object.assign(new Item(), {uuid: 'item-uuid'}); + + const result = service.isProvidedItemTypeLeftType(relationshipType, itemType, item); + result.subscribe((resultValue) => { + expect(resultValue).toBeTrue(); + done(); + }); + }); + + it('should return false if the provided item corresponds to the right type of the relationship', (done) => { + const relationshipType = Object.assign(new RelationshipType(), { + leftType: createSuccessfulRemoteDataObject$({id: 'leftType'}), + rightType: createSuccessfulRemoteDataObject$({id: 'rightType'}), + }); + const itemType = Object.assign(new ItemType(), {id: 'rightType'} ); + const item = Object.assign(new Item(), {uuid: 'item-uuid'}); + + const result = service.isProvidedItemTypeLeftType(relationshipType, itemType, item); + result.subscribe((resultValue) => { + expect(resultValue).toBeFalse(); + done(); + }); + }); + + it('should return undefined if the provided item corresponds does not match any of the relationship types', (done) => { + const relationshipType = Object.assign(new RelationshipType(), { + leftType: createSuccessfulRemoteDataObject$({id: 'leftType'}), + rightType: createSuccessfulRemoteDataObject$({id: 'rightType'}), + }); + const itemType = Object.assign(new ItemType(), {id: 'something-else'} ); + const item = Object.assign(new Item(), {uuid: 'item-uuid'}); + + const result = service.isProvidedItemTypeLeftType(relationshipType, itemType, item); + result.subscribe((resultValue) => { + expect(resultValue).toBeUndefined(); + done(); + }); + }); + }); + + describe('relationshipMatchesBothSameTypes', () => { + it('should return true if both left and right type of the relationship type are the same and match the provided itemtype', (done) => { + const relationshipType = Object.assign(new RelationshipType(), { + leftType: createSuccessfulRemoteDataObject$({id: 'sameType'}), + rightType: createSuccessfulRemoteDataObject$({id:'sameType'}), + leftwardType: 'isDepartmentOfDivision', + rightwardType: 'isDivisionOfDepartment', + }); + const itemType = Object.assign(new ItemType(), {id: 'sameType'} ); + + const result = service.shouldDisplayBothRelationshipSides(relationshipType, itemType); + result.subscribe((resultValue) => { + expect(resultValue).toBeTrue(); + done(); + }); + }); + it('should return false if both left and right type of the relationship type are the same and match the provided itemtype but the leftwardType & rightwardType is identical', (done) => { + const relationshipType = Object.assign(new RelationshipType(), { + leftType: createSuccessfulRemoteDataObject$({ id: 'sameType' }), + rightType: createSuccessfulRemoteDataObject$({ id: 'sameType' }), + leftwardType: 'isOrgUnitOfOrgUnit', + rightwardType: 'isOrgUnitOfOrgUnit', + }); + const itemType = Object.assign(new ItemType(), { id: 'sameType' }); + + const result = service.shouldDisplayBothRelationshipSides(relationshipType, itemType); + result.subscribe((resultValue) => { + expect(resultValue).toBeFalse(); + done(); + }); + }); + it('should return false if both left and right type of the relationship type are the same and do not match the provided itemtype', (done) => { + const relationshipType = Object.assign(new RelationshipType(), { + leftType: createSuccessfulRemoteDataObject$({id: 'sameType'}), + rightType: createSuccessfulRemoteDataObject$({id: 'sameType'}), + leftwardType: 'isDepartmentOfDivision', + rightwardType: 'isDivisionOfDepartment', + }); + const itemType = Object.assign(new ItemType(), {id: 'something-else'} ); + + const result = service.shouldDisplayBothRelationshipSides(relationshipType, itemType); + result.subscribe((resultValue) => { + expect(resultValue).toBeFalse(); + done(); + }); + }); + it('should return false if both left and right type of the relationship type are different', (done) => { + const relationshipType = Object.assign(new RelationshipType(), { + leftType: createSuccessfulRemoteDataObject$({id: 'leftType'}), + rightType: createSuccessfulRemoteDataObject$({id: 'rightType'}), + leftwardType: 'isAuthorOfPublication', + rightwardType: 'isPublicationOfAuthor', + }); + const itemType = Object.assign(new ItemType(), {id: 'leftType'} ); + + const result = service.shouldDisplayBothRelationshipSides(relationshipType, itemType); + result.subscribe((resultValue) => { + expect(resultValue).toBeFalse(); + done(); + }); + }); + }); + describe('displayNotifications', () => { it('should show one success notification when multiple requests succeeded', () => { service.displayNotifications([ diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts b/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts index 2cecd878b7f..b9f3738e7bd 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts @@ -10,7 +10,7 @@ import { } from '../../../core/data/object-updates/object-updates.reducer'; import { RemoteData } from '../../../core/data/remote-data'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; -import { EMPTY, Observable, BehaviorSubject, Subscription } from 'rxjs'; +import { EMPTY, Observable, BehaviorSubject, Subscription, combineLatest as observableCombineLatest } from 'rxjs'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ItemDataService } from '../../../core/data/item-data.service'; import { Item } from '../../../core/shared/item.model'; @@ -20,6 +20,9 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { RelationshipDataService } from '../../../core/data/relationship-data.service'; import { EntityTypeDataService } from '../../../core/data/entity-type-data.service'; import { TranslateService } from '@ngx-translate/core'; +import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; +import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; +import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; @Injectable({ providedIn: 'root' @@ -58,7 +61,17 @@ export class EditItemRelationshipsService { // process each update one by one, while waiting for the previous to finish concatMap((update: FieldUpdate) => { if (update.changeType === FieldChangeType.REMOVE) { - return this.deleteRelationship(update.field as DeleteRelationship).pipe(take(1)); + return this.deleteRelationship(update.field as DeleteRelationship).pipe( + take(1), + switchMap((deleteRD: RemoteData) => { + if (deleteRD.hasSucceeded) { + return this.itemService.invalidateByHref((update.field as DeleteRelationship).relatedItem._links.self.href).pipe( + map(() => deleteRD), + ); + } + return [deleteRD]; + }), + ); } else if (update.changeType === FieldChangeType.ADD) { return this.addRelationship(update.field as RelationshipIdentifiable).pipe( take(1), @@ -169,6 +182,55 @@ export class EditItemRelationshipsService { } } + isProvidedItemTypeLeftType(relationshipType: RelationshipType, itemType: ItemType, item: Item): Observable { + return this.getRelationshipLeftAndRightType(relationshipType).pipe( + map(([leftType, rightType]: [ItemType, ItemType]) => { + if (leftType.id === itemType.id) { + return true; + } + + if (rightType.id === itemType.id) { + return false; + } + + // should never happen... + console.warn(`The item ${item.uuid} is not on the right or the left side of relationship type ${relationshipType.uuid}`); + return undefined; + }) + ); + } + + /** + * Whether both side of the relationship need to be displayed on the edit relationship page or not. + * + * @param relationshipType The relationship type + * @param itemType The item type + */ + shouldDisplayBothRelationshipSides(relationshipType: RelationshipType, itemType: ItemType): Observable { + return this.getRelationshipLeftAndRightType(relationshipType).pipe( + map(([leftType, rightType]: [ItemType, ItemType]) => { + return leftType.id === itemType.id && rightType.id === itemType.id && relationshipType.leftwardType !== relationshipType.rightwardType; + }), + ); + } + + protected getRelationshipLeftAndRightType(relationshipType: RelationshipType): Observable<[ItemType, ItemType]> { + const leftType$: Observable = relationshipType.leftType.pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + ); + + const rightType$: Observable = relationshipType.rightType.pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + ); + + return observableCombineLatest([ + leftType$, + rightType$, + ]); + } + /** @@ -185,6 +247,5 @@ export class EditItemRelationshipsService { */ getNotificationContent(key: string): string { return this.translateService.instant(this.notificationsPrefix + key + '.content'); - } } diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.html b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.html new file mode 100644 index 00000000000..ed7fb190f2d --- /dev/null +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.html @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.scss b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.spec.ts new file mode 100644 index 00000000000..a6f0c9e0a5f --- /dev/null +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.spec.ts @@ -0,0 +1,109 @@ +import { ChangeDetectorRef, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; +import { EditRelationshipListWrapperComponent } from './edit-relationship-list-wrapper.component'; +import { EditItemRelationshipsService } from '../edit-item-relationships.service'; +import { By } from '@angular/platform-browser'; +import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { ItemType } from '../../../../core/shared/item-relationships/item-type.model'; +import { Item } from '../../../../core/shared/item.model'; +import { cold } from 'jasmine-marbles'; + +describe('EditRelationshipListWrapperComponent', () => { + let editItemRelationshipsService: EditItemRelationshipsService; + let comp: EditRelationshipListWrapperComponent; + let fixture: ComponentFixture; + + const leftType = Object.assign(new ItemType(), {id: 'leftType', label: 'leftTypeString'}); + const rightType = Object.assign(new ItemType(), {id: 'rightType', label: 'rightTypeString'}); + + const relationshipType = Object.assign(new RelationshipType(), { + id: '1', + leftMaxCardinality: null, + leftMinCardinality: 0, + leftType: createSuccessfulRemoteDataObject$(leftType), + leftwardType: 'isOrgUnitOfOrgUnit', + rightMaxCardinality: null, + rightMinCardinality: 0, + rightType: createSuccessfulRemoteDataObject$(rightType), + rightwardType: 'isOrgUnitOfOrgUnit', + uuid: 'relationshiptype-1', + }); + + const item = Object.assign(new Item(), {uuid: 'item-uuid'}); + + beforeEach(waitForAsync(() => { + + editItemRelationshipsService = jasmine.createSpyObj('editItemRelationshipsService', { + isProvidedItemTypeLeftType: observableOf(true), + shouldDisplayBothRelationshipSides: observableOf(false), + }); + + + TestBed.configureTestingModule({ + // imports: [NoopAnimationsModule, SharedModule, TranslateModule.forRoot()], + declarations: [EditRelationshipListWrapperComponent], + providers: [ + {provide: EditItemRelationshipsService, useValue: editItemRelationshipsService}, + ChangeDetectorRef + ], schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }).compileComponents(); + + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditRelationshipListWrapperComponent); + comp = fixture.componentInstance; + comp.relationshipType = relationshipType; + comp.itemType = leftType; + comp.item = item; + + fixture.detectChanges(); + }); + + describe('onInit', () => { + it('should render the component', () => { + expect(comp).toBeTruthy(); + }); + it('should set currentItemIsLeftItem$ and bothItemsMatchType$ based on the provided relationshipType, itemType and item', () => { + expect(editItemRelationshipsService.isProvidedItemTypeLeftType).toHaveBeenCalledWith(relationshipType, leftType, item); + expect(editItemRelationshipsService.shouldDisplayBothRelationshipSides).toHaveBeenCalledWith(relationshipType, leftType); + + expect(comp.currentItemIsLeftItem$.getValue()).toEqual(true); + expect(comp.shouldDisplayBothRelationshipSides$).toBeObservable(cold('(a|)', { a: false })); + }); + }); + + describe('when the current item is left', () => { + it('should render one relationship list section', () => { + const relationshipLists = fixture.debugElement.queryAll(By.css('ds-edit-relationship-list')); + expect(relationshipLists.length).toEqual(1); + }); + }); + + describe('when the current item is right', () => { + it('should render one relationship list section', () => { + (editItemRelationshipsService.isProvidedItemTypeLeftType as jasmine.Spy).and.returnValue(observableOf(false)); + comp.ngOnInit(); + fixture.detectChanges(); + + const relationshipLists = fixture.debugElement.queryAll(By.css('ds-edit-relationship-list')); + expect(relationshipLists.length).toEqual(1); + }); + }); + + describe('when the current item is both left and right', () => { + it('should render two relationship list sections', () => { + (editItemRelationshipsService.shouldDisplayBothRelationshipSides as jasmine.Spy).and.returnValue(observableOf(true)); + comp.ngOnInit(); + fixture.detectChanges(); + + const relationshipLists = fixture.debugElement.queryAll(By.css('ds-edit-relationship-list')); + expect(relationshipLists.length).toEqual(2); + }); + }); + +}); diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.ts new file mode 100644 index 00000000000..8ff408cb792 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list-wrapper/edit-relationship-list-wrapper.component.ts @@ -0,0 +1,88 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { Item } from '../../../../core/shared/item.model'; +import { hasValue } from '../../../../shared/empty.util'; +import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; +import { ItemType } from '../../../../core/shared/item-relationships/item-type.model'; +import { EditItemRelationshipsService } from '../edit-item-relationships.service'; + +@Component({ + selector: 'ds-edit-relationship-list-wrapper', + styleUrls: ['./edit-relationship-list-wrapper.component.scss'], + templateUrl: './edit-relationship-list-wrapper.component.html', +}) +/** + * A component creating a list of editable relationships of a certain type + * The relationships are rendered as a list of related items + */ +export class EditRelationshipListWrapperComponent implements OnInit, OnDestroy { + + /** + * The item to display related items for + */ + @Input() item: Item; + + @Input() itemType: ItemType; + + /** + * The URL to the current page + * Used to fetch updates for the current item from the store + */ + @Input() url: string; + + /** + * The label of the relationship-type we're rendering a list for + */ + @Input() relationshipType: RelationshipType; + + /** + * If updated information has changed + */ + @Input() hasChanges!: Observable; + + /** + * The event emmiter to submit the new information + */ + @Output() submitModal: EventEmitter = new EventEmitter(); + + /** + * Observable that emits true if {@link itemType} is on the left-hand side of {@link relationshipType}, + * false if it is on the right-hand side and undefined in the rare case that it is on neither side. + */ + currentItemIsLeftItem$: BehaviorSubject = new BehaviorSubject(undefined); + + + isLeftItem$ = new BehaviorSubject(true); + + isRightItem$ = new BehaviorSubject(false); + + shouldDisplayBothRelationshipSides$: Observable; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + private subs: Subscription[] = []; + + constructor( + protected editItemRelationshipsService: EditItemRelationshipsService, + ) { + } + + + ngOnInit(): void { + this.subs.push(this.editItemRelationshipsService.isProvidedItemTypeLeftType(this.relationshipType, this.itemType, this.item) + .subscribe((nextValue: boolean) => { + this.currentItemIsLeftItem$.next(nextValue); + })); + + this.shouldDisplayBothRelationshipSides$ = this.editItemRelationshipsService.shouldDisplayBothRelationshipSides(this.relationshipType, this.itemType); + } + + + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } +} diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index c0f7517e395..312f2936ac7 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -2,7 +2,7 @@ import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; -import { of as observableOf } from 'rxjs'; +import { BehaviorSubject, of as observableOf } from 'rxjs'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { RelationshipDataService } from '../../../../core/data/relationship-data.service'; @@ -63,6 +63,7 @@ describe('EditRelationshipListComponent', () => { let relationships: Relationship[]; let relationshipType: RelationshipType; let paginationOptions: PaginationComponentOptions; + let currentItemIsLeftItem$ = new BehaviorSubject(true); const resetComponent = () => { fixture = TestBed.createComponent(EditRelationshipListComponent); @@ -73,6 +74,7 @@ describe('EditRelationshipListComponent', () => { comp.url = url; comp.relationshipType = relationshipType; comp.hasChanges = observableOf(false); + comp.currentItemIsLeftItem$ = currentItemIsLeftItem$; fixture.detectChanges(); }; @@ -296,6 +298,7 @@ describe('EditRelationshipListComponent', () => { leftwardType: 'isAuthorOfPublication', rightwardType: 'isPublicationOfAuthor', }); + currentItemIsLeftItem$ = new BehaviorSubject(true); relationshipService.getItemRelationshipsByLabel.calls.reset(); resetComponent(); }); @@ -320,6 +323,7 @@ describe('EditRelationshipListComponent', () => { leftwardType: 'isPublicationOfAuthor', rightwardType: 'isAuthorOfPublication', }); + currentItemIsLeftItem$ = new BehaviorSubject(false); relationshipService.getItemRelationshipsByLabel.calls.reset(); resetComponent(); }); diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index 24ab999f620..cd597c8f225 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -26,13 +26,14 @@ import { toArray, concatMap } from 'rxjs/operators'; -import { hasNoValue, hasValue, hasValueOperator } from '../../../../shared/empty.util'; +import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../../../shared/empty.util'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { getAllSucceededRemoteData, + getFirstCompletedRemoteData, getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload, @@ -113,7 +114,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { * Observable that emits true if {@link itemType} is on the left-hand side of {@link relationshipType}, * false if it is on the right-hand side and undefined in the rare case that it is on neither side. */ - private currentItemIsLeftItem$: BehaviorSubject = new BehaviorSubject(undefined); + @Input() currentItemIsLeftItem$: BehaviorSubject = new BehaviorSubject(undefined); relatedEntityType$: Observable; @@ -213,18 +214,15 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { * Get the relevant label for this relationship type */ private getLabel(): Observable { - return observableCombineLatest([ - this.relationshipType.leftType, - this.relationshipType.rightType, - ].map((itemTypeRD) => itemTypeRD.pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - ))).pipe( - map((itemTypes: ItemType[]) => [ - this.relationshipType.leftwardType, - this.relationshipType.rightwardType, - ][itemTypes.findIndex((itemType) => itemType.id === this.itemType.id)]), - ); + return this.currentItemIsLeftItem$.pipe( + map((currentItemIsLeftItem) => { + if (currentItemIsLeftItem) { + return this.relationshipType.leftwardType; + } else { + return this.relationshipType.rightwardType; + } + }) + ); } /** @@ -251,6 +249,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { modalComp.toAdd = []; modalComp.toRemove = []; modalComp.isPending = false; + modalComp.hiddenQuery = '-search.resourceid:' + this.item.uuid; this.item.owningCollection.pipe( getFirstSucceededRemoteDataPayload() @@ -279,7 +278,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { } } - this.loading$.next(true); + this.loading$.next(isNotEmpty(modalComp.toAdd) || isNotEmpty(modalComp.toRemove)); // emit the last page again to trigger a fieldupdates refresh this.relationshipsRd$.next(this.relationshipsRd$.getValue()); }); @@ -297,6 +296,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { } else { modalComp.toRemove.push(searchResult); } + this.loading$.next(isNotEmpty(modalComp.toAdd) || isNotEmpty(modalComp.toRemove)); }); }; @@ -336,6 +336,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { type: this.relationshipType, originalIsLeft: isLeft, originalItem: this.item, + relatedItem, relationship, } as RelationshipIdentifiable; return this.objectUpdatesService.saveRemoveFieldUpdate(this.url,update); @@ -369,6 +370,11 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { modalComp.toAdd = []; modalComp.toRemove = []; + this.loading$.next(false); + }; + + modalComp.closeEv = () => { + this.loading$.next(false); }; this.relatedEntityType$ @@ -424,24 +430,6 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { this.relationshipMessageKey$ = this.getRelationshipMessageKey(); - this.subs.push(this.relationshipLeftAndRightType$.pipe( - map(([leftType, rightType]: [ItemType, ItemType]) => { - if (leftType.id === this.itemType.id) { - return true; - } - - if (rightType.id === this.itemType.id) { - return false; - } - - // should never happen... - console.warn(`The item ${this.item.uuid} is not on the right or the left side of relationship type ${this.relationshipType.uuid}`); - return undefined; - }) - ).subscribe((nextValue: boolean) => { - this.currentItemIsLeftItem$.next(nextValue); - })); - // initialize the pagination options this.paginationConfig = new PaginationComponentOptions(); @@ -508,10 +496,24 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { this.relationshipService.isLeftItem(relationship, this.item).pipe( // emit an array containing both the relationship and whether it's the left item, // as we'll need both - map((isLeftItem: boolean) => [relationship, isLeftItem]) - ) + switchMap((isLeftItem: boolean) => { + if (isLeftItem) { + return relationship.rightItem.pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + map((relatedItem: Item) => [relationship, isLeftItem, relatedItem]), + ); + } else { + return relationship.leftItem.pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + map((relatedItem: Item) => [relationship, isLeftItem, relatedItem]), + ); + } + }), + ), ), - map(([relationship, isLeftItem]: [Relationship, boolean]) => { + map(([relationship, isLeftItem, relatedItem]: [Relationship, boolean, Item]) => { // turn it into a RelationshipIdentifiable, an const nameVariant = isLeftItem ? relationship.rightwardValue : relationship.leftwardValue; @@ -521,6 +523,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { relationship, originalIsLeft: isLeftItem, originalItem: this.item, + relatedItem: relatedItem, nameVariant, } as RelationshipIdentifiable; }), diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts index cf0c610f8f4..b56fc24ebb9 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts @@ -90,13 +90,11 @@ export class EditRelationshipComponent implements OnChanges { getRemoteDataPayload(), filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) ); - this.relatedItem$ = observableCombineLatest( + this.relatedItem$ = observableCombineLatest([ this.leftItem$, this.rightItem$, - ).pipe( - map((items: Item[]) => - items.find((item) => item.uuid !== this.editItem.uuid) - ) + ]).pipe( + map(([leftItem, rightItem]: [Item, Item]) => leftItem.uuid === this.editItem.uuid ? rightItem : leftItem), ); } else { this.relatedItem$ = of(this.update.relatedItem); diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.html index 755731f5768..85f41a25d9a 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.html +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -5,13 +5,13 @@
- + >
diff --git a/src/app/item-page/simple/item-types/shared/item-relationships-utils.ts b/src/app/item-page/simple/item-types/shared/item-relationships-utils.ts index 0c4e82178f5..d9f3a303696 100644 --- a/src/app/item-page/simple/item-types/shared/item-relationships-utils.ts +++ b/src/app/item-page/simple/item-types/shared/item-relationships-utils.ts @@ -1,4 +1,4 @@ -import { combineLatest as observableCombineLatest, Observable, zip as observableZip } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs'; import { distinctUntilChanged, map, mergeMap, switchMap } from 'rxjs/operators'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; @@ -45,17 +45,19 @@ export const compareArraysUsingIds = () => /** * Operator for turning a list of relationships into a list of the relevant items * @param {string} thisId The item's id of which the relations belong to - * @returns {(source: Observable) => Observable} */ -export const relationsToItems = (thisId: string) => +export const relationsToItems = (thisId: string): (source: Observable) => Observable => (source: Observable): Observable => source.pipe( - mergeMap((rels: Relationship[]) => - observableZip( - ...rels.map((rel: Relationship) => observableCombineLatest(rel.leftItem, rel.rightItem)) - ) - ), - map((arr) => + mergeMap((relationships: Relationship[]) => { + if (relationships.length === 0) { + return observableOf([]); + } + return observableZip( + ...relationships.map((rel: Relationship) => observableCombineLatest([rel.leftItem, rel.rightItem])), + ); + }), + map((arr: [RemoteData, RemoteData][]) => arr .filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded) .map(([leftItem, rightItem]) => { @@ -74,9 +76,9 @@ export const relationsToItems = (thisId: string) => * Operator for turning a paginated list of relationships into a paginated list of the relevant items * The result is wrapped in the original RemoteData and PaginatedList * @param {string} thisId The item's id of which the relations belong to - * @returns {(source: Observable) => Observable} */ -export const paginatedRelationsToItems = (thisId: string) => (source: Observable>>): Observable>> => +export const paginatedRelationsToItems = (thisId: string): (source: Observable>>) => Observable>> => + (source: Observable>>): Observable>> => source.pipe( getFirstCompletedRemoteData(), switchMap((relationshipsRD: RemoteData>) => { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index 4669c6e50f2..892cd9beb88 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -19,6 +19,7 @@