mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2025-12-27 16:46:16 +00:00
Compare commits
8 Commits
@openstapp
...
142-requir
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c30211ba2 | |||
| 63a38e0077 | |||
| c8b260201c | |||
|
123c50d1af
|
|||
|
|
d65e6351e9 | ||
|
|
2c5d7403db | ||
|
6ca03f463d
|
|||
|
|
1f74a9bc82 |
5
.changeset/good-eggs-add.md
Normal file
5
.changeset/good-eggs-add.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@openstapps/prettier-config": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Update Prettier to 3.1.1
|
||||||
5
.changeset/old-bottles-hide.md
Normal file
5
.changeset/old-bottles-hide.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@openstapps/backend": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Backend unit tests break every year
|
||||||
9
.changeset/spotty-ducks-cheer.md
Normal file
9
.changeset/spotty-ducks-cheer.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
'@openstapps/app': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Require full reload for setting & language changes
|
||||||
|
|
||||||
|
Setting changes are relatively rare, so it makes little sense
|
||||||
|
going through the effort of ensuring everything is reactive to
|
||||||
|
language changes as well as creating all the pipe bindings etc.
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,6 +24,7 @@ report-junit.xml
|
|||||||
# NixOS flake
|
# NixOS flake
|
||||||
result
|
result
|
||||||
hsperfdata_root
|
hsperfdata_root
|
||||||
|
.direnv/
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
lib-cov
|
lib-cov
|
||||||
|
|||||||
3
.tool-versions
Normal file
3
.tool-versions
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
nodejs 18.16.1
|
||||||
|
pnpm 8.8.0
|
||||||
|
python 3.11.5
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Open StApps Monorepo
|
# <img src="logo-bg.svg" height="24"> Open StApps Monorepo
|
||||||
|
|
||||||
Refer to the [contribution guide](./CONTRIBUTING.md)
|
Refer to the [contribution guide](./CONTRIBUTING.md)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the database configuration for the technical university of berlin
|
* This is the database configuration for the technical university of berlin
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCLanguageSetting} */
|
/** @type {import('@openstapps/core').SCLanguageSetting} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/** @type {import('@openstapps/core').SCAppConfigurationMenuCategory[]} */
|
/** @type {import('@openstapps/core').SCAppConfigurationMenuCategory[]} */
|
||||||
const menus = [
|
const menus = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCUserGroupSetting} */
|
/** @type {import('@openstapps/core').SCUserGroupSetting} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCThingType} from '@openstapps/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCBackendAggregationConfiguration[]} */
|
/** @type {import('@openstapps/core').SCBackendAggregationConfiguration[]} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {
|
import {
|
||||||
month,
|
month,
|
||||||
sommerRange,
|
sommerRange,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCThingType} from '@openstapps/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
import aggregations from './aggregations.js';
|
import aggregations from './aggregations.js';
|
||||||
import boostings from './boostings.js';
|
import boostings from './boostings.js';
|
||||||
@@ -17,7 +16,7 @@ export const backend = {
|
|||||||
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
|
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
|
||||||
mappingIgnoredTags: ['minlength', 'pattern', 'see', 'tjs-format'],
|
mappingIgnoredTags: ['minlength', 'pattern', 'see', 'tjs-format'],
|
||||||
maxMultiSearchRouteQueries: 5,
|
maxMultiSearchRouteQueries: 5,
|
||||||
maxRequestBodySize: 2 * 10 ** 6,
|
maxRequestBodySize: 2e6,
|
||||||
name: 'Goethe-Universität Frankfurt am Main',
|
name: 'Goethe-Universität Frankfurt am Main',
|
||||||
namespace: '909a8cbc-8520-456c-b474-ef1525f14209',
|
namespace: '909a8cbc-8520-456c-b474-ef1525f14209',
|
||||||
sortableFields: [
|
sortableFields: [
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import app from './app/index.js';
|
import app from './app/index.js';
|
||||||
import {backend, internal} from './backend/index.js';
|
import {backend, internal} from './backend/index.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the default configuration for elasticsearch (a database)
|
* This is the default configuration for elasticsearch (a database)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {readFile} from 'fs/promises';
|
import {readFile} from 'fs/promises';
|
||||||
import {SCAboutPageContentType} from '@openstapps/core';
|
import {SCAboutPageContentType} from '@openstapps/core';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/**
|
/**
|
||||||
* Generates a range of numbers that represent consecutive calendar months
|
* Generates a range of numbers that represent consecutive calendar months
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {readFile, readdir} from 'fs/promises';
|
import {readFile, readdir} from 'fs/promises';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCAboutPageContentType} from '@openstapps/core';
|
import {SCAboutPageContentType} from '@openstapps/core';
|
||||||
import {markdown} from '../../default/tools/markdown.js';
|
import {markdown} from '../../default/tools/markdown.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCAboutPageContentType} from '@openstapps/core';
|
import {SCAboutPageContentType} from '@openstapps/core';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCAboutPage} */
|
/** @type {import('@openstapps/core').SCAboutPage} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import about from './about.js';
|
import about from './about.js';
|
||||||
import imprint from './imprint.js';
|
import imprint from './imprint.js';
|
||||||
import privacy from './privacy.js';
|
import privacy from './privacy.js';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {markdown} from '../../default/tools/markdown.js';
|
import {markdown} from '../../default/tools/markdown.js';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCAboutPage} */
|
/** @type {import('@openstapps/core').SCAboutPage} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import aboutPages from './about-pages/index.js';
|
import aboutPages from './about-pages/index.js';
|
||||||
import defaultApp from '../default/app/index.js';
|
import defaultApp from '../default/app/index.js';
|
||||||
import {backend as defaultBackend, internal as defaultInternal} from '../default/backend/index.js';
|
import {backend as defaultBackend, internal as defaultInternal} from '../default/backend/index.js';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {versions} from '../../default/tools/version.js';
|
import {versions} from '../../default/tools/version.js';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCAppVersionInfo[]} */
|
/** @type {import('@openstapps/core').SCAppVersionInfo[]} */
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
SCBook,
|
||||||
SCBulkAddRoute,
|
SCBulkAddRoute,
|
||||||
SCBulkDoneRoute,
|
SCBulkDoneRoute,
|
||||||
SCBulkRequest,
|
SCBulkRequest,
|
||||||
@@ -23,29 +24,30 @@ import {
|
|||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import {bulk, DEFAULT_TEST_TIMEOUT} from '../common.js';
|
import {bulk, DEFAULT_TEST_TIMEOUT} from '../common.js';
|
||||||
import {testApp} from '../tests-setup.js';
|
import {testApp} from '../tests-setup.js';
|
||||||
import {readFile} from 'fs/promises';
|
|
||||||
import {v4} from 'uuid';
|
import {v4} from 'uuid';
|
||||||
|
import bookFile from '@openstapps/core/test/resources/indexable/Book.2.json' assert {type: 'json'};
|
||||||
|
|
||||||
const book = JSON.parse(
|
const book = bookFile.instance as SCBook;
|
||||||
await readFile('node_modules/@openstapps/core/test/resources/indexable/Book.2.json', 'utf8'),
|
|
||||||
).instance;
|
|
||||||
|
|
||||||
describe('Bulk routes', async function () {
|
describe('Bulk routes', async function () {
|
||||||
// increase timeout for the suite
|
// increase timeout for the suite
|
||||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||||
|
|
||||||
const request: SCBulkRequest = {
|
let request: SCBulkRequest;
|
||||||
expiration: bulk.expiration,
|
let bulkRoute: SCBulkRoute;
|
||||||
source: bulk.source,
|
let bulkAddRoute: SCBulkAddRoute;
|
||||||
type: bulk.type,
|
let bulkDoneRoute: SCBulkDoneRoute;
|
||||||
};
|
|
||||||
const bulkRoute = new SCBulkRoute();
|
|
||||||
const bulkAddRoute = new SCBulkAddRoute();
|
|
||||||
const bulkDoneRoute = new SCBulkDoneRoute();
|
|
||||||
|
|
||||||
// afterEach(async function() {
|
before(function () {
|
||||||
// TODO: Delete saved bulks
|
request = {
|
||||||
// });
|
expiration: bulk.expiration,
|
||||||
|
source: bulk.source,
|
||||||
|
type: bulk.type,
|
||||||
|
};
|
||||||
|
bulkRoute = new SCBulkRoute();
|
||||||
|
bulkAddRoute = new SCBulkAddRoute();
|
||||||
|
bulkDoneRoute = new SCBulkDoneRoute();
|
||||||
|
});
|
||||||
|
|
||||||
it('should create bulk', async function () {
|
it('should create bulk', async function () {
|
||||||
const {status, body, error} = await testApp
|
const {status, body, error} = await testApp
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ import {expect} from 'chai';
|
|||||||
describe('Index route', async function () {
|
describe('Index route', async function () {
|
||||||
// increase timeout for the suite
|
// increase timeout for the suite
|
||||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||||
const indexRoute = new SCIndexRoute();
|
|
||||||
|
let indexRoute: SCIndexRoute;
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
indexRoute = new SCIndexRoute();
|
||||||
|
});
|
||||||
|
|
||||||
it('should respond with both app and backend configuration', async function () {
|
it('should respond with both app and backend configuration', async function () {
|
||||||
const request: SCIndexRequest = {};
|
const request: SCIndexRequest = {};
|
||||||
|
|||||||
@@ -30,15 +30,11 @@ import chaiAsPromised from 'chai-as-promised';
|
|||||||
import {DEFAULT_TEST_TIMEOUT} from '../common.js';
|
import {DEFAULT_TEST_TIMEOUT} from '../common.js';
|
||||||
import {testApp} from '../tests-setup.js';
|
import {testApp} from '../tests-setup.js';
|
||||||
import {backendConfig} from '../../src/config.js';
|
import {backendConfig} from '../../src/config.js';
|
||||||
import {readFile} from 'fs/promises';
|
import registerRequest from '@openstapps/core/test/resources/PluginRegisterRequest.1.json' assert {type: 'json'};
|
||||||
|
|
||||||
// for using promises in expectations (to.eventually.be...)
|
// for using promises in expectations (to.eventually.be...)
|
||||||
use(chaiAsPromised);
|
use(chaiAsPromised);
|
||||||
|
|
||||||
const registerRequest = JSON.parse(
|
|
||||||
await readFile('node_modules/@openstapps/core/test/resources/PluginRegisterRequest.1.json', 'utf8'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// cast it because of "TS2322: Type 'string' is not assignable to type '"add"'"
|
// cast it because of "TS2322: Type 'string' is not assignable to type '"add"'"
|
||||||
export const registerAddRequest: SCPluginAdd = registerRequest.instance as SCPluginAdd;
|
export const registerAddRequest: SCPluginAdd = registerRequest.instance as SCPluginAdd;
|
||||||
|
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ describe('Create route', async function () {
|
|||||||
const statusCodeSuccess = 222;
|
const statusCodeSuccess = 222;
|
||||||
const bodySuccess = {foo: true};
|
const bodySuccess = {foo: true};
|
||||||
const sandbox = sinon.createSandbox();
|
const sandbox = sinon.createSandbox();
|
||||||
const validationError = new SCValidationErrorResponse([]);
|
let validationError: SCValidationErrorResponse;
|
||||||
const internalServerError = new SCInternalServerErrorResponse();
|
let internalServerError: SCInternalServerErrorResponse;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
app = express();
|
app = express();
|
||||||
@@ -64,6 +64,9 @@ describe('Create route', async function () {
|
|||||||
handler = (_request, _app) => {
|
handler = (_request, _app) => {
|
||||||
return Promise.resolve(bodySuccess);
|
return Promise.resolve(bodySuccess);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
validationError = new SCValidationErrorResponse([]);
|
||||||
|
internalServerError = new SCInternalServerErrorResponse();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
|||||||
@@ -29,11 +29,19 @@ import {backendConfig} from '../../src/config.js';
|
|||||||
describe('Search route', async function () {
|
describe('Search route', async function () {
|
||||||
// increase timeout for the suite
|
// increase timeout for the suite
|
||||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||||
const searchRoute = new SCSearchRoute();
|
let searchRoute: SCSearchRoute;
|
||||||
const multiSearchRoute = new SCMultiSearchRoute();
|
let multiSearchRoute: SCMultiSearchRoute;
|
||||||
const syntaxError = new SCSyntaxErrorResponse('Foo Message');
|
let syntaxError: SCSyntaxErrorResponse;
|
||||||
const methodNotAllowedError = new SCMethodNotAllowedErrorResponse();
|
let methodNotAllowedError: SCMethodNotAllowedErrorResponse;
|
||||||
const tooManyRequestsError = new SCTooManyRequestsErrorResponse();
|
let tooManyRequestsError: SCTooManyRequestsErrorResponse;
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
searchRoute = new SCSearchRoute();
|
||||||
|
multiSearchRoute = new SCMultiSearchRoute();
|
||||||
|
syntaxError = new SCSyntaxErrorResponse('Foo Message');
|
||||||
|
methodNotAllowedError = new SCMethodNotAllowedErrorResponse();
|
||||||
|
tooManyRequestsError = new SCTooManyRequestsErrorResponse();
|
||||||
|
});
|
||||||
|
|
||||||
it('should reject GET, PUT with a valid search query', async function () {
|
it('should reject GET, PUT with a valid search query', async function () {
|
||||||
// const expectedParams = JSON.parse(JSON.stringify(defaultParams));
|
// const expectedParams = JSON.parse(JSON.stringify(defaultParams));
|
||||||
|
|||||||
@@ -13,23 +13,25 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {SCThingUpdateRoute} from '@openstapps/core';
|
import {SCBook, SCThingUpdateRoute} from '@openstapps/core';
|
||||||
import chaiAsPromised from 'chai-as-promised';
|
import chaiAsPromised from 'chai-as-promised';
|
||||||
import {bulkStorageMock, DEFAULT_TEST_TIMEOUT} from '../common.js';
|
import {bulkStorageMock, DEFAULT_TEST_TIMEOUT} from '../common.js';
|
||||||
import {expect, use} from 'chai';
|
import {expect, use} from 'chai';
|
||||||
import {testApp} from '../tests-setup.js';
|
import {testApp} from '../tests-setup.js';
|
||||||
import {readFile} from 'fs/promises';
|
import bookFile from '@openstapps/core/test/resources/indexable/Book.1.json' assert {type: 'json'};
|
||||||
|
|
||||||
use(chaiAsPromised);
|
use(chaiAsPromised);
|
||||||
|
|
||||||
const book = JSON.parse(
|
const book = bookFile.instance as SCBook;
|
||||||
await readFile('node_modules/@openstapps/core/test/resources/indexable/Book.1.json', 'utf8'),
|
|
||||||
).instance;
|
|
||||||
|
|
||||||
describe('Thing update route', async function () {
|
describe('Thing update route', async function () {
|
||||||
// increase timeout for the suite
|
// increase timeout for the suite
|
||||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||||
const thingUpdateRoute = new SCThingUpdateRoute();
|
let thingUpdateRoute: SCThingUpdateRoute;
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
thingUpdateRoute = new SCThingUpdateRoute();
|
||||||
|
});
|
||||||
|
|
||||||
it('should update a thing', async function () {
|
it('should update a thing', async function () {
|
||||||
const thingUpdateRouteurlPath = thingUpdateRoute.urlPath
|
const thingUpdateRouteurlPath = thingUpdateRoute.urlPath
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import {Elasticsearch} from '../../../src/storage/elasticsearch/elasticsearch.js
|
|||||||
import {bulk, DEFAULT_TEST_TIMEOUT, getTransport, getIndex} from '../../common.js';
|
import {bulk, DEFAULT_TEST_TIMEOUT, getTransport, getIndex} from '../../common.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import {backendConfig} from '../../../src/config.js';
|
import {backendConfig} from '../../../src/config.js';
|
||||||
import {readFile} from 'fs/promises';
|
|
||||||
import {
|
import {
|
||||||
ACTIVE_INDICES_ALIAS,
|
ACTIVE_INDICES_ALIAS,
|
||||||
getIndexUID,
|
getIndexUID,
|
||||||
@@ -50,6 +49,11 @@ import {
|
|||||||
} from '../../../src/storage/elasticsearch/util/index.js';
|
} from '../../../src/storage/elasticsearch/util/index.js';
|
||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import {query} from './query.js';
|
import {query} from './query.js';
|
||||||
|
import messageFile from '@openstapps/core/test/resources/indexable/Message.1.json' assert {type: 'json'};
|
||||||
|
import bookFile from '@openstapps/core/test/resources/indexable/Book.1.json' assert {type: 'json'};
|
||||||
|
|
||||||
|
const message = messageFile.instance as SCMessage;
|
||||||
|
const book = bookFile.instance as SCBook;
|
||||||
|
|
||||||
use(chaiAsPromised);
|
use(chaiAsPromised);
|
||||||
|
|
||||||
@@ -60,13 +64,6 @@ function searchResponse<T>(...hits: SearchHit<T>[]): SearchResponse<T> {
|
|||||||
return {hits: {hits}, took: 0, timed_out: false, _shards: {total: 1, failed: 0, successful: 1}};
|
return {hits: {hits}, took: 0, timed_out: false, _shards: {total: 1, failed: 0, successful: 1}};
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = JSON.parse(
|
|
||||||
await readFile('node_modules/@openstapps/core/test/resources/indexable/Message.1.json', 'utf8'),
|
|
||||||
);
|
|
||||||
const book = JSON.parse(
|
|
||||||
await readFile('node_modules/@openstapps/core/test/resources/indexable/Book.1.json', 'utf8'),
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('Elasticsearch', function () {
|
describe('Elasticsearch', function () {
|
||||||
// increase timeout for the suite
|
// increase timeout for the suite
|
||||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||||
@@ -74,8 +71,15 @@ describe('Elasticsearch', function () {
|
|||||||
|
|
||||||
before(function () {
|
before(function () {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('before');
|
|
||||||
sandbox.stub(fs, 'readFileSync').returns('{}');
|
sandbox.stub(fs, 'readFileSync').returns('{}');
|
||||||
|
sandbox.stub(backendConfig.internal.boostings.default[0], 'fields').value({
|
||||||
|
'academicTerms.acronym': {
|
||||||
|
'SS 2023': 1.05,
|
||||||
|
'WS 2023/24': 1.1,
|
||||||
|
'SoSe 2023': 1.05,
|
||||||
|
'WiSe 2023/24': 1.1,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
after(function () {
|
after(function () {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
@@ -445,7 +449,7 @@ describe('Elasticsearch', function () {
|
|||||||
_id: '',
|
_id: '',
|
||||||
_index: '',
|
_index: '',
|
||||||
_score: 0,
|
_score: 0,
|
||||||
_source: message as SCMessage,
|
_source: message,
|
||||||
};
|
};
|
||||||
sandbox.stub(es.client, 'search').resolves(searchResponse(foundObject));
|
sandbox.stub(es.client, 'search').resolves(searchResponse(foundObject));
|
||||||
|
|
||||||
@@ -475,7 +479,7 @@ describe('Elasticsearch', function () {
|
|||||||
const object: SearchHit<SCMessage> = {
|
const object: SearchHit<SCMessage> = {
|
||||||
_id: '',
|
_id: '',
|
||||||
_index: oldIndex,
|
_index: oldIndex,
|
||||||
_source: message as SCMessage,
|
_source: message,
|
||||||
};
|
};
|
||||||
sandbox.stub(es.client, 'search').resolves(searchResponse<SCMessage>(object));
|
sandbox.stub(es.client, 'search').resolves(searchResponse<SCMessage>(object));
|
||||||
sandbox.stub(es, 'prepareBulkWrite').resolves(index);
|
sandbox.stub(es, 'prepareBulkWrite').resolves(index);
|
||||||
@@ -489,7 +493,7 @@ describe('Elasticsearch', function () {
|
|||||||
sandbox.stub(es.client, 'create').resolves({result: 'not_found'} as CreateResponse);
|
sandbox.stub(es.client, 'create').resolves({result: 'not_found'} as CreateResponse);
|
||||||
|
|
||||||
await es.init();
|
await es.init();
|
||||||
return expect(es.post(message as SCMessage, bulk)).to.rejectedWith('creation');
|
return expect(es.post(message, bulk)).to.rejectedWith('creation');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new object', async function () {
|
it('should create a new object', async function () {
|
||||||
@@ -502,7 +506,7 @@ describe('Elasticsearch', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await es.init();
|
await es.init();
|
||||||
await es.post(message as SCMessage, bulk);
|
await es.post(message, bulk);
|
||||||
|
|
||||||
expect(createStub.called).to.be.true;
|
expect(createStub.called).to.be.true;
|
||||||
expect(caughtParameter.document).to.be.eql({
|
expect(caughtParameter.document).to.be.eql({
|
||||||
@@ -527,7 +531,7 @@ describe('Elasticsearch', function () {
|
|||||||
_id: '',
|
_id: '',
|
||||||
_index: getIndex(),
|
_index: getIndex(),
|
||||||
_score: 0,
|
_score: 0,
|
||||||
_source: message as SCMessage,
|
_source: message,
|
||||||
};
|
};
|
||||||
sandbox.stub(es.client, 'search').resolves(searchResponse());
|
sandbox.stub(es.client, 'search').resolves(searchResponse());
|
||||||
|
|
||||||
@@ -541,7 +545,7 @@ describe('Elasticsearch', function () {
|
|||||||
_id: '',
|
_id: '',
|
||||||
_index: getIndex(),
|
_index: getIndex(),
|
||||||
_score: 0,
|
_score: 0,
|
||||||
_source: message as SCMessage,
|
_source: message,
|
||||||
};
|
};
|
||||||
sandbox.stub(es.client, 'search').resolves(searchResponse(object));
|
sandbox.stub(es.client, 'search').resolves(searchResponse(object));
|
||||||
// @ts-expect-error unused
|
// @ts-expect-error unused
|
||||||
@@ -564,13 +568,13 @@ describe('Elasticsearch', function () {
|
|||||||
_id: '123',
|
_id: '123',
|
||||||
_index: getIndex(),
|
_index: getIndex(),
|
||||||
_score: 0,
|
_score: 0,
|
||||||
_source: message as SCMessage,
|
_source: message,
|
||||||
};
|
};
|
||||||
const objectBook: SearchHit<SCBook> = {
|
const objectBook: SearchHit<SCBook> = {
|
||||||
_id: '321',
|
_id: '321',
|
||||||
_index: getIndex(),
|
_index: getIndex(),
|
||||||
_score: 0,
|
_score: 0,
|
||||||
_source: book as SCBook,
|
_source: book,
|
||||||
};
|
};
|
||||||
const fakeEsAggregations = {
|
const fakeEsAggregations = {
|
||||||
'@all': {
|
'@all': {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const child_process = require("child_process");
|
const child_process = require("child_process");
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
const rule = require('./copyright-header-rule')
|
const rule = require('./copyright-header-rule')
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
/** @type {import('eslint').Linter.Config} */
|
/** @type {import('eslint').Linter.Config} */
|
||||||
const config = {
|
const config = {
|
||||||
root: true,
|
root: true,
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
"test": "prettier --config index.js --check \"test/*.js\""
|
"test": "prettier --config index.js --check \"test/*.js\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.1.0"
|
"prettier": "3.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"prettier": "3.1.0"
|
"prettier": "3.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
|||||||
@@ -1,44 +1,62 @@
|
|||||||
version: '3.7'
|
version: '3.7'
|
||||||
|
|
||||||
|
x-development-variables: &development-variables
|
||||||
|
NODE_ENV: "development"
|
||||||
|
ALLOW_NO_TRANSPORT: "true"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
image: registry.gitlab.com/openstapps/openstapps/database:2.0.0
|
image: registry.gitlab.com/openstapps/openstapps/database:3.0.0
|
||||||
volumes:
|
# If you need persistence for debugging purposes uncomment the following lines
|
||||||
- ./database:/usr/share/elasticsearch/data
|
#volumes:
|
||||||
|
# - ./database:/usr/share/elasticsearch/data
|
||||||
expose:
|
expose:
|
||||||
- "9200"
|
- 9200
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:9200:9200
|
||||||
|
environment:
|
||||||
|
- bootstrap.memory_lock=true
|
||||||
|
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||||
|
- discovery.type=single-node
|
||||||
|
ulimits:
|
||||||
|
memlock:
|
||||||
|
soft: -1
|
||||||
|
hard: -1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
image: registry.gitlab.com/openstapps/openstapps/backend:3.0.0-next.0
|
image: registry.gitlab.com/openstapps/openstapps/backend:3.1.0
|
||||||
environment:
|
environment:
|
||||||
|
<<: *development-variables
|
||||||
ES_ADDR: "http://database:9200"
|
ES_ADDR: "http://database:9200"
|
||||||
NODE_CONFIG_ENV: "elasticsearch"
|
NODE_CONFIG_ENV: "elasticsearch"
|
||||||
ALLOW_NO_TRANSPORT: "true"
|
NODE_APP_INSTANCE: "f-u"
|
||||||
|
PROMETHEUS_MIDDLEWARE: "false"
|
||||||
expose:
|
expose:
|
||||||
- 3000
|
- 3000
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 127.0.0.1:3000:3000
|
||||||
links:
|
|
||||||
- "database"
|
|
||||||
labels:
|
labels:
|
||||||
- stapps.version=1.0.0
|
- stapps.version=4.1.0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
|
|
||||||
api:
|
|
||||||
image: registry.gitlab.com/openstapps/openstapps/api:3.0.0-next.0
|
|
||||||
links:
|
links:
|
||||||
- "backend"
|
- database
|
||||||
|
|
||||||
minimal-connector:
|
# api:
|
||||||
image: registry.gitlab.com/openstapps/minimal-connector:core-0.23
|
# image: registry.gitlab.com/openstapps/openstapps/api:3.0.0
|
||||||
container_name: minimal-connector-0.23
|
# links:
|
||||||
command: ["http://backend:3000", "minimal-connector", "f-u"]
|
# - backend
|
||||||
|
|
||||||
app:
|
# minimal-connector:
|
||||||
image: registry.gitlab.com/openstapps/app/executable:core-0.23
|
# image: registry.gitlab.com/openstapps/minimal-connector:core-0.23
|
||||||
expose:
|
# container_name: minimal-connector-0.23
|
||||||
- 8100
|
# command: ["http://backend:3000", "minimal-connector", "f-u"]
|
||||||
ports:
|
|
||||||
- 8100:8100
|
# app:
|
||||||
|
# image: registry.gitlab.com/openstapps/app/executable:core-0.23
|
||||||
|
# expose:
|
||||||
|
# - 8100
|
||||||
|
# ports:
|
||||||
|
# - 8100:8100
|
||||||
|
|||||||
34
flake.lock
generated
34
flake.lock
generated
@@ -1,5 +1,23 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1701680307,
|
||||||
|
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1701626906,
|
"lastModified": 1701626906,
|
||||||
@@ -18,8 +36,24 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
142
flake.nix
142
flake.nix
@@ -1,77 +1,75 @@
|
|||||||
{
|
{
|
||||||
description = "A Nix-flake-based development environment for OpenStApps";
|
description = "A Nix-flake-based development environment for OpenStApps";
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
inputs = {
|
||||||
outputs = { self, nixpkgs }:
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
let
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
buildToolsVersion = "30.0.3";
|
};
|
||||||
overlays = [
|
outputs = {
|
||||||
(final: prev: rec {
|
self,
|
||||||
nodejs = prev.nodejs-18_x;
|
nixpkgs,
|
||||||
pnpm = prev.nodePackages.pnpm;
|
flake-utils,
|
||||||
chrome = prev.google-chrome;
|
}: let
|
||||||
firefox = prev.firefox;
|
aapt2buildToolsVersion = "33.0.2";
|
||||||
webkit = prev.epiphany; # Safari-ish browser
|
in
|
||||||
android = prev.androidenv.composeAndroidPackages {
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
buildToolsVersions = [ "${buildToolsVersion}" ];
|
pkgs = import nixpkgs {
|
||||||
platformVersions = [ "33" ];
|
inherit system;
|
||||||
};
|
overlays = [
|
||||||
cypress = prev.cypress.overrideAttrs(cyPrev: rec {
|
(final: prev: rec {
|
||||||
version = "13.2.0";
|
fontMin = prev.python311.withPackages (ps: with ps; [brotli fonttools] ++ (with fonttools.optional-dependencies; [woff]));
|
||||||
src = prev.fetchzip {
|
android = prev.androidenv.composeAndroidPackages {
|
||||||
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
|
buildToolsVersions = ["30.0.3" aapt2buildToolsVersion];
|
||||||
hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM=";
|
platformVersions = ["33"];
|
||||||
};
|
};
|
||||||
});
|
cypress = prev.cypress.overrideAttrs (cyPrev: rec {
|
||||||
})
|
version = "13.2.0";
|
||||||
];
|
src = prev.fetchzip {
|
||||||
# TODO: aarch64-linux, x68_64-darwin, aarch64-darwin
|
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
|
||||||
supportedSystems = [ "x86_64-linux" ];
|
hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM=";
|
||||||
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
|
};
|
||||||
pkgs = import nixpkgs {
|
});
|
||||||
inherit overlays system;
|
})
|
||||||
config = {
|
];
|
||||||
allowUnfree = true;
|
config = {
|
||||||
android_sdk.accept_license = true;
|
allowUnfree = true;
|
||||||
};
|
android_sdk.accept_license = true;
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
in
|
androidFhs = pkgs.buildFHSUserEnv {
|
||||||
{
|
name = "android-env";
|
||||||
devShells = forEachSupportedSystem ({ pkgs }:
|
targetPkgs = pkgs: with pkgs; [];
|
||||||
let
|
runScript = "bash";
|
||||||
python = (pkgs.python311.withPackages(ps: with ps; [ brotli fonttools ] ++ (with fonttools.optional-dependencies; [ ufo lxml unicode woff ])));
|
profile = ''
|
||||||
in
|
export ALLOW_NINJA_ENV=true
|
||||||
{
|
export USE_CCACHE=1
|
||||||
default = (pkgs.buildFHSUserEnv {
|
export LD_LIBRARY_PATH=/usr/lib:/usr/lib32
|
||||||
name = "StApps Dev";
|
'';
|
||||||
targetPkgs = pkgs: with pkgs; [
|
};
|
||||||
nodejs
|
in {
|
||||||
pnpm
|
devShell = pkgs.mkShell rec {
|
||||||
python
|
nativeBuildInputs = [androidFhs];
|
||||||
docker
|
buildInputs = with pkgs; [
|
||||||
# tools
|
nodejs-18_x
|
||||||
curl
|
nodePackages.pnpm
|
||||||
jq
|
# tools
|
||||||
# browsers
|
curl
|
||||||
firefox
|
jq
|
||||||
chrome
|
fontMin
|
||||||
webkit
|
# browsers
|
||||||
cypress
|
firefox
|
||||||
# android
|
google-chrome
|
||||||
jdk17
|
epiphany # Safari-ish browser
|
||||||
android.androidsdk
|
cypress
|
||||||
musl
|
# android
|
||||||
];
|
jdk17
|
||||||
runScript = "bash";
|
android.androidsdk
|
||||||
profile = ''
|
musl
|
||||||
export CYPRESS_INSTALL_BINARY=0
|
];
|
||||||
export CYPRESS_RUN_BINARY=${pkgs.cypress}/bin/Cypress
|
ANDROID_JAVA_HOME = "${pkgs.jdk.home}";
|
||||||
export ANDROID_SDK_ROOT=${pkgs.android.androidsdk}/libexec/android-sdk
|
ANDROID_SDK_ROOT = "${pkgs.android.androidsdk}/libexec/android-sdk";
|
||||||
export ANDROID_JAVA_HOME=${pkgs.jdk.home}
|
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/${aapt2buildToolsVersion}/aapt2";
|
||||||
export DOCKER_HOST=unix:///run/user/1000/docker.sock
|
CYPRESS_INSTALL_BINARY = "0";
|
||||||
{ dockerd-rootless & } 2>/dev/null
|
CYPRESS_RUN_BINARY = "${pkgs.cypress}/bin/Cypress";
|
||||||
'';
|
};
|
||||||
}).env;
|
});
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def capacitor_pods
|
|||||||
pod 'CapacitorPreferences', :path => '../../../../node_modules/.pnpm/@capacitor+preferences@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/preferences'
|
pod 'CapacitorPreferences', :path => '../../../../node_modules/.pnpm/@capacitor+preferences@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/preferences'
|
||||||
pod 'CapacitorShare', :path => '../../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share'
|
pod 'CapacitorShare', :path => '../../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share'
|
||||||
pod 'CapacitorSplashScreen', :path => '../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen'
|
pod 'CapacitorSplashScreen', :path => '../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen'
|
||||||
pod 'TransistorsoftCapacitorBackgroundFetch', :path => '../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@1.0.2_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch'
|
pod 'TransistorsoftCapacitorBackgroundFetch', :path => '../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.1.1_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch'
|
||||||
pod 'CapacitorSecureStoragePlugin', :path => '../../../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.0/node_modules/capacitor-secure-storage-plugin'
|
pod 'CapacitorSecureStoragePlugin', :path => '../../../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.0/node_modules/capacitor-secure-storage-plugin'
|
||||||
pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins'
|
pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ PODS:
|
|||||||
- CordovaPlugins (5.5.0):
|
- CordovaPlugins (5.5.0):
|
||||||
- CapacitorCordova
|
- CapacitorCordova
|
||||||
- SwiftKeychainWrapper (4.0.1)
|
- SwiftKeychainWrapper (4.0.1)
|
||||||
- TransistorsoftCapacitorBackgroundFetch (1.0.2):
|
- TransistorsoftCapacitorBackgroundFetch (5.1.1):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
@@ -58,7 +58,7 @@ DEPENDENCIES:
|
|||||||
- "CapacitorShare (from `../../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share`)"
|
- "CapacitorShare (from `../../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share`)"
|
||||||
- "CapacitorSplashScreen (from `../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen`)"
|
- "CapacitorSplashScreen (from `../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen`)"
|
||||||
- CordovaPlugins (from `../capacitor-cordova-ios-plugins`)
|
- CordovaPlugins (from `../capacitor-cordova-ios-plugins`)
|
||||||
- "TransistorsoftCapacitorBackgroundFetch (from `../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@1.0.2_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch`)"
|
- "TransistorsoftCapacitorBackgroundFetch (from `../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.1.1_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch`)"
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@@ -102,7 +102,7 @@ EXTERNAL SOURCES:
|
|||||||
CordovaPlugins:
|
CordovaPlugins:
|
||||||
:path: "../capacitor-cordova-ios-plugins"
|
:path: "../capacitor-cordova-ios-plugins"
|
||||||
TransistorsoftCapacitorBackgroundFetch:
|
TransistorsoftCapacitorBackgroundFetch:
|
||||||
:path: "../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@1.0.2_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch"
|
:path: "../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.1.1_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Capacitor: 57890b363df14d5d2d5d8461aa23e886cb34da2a
|
Capacitor: 57890b363df14d5d2d5d8461aa23e886cb34da2a
|
||||||
@@ -124,8 +124,8 @@ SPEC CHECKSUMS:
|
|||||||
CapacitorSplashScreen: 5fa2ab5e46cf5cc530cf16a51c80c7a986579ccd
|
CapacitorSplashScreen: 5fa2ab5e46cf5cc530cf16a51c80c7a986579ccd
|
||||||
CordovaPlugins: de5669381702d76ed5b1d442177a6a5fc3252a9d
|
CordovaPlugins: de5669381702d76ed5b1d442177a6a5fc3252a9d
|
||||||
SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c
|
SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c
|
||||||
TransistorsoftCapacitorBackgroundFetch: 74ca62dae7ec78639eaf3d0d1e24c595ada213dd
|
TransistorsoftCapacitorBackgroundFetch: ce4b3e01b898cef516e68485d2160a078016ee97
|
||||||
|
|
||||||
PODFILE CHECKSUM: 073b899f90bacc5049101cb9c562a168757d554e
|
PODFILE CHECKSUM: 229278f2c257e8ab555325c7115b2e187e8e628d
|
||||||
|
|
||||||
COCOAPODS: 1.13.0
|
COCOAPODS: 1.13.0
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 StApps
|
* Copyright (C) 2022 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
"cordova-plugin-calendar": "5.1.6",
|
"cordova-plugin-calendar": "5.1.6",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
|
"fast-deep-equal": "3.1.3",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"geojson": "0.5.0",
|
"geojson": "0.5.0",
|
||||||
"ionic-appauth": "0.9.0",
|
"ionic-appauth": "0.9.0",
|
||||||
|
|||||||
80
frontend/app/src/app/animation/splash.ts
Normal file
80
frontend/app/src/app/animation/splash.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {Animation, AnimationController} from '@ionic/angular';
|
||||||
|
import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splash screen animation
|
||||||
|
*/
|
||||||
|
export function splashAnimation(animationCtl: AnimationController): Animation {
|
||||||
|
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
return animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('opacity', 0, 1)
|
||||||
|
.duration(150)
|
||||||
|
.beforeClearStyles(['visibility'])
|
||||||
|
.addElement(document.querySelector('ion-app')!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMd = document.querySelector('ion-app.md') !== null;
|
||||||
|
const navElement = document.querySelector('stapps-navigation-tabs')!;
|
||||||
|
const navBounds = navElement.getBoundingClientRect();
|
||||||
|
let horizontal = navBounds.width < navBounds.height;
|
||||||
|
if (window.getComputedStyle(navElement).display === 'none') {
|
||||||
|
horizontal = true;
|
||||||
|
}
|
||||||
|
const translate = (amount: number, unit = 'px') =>
|
||||||
|
`translate${horizontal ? 'X' : 'Y'}(${horizontal ? amount * -1 : amount}${unit})`;
|
||||||
|
const duration = 2 * (isMd ? mdDuration : iosDuration);
|
||||||
|
|
||||||
|
const animation = animationCtl
|
||||||
|
.create()
|
||||||
|
.duration(duration)
|
||||||
|
.easing(isMd ? mdEasing : iosEasing)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl.create().beforeClearStyles(['visibility']).addElement(document.querySelector('ion-app')!),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(horizontal ? 64 : 192), translate(0))
|
||||||
|
.fromTo('opacity', 0, 1)
|
||||||
|
.addElement(document.querySelector('stapps-navigation > ion-split-pane')!),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(64), translate(0))
|
||||||
|
.addElement(document.querySelectorAll('ion-split-pane > ion-menu > ion-content')),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(horizontal ? 32 : -72), translate(0))
|
||||||
|
.addElement(document.querySelectorAll('ion-router-outlet > .ion-page > ion-content')!),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(100, '%'), translate(0, '%'))
|
||||||
|
.addElement(document.querySelector('stapps-navigation-tabs')!),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!horizontal) {
|
||||||
|
animation.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('background', 'none', 'none')
|
||||||
|
.addElement(document.querySelector('ion-router-outlet')!),
|
||||||
|
);
|
||||||
|
|
||||||
|
const parallax = document
|
||||||
|
.querySelector('ion-router-outlet > .ion-page > ion-content')
|
||||||
|
?.shadowRoot?.querySelector('[part=parallax]');
|
||||||
|
if (parallax) {
|
||||||
|
animation.addAnimation(
|
||||||
|
animationCtl.create().fromTo('translate', '0 256px', '0 0px').addElement(parallax),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
@@ -22,28 +22,16 @@ import {environment} from '../environments/environment';
|
|||||||
import {Capacitor} from '@capacitor/core';
|
import {Capacitor} from '@capacitor/core';
|
||||||
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
|
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
|
||||||
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
|
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
|
||||||
import {AppVersionService} from './modules/about/app-version.service';
|
|
||||||
import {SplashScreen} from '@capacitor/splash-screen';
|
import {SplashScreen} from '@capacitor/splash-screen';
|
||||||
|
import {AppVersionService} from './modules/about/app-version.service';
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: 'app.component.html',
|
templateUrl: 'app.component.html',
|
||||||
})
|
})
|
||||||
export class AppComponent implements AfterContentInit {
|
export class AppComponent implements AfterContentInit {
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
pages: Array<{
|
pages: Array<{
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
component: unknown;
|
component: unknown;
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
title: string;
|
title: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -65,7 +53,7 @@ export class AppComponent implements AfterContentInit {
|
|||||||
void this.initializeApp();
|
void this.initializeApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngAfterContentInit() {
|
ngAfterContentInit() {
|
||||||
this.scheduleSyncService.init();
|
this.scheduleSyncService.init();
|
||||||
void this.scheduleSyncService.enable();
|
void this.scheduleSyncService.enable();
|
||||||
this.versionService.getPendingReleaseNotes().then(notes => {
|
this.versionService.getPendingReleaseNotes().then(notes => {
|
||||||
@@ -74,24 +62,11 @@ export class AppComponent implements AfterContentInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (document.readyState === 'complete') {
|
|
||||||
this.hideSplash();
|
|
||||||
} else {
|
|
||||||
document.addEventListener('readystatechange', () => {
|
|
||||||
if (document.readyState === 'complete') this.hideSplash();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async hideSplash() {
|
|
||||||
if (Capacitor.isNativePlatform()) {
|
if (Capacitor.isNativePlatform()) {
|
||||||
void SplashScreen.hide();
|
void SplashScreen.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
async initializeApp() {
|
async initializeApp() {
|
||||||
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
|
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
|
|||||||
@@ -25,12 +25,10 @@ import moment from 'moment';
|
|||||||
import 'moment/min/locales';
|
import 'moment/min/locales';
|
||||||
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
|
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
|
||||||
import SwiperCore, {FreeMode, Navigation} from 'swiper';
|
import SwiperCore, {FreeMode, Navigation} from 'swiper';
|
||||||
|
|
||||||
import {environment} from '../environments/environment';
|
import {environment} from '../environments/environment';
|
||||||
import {AppRoutingModule} from './app-routing.module';
|
import {AppRoutingModule} from './app-routing.module';
|
||||||
import {AppComponent} from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
import {CatalogModule} from './modules/catalog/catalog.module';
|
import {CatalogModule} from './modules/catalog/catalog.module';
|
||||||
import {ConfigModule} from './modules/config/config.module';
|
|
||||||
import {ConfigProvider} from './modules/config/config.provider';
|
import {ConfigProvider} from './modules/config/config.provider';
|
||||||
import {DashboardModule} from './modules/dashboard/dashboard.module';
|
import {DashboardModule} from './modules/dashboard/dashboard.module';
|
||||||
import {DataModule} from './modules/data/data.module';
|
import {DataModule} from './modules/data/data.module';
|
||||||
@@ -44,7 +42,6 @@ import {SettingsProvider} from './modules/settings/settings.provider';
|
|||||||
import {StorageModule} from './modules/storage/storage.module';
|
import {StorageModule} from './modules/storage/storage.module';
|
||||||
import {ThingTranslateModule} from './translation/thing-translate.module';
|
import {ThingTranslateModule} from './translation/thing-translate.module';
|
||||||
import {UtilModule} from './util/util.module';
|
import {UtilModule} from './util/util.module';
|
||||||
import {initLogger} from './_helpers/ts-logger';
|
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import {AboutModule} from './modules/about/about.module';
|
import {AboutModule} from './modules/about/about.module';
|
||||||
import {JobModule} from './modules/jobs/jobs.module';
|
import {JobModule} from './modules/jobs/jobs.module';
|
||||||
@@ -91,28 +88,25 @@ export function initializerFactory(
|
|||||||
) {
|
) {
|
||||||
return async () => {
|
return async () => {
|
||||||
try {
|
try {
|
||||||
initLogger(logger);
|
|
||||||
await storageProvider.init();
|
await storageProvider.init();
|
||||||
await configProvider.init();
|
await configProvider.init();
|
||||||
await settingsProvider.init();
|
if (configProvider.firstSession) {
|
||||||
|
// set language from browser
|
||||||
|
await settingsProvider.setSettingValue(
|
||||||
|
'profile',
|
||||||
|
'language',
|
||||||
|
translateService.getBrowserLang() as SCSettingValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const languageCode = await settingsProvider.getSetting<string>('profile', 'language');
|
||||||
|
// this language will be used as a fallback when a translation isn't found in the current language
|
||||||
|
translateService.setDefaultLang('en');
|
||||||
|
translateService.use(languageCode);
|
||||||
|
moment.locale(languageCode);
|
||||||
|
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
|
||||||
|
setDefaultOptions({locale: dateFnsLocale});
|
||||||
|
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
||||||
try {
|
try {
|
||||||
if (configProvider.firstSession) {
|
|
||||||
// set language from browser
|
|
||||||
await settingsProvider.setSettingValue(
|
|
||||||
'profile',
|
|
||||||
'language',
|
|
||||||
translateService.getBrowserLang() as SCSettingValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const languageCode = (await settingsProvider.getValue('profile', 'language')) as string;
|
|
||||||
// this language will be used as a fallback when a translation isn't found in the current language
|
|
||||||
translateService.setDefaultLang('en');
|
|
||||||
translateService.use(languageCode);
|
|
||||||
moment.locale(languageCode);
|
|
||||||
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
|
|
||||||
setDefaultOptions({locale: dateFnsLocale});
|
|
||||||
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
|
||||||
|
|
||||||
await defaultAuthService.init();
|
await defaultAuthService.init();
|
||||||
await paiaAuthService.init();
|
await paiaAuthService.init();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -151,11 +145,12 @@ export function createTranslateLoader(http: HttpClient) {
|
|||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
CatalogModule,
|
CatalogModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ConfigModule,
|
|
||||||
DashboardModule,
|
DashboardModule,
|
||||||
DataModule,
|
DataModule,
|
||||||
HebisModule,
|
HebisModule,
|
||||||
IonicModule.forRoot(),
|
IonicModule.forRoot({
|
||||||
|
animated: 'Cypress' in window ? false : undefined,
|
||||||
|
}),
|
||||||
IonIconModule,
|
IonIconModule,
|
||||||
JobModule,
|
JobModule,
|
||||||
FavoritesModule,
|
FavoritesModule,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
|
import {SCAboutPage} from '@openstapps/core';
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
import packageJson from '../../../../../package.json';
|
import packageJson from '../../../../../package.json';
|
||||||
import config from 'capacitor.config';
|
import config from 'capacitor.config';
|
||||||
@@ -42,8 +42,7 @@ export class AboutPageComponent implements OnInit {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const route = this.route.snapshot.url.map(it => it.path).join('/');
|
const route = this.route.snapshot.url.map(it => it.path).join('/');
|
||||||
this.content =
|
this.content = this.configProvider.config.app.aboutPages[route] ?? {};
|
||||||
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
|
|
||||||
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
|
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {FormsModule} from '@angular/forms';
|
|||||||
import {IonicModule} from '@ionic/angular';
|
import {IonicModule} from '@ionic/angular';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
|
||||||
import {AboutPageComponent} from './about-page/about-page.component';
|
import {AboutPageComponent} from './about-page/about-page.component';
|
||||||
import {MarkdownModule} from 'ngx-markdown';
|
import {MarkdownModule} from 'ngx-markdown';
|
||||||
import {AboutPageContentComponent} from './about-page/about-page-content.component';
|
import {AboutPageContentComponent} from './about-page/about-page-content.component';
|
||||||
@@ -64,6 +63,5 @@ const settingsRoutes: Routes = [
|
|||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [ConfigProvider],
|
|
||||||
})
|
})
|
||||||
export class AboutModule {}
|
export class AboutModule {}
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ import {IPAIAAuthAction} from './paia/paia-auth-action';
|
|||||||
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {JSONPath} from 'jsonpath-plus';
|
import {JSONPath} from 'jsonpath-plus';
|
||||||
import {
|
import {SCAuthorizationProviderType, SCUserConfiguration} from '@openstapps/core';
|
||||||
SCAuthorizationProvider,
|
|
||||||
SCAuthorizationProviderType,
|
|
||||||
SCUserConfiguration,
|
|
||||||
SCUserConfigurationMap,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {DefaultAuthService} from './default-auth.service';
|
import {DefaultAuthService} from './default-auth.service';
|
||||||
@@ -37,8 +32,6 @@ const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthHelperService {
|
export class AuthHelperService {
|
||||||
userConfigurationMap: SCUserConfigurationMap;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private configProvider: ConfigProvider,
|
private configProvider: ConfigProvider,
|
||||||
@@ -47,14 +40,7 @@ export class AuthHelperService {
|
|||||||
private paiaAuth: PAIAAuthService,
|
private paiaAuth: PAIAAuthService,
|
||||||
private browser: SimpleBrowser,
|
private browser: SimpleBrowser,
|
||||||
private alertController: AlertController,
|
private alertController: AlertController,
|
||||||
) {
|
) {}
|
||||||
this.userConfigurationMap =
|
|
||||||
(
|
|
||||||
this.configProvider.getAnyValue('auth') as {
|
|
||||||
default: SCAuthorizationProvider;
|
|
||||||
}
|
|
||||||
).default?.endpoints.mapping ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
|
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
|
||||||
let message: string | undefined;
|
let message: string | undefined;
|
||||||
@@ -77,9 +63,10 @@ export class AuthHelperService {
|
|||||||
name: '',
|
name: '',
|
||||||
role: 'student',
|
role: 'student',
|
||||||
};
|
};
|
||||||
for (const key in this.userConfigurationMap) {
|
const mapping = this.configProvider.config.auth.default!.endpoints.mapping;
|
||||||
|
for (const key in mapping) {
|
||||||
user[key as keyof SCUserConfiguration] = JSONPath({
|
user[key as keyof SCUserConfiguration] = JSONPath({
|
||||||
path: this.userConfigurationMap[key as keyof SCUserConfiguration] as string,
|
path: mapping[key as keyof SCUserConfiguration] as string,
|
||||||
json: userInfo,
|
json: userInfo,
|
||||||
preventEval: true,
|
preventEval: true,
|
||||||
})[0];
|
})[0];
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthorizationRequestHandler,
|
AuthorizationRequestHandler,
|
||||||
AuthorizationServiceConfiguration,
|
AuthorizationServiceConfiguration,
|
||||||
@@ -24,7 +23,6 @@ import {
|
|||||||
} from '@openid/appauth';
|
} from '@openid/appauth';
|
||||||
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
|
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
|
||||||
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
|
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {AuthService} from './auth.service';
|
import {AuthService} from './auth.service';
|
||||||
@@ -67,12 +65,9 @@ export class DefaultAuthService extends AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupConfiguration() {
|
setupConfiguration() {
|
||||||
const authConfig = this.configProvider.getAnyValue('auth') as {
|
this.authConfig = getClientConfig('default', this.configProvider.config.auth);
|
||||||
default: SCAuthorizationProvider;
|
|
||||||
};
|
|
||||||
this.authConfig = getClientConfig('default', authConfig);
|
|
||||||
this.localConfiguration = new AuthorizationServiceConfiguration(
|
this.localConfiguration = new AuthorizationServiceConfiguration(
|
||||||
getEndpointsConfig('default', authConfig),
|
getEndpointsConfig('default', this.configProvider.config.auth),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthorizationError,
|
AuthorizationError,
|
||||||
AuthorizationRequest,
|
AuthorizationRequest,
|
||||||
@@ -47,7 +46,6 @@ import {PAIAAuthorizationResponse} from './paia-authorization-response';
|
|||||||
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
|
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
|
||||||
import {PAIATokenResponse} from './paia-token-response';
|
import {PAIATokenResponse} from './paia-token-response';
|
||||||
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
|
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
|
||||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
|
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
@@ -154,11 +152,10 @@ export class PAIAAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupConfiguration() {
|
setupConfiguration() {
|
||||||
const authConfig = this.configProvider.getAnyValue('auth') as {
|
this.authConfig = getClientConfig('paia', this.configProvider.config.auth);
|
||||||
paia: SCAuthorizationProvider;
|
this.localConfiguration = new AuthorizationServiceConfiguration(
|
||||||
};
|
getEndpointsConfig('paia', this.configProvider.config.auth),
|
||||||
this.authConfig = getClientConfig('paia', authConfig);
|
);
|
||||||
this.localConfiguration = new AuthorizationServiceConfiguration(getEndpointsConfig('paia', authConfig));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected notifyActionListers(action: IPAIAAuthAction) {
|
protected notifyActionListers(action: IPAIAAuthAction) {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
|
import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {ICalEvent} from './ical/ical';
|
import {ICalEvent} from './ical/ical';
|
||||||
@@ -35,14 +34,14 @@ export class CalendarService {
|
|||||||
|
|
||||||
goToDateClicked = this.goToDate.asObservable();
|
goToDateClicked = this.goToDate.asObservable();
|
||||||
|
|
||||||
calendarName = 'StApps';
|
calendarName: string;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
constructor(
|
constructor(
|
||||||
readonly calendar: Calendar,
|
readonly calendar: Calendar,
|
||||||
private readonly configProvider: ConfigProvider,
|
private readonly configProvider: ConfigProvider,
|
||||||
) {
|
) {
|
||||||
this.calendarName = (this.configProvider.getValue('name') as string) ?? 'StApps';
|
this.calendarName = this.configProvider.config.app.name ?? 'StApps';
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCalendar(): Promise<CalendarInfo | undefined> {
|
async createCalendar(): Promise<CalendarInfo | undefined> {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {IonicModule} from '@ionic/angular';
|
|||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {MomentModule} from 'ngx-moment';
|
import {MomentModule} from 'ngx-moment';
|
||||||
import {DataModule} from '../data/data.module';
|
import {DataModule} from '../data/data.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {CatalogComponent} from './catalog.component';
|
import {CatalogComponent} from './catalog.component';
|
||||||
import {UtilModule} from '../../util/util.module';
|
import {UtilModule} from '../../util/util.module';
|
||||||
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
||||||
@@ -46,6 +45,5 @@ const catalogRoutes: Routes = [
|
|||||||
DataModule,
|
DataModule,
|
||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [SettingsProvider],
|
|
||||||
})
|
})
|
||||||
export class CatalogModule {}
|
export class CatalogModule {}
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ import {TestBed} from '@angular/core/testing';
|
|||||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
|
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
|
||||||
import {
|
|
||||||
ConfigFetchError,
|
|
||||||
ConfigInitError,
|
|
||||||
SavedConfigNotAvailable,
|
|
||||||
WrongConfigVersionInStorage,
|
|
||||||
} from './errors';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';
|
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';
|
||||||
|
|
||||||
|
|||||||
@@ -14,19 +14,14 @@
|
|||||||
*/
|
*/
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {Client} from '@openstapps/api';
|
import {Client} from '@openstapps/api';
|
||||||
import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core';
|
import {SCIndexResponse} from '@openstapps/core';
|
||||||
import packageInfo from '@openstapps/core/package.json';
|
import packageInfo from '@openstapps/core/package.json';
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {environment} from '../../../environments/environment';
|
import {environment} from '../../../environments/environment';
|
||||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {
|
import equals from 'fast-deep-equal/es6';
|
||||||
ConfigFetchError,
|
import {BehaviorSubject} from 'rxjs';
|
||||||
ConfigInitError,
|
|
||||||
ConfigValueNotAvailable,
|
|
||||||
SavedConfigNotAvailable,
|
|
||||||
WrongConfigVersionInStorage,
|
|
||||||
} from './errors';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key to store config in storage module
|
* Key to store config in storage module
|
||||||
@@ -35,6 +30,17 @@ import {
|
|||||||
*/
|
*/
|
||||||
export const STORAGE_KEY_CONFIG = 'stapps.config';
|
export const STORAGE_KEY_CONFIG = 'stapps.config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an object deeply immutable
|
||||||
|
*/
|
||||||
|
function deepFreeze<T extends object>(object: T) {
|
||||||
|
for (const key of Object.keys(object)) {
|
||||||
|
const value = (object as Record<string, unknown>)[key];
|
||||||
|
if (typeof value === 'object' && !Object.isFrozen(value)) deepFreeze(value!);
|
||||||
|
}
|
||||||
|
return Object.freeze(object);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides configuration
|
* Provides configuration
|
||||||
*/
|
*/
|
||||||
@@ -50,7 +56,7 @@ export class ConfigProvider {
|
|||||||
/**
|
/**
|
||||||
* App configuration as IndexResponse
|
* App configuration as IndexResponse
|
||||||
*/
|
*/
|
||||||
config: SCIndexResponse;
|
config: Readonly<SCIndexResponse>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version of the @openstapps/core package that app is using
|
* Version of the @openstapps/core package that app is using
|
||||||
@@ -62,6 +68,11 @@ export class ConfigProvider {
|
|||||||
*/
|
*/
|
||||||
firstSession = true;
|
firstSession = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the config requires an update
|
||||||
|
*/
|
||||||
|
needsUpdate$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor, initialise api client
|
* Constructor, initialise api client
|
||||||
* @param storageProvider StorageProvider to load persistent configuration
|
* @param storageProvider StorageProvider to load persistent configuration
|
||||||
@@ -76,104 +87,35 @@ export class ConfigProvider {
|
|||||||
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
|
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches configuration from backend
|
|
||||||
*/
|
|
||||||
async fetch(): Promise<SCIndexResponse> {
|
|
||||||
try {
|
|
||||||
return await this.client.handshake(this.scVersion);
|
|
||||||
} catch {
|
|
||||||
throw new ConfigFetchError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value of an app configuration
|
|
||||||
* @param attribute requested attribute from app configuration
|
|
||||||
*/
|
|
||||||
public getValue(attribute: keyof SCAppConfiguration) {
|
|
||||||
if (this.config.app[attribute] !== undefined) {
|
|
||||||
return this.config.app[attribute];
|
|
||||||
}
|
|
||||||
throw new ConfigValueNotAvailable(attribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a value of the configuration (not only app configuration)
|
|
||||||
* @param attribute requested attribute from the configuration
|
|
||||||
*/
|
|
||||||
public getAnyValue(attribute: keyof SCIndexResponse) {
|
|
||||||
if (this.config[attribute] !== undefined) {
|
|
||||||
return this.config[attribute];
|
|
||||||
}
|
|
||||||
throw new ConfigValueNotAvailable(attribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialises the ConfigProvider
|
* Initialises the ConfigProvider
|
||||||
* @throws ConfigInitError if no configuration could be loaded.
|
|
||||||
* @throws WrongConfigVersionInStorage if fetch failed and saved config has wrong SCVersion
|
|
||||||
*/
|
*/
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
let loadError;
|
this.config = (await this.storageProvider.has(STORAGE_KEY_CONFIG))
|
||||||
let fetchError;
|
? await this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG)
|
||||||
// load saved configuration
|
: undefined!;
|
||||||
try {
|
this.firstSession = !this.config;
|
||||||
this.config = await this.loadLocal();
|
|
||||||
this.firstSession = false;
|
const updatedConfig = this.client.handshake(this.scVersion).then(async fetchedConfig => {
|
||||||
this.logger.log(`initialised configuration from storage`);
|
if (!equals(fetchedConfig, this.config)) {
|
||||||
if (this.config.backend.SCVersion !== this.scVersion) {
|
await this.storageProvider.put(STORAGE_KEY_CONFIG, fetchedConfig);
|
||||||
loadError = new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion);
|
this.logger.log(`Config updated`);
|
||||||
|
this.needsUpdate$.next(true);
|
||||||
|
this.needsUpdate$.complete();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
return fetchedConfig;
|
||||||
loadError = error;
|
});
|
||||||
}
|
|
||||||
// fetch remote configuration from backend
|
|
||||||
try {
|
|
||||||
const fetchedConfig: SCIndexResponse = await this.fetch();
|
|
||||||
await this.set(fetchedConfig);
|
|
||||||
this.logger.log(`initialised configuration from remote`);
|
|
||||||
} catch (error) {
|
|
||||||
fetchError = error;
|
|
||||||
}
|
|
||||||
// check for occurred errors and throw them
|
|
||||||
if (loadError !== undefined && fetchError !== undefined) {
|
|
||||||
throw new ConfigInitError();
|
|
||||||
}
|
|
||||||
if (loadError !== undefined) {
|
|
||||||
this.logger.warn(loadError);
|
|
||||||
}
|
|
||||||
if (fetchError !== undefined) {
|
|
||||||
this.logger.warn(fetchError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.config ??= await updatedConfig;
|
||||||
* Returns saved configuration from StorageModule
|
this.config = deepFreeze(this.config);
|
||||||
* @throws SavedConfigNotAvailable if no configuration could be loaded
|
|
||||||
*/
|
if (this.config.backend.SCVersion !== this.scVersion) {
|
||||||
async loadLocal(): Promise<SCIndexResponse> {
|
this.logger.warn(
|
||||||
// get local configuration
|
'Incompatible config, expected',
|
||||||
if (await this.storageProvider.has(STORAGE_KEY_CONFIG)) {
|
this.scVersion,
|
||||||
return this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG);
|
'but got',
|
||||||
|
this.config.backend.SCVersion,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw new SavedConfigNotAvailable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the configuration from the provider
|
|
||||||
* @param config configuration to save
|
|
||||||
*/
|
|
||||||
async save(config: SCIndexResponse): Promise<void> {
|
|
||||||
await this.storageProvider.put(STORAGE_KEY_CONFIG, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the configuration in the module and writes it into app storage
|
|
||||||
* @param config SCIndexResponse to set
|
|
||||||
*/
|
|
||||||
async set(config: SCIndexResponse): Promise<void> {
|
|
||||||
this.config = config;
|
|
||||||
await this.save(this.config);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 StApps
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, version 3.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {AppError} from '../../_helpers/errors';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when fetching from backend fails
|
|
||||||
*/
|
|
||||||
export class ConfigFetchError extends AppError {
|
|
||||||
constructor() {
|
|
||||||
super('ConfigFetchError', 'App configuration could not be fetched!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when the ConfigProvider could be initialised
|
|
||||||
*/
|
|
||||||
export class ConfigInitError extends AppError {
|
|
||||||
constructor() {
|
|
||||||
super('ConfigInitError', 'App configuration could not be initialised!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when the requested config value is not available
|
|
||||||
*/
|
|
||||||
export class ConfigValueNotAvailable extends AppError {
|
|
||||||
constructor(valueKey: string) {
|
|
||||||
super('ConfigValueNotAvailable', `No attribute "${valueKey}" in config available!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when no saved config is available
|
|
||||||
*/
|
|
||||||
export class SavedConfigNotAvailable extends AppError {
|
|
||||||
constructor() {
|
|
||||||
super('SavedConfigNotAvailable', 'No saved app configuration available.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when the SCVersion of the saved config is not compatible with the app
|
|
||||||
*/
|
|
||||||
export class WrongConfigVersionInStorage extends AppError {
|
|
||||||
constructor(correctVersion: string, savedVersion: string) {
|
|
||||||
super(
|
|
||||||
'WrongConfigVersionInStorage',
|
|
||||||
`The saved configs backend version ${savedVersion} ` +
|
|
||||||
`does not equal the configured backend version ${correctVersion} of the app.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ import {SwiperModule} from 'swiper/angular';
|
|||||||
import {TranslateModule, TranslatePipe} from '@ngx-translate/core';
|
import {TranslateModule, TranslatePipe} from '@ngx-translate/core';
|
||||||
import {MomentModule} from 'ngx-moment';
|
import {MomentModule} from 'ngx-moment';
|
||||||
import {DataModule} from '../data/data.module';
|
import {DataModule} from '../data/data.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {DashboardComponent} from './dashboard.component';
|
import {DashboardComponent} from './dashboard.component';
|
||||||
import {SearchSectionComponent} from './sections/search-section/search-section.component';
|
import {SearchSectionComponent} from './sections/search-section/search-section.component';
|
||||||
import {NewsSectionComponent} from './sections/news-section/news-section.component';
|
import {NewsSectionComponent} from './sections/news-section/news-section.component';
|
||||||
@@ -70,6 +69,6 @@ const catalogRoutes: Routes = [
|
|||||||
NewsModule,
|
NewsModule,
|
||||||
JobModule,
|
JobModule,
|
||||||
],
|
],
|
||||||
providers: [SettingsProvider, TranslatePipe],
|
providers: [TranslatePipe],
|
||||||
})
|
})
|
||||||
export class DashboardModule {}
|
export class DashboardModule {}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {ScheduleProvider} from '../calendar/schedule.provider';
|
|||||||
import {GeoNavigationDirective} from '../map/geo-navigation.directive';
|
import {GeoNavigationDirective} from '../map/geo-navigation.directive';
|
||||||
import {MapWidgetComponent} from '../map/widget/map-widget.component';
|
import {MapWidgetComponent} from '../map/widget/map-widget.component';
|
||||||
import {MenuModule} from '../menu/menu.module';
|
import {MenuModule} from '../menu/menu.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {StorageModule} from '../storage/storage.module';
|
import {StorageModule} from '../storage/storage.module';
|
||||||
import {ActionChipListComponent} from './chips/action-chip-list.component';
|
import {ActionChipListComponent} from './chips/action-chip-list.component';
|
||||||
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
|
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
|
||||||
@@ -214,7 +213,6 @@ import {ShareButtonComponent} from './elements/share-button.component';
|
|||||||
StAppsWebHttpClient,
|
StAppsWebHttpClient,
|
||||||
CalendarService,
|
CalendarService,
|
||||||
RoutingStackService,
|
RoutingStackService,
|
||||||
SettingsProvider,
|
|
||||||
{
|
{
|
||||||
provide: SimpleBrowser,
|
provide: SimpleBrowser,
|
||||||
useFactory: browserFactory,
|
useFactory: browserFactory,
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
|
import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
import {ModalController} from '@ionic/angular';
|
import {ModalController} from '@ionic/angular';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
|
||||||
import {SCLanguageCode, SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
|
|
||||||
import {DataProvider, DataScope} from '../data.provider';
|
import {DataProvider, DataScope} from '../data.provider';
|
||||||
import {FavoritesService} from '../../favorites/favorites.service';
|
import {FavoritesService} from '../../favorites/favorites.service';
|
||||||
import {take} from 'rxjs/operators';
|
import {take} from 'rxjs/operators';
|
||||||
import {Network} from '@capacitor/network';
|
import {Network} from '@capacitor/network';
|
||||||
import {DataListContext} from '../list/data-list.component';
|
import {DataListContext} from '../list/data-list.component';
|
||||||
|
import {lastValueFrom} from 'rxjs';
|
||||||
|
|
||||||
export interface ExternalDataLoadEvent {
|
export interface ExternalDataLoadEvent {
|
||||||
uid: SCUuid;
|
uid: SCUuid;
|
||||||
@@ -29,6 +29,13 @@ export interface ExternalDataLoadEvent {
|
|||||||
resolve: (item: SCThings | null | undefined) => void;
|
resolve: (item: SCThings | null | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for SCSavableThing
|
||||||
|
*/
|
||||||
|
function isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
|
||||||
|
return (thing as SCSaveableThing).data !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Component to display an SCThing detailed
|
* A Component to display an SCThing detailed
|
||||||
*/
|
*/
|
||||||
@@ -53,11 +60,6 @@ export class DataDetailComponent implements OnInit {
|
|||||||
|
|
||||||
@Input() autoRouteDataPath = true;
|
@Input() autoRouteDataPath = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* The language of the item
|
|
||||||
*/
|
|
||||||
language: SCLanguageCode;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicating wether internet connectivity is given or not
|
* Indicating wether internet connectivity is given or not
|
||||||
*/
|
*/
|
||||||
@@ -79,20 +81,12 @@ export class DataDetailComponent implements OnInit {
|
|||||||
|
|
||||||
@Output() loadItem: EventEmitter<ExternalDataLoadEvent> = new EventEmitter<ExternalDataLoadEvent>();
|
@Output() loadItem: EventEmitter<ExternalDataLoadEvent> = new EventEmitter<ExternalDataLoadEvent>();
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard for SCSavableThing
|
|
||||||
*/
|
|
||||||
static isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
|
|
||||||
return (thing as SCSaveableThing).data !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly route: ActivatedRoute,
|
protected readonly route: ActivatedRoute,
|
||||||
router: Router,
|
router: Router,
|
||||||
private readonly dataProvider: DataProvider,
|
private readonly dataProvider: DataProvider,
|
||||||
private readonly favoritesService: FavoritesService,
|
private readonly favoritesService: FavoritesService,
|
||||||
readonly modalController: ModalController,
|
readonly modalController: ModalController,
|
||||||
translateService: TranslateService,
|
|
||||||
) {
|
) {
|
||||||
this.inputItem = router.getCurrentNavigation()?.extras.state?.item;
|
this.inputItem = router.getCurrentNavigation()?.extras.state?.item;
|
||||||
if (!this.inputItem?.origin) {
|
if (!this.inputItem?.origin) {
|
||||||
@@ -100,10 +94,6 @@ export class DataDetailComponent implements OnInit {
|
|||||||
// This can happen, for example, when detail views use `inPlace` list items
|
// This can happen, for example, when detail views use `inPlace` list items
|
||||||
delete this.inputItem;
|
delete this.inputItem;
|
||||||
}
|
}
|
||||||
this.language = translateService.currentLang as SCLanguageCode;
|
|
||||||
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as SCLanguageCode;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.isDisconnected = new Promise(async resolve => {
|
this.isDisconnected = new Promise(async resolve => {
|
||||||
const isConnected = (await Network.getStatus()).connected;
|
const isConnected = (await Network.getStatus()).connected;
|
||||||
@@ -126,13 +116,8 @@ export class DataDetailComponent implements OnInit {
|
|||||||
)
|
)
|
||||||
: this.dataProvider.get(uid, DataScope.Remote)));
|
: this.dataProvider.get(uid, DataScope.Remote)));
|
||||||
|
|
||||||
this.item = item
|
// eslint-disable-next-line unicorn/no-null
|
||||||
? // eslint-disable-next-line unicorn/no-null
|
this.item = item ? (isSCSavableThing(item) ? item.data : item) : null;
|
||||||
DataDetailComponent.isSCSavableThing(item)
|
|
||||||
? item.data
|
|
||||||
: item
|
|
||||||
: // eslint-disable-next-line unicorn/no-null
|
|
||||||
null;
|
|
||||||
} catch {
|
} catch {
|
||||||
// eslint-disable-next-line unicorn/no-null
|
// eslint-disable-next-line unicorn/no-null
|
||||||
this.item = null;
|
this.item = null;
|
||||||
@@ -144,14 +129,10 @@ export class DataDetailComponent implements OnInit {
|
|||||||
await this.getItem(uid ?? '', false);
|
await this.getItem(uid ?? '', false);
|
||||||
// fallback to the saved item (from favorites)
|
// fallback to the saved item (from favorites)
|
||||||
if (this.item === null) {
|
if (this.item === null) {
|
||||||
this.favoritesService
|
const item = await lastValueFrom(this.favoritesService.get(uid).pipe(take(1)));
|
||||||
.get(uid)
|
if (item) {
|
||||||
.pipe(take(1))
|
this.item = item.data;
|
||||||
.subscribe(item => {
|
}
|
||||||
if (item !== undefined) {
|
|
||||||
this.item = item.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export class OffersInListComponent {
|
|||||||
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
|
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
|
||||||
this._offers = it;
|
this._offers = it;
|
||||||
this.price = it[0].prices?.default;
|
this.price = it[0].prices?.default;
|
||||||
this.settingsProvider.getSetting('profile', 'group').then(group => {
|
this.settingsProvider.getSetting<string>('profile', 'group').then(group => {
|
||||||
this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never];
|
this.price = it[0].prices?.[group.replace(/s$/, '') as never];
|
||||||
});
|
});
|
||||||
|
|
||||||
const availabilities = new Set(it.map(offer => offer.availability));
|
const availabilities = new Set(it.map(offer => offer.availability));
|
||||||
|
|||||||
@@ -17,14 +17,7 @@ import {ActivatedRoute, Router} from '@angular/router';
|
|||||||
import {Keyboard} from '@capacitor/keyboard';
|
import {Keyboard} from '@capacitor/keyboard';
|
||||||
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
|
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
|
||||||
import {Capacitor} from '@capacitor/core';
|
import {Capacitor} from '@capacitor/core';
|
||||||
import {
|
import {SCFacet, SCSearchFilter, SCSearchQuery, SCSearchSort, SCThings} from '@openstapps/core';
|
||||||
SCFacet,
|
|
||||||
SCFeatureConfiguration,
|
|
||||||
SCSearchFilter,
|
|
||||||
SCSearchQuery,
|
|
||||||
SCSearchSort,
|
|
||||||
SCThings,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {combineLatest, Subject} from 'rxjs';
|
import {combineLatest, Subject} from 'rxjs';
|
||||||
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
|
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
|
||||||
@@ -170,9 +163,8 @@ export class SearchPageComponent implements OnInit {
|
|||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
protected positionService: PositionService,
|
protected positionService: PositionService,
|
||||||
private readonly configProvider: ConfigProvider,
|
private readonly configProvider: ConfigProvider,
|
||||||
animationController: AnimationController,
|
|
||||||
) {
|
) {
|
||||||
this.routeAnimation = searchPageSwitchAnimation(animationController);
|
this.routeAnimation = searchPageSwitchAnimation(inject(AnimationController));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -323,16 +315,6 @@ export class SearchPageComponent implements OnInit {
|
|||||||
this.queryChanged.next();
|
this.queryChanged.next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.settingsProvider.settingsActionChanged$
|
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
|
||||||
.subscribe(({type, payload}) => {
|
|
||||||
if (type === 'stapps.settings.changed') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const {category, name, value} = payload!;
|
|
||||||
this.logger.log(`received event "settings.changed" with category:
|
|
||||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.dataRoutingService
|
this.dataRoutingService
|
||||||
.itemSelectListener()
|
.itemSelectListener()
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
.pipe(takeUntilDestroyed(this.destroy$))
|
||||||
@@ -342,12 +324,8 @@ export class SearchPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
this.isHebisAvailable =
|
||||||
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
this.configProvider.config.app.features.plugins?.['hebis-plugin']?.urlPath !== undefined;
|
||||||
this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
SCRatingResponse,
|
SCRatingResponse,
|
||||||
SCRatingRoute,
|
SCRatingRoute,
|
||||||
SCUserGroup,
|
SCUserGroup,
|
||||||
SCUserGroupSetting,
|
|
||||||
SCUuid,
|
SCUuid,
|
||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||||
@@ -63,9 +62,7 @@ export class RatingProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get userGroup(): Promise<SCUserGroup> {
|
private get userGroup(): Promise<SCUserGroup> {
|
||||||
return this.settingsProvider
|
return this.settingsProvider.getSetting<SCUserGroup>('profile', 'group');
|
||||||
.getSetting('profile', 'group')
|
|
||||||
.then(it => (it as SCUserGroupSetting).value as SCUserGroup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getStoredRatings(): Promise<RatingStorage> {
|
private async getStoredRatings(): Promise<RatingStorage> {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class PlaceMensaService {
|
|||||||
sort: [
|
sort: [
|
||||||
{
|
{
|
||||||
arguments: {
|
arguments: {
|
||||||
field: `offers.prices.${(priceGroup.value as string).replace(/s$/, '')}`,
|
field: `offers.prices.${(priceGroup as string).replace(/s$/, '')}`,
|
||||||
},
|
},
|
||||||
order: 'desc',
|
order: 'desc',
|
||||||
type: 'generic',
|
type: 'generic',
|
||||||
|
|||||||
@@ -12,21 +12,13 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, inject, OnInit} from '@angular/core';
|
||||||
import {AlertController, AnimationController} from '@ionic/angular';
|
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
|
||||||
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
|
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
|
||||||
import {combineLatest} from 'rxjs';
|
import {combineLatest} from 'rxjs';
|
||||||
import {SCThingType} from '@openstapps/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
import {FavoritesService} from './favorites.service';
|
import {FavoritesService} from './favorites.service';
|
||||||
import {DataRoutingService} from '../data/data-routing.service';
|
|
||||||
import {ContextMenuService} from '../menu/context/context-menu.service';
|
import {ContextMenuService} from '../menu/context/context-menu.service';
|
||||||
import {SearchPageComponent} from '../data/list/search-page.component';
|
import {SearchPageComponent} from '../data/list/search-page.component';
|
||||||
import {DataProvider} from '../data/data.provider';
|
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {PositionService} from '../map/position.service';
|
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,34 +34,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
|
|||||||
|
|
||||||
showNavigation = false;
|
showNavigation = false;
|
||||||
|
|
||||||
constructor(
|
private favoritesService = inject(FavoritesService);
|
||||||
alertController: AlertController,
|
|
||||||
dataProvider: DataProvider,
|
|
||||||
contextMenuService: ContextMenuService,
|
|
||||||
settingsProvider: SettingsProvider,
|
|
||||||
logger: NGXLogger,
|
|
||||||
dataRoutingService: DataRoutingService,
|
|
||||||
router: Router,
|
|
||||||
route: ActivatedRoute,
|
|
||||||
positionService: PositionService,
|
|
||||||
private favoritesService: FavoritesService,
|
|
||||||
configProvider: ConfigProvider,
|
|
||||||
animationController: AnimationController,
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
alertController,
|
|
||||||
dataProvider,
|
|
||||||
contextMenuService,
|
|
||||||
settingsProvider,
|
|
||||||
logger,
|
|
||||||
dataRoutingService,
|
|
||||||
router,
|
|
||||||
route,
|
|
||||||
positionService,
|
|
||||||
configProvider,
|
|
||||||
animationController,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit(false);
|
super.ngOnInit(false);
|
||||||
@@ -96,16 +61,6 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
|
|||||||
this.queryChanged.next();
|
this.queryChanged.next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.settingsProvider.settingsActionChanged$
|
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
|
||||||
.subscribe(({type, payload}) => {
|
|
||||||
if (type === 'stapps.settings.changed') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const {category, name, value} = payload!;
|
|
||||||
this.logger.log(`received event "settings.changed" with category:
|
|
||||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.dataRoutingService
|
this.dataRoutingService
|
||||||
.itemSelectListener()
|
.itemSelectListener()
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
.pipe(takeUntilDestroyed(this.destroy$))
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {DataProvider} from '../data/data.provider';
|
import {DataProvider} from '../data/data.provider';
|
||||||
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
||||||
import {BehaviorSubject, Observable} from 'rxjs';
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
@@ -41,11 +40,6 @@ import {debounceTime, map} from 'rxjs/operators';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class FavoritesService {
|
export class FavoritesService {
|
||||||
/**
|
|
||||||
* Translation pipe
|
|
||||||
*/
|
|
||||||
thingTranslatePipe: ThingTranslatePipe;
|
|
||||||
|
|
||||||
favorites = new BehaviorSubject<Map<string, SCFavorite>>(new Map<string, SCFavorite>());
|
favorites = new BehaviorSubject<Map<string, SCFavorite>>(new Map<string, SCFavorite>());
|
||||||
|
|
||||||
// using debounce time 0 allows change detection to run through async suspension
|
// using debounce time 0 allows change detection to run through async suspension
|
||||||
@@ -93,8 +87,8 @@ export class FavoritesService {
|
|||||||
return items.sort((a, b) => {
|
return items.sort((a, b) => {
|
||||||
return (
|
return (
|
||||||
new Intl.Collator(this.translate.currentLang).compare(
|
new Intl.Collator(this.translate.currentLang).compare(
|
||||||
this.thingTranslatePipe.transform(field, a),
|
this.thingTranslate.get(a, field) as string,
|
||||||
this.thingTranslatePipe.transform(field, b),
|
this.thingTranslate.get(b, field) as string,
|
||||||
) * reverse
|
) * reverse
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -124,7 +118,6 @@ export class FavoritesService {
|
|||||||
private readonly translate: TranslateService,
|
private readonly translate: TranslateService,
|
||||||
private readonly thingTranslate: ThingTranslateService,
|
private readonly thingTranslate: ThingTranslateService,
|
||||||
) {
|
) {
|
||||||
this.thingTranslatePipe = new ThingTranslatePipe(this.translate, this.thingTranslate);
|
|
||||||
void this.emitAll();
|
void this.emitAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +178,9 @@ export class FavoritesService {
|
|||||||
const textFilteredItems: SCIndexableThings[] = [];
|
const textFilteredItems: SCIndexableThings[] = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (
|
if (
|
||||||
this.thingTranslatePipe.transform('name', item).toLowerCase().includes(queryText.toLowerCase())
|
(this.thingTranslate.get(item, 'name') as string)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(queryText.toLowerCase())
|
||||||
) {
|
) {
|
||||||
textFilteredItems.push(item);
|
textFilteredItems.push(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {DaiaAvailabilityResponse, DaiaHolding, DaiaService} from './protocol/res
|
|||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {SCFeatureConfiguration} from '@openstapps/core';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -67,7 +66,7 @@ export class DaiaDataProvider {
|
|||||||
async getAvailability(id: string): Promise<DaiaHolding[] | undefined> {
|
async getAvailability(id: string): Promise<DaiaHolding[] | undefined> {
|
||||||
if (this.daiaServiceUrl === undefined) {
|
if (this.daiaServiceUrl === undefined) {
|
||||||
try {
|
try {
|
||||||
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
const features = this.configProvider.config.app.features;
|
||||||
if (features.extern?.daia?.url) {
|
if (features.extern?.daia?.url) {
|
||||||
this.daiaServiceUrl = features.extern?.daia?.url;
|
this.daiaServiceUrl = features.extern?.daia?.url;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -114,16 +114,6 @@ export class HebisSearchPageComponent extends SearchPageComponent implements OnI
|
|||||||
this.queryChanged.next();
|
this.queryChanged.next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.settingsProvider.settingsActionChanged$
|
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
|
||||||
.subscribe(({type, payload}) => {
|
|
||||||
if (type === 'stapps.settings.changed') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const {category, name, value} = payload!;
|
|
||||||
this.logger.log(`received event "settings.changed" with category:
|
|
||||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.dataRoutingService
|
this.dataRoutingService
|
||||||
.itemSelectListener()
|
.itemSelectListener()
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
.pipe(takeUntilDestroyed(this.destroy$))
|
||||||
|
|||||||
@@ -12,14 +12,9 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {JQueryRequestor, Requestor} from '@openid/appauth';
|
import {JQueryRequestor, Requestor} from '@openid/appauth';
|
||||||
import {
|
import {SCAuthorizationProviderType, SCFeatureConfigurationExtern} from '@openstapps/core';
|
||||||
SCAuthorizationProviderType,
|
|
||||||
SCFeatureConfiguration,
|
|
||||||
SCFeatureConfigurationExtern,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
|
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
|
||||||
import {HebisDataProvider} from '../../hebis/hebis-data.provider';
|
import {HebisDataProvider} from '../../hebis/hebis-data.provider';
|
||||||
import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
|
import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
|
||||||
@@ -53,9 +48,7 @@ export class LibraryAccountService {
|
|||||||
private readonly toastController: ToastController,
|
private readonly toastController: ToastController,
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const config: SCFeatureConfigurationExtern = (
|
const config: SCFeatureConfigurationExtern = configProvider.config.app.features.extern!.paia;
|
||||||
configProvider.getValue('features') as SCFeatureConfiguration
|
|
||||||
).extern!.paia;
|
|
||||||
this.baseUrl = config.url;
|
this.baseUrl = config.url;
|
||||||
this.authType = config.authProvider as SCAuthorizationProviderType;
|
this.authType = config.authProvider as SCAuthorizationProviderType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {LeafletModule} from '@asymmetrik/ngx-leaflet';
|
|||||||
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
||||||
import {IonicModule} from '@ionic/angular';
|
import {IonicModule} from '@ionic/angular';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {Polygon} from 'geojson';
|
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {DataFacetsProvider} from '../data/data-facets.provider';
|
import {DataFacetsProvider} from '../data/data-facets.provider';
|
||||||
@@ -42,7 +41,7 @@ import {GeoNavigationDirective} from './geo-navigation.directive';
|
|||||||
*/
|
*/
|
||||||
export function initMapConfigFactory(configProvider: ConfigProvider, mapProvider: MapProvider) {
|
export function initMapConfigFactory(configProvider: ConfigProvider, mapProvider: MapProvider) {
|
||||||
return async () => {
|
return async () => {
|
||||||
mapProvider.defaultPolygon = (await configProvider.getValue('campusPolygon')) as Polygon;
|
mapProvider.defaultPolygon = configProvider.config.app.campusPolygon;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export class MapProvider {
|
|||||||
private positionService: PositionService,
|
private positionService: PositionService,
|
||||||
private configProvider: ConfigProvider,
|
private configProvider: ConfigProvider,
|
||||||
) {
|
) {
|
||||||
this.defaultPolygon = this.configProvider.getValue('campusPolygon') as Polygon;
|
this.defaultPolygon = this.configProvider.config.app.campusPolygon;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, Input} from '@angular/core';
|
import {Component, Input} from '@angular/core';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
import {SCLanguage, SCThingTranslator, SCThingType, SCTranslations} from '@openstapps/core';
|
|
||||||
import {ContextMenuService} from './context-menu.service';
|
import {ContextMenuService} from './context-menu.service';
|
||||||
import {FilterContext, FilterFacet, SortContext, SortContextOption} from './context-type.js';
|
import {FilterContext, FilterFacet, SortContext, SortContextOption} from './context-type.js';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
import {ThingTranslateService} from '../../../translation/thing-translate.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The context menu
|
* The context menu
|
||||||
@@ -59,11 +59,6 @@ export class ContextMenuComponent {
|
|||||||
return this.filterOption.options.filter(it => it.buckets.length > 0);
|
return this.filterOption.options.filter(it => it.buckets.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Possible languages to be used for translation
|
|
||||||
*/
|
|
||||||
language: keyof SCTranslations<SCLanguage>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of SCThingType
|
* Mapping of SCThingType
|
||||||
*/
|
*/
|
||||||
@@ -74,22 +69,10 @@ export class ContextMenuComponent {
|
|||||||
*/
|
*/
|
||||||
sortOption: SortContext;
|
sortOption: SortContext;
|
||||||
|
|
||||||
/**
|
|
||||||
* Core translator
|
|
||||||
*/
|
|
||||||
translator: SCThingTranslator;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private translateService: TranslateService,
|
|
||||||
private readonly contextMenuService: ContextMenuService,
|
private readonly contextMenuService: ContextMenuService,
|
||||||
|
private readonly thingTranslateService: ThingTranslateService,
|
||||||
) {
|
) {
|
||||||
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
|
|
||||||
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
});
|
|
||||||
this.contextMenuService.filterContextChanged$.pipe(takeUntilDestroyed()).subscribe(filterContext => {
|
this.contextMenuService.filterContextChanged$.pipe(takeUntilDestroyed()).subscribe(filterContext => {
|
||||||
this.filterOption = filterContext;
|
this.filterOption = filterContext;
|
||||||
});
|
});
|
||||||
@@ -109,7 +92,7 @@ export class ContextMenuComponent {
|
|||||||
* Returns translated property value
|
* Returns translated property value
|
||||||
*/
|
*/
|
||||||
getTranslatedPropertyValue(onlyForType: SCThingType, field: string, key?: string): string | undefined {
|
getTranslatedPropertyValue(onlyForType: SCThingType, field: string, key?: string): string | undefined {
|
||||||
return this.translator.translatedPropertyValue(onlyForType, field, key);
|
return this.thingTranslateService.translator.translatedPropertyValue(onlyForType, field, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,74 +13,23 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
||||||
import {
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
SCAppConfigurationMenuCategory,
|
|
||||||
SCLanguage,
|
|
||||||
SCThingTranslator,
|
|
||||||
SCTranslations,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {NavigationService} from './navigation.service';
|
|
||||||
import config from 'capacitor.config';
|
|
||||||
import {SettingsProvider} from '../../settings/settings.provider';
|
|
||||||
import {BreakpointObserver} from '@angular/cdk/layout';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generated class for the MenuPage page.
|
|
||||||
*
|
|
||||||
* See https://ionicframework.com/docs/components/#navigation for more info on
|
|
||||||
* Ionic pages and navigation.
|
|
||||||
*/
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'stapps-navigation',
|
selector: 'stapps-navigation',
|
||||||
styleUrls: ['navigation.scss'],
|
styleUrls: ['navigation.scss'],
|
||||||
templateUrl: 'navigation.html',
|
templateUrl: 'navigation.html',
|
||||||
})
|
})
|
||||||
export class NavigationComponent implements OnInit {
|
export class NavigationComponent implements OnInit {
|
||||||
showTabbar = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the app
|
|
||||||
*/
|
|
||||||
appName = config.appName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Possible languages to be used for translation
|
|
||||||
*/
|
|
||||||
language: keyof SCTranslations<SCLanguage>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu entries from config module
|
* Menu entries from config module
|
||||||
*/
|
*/
|
||||||
menu: SCAppConfigurationMenuCategory[];
|
menu: SCAppConfigurationMenuCategory[];
|
||||||
|
|
||||||
/**
|
constructor(private config: ConfigProvider) {}
|
||||||
* Core translator
|
|
||||||
*/
|
|
||||||
translator: SCThingTranslator;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public translateService: TranslateService,
|
|
||||||
private navigationService: NavigationService,
|
|
||||||
private settingsProvider: SettingsProvider,
|
|
||||||
private responsive: BreakpointObserver,
|
|
||||||
) {
|
|
||||||
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.responsive.observe(['(min-width: 768px)']).subscribe(result => {
|
|
||||||
this.showTabbar = !result.matches;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.language = (await this.settingsProvider.getValue(
|
this.menu = this.config.config.app.menus;
|
||||||
'profile',
|
|
||||||
'language',
|
|
||||||
)) as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
this.menu = await this.navigationService.getMenu();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,11 @@
|
|||||||
class="menu-category"
|
class="menu-category"
|
||||||
>
|
>
|
||||||
<ion-icon slot="end" [name]="category.icon"></ion-icon>
|
<ion-icon slot="end" [name]="category.icon"></ion-icon>
|
||||||
<ion-label> {{ category.translations[language]?.title | titlecase }} </ion-label>
|
<ion-label> {{ 'title' | translateSimple: category | titlecase }} </ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route">
|
<ion-item *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route">
|
||||||
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
||||||
<ion-label> {{ item.translations[language]?.title | titlecase }} </ion-label>
|
<ion-label> {{ 'title' | translateSimple: item | titlecase }} </ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
|
|||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {RouterModule} from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import {OfflineNoticeComponent} from './offline-notice.component';
|
import {OfflineNoticeComponent} from './offline-notice.component';
|
||||||
|
import {ThingTranslateModule} from '../../../translation/thing-translate.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
|
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
|
||||||
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule],
|
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule, ThingTranslateModule],
|
||||||
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
|
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
|
||||||
})
|
})
|
||||||
export class NavigationModule {}
|
export class NavigationModule {}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ stapps-navigation-tabs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stapps-offline-notice.needs-reload ~ ion-split-pane,
|
||||||
stapps-offline-notice.has-error ~ ion-split-pane,
|
stapps-offline-notice.has-error ~ ion-split-pane,
|
||||||
stapps-offline-notice.is-offline ~ ion-split-pane {
|
stapps-offline-notice.is-offline ~ ion-split-pane {
|
||||||
margin-top: calc(var(--font-size-md) + 2 * var(--spacing-sm));
|
margin-top: calc(var(--font-size-md) + 2 * var(--spacing-sm));
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 StApps
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, version 3.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Injectable} from '@angular/core';
|
|
||||||
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class NavigationService {
|
|
||||||
constructor(
|
|
||||||
private configProvider: ConfigProvider,
|
|
||||||
private logger: NGXLogger,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getMenu() {
|
|
||||||
let menu: SCAppConfigurationMenuCategory[] = [];
|
|
||||||
try {
|
|
||||||
menu = this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[];
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`error from loading menu entries: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,10 @@ import {InternetConnectionService} from '../../../util/internet-connection.servi
|
|||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
import {SettingsProvider} from '../../settings/settings.provider';
|
||||||
|
import {AnimationController} from '@ionic/angular';
|
||||||
|
import {filter, race} from 'rxjs';
|
||||||
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'stapps-offline-notice',
|
selector: 'stapps-offline-notice',
|
||||||
@@ -28,12 +32,17 @@ export class OfflineNoticeComponent {
|
|||||||
|
|
||||||
@HostBinding('class.has-error') hasError = false;
|
@HostBinding('class.has-error') hasError = false;
|
||||||
|
|
||||||
|
@HostBinding('class.needs-reload') needsReload = false;
|
||||||
|
|
||||||
@ViewChild('spinIcon', {read: ElementRef}) spinIcon: ElementRef;
|
@ViewChild('spinIcon', {read: ElementRef}) spinIcon: ElementRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly offlineProvider: InternetConnectionService,
|
readonly offlineProvider: InternetConnectionService,
|
||||||
readonly router: Router,
|
readonly router: Router,
|
||||||
readonly logger: NGXLogger,
|
readonly logger: NGXLogger,
|
||||||
|
readonly animationCtl: AnimationController,
|
||||||
|
settingsProvider: SettingsProvider,
|
||||||
|
configProvider: ConfigProvider,
|
||||||
) {
|
) {
|
||||||
this.offlineProvider.offline$.pipe(takeUntilDestroyed()).subscribe(isOffline => {
|
this.offlineProvider.offline$.pipe(takeUntilDestroyed()).subscribe(isOffline => {
|
||||||
this.isOffline = isOffline;
|
this.isOffline = isOffline;
|
||||||
@@ -41,6 +50,15 @@ export class OfflineNoticeComponent {
|
|||||||
this.offlineProvider.error$.pipe(takeUntilDestroyed()).subscribe(hasError => {
|
this.offlineProvider.error$.pipe(takeUntilDestroyed()).subscribe(hasError => {
|
||||||
this.hasError = hasError;
|
this.hasError = hasError;
|
||||||
});
|
});
|
||||||
|
race(
|
||||||
|
settingsProvider.needsReload$.pipe(filter(it => it)),
|
||||||
|
configProvider.needsUpdate$.pipe(filter(it => it)),
|
||||||
|
)
|
||||||
|
.pipe(takeUntilDestroyed())
|
||||||
|
.subscribe(() => {
|
||||||
|
console.log('aha!');
|
||||||
|
this.needsReload = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
retry() {
|
retry() {
|
||||||
@@ -49,4 +67,15 @@ export class OfflineNoticeComponent {
|
|||||||
this.spinIcon.nativeElement.classList.add('spin');
|
this.spinIcon.nativeElement.classList.add('spin');
|
||||||
this.offlineProvider.retry();
|
this.offlineProvider.retry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reloadPage() {
|
||||||
|
await this.animationCtl
|
||||||
|
.create()
|
||||||
|
.duration(100)
|
||||||
|
.fromTo('opacity', 1, 0)
|
||||||
|
.addElement(document.querySelector('ion-app')!)
|
||||||
|
.play();
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
<ion-icon #spinIcon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
|
<ion-icon #spinIcon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
|
||||||
<ion-label>{{ 'app.errors.CONNECTION_ERROR' | translate }}</ion-label>
|
<ion-label>{{ 'app.errors.CONNECTION_ERROR' | translate }}</ion-label>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
<ion-button class="reload" color="warning" (click)="reloadPage()">
|
||||||
|
<ion-icon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
|
||||||
|
<ion-label>{{ 'settings.reloadPage' | translate }}</ion-label>
|
||||||
|
</ion-button>
|
||||||
<ion-button class="close" fill="clear" color="light" (click)="offlineProvider.dismissError()"
|
<ion-button class="close" fill="clear" color="light" (click)="offlineProvider.dismissError()"
|
||||||
><ion-icon [size]="16" [weight]="800" name="close" slot="icon-only"></ion-icon
|
><ion-icon [size]="16" [weight]="800" name="close" slot="icon-only"></ion-icon
|
||||||
></ion-button>
|
></ion-button>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
transition: all 150ms ease;
|
transition: all 150ms ease;
|
||||||
|
|
||||||
|
&.needs-reload,
|
||||||
&.is-offline,
|
&.is-offline,
|
||||||
&.has-error {
|
&.has-error {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.needs-reload > .reload,
|
||||||
&.is-offline > .offline-button,
|
&.is-offline > .offline-button,
|
||||||
&.has-error > .close,
|
&.has-error > .close,
|
||||||
&.has-error > .error-button {
|
&.has-error > .error-button {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import type {AnimationBuilder} from '@ionic/angular';
|
import type {AnimationBuilder} from '@ionic/angular';
|
||||||
import {AnimationController} from '@ionic/angular';
|
import {AnimationController} from '@ionic/angular';
|
||||||
import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
|
import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
|
||||||
|
import {iosDuration, iosEasing, mdDuration, mdEasing} from '../../../animation/easings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -23,10 +24,11 @@ import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
|
|||||||
export function tabsTransition(animationController: AnimationController): AnimationBuilder {
|
export function tabsTransition(animationController: AnimationController): AnimationBuilder {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
|
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
|
||||||
const duration = options.duration || 350;
|
const isMd = document.querySelector('ion-app.md') !== null;
|
||||||
const contentExitDuration = options.contentExitDuration || 100;
|
const duration = isMd ? mdDuration : iosDuration;
|
||||||
|
const easing = isMd ? mdEasing : iosEasing;
|
||||||
|
|
||||||
const rootTransition = animationController.create().duration(duration);
|
const rootTransition = animationController.create().duration(duration).easing(easing);
|
||||||
|
|
||||||
const enterTransition = animationController
|
const enterTransition = animationController
|
||||||
.create()
|
.create()
|
||||||
@@ -39,23 +41,15 @@ export function tabsTransition(animationController: AnimationController): Animat
|
|||||||
.addElement(options.leavingEl);
|
.addElement(options.leavingEl);
|
||||||
const exitTransition = animationController
|
const exitTransition = animationController
|
||||||
.create()
|
.create()
|
||||||
.duration(contentExitDuration * 2)
|
|
||||||
.easing('cubic-bezier(0.87, 0, 0.13, 1)')
|
|
||||||
.fromTo('opacity', '1', '0')
|
.fromTo('opacity', '1', '0')
|
||||||
.addElement(options.leavingEl.querySelector('ion-header'));
|
.addElement(options.leavingEl.querySelector('ion-header'));
|
||||||
const contentExit = animationController
|
const contentExit = animationController
|
||||||
.create()
|
.create()
|
||||||
.easing('linear')
|
|
||||||
.duration(contentExitDuration)
|
|
||||||
.fromTo('opacity', '1', '0')
|
.fromTo('opacity', '1', '0')
|
||||||
.addElement(options.leavingEl.querySelectorAll(':scope > *:not(ion-header)'));
|
.addElement(options.leavingEl.querySelectorAll(':scope > *:not(ion-header)'));
|
||||||
const contentEnter = animationController
|
const contentEnter = animationController
|
||||||
.create()
|
.create()
|
||||||
.delay(contentExitDuration)
|
.fromTo('transform', 'scale(1.05)', 'scale(1)')
|
||||||
.duration(duration - contentExitDuration)
|
|
||||||
.easing('cubic-bezier(0.16, 1, 0.3, 1)')
|
|
||||||
.fromTo('transform', 'scale(1.025)', 'scale(1)')
|
|
||||||
.fromTo('opacity', '0', '1')
|
|
||||||
.addElement(options.enteringEl.querySelectorAll(':scope > *:not(ion-header)'));
|
.addElement(options.enteringEl.querySelectorAll(':scope > *:not(ion-header)'));
|
||||||
|
|
||||||
rootTransition.addAnimation([enterTransition, contentExit, contentEnter, exitTransition, exitZIndex]);
|
rootTransition.addAnimation([enterTransition, contentExit, contentEnter, exitTransition, exitZIndex]);
|
||||||
|
|||||||
@@ -12,17 +12,11 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {NavigationEnd, Router} from '@angular/router';
|
import {NavigationEnd, Router} from '@angular/router';
|
||||||
import {
|
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
||||||
SCAppConfigurationMenuCategory,
|
|
||||||
SCLanguage,
|
|
||||||
SCThingTranslator,
|
|
||||||
SCTranslations,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -31,21 +25,11 @@ import {NGXLogger} from 'ngx-logger';
|
|||||||
styleUrls: ['./tabs.component.scss'],
|
styleUrls: ['./tabs.component.scss'],
|
||||||
})
|
})
|
||||||
export class TabsComponent {
|
export class TabsComponent {
|
||||||
/**
|
|
||||||
* Possible languages to be used for translation
|
|
||||||
*/
|
|
||||||
language: keyof SCTranslations<SCLanguage>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu entries from config module
|
* Menu entries from config module
|
||||||
*/
|
*/
|
||||||
menu: SCAppConfigurationMenuCategory[];
|
menu: SCAppConfigurationMenuCategory[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Core translator
|
|
||||||
*/
|
|
||||||
translator: SCThingTranslator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of selected tab
|
* Name of selected tab
|
||||||
*/
|
*/
|
||||||
@@ -57,8 +41,6 @@ export class TabsComponent {
|
|||||||
private readonly logger: NGXLogger,
|
private readonly logger: NGXLogger,
|
||||||
private readonly router: Router,
|
private readonly router: Router,
|
||||||
) {
|
) {
|
||||||
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
void this.loadMenuEntries();
|
void this.loadMenuEntries();
|
||||||
this.router.events.subscribe((event: unknown) => {
|
this.router.events.subscribe((event: unknown) => {
|
||||||
if (event instanceof NavigationEnd) {
|
if (event instanceof NavigationEnd) {
|
||||||
@@ -66,11 +48,6 @@ export class TabsComponent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.selectTab(router.url);
|
this.selectTab(router.url);
|
||||||
|
|
||||||
translateService.onLangChange?.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +55,7 @@ export class TabsComponent {
|
|||||||
*/
|
*/
|
||||||
async loadMenuEntries() {
|
async loadMenuEntries() {
|
||||||
try {
|
try {
|
||||||
const menus = (await this.configProvider.getValue('menus')) as SCAppConfigurationMenuCategory[];
|
const menus = this.configProvider.config.app.menus;
|
||||||
|
|
||||||
const menu = menus.slice(0, 5);
|
const menu = menus.slice(0, 5);
|
||||||
if (menu) {
|
if (menu) {
|
||||||
|
|||||||
@@ -46,6 +46,6 @@
|
|||||||
[tab]="category.title"
|
[tab]="category.title"
|
||||||
>
|
>
|
||||||
<ion-icon [name]="category.icon"></ion-icon>
|
<ion-icon [name]="category.icon"></ion-icon>
|
||||||
<ion-label>{{ category.translations[language]?.title | titlecase }}</ion-label>
|
<ion-label>{{ 'title' | translateSimple: category | titlecase }}</ion-label>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
</ion-tab-bar>
|
</ion-tab-bar>
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<ng-container *ngFor="let setting of settings">
|
|
||||||
<stapps-chip-filter
|
|
||||||
[displayValue]="setting | settingValueTranslate | titlecase"
|
|
||||||
[value]="setting"
|
|
||||||
[active]="!!filtersMap.get($any(setting.name))"
|
|
||||||
(toggle)="stateChanged($any($event))"
|
|
||||||
>
|
|
||||||
</stapps-chip-filter>
|
|
||||||
</ng-container>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 StApps
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, version 3.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
|
||||||
import {newsFilterSettingsFieldsMapping, NewsFilterSettingsNames} from '../../news-filter-settings';
|
|
||||||
import {SCSearchValueFilter, SCSetting} from '@openstapps/core';
|
|
||||||
import {DataProvider} from '../../../data/data.provider';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'stapps-news-settings-filter',
|
|
||||||
templateUrl: './news-settings-filter.component.html',
|
|
||||||
styleUrls: ['./news-settings-filter.component.scss'],
|
|
||||||
})
|
|
||||||
export class NewsSettingsFilterComponent implements OnInit {
|
|
||||||
/**
|
|
||||||
* A map of the filters where the keys are settings names
|
|
||||||
*/
|
|
||||||
filtersMap = new Map<NewsFilterSettingsNames, SCSearchValueFilter>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits the current filters
|
|
||||||
*/
|
|
||||||
@Output() filtersChanged = new EventEmitter<SCSearchValueFilter[]>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provided settings to show the filters for
|
|
||||||
*/
|
|
||||||
@Input() settings: SCSetting[];
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
for (const setting of this.settings) {
|
|
||||||
this.filtersMap.set(
|
|
||||||
setting.name as NewsFilterSettingsNames,
|
|
||||||
DataProvider.createValueFilter(
|
|
||||||
newsFilterSettingsFieldsMapping[setting.name as NewsFilterSettingsNames],
|
|
||||||
setting.value as string,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.filtersChanged.emit([...this.filtersMap.values()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To be executed when a chip filter has been enabled/disabled
|
|
||||||
* @param setting The value of the filter
|
|
||||||
*/
|
|
||||||
stateChanged(setting: SCSetting) {
|
|
||||||
if (this.filtersMap.get(setting.name as NewsFilterSettingsNames) === undefined) {
|
|
||||||
this.filtersMap.set(
|
|
||||||
setting.name as NewsFilterSettingsNames,
|
|
||||||
DataProvider.createValueFilter(
|
|
||||||
newsFilterSettingsFieldsMapping[setting.name as NewsFilterSettingsNames],
|
|
||||||
setting.value as string,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.filtersMap.delete(setting.name as NewsFilterSettingsNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.filtersChanged.emit([...this.filtersMap.values()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,13 +20,11 @@ import {TranslateModule} from '@ngx-translate/core';
|
|||||||
import {MomentModule} from 'ngx-moment';
|
import {MomentModule} from 'ngx-moment';
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {DataModule} from '../data/data.module';
|
import {DataModule} from '../data/data.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {NewsItemComponent} from './item/news-item.component';
|
import {NewsItemComponent} from './item/news-item.component';
|
||||||
import {NewsPageComponent} from './page/news-page.component';
|
import {NewsPageComponent} from './page/news-page.component';
|
||||||
import {SkeletonNewsItemComponent} from './item/skeleton-news-item.component';
|
import {SkeletonNewsItemComponent} from './item/skeleton-news-item.component';
|
||||||
import {ChipFilterComponent} from '../data/chips/filter/chip-filter.component';
|
import {ChipFilterComponent} from '../data/chips/filter/chip-filter.component';
|
||||||
import {SettingsModule} from '../settings/settings.module';
|
import {SettingsModule} from '../settings/settings.module';
|
||||||
import {NewsSettingsFilterComponent} from './elements/news-filter-settings/news-settings-filter.component';
|
|
||||||
import {UtilModule} from '../../util/util.module';
|
import {UtilModule} from '../../util/util.module';
|
||||||
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
||||||
|
|
||||||
@@ -36,13 +34,7 @@ const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}];
|
|||||||
* News Module
|
* News Module
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [NewsPageComponent, SkeletonNewsItemComponent, NewsItemComponent, ChipFilterComponent],
|
||||||
NewsPageComponent,
|
|
||||||
SkeletonNewsItemComponent,
|
|
||||||
NewsItemComponent,
|
|
||||||
ChipFilterComponent,
|
|
||||||
NewsSettingsFilterComponent,
|
|
||||||
],
|
|
||||||
imports: [
|
imports: [
|
||||||
IonicModule.forRoot(),
|
IonicModule.forRoot(),
|
||||||
ThingTranslateModule.forChild(),
|
ThingTranslateModule.forChild(),
|
||||||
@@ -56,7 +48,6 @@ const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}];
|
|||||||
SettingsModule,
|
SettingsModule,
|
||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [SettingsProvider],
|
|
||||||
exports: [NewsItemComponent],
|
exports: [NewsItemComponent],
|
||||||
})
|
})
|
||||||
export class NewsModule {}
|
export class NewsModule {}
|
||||||
|
|||||||
@@ -19,17 +19,18 @@ import {
|
|||||||
SCSearchBooleanFilter,
|
SCSearchBooleanFilter,
|
||||||
SCSearchFilter,
|
SCSearchFilter,
|
||||||
SCSearchQuery,
|
SCSearchQuery,
|
||||||
SCSearchValueFilter,
|
|
||||||
SCSetting,
|
|
||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import {DataProvider} from '../data/data.provider';
|
import {DataProvider} from '../data/data.provider';
|
||||||
import {
|
|
||||||
newsFilterSettingsCategory,
|
|
||||||
newsFilterSettingsFieldsMapping,
|
|
||||||
NewsFilterSettingsNames,
|
|
||||||
} from './news-filter-settings';
|
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
import {SettingsProvider} from '../settings/settings.provider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mapping between settings and corresponding data fields for building a value filter
|
||||||
|
*/
|
||||||
|
const newsFilterSettingsFieldsMapping = [
|
||||||
|
['language', 'inLanguage'],
|
||||||
|
['group', 'audiences'],
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for providing news messages
|
* Service for providing news messages
|
||||||
*/
|
*/
|
||||||
@@ -42,28 +43,22 @@ export class NewsProvider {
|
|||||||
private settingsProvider: SettingsProvider,
|
private settingsProvider: SettingsProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getCurrentSettings(): Promise<SCSetting[]> {
|
/**
|
||||||
const settings: SCSetting[] = [];
|
* Gets the news filter based on user group and language settings
|
||||||
for (const settingName of Object.keys(newsFilterSettingsFieldsMapping) as NewsFilterSettingsNames[]) {
|
*/
|
||||||
settings.push(await this.settingsProvider.getSetting(newsFilterSettingsCategory, settingName));
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCurrentFilters(): Promise<SCSearchFilter[]> {
|
async getCurrentFilters(): Promise<SCSearchFilter[]> {
|
||||||
const settings = await this.getCurrentSettings();
|
return Promise.all(
|
||||||
const filtersMap = new Map<NewsFilterSettingsNames, SCSearchValueFilter>();
|
newsFilterSettingsFieldsMapping.map(
|
||||||
for (const setting of settings) {
|
async ([setting, field]) =>
|
||||||
filtersMap.set(
|
({
|
||||||
setting.name as NewsFilterSettingsNames,
|
type: 'value',
|
||||||
DataProvider.createValueFilter(
|
arguments: {
|
||||||
newsFilterSettingsFieldsMapping[setting.name as NewsFilterSettingsNames],
|
field,
|
||||||
setting.value as string,
|
value: await this.settingsProvider.getSetting('profile', setting),
|
||||||
),
|
},
|
||||||
);
|
}) satisfies SCSearchFilter,
|
||||||
}
|
),
|
||||||
|
);
|
||||||
return [...filtersMap.values()];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {IonRefresher} from '@ionic/angular';
|
import {IonRefresher} from '@ionic/angular';
|
||||||
import {SCMessage, SCSearchFilter, SCSearchValueFilter, SCSetting} from '@openstapps/core';
|
import {SCMessage} from '@openstapps/core';
|
||||||
import {NewsProvider} from '../news.provider';
|
import {NewsProvider} from '../news.provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +25,7 @@ import {NewsProvider} from '../news.provider';
|
|||||||
templateUrl: 'news-page.html',
|
templateUrl: 'news-page.html',
|
||||||
styleUrls: ['news-page.scss'],
|
styleUrls: ['news-page.scss'],
|
||||||
})
|
})
|
||||||
export class NewsPageComponent implements OnInit {
|
export class NewsPageComponent {
|
||||||
/**
|
/**
|
||||||
* Thing counter to start query the next page from
|
* Thing counter to start query the next page from
|
||||||
*/
|
*/
|
||||||
@@ -51,24 +51,20 @@ export class NewsPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
elementSize = [300, 300];
|
elementSize = [300, 300];
|
||||||
|
|
||||||
/**
|
constructor(private newsProvider: NewsProvider) {
|
||||||
* Relevant settings
|
this.fetchNews();
|
||||||
*/
|
}
|
||||||
settings: SCSetting[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Active filters
|
|
||||||
*/
|
|
||||||
filters: SCSearchFilter[];
|
|
||||||
|
|
||||||
constructor(private newsProvider: NewsProvider) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch news from the backend
|
* Fetch news from the backend
|
||||||
*/
|
*/
|
||||||
async fetchNews() {
|
async fetchNews() {
|
||||||
this.from = this.pageSize;
|
this.from = this.pageSize;
|
||||||
this.news = await this.newsProvider.getList(this.pageSize, 0, [...this.filters]);
|
this.news = await this.newsProvider.getList(
|
||||||
|
this.pageSize,
|
||||||
|
0,
|
||||||
|
await this.newsProvider.getCurrentFilters(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,7 +73,11 @@ export class NewsPageComponent implements OnInit {
|
|||||||
async loadMore(infiniteScrollElement?: HTMLIonInfiniteScrollElement, more = this.pageSize): Promise<void> {
|
async loadMore(infiniteScrollElement?: HTMLIonInfiniteScrollElement, more = this.pageSize): Promise<void> {
|
||||||
const from = this.from;
|
const from = this.from;
|
||||||
this.from += more;
|
this.from += more;
|
||||||
const fetchedNews = await this.newsProvider.getList(more, from, [...this.filters]);
|
const fetchedNews = await this.newsProvider.getList(
|
||||||
|
more,
|
||||||
|
from,
|
||||||
|
await this.newsProvider.getCurrentFilters(),
|
||||||
|
);
|
||||||
|
|
||||||
this.news = [...this.news, ...fetchedNews];
|
this.news = [...this.news, ...fetchedNews];
|
||||||
await infiniteScrollElement?.complete();
|
await infiniteScrollElement?.complete();
|
||||||
@@ -96,13 +96,6 @@ export class NewsPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the local variables on component initialization
|
|
||||||
*/
|
|
||||||
async ngOnInit() {
|
|
||||||
this.settings = await this.newsProvider.getCurrentSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the shown list
|
* Updates the shown list
|
||||||
* @param refresher Refresher component that triggers the update
|
* @param refresher Refresher component that triggers the update
|
||||||
@@ -116,13 +109,4 @@ export class NewsPageComponent implements OnInit {
|
|||||||
await refresher.complete();
|
await refresher.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Executed when filters have been changed
|
|
||||||
* @param filters Current filters to be used
|
|
||||||
*/
|
|
||||||
toggleFilter(filters: SCSearchValueFilter[]) {
|
|
||||||
this.filters = filters;
|
|
||||||
void this.fetchNews();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,17 +32,6 @@
|
|||||||
>
|
>
|
||||||
</ion-refresher-content>
|
</ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
<ion-grid>
|
|
||||||
<ion-row>
|
|
||||||
<ion-col size="12">
|
|
||||||
<stapps-news-settings-filter
|
|
||||||
*ngIf="settings"
|
|
||||||
[settings]="settings"
|
|
||||||
(filtersChanged)="toggleFilter($event)"
|
|
||||||
></stapps-news-settings-filter>
|
|
||||||
</ion-col>
|
|
||||||
</ion-row>
|
|
||||||
</ion-grid>
|
|
||||||
<div class="news-grid">
|
<div class="news-grid">
|
||||||
<ng-container *ngIf="!news">
|
<ng-container *ngIf="!news">
|
||||||
<stapps-skeleton-news-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-news-item>
|
<stapps-skeleton-news-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-news-item>
|
||||||
|
|||||||
@@ -14,8 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
import {Component, Input} from '@angular/core';
|
import {Component, Input} from '@angular/core';
|
||||||
import {AlertController} from '@ionic/angular';
|
import {AlertController} from '@ionic/angular';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
||||||
import {SCLanguageCode, SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
|
||||||
import {SettingsProvider} from '../settings.provider';
|
import {SettingsProvider} from '../settings.provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,23 +41,10 @@ export class SettingsItemComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() setting: SCSetting;
|
@Input() setting: SCSetting;
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param alertCtrl AlertController
|
|
||||||
* @param translateService TranslateService
|
|
||||||
* @param settingsProvider SettingProvider
|
|
||||||
*/
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
private readonly translateService: TranslateService,
|
|
||||||
private readonly settingsProvider: SettingsProvider,
|
private readonly settingsProvider: SettingsProvider,
|
||||||
) {
|
) {}
|
||||||
translateService.onLangChange.subscribe((_event: LangChangeEvent) => {
|
|
||||||
this.isVisible = false;
|
|
||||||
// TODO: Issue #53 check workaround for selected 'select option' not updating translation
|
|
||||||
setTimeout(() => (this.isVisible = true));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows alert with given title and message and a 'ok' button
|
* Shows alert with given title and message and a 'ok' button
|
||||||
@@ -82,14 +68,6 @@ export class SettingsItemComponent {
|
|||||||
this.setting.value !== undefined &&
|
this.setting.value !== undefined &&
|
||||||
SettingsProvider.validateValue(this.setting, this.setting.value)
|
SettingsProvider.validateValue(this.setting, this.setting.value)
|
||||||
) {
|
) {
|
||||||
// handle general settings, with special actions
|
|
||||||
switch (this.setting.name) {
|
|
||||||
case 'language': {
|
|
||||||
this.translateService.use(this.setting.value as SCLanguageCode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
await this.settingsProvider.setSettingValue(
|
await this.settingsProvider.setSettingValue(
|
||||||
this.setting.categories[0],
|
this.setting.categories[0],
|
||||||
this.setting.name,
|
this.setting.name,
|
||||||
@@ -97,7 +75,7 @@ export class SettingsItemComponent {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// reset setting
|
// reset setting
|
||||||
this.setting.value = (await this.settingsProvider.getValue(
|
this.setting.value = (await this.settingsProvider.getSetting(
|
||||||
this.setting.categories[0],
|
this.setting.categories[0],
|
||||||
this.setting.name,
|
this.setting.name,
|
||||||
)) as SCSettingValue | SCSettingValues;
|
)) as SCSettingValue | SCSettingValues;
|
||||||
|
|||||||
@@ -47,14 +47,6 @@ export class SettingsPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
settingsCache: SettingsCache;
|
settingsCache: SettingsCache;
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param alertController AlertController
|
|
||||||
* @param settingsProvider SettingsProvider
|
|
||||||
* @param toastController ToastController
|
|
||||||
* @param translateService TranslateService
|
|
||||||
* @param changeDetectorRef ChangeDetectorRef
|
|
||||||
*/
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly alertController: AlertController,
|
private readonly alertController: AlertController,
|
||||||
private readonly settingsProvider: SettingsProvider,
|
private readonly settingsProvider: SettingsProvider,
|
||||||
|
|||||||
@@ -26,20 +26,6 @@
|
|||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<ng-container *ngFor="let categoryKey of categoriesOrder">
|
<ng-container *ngFor="let categoryKey of categoriesOrder">
|
||||||
<ion-list *ngIf="objectKeys(settingsCache).includes(categoryKey)">
|
<ion-list *ngIf="objectKeys(settingsCache).includes(categoryKey)">
|
||||||
<!-- <ion-item-divider>
|
|
||||||
<h2>
|
|
||||||
{{
|
|
||||||
'categories[0]'
|
|
||||||
| thingTranslate
|
|
||||||
: $any(
|
|
||||||
settingsCache[categoryKey]?.settings[
|
|
||||||
objectKeys(settingsCache[categoryKey]?.settings)[0]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
| titlecase
|
|
||||||
}}
|
|
||||||
</h2>
|
|
||||||
</ion-item-divider> -->
|
|
||||||
<stapps-settings-item
|
<stapps-settings-item
|
||||||
*ngFor="let settingKeys of objectKeys(settingsCache[categoryKey].settings)"
|
*ngFor="let settingKeys of objectKeys(settingsCache[categoryKey].settings)"
|
||||||
[setting]="settingsCache[categoryKey].settings[settingKeys]"
|
[setting]="settingsCache[categoryKey].settings[settingKeys]"
|
||||||
@@ -49,7 +35,7 @@
|
|||||||
|
|
||||||
<calendar-sync-settings></calendar-sync-settings>
|
<calendar-sync-settings></calendar-sync-settings>
|
||||||
|
|
||||||
<ion-button expand="block" (click)="presentResetAlert()" fill="outline">
|
<ion-button expand="block" (click)="presentResetAlert()" fill="outline" color="danger">
|
||||||
{{ 'settings.resetSettings' | translate }}
|
{{ 'settings.resetSettings' | translate }}
|
||||||
<ion-icon slot="start" name="device_reset"></ion-icon>
|
<ion-icon slot="start" name="device_reset"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|||||||
@@ -12,11 +12,8 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Pipe, PipeTransform} from '@angular/core';
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
|
||||||
import {SCSetting} from '@openstapps/core';
|
import {SCSetting} from '@openstapps/core';
|
||||||
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
|
||||||
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,14 +24,10 @@ import {ThingTranslateService} from '../../translation/thing-translate.service';
|
|||||||
pure: true,
|
pure: true,
|
||||||
})
|
})
|
||||||
export class SettingTranslatePipe implements PipeTransform {
|
export class SettingTranslatePipe implements PipeTransform {
|
||||||
constructor(
|
constructor(private readonly thingTranslate: ThingTranslateService) {}
|
||||||
private readonly translate: TranslateService,
|
|
||||||
private readonly thingTranslate: ThingTranslateService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
transform(setting: SCSetting): string | undefined {
|
transform(setting: SCSetting) {
|
||||||
const thingTranslatePipe = new ThingTranslatePipe(this.translate, this.thingTranslate);
|
const translatedSettingValues = this.thingTranslate.get(setting, 'values') as string;
|
||||||
const translatedSettingValues = thingTranslatePipe.transform('values', setting);
|
|
||||||
|
|
||||||
return translatedSettingValues
|
return translatedSettingValues
|
||||||
? String(translatedSettingValues[setting.values?.indexOf(setting.value as string) as number])
|
? String(translatedSettingValues[setting.values?.indexOf(setting.value as string) as number])
|
||||||
|
|||||||
@@ -18,13 +18,10 @@ import {FormsModule} from '@angular/forms';
|
|||||||
import {RouterModule, Routes} from '@angular/router';
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
import {IonicModule} from '@ionic/angular';
|
import {IonicModule} from '@ionic/angular';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
|
||||||
import {SettingsItemComponent} from './item/settings-item.component';
|
import {SettingsItemComponent} from './item/settings-item.component';
|
||||||
import {SettingsPageComponent} from './page/settings-page.component';
|
import {SettingsPageComponent} from './page/settings-page.component';
|
||||||
import {SettingTranslatePipe} from './setting-translate.pipe';
|
import {SettingTranslatePipe} from './setting-translate.pipe';
|
||||||
import {SettingsProvider} from './settings.provider';
|
|
||||||
import {CalendarSyncSettingsComponent} from './page/calendar-sync-settings.component';
|
import {CalendarSyncSettingsComponent} from './page/calendar-sync-settings.component';
|
||||||
import {ScheduleProvider} from '../calendar/schedule.provider';
|
import {ScheduleProvider} from '../calendar/schedule.provider';
|
||||||
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
||||||
@@ -60,13 +57,6 @@ const settingsRoutes: Routes = [{path: 'settings', component: SettingsPageCompon
|
|||||||
RouterModule.forChild(settingsRoutes),
|
RouterModule.forChild(settingsRoutes),
|
||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [ScheduleSyncService, CalendarService, ScheduleProvider, ThingTranslatePipe],
|
||||||
ScheduleSyncService,
|
|
||||||
ConfigProvider,
|
|
||||||
SettingsProvider,
|
|
||||||
CalendarService,
|
|
||||||
ScheduleProvider,
|
|
||||||
ThingTranslatePipe,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class SettingsModule {}
|
export class SettingsModule {}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
||||||
import deepMerge from 'deepmerge';
|
import deepMerge from 'deepmerge';
|
||||||
import {Subject} from 'rxjs';
|
import {BehaviorSubject, Subject} from 'rxjs';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
|
|
||||||
@@ -89,7 +89,9 @@ export interface SettingsAction {
|
|||||||
/**
|
/**
|
||||||
* Provider for app settings
|
* Provider for app settings
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
export class SettingsProvider {
|
export class SettingsProvider {
|
||||||
/**
|
/**
|
||||||
* Source of settings actions
|
* Source of settings actions
|
||||||
@@ -103,16 +105,16 @@ export class SettingsProvider {
|
|||||||
*/
|
*/
|
||||||
categoriesOrder: string[];
|
categoriesOrder: string[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings actions observable
|
|
||||||
*/
|
|
||||||
settingsActionChanged$ = this.settingsActionSource.asObservable();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache for the imported settings
|
* Cache for the imported settings
|
||||||
*/
|
*/
|
||||||
settingsCache: SettingsCache;
|
settingsCache: SettingsCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the page needs a reload
|
||||||
|
*/
|
||||||
|
needsReload$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if all given values are valid to possible values in given settingInput
|
* Return true if all given values are valid to possible values in given settingInput
|
||||||
* @param possibleValues Possible values
|
* @param possibleValues Possible values
|
||||||
@@ -148,9 +150,7 @@ export class SettingsProvider {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return Array.isArray(possibleValues) && possibleValues.includes(enteredValue);
|
||||||
possibleValues !== undefined && Array.isArray(possibleValues) && possibleValues.includes(enteredValue)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,7 +206,7 @@ export class SettingsProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an Setting to the Cache if not exist and set undefined value to defaultValue
|
* Add a Setting to the Cache if not exist and set undefined value to defaultValue
|
||||||
* @param setting Setting with categories, defaultValue, name, input type and valid values
|
* @param setting Setting with categories, defaultValue, name, input type and valid values
|
||||||
*/
|
*/
|
||||||
private addSetting(setting: SCSetting): void {
|
private addSetting(setting: SCSetting): void {
|
||||||
@@ -281,34 +281,20 @@ export class SettingsProvider {
|
|||||||
return this.categoriesOrder;
|
return this.categoriesOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns copy of a setting if exist
|
|
||||||
* @param category the category of requested setting
|
|
||||||
* @param name the name of requested setting
|
|
||||||
* @throws Exception if setting is not provided
|
|
||||||
*/
|
|
||||||
public async getSetting(category: string, name: string): Promise<SCSetting> {
|
|
||||||
await this.init();
|
|
||||||
if (this.settingExists(category, name)) {
|
|
||||||
// return a copy of the settings
|
|
||||||
return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name]));
|
|
||||||
}
|
|
||||||
throw new Error(`Setting "${name}" not provided`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns copy of a settings value if exist
|
* Returns copy of a settings value if exist
|
||||||
* @param category the category of requested setting
|
* @param category the category of requested setting
|
||||||
* @param name the name of requested setting
|
* @param name the name of requested setting
|
||||||
* @throws Exception if setting is not provided
|
* @throws Exception if setting is not provided
|
||||||
*/
|
*/
|
||||||
public async getValue(category: string, name: string): Promise<SCSettingValue | SCSettingValues> {
|
public async getSetting<T extends SCSettingValue | SCSettingValues>(
|
||||||
await this.init();
|
category: 'profile' | string,
|
||||||
if (this.settingExists(category, name)) {
|
name: string,
|
||||||
// return a copy of the settings value
|
): Promise<T> {
|
||||||
return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name].value));
|
const settings = await this.storage.get<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES);
|
||||||
}
|
const value = settings[category]?.[name];
|
||||||
throw new Error(`Setting "${name}" not provided`);
|
if (!value) throw new Error(`Setting "${name}" not provided`);
|
||||||
|
return value as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -319,8 +305,8 @@ export class SettingsProvider {
|
|||||||
this.needsInit = false;
|
this.needsInit = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings: SCSetting[] = this.configProvider.getValue('settings') as SCSetting[];
|
const settings: SCSetting[] = this.configProvider.config.app.settings;
|
||||||
for (const setting of settings) this.addSetting(setting);
|
for (const setting of settings) this.addSetting(JSON.parse(JSON.stringify(setting)));
|
||||||
|
|
||||||
for (const category of Object.keys(this.settingsCache)) {
|
for (const category of Object.keys(this.settingsCache)) {
|
||||||
if (!this.categoriesOrder.includes(category)) {
|
if (!this.categoriesOrder.includes(category)) {
|
||||||
@@ -347,7 +333,6 @@ export class SettingsProvider {
|
|||||||
: this.settingsCache[categoryKey].settings[settingKey].defaultValue;
|
: this.settingsCache[categoryKey].settings[settingKey].defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.saveSettingValues();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +382,9 @@ export class SettingsProvider {
|
|||||||
this.getSettingValuesFromCache(),
|
this.getSettingValuesFromCache(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.needsReload$.next(true);
|
||||||
|
this.needsReload$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 StApps
|
* Copyright (C) 2023 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
* Software Foundation, version 3.
|
* Software Foundation, version 3.
|
||||||
@@ -12,16 +12,15 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {NgModule} from '@angular/core';
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
import {DataModule} from '../data/data.module';
|
|
||||||
import {StorageModule} from '../storage/storage.module';
|
|
||||||
import {ConfigProvider} from './config.provider';
|
|
||||||
|
|
||||||
/**
|
@Injectable()
|
||||||
* TODO
|
@Pipe({
|
||||||
*/
|
name: 'entries',
|
||||||
@NgModule({
|
pure: true,
|
||||||
imports: [StorageModule, DataModule],
|
|
||||||
providers: [ConfigProvider],
|
|
||||||
})
|
})
|
||||||
export class ConfigModule {}
|
export class EntriesPipe implements PipeTransform {
|
||||||
|
transform<T>(value: Record<string | number | symbol, T>): T[] {
|
||||||
|
return Object.values(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2021 StApps
|
* Copyright (C) 2023 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
* Software Foundation, version 3.
|
* Software Foundation, version 3.
|
||||||
@@ -12,21 +12,23 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {SCSettingCategories, SCThingsField} from '@openstapps/core';
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
/**
|
|
||||||
* Category of settings to use for news filter
|
@Injectable()
|
||||||
*/
|
@Pipe({
|
||||||
export const newsFilterSettingsCategory: SCSettingCategories = 'profile';
|
name: 'join',
|
||||||
/**
|
pure: true,
|
||||||
* Settings to use for news filter
|
})
|
||||||
*/
|
export class ArrayJoinPipe implements PipeTransform {
|
||||||
export type NewsFilterSettingsNames = 'language' | 'group';
|
transform(anArray: unknown[] | unknown, separator: string | unknown): string {
|
||||||
/**
|
if (typeof separator !== 'string' || separator.length <= 0) {
|
||||||
* The mapping between settings and corresponding data fields for building a value filter
|
return '';
|
||||||
*/
|
}
|
||||||
export const newsFilterSettingsFieldsMapping: {
|
|
||||||
[key in NewsFilterSettingsNames]: SCThingsField;
|
if (!Array.isArray(anArray)) {
|
||||||
} = {
|
throw new SyntaxError(`Wrong parameter in ArrayJoinPipe. Expected a valid Array, received: ${anArray}`);
|
||||||
language: 'inLanguage',
|
}
|
||||||
group: 'audiences',
|
|
||||||
};
|
return anArray.join(separator);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,416 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2023 StApps
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, version 3.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
|
||||||
import moment from 'moment';
|
|
||||||
import {Subscription} from 'rxjs';
|
|
||||||
import {logger} from '../_helpers/ts-logger';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'join',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class ArrayJoinPipe implements PipeTransform {
|
|
||||||
value = '';
|
|
||||||
|
|
||||||
transform(anArray: unknown[] | unknown, separator: string | unknown): string {
|
|
||||||
if (typeof separator !== 'string' || separator.length <= 0) {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(anArray)) {
|
|
||||||
throw new SyntaxError(`Wrong parameter in ArrayJoinPipe. Expected a valid Array, received: ${anArray}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = anArray.join(separator);
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'entries',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class EntriesPipe implements PipeTransform {
|
|
||||||
transform<T>(value: Record<string | number | symbol, T>): T[] {
|
|
||||||
return Object.values(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'toUnix',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class ToUnixPipe implements PipeTransform {
|
|
||||||
transform(value: string | number | Date | null | undefined): number {
|
|
||||||
return (value instanceof Date ? value : new Date(value ?? 0)).valueOf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'sentencecase',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class SentenceCasePipe implements PipeTransform {
|
|
||||||
value = '';
|
|
||||||
|
|
||||||
transform(aString: string | unknown): string {
|
|
||||||
if (typeof aString !== 'string') {
|
|
||||||
throw new SyntaxError(
|
|
||||||
`Wrong parameter in StringSplitPipe. Expected a valid String, received: ${aString}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = aString.slice(0, 1).toUpperCase() + aString.slice(1);
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'split',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class StringSplitPipe implements PipeTransform {
|
|
||||||
value = new Array<unknown>();
|
|
||||||
|
|
||||||
transform(aString: string | unknown, splitter: string | unknown): unknown[] {
|
|
||||||
if (typeof splitter !== 'string' || splitter.length <= 0) {
|
|
||||||
return this.value as never;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof aString !== 'string') {
|
|
||||||
throw new SyntaxError(
|
|
||||||
`Wrong parameter in StringSplitPipe. Expected a valid String, received: ${aString}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = aString.split(splitter);
|
|
||||||
|
|
||||||
return this.value as never;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'durationLocalized',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class DurationLocalizedPipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
frequencyPrefixes: {[iso6391Code: string]: string} = {
|
|
||||||
de: 'alle',
|
|
||||||
en: 'every',
|
|
||||||
es: 'cada',
|
|
||||||
pt: 'a cada',
|
|
||||||
fr: 'tous les',
|
|
||||||
cn: '每',
|
|
||||||
ru: 'kаждые',
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value An ISO 8601 duration string
|
|
||||||
* @param isFrequency Boolean indicating if this duration is to be interpreted as repeat frequency
|
|
||||||
*/
|
|
||||||
transform(value: string | unknown, isFrequency = false): string {
|
|
||||||
this.updateValue(value, isFrequency);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value, isFrequency);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: string | unknown, isFrequency = false): void {
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
logger.warn(`durationLocalized pipe unable to parse input: ${value}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFrequency) {
|
|
||||||
const fequencyPrefix = Object.keys(this.frequencyPrefixes).filter(element =>
|
|
||||||
this.locale.includes(element),
|
|
||||||
);
|
|
||||||
this.value = [
|
|
||||||
fequencyPrefix.length > 0 ? this.frequencyPrefixes[fequencyPrefix[0]] : this.frequencyPrefixes.en,
|
|
||||||
moment.duration(value).humanize(),
|
|
||||||
].join(' ');
|
|
||||||
} else {
|
|
||||||
this.value = moment.duration(value).humanize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'metersLocalized',
|
|
||||||
pure: false,
|
|
||||||
})
|
|
||||||
export class MetersLocalizedPipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value = '';
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
transform(value: string | number | unknown): string {
|
|
||||||
this.updateValue(value);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: string | number | unknown) {
|
|
||||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
||||||
logger.warn(`metersLocalized pipe unable to parse input: ${value}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const imperialLocale = ['US', 'UK', 'LR', 'MM'].some(term => this.locale.includes(term));
|
|
||||||
const meters = typeof value === 'string' ? Number.parseFloat(value) : (value as number);
|
|
||||||
|
|
||||||
if (imperialLocale) {
|
|
||||||
const yards = meters * 1.0936;
|
|
||||||
const options = {
|
|
||||||
style: 'unit',
|
|
||||||
unit: yards >= 1760 ? 'mile' : 'yard',
|
|
||||||
maximumFractionDigits: yards >= 1760 ? 1 : 0,
|
|
||||||
} as unknown as Intl.NumberFormatOptions;
|
|
||||||
this.value = new Intl.NumberFormat(this.locale, options).format(yards >= 1760 ? yards / 1760 : yards);
|
|
||||||
} else {
|
|
||||||
const options = {
|
|
||||||
style: 'unit',
|
|
||||||
unit: meters >= 1000 ? 'kilometer' : 'meter',
|
|
||||||
maximumFractionDigits: meters >= 1000 ? 1 : 0,
|
|
||||||
} as unknown as Intl.NumberFormatOptions;
|
|
||||||
this.value = new Intl.NumberFormat(this.locale, options).format(
|
|
||||||
meters >= 1000 ? meters / 1000 : meters,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'isNaN',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class IsNaNPipe implements PipeTransform {
|
|
||||||
transform(value: unknown): boolean {
|
|
||||||
return Number.isNaN(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'isNumeric',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class IsNumericPipe implements PipeTransform {
|
|
||||||
transform(value: unknown): boolean {
|
|
||||||
return !Number.isNaN(
|
|
||||||
typeof value === 'number' ? value : typeof value === 'string' ? Number.parseFloat(value) : Number.NaN,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'numberLocalized',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class NumberLocalizedPipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value The number to be formatted
|
|
||||||
* @param formatOptions Formatting options to include.
|
|
||||||
* As specified by Intl.NumberFormatOptions as comma seperated key:value pairs.
|
|
||||||
*/
|
|
||||||
transform(value: string | number | unknown, formatOptions?: string): string {
|
|
||||||
this.updateValue(value, formatOptions);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value, formatOptions);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: string | number | unknown, formatOptions?: string): void {
|
|
||||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
||||||
logger.warn(`numberLocalized pipe unable to parse input: ${value}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const options = formatOptions
|
|
||||||
?.split(',')
|
|
||||||
.map(element => element.split(':'))
|
|
||||||
.reduce(
|
|
||||||
(accumulator, [key, value_]) => ({
|
|
||||||
...accumulator,
|
|
||||||
[key.trim()]: value_.trim(),
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
) as Intl.NumberFormatOptions;
|
|
||||||
const float = typeof value === 'string' ? Number.parseFloat(value) : (value as number);
|
|
||||||
this.value = new Intl.NumberFormat(this.locale, options).format(float);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'dateFormat',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class DateLocalizedFormatPipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value The date to be formatted
|
|
||||||
* @param formatOptions Dateformat options to include.
|
|
||||||
* As specified by Intl.DateTimeFormatOptions as comma seperated key:value pairs
|
|
||||||
* Default is year,month,day,hour and minute in numeric representation e.g. (en-US) "8/6/2021, 10:35"
|
|
||||||
*/
|
|
||||||
transform(value: string | unknown, formatOptions?: string): string {
|
|
||||||
this.updateValue(value, formatOptions);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value, formatOptions);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: string | Date | unknown, formatOptions?: string): void {
|
|
||||||
if (typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object Date]') {
|
|
||||||
logger.warn(`dateFormat pipe unable to parse input: ${value}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const options = formatOptions
|
|
||||||
?.split(',')
|
|
||||||
.map(element => element.split(':'))
|
|
||||||
.reduce(
|
|
||||||
(accumulator, [key, value_]) => ({
|
|
||||||
...accumulator,
|
|
||||||
[key.trim()]: value_.trim(),
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
) as Intl.DateTimeFormatOptions;
|
|
||||||
const date = typeof value === 'string' ? Date.parse(value) : (value as Date);
|
|
||||||
this.value = new Intl.DateTimeFormat(
|
|
||||||
this.locale,
|
|
||||||
options ?? {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
},
|
|
||||||
).format(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
import {logger} from '../../_helpers/ts-logger';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Pipe({
|
||||||
|
name: 'dateFormat',
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class DateLocalizedFormatPipe implements PipeTransform {
|
||||||
|
constructor(private readonly translate: TranslateService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param value The date to be formatted
|
||||||
|
* @param formatOptions Dateformat options to include.
|
||||||
|
* As specified by Intl.DateTimeFormatOptions as comma seperated key:value pairs
|
||||||
|
* Default is year,month,day,hour and minute in numeric representation e.g. (en-US) "8/6/2021, 10:35"
|
||||||
|
*/
|
||||||
|
transform(value: string | unknown, formatOptions?: string): string {
|
||||||
|
if (typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object Date]') {
|
||||||
|
logger.warn(`dateFormat pipe unable to parse input: ${value}`);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const options = formatOptions
|
||||||
|
?.split(',')
|
||||||
|
.map(element => element.split(':'))
|
||||||
|
.reduce(
|
||||||
|
(accumulator, [key, value_]) => ({
|
||||||
|
...accumulator,
|
||||||
|
[key.trim()]: value_.trim(),
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
) as Intl.DateTimeFormatOptions;
|
||||||
|
const date = typeof value === 'string' ? Date.parse(value) : (value as Date);
|
||||||
|
return new Intl.DateTimeFormat(
|
||||||
|
this.translate.currentLang,
|
||||||
|
options ?? {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
},
|
||||||
|
).format(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user