mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2025-12-17 11:46:19 +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
|
||||
result
|
||||
hsperfdata_root
|
||||
.direnv/
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* This is the database configuration for the technical university of berlin
|
||||
*
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
||||
|
||||
/** @type {import('@openstapps/core').SCLanguageSetting} */
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
/** @type {import('@openstapps/core').SCAppConfigurationMenuCategory[]} */
|
||||
const menus = [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
||||
|
||||
/** @type {import('@openstapps/core').SCUserGroupSetting} */
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import {SCThingType} from '@openstapps/core';
|
||||
|
||||
/** @type {import('@openstapps/core').SCBackendAggregationConfiguration[]} */
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import {
|
||||
month,
|
||||
sommerRange,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import {SCThingType} from '@openstapps/core';
|
||||
import aggregations from './aggregations.js';
|
||||
import boostings from './boostings.js';
|
||||
@@ -17,7 +16,7 @@ export const backend = {
|
||||
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
|
||||
mappingIgnoredTags: ['minlength', 'pattern', 'see', 'tjs-format'],
|
||||
maxMultiSearchRouteQueries: 5,
|
||||
maxRequestBodySize: 2 * 10 ** 6,
|
||||
maxRequestBodySize: 2e6,
|
||||
name: 'Goethe-Universität Frankfurt am Main',
|
||||
namespace: '909a8cbc-8520-456c-b474-ef1525f14209',
|
||||
sortableFields: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import app from './app/index.js';
|
||||
import {backend, internal} from './backend/index.js';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* This is the default configuration for elasticsearch (a database)
|
||||
*
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import {readFile} from 'fs/promises';
|
||||
import {SCAboutPageContentType} from '@openstapps/core';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Generates a range of numbers that represent consecutive calendar months
|
||||
*
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import {readFile, readdir} from 'fs/promises';
|
||||
import url from 'url';
|
||||
import path from 'path';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import {SCAboutPageContentType} from '@openstapps/core';
|
||||
import {markdown} from '../../default/tools/markdown.js';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import {SCAboutPageContentType} from '@openstapps/core';
|
||||
|
||||
/** @type {import('@openstapps/core').SCAboutPage} */
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import about from './about.js';
|
||||
import imprint from './imprint.js';
|
||||
import privacy from './privacy.js';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import {markdown} from '../../default/tools/markdown.js';
|
||||
|
||||
/** @type {import('@openstapps/core').SCAboutPage} */
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
import aboutPages from './about-pages/index.js';
|
||||
import defaultApp from '../default/app/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';
|
||||
|
||||
/** @type {import('@openstapps/core').SCAppVersionInfo[]} */
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {
|
||||
SCBook,
|
||||
SCBulkAddRoute,
|
||||
SCBulkDoneRoute,
|
||||
SCBulkRequest,
|
||||
@@ -23,29 +24,30 @@ import {
|
||||
import {expect} from 'chai';
|
||||
import {bulk, DEFAULT_TEST_TIMEOUT} from '../common.js';
|
||||
import {testApp} from '../tests-setup.js';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {v4} from 'uuid';
|
||||
import bookFile from '@openstapps/core/test/resources/indexable/Book.2.json' assert {type: 'json'};
|
||||
|
||||
const book = JSON.parse(
|
||||
await readFile('node_modules/@openstapps/core/test/resources/indexable/Book.2.json', 'utf8'),
|
||||
).instance;
|
||||
const book = bookFile.instance as SCBook;
|
||||
|
||||
describe('Bulk routes', async function () {
|
||||
// increase timeout for the suite
|
||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||
|
||||
const request: SCBulkRequest = {
|
||||
expiration: bulk.expiration,
|
||||
source: bulk.source,
|
||||
type: bulk.type,
|
||||
};
|
||||
const bulkRoute = new SCBulkRoute();
|
||||
const bulkAddRoute = new SCBulkAddRoute();
|
||||
const bulkDoneRoute = new SCBulkDoneRoute();
|
||||
let request: SCBulkRequest;
|
||||
let bulkRoute: SCBulkRoute;
|
||||
let bulkAddRoute: SCBulkAddRoute;
|
||||
let bulkDoneRoute: SCBulkDoneRoute;
|
||||
|
||||
// afterEach(async function() {
|
||||
// TODO: Delete saved bulks
|
||||
// });
|
||||
before(function () {
|
||||
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 () {
|
||||
const {status, body, error} = await testApp
|
||||
|
||||
@@ -21,7 +21,12 @@ import {expect} from 'chai';
|
||||
describe('Index route', async function () {
|
||||
// increase timeout for the suite
|
||||
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 () {
|
||||
const request: SCIndexRequest = {};
|
||||
|
||||
@@ -30,15 +30,11 @@ import chaiAsPromised from 'chai-as-promised';
|
||||
import {DEFAULT_TEST_TIMEOUT} from '../common.js';
|
||||
import {testApp} from '../tests-setup.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...)
|
||||
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"'"
|
||||
export const registerAddRequest: SCPluginAdd = registerRequest.instance as SCPluginAdd;
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ describe('Create route', async function () {
|
||||
const statusCodeSuccess = 222;
|
||||
const bodySuccess = {foo: true};
|
||||
const sandbox = sinon.createSandbox();
|
||||
const validationError = new SCValidationErrorResponse([]);
|
||||
const internalServerError = new SCInternalServerErrorResponse();
|
||||
let validationError: SCValidationErrorResponse;
|
||||
let internalServerError: SCInternalServerErrorResponse;
|
||||
|
||||
beforeEach(function () {
|
||||
app = express();
|
||||
@@ -64,6 +64,9 @@ describe('Create route', async function () {
|
||||
handler = (_request, _app) => {
|
||||
return Promise.resolve(bodySuccess);
|
||||
};
|
||||
|
||||
validationError = new SCValidationErrorResponse([]);
|
||||
internalServerError = new SCInternalServerErrorResponse();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
|
||||
@@ -29,11 +29,19 @@ import {backendConfig} from '../../src/config.js';
|
||||
describe('Search route', async function () {
|
||||
// increase timeout for the suite
|
||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||
const searchRoute = new SCSearchRoute();
|
||||
const multiSearchRoute = new SCMultiSearchRoute();
|
||||
const syntaxError = new SCSyntaxErrorResponse('Foo Message');
|
||||
const methodNotAllowedError = new SCMethodNotAllowedErrorResponse();
|
||||
const tooManyRequestsError = new SCTooManyRequestsErrorResponse();
|
||||
let searchRoute: SCSearchRoute;
|
||||
let multiSearchRoute: SCMultiSearchRoute;
|
||||
let syntaxError: SCSyntaxErrorResponse;
|
||||
let methodNotAllowedError: SCMethodNotAllowedErrorResponse;
|
||||
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 () {
|
||||
// const expectedParams = JSON.parse(JSON.stringify(defaultParams));
|
||||
|
||||
@@ -13,23 +13,25 @@
|
||||
* 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/>.
|
||||
*/
|
||||
import {SCThingUpdateRoute} from '@openstapps/core';
|
||||
import {SCBook, SCThingUpdateRoute} from '@openstapps/core';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import {bulkStorageMock, DEFAULT_TEST_TIMEOUT} from '../common.js';
|
||||
import {expect, use} from 'chai';
|
||||
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);
|
||||
|
||||
const book = JSON.parse(
|
||||
await readFile('node_modules/@openstapps/core/test/resources/indexable/Book.1.json', 'utf8'),
|
||||
).instance;
|
||||
const book = bookFile.instance as SCBook;
|
||||
|
||||
describe('Thing update route', async function () {
|
||||
// increase timeout for the suite
|
||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||
const thingUpdateRoute = new SCThingUpdateRoute();
|
||||
let thingUpdateRoute: SCThingUpdateRoute;
|
||||
|
||||
before(function () {
|
||||
thingUpdateRoute = new SCThingUpdateRoute();
|
||||
});
|
||||
|
||||
it('should update a thing', async function () {
|
||||
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 fs from 'fs';
|
||||
import {backendConfig} from '../../../src/config.js';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {
|
||||
ACTIVE_INDICES_ALIAS,
|
||||
getIndexUID,
|
||||
@@ -50,6 +49,11 @@ import {
|
||||
} from '../../../src/storage/elasticsearch/util/index.js';
|
||||
import cron from 'node-cron';
|
||||
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);
|
||||
|
||||
@@ -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}};
|
||||
}
|
||||
|
||||
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 () {
|
||||
// increase timeout for the suite
|
||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||
@@ -74,8 +71,15 @@ describe('Elasticsearch', function () {
|
||||
|
||||
before(function () {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('before');
|
||||
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 () {
|
||||
sandbox.restore();
|
||||
@@ -445,7 +449,7 @@ describe('Elasticsearch', function () {
|
||||
_id: '',
|
||||
_index: '',
|
||||
_score: 0,
|
||||
_source: message as SCMessage,
|
||||
_source: message,
|
||||
};
|
||||
sandbox.stub(es.client, 'search').resolves(searchResponse(foundObject));
|
||||
|
||||
@@ -475,7 +479,7 @@ describe('Elasticsearch', function () {
|
||||
const object: SearchHit<SCMessage> = {
|
||||
_id: '',
|
||||
_index: oldIndex,
|
||||
_source: message as SCMessage,
|
||||
_source: message,
|
||||
};
|
||||
sandbox.stub(es.client, 'search').resolves(searchResponse<SCMessage>(object));
|
||||
sandbox.stub(es, 'prepareBulkWrite').resolves(index);
|
||||
@@ -489,7 +493,7 @@ describe('Elasticsearch', function () {
|
||||
sandbox.stub(es.client, 'create').resolves({result: 'not_found'} as CreateResponse);
|
||||
|
||||
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 () {
|
||||
@@ -502,7 +506,7 @@ describe('Elasticsearch', function () {
|
||||
});
|
||||
|
||||
await es.init();
|
||||
await es.post(message as SCMessage, bulk);
|
||||
await es.post(message, bulk);
|
||||
|
||||
expect(createStub.called).to.be.true;
|
||||
expect(caughtParameter.document).to.be.eql({
|
||||
@@ -527,7 +531,7 @@ describe('Elasticsearch', function () {
|
||||
_id: '',
|
||||
_index: getIndex(),
|
||||
_score: 0,
|
||||
_source: message as SCMessage,
|
||||
_source: message,
|
||||
};
|
||||
sandbox.stub(es.client, 'search').resolves(searchResponse());
|
||||
|
||||
@@ -541,7 +545,7 @@ describe('Elasticsearch', function () {
|
||||
_id: '',
|
||||
_index: getIndex(),
|
||||
_score: 0,
|
||||
_source: message as SCMessage,
|
||||
_source: message,
|
||||
};
|
||||
sandbox.stub(es.client, 'search').resolves(searchResponse(object));
|
||||
// @ts-expect-error unused
|
||||
@@ -564,13 +568,13 @@ describe('Elasticsearch', function () {
|
||||
_id: '123',
|
||||
_index: getIndex(),
|
||||
_score: 0,
|
||||
_source: message as SCMessage,
|
||||
_source: message,
|
||||
};
|
||||
const objectBook: SearchHit<SCBook> = {
|
||||
_id: '321',
|
||||
_index: getIndex(),
|
||||
_score: 0,
|
||||
_source: book as SCBook,
|
||||
_source: book,
|
||||
};
|
||||
const fakeEsAggregations = {
|
||||
'@all': {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
const fs = require("fs");
|
||||
const path = require("node:path");
|
||||
const child_process = require("child_process");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
"use strict"
|
||||
|
||||
const rule = require('./copyright-header-rule')
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {
|
||||
root: true,
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
"test": "prettier --config index.js --check \"test/*.js\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "3.1.0"
|
||||
"prettier": "3.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prettier": "3.1.0"
|
||||
"prettier": "3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"isolatedModules": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noImplicitAny": true,
|
||||
|
||||
@@ -1,44 +1,62 @@
|
||||
version: '3.7'
|
||||
|
||||
x-development-variables: &development-variables
|
||||
NODE_ENV: "development"
|
||||
ALLOW_NO_TRANSPORT: "true"
|
||||
|
||||
services:
|
||||
database:
|
||||
image: registry.gitlab.com/openstapps/openstapps/database:2.0.0
|
||||
volumes:
|
||||
- ./database:/usr/share/elasticsearch/data
|
||||
image: registry.gitlab.com/openstapps/openstapps/database:3.0.0
|
||||
# If you need persistence for debugging purposes uncomment the following lines
|
||||
#volumes:
|
||||
# - ./database:/usr/share/elasticsearch/data
|
||||
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
|
||||
|
||||
backend:
|
||||
image: registry.gitlab.com/openstapps/openstapps/backend:3.0.0-next.0
|
||||
image: registry.gitlab.com/openstapps/openstapps/backend:3.1.0
|
||||
environment:
|
||||
<<: *development-variables
|
||||
ES_ADDR: "http://database:9200"
|
||||
NODE_CONFIG_ENV: "elasticsearch"
|
||||
ALLOW_NO_TRANSPORT: "true"
|
||||
NODE_APP_INSTANCE: "f-u"
|
||||
PROMETHEUS_MIDDLEWARE: "false"
|
||||
expose:
|
||||
- 3000
|
||||
ports:
|
||||
- 3000:3000
|
||||
links:
|
||||
- "database"
|
||||
- 127.0.0.1:3000:3000
|
||||
labels:
|
||||
- stapps.version=1.0.0
|
||||
- stapps.version=4.1.0
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
api:
|
||||
image: registry.gitlab.com/openstapps/openstapps/api:3.0.0-next.0
|
||||
links:
|
||||
- "backend"
|
||||
- database
|
||||
|
||||
minimal-connector:
|
||||
image: registry.gitlab.com/openstapps/minimal-connector:core-0.23
|
||||
container_name: minimal-connector-0.23
|
||||
command: ["http://backend:3000", "minimal-connector", "f-u"]
|
||||
# api:
|
||||
# image: registry.gitlab.com/openstapps/openstapps/api:3.0.0
|
||||
# links:
|
||||
# - backend
|
||||
|
||||
app:
|
||||
image: registry.gitlab.com/openstapps/app/executable:core-0.23
|
||||
expose:
|
||||
- 8100
|
||||
ports:
|
||||
- 8100:8100
|
||||
# minimal-connector:
|
||||
# image: registry.gitlab.com/openstapps/minimal-connector:core-0.23
|
||||
# container_name: minimal-connector-0.23
|
||||
# command: ["http://backend:3000", "minimal-connector", "f-u"]
|
||||
|
||||
# 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": {
|
||||
"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": {
|
||||
"locked": {
|
||||
"lastModified": 1701626906,
|
||||
@@ -18,8 +36,24 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"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",
|
||||
|
||||
142
flake.nix
142
flake.nix
@@ -1,77 +1,75 @@
|
||||
{
|
||||
description = "A Nix-flake-based development environment for OpenStApps";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
buildToolsVersion = "30.0.3";
|
||||
overlays = [
|
||||
(final: prev: rec {
|
||||
nodejs = prev.nodejs-18_x;
|
||||
pnpm = prev.nodePackages.pnpm;
|
||||
chrome = prev.google-chrome;
|
||||
firefox = prev.firefox;
|
||||
webkit = prev.epiphany; # Safari-ish browser
|
||||
android = prev.androidenv.composeAndroidPackages {
|
||||
buildToolsVersions = [ "${buildToolsVersion}" ];
|
||||
platformVersions = [ "33" ];
|
||||
};
|
||||
cypress = prev.cypress.overrideAttrs(cyPrev: rec {
|
||||
version = "13.2.0";
|
||||
src = prev.fetchzip {
|
||||
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
|
||||
hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM=";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}: let
|
||||
aapt2buildToolsVersion = "33.0.2";
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
(final: prev: rec {
|
||||
fontMin = prev.python311.withPackages (ps: with ps; [brotli fonttools] ++ (with fonttools.optional-dependencies; [woff]));
|
||||
android = prev.androidenv.composeAndroidPackages {
|
||||
buildToolsVersions = ["30.0.3" aapt2buildToolsVersion];
|
||||
platformVersions = ["33"];
|
||||
};
|
||||
});
|
||||
})
|
||||
];
|
||||
# TODO: aarch64-linux, x68_64-darwin, aarch64-darwin
|
||||
supportedSystems = [ "x86_64-linux" ];
|
||||
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
|
||||
pkgs = import nixpkgs {
|
||||
inherit overlays system;
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
android_sdk.accept_license = true;
|
||||
};
|
||||
cypress = prev.cypress.overrideAttrs (cyPrev: rec {
|
||||
version = "13.2.0";
|
||||
src = prev.fetchzip {
|
||||
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
|
||||
hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM=";
|
||||
};
|
||||
});
|
||||
})
|
||||
];
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
android_sdk.accept_license = true;
|
||||
};
|
||||
});
|
||||
in
|
||||
{
|
||||
devShells = forEachSupportedSystem ({ pkgs }:
|
||||
let
|
||||
python = (pkgs.python311.withPackages(ps: with ps; [ brotli fonttools ] ++ (with fonttools.optional-dependencies; [ ufo lxml unicode woff ])));
|
||||
in
|
||||
{
|
||||
default = (pkgs.buildFHSUserEnv {
|
||||
name = "StApps Dev";
|
||||
targetPkgs = pkgs: with pkgs; [
|
||||
nodejs
|
||||
pnpm
|
||||
python
|
||||
docker
|
||||
# tools
|
||||
curl
|
||||
jq
|
||||
# browsers
|
||||
firefox
|
||||
chrome
|
||||
webkit
|
||||
cypress
|
||||
# android
|
||||
jdk17
|
||||
android.androidsdk
|
||||
musl
|
||||
];
|
||||
runScript = "bash";
|
||||
profile = ''
|
||||
export CYPRESS_INSTALL_BINARY=0
|
||||
export CYPRESS_RUN_BINARY=${pkgs.cypress}/bin/Cypress
|
||||
export ANDROID_SDK_ROOT=${pkgs.android.androidsdk}/libexec/android-sdk
|
||||
export ANDROID_JAVA_HOME=${pkgs.jdk.home}
|
||||
export DOCKER_HOST=unix:///run/user/1000/docker.sock
|
||||
{ dockerd-rootless & } 2>/dev/null
|
||||
'';
|
||||
}).env;
|
||||
});
|
||||
};
|
||||
};
|
||||
androidFhs = pkgs.buildFHSUserEnv {
|
||||
name = "android-env";
|
||||
targetPkgs = pkgs: with pkgs; [];
|
||||
runScript = "bash";
|
||||
profile = ''
|
||||
export ALLOW_NINJA_ENV=true
|
||||
export USE_CCACHE=1
|
||||
export LD_LIBRARY_PATH=/usr/lib:/usr/lib32
|
||||
'';
|
||||
};
|
||||
in {
|
||||
devShell = pkgs.mkShell rec {
|
||||
nativeBuildInputs = [androidFhs];
|
||||
buildInputs = with pkgs; [
|
||||
nodejs-18_x
|
||||
nodePackages.pnpm
|
||||
# tools
|
||||
curl
|
||||
jq
|
||||
fontMin
|
||||
# browsers
|
||||
firefox
|
||||
google-chrome
|
||||
epiphany # Safari-ish browser
|
||||
cypress
|
||||
# android
|
||||
jdk17
|
||||
android.androidsdk
|
||||
musl
|
||||
];
|
||||
ANDROID_JAVA_HOME = "${pkgs.jdk.home}";
|
||||
ANDROID_SDK_ROOT = "${pkgs.android.androidsdk}/libexec/android-sdk";
|
||||
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/${aapt2buildToolsVersion}/aapt2";
|
||||
CYPRESS_INSTALL_BINARY = "0";
|
||||
CYPRESS_RUN_BINARY = "${pkgs.cypress}/bin/Cypress";
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 '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 '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 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins'
|
||||
end
|
||||
|
||||
@@ -36,7 +36,7 @@ PODS:
|
||||
- CordovaPlugins (5.5.0):
|
||||
- CapacitorCordova
|
||||
- SwiftKeychainWrapper (4.0.1)
|
||||
- TransistorsoftCapacitorBackgroundFetch (1.0.2):
|
||||
- TransistorsoftCapacitorBackgroundFetch (5.1.1):
|
||||
- Capacitor
|
||||
|
||||
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`)"
|
||||
- "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`)
|
||||
- "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:
|
||||
trunk:
|
||||
@@ -102,7 +102,7 @@ EXTERNAL SOURCES:
|
||||
CordovaPlugins:
|
||||
:path: "../capacitor-cordova-ios-plugins"
|
||||
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:
|
||||
Capacitor: 57890b363df14d5d2d5d8461aa23e886cb34da2a
|
||||
@@ -124,8 +124,8 @@ SPEC CHECKSUMS:
|
||||
CapacitorSplashScreen: 5fa2ab5e46cf5cc530cf16a51c80c7a986579ccd
|
||||
CordovaPlugins: de5669381702d76ed5b1d442177a6a5fc3252a9d
|
||||
SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c
|
||||
TransistorsoftCapacitorBackgroundFetch: 74ca62dae7ec78639eaf3d0d1e24c595ada213dd
|
||||
TransistorsoftCapacitorBackgroundFetch: ce4b3e01b898cef516e68485d2160a078016ee97
|
||||
|
||||
PODFILE CHECKSUM: 073b899f90bacc5049101cb9c562a168757d554e
|
||||
PODFILE CHECKSUM: 229278f2c257e8ab555325c7115b2e187e8e628d
|
||||
|
||||
COCOAPODS: 1.13.0
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
/*
|
||||
* Copyright (C) 2022 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
"cordova-plugin-calendar": "5.1.6",
|
||||
"date-fns": "2.30.0",
|
||||
"deepmerge": "4.3.1",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"form-data": "4.0.0",
|
||||
"geojson": "0.5.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 {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
|
||||
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
|
||||
import {AppVersionService} from './modules/about/app-version.service';
|
||||
import {SplashScreen} from '@capacitor/splash-screen';
|
||||
import {AppVersionService} from './modules/about/app-version.service';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
})
|
||||
export class AppComponent implements AfterContentInit {
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
pages: Array<{
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
component: unknown;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
title: string;
|
||||
}>;
|
||||
|
||||
@@ -65,7 +53,7 @@ export class AppComponent implements AfterContentInit {
|
||||
void this.initializeApp();
|
||||
}
|
||||
|
||||
async ngAfterContentInit() {
|
||||
ngAfterContentInit() {
|
||||
this.scheduleSyncService.init();
|
||||
void this.scheduleSyncService.enable();
|
||||
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()) {
|
||||
void SplashScreen.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
async initializeApp() {
|
||||
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
|
||||
this.zone.run(() => {
|
||||
|
||||
@@ -25,12 +25,10 @@ import moment from 'moment';
|
||||
import 'moment/min/locales';
|
||||
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
|
||||
import SwiperCore, {FreeMode, Navigation} from 'swiper';
|
||||
|
||||
import {environment} from '../environments/environment';
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
import {AppComponent} from './app.component';
|
||||
import {CatalogModule} from './modules/catalog/catalog.module';
|
||||
import {ConfigModule} from './modules/config/config.module';
|
||||
import {ConfigProvider} from './modules/config/config.provider';
|
||||
import {DashboardModule} from './modules/dashboard/dashboard.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 {ThingTranslateModule} from './translation/thing-translate.module';
|
||||
import {UtilModule} from './util/util.module';
|
||||
import {initLogger} from './_helpers/ts-logger';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {AboutModule} from './modules/about/about.module';
|
||||
import {JobModule} from './modules/jobs/jobs.module';
|
||||
@@ -91,28 +88,25 @@ export function initializerFactory(
|
||||
) {
|
||||
return async () => {
|
||||
try {
|
||||
initLogger(logger);
|
||||
await storageProvider.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 {
|
||||
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 paiaAuthService.init();
|
||||
} catch (error) {
|
||||
@@ -151,11 +145,12 @@ export function createTranslateLoader(http: HttpClient) {
|
||||
BrowserAnimationsModule,
|
||||
CatalogModule,
|
||||
CommonModule,
|
||||
ConfigModule,
|
||||
DashboardModule,
|
||||
DataModule,
|
||||
HebisModule,
|
||||
IonicModule.forRoot(),
|
||||
IonicModule.forRoot({
|
||||
animated: 'Cypress' in window ? false : undefined,
|
||||
}),
|
||||
IonIconModule,
|
||||
JobModule,
|
||||
FavoritesModule,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
|
||||
import {SCAboutPage} from '@openstapps/core';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
import packageJson from '../../../../../package.json';
|
||||
import config from 'capacitor.config';
|
||||
@@ -42,8 +42,7 @@ export class AboutPageComponent implements OnInit {
|
||||
|
||||
async ngOnInit() {
|
||||
const route = this.route.snapshot.url.map(it => it.path).join('/');
|
||||
this.content =
|
||||
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
|
||||
this.content = this.configProvider.config.app.aboutPages[route] ?? {};
|
||||
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 {TranslateModule} from '@ngx-translate/core';
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {AboutPageComponent} from './about-page/about-page.component';
|
||||
import {MarkdownModule} from 'ngx-markdown';
|
||||
import {AboutPageContentComponent} from './about-page/about-page-content.component';
|
||||
@@ -64,6 +63,5 @@ const settingsRoutes: Routes = [
|
||||
ScrollingModule,
|
||||
UtilModule,
|
||||
],
|
||||
providers: [ConfigProvider],
|
||||
})
|
||||
export class AboutModule {}
|
||||
|
||||
@@ -18,12 +18,7 @@ import {IPAIAAuthAction} from './paia/paia-auth-action';
|
||||
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {JSONPath} from 'jsonpath-plus';
|
||||
import {
|
||||
SCAuthorizationProvider,
|
||||
SCAuthorizationProviderType,
|
||||
SCUserConfiguration,
|
||||
SCUserConfigurationMap,
|
||||
} from '@openstapps/core';
|
||||
import {SCAuthorizationProviderType, SCUserConfiguration} from '@openstapps/core';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {DefaultAuthService} from './default-auth.service';
|
||||
@@ -37,8 +32,6 @@ const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthHelperService {
|
||||
userConfigurationMap: SCUserConfigurationMap;
|
||||
|
||||
constructor(
|
||||
private translateService: TranslateService,
|
||||
private configProvider: ConfigProvider,
|
||||
@@ -47,14 +40,7 @@ export class AuthHelperService {
|
||||
private paiaAuth: PAIAAuthService,
|
||||
private browser: SimpleBrowser,
|
||||
private alertController: AlertController,
|
||||
) {
|
||||
this.userConfigurationMap =
|
||||
(
|
||||
this.configProvider.getAnyValue('auth') as {
|
||||
default: SCAuthorizationProvider;
|
||||
}
|
||||
).default?.endpoints.mapping ?? {};
|
||||
}
|
||||
) {}
|
||||
|
||||
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
|
||||
let message: string | undefined;
|
||||
@@ -77,9 +63,10 @@ export class AuthHelperService {
|
||||
name: '',
|
||||
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({
|
||||
path: this.userConfigurationMap[key as keyof SCUserConfiguration] as string,
|
||||
path: mapping[key as keyof SCUserConfiguration] as string,
|
||||
json: userInfo,
|
||||
preventEval: true,
|
||||
})[0];
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* 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 {
|
||||
AuthorizationRequestHandler,
|
||||
AuthorizationServiceConfiguration,
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
} from '@openid/appauth';
|
||||
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
||||
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {AuthService} from './auth.service';
|
||||
@@ -67,12 +65,9 @@ export class DefaultAuthService extends AuthService {
|
||||
}
|
||||
|
||||
setupConfiguration() {
|
||||
const authConfig = this.configProvider.getAnyValue('auth') as {
|
||||
default: SCAuthorizationProvider;
|
||||
};
|
||||
this.authConfig = getClientConfig('default', authConfig);
|
||||
this.authConfig = getClientConfig('default', this.configProvider.config.auth);
|
||||
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
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
AuthorizationError,
|
||||
AuthorizationRequest,
|
||||
@@ -47,7 +46,6 @@ import {PAIAAuthorizationResponse} from './paia-authorization-response';
|
||||
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
|
||||
import {PAIATokenResponse} from './paia-token-response';
|
||||
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
|
||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
|
||||
import {Injectable} from '@angular/core';
|
||||
@@ -154,11 +152,10 @@ export class PAIAAuthService {
|
||||
}
|
||||
|
||||
setupConfiguration() {
|
||||
const authConfig = this.configProvider.getAnyValue('auth') as {
|
||||
paia: SCAuthorizationProvider;
|
||||
};
|
||||
this.authConfig = getClientConfig('paia', authConfig);
|
||||
this.localConfiguration = new AuthorizationServiceConfiguration(getEndpointsConfig('paia', authConfig));
|
||||
this.authConfig = getClientConfig('paia', this.configProvider.config.auth);
|
||||
this.localConfiguration = new AuthorizationServiceConfiguration(
|
||||
getEndpointsConfig('paia', this.configProvider.config.auth),
|
||||
);
|
||||
}
|
||||
|
||||
protected notifyActionListers(action: IPAIAAuthAction) {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* 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 {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ICalEvent} from './ical/ical';
|
||||
@@ -35,14 +34,14 @@ export class CalendarService {
|
||||
|
||||
goToDateClicked = this.goToDate.asObservable();
|
||||
|
||||
calendarName = 'StApps';
|
||||
calendarName: string;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
constructor(
|
||||
readonly calendar: Calendar,
|
||||
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> {
|
||||
|
||||
@@ -20,7 +20,6 @@ import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {MomentModule} from 'ngx-moment';
|
||||
import {DataModule} from '../data/data.module';
|
||||
import {SettingsProvider} from '../settings/settings.provider';
|
||||
import {CatalogComponent} from './catalog.component';
|
||||
import {UtilModule} from '../../util/util.module';
|
||||
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
||||
@@ -46,6 +45,5 @@ const catalogRoutes: Routes = [
|
||||
DataModule,
|
||||
UtilModule,
|
||||
],
|
||||
providers: [SettingsProvider],
|
||||
})
|
||||
export class CatalogModule {}
|
||||
|
||||
@@ -16,12 +16,6 @@ import {TestBed} from '@angular/core/testing';
|
||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
|
||||
import {
|
||||
ConfigFetchError,
|
||||
ConfigInitError,
|
||||
SavedConfigNotAvailable,
|
||||
WrongConfigVersionInStorage,
|
||||
} from './errors';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';
|
||||
|
||||
|
||||
@@ -14,19 +14,14 @@
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Client} from '@openstapps/api';
|
||||
import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core';
|
||||
import {SCIndexResponse} from '@openstapps/core';
|
||||
import packageInfo from '@openstapps/core/package.json';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {environment} from '../../../environments/environment';
|
||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {
|
||||
ConfigFetchError,
|
||||
ConfigInitError,
|
||||
ConfigValueNotAvailable,
|
||||
SavedConfigNotAvailable,
|
||||
WrongConfigVersionInStorage,
|
||||
} from './errors';
|
||||
import equals from 'fast-deep-equal/es6';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
/**
|
||||
* Key to store config in storage module
|
||||
@@ -35,6 +30,17 @@ import {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -50,7 +56,7 @@ export class ConfigProvider {
|
||||
/**
|
||||
* App configuration as IndexResponse
|
||||
*/
|
||||
config: SCIndexResponse;
|
||||
config: Readonly<SCIndexResponse>;
|
||||
|
||||
/**
|
||||
* Version of the @openstapps/core package that app is using
|
||||
@@ -62,6 +68,11 @@ export class ConfigProvider {
|
||||
*/
|
||||
firstSession = true;
|
||||
|
||||
/**
|
||||
* If the config requires an update
|
||||
*/
|
||||
needsUpdate$ = new BehaviorSubject(false);
|
||||
|
||||
/**
|
||||
* Constructor, initialise api client
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws ConfigInitError if no configuration could be loaded.
|
||||
* @throws WrongConfigVersionInStorage if fetch failed and saved config has wrong SCVersion
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
let loadError;
|
||||
let fetchError;
|
||||
// load saved configuration
|
||||
try {
|
||||
this.config = await this.loadLocal();
|
||||
this.firstSession = false;
|
||||
this.logger.log(`initialised configuration from storage`);
|
||||
if (this.config.backend.SCVersion !== this.scVersion) {
|
||||
loadError = new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion);
|
||||
this.config = (await this.storageProvider.has(STORAGE_KEY_CONFIG))
|
||||
? await this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG)
|
||||
: undefined!;
|
||||
this.firstSession = !this.config;
|
||||
|
||||
const updatedConfig = this.client.handshake(this.scVersion).then(async fetchedConfig => {
|
||||
if (!equals(fetchedConfig, this.config)) {
|
||||
await this.storageProvider.put(STORAGE_KEY_CONFIG, fetchedConfig);
|
||||
this.logger.log(`Config updated`);
|
||||
this.needsUpdate$.next(true);
|
||||
this.needsUpdate$.complete();
|
||||
}
|
||||
} catch (error) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
return fetchedConfig;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns saved configuration from StorageModule
|
||||
* @throws SavedConfigNotAvailable if no configuration could be loaded
|
||||
*/
|
||||
async loadLocal(): Promise<SCIndexResponse> {
|
||||
// get local configuration
|
||||
if (await this.storageProvider.has(STORAGE_KEY_CONFIG)) {
|
||||
return this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG);
|
||||
this.config ??= await updatedConfig;
|
||||
this.config = deepFreeze(this.config);
|
||||
|
||||
if (this.config.backend.SCVersion !== this.scVersion) {
|
||||
this.logger.warn(
|
||||
'Incompatible config, expected',
|
||||
this.scVersion,
|
||||
'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 {MomentModule} from 'ngx-moment';
|
||||
import {DataModule} from '../data/data.module';
|
||||
import {SettingsProvider} from '../settings/settings.provider';
|
||||
import {DashboardComponent} from './dashboard.component';
|
||||
import {SearchSectionComponent} from './sections/search-section/search-section.component';
|
||||
import {NewsSectionComponent} from './sections/news-section/news-section.component';
|
||||
@@ -70,6 +69,6 @@ const catalogRoutes: Routes = [
|
||||
NewsModule,
|
||||
JobModule,
|
||||
],
|
||||
providers: [SettingsProvider, TranslatePipe],
|
||||
providers: [TranslatePipe],
|
||||
})
|
||||
export class DashboardModule {}
|
||||
|
||||
@@ -32,7 +32,6 @@ import {ScheduleProvider} from '../calendar/schedule.provider';
|
||||
import {GeoNavigationDirective} from '../map/geo-navigation.directive';
|
||||
import {MapWidgetComponent} from '../map/widget/map-widget.component';
|
||||
import {MenuModule} from '../menu/menu.module';
|
||||
import {SettingsProvider} from '../settings/settings.provider';
|
||||
import {StorageModule} from '../storage/storage.module';
|
||||
import {ActionChipListComponent} from './chips/action-chip-list.component';
|
||||
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
|
||||
@@ -214,7 +213,6 @@ import {ShareButtonComponent} from './elements/share-button.component';
|
||||
StAppsWebHttpClient,
|
||||
CalendarService,
|
||||
RoutingStackService,
|
||||
SettingsProvider,
|
||||
{
|
||||
provide: SimpleBrowser,
|
||||
useFactory: browserFactory,
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {ModalController} from '@ionic/angular';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import {SCLanguageCode, SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
|
||||
import {SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
|
||||
import {DataProvider, DataScope} from '../data.provider';
|
||||
import {FavoritesService} from '../../favorites/favorites.service';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {Network} from '@capacitor/network';
|
||||
import {DataListContext} from '../list/data-list.component';
|
||||
import {lastValueFrom} from 'rxjs';
|
||||
|
||||
export interface ExternalDataLoadEvent {
|
||||
uid: SCUuid;
|
||||
@@ -29,6 +29,13 @@ export interface ExternalDataLoadEvent {
|
||||
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
|
||||
*/
|
||||
@@ -53,11 +60,6 @@ export class DataDetailComponent implements OnInit {
|
||||
|
||||
@Input() autoRouteDataPath = true;
|
||||
|
||||
/**
|
||||
* The language of the item
|
||||
*/
|
||||
language: SCLanguageCode;
|
||||
|
||||
/**
|
||||
* Indicating wether internet connectivity is given or not
|
||||
*/
|
||||
@@ -79,20 +81,12 @@ export class DataDetailComponent implements OnInit {
|
||||
|
||||
@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(
|
||||
protected readonly route: ActivatedRoute,
|
||||
router: Router,
|
||||
private readonly dataProvider: DataProvider,
|
||||
private readonly favoritesService: FavoritesService,
|
||||
readonly modalController: ModalController,
|
||||
translateService: TranslateService,
|
||||
) {
|
||||
this.inputItem = router.getCurrentNavigation()?.extras.state?.item;
|
||||
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
|
||||
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 => {
|
||||
const isConnected = (await Network.getStatus()).connected;
|
||||
@@ -126,13 +116,8 @@ export class DataDetailComponent implements OnInit {
|
||||
)
|
||||
: this.dataProvider.get(uid, DataScope.Remote)));
|
||||
|
||||
this.item = item
|
||||
? // eslint-disable-next-line unicorn/no-null
|
||||
DataDetailComponent.isSCSavableThing(item)
|
||||
? item.data
|
||||
: item
|
||||
: // eslint-disable-next-line unicorn/no-null
|
||||
null;
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
this.item = item ? (isSCSavableThing(item) ? item.data : item) : null;
|
||||
} catch {
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
this.item = null;
|
||||
@@ -144,14 +129,10 @@ export class DataDetailComponent implements OnInit {
|
||||
await this.getItem(uid ?? '', false);
|
||||
// fallback to the saved item (from favorites)
|
||||
if (this.item === null) {
|
||||
this.favoritesService
|
||||
.get(uid)
|
||||
.pipe(take(1))
|
||||
.subscribe(item => {
|
||||
if (item !== undefined) {
|
||||
this.item = item.data;
|
||||
}
|
||||
});
|
||||
const item = await lastValueFrom(this.favoritesService.get(uid).pipe(take(1)));
|
||||
if (item) {
|
||||
this.item = item.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ export class OffersInListComponent {
|
||||
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
|
||||
this._offers = it;
|
||||
this.price = it[0].prices?.default;
|
||||
this.settingsProvider.getSetting('profile', 'group').then(group => {
|
||||
this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never];
|
||||
this.settingsProvider.getSetting<string>('profile', 'group').then(group => {
|
||||
this.price = it[0].prices?.[group.replace(/s$/, '') as never];
|
||||
});
|
||||
|
||||
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 {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
|
||||
import {Capacitor} from '@capacitor/core';
|
||||
import {
|
||||
SCFacet,
|
||||
SCFeatureConfiguration,
|
||||
SCSearchFilter,
|
||||
SCSearchQuery,
|
||||
SCSearchSort,
|
||||
SCThings,
|
||||
} from '@openstapps/core';
|
||||
import {SCFacet, SCSearchFilter, SCSearchQuery, SCSearchSort, SCThings} from '@openstapps/core';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {combineLatest, Subject} from 'rxjs';
|
||||
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
|
||||
@@ -170,9 +163,8 @@ export class SearchPageComponent implements OnInit {
|
||||
private readonly route: ActivatedRoute,
|
||||
protected positionService: PositionService,
|
||||
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.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
|
||||
.itemSelectListener()
|
||||
.pipe(takeUntilDestroyed(this.destroy$))
|
||||
@@ -342,12 +324,8 @@ export class SearchPageComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
try {
|
||||
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
||||
this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
this.isHebisAvailable =
|
||||
this.configProvider.config.app.features.plugins?.['hebis-plugin']?.urlPath !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
SCRatingResponse,
|
||||
SCRatingRoute,
|
||||
SCUserGroup,
|
||||
SCUserGroupSetting,
|
||||
SCUuid,
|
||||
} from '@openstapps/core';
|
||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||
@@ -63,9 +62,7 @@ export class RatingProvider {
|
||||
}
|
||||
|
||||
private get userGroup(): Promise<SCUserGroup> {
|
||||
return this.settingsProvider
|
||||
.getSetting('profile', 'group')
|
||||
.then(it => (it as SCUserGroupSetting).value as SCUserGroup);
|
||||
return this.settingsProvider.getSetting<SCUserGroup>('profile', 'group');
|
||||
}
|
||||
|
||||
private async getStoredRatings(): Promise<RatingStorage> {
|
||||
|
||||
@@ -75,7 +75,7 @@ export class PlaceMensaService {
|
||||
sort: [
|
||||
{
|
||||
arguments: {
|
||||
field: `offers.prices.${(priceGroup.value as string).replace(/s$/, '')}`,
|
||||
field: `offers.prices.${(priceGroup as string).replace(/s$/, '')}`,
|
||||
},
|
||||
order: 'desc',
|
||||
type: 'generic',
|
||||
|
||||
@@ -12,21 +12,13 @@
|
||||
* 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, OnInit} from '@angular/core';
|
||||
import {AlertController, AnimationController} from '@ionic/angular';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
|
||||
import {combineLatest} from 'rxjs';
|
||||
import {SCThingType} from '@openstapps/core';
|
||||
import {FavoritesService} from './favorites.service';
|
||||
import {DataRoutingService} from '../data/data-routing.service';
|
||||
import {ContextMenuService} from '../menu/context/context-menu.service';
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -42,34 +34,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
|
||||
|
||||
showNavigation = false;
|
||||
|
||||
constructor(
|
||||
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,
|
||||
);
|
||||
}
|
||||
private favoritesService = inject(FavoritesService);
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit(false);
|
||||
@@ -96,16 +61,6 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
|
||||
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
|
||||
.itemSelectListener()
|
||||
.pipe(takeUntilDestroyed(this.destroy$))
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from '@openstapps/core';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {DataProvider} from '../data/data.provider';
|
||||
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
@@ -41,11 +40,6 @@ import {debounceTime, map} from 'rxjs/operators';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class FavoritesService {
|
||||
/**
|
||||
* Translation pipe
|
||||
*/
|
||||
thingTranslatePipe: ThingTranslatePipe;
|
||||
|
||||
favorites = new BehaviorSubject<Map<string, SCFavorite>>(new Map<string, SCFavorite>());
|
||||
|
||||
// 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 (
|
||||
new Intl.Collator(this.translate.currentLang).compare(
|
||||
this.thingTranslatePipe.transform(field, a),
|
||||
this.thingTranslatePipe.transform(field, b),
|
||||
this.thingTranslate.get(a, field) as string,
|
||||
this.thingTranslate.get(b, field) as string,
|
||||
) * reverse
|
||||
);
|
||||
});
|
||||
@@ -124,7 +118,6 @@ export class FavoritesService {
|
||||
private readonly translate: TranslateService,
|
||||
private readonly thingTranslate: ThingTranslateService,
|
||||
) {
|
||||
this.thingTranslatePipe = new ThingTranslatePipe(this.translate, this.thingTranslate);
|
||||
void this.emitAll();
|
||||
}
|
||||
|
||||
@@ -185,7 +178,9 @@ export class FavoritesService {
|
||||
const textFilteredItems: SCIndexableThings[] = [];
|
||||
for (const item of items) {
|
||||
if (
|
||||
this.thingTranslatePipe.transform('name', item).toLowerCase().includes(queryText.toLowerCase())
|
||||
(this.thingTranslate.get(item, 'name') as string)
|
||||
.toLowerCase()
|
||||
.includes(queryText.toLowerCase())
|
||||
) {
|
||||
textFilteredItems.push(item);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {DaiaAvailabilityResponse, DaiaHolding, DaiaService} from './protocol/res
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SCFeatureConfiguration} from '@openstapps/core';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
|
||||
@@ -67,7 +66,7 @@ export class DaiaDataProvider {
|
||||
async getAvailability(id: string): Promise<DaiaHolding[] | undefined> {
|
||||
if (this.daiaServiceUrl === undefined) {
|
||||
try {
|
||||
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
||||
const features = this.configProvider.config.app.features;
|
||||
if (features.extern?.daia?.url) {
|
||||
this.daiaServiceUrl = features.extern?.daia?.url;
|
||||
} else {
|
||||
|
||||
@@ -114,16 +114,6 @@ export class HebisSearchPageComponent extends SearchPageComponent implements OnI
|
||||
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
|
||||
.itemSelectListener()
|
||||
.pipe(takeUntilDestroyed(this.destroy$))
|
||||
|
||||
@@ -12,14 +12,9 @@
|
||||
* 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 {JQueryRequestor, Requestor} from '@openid/appauth';
|
||||
import {
|
||||
SCAuthorizationProviderType,
|
||||
SCFeatureConfiguration,
|
||||
SCFeatureConfigurationExtern,
|
||||
} from '@openstapps/core';
|
||||
import {SCAuthorizationProviderType, SCFeatureConfigurationExtern} from '@openstapps/core';
|
||||
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
|
||||
import {HebisDataProvider} from '../../hebis/hebis-data.provider';
|
||||
import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
|
||||
@@ -53,9 +48,7 @@ export class LibraryAccountService {
|
||||
private readonly toastController: ToastController,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const config: SCFeatureConfigurationExtern = (
|
||||
configProvider.getValue('features') as SCFeatureConfiguration
|
||||
).extern!.paia;
|
||||
const config: SCFeatureConfigurationExtern = configProvider.config.app.features.extern!.paia;
|
||||
this.baseUrl = config.url;
|
||||
this.authType = config.authProvider as SCAuthorizationProviderType;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {LeafletModule} from '@asymmetrik/ngx-leaflet';
|
||||
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {Polygon} from 'geojson';
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {ConfigProvider} from '../config/config.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) {
|
||||
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 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/>.
|
||||
*/
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import {SCLanguage, SCThingTranslator, SCThingType, SCTranslations} from '@openstapps/core';
|
||||
import {SCThingType} from '@openstapps/core';
|
||||
import {ContextMenuService} from './context-menu.service';
|
||||
import {FilterContext, FilterFacet, SortContext, SortContextOption} from './context-type.js';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {ThingTranslateService} from '../../../translation/thing-translate.service';
|
||||
|
||||
/**
|
||||
* The context menu
|
||||
@@ -59,11 +59,6 @@ export class ContextMenuComponent {
|
||||
return this.filterOption.options.filter(it => it.buckets.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible languages to be used for translation
|
||||
*/
|
||||
language: keyof SCTranslations<SCLanguage>;
|
||||
|
||||
/**
|
||||
* Mapping of SCThingType
|
||||
*/
|
||||
@@ -74,22 +69,10 @@ export class ContextMenuComponent {
|
||||
*/
|
||||
sortOption: SortContext;
|
||||
|
||||
/**
|
||||
* Core translator
|
||||
*/
|
||||
translator: SCThingTranslator;
|
||||
|
||||
constructor(
|
||||
private translateService: TranslateService,
|
||||
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.filterOption = filterContext;
|
||||
});
|
||||
@@ -109,7 +92,7 @@ export class ContextMenuComponent {
|
||||
* Returns translated property value
|
||||
*/
|
||||
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/>.
|
||||
*/
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import {
|
||||
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';
|
||||
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
|
||||
/**
|
||||
* Generated class for the MenuPage page.
|
||||
*
|
||||
* See https://ionicframework.com/docs/components/#navigation for more info on
|
||||
* Ionic pages and navigation.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-navigation',
|
||||
styleUrls: ['navigation.scss'],
|
||||
templateUrl: 'navigation.html',
|
||||
})
|
||||
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: SCAppConfigurationMenuCategory[];
|
||||
|
||||
/**
|
||||
* 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;
|
||||
});
|
||||
}
|
||||
constructor(private config: ConfigProvider) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.language = (await this.settingsProvider.getValue(
|
||||
'profile',
|
||||
'language',
|
||||
)) as keyof SCTranslations<SCLanguage>;
|
||||
this.translator = new SCThingTranslator(this.language);
|
||||
this.menu = await this.navigationService.getMenu();
|
||||
this.menu = this.config.config.app.menus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
class="menu-category"
|
||||
>
|
||||
<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 *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route">
|
||||
<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-list>
|
||||
</ion-content>
|
||||
|
||||
@@ -22,10 +22,11 @@ import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {OfflineNoticeComponent} from './offline-notice.component';
|
||||
import {ThingTranslateModule} from '../../../translation/thing-translate.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
|
||||
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule],
|
||||
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule, ThingTranslateModule],
|
||||
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
|
||||
})
|
||||
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.is-offline ~ ion-split-pane {
|
||||
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 {NGXLogger} from 'ngx-logger';
|
||||
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({
|
||||
selector: 'stapps-offline-notice',
|
||||
@@ -28,12 +32,17 @@ export class OfflineNoticeComponent {
|
||||
|
||||
@HostBinding('class.has-error') hasError = false;
|
||||
|
||||
@HostBinding('class.needs-reload') needsReload = false;
|
||||
|
||||
@ViewChild('spinIcon', {read: ElementRef}) spinIcon: ElementRef;
|
||||
|
||||
constructor(
|
||||
readonly offlineProvider: InternetConnectionService,
|
||||
readonly router: Router,
|
||||
readonly logger: NGXLogger,
|
||||
readonly animationCtl: AnimationController,
|
||||
settingsProvider: SettingsProvider,
|
||||
configProvider: ConfigProvider,
|
||||
) {
|
||||
this.offlineProvider.offline$.pipe(takeUntilDestroyed()).subscribe(isOffline => {
|
||||
this.isOffline = isOffline;
|
||||
@@ -41,6 +50,15 @@ export class OfflineNoticeComponent {
|
||||
this.offlineProvider.error$.pipe(takeUntilDestroyed()).subscribe(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() {
|
||||
@@ -49,4 +67,15 @@ export class OfflineNoticeComponent {
|
||||
this.spinIcon.nativeElement.classList.add('spin');
|
||||
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-label>{{ 'app.errors.CONNECTION_ERROR' | translate }}</ion-label>
|
||||
</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-icon [size]="16" [weight]="800" name="close" slot="icon-only"></ion-icon
|
||||
></ion-button>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
transition: all 150ms ease;
|
||||
|
||||
&.needs-reload,
|
||||
&.is-offline,
|
||||
&.has-error {
|
||||
transform: translateY(0);
|
||||
@@ -64,6 +65,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.needs-reload > .reload,
|
||||
&.is-offline > .offline-button,
|
||||
&.has-error > .close,
|
||||
&.has-error > .error-button {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import type {AnimationBuilder} from '@ionic/angular';
|
||||
import {AnimationController} from '@ionic/angular';
|
||||
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 {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
|
||||
const duration = options.duration || 350;
|
||||
const contentExitDuration = options.contentExitDuration || 100;
|
||||
const isMd = document.querySelector('ion-app.md') !== null;
|
||||
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
|
||||
.create()
|
||||
@@ -39,23 +41,15 @@ export function tabsTransition(animationController: AnimationController): Animat
|
||||
.addElement(options.leavingEl);
|
||||
const exitTransition = animationController
|
||||
.create()
|
||||
.duration(contentExitDuration * 2)
|
||||
.easing('cubic-bezier(0.87, 0, 0.13, 1)')
|
||||
.fromTo('opacity', '1', '0')
|
||||
.addElement(options.leavingEl.querySelector('ion-header'));
|
||||
const contentExit = animationController
|
||||
.create()
|
||||
.easing('linear')
|
||||
.duration(contentExitDuration)
|
||||
.fromTo('opacity', '1', '0')
|
||||
.addElement(options.leavingEl.querySelectorAll(':scope > *:not(ion-header)'));
|
||||
const contentEnter = animationController
|
||||
.create()
|
||||
.delay(contentExitDuration)
|
||||
.duration(duration - contentExitDuration)
|
||||
.easing('cubic-bezier(0.16, 1, 0.3, 1)')
|
||||
.fromTo('transform', 'scale(1.025)', 'scale(1)')
|
||||
.fromTo('opacity', '0', '1')
|
||||
.fromTo('transform', 'scale(1.05)', 'scale(1)')
|
||||
.addElement(options.enteringEl.querySelectorAll(':scope > *:not(ion-header)'));
|
||||
|
||||
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
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Component} from '@angular/core';
|
||||
import {NavigationEnd, Router} from '@angular/router';
|
||||
import {
|
||||
SCAppConfigurationMenuCategory,
|
||||
SCLanguage,
|
||||
SCThingTranslator,
|
||||
SCTranslations,
|
||||
} from '@openstapps/core';
|
||||
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
|
||||
@Component({
|
||||
@@ -31,21 +25,11 @@ import {NGXLogger} from 'ngx-logger';
|
||||
styleUrls: ['./tabs.component.scss'],
|
||||
})
|
||||
export class TabsComponent {
|
||||
/**
|
||||
* Possible languages to be used for translation
|
||||
*/
|
||||
language: keyof SCTranslations<SCLanguage>;
|
||||
|
||||
/**
|
||||
* Menu entries from config module
|
||||
*/
|
||||
menu: SCAppConfigurationMenuCategory[];
|
||||
|
||||
/**
|
||||
* Core translator
|
||||
*/
|
||||
translator: SCThingTranslator;
|
||||
|
||||
/**
|
||||
* Name of selected tab
|
||||
*/
|
||||
@@ -57,8 +41,6 @@ export class TabsComponent {
|
||||
private readonly logger: NGXLogger,
|
||||
private readonly router: Router,
|
||||
) {
|
||||
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
|
||||
this.translator = new SCThingTranslator(this.language);
|
||||
void this.loadMenuEntries();
|
||||
this.router.events.subscribe((event: unknown) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
@@ -66,11 +48,6 @@ export class TabsComponent {
|
||||
}
|
||||
});
|
||||
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() {
|
||||
try {
|
||||
const menus = (await this.configProvider.getValue('menus')) as SCAppConfigurationMenuCategory[];
|
||||
const menus = this.configProvider.config.app.menus;
|
||||
|
||||
const menu = menus.slice(0, 5);
|
||||
if (menu) {
|
||||
|
||||
@@ -46,6 +46,6 @@
|
||||
[tab]="category.title"
|
||||
>
|
||||
<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-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 {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {DataModule} from '../data/data.module';
|
||||
import {SettingsProvider} from '../settings/settings.provider';
|
||||
import {NewsItemComponent} from './item/news-item.component';
|
||||
import {NewsPageComponent} from './page/news-page.component';
|
||||
import {SkeletonNewsItemComponent} from './item/skeleton-news-item.component';
|
||||
import {ChipFilterComponent} from '../data/chips/filter/chip-filter.component';
|
||||
import {SettingsModule} from '../settings/settings.module';
|
||||
import {NewsSettingsFilterComponent} from './elements/news-filter-settings/news-settings-filter.component';
|
||||
import {UtilModule} from '../../util/util.module';
|
||||
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
||||
|
||||
@@ -36,13 +34,7 @@ const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}];
|
||||
* News Module
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [
|
||||
NewsPageComponent,
|
||||
SkeletonNewsItemComponent,
|
||||
NewsItemComponent,
|
||||
ChipFilterComponent,
|
||||
NewsSettingsFilterComponent,
|
||||
],
|
||||
declarations: [NewsPageComponent, SkeletonNewsItemComponent, NewsItemComponent, ChipFilterComponent],
|
||||
imports: [
|
||||
IonicModule.forRoot(),
|
||||
ThingTranslateModule.forChild(),
|
||||
@@ -56,7 +48,6 @@ const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}];
|
||||
SettingsModule,
|
||||
UtilModule,
|
||||
],
|
||||
providers: [SettingsProvider],
|
||||
exports: [NewsItemComponent],
|
||||
})
|
||||
export class NewsModule {}
|
||||
|
||||
@@ -19,17 +19,18 @@ import {
|
||||
SCSearchBooleanFilter,
|
||||
SCSearchFilter,
|
||||
SCSearchQuery,
|
||||
SCSearchValueFilter,
|
||||
SCSetting,
|
||||
} from '@openstapps/core';
|
||||
import {DataProvider} from '../data/data.provider';
|
||||
import {
|
||||
newsFilterSettingsCategory,
|
||||
newsFilterSettingsFieldsMapping,
|
||||
NewsFilterSettingsNames,
|
||||
} from './news-filter-settings';
|
||||
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
|
||||
*/
|
||||
@@ -42,28 +43,22 @@ export class NewsProvider {
|
||||
private settingsProvider: SettingsProvider,
|
||||
) {}
|
||||
|
||||
async getCurrentSettings(): Promise<SCSetting[]> {
|
||||
const settings: SCSetting[] = [];
|
||||
for (const settingName of Object.keys(newsFilterSettingsFieldsMapping) as NewsFilterSettingsNames[]) {
|
||||
settings.push(await this.settingsProvider.getSetting(newsFilterSettingsCategory, settingName));
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the news filter based on user group and language settings
|
||||
*/
|
||||
async getCurrentFilters(): Promise<SCSearchFilter[]> {
|
||||
const settings = await this.getCurrentSettings();
|
||||
const filtersMap = new Map<NewsFilterSettingsNames, SCSearchValueFilter>();
|
||||
for (const setting of settings) {
|
||||
filtersMap.set(
|
||||
setting.name as NewsFilterSettingsNames,
|
||||
DataProvider.createValueFilter(
|
||||
newsFilterSettingsFieldsMapping[setting.name as NewsFilterSettingsNames],
|
||||
setting.value as string,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return [...filtersMap.values()];
|
||||
return Promise.all(
|
||||
newsFilterSettingsFieldsMapping.map(
|
||||
async ([setting, field]) =>
|
||||
({
|
||||
type: 'value',
|
||||
arguments: {
|
||||
field,
|
||||
value: await this.settingsProvider.getSetting('profile', setting),
|
||||
},
|
||||
}) satisfies SCSearchFilter,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
* 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, OnInit} from '@angular/core';
|
||||
import {Component} from '@angular/core';
|
||||
import {IonRefresher} from '@ionic/angular';
|
||||
import {SCMessage, SCSearchFilter, SCSearchValueFilter, SCSetting} from '@openstapps/core';
|
||||
import {SCMessage} from '@openstapps/core';
|
||||
import {NewsProvider} from '../news.provider';
|
||||
|
||||
/**
|
||||
@@ -25,7 +25,7 @@ import {NewsProvider} from '../news.provider';
|
||||
templateUrl: 'news-page.html',
|
||||
styleUrls: ['news-page.scss'],
|
||||
})
|
||||
export class NewsPageComponent implements OnInit {
|
||||
export class NewsPageComponent {
|
||||
/**
|
||||
* Thing counter to start query the next page from
|
||||
*/
|
||||
@@ -51,24 +51,20 @@ export class NewsPageComponent implements OnInit {
|
||||
*/
|
||||
elementSize = [300, 300];
|
||||
|
||||
/**
|
||||
* Relevant settings
|
||||
*/
|
||||
settings: SCSetting[];
|
||||
|
||||
/**
|
||||
* Active filters
|
||||
*/
|
||||
filters: SCSearchFilter[];
|
||||
|
||||
constructor(private newsProvider: NewsProvider) {}
|
||||
constructor(private newsProvider: NewsProvider) {
|
||||
this.fetchNews();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch news from the backend
|
||||
*/
|
||||
async fetchNews() {
|
||||
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> {
|
||||
const from = this.from;
|
||||
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];
|
||||
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
|
||||
* @param refresher Refresher component that triggers the update
|
||||
@@ -116,13 +109,4 @@ export class NewsPageComponent implements OnInit {
|
||||
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>
|
||||
<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">
|
||||
<ng-container *ngIf="!news">
|
||||
<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 {AlertController} from '@ionic/angular';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import {SCLanguageCode, SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
||||
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
||||
import {SettingsProvider} from '../settings.provider';
|
||||
|
||||
/**
|
||||
@@ -42,23 +41,10 @@ export class SettingsItemComponent {
|
||||
*/
|
||||
@Input() setting: SCSetting;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param alertCtrl AlertController
|
||||
* @param translateService TranslateService
|
||||
* @param settingsProvider SettingProvider
|
||||
*/
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly translateService: TranslateService,
|
||||
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
|
||||
@@ -82,14 +68,6 @@ export class SettingsItemComponent {
|
||||
this.setting.value !== undefined &&
|
||||
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(
|
||||
this.setting.categories[0],
|
||||
this.setting.name,
|
||||
@@ -97,7 +75,7 @@ export class SettingsItemComponent {
|
||||
);
|
||||
} else {
|
||||
// reset setting
|
||||
this.setting.value = (await this.settingsProvider.getValue(
|
||||
this.setting.value = (await this.settingsProvider.getSetting(
|
||||
this.setting.categories[0],
|
||||
this.setting.name,
|
||||
)) as SCSettingValue | SCSettingValues;
|
||||
|
||||
@@ -47,14 +47,6 @@ export class SettingsPageComponent implements OnInit {
|
||||
*/
|
||||
settingsCache: SettingsCache;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param alertController AlertController
|
||||
* @param settingsProvider SettingsProvider
|
||||
* @param toastController ToastController
|
||||
* @param translateService TranslateService
|
||||
* @param changeDetectorRef ChangeDetectorRef
|
||||
*/
|
||||
constructor(
|
||||
private readonly alertController: AlertController,
|
||||
private readonly settingsProvider: SettingsProvider,
|
||||
|
||||
@@ -26,20 +26,6 @@
|
||||
<div class="settings-content">
|
||||
<ng-container *ngFor="let categoryKey of categoriesOrder">
|
||||
<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
|
||||
*ngFor="let settingKeys of objectKeys(settingsCache[categoryKey].settings)"
|
||||
[setting]="settingsCache[categoryKey].settings[settingKeys]"
|
||||
@@ -49,7 +35,7 @@
|
||||
|
||||
<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 }}
|
||||
<ion-icon slot="start" name="device_reset"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
@@ -12,11 +12,8 @@
|
||||
* 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 {Pipe, PipeTransform} from '@angular/core';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {SCSetting} from '@openstapps/core';
|
||||
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
||||
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
||||
|
||||
/**
|
||||
@@ -27,14 +24,10 @@ import {ThingTranslateService} from '../../translation/thing-translate.service';
|
||||
pure: true,
|
||||
})
|
||||
export class SettingTranslatePipe implements PipeTransform {
|
||||
constructor(
|
||||
private readonly translate: TranslateService,
|
||||
private readonly thingTranslate: ThingTranslateService,
|
||||
) {}
|
||||
constructor(private readonly thingTranslate: ThingTranslateService) {}
|
||||
|
||||
transform(setting: SCSetting): string | undefined {
|
||||
const thingTranslatePipe = new ThingTranslatePipe(this.translate, this.thingTranslate);
|
||||
const translatedSettingValues = thingTranslatePipe.transform('values', setting);
|
||||
transform(setting: SCSetting) {
|
||||
const translatedSettingValues = this.thingTranslate.get(setting, 'values') as string;
|
||||
|
||||
return translatedSettingValues
|
||||
? 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 {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SettingsItemComponent} from './item/settings-item.component';
|
||||
import {SettingsPageComponent} from './page/settings-page.component';
|
||||
import {SettingTranslatePipe} from './setting-translate.pipe';
|
||||
import {SettingsProvider} from './settings.provider';
|
||||
import {CalendarSyncSettingsComponent} from './page/calendar-sync-settings.component';
|
||||
import {ScheduleProvider} from '../calendar/schedule.provider';
|
||||
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
||||
@@ -60,13 +57,6 @@ const settingsRoutes: Routes = [{path: 'settings', component: SettingsPageCompon
|
||||
RouterModule.forChild(settingsRoutes),
|
||||
UtilModule,
|
||||
],
|
||||
providers: [
|
||||
ScheduleSyncService,
|
||||
ConfigProvider,
|
||||
SettingsProvider,
|
||||
CalendarService,
|
||||
ScheduleProvider,
|
||||
ThingTranslatePipe,
|
||||
],
|
||||
providers: [ScheduleSyncService, CalendarService, ScheduleProvider, ThingTranslatePipe],
|
||||
})
|
||||
export class SettingsModule {}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
||||
import deepMerge from 'deepmerge';
|
||||
import {Subject} from 'rxjs';
|
||||
import {BehaviorSubject, Subject} from 'rxjs';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
|
||||
@@ -89,7 +89,9 @@ export interface SettingsAction {
|
||||
/**
|
||||
* Provider for app settings
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SettingsProvider {
|
||||
/**
|
||||
* Source of settings actions
|
||||
@@ -103,16 +105,16 @@ export class SettingsProvider {
|
||||
*/
|
||||
categoriesOrder: string[];
|
||||
|
||||
/**
|
||||
* Settings actions observable
|
||||
*/
|
||||
settingsActionChanged$ = this.settingsActionSource.asObservable();
|
||||
|
||||
/**
|
||||
* Cache for the imported settings
|
||||
*/
|
||||
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
|
||||
* @param possibleValues Possible values
|
||||
@@ -148,9 +150,7 @@ export class SettingsProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
possibleValues !== undefined && Array.isArray(possibleValues) && possibleValues.includes(enteredValue)
|
||||
);
|
||||
return 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
|
||||
*/
|
||||
private addSetting(setting: SCSetting): void {
|
||||
@@ -281,34 +281,20 @@ export class SettingsProvider {
|
||||
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
|
||||
* @param category the category of requested setting
|
||||
* @param name the name of requested setting
|
||||
* @throws Exception if setting is not provided
|
||||
*/
|
||||
public async getValue(category: string, name: string): Promise<SCSettingValue | SCSettingValues> {
|
||||
await this.init();
|
||||
if (this.settingExists(category, name)) {
|
||||
// return a copy of the settings value
|
||||
return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name].value));
|
||||
}
|
||||
throw new Error(`Setting "${name}" not provided`);
|
||||
public async getSetting<T extends SCSettingValue | SCSettingValues>(
|
||||
category: 'profile' | string,
|
||||
name: string,
|
||||
): Promise<T> {
|
||||
const settings = await this.storage.get<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES);
|
||||
const value = settings[category]?.[name];
|
||||
if (!value) throw new Error(`Setting "${name}" not provided`);
|
||||
return value as T;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,8 +305,8 @@ export class SettingsProvider {
|
||||
this.needsInit = false;
|
||||
|
||||
try {
|
||||
const settings: SCSetting[] = this.configProvider.getValue('settings') as SCSetting[];
|
||||
for (const setting of settings) this.addSetting(setting);
|
||||
const settings: SCSetting[] = this.configProvider.config.app.settings;
|
||||
for (const setting of settings) this.addSetting(JSON.parse(JSON.stringify(setting)));
|
||||
|
||||
for (const category of Object.keys(this.settingsCache)) {
|
||||
if (!this.categoriesOrder.includes(category)) {
|
||||
@@ -347,7 +333,6 @@ export class SettingsProvider {
|
||||
: this.settingsCache[categoryKey].settings[settingKey].defaultValue;
|
||||
}
|
||||
}
|
||||
await this.saveSettingValues();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,6 +382,9 @@ export class SettingsProvider {
|
||||
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
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -12,16 +12,15 @@
|
||||
* 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 {NgModule} from '@angular/core';
|
||||
import {DataModule} from '../data/data.module';
|
||||
import {StorageModule} from '../storage/storage.module';
|
||||
import {ConfigProvider} from './config.provider';
|
||||
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [StorageModule, DataModule],
|
||||
providers: [ConfigProvider],
|
||||
@Injectable()
|
||||
@Pipe({
|
||||
name: 'entries',
|
||||
pure: true,
|
||||
})
|
||||
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
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -12,21 +12,23 @@
|
||||
* 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 {SCSettingCategories, SCThingsField} from '@openstapps/core';
|
||||
/**
|
||||
* Category of settings to use for news filter
|
||||
*/
|
||||
export const newsFilterSettingsCategory: SCSettingCategories = 'profile';
|
||||
/**
|
||||
* Settings to use for news filter
|
||||
*/
|
||||
export type NewsFilterSettingsNames = 'language' | 'group';
|
||||
/**
|
||||
* The mapping between settings and corresponding data fields for building a value filter
|
||||
*/
|
||||
export const newsFilterSettingsFieldsMapping: {
|
||||
[key in NewsFilterSettingsNames]: SCThingsField;
|
||||
} = {
|
||||
language: 'inLanguage',
|
||||
group: 'audiences',
|
||||
};
|
||||
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
@Pipe({
|
||||
name: 'join',
|
||||
pure: true,
|
||||
})
|
||||
export class ArrayJoinPipe implements PipeTransform {
|
||||
transform(anArray: unknown[] | unknown, separator: string | unknown): string {
|
||||
if (typeof separator !== 'string' || separator.length <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!Array.isArray(anArray)) {
|
||||
throw new SyntaxError(`Wrong parameter in ArrayJoinPipe. Expected a valid Array, received: ${anArray}`);
|
||||
}
|
||||
|
||||
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