Browse Source

refactor: Remove OC.SystemTags JS API

Signed-off-by: Carl Schwan <carl.schwan@nextcloud.com>
pull/55320/head
Carl Schwan 2 weeks ago
committed by Ferdinand Thiessen
parent
commit
e86ca11e0b
  1. 1
      apps/systemtags/lib/AppInfo/Application.php
  2. 5
      core/css/systemtags.css
  3. 1
      core/css/systemtags.css.map
  4. 79
      core/css/systemtags.scss
  5. 54
      core/js/tests/specs/systemtags/systemtagsSpec.js
  6. 68
      core/js/tests/specs/systemtags/systemtagscollectionSpec.js
  7. 614
      core/js/tests/specs/systemtags/systemtagsinputfieldSpec.js
  8. 13
      core/src/systemtags/merged-systemtags.js
  9. 58
      core/src/systemtags/systemtagmodel.js
  10. 53
      core/src/systemtags/systemtags.js
  11. 88
      core/src/systemtags/systemtagscollection.js
  12. 444
      core/src/systemtags/systemtagsinputfield.js
  13. 84
      core/src/systemtags/systemtagsmappingcollection.js
  14. 13
      core/src/systemtags/templates/result.handlebars
  15. 7
      core/src/systemtags/templates/result_form.handlebars
  16. 5
      core/src/systemtags/templates/selection.handlebars
  17. 1
      webpack.modules.js

1
apps/systemtags/lib/AppInfo/Application.php

@ -49,7 +49,6 @@ class Application extends App implements IBootstrap {
$dispatcher->addListener(
LoadAdditionalScriptsEvent::class,
function (): void {
Util::addScript('core', 'systemtags');
Util::addInitScript(self::APP_ID, 'init');
}
);

5
core/css/systemtags.css

@ -1,5 +0,0 @@
/*!
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/.systemtags-select2-dropdown .select2-result-label{height:25px}.systemtags-select2-dropdown .select2-result-label .checkmark{visibility:hidden;margin-inline:-5px 5px;padding:4px}.systemtags-select2-dropdown .select2-result-label .new-item .systemtags-actions{display:none}.systemtags-select2-dropdown .select2-selected .select2-result-label .checkmark{visibility:visible}.systemtags-select2-dropdown .select2-result-label .icon{display:inline-block;opacity:.5}.systemtags-select2-dropdown .select2-result-label .icon.rename{padding:4px}.systemtags-select2-dropdown .systemtags-actions{position:absolute;inset-inline-end:5px}.systemtags-select2-dropdown .systemtags-rename-form{display:inline-block;width:calc(100% - 20px);top:-6px;position:relative}.systemtags-select2-dropdown .systemtags-rename-form input{display:inline-block;height:30px;width:calc(100% - 40px)}.systemtags-select2-dropdown .label{width:85%;display:inline-block;overflow:hidden;text-overflow:ellipsis}.systemtags-select2-dropdown .label.hidden{display:none}.systemtags-select2-dropdown span{line-height:25px}.systemtags-select2-dropdown .systemtags-item{display:inline-block;height:25px;width:100%}.systemtags-select2-container{width:100%}.systemtags-select2-container .select2-choices{flex-wrap:nowrap !important;max-height:44px}.systemtags-select2-container .select2-choices .select2-search-choice.select2-locked .label{opacity:.5}#select2-drop.systemtags-select2-dropdown .select2-results li.select2-result{padding:5px}/*# sourceMappingURL=systemtags.css.map */

1
core/css/systemtags.css.map

@ -1 +0,0 @@
{"version":3,"sourceRoot":"","sources":["systemtags.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA,GAOC,mDACC,YAEA,8DACC,kBACA,uBACA,YAED,iFACC,aAGF,gFACC,mBAED,yDACC,qBACA,WACA,gEACC,YAGF,iDACC,kBACA,qBAED,qDACC,qBACA,wBACA,SACA,kBACA,2DACC,qBACA,YACA,wBAGF,oCACC,UACA,qBACA,gBACA,uBACA,2CACC,aAGF,kCACC,iBAED,8CACC,qBACA,YACA,WAIF,8BACC,WAEA,+CACC,4BACA,gBAGD,4FACC,WAIF,6EACC","file":"systemtags.css"}

79
core/css/systemtags.scss

@ -1,79 +0,0 @@
/*!
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
.systemtags-select2-dropdown {
.select2-result-label {
height: 25px;
.checkmark {
visibility: hidden;
margin-inline: -5px 5px;
padding: 4px;
}
.new-item .systemtags-actions {
display: none;
}
}
.select2-selected .select2-result-label .checkmark {
visibility: visible;
}
.select2-result-label .icon {
display: inline-block;
opacity: .5;
&.rename {
padding: 4px;
}
}
.systemtags-actions {
position: absolute;
inset-inline-end: 5px;
}
.systemtags-rename-form {
display: inline-block;
width: calc(100% - 20px);
top: -6px;
position: relative;
input {
display: inline-block;
height: 30px;
width: calc(100% - 40px);
}
}
.label {
width: 85%;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
&.hidden {
display: none;
}
}
span {
line-height: 25px;
}
.systemtags-item {
display: inline-block;
height: 25px;
width: 100%;
}
}
.systemtags-select2-container {
width: 100%;
.select2-choices {
flex-wrap: nowrap !important;
max-height: 44px;
}
.select2-choices .select2-search-choice.select2-locked .label {
opacity: 0.5;
}
}
#select2-drop.systemtags-select2-dropdown .select2-results li.select2-result {
padding: 5px;
}

54
core/js/tests/specs/systemtags/systemtagsSpec.js

@ -1,54 +0,0 @@
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
describe('OC.SystemTags tests', function() {
it('describes non existing tag', function() {
var $return = OC.SystemTags.getDescriptiveTag('23');
expect($return.textContent).toEqual('Non-existing tag #23');
expect($return.classList.contains('non-existing-tag')).toEqual(true);
});
it('describes SystemTagModel', function() {
var tag = new OC.SystemTags.SystemTagModel({
id: 23,
name: 'Twenty Three',
userAssignable: true,
userVisible: true
});
var $return = OC.SystemTags.getDescriptiveTag(tag);
expect($return.textContent).toEqual('Twenty Three');
expect($return.classList.contains('non-existing-tag')).toEqual(false);
});
it('describes JSON tag object', function() {
var $return = OC.SystemTags.getDescriptiveTag({
id: 42,
name: 'Fourty Two',
userAssignable: true,
userVisible: true
});
expect($return.textContent).toEqual('Fourty Two');
expect($return.classList.contains('non-existing-tag')).toEqual(false);
});
it('scope', function() {
function testScope(userVisible, userAssignable, expectedText) {
var $return = OC.SystemTags.getDescriptiveTag({
id: 42,
name: 'Fourty Two',
userAssignable: userAssignable,
userVisible: userVisible
});
expect($return.textContent).toEqual(expectedText);
expect($return.classList.contains('non-existing-tag')).toEqual(false);
}
testScope(true, true, 'Fourty Two');
testScope(false, true, 'Fourty Two (Invisible)');
testScope(false, false, 'Fourty Two (Invisible)');
testScope(true, false, 'Fourty Two (Restricted)');
});
});

68
core/js/tests/specs/systemtags/systemtagscollectionSpec.js

@ -1,68 +0,0 @@
/**
* SPDX-FileCopyrightText: 2016 ownCloud Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
describe('OC.SystemTags.SystemTagsCollection tests', function() {
var collection;
beforeEach(function() {
collection = new OC.SystemTags.SystemTagsCollection();
});
it('fetches only once, until reset', function() {
var syncStub = sinon.stub(collection, 'sync');
var callback = sinon.stub();
var callback2 = sinon.stub();
var callback3 = sinon.stub();
var eventHandler = sinon.stub();
collection.on('sync', eventHandler);
collection.fetch({
success: callback
});
expect(callback.notCalled).toEqual(true);
expect(syncStub.calledOnce).toEqual(true);
expect(eventHandler.notCalled).toEqual(true);
syncStub.yieldTo('success', collection);
expect(callback.calledOnce).toEqual(true);
expect(callback.firstCall.args[0]).toEqual(collection);
expect(eventHandler.calledOnce).toEqual(true);
expect(eventHandler.firstCall.args[0]).toEqual(collection);
collection.fetch({
success: callback2
});
expect(eventHandler.calledTwice).toEqual(true);
expect(eventHandler.secondCall.args[0]).toEqual(collection);
// not re-called
expect(syncStub.calledOnce).toEqual(true);
expect(callback.calledOnce).toEqual(true);
expect(callback2.calledOnce).toEqual(true);
expect(callback2.firstCall.args[0]).toEqual(collection);
expect(collection.fetched).toEqual(true);
collection.reset();
expect(collection.fetched).toEqual(false);
collection.fetch({
success: callback3
});
expect(syncStub.calledTwice).toEqual(true);
syncStub.yieldTo('success', collection);
expect(callback3.calledOnce).toEqual(true);
expect(callback3.firstCall.args[0]).toEqual(collection);
syncStub.restore();
});
});

614
core/js/tests/specs/systemtags/systemtagsinputfieldSpec.js

@ -1,614 +0,0 @@
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
describe('OC.SystemTags.SystemTagsInputField tests', function() {
var view, select2Stub, clock;
beforeEach(function() {
clock = sinon.useFakeTimers();
var $container = $('<div class="testInputContainer"></div>');
select2Stub = sinon.stub($.fn, 'select2');
select2Stub.returnsThis();
$('#testArea').append($container);
});
afterEach(function() {
select2Stub.restore();
OC.SystemTags.collection.reset();
clock.restore();
view.remove();
view = undefined;
});
describe('general behavior', function() {
var $dropdown;
beforeEach(function() {
view = new OC.SystemTags.SystemTagsInputField();
$('.testInputContainer').append(view.$el);
$dropdown = $('<div class="select2-dropdown"></div>');
select2Stub.withArgs('dropdown').returns($dropdown);
$('#testArea').append($dropdown);
view.render();
});
describe('rendering', function() {
it('calls select2 on rendering', function() {
expect(view.$el.find('input[name=tags]').length).toEqual(1);
expect(select2Stub.called).toEqual(true);
});
it('formatResult renders rename button', function() {
var opts = select2Stub.getCall(0).args[0];
var $el = $(opts.formatResult({id: '1', name: 'test'}));
expect($el.find('.rename').length).toEqual(1);
});
});
describe('tag selection', function() {
beforeEach(function() {
var $el = view.$el.find('input');
$el.val('1');
view.collection.add([
new OC.SystemTags.SystemTagModel({id: '1', name: 'abc'}),
new OC.SystemTags.SystemTagModel({id: '2', name: 'def'}),
new OC.SystemTags.SystemTagModel({id: '3', name: 'abd', userAssignable: false, canAssign: false}),
]);
});
it('does not create dummy tag when user types non-matching name', function() {
var opts = select2Stub.getCall(0).args[0];
var result = opts.createSearchChoice('abc');
expect(result).not.toBeDefined();
});
it('creates dummy tag when user types non-matching name', function() {
var opts = select2Stub.getCall(0).args[0];
var result = opts.createSearchChoice('abnew');
expect(result.id).toEqual(-1);
expect(result.name).toEqual('abnew');
expect(result.isNew).toEqual(true);
expect(result.userVisible).toEqual(true);
expect(result.userAssignable).toEqual(true);
expect(result.canAssign).toEqual(true);
});
it('creates dummy tag when user types non-matching name even with prefix of existing tag', function() {
var opts = select2Stub.getCall(0).args[0];
var result = opts.createSearchChoice('ab');
expect(result.id).toEqual(-1);
expect(result.name).toEqual('ab');
expect(result.isNew).toEqual(true);
expect(result.userVisible).toEqual(true);
expect(result.userAssignable).toEqual(true);
expect(result.canAssign).toEqual(true);
});
it('creates the real tag and fires select event after user selects the dummy tag', function() {
var selectHandler = sinon.stub();
view.on('select', selectHandler);
var createStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'create');
view.$el.find('input').trigger(new $.Event('select2-selecting', {
object: {
id: -1,
name: 'newname',
isNew: true
}
}));
expect(createStub.calledOnce).toEqual(true);
expect(createStub.getCall(0).args[0]).toEqual({
name: 'newname',
userVisible: true,
userAssignable: true,
canAssign: true
});
var newModel = new OC.SystemTags.SystemTagModel({
id: '123',
name: 'newname',
userVisible: true,
userAssignable: true,
canAssign: true
});
// not called yet
expect(selectHandler.notCalled).toEqual(true);
select2Stub.withArgs('data').returns([{
id: '1',
name: 'abc'
}]);
createStub.yieldTo('success', newModel);
expect(select2Stub.lastCall.args[0]).toEqual('data');
expect(select2Stub.lastCall.args[1]).toEqual([{
id: '1',
name: 'abc'
},
newModel.toJSON()
]);
expect(selectHandler.calledOnce).toEqual(true);
expect(selectHandler.getCall(0).args[0]).toEqual(newModel);
createStub.restore();
});
it('triggers select event after selecting an existing tag', function() {
var selectHandler = sinon.stub();
view.on('select', selectHandler);
view.$el.find('input').trigger(new $.Event('select2-selecting', {
object: {
id: '2',
name: 'def'
}
}));
expect(selectHandler.calledOnce).toEqual(true);
expect(selectHandler.getCall(0).args[0]).toEqual(view.collection.get('2'));
});
it('triggers deselect event after deselecting an existing tag', function() {
var selectHandler = sinon.stub();
view.on('deselect', selectHandler);
view.$el.find('input').trigger(new $.Event('select2-removing', {
choice: {
id: '2',
name: 'def'
}
}));
expect(selectHandler.calledOnce).toEqual(true);
expect(selectHandler.getCall(0).args[0]).toEqual('2');
});
it('triggers select event and still adds to list even in case of conflict', function() {
var selectHandler = sinon.stub();
view.on('select', selectHandler);
var fetchStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'fetch');
var createStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'create');
view.$el.find('input').trigger(new $.Event('select2-selecting', {
object: {
id: -1,
name: 'newname',
isNew: true
}
}));
expect(createStub.calledOnce).toEqual(true);
expect(createStub.getCall(0).args[0]).toEqual({
name: 'newname',
userVisible: true,
userAssignable: true,
canAssign: true
});
var newModel = new OC.SystemTags.SystemTagModel({
id: '123',
name: 'newname',
userVisible: true,
userAssignable: true
});
// not called yet
expect(selectHandler.notCalled).toEqual(true);
select2Stub.withArgs('data').returns([{
id: '1',
name: 'abc'
}]);
// simulate conflict response for tag creation
createStub.yieldTo('error', view.collection, {status: 409});
// at this point it fetches from the server
expect(fetchStub.calledOnce).toEqual(true);
// simulate fetch result by adding model to the collection
view.collection.add(newModel);
fetchStub.yieldTo('success', view.collection);
expect(select2Stub.lastCall.args[0]).toEqual('data');
expect(select2Stub.lastCall.args[1]).toEqual([{
id: '1',
name: 'abc'
},
newModel.toJSON()
]);
// select event still called
expect(selectHandler.calledOnce).toEqual(true);
expect(selectHandler.getCall(0).args[0]).toEqual(newModel);
createStub.restore();
fetchStub.restore();
});
});
describe('tag actions', function() {
var opts;
beforeEach(function() {
opts = select2Stub.getCall(0).args[0];
view.collection.add([
new OC.SystemTags.SystemTagModel({id: '1', name: 'abc'}),
]);
$dropdown.append(opts.formatResult(view.collection.get('1').toJSON()));
});
it('displays rename form when clicking rename', function() {
$dropdown.find('.rename').mouseup();
expect($dropdown.find('form.systemtags-rename-form').length).toEqual(1);
expect($dropdown.find('form.systemtags-rename-form input').val()).toEqual('abc');
});
it('renames model and submits change when submitting form', function() {
var saveStub = sinon.stub(OC.SystemTags.SystemTagModel.prototype, 'save');
$dropdown.find('.rename').mouseup();
$dropdown.find('form input').val('abc_renamed');
$dropdown.find('form').trigger(new $.Event('submit'));
expect(saveStub.calledOnce).toEqual(true);
expect(saveStub.getCall(0).args[0]).toEqual({'name': 'abc_renamed'});
expect($dropdown.find('.label').text()).toEqual('abc_renamed');
expect($dropdown.find('form').length).toEqual(0);
saveStub.restore();
});
});
describe('setting data', function() {
it('sets value when calling setValues', function() {
var vals = ['1', '2'];
view.setValues(vals);
expect(select2Stub.lastCall.args[0]).toEqual('val');
expect(select2Stub.lastCall.args[1]).toEqual(vals);
});
it('sets data when calling setData', function() {
var vals = [{id: '1', name: 'test1'}, {id: '2', name: 'test2'}];
view.setData(vals);
expect(select2Stub.lastCall.args[0]).toEqual('data');
expect(select2Stub.lastCall.args[1]).toEqual(vals);
});
});
});
describe('as admin', function() {
var $dropdown;
beforeEach(function() {
view = new OC.SystemTags.SystemTagsInputField({
isAdmin: true
});
$('.testInputContainer').append(view.$el);
$dropdown = $('<div class="select2-dropdown"></div>');
select2Stub.withArgs('dropdown').returns($dropdown);
$('#testArea').append($dropdown);
view.render();
});
it('formatResult renders tag name with visibility', function() {
var opts = select2Stub.getCall(0).args[0];
var $el = $(opts.formatResult({id: '1', name: 'test', userVisible: false, userAssignable: false}));
expect($el.find('.label').text()).toEqual('test (Invisible)');
});
it('formatSelection renders tag name with visibility', function() {
var opts = select2Stub.getCall(0).args[0];
var $el = $(opts.formatSelection({id: '1', name: 'test', userVisible: false, userAssignable: false}));
expect($el.text().trim()).toEqual('test (Invisible)');
});
describe('initSelection', function() {
var fetchStub;
var testTags;
beforeEach(function() {
fetchStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'fetch');
testTags = [
new OC.SystemTags.SystemTagModel({id: '1', name: 'test1'}),
new OC.SystemTags.SystemTagModel({id: '2', name: 'test2'}),
new OC.SystemTags.SystemTagModel({id: '3', name: 'test3', userAssignable: false, canAssign: false}),
new OC.SystemTags.SystemTagModel({id: '4', name: 'test4', userAssignable: false, canAssign: true})
];
});
afterEach(function() {
fetchStub.restore();
});
it('grabs values from the full collection', function() {
var $el = view.$el.find('input');
$el.val('1,3,4');
var opts = select2Stub.getCall(0).args[0];
var callback = sinon.stub();
opts.initSelection($el, callback);
expect(fetchStub.calledOnce).toEqual(true);
view.collection.add(testTags);
fetchStub.yieldTo('success', view.collection);
expect(callback.calledOnce).toEqual(true);
var models = callback.getCall(0).args[0];
expect(models.length).toEqual(3);
expect(models[0].id).toEqual('1');
expect(models[0].name).toEqual('test1');
expect(models[0].locked).toBeFalsy();
expect(models[1].id).toEqual('3');
expect(models[1].name).toEqual('test3');
expect(models[1].locked).toBeFalsy();
expect(models[2].id).toEqual('4');
expect(models[2].name).toEqual('test4');
expect(models[2].locked).toBeFalsy();
});
});
describe('autocomplete', function() {
var fetchStub, opts;
beforeEach(function() {
fetchStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'fetch');
opts = select2Stub.getCall(0).args[0];
view.collection.add([
new OC.SystemTags.SystemTagModel({id: '1', name: 'abc'}),
new OC.SystemTags.SystemTagModel({id: '2', name: 'def'}),
new OC.SystemTags.SystemTagModel({id: '3', name: 'abd', userAssignable: false, canAssign: false}),
new OC.SystemTags.SystemTagModel({id: '4', name: 'Deg'}),
]);
});
afterEach(function() {
fetchStub.restore();
});
it('completes results', function() {
var callback = sinon.stub();
opts.query({
term: 'ab',
callback: callback
});
expect(fetchStub.calledOnce).toEqual(true);
fetchStub.yieldTo('success', view.collection);
expect(callback.calledOnce).toEqual(true);
expect(callback.getCall(0).args[0].results).toEqual([
{
id: '1',
name: 'abc',
userVisible: true,
userAssignable: true,
canAssign: true
},
{
id: '3',
name: 'abd',
userVisible: true,
userAssignable: false,
canAssign: false
}
]);
});
it('completes case insensitive', function() {
var callback = sinon.stub();
opts.query({
term: 'de',
callback: callback
});
expect(fetchStub.calledOnce).toEqual(true);
fetchStub.yieldTo('success', view.collection);
expect(callback.calledOnce).toEqual(true);
expect(callback.getCall(0).args[0].results).toEqual([
{
id: '2',
name: 'def',
userVisible: true,
userAssignable: true,
canAssign: true
},
{
id: '4',
name: 'Deg',
userVisible: true,
userAssignable: true,
canAssign: true
}
]);
});
});
describe('tag actions', function() {
var opts;
beforeEach(function() {
opts = select2Stub.getCall(0).args[0];
view.collection.add([
new OC.SystemTags.SystemTagModel({id: '1', name: 'abc'}),
]);
$dropdown.append(opts.formatResult(view.collection.get('1').toJSON()));
});
it('deletes model and submits change when clicking delete', function() {
var destroyStub = sinon.stub(OC.SystemTags.SystemTagModel.prototype, 'destroy');
expect($dropdown.find('.delete').length).toEqual(0);
$dropdown.find('.rename').mouseup();
// delete button appears
expect($dropdown.find('.delete').length).toEqual(1);
$dropdown.find('.delete').mouseup();
expect(destroyStub.calledOnce).toEqual(true);
expect(destroyStub.calledOn(view.collection.get('1')));
destroyStub.restore();
});
});
});
describe('as user', function() {
var $dropdown;
beforeEach(function() {
view = new OC.SystemTags.SystemTagsInputField({
isAdmin: false
});
$('.testInputContainer').append(view.$el);
$dropdown = $('<div class="select2-dropdown"></div>');
select2Stub.withArgs('dropdown').returns($dropdown);
$('#testArea').append($dropdown);
view.render();
});
it('formatResult renders tag name only', function() {
var opts = select2Stub.getCall(0).args[0];
var $el = $(opts.formatResult({id: '1', name: 'test'}));
expect($el.find('.label').text()).toEqual('test');
});
it('formatSelection renders tag name only', function() {
var opts = select2Stub.getCall(0).args[0];
var $el = $(opts.formatSelection({id: '1', name: 'test'}));
expect($el.text().trim()).toEqual('test');
});
describe('initSelection', function() {
var fetchStub;
var testTags;
beforeEach(function() {
fetchStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'fetch');
testTags = [
new OC.SystemTags.SystemTagModel({id: '1', name: 'test1'}),
new OC.SystemTags.SystemTagModel({id: '2', name: 'test2'}),
new OC.SystemTags.SystemTagModel({id: '3', name: 'test3', userAssignable: false, canAssign: false}),
new OC.SystemTags.SystemTagModel({id: '4', name: 'test4', userAssignable: false, canAssign: true})
];
view.render();
});
afterEach(function() {
fetchStub.restore();
});
it('grabs values from the full collection', function() {
var $el = view.$el.find('input');
$el.val('1,3,4');
var opts = select2Stub.getCall(0).args[0];
var callback = sinon.stub();
opts.initSelection($el, callback);
expect(fetchStub.calledOnce).toEqual(true);
view.collection.add(testTags);
fetchStub.yieldTo('success', view.collection);
expect(callback.calledOnce).toEqual(true);
var models = callback.getCall(0).args[0];
expect(models.length).toEqual(3);
expect(models[0].id).toEqual('1');
expect(models[0].name).toEqual('test1');
expect(models[0].locked).toBeFalsy();
expect(models[1].id).toEqual('3');
expect(models[1].name).toEqual('test3');
// restricted / cannot assign locks the entry
expect(models[1].locked).toEqual(true);
expect(models[2].id).toEqual('4');
expect(models[2].name).toEqual('test4');
expect(models[2].locked).toBeFalsy();
});
});
describe('autocomplete', function() {
var fetchStub, opts;
beforeEach(function() {
fetchStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'fetch');
view.render();
opts = select2Stub.getCall(0).args[0];
view.collection.add([
new OC.SystemTags.SystemTagModel({id: '1', name: 'abc'}),
new OC.SystemTags.SystemTagModel({id: '2', name: 'def'}),
new OC.SystemTags.SystemTagModel({id: '3', name: 'abd', userAssignable: false, canAssign: false}),
new OC.SystemTags.SystemTagModel({id: '4', name: 'Deg'}),
new OC.SystemTags.SystemTagModel({id: '5', name: 'abe', userAssignable: false, canAssign: true})
]);
});
afterEach(function() {
fetchStub.restore();
});
it('completes results excluding non-assignable tags', function() {
var callback = sinon.stub();
opts.query({
term: 'ab',
callback: callback
});
expect(fetchStub.calledOnce).toEqual(true);
fetchStub.yieldTo('success', view.collection);
expect(callback.calledOnce).toEqual(true);
expect(callback.getCall(0).args[0].results).toEqual([
{
id: '1',
name: 'abc',
userVisible: true,
userAssignable: true,
canAssign: true
},
{
id: '5',
name: 'abe',
userVisible: true,
userAssignable: false,
canAssign: true
}
]);
});
it('completes case insensitive', function() {
var callback = sinon.stub();
opts.query({
term: 'de',
callback: callback
});
expect(fetchStub.calledOnce).toEqual(true);
fetchStub.yieldTo('success', view.collection);
expect(callback.calledOnce).toEqual(true);
expect(callback.getCall(0).args[0].results).toEqual([
{
id: '2',
name: 'def',
userVisible: true,
userAssignable: true,
canAssign: true
},
{
id: '4',
name: 'Deg',
userVisible: true,
userAssignable: true,
canAssign: true
}
]);
});
});
describe('tag actions', function() {
var opts;
beforeEach(function() {
opts = select2Stub.getCall(0).args[0];
view.collection.add([
new OC.SystemTags.SystemTagModel({id: '1', name: 'abc'}),
]);
$dropdown.append(opts.formatResult(view.collection.get('1').toJSON()));
});
it('deletes model and submits change when clicking delete', function() {
var destroyStub = sinon.stub(OC.SystemTags.SystemTagModel.prototype, 'destroy');
expect($dropdown.find('.delete').length).toEqual(0);
$dropdown.find('.rename').mouseup();
// delete button appears only for admins
expect($dropdown.find('.delete').length).toEqual(0);
$dropdown.find('.delete').mouseup();
expect(destroyStub.notCalled).toEqual(true);
destroyStub.restore();
});
});
});
});

13
core/src/systemtags/merged-systemtags.js

@ -1,13 +0,0 @@
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import './systemtags.js'
import './systemtagmodel.js'
import './systemtagsmappingcollection.js'
import './systemtagscollection.js'
import './systemtagsinputfield.js'
import '../../css/systemtags.scss'

58
core/src/systemtags/systemtagmodel.js

@ -1,58 +0,0 @@
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
* @param {object} OC The OC namespace
*/
(function(OC) {
if (OC?.Files?.Client) {
_.extend(OC.Files.Client, {
PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id',
PROPERTY_CAN_ASSIGN: '{' + OC.Files.Client.NS_OWNCLOUD + '}can-assign',
PROPERTY_DISPLAYNAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}display-name',
PROPERTY_USERVISIBLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-visible',
PROPERTY_USERASSIGNABLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-assignable',
})
/**
* @class OCA.SystemTags.SystemTagsCollection
* @classdesc
*
* System tag
*
*/
const SystemTagModel = OC.Backbone.Model.extend(
/** @lends OCA.SystemTags.SystemTagModel.prototype */ {
sync: OC.Backbone.davSync,
defaults: {
userVisible: true,
userAssignable: true,
canAssign: true,
},
davProperties: {
id: OC.Files.Client.PROPERTY_FILEID,
name: OC.Files.Client.PROPERTY_DISPLAYNAME,
userVisible: OC.Files.Client.PROPERTY_USERVISIBLE,
userAssignable: OC.Files.Client.PROPERTY_USERASSIGNABLE,
// read-only, effective permissions computed by the server,
canAssign: OC.Files.Client.PROPERTY_CAN_ASSIGN,
},
parse(data) {
return {
id: data.id,
name: data.name,
userVisible: data.userVisible === true || data.userVisible === 'true',
userAssignable: data.userAssignable === true || data.userAssignable === 'true',
canAssign: data.canAssign === true || data.canAssign === 'true',
}
},
})
OC.SystemTags = OC.SystemTags || {}
OC.SystemTags.SystemTagModel = SystemTagModel
}
})(OC)

53
core/src/systemtags/systemtags.js

@ -1,53 +0,0 @@
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
import escapeHTML from 'escape-html'
(function(OC) {
/**
* @namespace
*/
OC.SystemTags = {
/**
*
* @param {OC.SystemTags.SystemTagModel|Object|String} tag
* @returns {HTMLElement}
*/
getDescriptiveTag: function(tag) {
if (_.isUndefined(tag.name) && !_.isUndefined(tag.toJSON)) {
tag = tag.toJSON()
}
var $span = document.createElement('span')
if (_.isUndefined(tag.name)) {
$span.classList.add('non-existing-tag')
$span.textContent = t('core', 'Non-existing tag #{tag}', {
tag: tag
})
return $span
}
$span.textContent = escapeHTML(tag.name)
var scope
if (!tag.userAssignable) {
scope = t('core', 'Restricted')
}
if (!tag.userVisible) {
// invisible also implicitly means not assignable
scope = t('core', 'Invisible')
}
if (scope) {
var $scope = document.createElement('em')
$scope.textContent = ' (' + scope + ')'
$span.appendChild($scope)
}
return $span
}
}
})(OC)

88
core/src/systemtags/systemtagscollection.js

@ -1,88 +0,0 @@
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
(function(OC) {
function filterFunction(model, term) {
return model.get('name').substr(0, term.length).toLowerCase() === term.toLowerCase()
}
/**
* @class OCA.SystemTags.SystemTagsCollection
* @classdesc
*
* Collection of tags assigned to a file
*
*/
var SystemTagsCollection = OC.Backbone.Collection.extend(
/** @lends OC.SystemTags.SystemTagsCollection.prototype */ {
sync: OC.Backbone.davSync,
model: OC.SystemTags.SystemTagModel,
url: function() {
return OC.linkToRemote('dav') + '/systemtags/'
},
filterByName: function(name) {
return this.filter(function(model) {
return filterFunction(model, name)
})
},
reset: function() {
this.fetched = false
return OC.Backbone.Collection.prototype.reset.apply(this, arguments)
},
/**
* Lazy fetch.
* Only fetches once, subsequent calls will directly call the success handler.
*
* @param {any} options -
* @param [options.force] true to force fetch even if cached entries exist
*
* @see Backbone.Collection#fetch
*/
fetch: function(options) {
var self = this
options = options || {}
if (this.fetched || this.working || options.force) {
// directly call handler
if (options.success) {
options.success(this, null, options)
}
// trigger sync event
this.trigger('sync', this, null, options)
return Promise.resolve()
}
this.working = true
var success = options.success
options = _.extend({}, options)
options.success = function() {
self.fetched = true
self.working = false
if (success) {
return success.apply(this, arguments)
}
}
return OC.Backbone.Collection.prototype.fetch.call(this, options)
}
})
OC.SystemTags = OC.SystemTags || {}
OC.SystemTags.SystemTagsCollection = SystemTagsCollection
/**
* @type OC.SystemTags.SystemTagsCollection
*/
OC.SystemTags.collection = new OC.SystemTags.SystemTagsCollection()
})(OC)

444
core/src/systemtags/systemtagsinputfield.js

@ -1,444 +0,0 @@
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
import templateResult from './templates/result.handlebars'
import templateResultForm from './templates/result_form.handlebars'
import templateSelection from './templates/selection.handlebars'
(function(OC) {
/**
* @class OC.SystemTags.SystemTagsInputField
* @classdesc
*
* Displays a file's system tags
*
*/
var SystemTagsInputField = OC.Backbone.View.extend(
/** @lends OC.SystemTags.SystemTagsInputField.prototype */ {
_rendered: false,
_newTag: null,
_lastUsedTags: [],
className: 'systemTagsInputFieldContainer',
template: function(data) {
return '<input class="systemTagsInputField" type="hidden" name="tags" value=""/>'
},
/**
* Creates a new SystemTagsInputField
*
* @param {Object} [options]
* @param {string} [options.objectType=files] object type for which tags are assigned to
* @param {boolean} [options.multiple=false] whether to allow selecting multiple tags
* @param {boolean} [options.allowActions=true] whether tags can be renamed/delete within the dropdown
* @param {boolean} [options.allowCreate=true] whether new tags can be created
* @param {boolean} [options.isAdmin=true] whether the user is an administrator
* @param {Function} options.initSelection function to convert selection to data
*/
initialize: function(options) {
options = options || {}
this._multiple = !!options.multiple
this._allowActions = _.isUndefined(options.allowActions) || !!options.allowActions
this._allowCreate = _.isUndefined(options.allowCreate) || !!options.allowCreate
this._isAdmin = !!options.isAdmin
if (_.isFunction(options.initSelection)) {
this._initSelection = options.initSelection
}
this.collection = options.collection || OC.SystemTags.collection
var self = this
this.collection.on('change:name remove', function() {
// refresh selection
_.defer(self._refreshSelection)
})
_.defer(_.bind(this._getLastUsedTags, this))
_.bindAll(
this,
'_refreshSelection',
'_onClickRenameTag',
'_onClickDeleteTag',
'_onSelectTag',
'_onDeselectTag',
'_onSubmitRenameTag'
)
},
_getLastUsedTags: function() {
var self = this
$.ajax({
type: 'GET',
url: OC.generateUrl('/apps/systemtags/lastused'),
success: function(response) {
self._lastUsedTags = response
}
})
},
/**
* Refreshes the selection, triggering a call to
* select2's initSelection
*/
_refreshSelection: function() {
this.$tagsField.select2('val', this.$tagsField.val())
},
/**
* Event handler whenever the user clicked the "rename" action.
* This will display the rename field.
*/
_onClickRenameTag: function(ev) {
var $item = $(ev.target).closest('.systemtags-item')
var tagId = $item.attr('data-id')
var tagModel = this.collection.get(tagId)
var oldName = tagModel.get('name')
var $renameForm = $(templateResultForm({
cid: this.cid,
name: oldName,
deleteTooltip: t('core', 'Delete'),
renameLabel: t('core', 'Rename'),
isAdmin: this._isAdmin
}))
$item.find('.label').after($renameForm)
$item.find('.label, .systemtags-actions').addClass('hidden')
$item.closest('.select2-result').addClass('has-form')
$renameForm.find('[title]').tooltip({
placement: 'bottom',
container: 'body'
})
$renameForm.find('input').focus().selectRange(0, oldName.length)
return false
},
/**
* Event handler whenever the rename form has been submitted after
* the user entered a new tag name.
* This will submit the change to the server.
*
* @param {Object} ev event
*/
_onSubmitRenameTag: function(ev) {
ev.preventDefault()
var $form = $(ev.target)
var $item = $form.closest('.systemtags-item')
var tagId = $item.attr('data-id')
var tagModel = this.collection.get(tagId)
var newName = $(ev.target).find('input').val().trim()
if (newName && newName !== tagModel.get('name')) {
tagModel.save({ 'name': newName })
// TODO: spinner, and only change text after finished saving
$item.find('.label').text(newName)
}
$item.find('.label, .systemtags-actions').removeClass('hidden')
$form.remove()
$item.closest('.select2-result').removeClass('has-form')
},
/**
* Event handler whenever a tag must be deleted
*
* @param {Object} ev event
*/
_onClickDeleteTag: function(ev) {
var $item = $(ev.target).closest('.systemtags-item')
var tagId = $item.attr('data-id')
this.collection.get(tagId).destroy()
$(ev.target).tooltip('option', 'hide')
$item.closest('.select2-result').remove()
// TODO: spinner
return false
},
_addToSelect2Selection: function(selection) {
var data = this.$tagsField.select2('data')
data.push(selection)
this.$tagsField.select2('data', data)
},
/**
* Event handler whenever a tag is selected.
* Also called whenever tag creation is requested through the dummy tag object.
*
* @param {Object} e event
*/
_onSelectTag: function(e) {
var self = this
var tag
if (e.object && e.object.isNew) {
// newly created tag, check if existing
// create a new tag
tag = this.collection.create({
name: e.object.name.trim(),
userVisible: true,
userAssignable: true,
canAssign: true
}, {
success: function(model) {
self._addToSelect2Selection(model.toJSON())
self._lastUsedTags.unshift(model.id)
self.trigger('select', model)
},
error: function(model, xhr) {
if (xhr.status === 409) {
// re-fetch collection to get the missing tag
self.collection.reset()
self.collection.fetch({
success: function(collection) {
// find the tag in the collection
var model = collection.where({
name: e.object.name.trim(),
userVisible: true,
userAssignable: true
})
if (model.length) {
model = model[0]
// the tag already exists or was already assigned,
// add it to the list anyway
self._addToSelect2Selection(model.toJSON())
self.trigger('select', model)
}
}
})
}
}
})
this.$tagsField.select2('close')
e.preventDefault()
return false
} else {
tag = this.collection.get(e.object.id)
this._lastUsedTags.unshift(tag.id)
}
this._newTag = null
this.trigger('select', tag)
},
/**
* Event handler whenever a tag gets deselected.
*
* @param {Object} e event
*/
_onDeselectTag: function(e) {
this.trigger('deselect', e.choice.id)
},
/**
* Autocomplete function for dropdown results
*
* @param {Object} query select2 query object
*/
_queryTagsAutocomplete: function(query) {
var self = this
this.collection.fetch({
success: function(collection) {
var tagModels = collection.filterByName(query.term.trim())
if (!self._isAdmin) {
tagModels = _.filter(tagModels, function(tagModel) {
return tagModel.get('canAssign')
})
}
query.callback({
results: _.invoke(tagModels, 'toJSON')
})
}
})
},
_preventDefault: function(e) {
e.stopPropagation()
},
/**
* Formats a single dropdown result
*
* @param {Object} data data to format
* @returns {string} HTML markup
*/
_formatDropDownResult: function(data) {
return templateResult(_.extend({
renameTooltip: t('core', 'Rename'),
allowActions: this._allowActions,
tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data).innerHTML : null,
isAdmin: this._isAdmin
}, data))
},
/**
* Formats a single selection item
*
* @param {Object} data data to format
* @returns {string} HTML markup
*/
_formatSelection: function(data) {
return templateSelection(_.extend({
tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data).innerHTML : null,
isAdmin: this._isAdmin
}, data))
},
/**
* Create new dummy choice for select2 when the user
* types an arbitrary string
*
* @param {string} term entered term
* @returns {Object} dummy tag
*/
_createSearchChoice: function(term) {
term = term.trim()
if (this.collection.filter(function(entry) {
return entry.get('name') === term
}).length) {
return
}
if (!this._newTag) {
this._newTag = {
id: -1,
name: term,
userAssignable: true,
userVisible: true,
canAssign: true,
isNew: true
}
} else {
this._newTag.name = term
}
return this._newTag
},
_initSelection: function(element, callback) {
var self = this
var ids = $(element).val().split(',')
function modelToSelection(model) {
var data = model.toJSON()
if (!self._isAdmin && !data.canAssign) {
// lock static tags for non-admins
data.locked = true
}
return data
}
function findSelectedObjects(ids) {
var selectedModels = self.collection.filter(function(model) {
return ids.indexOf(model.id) >= 0 && (self._isAdmin || model.get('userVisible'))
})
return _.map(selectedModels, modelToSelection)
}
this.collection.fetch({
success: function() {
callback(findSelectedObjects(ids))
}
})
},
/**
* Renders this details view
*/
render: function() {
var self = this
this.$el.html(this.template())
this.$el.find('[title]').tooltip({ placement: 'bottom' })
this.$tagsField = this.$el.find('[name=tags]')
this.$tagsField.select2({
placeholder: t('core', 'Collaborative tags'),
containerCssClass: 'systemtags-select2-container',
dropdownCssClass: 'systemtags-select2-dropdown',
closeOnSelect: false,
allowClear: false,
multiple: this._multiple,
toggleSelect: this._multiple,
query: _.bind(this._queryTagsAutocomplete, this),
id: function(tag) {
return tag.id
},
initSelection: _.bind(this._initSelection, this),
formatResult: _.bind(this._formatDropDownResult, this),
formatSelection: _.bind(this._formatSelection, this),
createSearchChoice: this._allowCreate ? _.bind(this._createSearchChoice, this) : undefined,
sortResults: function(results) {
var selectedItems = _.pluck(self.$tagsField.select2('data'), 'id')
results.sort(function(a, b) {
var aSelected = selectedItems.indexOf(a.id) >= 0
var bSelected = selectedItems.indexOf(b.id) >= 0
if (aSelected === bSelected) {
var aLastUsed = self._lastUsedTags.indexOf(a.id)
var bLastUsed = self._lastUsedTags.indexOf(b.id)
if (aLastUsed !== bLastUsed) {
if (bLastUsed === -1) {
return -1
}
if (aLastUsed === -1) {
return 1
}
return aLastUsed < bLastUsed ? -1 : 1
}
// Both not found
return OC.Util.naturalSortCompare(a.name, b.name)
}
if (aSelected && !bSelected) {
return -1
}
return 1
})
return results
},
formatNoMatches: function() {
return t('core', 'No tags found')
}
})
.on('select2-selecting', this._onSelectTag)
.on('select2-removing', this._onDeselectTag)
var $dropDown = this.$tagsField.select2('dropdown')
// register events for inside the dropdown
$dropDown.on('mouseup', '.rename', this._onClickRenameTag)
$dropDown.on('mouseup', '.delete', this._onClickDeleteTag)
$dropDown.on('mouseup', '.select2-result-selectable.has-form', this._preventDefault)
$dropDown.on('submit', '.systemtags-rename-form', this._onSubmitRenameTag)
this.delegateEvents()
},
remove: function() {
if (this.$tagsField) {
this.$tagsField.select2('destroy')
}
},
getValues: function() {
this.$tagsField.select2('val')
},
setValues: function(values) {
this.$tagsField.select2('val', values)
},
setData: function(data) {
this.$tagsField.select2('data', data)
}
})
OC.SystemTags = OC.SystemTags || {}
OC.SystemTags.SystemTagsInputField = SystemTagsInputField
})(OC)

84
core/src/systemtags/systemtagsmappingcollection.js

@ -1,84 +0,0 @@
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { generateRemoteUrl } from '@nextcloud/router'
(function(OC) {
/**
* @class OC.SystemTags.SystemTagsMappingCollection
* @classdesc
*
* Collection of tags assigned to a an object
*
*/
const SystemTagsMappingCollection = OC.Backbone.Collection.extend(
/** @lends OC.SystemTags.SystemTagsMappingCollection.prototype */ {
sync: OC.Backbone.davSync,
/**
* Use PUT instead of PROPPATCH
*/
usePUT: true,
/**
* Id of the file for which to filter activities by
*
* @member int
*/
_objectId: null,
/**
* Type of the object to filter by
*
* @member string
*/
_objectType: 'files',
model: OC.SystemTags.SystemTagModel,
url() {
return generateRemoteUrl('dav') + '/systemtags-relations/' + this._objectType + '/' + this._objectId
},
/**
* Sets the object id to filter by or null for all.
*
* @param {number} objectId file id or null
*/
setObjectId(objectId) {
this._objectId = objectId
},
/**
* Sets the object type to filter by or null for all.
*
* @param {number} objectType file id or null
*/
setObjectType(objectType) {
this._objectType = objectType
},
initialize(models, options) {
options = options || {}
if (!_.isUndefined(options.objectId)) {
this._objectId = options.objectId
}
if (!_.isUndefined(options.objectType)) {
this._objectType = options.objectType
}
},
getTagIds() {
return this.map(function(model) {
return model.id
})
},
})
OC.SystemTags = OC.SystemTags || {}
OC.SystemTags.SystemTagsMappingCollection = SystemTagsMappingCollection
})(OC)

13
core/src/systemtags/templates/result.handlebars

@ -1,13 +0,0 @@
<span class="systemtags-item{{#if isNew}} new-item{{/if}}" data-id="{{id}}">
<span class="checkmark icon icon-checkmark"></span>
{{#if isAdmin}}
<span class="label">{{{tagMarkup}}}</span>
{{else}}
<span class="label">{{name}}</span>
{{/if}}
{{#allowActions}}
<span class="systemtags-actions">
<a href="#" class="rename icon icon-rename" title="{{renameTooltip}}"></a>
</span>
{{/allowActions}}
</span>

7
core/src/systemtags/templates/result_form.handlebars

@ -1,7 +0,0 @@
<form class="systemtags-rename-form">
<label class="hidden-visually" for="{{cid}}-rename-input">{{renameLabel}}</label>
<input id="{{cid}}-rename-input" type="text" value="{{name}}">
{{#if isAdmin}}
<a href="#" class="delete icon icon-delete" title="{{deleteTooltip}}"></a>
{{/if}}
</form>

5
core/src/systemtags/templates/selection.handlebars

@ -1,5 +0,0 @@
{{#if isAdmin}}
<span class="label">{{{tagMarkup}}}</span>
{{else}}
<span class="label">{{name}}</span>
{{/if}}

1
webpack.modules.js

@ -21,7 +21,6 @@ module.exports = {
'public-page-menu': path.resolve(__dirname, 'core/src', 'public-page-menu.ts'),
'public-page-user-menu': path.resolve(__dirname, 'core/src', 'public-page-user-menu.ts'),
recommendedapps: path.join(__dirname, 'core/src', 'recommendedapps.js'),
systemtags: path.resolve(__dirname, 'core/src', 'systemtags/merged-systemtags.js'),
'unified-search': path.join(__dirname, 'core/src', 'unified-search.ts'),
'legacy-unified-search': path.join(__dirname, 'core/src', 'legacy-unified-search.js'),
'unsupported-browser': path.join(__dirname, 'core/src', 'unsupported-browser.js'),

Loading…
Cancel
Save