mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-04-20 21:29:19 +00:00
Compare commits
8 Commits
192-fix-te
...
142-requir
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c30211ba2 | |||
| 63a38e0077 | |||
| c8b260201c | |||
|
123c50d1af
|
|||
|
|
d65e6351e9 | ||
|
|
2c5d7403db | ||
|
6ca03f463d
|
|||
|
|
1f74a9bc82 |
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
"@openstapps/node-builder": patch
|
|
||||||
"@openstapps/database": patch
|
|
||||||
"@openstapps/node-base": patch
|
|
||||||
"@openstapps/backend": patch
|
|
||||||
"@openstapps/app": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
pin alpine version to 3.18 and add healthchecks
|
|
||||||
5
.changeset/good-eggs-add.md
Normal file
5
.changeset/good-eggs-add.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@openstapps/prettier-config": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Update Prettier to 3.1.1
|
||||||
5
.changeset/old-bottles-hide.md
Normal file
5
.changeset/old-bottles-hide.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@openstapps/backend": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Backend unit tests break every year
|
||||||
9
.changeset/spotty-ducks-cheer.md
Normal file
9
.changeset/spotty-ducks-cheer.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
'@openstapps/app': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Require full reload for setting & language changes
|
||||||
|
|
||||||
|
Setting changes are relatively rare, so it makes little sense
|
||||||
|
going through the effort of ensuring everything is reactive to
|
||||||
|
language changes as well as creating all the pipe bindings etc.
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,6 +24,7 @@ report-junit.xml
|
|||||||
# NixOS flake
|
# NixOS flake
|
||||||
result
|
result
|
||||||
hsperfdata_root
|
hsperfdata_root
|
||||||
|
.direnv/
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
lib-cov
|
lib-cov
|
||||||
|
|||||||
3
.tool-versions
Normal file
3
.tool-versions
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
nodejs 18.16.1
|
||||||
|
pnpm 8.8.0
|
||||||
|
python 3.11.5
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Open StApps Monorepo
|
# <img src="logo-bg.svg" height="24"> Open StApps Monorepo
|
||||||
|
|
||||||
Refer to the [contribution guide](./CONTRIBUTING.md)
|
Refer to the [contribution guide](./CONTRIBUTING.md)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,4 @@ ENV NODE_ENV=production
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
HEALTHCHECK --interval=10s --timeout=10s --start-period=10s --retries=12 CMD curl -s --fail --request POST --data '{}' --header 'Content-Type: application/json' http://localhost:3000/ >/dev/null || exit 1
|
|
||||||
|
|
||||||
ENTRYPOINT ["node", "app.js"]
|
ENTRYPOINT ["node", "app.js"]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the database configuration for the technical university of berlin
|
* This is the database configuration for the technical university of berlin
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCLanguageSetting} */
|
/** @type {import('@openstapps/core').SCLanguageSetting} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/** @type {import('@openstapps/core').SCAppConfigurationMenuCategory[]} */
|
/** @type {import('@openstapps/core').SCAppConfigurationMenuCategory[]} */
|
||||||
const menus = [
|
const menus = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCUserGroupSetting} */
|
/** @type {import('@openstapps/core').SCUserGroupSetting} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCThingType} from '@openstapps/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCBackendAggregationConfiguration[]} */
|
/** @type {import('@openstapps/core').SCBackendAggregationConfiguration[]} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {
|
import {
|
||||||
month,
|
month,
|
||||||
sommerRange,
|
sommerRange,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCThingType} from '@openstapps/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
import aggregations from './aggregations.js';
|
import aggregations from './aggregations.js';
|
||||||
import boostings from './boostings.js';
|
import boostings from './boostings.js';
|
||||||
@@ -17,7 +16,7 @@ export const backend = {
|
|||||||
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
|
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
|
||||||
mappingIgnoredTags: ['minlength', 'pattern', 'see', 'tjs-format'],
|
mappingIgnoredTags: ['minlength', 'pattern', 'see', 'tjs-format'],
|
||||||
maxMultiSearchRouteQueries: 5,
|
maxMultiSearchRouteQueries: 5,
|
||||||
maxRequestBodySize: 2 * 10 ** 6,
|
maxRequestBodySize: 2e6,
|
||||||
name: 'Goethe-Universität Frankfurt am Main',
|
name: 'Goethe-Universität Frankfurt am Main',
|
||||||
namespace: '909a8cbc-8520-456c-b474-ef1525f14209',
|
namespace: '909a8cbc-8520-456c-b474-ef1525f14209',
|
||||||
sortableFields: [
|
sortableFields: [
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import app from './app/index.js';
|
import app from './app/index.js';
|
||||||
import {backend, internal} from './backend/index.js';
|
import {backend, internal} from './backend/index.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the default configuration for elasticsearch (a database)
|
* This is the default configuration for elasticsearch (a database)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {readFile} from 'fs/promises';
|
import {readFile} from 'fs/promises';
|
||||||
import {SCAboutPageContentType} from '@openstapps/core';
|
import {SCAboutPageContentType} from '@openstapps/core';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/**
|
/**
|
||||||
* Generates a range of numbers that represent consecutive calendar months
|
* Generates a range of numbers that represent consecutive calendar months
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {readFile, readdir} from 'fs/promises';
|
import {readFile, readdir} from 'fs/promises';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCAboutPageContentType} from '@openstapps/core';
|
import {SCAboutPageContentType} from '@openstapps/core';
|
||||||
import {markdown} from '../../default/tools/markdown.js';
|
import {markdown} from '../../default/tools/markdown.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {SCAboutPageContentType} from '@openstapps/core';
|
import {SCAboutPageContentType} from '@openstapps/core';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCAboutPage} */
|
/** @type {import('@openstapps/core').SCAboutPage} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import about from './about.js';
|
import about from './about.js';
|
||||||
import imprint from './imprint.js';
|
import imprint from './imprint.js';
|
||||||
import privacy from './privacy.js';
|
import privacy from './privacy.js';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {markdown} from '../../default/tools/markdown.js';
|
import {markdown} from '../../default/tools/markdown.js';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCAboutPage} */
|
/** @type {import('@openstapps/core').SCAboutPage} */
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import aboutPages from './about-pages/index.js';
|
import aboutPages from './about-pages/index.js';
|
||||||
import defaultApp from '../default/app/index.js';
|
import defaultApp from '../default/app/index.js';
|
||||||
import {backend as defaultBackend, internal as defaultInternal} from '../default/backend/index.js';
|
import {backend as defaultBackend, internal as defaultInternal} from '../default/backend/index.js';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
import {versions} from '../../default/tools/version.js';
|
import {versions} from '../../default/tools/version.js';
|
||||||
|
|
||||||
/** @type {import('@openstapps/core').SCAppVersionInfo[]} */
|
/** @type {import('@openstapps/core').SCAppVersionInfo[]} */
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
"start:debug": "cross-env STAPPS_LOG_LEVEL=31 NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true node app.js",
|
"start:debug": "cross-env STAPPS_LOG_LEVEL=31 NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true node app.js",
|
||||||
"test": "pnpm run test:unit",
|
"test": "pnpm run test:unit",
|
||||||
"test:integration": "sh integration-test.sh",
|
"test:integration": "sh integration-test.sh",
|
||||||
"test:unit": "cross-env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true STAPPS_LOG_LEVEL=0 c8 mocha"
|
"test:unit": "cross-env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true STAPPS_LOG_LEVEL=0 mocha --exit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elastic/elasticsearch": "8.4.0",
|
"@elastic/elasticsearch": "8.4.0",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
import {Logger, SMTP} from '@openstapps/logger';
|
import {Logger, SMTP} from '@openstapps/logger';
|
||||||
import {MailOptions} from 'nodemailer/lib/sendmail-transport';
|
import {MailOptions} from 'nodemailer/lib/sendmail-transport';
|
||||||
|
import Queue from 'promise-queue';
|
||||||
/**
|
/**
|
||||||
* A queue that can send mails in serial
|
* A queue that can send mails in serial
|
||||||
*/
|
*/
|
||||||
@@ -32,45 +32,80 @@ export class MailQueue {
|
|||||||
static readonly VERIFICATION_TIMEOUT = 5000;
|
static readonly VERIFICATION_TIMEOUT = 5000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A promise that resolves when the last mail was sent
|
* A queue that saves mails, before the transport is ready. When
|
||||||
|
* the transport gets ready this mails are getting pushed in to
|
||||||
|
* the normal queue.
|
||||||
*/
|
*/
|
||||||
last?: Promise<string>;
|
dryQueue: MailOptions[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A queue that saves mails, that are being sent in series
|
||||||
|
*/
|
||||||
|
queue: Queue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A counter for the number of verifications that failed
|
||||||
|
*/
|
||||||
|
verificationCounter: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a mail queue
|
* Creates a mail queue
|
||||||
* @param transport Transport which is used for sending mails
|
* @param transport Transport which is used for sending mails
|
||||||
*/
|
*/
|
||||||
constructor(private readonly transport: SMTP) {}
|
constructor(private readonly transport: SMTP) {
|
||||||
|
this.queue = new Queue(1);
|
||||||
|
|
||||||
|
// this queue saves all request when the transport is not ready yet
|
||||||
|
this.dryQueue = [];
|
||||||
|
|
||||||
|
this.verificationCounter = 0;
|
||||||
|
|
||||||
|
// if the transport can be verified it should check if it was done...
|
||||||
|
this.checkForVerification();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for the transport to be verified
|
* Adds a mail into the queue so it gets send when the queue is ready
|
||||||
|
* @param mail Information required for sending a mail
|
||||||
*/
|
*/
|
||||||
private async waitForVerification() {
|
private async addToQueue(mail: MailOptions) {
|
||||||
for (let i = 0; i < MailQueue.MAX_VERIFICATION_ATTEMPTS; i++) {
|
return this.queue.add<string>(() => this.transport.sendMail(mail));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the given transport
|
||||||
|
*/
|
||||||
|
private checkForVerification() {
|
||||||
|
if (this.verificationCounter >= MailQueue.MAX_VERIFICATION_ATTEMPTS) {
|
||||||
|
throw new Error('Failed to initialize the SMTP transport for the mail queue');
|
||||||
|
}
|
||||||
|
|
||||||
if (this.transport.isVerified()) {
|
if (this.transport.isVerified()) {
|
||||||
Logger.ok('Transport for mail queue was verified. We can send mails now');
|
Logger.ok('Transport for mail queue was verified. We can send mails now');
|
||||||
return;
|
// if the transport finally was verified send all our mails from the dry queue
|
||||||
|
for (const mail of this.dryQueue) {
|
||||||
|
void this.addToQueue(mail);
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, MailQueue.VERIFICATION_TIMEOUT));
|
} else {
|
||||||
|
this.verificationCounter++;
|
||||||
|
setTimeout(() => {
|
||||||
Logger.warn('Transport not verified yet. Trying to send mails here...');
|
Logger.warn('Transport not verified yet. Trying to send mails here...');
|
||||||
|
this.checkForVerification();
|
||||||
|
}, MailQueue.VERIFICATION_TIMEOUT);
|
||||||
}
|
}
|
||||||
throw new Error('Failed to initialize the SMTP transport for the mail queue');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push a mail into the queue so it gets send when the queue is ready
|
* Push a mail into the queue so it gets send when the queue is ready
|
||||||
* @param mail Information required for sending a mail
|
* @param mail Information required for sending a mail
|
||||||
*/
|
*/
|
||||||
public async push(mail: MailOptions): Promise<string> {
|
public async push(mail: MailOptions) {
|
||||||
const previousQueue = this.last ?? this.waitForVerification();
|
if (this.transport.isVerified()) {
|
||||||
this.last = previousQueue.then(() =>
|
await this.addToQueue(mail);
|
||||||
Promise.race([
|
} else {
|
||||||
this.transport.sendMail(mail),
|
// the transport has verification, but is not verified yet
|
||||||
new Promise<string>((_, reject) =>
|
// push to a dry queue which gets pushed to the real queue when the transport is verified
|
||||||
setTimeout(() => reject(new Error('Timeout')), MailQueue.VERIFICATION_TIMEOUT),
|
this.dryQueue.push(mail);
|
||||||
),
|
}
|
||||||
]),
|
|
||||||
);
|
|
||||||
return this.last;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe('MailQueue', async function () {
|
|||||||
clock.tick(MailQueue.VERIFICATION_TIMEOUT * (MailQueue.MAX_VERIFICATION_ATTEMPTS + 1));
|
clock.tick(MailQueue.VERIFICATION_TIMEOUT * (MailQueue.MAX_VERIFICATION_ATTEMPTS + 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(test).to.throw();
|
expect(() => test()).to.throw();
|
||||||
expect(loggerStub.callCount).to.be.equal(MailQueue.MAX_VERIFICATION_ATTEMPTS);
|
expect(loggerStub.callCount).to.be.equal(MailQueue.MAX_VERIFICATION_ATTEMPTS);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
SCBook,
|
||||||
SCBulkAddRoute,
|
SCBulkAddRoute,
|
||||||
SCBulkDoneRoute,
|
SCBulkDoneRoute,
|
||||||
SCBulkRequest,
|
SCBulkRequest,
|
||||||
@@ -23,29 +24,30 @@ import {
|
|||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import {bulk, DEFAULT_TEST_TIMEOUT} from '../common.js';
|
import {bulk, DEFAULT_TEST_TIMEOUT} from '../common.js';
|
||||||
import {testApp} from '../tests-setup.js';
|
import {testApp} from '../tests-setup.js';
|
||||||
import {readFile} from 'fs/promises';
|
|
||||||
import {v4} from 'uuid';
|
import {v4} from 'uuid';
|
||||||
|
import bookFile from '@openstapps/core/test/resources/indexable/Book.2.json' assert {type: 'json'};
|
||||||
|
|
||||||
const book = JSON.parse(
|
const book = bookFile.instance as SCBook;
|
||||||
await readFile('node_modules/@openstapps/core/test/resources/indexable/Book.2.json', 'utf8'),
|
|
||||||
).instance;
|
|
||||||
|
|
||||||
describe('Bulk routes', async function () {
|
describe('Bulk routes', async function () {
|
||||||
// increase timeout for the suite
|
// increase timeout for the suite
|
||||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||||
|
|
||||||
const request: SCBulkRequest = {
|
let request: SCBulkRequest;
|
||||||
|
let bulkRoute: SCBulkRoute;
|
||||||
|
let bulkAddRoute: SCBulkAddRoute;
|
||||||
|
let bulkDoneRoute: SCBulkDoneRoute;
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
request = {
|
||||||
expiration: bulk.expiration,
|
expiration: bulk.expiration,
|
||||||
source: bulk.source,
|
source: bulk.source,
|
||||||
type: bulk.type,
|
type: bulk.type,
|
||||||
};
|
};
|
||||||
const bulkRoute = new SCBulkRoute();
|
bulkRoute = new SCBulkRoute();
|
||||||
const bulkAddRoute = new SCBulkAddRoute();
|
bulkAddRoute = new SCBulkAddRoute();
|
||||||
const bulkDoneRoute = new SCBulkDoneRoute();
|
bulkDoneRoute = new SCBulkDoneRoute();
|
||||||
|
});
|
||||||
// afterEach(async function() {
|
|
||||||
// TODO: Delete saved bulks
|
|
||||||
// });
|
|
||||||
|
|
||||||
it('should create bulk', async function () {
|
it('should create bulk', async function () {
|
||||||
const {status, body, error} = await testApp
|
const {status, body, error} = await testApp
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ import {expect} from 'chai';
|
|||||||
describe('Index route', async function () {
|
describe('Index route', async function () {
|
||||||
// increase timeout for the suite
|
// increase timeout for the suite
|
||||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||||
const indexRoute = new SCIndexRoute();
|
|
||||||
|
let indexRoute: SCIndexRoute;
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
indexRoute = new SCIndexRoute();
|
||||||
|
});
|
||||||
|
|
||||||
it('should respond with both app and backend configuration', async function () {
|
it('should respond with both app and backend configuration', async function () {
|
||||||
const request: SCIndexRequest = {};
|
const request: SCIndexRequest = {};
|
||||||
|
|||||||
@@ -30,15 +30,11 @@ import chaiAsPromised from 'chai-as-promised';
|
|||||||
import {DEFAULT_TEST_TIMEOUT} from '../common.js';
|
import {DEFAULT_TEST_TIMEOUT} from '../common.js';
|
||||||
import {testApp} from '../tests-setup.js';
|
import {testApp} from '../tests-setup.js';
|
||||||
import {backendConfig} from '../../src/config.js';
|
import {backendConfig} from '../../src/config.js';
|
||||||
import {readFile} from 'fs/promises';
|
import registerRequest from '@openstapps/core/test/resources/PluginRegisterRequest.1.json' assert {type: 'json'};
|
||||||
|
|
||||||
// for using promises in expectations (to.eventually.be...)
|
// for using promises in expectations (to.eventually.be...)
|
||||||
use(chaiAsPromised);
|
use(chaiAsPromised);
|
||||||
|
|
||||||
const registerRequest = JSON.parse(
|
|
||||||
await readFile('node_modules/@openstapps/core/test/resources/PluginRegisterRequest.1.json', 'utf8'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// cast it because of "TS2322: Type 'string' is not assignable to type '"add"'"
|
// cast it because of "TS2322: Type 'string' is not assignable to type '"add"'"
|
||||||
export const registerAddRequest: SCPluginAdd = registerRequest.instance as SCPluginAdd;
|
export const registerAddRequest: SCPluginAdd = registerRequest.instance as SCPluginAdd;
|
||||||
|
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ describe('Create route', async function () {
|
|||||||
const statusCodeSuccess = 222;
|
const statusCodeSuccess = 222;
|
||||||
const bodySuccess = {foo: true};
|
const bodySuccess = {foo: true};
|
||||||
const sandbox = sinon.createSandbox();
|
const sandbox = sinon.createSandbox();
|
||||||
const validationError = new SCValidationErrorResponse([]);
|
let validationError: SCValidationErrorResponse;
|
||||||
const internalServerError = new SCInternalServerErrorResponse();
|
let internalServerError: SCInternalServerErrorResponse;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
app = express();
|
app = express();
|
||||||
@@ -64,6 +64,9 @@ describe('Create route', async function () {
|
|||||||
handler = (_request, _app) => {
|
handler = (_request, _app) => {
|
||||||
return Promise.resolve(bodySuccess);
|
return Promise.resolve(bodySuccess);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
validationError = new SCValidationErrorResponse([]);
|
||||||
|
internalServerError = new SCInternalServerErrorResponse();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
|||||||
@@ -29,11 +29,19 @@ import {backendConfig} from '../../src/config.js';
|
|||||||
describe('Search route', async function () {
|
describe('Search route', async function () {
|
||||||
// increase timeout for the suite
|
// increase timeout for the suite
|
||||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||||
const searchRoute = new SCSearchRoute();
|
let searchRoute: SCSearchRoute;
|
||||||
const multiSearchRoute = new SCMultiSearchRoute();
|
let multiSearchRoute: SCMultiSearchRoute;
|
||||||
const syntaxError = new SCSyntaxErrorResponse('Foo Message');
|
let syntaxError: SCSyntaxErrorResponse;
|
||||||
const methodNotAllowedError = new SCMethodNotAllowedErrorResponse();
|
let methodNotAllowedError: SCMethodNotAllowedErrorResponse;
|
||||||
const tooManyRequestsError = new SCTooManyRequestsErrorResponse();
|
let tooManyRequestsError: SCTooManyRequestsErrorResponse;
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
searchRoute = new SCSearchRoute();
|
||||||
|
multiSearchRoute = new SCMultiSearchRoute();
|
||||||
|
syntaxError = new SCSyntaxErrorResponse('Foo Message');
|
||||||
|
methodNotAllowedError = new SCMethodNotAllowedErrorResponse();
|
||||||
|
tooManyRequestsError = new SCTooManyRequestsErrorResponse();
|
||||||
|
});
|
||||||
|
|
||||||
it('should reject GET, PUT with a valid search query', async function () {
|
it('should reject GET, PUT with a valid search query', async function () {
|
||||||
// const expectedParams = JSON.parse(JSON.stringify(defaultParams));
|
// const expectedParams = JSON.parse(JSON.stringify(defaultParams));
|
||||||
|
|||||||
@@ -13,23 +13,25 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {SCThingUpdateRoute} from '@openstapps/core';
|
import {SCBook, SCThingUpdateRoute} from '@openstapps/core';
|
||||||
import chaiAsPromised from 'chai-as-promised';
|
import chaiAsPromised from 'chai-as-promised';
|
||||||
import {bulkStorageMock, DEFAULT_TEST_TIMEOUT} from '../common.js';
|
import {bulkStorageMock, DEFAULT_TEST_TIMEOUT} from '../common.js';
|
||||||
import {expect, use} from 'chai';
|
import {expect, use} from 'chai';
|
||||||
import {testApp} from '../tests-setup.js';
|
import {testApp} from '../tests-setup.js';
|
||||||
import {readFile} from 'fs/promises';
|
import bookFile from '@openstapps/core/test/resources/indexable/Book.1.json' assert {type: 'json'};
|
||||||
|
|
||||||
use(chaiAsPromised);
|
use(chaiAsPromised);
|
||||||
|
|
||||||
const book = JSON.parse(
|
const book = bookFile.instance as SCBook;
|
||||||
await readFile('node_modules/@openstapps/core/test/resources/indexable/Book.1.json', 'utf8'),
|
|
||||||
).instance;
|
|
||||||
|
|
||||||
describe('Thing update route', async function () {
|
describe('Thing update route', async function () {
|
||||||
// increase timeout for the suite
|
// increase timeout for the suite
|
||||||
this.timeout(DEFAULT_TEST_TIMEOUT);
|
this.timeout(DEFAULT_TEST_TIMEOUT);
|
||||||
const thingUpdateRoute = new SCThingUpdateRoute();
|
let thingUpdateRoute: SCThingUpdateRoute;
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
thingUpdateRoute = new SCThingUpdateRoute();
|
||||||
|
});
|
||||||
|
|
||||||
it('should update a thing', async function () {
|
it('should update a thing', async function () {
|
||||||
const thingUpdateRouteurlPath = thingUpdateRoute.urlPath
|
const thingUpdateRouteurlPath = thingUpdateRoute.urlPath
|
||||||
|
|||||||
@@ -49,8 +49,11 @@ import {
|
|||||||
} from '../../../src/storage/elasticsearch/util/index.js';
|
} from '../../../src/storage/elasticsearch/util/index.js';
|
||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import {query} from './query.js';
|
import {query} from './query.js';
|
||||||
import message from '@openstapps/core/test/resources/indexable/Message.1.json' assert {type: 'json'};
|
import messageFile from '@openstapps/core/test/resources/indexable/Message.1.json' assert {type: 'json'};
|
||||||
import book from '@openstapps/core/test/resources/indexable/Book.1.json' assert {type: 'json'};
|
import bookFile from '@openstapps/core/test/resources/indexable/Book.1.json' assert {type: 'json'};
|
||||||
|
|
||||||
|
const message = messageFile.instance as SCMessage;
|
||||||
|
const book = bookFile.instance as SCBook;
|
||||||
|
|
||||||
use(chaiAsPromised);
|
use(chaiAsPromised);
|
||||||
|
|
||||||
@@ -67,7 +70,16 @@ describe('Elasticsearch', function () {
|
|||||||
const sandbox = sinon.createSandbox();
|
const sandbox = sinon.createSandbox();
|
||||||
|
|
||||||
before(function () {
|
before(function () {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
sandbox.stub(fs, 'readFileSync').returns('{}');
|
sandbox.stub(fs, 'readFileSync').returns('{}');
|
||||||
|
sandbox.stub(backendConfig.internal.boostings.default[0], 'fields').value({
|
||||||
|
'academicTerms.acronym': {
|
||||||
|
'SS 2023': 1.05,
|
||||||
|
'WS 2023/24': 1.1,
|
||||||
|
'SoSe 2023': 1.05,
|
||||||
|
'WiSe 2023/24': 1.1,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
after(function () {
|
after(function () {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
@@ -261,7 +273,7 @@ describe('Elasticsearch', function () {
|
|||||||
return expect(es.init()).to.be.rejected;
|
return expect(es.init()).to.be.rejected;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should setup the monitoring if there is monitoring is set and mail queue is defined', async function () {
|
it('should setup the monitoring if there is monitoring is set and mail queue is defined', function () {
|
||||||
const config: SCConfigFile = {
|
const config: SCConfigFile = {
|
||||||
...backendConfig,
|
...backendConfig,
|
||||||
internal: {
|
internal: {
|
||||||
@@ -283,7 +295,7 @@ describe('Elasticsearch', function () {
|
|||||||
const cronSetupStub = sandbox.stub(cron, 'schedule');
|
const cronSetupStub = sandbox.stub(cron, 'schedule');
|
||||||
|
|
||||||
const es = new Elasticsearch(config, new MailQueue(getTransport(false) as unknown as SMTP));
|
const es = new Elasticsearch(config, new MailQueue(getTransport(false) as unknown as SMTP));
|
||||||
await es.init();
|
es.init();
|
||||||
|
|
||||||
expect(cronSetupStub.called).to.be.true;
|
expect(cronSetupStub.called).to.be.true;
|
||||||
});
|
});
|
||||||
@@ -437,7 +449,7 @@ describe('Elasticsearch', function () {
|
|||||||
_id: '',
|
_id: '',
|
||||||
_index: '',
|
_index: '',
|
||||||
_score: 0,
|
_score: 0,
|
||||||
_source: message.instance as SCMessage,
|
_source: message,
|
||||||
};
|
};
|
||||||
sandbox.stub(es.client, 'search').resolves(searchResponse(foundObject));
|
sandbox.stub(es.client, 'search').resolves(searchResponse(foundObject));
|
||||||
|
|
||||||
@@ -467,7 +479,7 @@ describe('Elasticsearch', function () {
|
|||||||
const object: SearchHit<SCMessage> = {
|
const object: SearchHit<SCMessage> = {
|
||||||
_id: '',
|
_id: '',
|
||||||
_index: oldIndex,
|
_index: oldIndex,
|
||||||
_source: message.instance as SCMessage,
|
_source: message,
|
||||||
};
|
};
|
||||||
sandbox.stub(es.client, 'search').resolves(searchResponse<SCMessage>(object));
|
sandbox.stub(es.client, 'search').resolves(searchResponse<SCMessage>(object));
|
||||||
sandbox.stub(es, 'prepareBulkWrite').resolves(index);
|
sandbox.stub(es, 'prepareBulkWrite').resolves(index);
|
||||||
@@ -481,7 +493,7 @@ describe('Elasticsearch', function () {
|
|||||||
sandbox.stub(es.client, 'create').resolves({result: 'not_found'} as CreateResponse);
|
sandbox.stub(es.client, 'create').resolves({result: 'not_found'} as CreateResponse);
|
||||||
|
|
||||||
await es.init();
|
await es.init();
|
||||||
return expect(es.post(message.instance as SCMessage, bulk)).to.rejectedWith('creation');
|
return expect(es.post(message, bulk)).to.rejectedWith('creation');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new object', async function () {
|
it('should create a new object', async function () {
|
||||||
@@ -494,11 +506,11 @@ describe('Elasticsearch', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await es.init();
|
await es.init();
|
||||||
await es.post(message.instance as SCMessage, bulk);
|
await es.post(message, bulk);
|
||||||
|
|
||||||
expect(createStub.called).to.be.true;
|
expect(createStub.called).to.be.true;
|
||||||
expect(caughtParameter.document).to.be.eql({
|
expect(caughtParameter.document).to.be.eql({
|
||||||
...message.instance,
|
...message,
|
||||||
creation_date: caughtParameter.document.creation_date,
|
creation_date: caughtParameter.document.creation_date,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -519,7 +531,7 @@ describe('Elasticsearch', function () {
|
|||||||
_id: '',
|
_id: '',
|
||||||
_index: getIndex(),
|
_index: getIndex(),
|
||||||
_score: 0,
|
_score: 0,
|
||||||
_source: message.instance as SCMessage,
|
_source: message,
|
||||||
};
|
};
|
||||||
sandbox.stub(es.client, 'search').resolves(searchResponse());
|
sandbox.stub(es.client, 'search').resolves(searchResponse());
|
||||||
|
|
||||||
@@ -533,7 +545,7 @@ describe('Elasticsearch', function () {
|
|||||||
_id: '',
|
_id: '',
|
||||||
_index: getIndex(),
|
_index: getIndex(),
|
||||||
_score: 0,
|
_score: 0,
|
||||||
_source: message.instance as SCMessage,
|
_source: message,
|
||||||
};
|
};
|
||||||
sandbox.stub(es.client, 'search').resolves(searchResponse(object));
|
sandbox.stub(es.client, 'search').resolves(searchResponse(object));
|
||||||
// @ts-expect-error unused
|
// @ts-expect-error unused
|
||||||
@@ -556,13 +568,13 @@ describe('Elasticsearch', function () {
|
|||||||
_id: '123',
|
_id: '123',
|
||||||
_index: getIndex(),
|
_index: getIndex(),
|
||||||
_score: 0,
|
_score: 0,
|
||||||
_source: message.instance as SCMessage,
|
_source: message,
|
||||||
};
|
};
|
||||||
const objectBook: SearchHit<SCBook> = {
|
const objectBook: SearchHit<SCBook> = {
|
||||||
_id: '321',
|
_id: '321',
|
||||||
_index: getIndex(),
|
_index: getIndex(),
|
||||||
_score: 0,
|
_score: 0,
|
||||||
_source: book.instance as SCBook,
|
_source: book,
|
||||||
};
|
};
|
||||||
const fakeEsAggregations = {
|
const fakeEsAggregations = {
|
||||||
'@all': {
|
'@all': {
|
||||||
|
|||||||
@@ -14,6 +14,4 @@ RUN chown elasticsearch:elasticsearch config/elasticsearch.yml
|
|||||||
|
|
||||||
USER elasticsearch
|
USER elasticsearch
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=10s --start-period=60s --retries=12 CMD curl --fail -s http://localhost:9200/ >/dev/null || exit 1
|
|
||||||
|
|
||||||
CMD ["/usr/share/elasticsearch/bin/elasticsearch"]
|
CMD ["/usr/share/elasticsearch/bin/elasticsearch"]
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ discovery.type: "single-node"
|
|||||||
cluster.routing.allocation.disk.threshold_enabled: false
|
cluster.routing.allocation.disk.threshold_enabled: false
|
||||||
network.bind_host: 0.0.0.0
|
network.bind_host: 0.0.0.0
|
||||||
xpack.security.enabled: false
|
xpack.security.enabled: false
|
||||||
ingest.geoip.downloader.enabled: false
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
nginx &
|
nginx &
|
||||||
node ./app.js
|
node ./lib/cli.js
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// ESM is not supported, and cts is not detected, so we use type-checked cjs instead.
|
||||||
/** @type {import('../src/common').ConfigFile} */
|
/** @type {import('../src/common').ConfigFile} */
|
||||||
const configFile = {
|
const configFile = {
|
||||||
activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'],
|
activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'],
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
"@types/dockerode": "3.3.17",
|
"@types/dockerode": "3.3.17",
|
||||||
"@types/node": "18.15.3",
|
"@types/node": "18.15.3",
|
||||||
"@types/sha1": "1.1.3",
|
"@types/sha1": "1.1.3",
|
||||||
|
"config": "3.3.9",
|
||||||
"dockerode": "3.3.5",
|
"dockerode": "3.3.5",
|
||||||
"is-cidr": "4.0.2",
|
"is-cidr": "4.0.2",
|
||||||
"mustache": "4.2.0",
|
"mustache": "4.2.0",
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
"@openstapps/prettier-config": "workspace:*",
|
"@openstapps/prettier-config": "workspace:*",
|
||||||
"@openstapps/tsconfig": "workspace:*",
|
"@openstapps/tsconfig": "workspace:*",
|
||||||
"@types/chai": "4.3.5",
|
"@types/chai": "4.3.5",
|
||||||
|
"@types/config": "3.3.0",
|
||||||
"@types/dockerode": "3.3.17",
|
"@types/dockerode": "3.3.17",
|
||||||
"@types/mocha": "10.0.1",
|
"@types/mocha": "10.0.1",
|
||||||
"@types/mustache": "4.2.2",
|
"@types/mustache": "4.2.2",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Logger, SMTP} from '@openstapps/logger';
|
import {Logger, SMTP} from '@openstapps/logger';
|
||||||
|
import config from 'config';
|
||||||
import {existsSync} from 'fs';
|
import {existsSync} from 'fs';
|
||||||
|
|
||||||
// set transport on logger
|
// set transport on logger
|
||||||
@@ -162,7 +163,7 @@ ssl_stapling_verify on;`;
|
|||||||
/**
|
/**
|
||||||
* Config file
|
* Config file
|
||||||
*/
|
*/
|
||||||
export const configFile: ConfigFile = await import('../config/default.js').then(it => it.default);
|
export const configFile: ConfigFile = config.util.toObject();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if path is a specific file type
|
* Check if path is a specific file type
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const child_process = require("child_process");
|
const child_process = require("child_process");
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
const rule = require('./copyright-header-rule')
|
const rule = require('./copyright-header-rule')
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
/** @type {import('eslint').Linter.Config} */
|
/** @type {import('eslint').Linter.Config} */
|
||||||
const config = {
|
const config = {
|
||||||
root: true,
|
root: true,
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
"test": "prettier --config index.js --check \"test/*.js\""
|
"test": "prettier --config index.js --check \"test/*.js\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.1.0"
|
"prettier": "3.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"prettier": "3.1.0"
|
"prettier": "3.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
|||||||
@@ -1,44 +1,62 @@
|
|||||||
version: '3.7'
|
version: '3.7'
|
||||||
|
|
||||||
|
x-development-variables: &development-variables
|
||||||
|
NODE_ENV: "development"
|
||||||
|
ALLOW_NO_TRANSPORT: "true"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
image: registry.gitlab.com/openstapps/openstapps/database:2.0.0
|
image: registry.gitlab.com/openstapps/openstapps/database:3.0.0
|
||||||
volumes:
|
# If you need persistence for debugging purposes uncomment the following lines
|
||||||
- ./database:/usr/share/elasticsearch/data
|
#volumes:
|
||||||
|
# - ./database:/usr/share/elasticsearch/data
|
||||||
expose:
|
expose:
|
||||||
- "9200"
|
- 9200
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:9200:9200
|
||||||
|
environment:
|
||||||
|
- bootstrap.memory_lock=true
|
||||||
|
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||||
|
- discovery.type=single-node
|
||||||
|
ulimits:
|
||||||
|
memlock:
|
||||||
|
soft: -1
|
||||||
|
hard: -1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
image: registry.gitlab.com/openstapps/openstapps/backend:3.0.0-next.0
|
image: registry.gitlab.com/openstapps/openstapps/backend:3.1.0
|
||||||
environment:
|
environment:
|
||||||
|
<<: *development-variables
|
||||||
ES_ADDR: "http://database:9200"
|
ES_ADDR: "http://database:9200"
|
||||||
NODE_CONFIG_ENV: "elasticsearch"
|
NODE_CONFIG_ENV: "elasticsearch"
|
||||||
ALLOW_NO_TRANSPORT: "true"
|
NODE_APP_INSTANCE: "f-u"
|
||||||
|
PROMETHEUS_MIDDLEWARE: "false"
|
||||||
expose:
|
expose:
|
||||||
- 3000
|
- 3000
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 127.0.0.1:3000:3000
|
||||||
links:
|
|
||||||
- "database"
|
|
||||||
labels:
|
labels:
|
||||||
- stapps.version=1.0.0
|
- stapps.version=4.1.0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
|
|
||||||
api:
|
|
||||||
image: registry.gitlab.com/openstapps/openstapps/api:3.0.0-next.0
|
|
||||||
links:
|
links:
|
||||||
- "backend"
|
- database
|
||||||
|
|
||||||
minimal-connector:
|
# api:
|
||||||
image: registry.gitlab.com/openstapps/minimal-connector:core-0.23
|
# image: registry.gitlab.com/openstapps/openstapps/api:3.0.0
|
||||||
container_name: minimal-connector-0.23
|
# links:
|
||||||
command: ["http://backend:3000", "minimal-connector", "f-u"]
|
# - backend
|
||||||
|
|
||||||
app:
|
# minimal-connector:
|
||||||
image: registry.gitlab.com/openstapps/app/executable:core-0.23
|
# image: registry.gitlab.com/openstapps/minimal-connector:core-0.23
|
||||||
expose:
|
# container_name: minimal-connector-0.23
|
||||||
- 8100
|
# command: ["http://backend:3000", "minimal-connector", "f-u"]
|
||||||
ports:
|
|
||||||
- 8100:8100
|
# app:
|
||||||
|
# image: registry.gitlab.com/openstapps/app/executable:core-0.23
|
||||||
|
# expose:
|
||||||
|
# - 8100
|
||||||
|
# ports:
|
||||||
|
# - 8100:8100
|
||||||
|
|||||||
34
flake.lock
generated
34
flake.lock
generated
@@ -1,5 +1,23 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1701680307,
|
||||||
|
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1701626906,
|
"lastModified": 1701626906,
|
||||||
@@ -18,8 +36,24 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
88
flake.nix
88
flake.nix
@@ -1,21 +1,27 @@
|
|||||||
{
|
{
|
||||||
description = "A Nix-flake-based development environment for OpenStApps";
|
description = "A Nix-flake-based development environment for OpenStApps";
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
inputs = {
|
||||||
outputs = { self, nixpkgs }:
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
let
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
buildToolsVersion = "30.0.3";
|
};
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
}: let
|
||||||
|
aapt2buildToolsVersion = "33.0.2";
|
||||||
|
in
|
||||||
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
overlays = [
|
overlays = [
|
||||||
(final: prev: rec {
|
(final: prev: rec {
|
||||||
nodejs = prev.nodejs-18_x;
|
fontMin = prev.python311.withPackages (ps: with ps; [brotli fonttools] ++ (with fonttools.optional-dependencies; [woff]));
|
||||||
pnpm = prev.nodePackages.pnpm;
|
|
||||||
chrome = prev.google-chrome;
|
|
||||||
firefox = prev.firefox;
|
|
||||||
webkit = prev.epiphany; # Safari-ish browser
|
|
||||||
android = prev.androidenv.composeAndroidPackages {
|
android = prev.androidenv.composeAndroidPackages {
|
||||||
buildToolsVersions = [ "${buildToolsVersion}" ];
|
buildToolsVersions = ["30.0.3" aapt2buildToolsVersion];
|
||||||
platformVersions = [ "33" ];
|
platformVersions = ["33"];
|
||||||
};
|
};
|
||||||
cypress = prev.cypress.overrideAttrs(cyPrev: rec {
|
cypress = prev.cypress.overrideAttrs (cyPrev: rec {
|
||||||
version = "13.2.0";
|
version = "13.2.0";
|
||||||
src = prev.fetchzip {
|
src = prev.fetchzip {
|
||||||
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
|
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
|
||||||
@@ -24,54 +30,46 @@
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
# 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 = {
|
config = {
|
||||||
allowUnfree = true;
|
allowUnfree = true;
|
||||||
android_sdk.accept_license = true;
|
android_sdk.accept_license = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
androidFhs = pkgs.buildFHSUserEnv {
|
||||||
in
|
name = "android-env";
|
||||||
{
|
targetPkgs = pkgs: with pkgs; [];
|
||||||
devShells = forEachSupportedSystem ({ pkgs }:
|
runScript = "bash";
|
||||||
let
|
profile = ''
|
||||||
python = (pkgs.python311.withPackages(ps: with ps; [ brotli fonttools ] ++ (with fonttools.optional-dependencies; [ ufo lxml unicode woff ])));
|
export ALLOW_NINJA_ENV=true
|
||||||
in
|
export USE_CCACHE=1
|
||||||
{
|
export LD_LIBRARY_PATH=/usr/lib:/usr/lib32
|
||||||
default = (pkgs.buildFHSUserEnv {
|
'';
|
||||||
name = "StApps Dev";
|
};
|
||||||
targetPkgs = pkgs: with pkgs; [
|
in {
|
||||||
nodejs
|
devShell = pkgs.mkShell rec {
|
||||||
pnpm
|
nativeBuildInputs = [androidFhs];
|
||||||
python
|
buildInputs = with pkgs; [
|
||||||
docker
|
nodejs-18_x
|
||||||
|
nodePackages.pnpm
|
||||||
# tools
|
# tools
|
||||||
curl
|
curl
|
||||||
jq
|
jq
|
||||||
|
fontMin
|
||||||
# browsers
|
# browsers
|
||||||
firefox
|
firefox
|
||||||
chrome
|
google-chrome
|
||||||
webkit
|
epiphany # Safari-ish browser
|
||||||
cypress
|
cypress
|
||||||
# android
|
# android
|
||||||
jdk17
|
jdk17
|
||||||
android.androidsdk
|
android.androidsdk
|
||||||
musl
|
musl
|
||||||
];
|
];
|
||||||
runScript = "bash";
|
ANDROID_JAVA_HOME = "${pkgs.jdk.home}";
|
||||||
profile = ''
|
ANDROID_SDK_ROOT = "${pkgs.android.androidsdk}/libexec/android-sdk";
|
||||||
export CYPRESS_INSTALL_BINARY=0
|
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/${aapt2buildToolsVersion}/aapt2";
|
||||||
export CYPRESS_RUN_BINARY=${pkgs.cypress}/bin/Cypress
|
CYPRESS_INSTALL_BINARY = "0";
|
||||||
export ANDROID_SDK_ROOT=${pkgs.android.androidsdk}/libexec/android-sdk
|
CYPRESS_RUN_BINARY = "${pkgs.cypress}/bin/Cypress";
|
||||||
export ANDROID_JAVA_HOME=${pkgs.jdk.home}
|
|
||||||
export DOCKER_HOST=unix:///run/user/1000/docker.sock
|
|
||||||
{ dockerd-rootless & } 2>/dev/null
|
|
||||||
'';
|
|
||||||
}).env;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Creates a docker image with only the app as an executable unit
|
# Creates a docker image with only the app as an executable unit
|
||||||
# Dependencies need to be installed beforehand
|
# Dependencies need to be installed beforehand
|
||||||
# Needs to be build beforehand
|
# Needs to be build beforehand
|
||||||
FROM node:18-alpine3.18
|
FROM node:18-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY www/ /app/www
|
COPY www/ /app/www
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 StApps
|
* Copyright (C) 2022 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
@@ -13,8 +12,9 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
/** @type {import('./scripts/icon-config').IconConfig} */
|
import type {IconConfig} from './scripts/icon-config';
|
||||||
const config = {
|
|
||||||
|
const config: IconConfig = {
|
||||||
inputPath: 'node_modules/material-symbols/material-symbols-rounded.woff2',
|
inputPath: 'node_modules/material-symbols/material-symbols-rounded.woff2',
|
||||||
outputPath: 'src/assets/icons.min.woff2',
|
outputPath: 'src/assets/icons.min.woff2',
|
||||||
htmlGlob: 'src/**/*.html',
|
htmlGlob: 'src/**/*.html',
|
||||||
@@ -25,7 +25,7 @@ def capacitor_pods
|
|||||||
pod 'CapacitorPreferences', :path => '../../../../node_modules/.pnpm/@capacitor+preferences@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/preferences'
|
pod 'CapacitorPreferences', :path => '../../../../node_modules/.pnpm/@capacitor+preferences@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/preferences'
|
||||||
pod 'CapacitorShare', :path => '../../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share'
|
pod 'CapacitorShare', :path => '../../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share'
|
||||||
pod 'CapacitorSplashScreen', :path => '../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen'
|
pod 'CapacitorSplashScreen', :path => '../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen'
|
||||||
pod 'TransistorsoftCapacitorBackgroundFetch', :path => '../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@1.0.2_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch'
|
pod 'TransistorsoftCapacitorBackgroundFetch', :path => '../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.1.1_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch'
|
||||||
pod 'CapacitorSecureStoragePlugin', :path => '../../../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.0/node_modules/capacitor-secure-storage-plugin'
|
pod 'CapacitorSecureStoragePlugin', :path => '../../../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.0/node_modules/capacitor-secure-storage-plugin'
|
||||||
pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins'
|
pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ PODS:
|
|||||||
- CordovaPlugins (5.5.0):
|
- CordovaPlugins (5.5.0):
|
||||||
- CapacitorCordova
|
- CapacitorCordova
|
||||||
- SwiftKeychainWrapper (4.0.1)
|
- SwiftKeychainWrapper (4.0.1)
|
||||||
- TransistorsoftCapacitorBackgroundFetch (1.0.2):
|
- TransistorsoftCapacitorBackgroundFetch (5.1.1):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
@@ -58,7 +58,7 @@ DEPENDENCIES:
|
|||||||
- "CapacitorShare (from `../../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share`)"
|
- "CapacitorShare (from `../../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share`)"
|
||||||
- "CapacitorSplashScreen (from `../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen`)"
|
- "CapacitorSplashScreen (from `../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen`)"
|
||||||
- CordovaPlugins (from `../capacitor-cordova-ios-plugins`)
|
- CordovaPlugins (from `../capacitor-cordova-ios-plugins`)
|
||||||
- "TransistorsoftCapacitorBackgroundFetch (from `../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@1.0.2_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch`)"
|
- "TransistorsoftCapacitorBackgroundFetch (from `../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.1.1_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch`)"
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@@ -102,7 +102,7 @@ EXTERNAL SOURCES:
|
|||||||
CordovaPlugins:
|
CordovaPlugins:
|
||||||
:path: "../capacitor-cordova-ios-plugins"
|
:path: "../capacitor-cordova-ios-plugins"
|
||||||
TransistorsoftCapacitorBackgroundFetch:
|
TransistorsoftCapacitorBackgroundFetch:
|
||||||
:path: "../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@1.0.2_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch"
|
:path: "../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.1.1_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Capacitor: 57890b363df14d5d2d5d8461aa23e886cb34da2a
|
Capacitor: 57890b363df14d5d2d5d8461aa23e886cb34da2a
|
||||||
@@ -124,8 +124,8 @@ SPEC CHECKSUMS:
|
|||||||
CapacitorSplashScreen: 5fa2ab5e46cf5cc530cf16a51c80c7a986579ccd
|
CapacitorSplashScreen: 5fa2ab5e46cf5cc530cf16a51c80c7a986579ccd
|
||||||
CordovaPlugins: de5669381702d76ed5b1d442177a6a5fc3252a9d
|
CordovaPlugins: de5669381702d76ed5b1d442177a6a5fc3252a9d
|
||||||
SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c
|
SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c
|
||||||
TransistorsoftCapacitorBackgroundFetch: 74ca62dae7ec78639eaf3d0d1e24c595ada213dd
|
TransistorsoftCapacitorBackgroundFetch: ce4b3e01b898cef516e68485d2160a078016ee97
|
||||||
|
|
||||||
PODFILE CHECKSUM: 073b899f90bacc5049101cb9c562a168757d554e
|
PODFILE CHECKSUM: 229278f2c257e8ab555325c7115b2e187e8e628d
|
||||||
|
|
||||||
COCOAPODS: 1.13.0
|
COCOAPODS: 1.13.0
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 StApps
|
* Copyright (C) 2022 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"build:prod": "ng build --configuration=production",
|
"build:prod": "ng build --configuration=production",
|
||||||
"build:stats": "ng build --configuration=production --stats-json",
|
"build:stats": "ng build --configuration=production --stats-json",
|
||||||
"changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0",
|
"changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0",
|
||||||
"check-icons": "node scripts/check-icon-correctness.mjs",
|
"check-icons": "ts-node-esm scripts/check-icon-correctness.ts",
|
||||||
"chromium:no-cors": "chromium --disable-web-security --user-data-dir=\".browser-data/chromium\"",
|
"chromium:no-cors": "chromium --disable-web-security --user-data-dir=\".browser-data/chromium\"",
|
||||||
"chromium:virtual-host": "chromium --host-resolver-rules=\"MAP mobile.app.uni-frankfurt.de:* localhost:8100\" --ignore-certificate-errors",
|
"chromium:virtual-host": "chromium --host-resolver-rules=\"MAP mobile.app.uni-frankfurt.de:* localhost:8100\" --ignore-certificate-errors",
|
||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"licenses": "license-checker --json > src/assets/about/licenses.json && ts-node ./scripts/accumulate-licenses.ts && git add src/assets/about/licenses.json",
|
"licenses": "license-checker --json > src/assets/about/licenses.json && ts-node ./scripts/accumulate-licenses.ts && git add src/assets/about/licenses.json",
|
||||||
"lint": "ng lint && stylelint \"**/*.scss\"",
|
"lint": "ng lint && stylelint \"**/*.scss\"",
|
||||||
"lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/ && stylelint --fix \"**/*.scss\"",
|
"lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/ && stylelint --fix \"**/*.scss\"",
|
||||||
"minify-icons": "node scripts/minify-icon-font.mjs",
|
"minify-icons": "ts-node-esm scripts/minify-icon-font.ts",
|
||||||
"postinstall": "jetify && echo \"skipping jetify in production mode\"",
|
"postinstall": "jetify && echo \"skipping jetify in production mode\"",
|
||||||
"preview": "http-server www --p 8101 -o",
|
"preview": "http-server www --p 8101 -o",
|
||||||
"push": "git push && git push origin \"v$npm_package_version\"",
|
"push": "git push && git push origin \"v$npm_package_version\"",
|
||||||
@@ -93,6 +93,7 @@
|
|||||||
"cordova-plugin-calendar": "5.1.6",
|
"cordova-plugin-calendar": "5.1.6",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
|
"fast-deep-equal": "3.1.3",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"geojson": "0.5.0",
|
"geojson": "0.5.0",
|
||||||
"ionic-appauth": "0.9.0",
|
"ionic-appauth": "0.9.0",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 StApps
|
* Copyright (C) 2022 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
@@ -14,14 +13,13 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import {omit, pickBy} from '@openstapps/collection-utils';
|
import {omit} from '../src/app/_helpers/collections/omit';
|
||||||
|
import {pickBy} from '../src/app/_helpers/collections/pick';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* accumulate and transform licenses based on two license files
|
* accumulate and transform licenses based on two license files
|
||||||
* @param {string} path
|
|
||||||
* @param {string} additionalLicensesPath
|
|
||||||
*/
|
*/
|
||||||
function accumulateFile(path, additionalLicensesPath) {
|
function accumulateFile(path: string, additionalLicensesPath: string) {
|
||||||
const packageJson = JSON.parse(fs.readFileSync('./package.json').toString());
|
const packageJson = JSON.parse(fs.readFileSync('./package.json').toString());
|
||||||
const dependencies = packageJson.dependencies;
|
const dependencies = packageJson.dependencies;
|
||||||
|
|
||||||
@@ -30,9 +28,10 @@ function accumulateFile(path, additionalLicensesPath) {
|
|||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path,
|
path,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
Object.entries({
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
...pickBy(JSON.parse(fs.readFileSync(path).toString()), (_, key) => {
|
Object.entries<any>({
|
||||||
const parts = /** @type {string} */ (key).split('@');
|
...pickBy(JSON.parse(fs.readFileSync(path).toString()), (_, key: string) => {
|
||||||
|
const parts = key.split('@');
|
||||||
|
|
||||||
return dependencies[parts.slice(0, -1).join('@')] === parts[parts.length - 1];
|
return dependencies[parts.slice(0, -1).join('@')] === parts[parts.length - 1];
|
||||||
}),
|
}),
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 StApps
|
* Copyright (C) 2022 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
@@ -13,18 +12,18 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {openSync} from 'fontkit';
|
import fontkit, {Font} from 'fontkit';
|
||||||
import config from '../icons.config.mjs';
|
import config from '../icons.config';
|
||||||
import {existsSync} from 'fs';
|
import {existsSync} from 'fs';
|
||||||
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs';
|
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons';
|
||||||
|
|
||||||
const commandName = '"npm run minify-icons"';
|
const commandName = '"npm run minify-icons"';
|
||||||
const originalFont = openSync(config.inputPath);
|
const originalFont = fontkit.openSync(config.inputPath);
|
||||||
if (!existsSync(config.outputPath)) {
|
if (!existsSync(config.outputPath)) {
|
||||||
console.error(`Minified font not found. Run ${commandName} first.`);
|
console.error(`Minified font not found. Run ${commandName} first.`);
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
const modifiedFont = openSync(config.outputPath);
|
const modifiedFont = fontkit.openSync(config.outputPath);
|
||||||
|
|
||||||
let success = true;
|
let success = true;
|
||||||
|
|
||||||
@@ -49,9 +48,9 @@ async function checkAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Record<string, string[]>} icons
|
*
|
||||||
*/
|
*/
|
||||||
function check(icons) {
|
function check(icons: Record<string, string[]>) {
|
||||||
for (const [purpose, iconSet] of Object.entries(icons)) {
|
for (const [purpose, iconSet] of Object.entries(icons)) {
|
||||||
for (const icon of iconSet) {
|
for (const icon of iconSet) {
|
||||||
if (!hasIcon(originalFont, icon)) {
|
if (!hasIcon(originalFont, icon)) {
|
||||||
@@ -66,9 +65,8 @@ function check(icons) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('fontkit').Font} font
|
*
|
||||||
* @param {string} icon
|
|
||||||
*/
|
*/
|
||||||
function hasIcon(font, icon) {
|
function hasIcon(font: Font, icon: string) {
|
||||||
return font.layout(icon).glyphs.some(it => it.isLigature);
|
return font.layout(icon).glyphs.some(it => it.isLigature);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 StApps
|
* Copyright (C) 2022 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
@@ -15,33 +14,31 @@
|
|||||||
*/
|
*/
|
||||||
import {glob} from 'glob';
|
import {glob} from 'glob';
|
||||||
import {readFileSync} from 'fs';
|
import {readFileSync} from 'fs';
|
||||||
import {matchPropertyContent, matchTagProperties} from '../src/app/util/ion-icon/icon-match.mjs';
|
import {matchPropertyContent, matchTagProperties} from '../src/app/util/ion-icon/icon-match';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Promise<Record<string, string[]>>}
|
*
|
||||||
*/
|
*/
|
||||||
export async function getUsedIconsHtml(pattern = 'src/**/*.html') {
|
export async function getUsedIconsHtml(pattern = 'src/**/*.html'): Promise<Record<string, string[]>> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
(await glob(pattern))
|
(await glob(pattern))
|
||||||
.map(file => [
|
.map(file => [
|
||||||
file,
|
file,
|
||||||
/** @type {string[]} */ (
|
(readFileSync(file, 'utf8')
|
||||||
readFileSync(file, 'utf8')
|
|
||||||
.match(matchTagProperties('ion-icon'))
|
.match(matchTagProperties('ion-icon'))
|
||||||
?.flatMap(match => {
|
?.flatMap(match => {
|
||||||
return match.match(matchPropertyContent(['name', 'md', 'ios']));
|
return match.match(matchPropertyContent(['name', 'md', 'ios']));
|
||||||
})
|
})
|
||||||
.filter(it => !!it)
|
.filter(it => !!it) as string[]) || [],
|
||||||
) || [],
|
|
||||||
])
|
])
|
||||||
.filter(([, values]) => values.length > 0),
|
.filter(([, values]) => values.length > 0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Promise<Record<string, string[]>>}
|
*
|
||||||
*/
|
*/
|
||||||
export async function getUsedIconsTS(pattern = 'src/**/*.ts') {
|
export async function getUsedIconsTS(pattern = 'src/**/*.ts'): Promise<Record<string, string[]>> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
(await glob(pattern))
|
(await glob(pattern))
|
||||||
.map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []])
|
.map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []])
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 StApps
|
* Copyright (C) 2022 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
@@ -13,17 +12,18 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {openSync} from 'fontkit';
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import fontkit from 'fontkit';
|
||||||
import {exec} from 'child_process';
|
import {exec} from 'child_process';
|
||||||
import config from '../icons.config.mjs';
|
import config from '../icons.config';
|
||||||
import {statSync} from 'fs';
|
import {statSync} from 'fs';
|
||||||
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs';
|
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string[] | string} command
|
*
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
*/
|
||||||
async function run(command) {
|
async function run(command: string[] | string): Promise<string> {
|
||||||
const fullCommand = Array.isArray(command) ? command.join(' ') : command;
|
const fullCommand = Array.isArray(command) ? command.join(' ') : command;
|
||||||
console.log(`>> ${fullCommand}`);
|
console.log(`>> ${fullCommand}`);
|
||||||
|
|
||||||
@@ -44,8 +44,7 @@ async function run(command) {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async function minifyIconFont() {
|
async function minifyIconFont() {
|
||||||
/** @type {Set<string>} */
|
const icons = new Set<string>();
|
||||||
const icons = new Set();
|
|
||||||
|
|
||||||
for (const iconSet of [
|
for (const iconSet of [
|
||||||
...Object.values(config.additionalIcons || []),
|
...Object.values(config.additionalIcons || []),
|
||||||
@@ -58,10 +57,9 @@ async function minifyIconFont() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Icons used:', [...icons.values()].sort());
|
console.log('Icons used:', [...icons.values()].sort());
|
||||||
const font = openSync(config.inputPath);
|
const font = fontkit.openSync(config.inputPath);
|
||||||
|
|
||||||
/** @type {string[]} */
|
const glyphs: string[] = ['5f-7a', '30-39'];
|
||||||
const glyphs = ['5f-7a', '30-39'];
|
|
||||||
for (const icon of icons) {
|
for (const icon of icons) {
|
||||||
const iconGlyphs = font.layout(icon).glyphs;
|
const iconGlyphs = font.layout(icon).glyphs;
|
||||||
if (iconGlyphs.length === 0) {
|
if (iconGlyphs.length === 0) {
|
||||||
@@ -72,7 +70,7 @@ async function minifyIconFont() {
|
|||||||
const codePoints = iconGlyphs
|
const codePoints = iconGlyphs
|
||||||
.flatMap(it => font.stringsForGlyph(it.id))
|
.flatMap(it => font.stringsForGlyph(it.id))
|
||||||
.flatMap(it => [...it])
|
.flatMap(it => [...it])
|
||||||
.map(it => it.codePointAt(0).toString(16));
|
.map(it => it.codePointAt(0)!.toString(16));
|
||||||
|
|
||||||
if (codePoints.length === 0) {
|
if (codePoints.length === 0) {
|
||||||
if (config.codePoints?.[icon]) {
|
if (config.codePoints?.[icon]) {
|
||||||
@@ -116,10 +114,8 @@ minifyIconFont();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Bytes to respective units
|
* Bytes to respective units
|
||||||
* @param {number} value
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
function toByteUnit(value) {
|
function toByteUnit(value: number): string {
|
||||||
if (value < 1024) {
|
if (value < 1024) {
|
||||||
return `${value}B`;
|
return `${value}B`;
|
||||||
} else if (value < 1024 * 1024) {
|
} else if (value < 1024 * 1024) {
|
||||||
7
frontend/app/scripts/tsconfig.json
Normal file
7
frontend/app/scripts/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "@openstapps/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node"
|
||||||
|
}
|
||||||
|
}
|
||||||
80
frontend/app/src/app/animation/splash.ts
Normal file
80
frontend/app/src/app/animation/splash.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {Animation, AnimationController} from '@ionic/angular';
|
||||||
|
import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splash screen animation
|
||||||
|
*/
|
||||||
|
export function splashAnimation(animationCtl: AnimationController): Animation {
|
||||||
|
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
return animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('opacity', 0, 1)
|
||||||
|
.duration(150)
|
||||||
|
.beforeClearStyles(['visibility'])
|
||||||
|
.addElement(document.querySelector('ion-app')!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMd = document.querySelector('ion-app.md') !== null;
|
||||||
|
const navElement = document.querySelector('stapps-navigation-tabs')!;
|
||||||
|
const navBounds = navElement.getBoundingClientRect();
|
||||||
|
let horizontal = navBounds.width < navBounds.height;
|
||||||
|
if (window.getComputedStyle(navElement).display === 'none') {
|
||||||
|
horizontal = true;
|
||||||
|
}
|
||||||
|
const translate = (amount: number, unit = 'px') =>
|
||||||
|
`translate${horizontal ? 'X' : 'Y'}(${horizontal ? amount * -1 : amount}${unit})`;
|
||||||
|
const duration = 2 * (isMd ? mdDuration : iosDuration);
|
||||||
|
|
||||||
|
const animation = animationCtl
|
||||||
|
.create()
|
||||||
|
.duration(duration)
|
||||||
|
.easing(isMd ? mdEasing : iosEasing)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl.create().beforeClearStyles(['visibility']).addElement(document.querySelector('ion-app')!),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(horizontal ? 64 : 192), translate(0))
|
||||||
|
.fromTo('opacity', 0, 1)
|
||||||
|
.addElement(document.querySelector('stapps-navigation > ion-split-pane')!),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(64), translate(0))
|
||||||
|
.addElement(document.querySelectorAll('ion-split-pane > ion-menu > ion-content')),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(horizontal ? 32 : -72), translate(0))
|
||||||
|
.addElement(document.querySelectorAll('ion-router-outlet > .ion-page > ion-content')!),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(100, '%'), translate(0, '%'))
|
||||||
|
.addElement(document.querySelector('stapps-navigation-tabs')!),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!horizontal) {
|
||||||
|
animation.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('background', 'none', 'none')
|
||||||
|
.addElement(document.querySelector('ion-router-outlet')!),
|
||||||
|
);
|
||||||
|
|
||||||
|
const parallax = document
|
||||||
|
.querySelector('ion-router-outlet > .ion-page > ion-content')
|
||||||
|
?.shadowRoot?.querySelector('[part=parallax]');
|
||||||
|
if (parallax) {
|
||||||
|
animation.addAnimation(
|
||||||
|
animationCtl.create().fromTo('translate', '0 256px', '0 0px').addElement(parallax),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
@@ -22,28 +22,16 @@ import {environment} from '../environments/environment';
|
|||||||
import {Capacitor} from '@capacitor/core';
|
import {Capacitor} from '@capacitor/core';
|
||||||
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
|
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
|
||||||
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
|
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
|
||||||
import {AppVersionService} from './modules/about/app-version.service';
|
|
||||||
import {SplashScreen} from '@capacitor/splash-screen';
|
import {SplashScreen} from '@capacitor/splash-screen';
|
||||||
|
import {AppVersionService} from './modules/about/app-version.service';
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: 'app.component.html',
|
templateUrl: 'app.component.html',
|
||||||
})
|
})
|
||||||
export class AppComponent implements AfterContentInit {
|
export class AppComponent implements AfterContentInit {
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
pages: Array<{
|
pages: Array<{
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
component: unknown;
|
component: unknown;
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
title: string;
|
title: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -65,7 +53,7 @@ export class AppComponent implements AfterContentInit {
|
|||||||
void this.initializeApp();
|
void this.initializeApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngAfterContentInit() {
|
ngAfterContentInit() {
|
||||||
this.scheduleSyncService.init();
|
this.scheduleSyncService.init();
|
||||||
void this.scheduleSyncService.enable();
|
void this.scheduleSyncService.enable();
|
||||||
this.versionService.getPendingReleaseNotes().then(notes => {
|
this.versionService.getPendingReleaseNotes().then(notes => {
|
||||||
@@ -74,24 +62,11 @@ export class AppComponent implements AfterContentInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (document.readyState === 'complete') {
|
|
||||||
this.hideSplash();
|
|
||||||
} else {
|
|
||||||
document.addEventListener('readystatechange', () => {
|
|
||||||
if (document.readyState === 'complete') this.hideSplash();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async hideSplash() {
|
|
||||||
if (Capacitor.isNativePlatform()) {
|
if (Capacitor.isNativePlatform()) {
|
||||||
void SplashScreen.hide();
|
void SplashScreen.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
async initializeApp() {
|
async initializeApp() {
|
||||||
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
|
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
|
|||||||
@@ -25,12 +25,10 @@ import moment from 'moment';
|
|||||||
import 'moment/min/locales';
|
import 'moment/min/locales';
|
||||||
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
|
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
|
||||||
import SwiperCore, {FreeMode, Navigation} from 'swiper';
|
import SwiperCore, {FreeMode, Navigation} from 'swiper';
|
||||||
|
|
||||||
import {environment} from '../environments/environment';
|
import {environment} from '../environments/environment';
|
||||||
import {AppRoutingModule} from './app-routing.module';
|
import {AppRoutingModule} from './app-routing.module';
|
||||||
import {AppComponent} from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
import {CatalogModule} from './modules/catalog/catalog.module';
|
import {CatalogModule} from './modules/catalog/catalog.module';
|
||||||
import {ConfigModule} from './modules/config/config.module';
|
|
||||||
import {ConfigProvider} from './modules/config/config.provider';
|
import {ConfigProvider} from './modules/config/config.provider';
|
||||||
import {DashboardModule} from './modules/dashboard/dashboard.module';
|
import {DashboardModule} from './modules/dashboard/dashboard.module';
|
||||||
import {DataModule} from './modules/data/data.module';
|
import {DataModule} from './modules/data/data.module';
|
||||||
@@ -44,7 +42,6 @@ import {SettingsProvider} from './modules/settings/settings.provider';
|
|||||||
import {StorageModule} from './modules/storage/storage.module';
|
import {StorageModule} from './modules/storage/storage.module';
|
||||||
import {ThingTranslateModule} from './translation/thing-translate.module';
|
import {ThingTranslateModule} from './translation/thing-translate.module';
|
||||||
import {UtilModule} from './util/util.module';
|
import {UtilModule} from './util/util.module';
|
||||||
import {initLogger} from './_helpers/ts-logger';
|
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import {AboutModule} from './modules/about/about.module';
|
import {AboutModule} from './modules/about/about.module';
|
||||||
import {JobModule} from './modules/jobs/jobs.module';
|
import {JobModule} from './modules/jobs/jobs.module';
|
||||||
@@ -91,11 +88,8 @@ export function initializerFactory(
|
|||||||
) {
|
) {
|
||||||
return async () => {
|
return async () => {
|
||||||
try {
|
try {
|
||||||
initLogger(logger);
|
|
||||||
await storageProvider.init();
|
await storageProvider.init();
|
||||||
await configProvider.init();
|
await configProvider.init();
|
||||||
await settingsProvider.init();
|
|
||||||
try {
|
|
||||||
if (configProvider.firstSession) {
|
if (configProvider.firstSession) {
|
||||||
// set language from browser
|
// set language from browser
|
||||||
await settingsProvider.setSettingValue(
|
await settingsProvider.setSettingValue(
|
||||||
@@ -104,7 +98,7 @@ export function initializerFactory(
|
|||||||
translateService.getBrowserLang() as SCSettingValue,
|
translateService.getBrowserLang() as SCSettingValue,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const languageCode = (await settingsProvider.getValue('profile', 'language')) as string;
|
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
|
// this language will be used as a fallback when a translation isn't found in the current language
|
||||||
translateService.setDefaultLang('en');
|
translateService.setDefaultLang('en');
|
||||||
translateService.use(languageCode);
|
translateService.use(languageCode);
|
||||||
@@ -112,7 +106,7 @@ export function initializerFactory(
|
|||||||
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
|
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
|
||||||
setDefaultOptions({locale: dateFnsLocale});
|
setDefaultOptions({locale: dateFnsLocale});
|
||||||
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
||||||
|
try {
|
||||||
await defaultAuthService.init();
|
await defaultAuthService.init();
|
||||||
await paiaAuthService.init();
|
await paiaAuthService.init();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -151,11 +145,12 @@ export function createTranslateLoader(http: HttpClient) {
|
|||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
CatalogModule,
|
CatalogModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ConfigModule,
|
|
||||||
DashboardModule,
|
DashboardModule,
|
||||||
DataModule,
|
DataModule,
|
||||||
HebisModule,
|
HebisModule,
|
||||||
IonicModule.forRoot(),
|
IonicModule.forRoot({
|
||||||
|
animated: 'Cypress' in window ? false : undefined,
|
||||||
|
}),
|
||||||
IonIconModule,
|
IonIconModule,
|
||||||
JobModule,
|
JobModule,
|
||||||
FavoritesModule,
|
FavoritesModule,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
|
import {SCAboutPage} from '@openstapps/core';
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
import packageJson from '../../../../../package.json';
|
import packageJson from '../../../../../package.json';
|
||||||
import config from 'capacitor.config';
|
import config from 'capacitor.config';
|
||||||
@@ -42,8 +42,7 @@ export class AboutPageComponent implements OnInit {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const route = this.route.snapshot.url.map(it => it.path).join('/');
|
const route = this.route.snapshot.url.map(it => it.path).join('/');
|
||||||
this.content =
|
this.content = this.configProvider.config.app.aboutPages[route] ?? {};
|
||||||
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
|
|
||||||
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
|
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {FormsModule} from '@angular/forms';
|
|||||||
import {IonicModule} from '@ionic/angular';
|
import {IonicModule} from '@ionic/angular';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
|
||||||
import {AboutPageComponent} from './about-page/about-page.component';
|
import {AboutPageComponent} from './about-page/about-page.component';
|
||||||
import {MarkdownModule} from 'ngx-markdown';
|
import {MarkdownModule} from 'ngx-markdown';
|
||||||
import {AboutPageContentComponent} from './about-page/about-page-content.component';
|
import {AboutPageContentComponent} from './about-page/about-page-content.component';
|
||||||
@@ -64,6 +63,5 @@ const settingsRoutes: Routes = [
|
|||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [ConfigProvider],
|
|
||||||
})
|
})
|
||||||
export class AboutModule {}
|
export class AboutModule {}
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ import {IPAIAAuthAction} from './paia/paia-auth-action';
|
|||||||
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {JSONPath} from 'jsonpath-plus';
|
import {JSONPath} from 'jsonpath-plus';
|
||||||
import {
|
import {SCAuthorizationProviderType, SCUserConfiguration} from '@openstapps/core';
|
||||||
SCAuthorizationProvider,
|
|
||||||
SCAuthorizationProviderType,
|
|
||||||
SCUserConfiguration,
|
|
||||||
SCUserConfigurationMap,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {DefaultAuthService} from './default-auth.service';
|
import {DefaultAuthService} from './default-auth.service';
|
||||||
@@ -37,8 +32,6 @@ const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthHelperService {
|
export class AuthHelperService {
|
||||||
userConfigurationMap: SCUserConfigurationMap;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private configProvider: ConfigProvider,
|
private configProvider: ConfigProvider,
|
||||||
@@ -47,14 +40,7 @@ export class AuthHelperService {
|
|||||||
private paiaAuth: PAIAAuthService,
|
private paiaAuth: PAIAAuthService,
|
||||||
private browser: SimpleBrowser,
|
private browser: SimpleBrowser,
|
||||||
private alertController: AlertController,
|
private alertController: AlertController,
|
||||||
) {
|
) {}
|
||||||
this.userConfigurationMap =
|
|
||||||
(
|
|
||||||
this.configProvider.getAnyValue('auth') as {
|
|
||||||
default: SCAuthorizationProvider;
|
|
||||||
}
|
|
||||||
).default?.endpoints.mapping ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
|
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
|
||||||
let message: string | undefined;
|
let message: string | undefined;
|
||||||
@@ -77,9 +63,10 @@ export class AuthHelperService {
|
|||||||
name: '',
|
name: '',
|
||||||
role: 'student',
|
role: 'student',
|
||||||
};
|
};
|
||||||
for (const key in this.userConfigurationMap) {
|
const mapping = this.configProvider.config.auth.default!.endpoints.mapping;
|
||||||
|
for (const key in mapping) {
|
||||||
user[key as keyof SCUserConfiguration] = JSONPath({
|
user[key as keyof SCUserConfiguration] = JSONPath({
|
||||||
path: this.userConfigurationMap[key as keyof SCUserConfiguration] as string,
|
path: mapping[key as keyof SCUserConfiguration] as string,
|
||||||
json: userInfo,
|
json: userInfo,
|
||||||
preventEval: true,
|
preventEval: true,
|
||||||
})[0];
|
})[0];
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthorizationRequestHandler,
|
AuthorizationRequestHandler,
|
||||||
AuthorizationServiceConfiguration,
|
AuthorizationServiceConfiguration,
|
||||||
@@ -24,7 +23,6 @@ import {
|
|||||||
} from '@openid/appauth';
|
} from '@openid/appauth';
|
||||||
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
|
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
|
||||||
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
|
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {AuthService} from './auth.service';
|
import {AuthService} from './auth.service';
|
||||||
@@ -67,12 +65,9 @@ export class DefaultAuthService extends AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupConfiguration() {
|
setupConfiguration() {
|
||||||
const authConfig = this.configProvider.getAnyValue('auth') as {
|
this.authConfig = getClientConfig('default', this.configProvider.config.auth);
|
||||||
default: SCAuthorizationProvider;
|
|
||||||
};
|
|
||||||
this.authConfig = getClientConfig('default', authConfig);
|
|
||||||
this.localConfiguration = new AuthorizationServiceConfiguration(
|
this.localConfiguration = new AuthorizationServiceConfiguration(
|
||||||
getEndpointsConfig('default', authConfig),
|
getEndpointsConfig('default', this.configProvider.config.auth),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthorizationError,
|
AuthorizationError,
|
||||||
AuthorizationRequest,
|
AuthorizationRequest,
|
||||||
@@ -47,7 +46,6 @@ import {PAIAAuthorizationResponse} from './paia-authorization-response';
|
|||||||
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
|
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
|
||||||
import {PAIATokenResponse} from './paia-token-response';
|
import {PAIATokenResponse} from './paia-token-response';
|
||||||
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
|
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
|
||||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
|
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
@@ -154,11 +152,10 @@ export class PAIAAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupConfiguration() {
|
setupConfiguration() {
|
||||||
const authConfig = this.configProvider.getAnyValue('auth') as {
|
this.authConfig = getClientConfig('paia', this.configProvider.config.auth);
|
||||||
paia: SCAuthorizationProvider;
|
this.localConfiguration = new AuthorizationServiceConfiguration(
|
||||||
};
|
getEndpointsConfig('paia', this.configProvider.config.auth),
|
||||||
this.authConfig = getClientConfig('paia', authConfig);
|
);
|
||||||
this.localConfiguration = new AuthorizationServiceConfiguration(getEndpointsConfig('paia', authConfig));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected notifyActionListers(action: IPAIAAuthAction) {
|
protected notifyActionListers(action: IPAIAAuthAction) {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
|
import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {ICalEvent} from './ical/ical';
|
import {ICalEvent} from './ical/ical';
|
||||||
@@ -35,14 +34,14 @@ export class CalendarService {
|
|||||||
|
|
||||||
goToDateClicked = this.goToDate.asObservable();
|
goToDateClicked = this.goToDate.asObservable();
|
||||||
|
|
||||||
calendarName = 'StApps';
|
calendarName: string;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
constructor(
|
constructor(
|
||||||
readonly calendar: Calendar,
|
readonly calendar: Calendar,
|
||||||
private readonly configProvider: ConfigProvider,
|
private readonly configProvider: ConfigProvider,
|
||||||
) {
|
) {
|
||||||
this.calendarName = (this.configProvider.getValue('name') as string) ?? 'StApps';
|
this.calendarName = this.configProvider.config.app.name ?? 'StApps';
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCalendar(): Promise<CalendarInfo | undefined> {
|
async createCalendar(): Promise<CalendarInfo | undefined> {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {IonicModule} from '@ionic/angular';
|
|||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {MomentModule} from 'ngx-moment';
|
import {MomentModule} from 'ngx-moment';
|
||||||
import {DataModule} from '../data/data.module';
|
import {DataModule} from '../data/data.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {CatalogComponent} from './catalog.component';
|
import {CatalogComponent} from './catalog.component';
|
||||||
import {UtilModule} from '../../util/util.module';
|
import {UtilModule} from '../../util/util.module';
|
||||||
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
||||||
@@ -46,6 +45,5 @@ const catalogRoutes: Routes = [
|
|||||||
DataModule,
|
DataModule,
|
||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [SettingsProvider],
|
|
||||||
})
|
})
|
||||||
export class CatalogModule {}
|
export class CatalogModule {}
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ import {TestBed} from '@angular/core/testing';
|
|||||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
|
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
|
||||||
import {
|
|
||||||
ConfigFetchError,
|
|
||||||
ConfigInitError,
|
|
||||||
SavedConfigNotAvailable,
|
|
||||||
WrongConfigVersionInStorage,
|
|
||||||
} from './errors';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';
|
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';
|
||||||
|
|
||||||
|
|||||||
@@ -14,19 +14,14 @@
|
|||||||
*/
|
*/
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {Client} from '@openstapps/api';
|
import {Client} from '@openstapps/api';
|
||||||
import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core';
|
import {SCIndexResponse} from '@openstapps/core';
|
||||||
import packageInfo from '@openstapps/core/package.json';
|
import packageInfo from '@openstapps/core/package.json';
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {environment} from '../../../environments/environment';
|
import {environment} from '../../../environments/environment';
|
||||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {
|
import equals from 'fast-deep-equal/es6';
|
||||||
ConfigFetchError,
|
import {BehaviorSubject} from 'rxjs';
|
||||||
ConfigInitError,
|
|
||||||
ConfigValueNotAvailable,
|
|
||||||
SavedConfigNotAvailable,
|
|
||||||
WrongConfigVersionInStorage,
|
|
||||||
} from './errors';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key to store config in storage module
|
* Key to store config in storage module
|
||||||
@@ -35,6 +30,17 @@ import {
|
|||||||
*/
|
*/
|
||||||
export const STORAGE_KEY_CONFIG = 'stapps.config';
|
export const STORAGE_KEY_CONFIG = 'stapps.config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an object deeply immutable
|
||||||
|
*/
|
||||||
|
function deepFreeze<T extends object>(object: T) {
|
||||||
|
for (const key of Object.keys(object)) {
|
||||||
|
const value = (object as Record<string, unknown>)[key];
|
||||||
|
if (typeof value === 'object' && !Object.isFrozen(value)) deepFreeze(value!);
|
||||||
|
}
|
||||||
|
return Object.freeze(object);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides configuration
|
* Provides configuration
|
||||||
*/
|
*/
|
||||||
@@ -50,7 +56,7 @@ export class ConfigProvider {
|
|||||||
/**
|
/**
|
||||||
* App configuration as IndexResponse
|
* App configuration as IndexResponse
|
||||||
*/
|
*/
|
||||||
config: SCIndexResponse;
|
config: Readonly<SCIndexResponse>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version of the @openstapps/core package that app is using
|
* Version of the @openstapps/core package that app is using
|
||||||
@@ -62,6 +68,11 @@ export class ConfigProvider {
|
|||||||
*/
|
*/
|
||||||
firstSession = true;
|
firstSession = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the config requires an update
|
||||||
|
*/
|
||||||
|
needsUpdate$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor, initialise api client
|
* Constructor, initialise api client
|
||||||
* @param storageProvider StorageProvider to load persistent configuration
|
* @param storageProvider StorageProvider to load persistent configuration
|
||||||
@@ -76,104 +87,35 @@ export class ConfigProvider {
|
|||||||
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
|
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches configuration from backend
|
|
||||||
*/
|
|
||||||
async fetch(): Promise<SCIndexResponse> {
|
|
||||||
try {
|
|
||||||
return await this.client.handshake(this.scVersion);
|
|
||||||
} catch {
|
|
||||||
throw new ConfigFetchError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value of an app configuration
|
|
||||||
* @param attribute requested attribute from app configuration
|
|
||||||
*/
|
|
||||||
public getValue(attribute: keyof SCAppConfiguration) {
|
|
||||||
if (this.config.app[attribute] !== undefined) {
|
|
||||||
return this.config.app[attribute];
|
|
||||||
}
|
|
||||||
throw new ConfigValueNotAvailable(attribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a value of the configuration (not only app configuration)
|
|
||||||
* @param attribute requested attribute from the configuration
|
|
||||||
*/
|
|
||||||
public getAnyValue(attribute: keyof SCIndexResponse) {
|
|
||||||
if (this.config[attribute] !== undefined) {
|
|
||||||
return this.config[attribute];
|
|
||||||
}
|
|
||||||
throw new ConfigValueNotAvailable(attribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialises the ConfigProvider
|
* Initialises the ConfigProvider
|
||||||
* @throws ConfigInitError if no configuration could be loaded.
|
|
||||||
* @throws WrongConfigVersionInStorage if fetch failed and saved config has wrong SCVersion
|
|
||||||
*/
|
*/
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
let loadError;
|
this.config = (await this.storageProvider.has(STORAGE_KEY_CONFIG))
|
||||||
let fetchError;
|
? await this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG)
|
||||||
// load saved configuration
|
: undefined!;
|
||||||
try {
|
this.firstSession = !this.config;
|
||||||
this.config = await this.loadLocal();
|
|
||||||
this.firstSession = false;
|
const updatedConfig = this.client.handshake(this.scVersion).then(async fetchedConfig => {
|
||||||
this.logger.log(`initialised configuration from storage`);
|
if (!equals(fetchedConfig, this.config)) {
|
||||||
|
await this.storageProvider.put(STORAGE_KEY_CONFIG, fetchedConfig);
|
||||||
|
this.logger.log(`Config updated`);
|
||||||
|
this.needsUpdate$.next(true);
|
||||||
|
this.needsUpdate$.complete();
|
||||||
|
}
|
||||||
|
return fetchedConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.config ??= await updatedConfig;
|
||||||
|
this.config = deepFreeze(this.config);
|
||||||
|
|
||||||
if (this.config.backend.SCVersion !== this.scVersion) {
|
if (this.config.backend.SCVersion !== this.scVersion) {
|
||||||
loadError = new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion);
|
this.logger.warn(
|
||||||
|
'Incompatible config, expected',
|
||||||
|
this.scVersion,
|
||||||
|
'but got',
|
||||||
|
this.config.backend.SCVersion,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
throw new SavedConfigNotAvailable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the configuration from the provider
|
|
||||||
* @param config configuration to save
|
|
||||||
*/
|
|
||||||
async save(config: SCIndexResponse): Promise<void> {
|
|
||||||
await this.storageProvider.put(STORAGE_KEY_CONFIG, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the configuration in the module and writes it into app storage
|
|
||||||
* @param config SCIndexResponse to set
|
|
||||||
*/
|
|
||||||
async set(config: SCIndexResponse): Promise<void> {
|
|
||||||
this.config = config;
|
|
||||||
await this.save(this.config);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 StApps
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, version 3.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {AppError} from '../../_helpers/errors';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when fetching from backend fails
|
|
||||||
*/
|
|
||||||
export class ConfigFetchError extends AppError {
|
|
||||||
constructor() {
|
|
||||||
super('ConfigFetchError', 'App configuration could not be fetched!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when the ConfigProvider could be initialised
|
|
||||||
*/
|
|
||||||
export class ConfigInitError extends AppError {
|
|
||||||
constructor() {
|
|
||||||
super('ConfigInitError', 'App configuration could not be initialised!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when the requested config value is not available
|
|
||||||
*/
|
|
||||||
export class ConfigValueNotAvailable extends AppError {
|
|
||||||
constructor(valueKey: string) {
|
|
||||||
super('ConfigValueNotAvailable', `No attribute "${valueKey}" in config available!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when no saved config is available
|
|
||||||
*/
|
|
||||||
export class SavedConfigNotAvailable extends AppError {
|
|
||||||
constructor() {
|
|
||||||
super('SavedConfigNotAvailable', 'No saved app configuration available.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when the SCVersion of the saved config is not compatible with the app
|
|
||||||
*/
|
|
||||||
export class WrongConfigVersionInStorage extends AppError {
|
|
||||||
constructor(correctVersion: string, savedVersion: string) {
|
|
||||||
super(
|
|
||||||
'WrongConfigVersionInStorage',
|
|
||||||
`The saved configs backend version ${savedVersion} ` +
|
|
||||||
`does not equal the configured backend version ${correctVersion} of the app.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ import {SwiperModule} from 'swiper/angular';
|
|||||||
import {TranslateModule, TranslatePipe} from '@ngx-translate/core';
|
import {TranslateModule, TranslatePipe} from '@ngx-translate/core';
|
||||||
import {MomentModule} from 'ngx-moment';
|
import {MomentModule} from 'ngx-moment';
|
||||||
import {DataModule} from '../data/data.module';
|
import {DataModule} from '../data/data.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {DashboardComponent} from './dashboard.component';
|
import {DashboardComponent} from './dashboard.component';
|
||||||
import {SearchSectionComponent} from './sections/search-section/search-section.component';
|
import {SearchSectionComponent} from './sections/search-section/search-section.component';
|
||||||
import {NewsSectionComponent} from './sections/news-section/news-section.component';
|
import {NewsSectionComponent} from './sections/news-section/news-section.component';
|
||||||
@@ -70,6 +69,6 @@ const catalogRoutes: Routes = [
|
|||||||
NewsModule,
|
NewsModule,
|
||||||
JobModule,
|
JobModule,
|
||||||
],
|
],
|
||||||
providers: [SettingsProvider, TranslatePipe],
|
providers: [TranslatePipe],
|
||||||
})
|
})
|
||||||
export class DashboardModule {}
|
export class DashboardModule {}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {ScheduleProvider} from '../calendar/schedule.provider';
|
|||||||
import {GeoNavigationDirective} from '../map/geo-navigation.directive';
|
import {GeoNavigationDirective} from '../map/geo-navigation.directive';
|
||||||
import {MapWidgetComponent} from '../map/widget/map-widget.component';
|
import {MapWidgetComponent} from '../map/widget/map-widget.component';
|
||||||
import {MenuModule} from '../menu/menu.module';
|
import {MenuModule} from '../menu/menu.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {StorageModule} from '../storage/storage.module';
|
import {StorageModule} from '../storage/storage.module';
|
||||||
import {ActionChipListComponent} from './chips/action-chip-list.component';
|
import {ActionChipListComponent} from './chips/action-chip-list.component';
|
||||||
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
|
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
|
||||||
@@ -214,7 +213,6 @@ import {ShareButtonComponent} from './elements/share-button.component';
|
|||||||
StAppsWebHttpClient,
|
StAppsWebHttpClient,
|
||||||
CalendarService,
|
CalendarService,
|
||||||
RoutingStackService,
|
RoutingStackService,
|
||||||
SettingsProvider,
|
|
||||||
{
|
{
|
||||||
provide: SimpleBrowser,
|
provide: SimpleBrowser,
|
||||||
useFactory: browserFactory,
|
useFactory: browserFactory,
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
|
import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
import {ModalController} from '@ionic/angular';
|
import {ModalController} from '@ionic/angular';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
|
||||||
import {SCLanguageCode, SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
|
|
||||||
import {DataProvider, DataScope} from '../data.provider';
|
import {DataProvider, DataScope} from '../data.provider';
|
||||||
import {FavoritesService} from '../../favorites/favorites.service';
|
import {FavoritesService} from '../../favorites/favorites.service';
|
||||||
import {take} from 'rxjs/operators';
|
import {take} from 'rxjs/operators';
|
||||||
import {Network} from '@capacitor/network';
|
import {Network} from '@capacitor/network';
|
||||||
import {DataListContext} from '../list/data-list.component';
|
import {DataListContext} from '../list/data-list.component';
|
||||||
|
import {lastValueFrom} from 'rxjs';
|
||||||
|
|
||||||
export interface ExternalDataLoadEvent {
|
export interface ExternalDataLoadEvent {
|
||||||
uid: SCUuid;
|
uid: SCUuid;
|
||||||
@@ -29,6 +29,13 @@ export interface ExternalDataLoadEvent {
|
|||||||
resolve: (item: SCThings | null | undefined) => void;
|
resolve: (item: SCThings | null | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for SCSavableThing
|
||||||
|
*/
|
||||||
|
function isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
|
||||||
|
return (thing as SCSaveableThing).data !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Component to display an SCThing detailed
|
* A Component to display an SCThing detailed
|
||||||
*/
|
*/
|
||||||
@@ -53,11 +60,6 @@ export class DataDetailComponent implements OnInit {
|
|||||||
|
|
||||||
@Input() autoRouteDataPath = true;
|
@Input() autoRouteDataPath = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* The language of the item
|
|
||||||
*/
|
|
||||||
language: SCLanguageCode;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicating wether internet connectivity is given or not
|
* Indicating wether internet connectivity is given or not
|
||||||
*/
|
*/
|
||||||
@@ -79,20 +81,12 @@ export class DataDetailComponent implements OnInit {
|
|||||||
|
|
||||||
@Output() loadItem: EventEmitter<ExternalDataLoadEvent> = new EventEmitter<ExternalDataLoadEvent>();
|
@Output() loadItem: EventEmitter<ExternalDataLoadEvent> = new EventEmitter<ExternalDataLoadEvent>();
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard for SCSavableThing
|
|
||||||
*/
|
|
||||||
static isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
|
|
||||||
return (thing as SCSaveableThing).data !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly route: ActivatedRoute,
|
protected readonly route: ActivatedRoute,
|
||||||
router: Router,
|
router: Router,
|
||||||
private readonly dataProvider: DataProvider,
|
private readonly dataProvider: DataProvider,
|
||||||
private readonly favoritesService: FavoritesService,
|
private readonly favoritesService: FavoritesService,
|
||||||
readonly modalController: ModalController,
|
readonly modalController: ModalController,
|
||||||
translateService: TranslateService,
|
|
||||||
) {
|
) {
|
||||||
this.inputItem = router.getCurrentNavigation()?.extras.state?.item;
|
this.inputItem = router.getCurrentNavigation()?.extras.state?.item;
|
||||||
if (!this.inputItem?.origin) {
|
if (!this.inputItem?.origin) {
|
||||||
@@ -100,10 +94,6 @@ export class DataDetailComponent implements OnInit {
|
|||||||
// This can happen, for example, when detail views use `inPlace` list items
|
// This can happen, for example, when detail views use `inPlace` list items
|
||||||
delete this.inputItem;
|
delete this.inputItem;
|
||||||
}
|
}
|
||||||
this.language = translateService.currentLang as SCLanguageCode;
|
|
||||||
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as SCLanguageCode;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.isDisconnected = new Promise(async resolve => {
|
this.isDisconnected = new Promise(async resolve => {
|
||||||
const isConnected = (await Network.getStatus()).connected;
|
const isConnected = (await Network.getStatus()).connected;
|
||||||
@@ -126,13 +116,8 @@ export class DataDetailComponent implements OnInit {
|
|||||||
)
|
)
|
||||||
: this.dataProvider.get(uid, DataScope.Remote)));
|
: this.dataProvider.get(uid, DataScope.Remote)));
|
||||||
|
|
||||||
this.item = item
|
// eslint-disable-next-line unicorn/no-null
|
||||||
? // eslint-disable-next-line unicorn/no-null
|
this.item = item ? (isSCSavableThing(item) ? item.data : item) : null;
|
||||||
DataDetailComponent.isSCSavableThing(item)
|
|
||||||
? item.data
|
|
||||||
: item
|
|
||||||
: // eslint-disable-next-line unicorn/no-null
|
|
||||||
null;
|
|
||||||
} catch {
|
} catch {
|
||||||
// eslint-disable-next-line unicorn/no-null
|
// eslint-disable-next-line unicorn/no-null
|
||||||
this.item = null;
|
this.item = null;
|
||||||
@@ -144,14 +129,10 @@ export class DataDetailComponent implements OnInit {
|
|||||||
await this.getItem(uid ?? '', false);
|
await this.getItem(uid ?? '', false);
|
||||||
// fallback to the saved item (from favorites)
|
// fallback to the saved item (from favorites)
|
||||||
if (this.item === null) {
|
if (this.item === null) {
|
||||||
this.favoritesService
|
const item = await lastValueFrom(this.favoritesService.get(uid).pipe(take(1)));
|
||||||
.get(uid)
|
if (item) {
|
||||||
.pipe(take(1))
|
|
||||||
.subscribe(item => {
|
|
||||||
if (item !== undefined) {
|
|
||||||
this.item = item.data;
|
this.item = item.data;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export class OffersInListComponent {
|
|||||||
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
|
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
|
||||||
this._offers = it;
|
this._offers = it;
|
||||||
this.price = it[0].prices?.default;
|
this.price = it[0].prices?.default;
|
||||||
this.settingsProvider.getSetting('profile', 'group').then(group => {
|
this.settingsProvider.getSetting<string>('profile', 'group').then(group => {
|
||||||
this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never];
|
this.price = it[0].prices?.[group.replace(/s$/, '') as never];
|
||||||
});
|
});
|
||||||
|
|
||||||
const availabilities = new Set(it.map(offer => offer.availability));
|
const availabilities = new Set(it.map(offer => offer.availability));
|
||||||
|
|||||||
@@ -17,14 +17,7 @@ import {ActivatedRoute, Router} from '@angular/router';
|
|||||||
import {Keyboard} from '@capacitor/keyboard';
|
import {Keyboard} from '@capacitor/keyboard';
|
||||||
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
|
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
|
||||||
import {Capacitor} from '@capacitor/core';
|
import {Capacitor} from '@capacitor/core';
|
||||||
import {
|
import {SCFacet, SCSearchFilter, SCSearchQuery, SCSearchSort, SCThings} from '@openstapps/core';
|
||||||
SCFacet,
|
|
||||||
SCFeatureConfiguration,
|
|
||||||
SCSearchFilter,
|
|
||||||
SCSearchQuery,
|
|
||||||
SCSearchSort,
|
|
||||||
SCThings,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {combineLatest, Subject} from 'rxjs';
|
import {combineLatest, Subject} from 'rxjs';
|
||||||
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
|
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
|
||||||
@@ -170,9 +163,8 @@ export class SearchPageComponent implements OnInit {
|
|||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
protected positionService: PositionService,
|
protected positionService: PositionService,
|
||||||
private readonly configProvider: ConfigProvider,
|
private readonly configProvider: ConfigProvider,
|
||||||
animationController: AnimationController,
|
|
||||||
) {
|
) {
|
||||||
this.routeAnimation = searchPageSwitchAnimation(animationController);
|
this.routeAnimation = searchPageSwitchAnimation(inject(AnimationController));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -323,16 +315,6 @@ export class SearchPageComponent implements OnInit {
|
|||||||
this.queryChanged.next();
|
this.queryChanged.next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.settingsProvider.settingsActionChanged$
|
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
|
||||||
.subscribe(({type, payload}) => {
|
|
||||||
if (type === 'stapps.settings.changed') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const {category, name, value} = payload!;
|
|
||||||
this.logger.log(`received event "settings.changed" with category:
|
|
||||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.dataRoutingService
|
this.dataRoutingService
|
||||||
.itemSelectListener()
|
.itemSelectListener()
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
.pipe(takeUntilDestroyed(this.destroy$))
|
||||||
@@ -342,12 +324,8 @@ export class SearchPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
this.isHebisAvailable =
|
||||||
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
this.configProvider.config.app.features.plugins?.['hebis-plugin']?.urlPath !== undefined;
|
||||||
this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
SCRatingResponse,
|
SCRatingResponse,
|
||||||
SCRatingRoute,
|
SCRatingRoute,
|
||||||
SCUserGroup,
|
SCUserGroup,
|
||||||
SCUserGroupSetting,
|
|
||||||
SCUuid,
|
SCUuid,
|
||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||||
@@ -63,9 +62,7 @@ export class RatingProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get userGroup(): Promise<SCUserGroup> {
|
private get userGroup(): Promise<SCUserGroup> {
|
||||||
return this.settingsProvider
|
return this.settingsProvider.getSetting<SCUserGroup>('profile', 'group');
|
||||||
.getSetting('profile', 'group')
|
|
||||||
.then(it => (it as SCUserGroupSetting).value as SCUserGroup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getStoredRatings(): Promise<RatingStorage> {
|
private async getStoredRatings(): Promise<RatingStorage> {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class PlaceMensaService {
|
|||||||
sort: [
|
sort: [
|
||||||
{
|
{
|
||||||
arguments: {
|
arguments: {
|
||||||
field: `offers.prices.${(priceGroup.value as string).replace(/s$/, '')}`,
|
field: `offers.prices.${(priceGroup as string).replace(/s$/, '')}`,
|
||||||
},
|
},
|
||||||
order: 'desc',
|
order: 'desc',
|
||||||
type: 'generic',
|
type: 'generic',
|
||||||
|
|||||||
@@ -12,21 +12,13 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, inject, OnInit} from '@angular/core';
|
||||||
import {AlertController, AnimationController} from '@ionic/angular';
|
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
|
||||||
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
|
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
|
||||||
import {combineLatest} from 'rxjs';
|
import {combineLatest} from 'rxjs';
|
||||||
import {SCThingType} from '@openstapps/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
import {FavoritesService} from './favorites.service';
|
import {FavoritesService} from './favorites.service';
|
||||||
import {DataRoutingService} from '../data/data-routing.service';
|
|
||||||
import {ContextMenuService} from '../menu/context/context-menu.service';
|
import {ContextMenuService} from '../menu/context/context-menu.service';
|
||||||
import {SearchPageComponent} from '../data/list/search-page.component';
|
import {SearchPageComponent} from '../data/list/search-page.component';
|
||||||
import {DataProvider} from '../data/data.provider';
|
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {PositionService} from '../map/position.service';
|
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,34 +34,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
|
|||||||
|
|
||||||
showNavigation = false;
|
showNavigation = false;
|
||||||
|
|
||||||
constructor(
|
private favoritesService = inject(FavoritesService);
|
||||||
alertController: AlertController,
|
|
||||||
dataProvider: DataProvider,
|
|
||||||
contextMenuService: ContextMenuService,
|
|
||||||
settingsProvider: SettingsProvider,
|
|
||||||
logger: NGXLogger,
|
|
||||||
dataRoutingService: DataRoutingService,
|
|
||||||
router: Router,
|
|
||||||
route: ActivatedRoute,
|
|
||||||
positionService: PositionService,
|
|
||||||
private favoritesService: FavoritesService,
|
|
||||||
configProvider: ConfigProvider,
|
|
||||||
animationController: AnimationController,
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
alertController,
|
|
||||||
dataProvider,
|
|
||||||
contextMenuService,
|
|
||||||
settingsProvider,
|
|
||||||
logger,
|
|
||||||
dataRoutingService,
|
|
||||||
router,
|
|
||||||
route,
|
|
||||||
positionService,
|
|
||||||
configProvider,
|
|
||||||
animationController,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit(false);
|
super.ngOnInit(false);
|
||||||
@@ -96,16 +61,6 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
|
|||||||
this.queryChanged.next();
|
this.queryChanged.next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.settingsProvider.settingsActionChanged$
|
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
|
||||||
.subscribe(({type, payload}) => {
|
|
||||||
if (type === 'stapps.settings.changed') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const {category, name, value} = payload!;
|
|
||||||
this.logger.log(`received event "settings.changed" with category:
|
|
||||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.dataRoutingService
|
this.dataRoutingService
|
||||||
.itemSelectListener()
|
.itemSelectListener()
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
.pipe(takeUntilDestroyed(this.destroy$))
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {DataProvider} from '../data/data.provider';
|
import {DataProvider} from '../data/data.provider';
|
||||||
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
||||||
import {BehaviorSubject, Observable} from 'rxjs';
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
@@ -41,11 +40,6 @@ import {debounceTime, map} from 'rxjs/operators';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class FavoritesService {
|
export class FavoritesService {
|
||||||
/**
|
|
||||||
* Translation pipe
|
|
||||||
*/
|
|
||||||
thingTranslatePipe: ThingTranslatePipe;
|
|
||||||
|
|
||||||
favorites = new BehaviorSubject<Map<string, SCFavorite>>(new Map<string, SCFavorite>());
|
favorites = new BehaviorSubject<Map<string, SCFavorite>>(new Map<string, SCFavorite>());
|
||||||
|
|
||||||
// using debounce time 0 allows change detection to run through async suspension
|
// using debounce time 0 allows change detection to run through async suspension
|
||||||
@@ -93,8 +87,8 @@ export class FavoritesService {
|
|||||||
return items.sort((a, b) => {
|
return items.sort((a, b) => {
|
||||||
return (
|
return (
|
||||||
new Intl.Collator(this.translate.currentLang).compare(
|
new Intl.Collator(this.translate.currentLang).compare(
|
||||||
this.thingTranslatePipe.transform(field, a),
|
this.thingTranslate.get(a, field) as string,
|
||||||
this.thingTranslatePipe.transform(field, b),
|
this.thingTranslate.get(b, field) as string,
|
||||||
) * reverse
|
) * reverse
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -124,7 +118,6 @@ export class FavoritesService {
|
|||||||
private readonly translate: TranslateService,
|
private readonly translate: TranslateService,
|
||||||
private readonly thingTranslate: ThingTranslateService,
|
private readonly thingTranslate: ThingTranslateService,
|
||||||
) {
|
) {
|
||||||
this.thingTranslatePipe = new ThingTranslatePipe(this.translate, this.thingTranslate);
|
|
||||||
void this.emitAll();
|
void this.emitAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +178,9 @@ export class FavoritesService {
|
|||||||
const textFilteredItems: SCIndexableThings[] = [];
|
const textFilteredItems: SCIndexableThings[] = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (
|
if (
|
||||||
this.thingTranslatePipe.transform('name', item).toLowerCase().includes(queryText.toLowerCase())
|
(this.thingTranslate.get(item, 'name') as string)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(queryText.toLowerCase())
|
||||||
) {
|
) {
|
||||||
textFilteredItems.push(item);
|
textFilteredItems.push(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {DaiaAvailabilityResponse, DaiaHolding, DaiaService} from './protocol/res
|
|||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {SCFeatureConfiguration} from '@openstapps/core';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -67,7 +66,7 @@ export class DaiaDataProvider {
|
|||||||
async getAvailability(id: string): Promise<DaiaHolding[] | undefined> {
|
async getAvailability(id: string): Promise<DaiaHolding[] | undefined> {
|
||||||
if (this.daiaServiceUrl === undefined) {
|
if (this.daiaServiceUrl === undefined) {
|
||||||
try {
|
try {
|
||||||
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
const features = this.configProvider.config.app.features;
|
||||||
if (features.extern?.daia?.url) {
|
if (features.extern?.daia?.url) {
|
||||||
this.daiaServiceUrl = features.extern?.daia?.url;
|
this.daiaServiceUrl = features.extern?.daia?.url;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -114,16 +114,6 @@ export class HebisSearchPageComponent extends SearchPageComponent implements OnI
|
|||||||
this.queryChanged.next();
|
this.queryChanged.next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.settingsProvider.settingsActionChanged$
|
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
|
||||||
.subscribe(({type, payload}) => {
|
|
||||||
if (type === 'stapps.settings.changed') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const {category, name, value} = payload!;
|
|
||||||
this.logger.log(`received event "settings.changed" with category:
|
|
||||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.dataRoutingService
|
this.dataRoutingService
|
||||||
.itemSelectListener()
|
.itemSelectListener()
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
.pipe(takeUntilDestroyed(this.destroy$))
|
||||||
|
|||||||
@@ -12,14 +12,9 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {JQueryRequestor, Requestor} from '@openid/appauth';
|
import {JQueryRequestor, Requestor} from '@openid/appauth';
|
||||||
import {
|
import {SCAuthorizationProviderType, SCFeatureConfigurationExtern} from '@openstapps/core';
|
||||||
SCAuthorizationProviderType,
|
|
||||||
SCFeatureConfiguration,
|
|
||||||
SCFeatureConfigurationExtern,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
|
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
|
||||||
import {HebisDataProvider} from '../../hebis/hebis-data.provider';
|
import {HebisDataProvider} from '../../hebis/hebis-data.provider';
|
||||||
import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
|
import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
|
||||||
@@ -53,9 +48,7 @@ export class LibraryAccountService {
|
|||||||
private readonly toastController: ToastController,
|
private readonly toastController: ToastController,
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const config: SCFeatureConfigurationExtern = (
|
const config: SCFeatureConfigurationExtern = configProvider.config.app.features.extern!.paia;
|
||||||
configProvider.getValue('features') as SCFeatureConfiguration
|
|
||||||
).extern!.paia;
|
|
||||||
this.baseUrl = config.url;
|
this.baseUrl = config.url;
|
||||||
this.authType = config.authProvider as SCAuthorizationProviderType;
|
this.authType = config.authProvider as SCAuthorizationProviderType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {LeafletModule} from '@asymmetrik/ngx-leaflet';
|
|||||||
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
||||||
import {IonicModule} from '@ionic/angular';
|
import {IonicModule} from '@ionic/angular';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {Polygon} from 'geojson';
|
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {DataFacetsProvider} from '../data/data-facets.provider';
|
import {DataFacetsProvider} from '../data/data-facets.provider';
|
||||||
@@ -42,7 +41,7 @@ import {GeoNavigationDirective} from './geo-navigation.directive';
|
|||||||
*/
|
*/
|
||||||
export function initMapConfigFactory(configProvider: ConfigProvider, mapProvider: MapProvider) {
|
export function initMapConfigFactory(configProvider: ConfigProvider, mapProvider: MapProvider) {
|
||||||
return async () => {
|
return async () => {
|
||||||
mapProvider.defaultPolygon = (await configProvider.getValue('campusPolygon')) as Polygon;
|
mapProvider.defaultPolygon = configProvider.config.app.campusPolygon;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export class MapProvider {
|
|||||||
private positionService: PositionService,
|
private positionService: PositionService,
|
||||||
private configProvider: ConfigProvider,
|
private configProvider: ConfigProvider,
|
||||||
) {
|
) {
|
||||||
this.defaultPolygon = this.configProvider.getValue('campusPolygon') as Polygon;
|
this.defaultPolygon = this.configProvider.config.app.campusPolygon;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, Input} from '@angular/core';
|
import {Component, Input} from '@angular/core';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
import {SCLanguage, SCThingTranslator, SCThingType, SCTranslations} from '@openstapps/core';
|
|
||||||
import {ContextMenuService} from './context-menu.service';
|
import {ContextMenuService} from './context-menu.service';
|
||||||
import {FilterContext, FilterFacet, SortContext, SortContextOption} from './context-type.js';
|
import {FilterContext, FilterFacet, SortContext, SortContextOption} from './context-type.js';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
import {ThingTranslateService} from '../../../translation/thing-translate.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The context menu
|
* The context menu
|
||||||
@@ -59,11 +59,6 @@ export class ContextMenuComponent {
|
|||||||
return this.filterOption.options.filter(it => it.buckets.length > 0);
|
return this.filterOption.options.filter(it => it.buckets.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Possible languages to be used for translation
|
|
||||||
*/
|
|
||||||
language: keyof SCTranslations<SCLanguage>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of SCThingType
|
* Mapping of SCThingType
|
||||||
*/
|
*/
|
||||||
@@ -74,22 +69,10 @@ export class ContextMenuComponent {
|
|||||||
*/
|
*/
|
||||||
sortOption: SortContext;
|
sortOption: SortContext;
|
||||||
|
|
||||||
/**
|
|
||||||
* Core translator
|
|
||||||
*/
|
|
||||||
translator: SCThingTranslator;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private translateService: TranslateService,
|
|
||||||
private readonly contextMenuService: ContextMenuService,
|
private readonly contextMenuService: ContextMenuService,
|
||||||
|
private readonly thingTranslateService: ThingTranslateService,
|
||||||
) {
|
) {
|
||||||
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
|
|
||||||
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
});
|
|
||||||
this.contextMenuService.filterContextChanged$.pipe(takeUntilDestroyed()).subscribe(filterContext => {
|
this.contextMenuService.filterContextChanged$.pipe(takeUntilDestroyed()).subscribe(filterContext => {
|
||||||
this.filterOption = filterContext;
|
this.filterOption = filterContext;
|
||||||
});
|
});
|
||||||
@@ -109,7 +92,7 @@ export class ContextMenuComponent {
|
|||||||
* Returns translated property value
|
* Returns translated property value
|
||||||
*/
|
*/
|
||||||
getTranslatedPropertyValue(onlyForType: SCThingType, field: string, key?: string): string | undefined {
|
getTranslatedPropertyValue(onlyForType: SCThingType, field: string, key?: string): string | undefined {
|
||||||
return this.translator.translatedPropertyValue(onlyForType, field, key);
|
return this.thingTranslateService.translator.translatedPropertyValue(onlyForType, field, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,74 +13,23 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
||||||
import {
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
SCAppConfigurationMenuCategory,
|
|
||||||
SCLanguage,
|
|
||||||
SCThingTranslator,
|
|
||||||
SCTranslations,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {NavigationService} from './navigation.service';
|
|
||||||
import config from 'capacitor.config';
|
|
||||||
import {SettingsProvider} from '../../settings/settings.provider';
|
|
||||||
import {BreakpointObserver} from '@angular/cdk/layout';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generated class for the MenuPage page.
|
|
||||||
*
|
|
||||||
* See https://ionicframework.com/docs/components/#navigation for more info on
|
|
||||||
* Ionic pages and navigation.
|
|
||||||
*/
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'stapps-navigation',
|
selector: 'stapps-navigation',
|
||||||
styleUrls: ['navigation.scss'],
|
styleUrls: ['navigation.scss'],
|
||||||
templateUrl: 'navigation.html',
|
templateUrl: 'navigation.html',
|
||||||
})
|
})
|
||||||
export class NavigationComponent implements OnInit {
|
export class NavigationComponent implements OnInit {
|
||||||
showTabbar = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the app
|
|
||||||
*/
|
|
||||||
appName = config.appName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Possible languages to be used for translation
|
|
||||||
*/
|
|
||||||
language: keyof SCTranslations<SCLanguage>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu entries from config module
|
* Menu entries from config module
|
||||||
*/
|
*/
|
||||||
menu: SCAppConfigurationMenuCategory[];
|
menu: SCAppConfigurationMenuCategory[];
|
||||||
|
|
||||||
/**
|
constructor(private config: ConfigProvider) {}
|
||||||
* Core translator
|
|
||||||
*/
|
|
||||||
translator: SCThingTranslator;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public translateService: TranslateService,
|
|
||||||
private navigationService: NavigationService,
|
|
||||||
private settingsProvider: SettingsProvider,
|
|
||||||
private responsive: BreakpointObserver,
|
|
||||||
) {
|
|
||||||
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.responsive.observe(['(min-width: 768px)']).subscribe(result => {
|
|
||||||
this.showTabbar = !result.matches;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.language = (await this.settingsProvider.getValue(
|
this.menu = this.config.config.app.menus;
|
||||||
'profile',
|
|
||||||
'language',
|
|
||||||
)) as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
this.menu = await this.navigationService.getMenu();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,11 @@
|
|||||||
class="menu-category"
|
class="menu-category"
|
||||||
>
|
>
|
||||||
<ion-icon slot="end" [name]="category.icon"></ion-icon>
|
<ion-icon slot="end" [name]="category.icon"></ion-icon>
|
||||||
<ion-label> {{ category.translations[language]?.title | titlecase }} </ion-label>
|
<ion-label> {{ 'title' | translateSimple: category | titlecase }} </ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route">
|
<ion-item *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route">
|
||||||
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
||||||
<ion-label> {{ item.translations[language]?.title | titlecase }} </ion-label>
|
<ion-label> {{ 'title' | translateSimple: item | titlecase }} </ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
|
|||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {RouterModule} from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import {OfflineNoticeComponent} from './offline-notice.component';
|
import {OfflineNoticeComponent} from './offline-notice.component';
|
||||||
|
import {ThingTranslateModule} from '../../../translation/thing-translate.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
|
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
|
||||||
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule],
|
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule, ThingTranslateModule],
|
||||||
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
|
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
|
||||||
})
|
})
|
||||||
export class NavigationModule {}
|
export class NavigationModule {}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ stapps-navigation-tabs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stapps-offline-notice.needs-reload ~ ion-split-pane,
|
||||||
stapps-offline-notice.has-error ~ ion-split-pane,
|
stapps-offline-notice.has-error ~ ion-split-pane,
|
||||||
stapps-offline-notice.is-offline ~ ion-split-pane {
|
stapps-offline-notice.is-offline ~ ion-split-pane {
|
||||||
margin-top: calc(var(--font-size-md) + 2 * var(--spacing-sm));
|
margin-top: calc(var(--font-size-md) + 2 * var(--spacing-sm));
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 StApps
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, version 3.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Injectable} from '@angular/core';
|
|
||||||
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class NavigationService {
|
|
||||||
constructor(
|
|
||||||
private configProvider: ConfigProvider,
|
|
||||||
private logger: NGXLogger,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getMenu() {
|
|
||||||
let menu: SCAppConfigurationMenuCategory[] = [];
|
|
||||||
try {
|
|
||||||
menu = this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[];
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`error from loading menu entries: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,10 @@ import {InternetConnectionService} from '../../../util/internet-connection.servi
|
|||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
import {SettingsProvider} from '../../settings/settings.provider';
|
||||||
|
import {AnimationController} from '@ionic/angular';
|
||||||
|
import {filter, race} from 'rxjs';
|
||||||
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'stapps-offline-notice',
|
selector: 'stapps-offline-notice',
|
||||||
@@ -28,12 +32,17 @@ export class OfflineNoticeComponent {
|
|||||||
|
|
||||||
@HostBinding('class.has-error') hasError = false;
|
@HostBinding('class.has-error') hasError = false;
|
||||||
|
|
||||||
|
@HostBinding('class.needs-reload') needsReload = false;
|
||||||
|
|
||||||
@ViewChild('spinIcon', {read: ElementRef}) spinIcon: ElementRef;
|
@ViewChild('spinIcon', {read: ElementRef}) spinIcon: ElementRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly offlineProvider: InternetConnectionService,
|
readonly offlineProvider: InternetConnectionService,
|
||||||
readonly router: Router,
|
readonly router: Router,
|
||||||
readonly logger: NGXLogger,
|
readonly logger: NGXLogger,
|
||||||
|
readonly animationCtl: AnimationController,
|
||||||
|
settingsProvider: SettingsProvider,
|
||||||
|
configProvider: ConfigProvider,
|
||||||
) {
|
) {
|
||||||
this.offlineProvider.offline$.pipe(takeUntilDestroyed()).subscribe(isOffline => {
|
this.offlineProvider.offline$.pipe(takeUntilDestroyed()).subscribe(isOffline => {
|
||||||
this.isOffline = isOffline;
|
this.isOffline = isOffline;
|
||||||
@@ -41,6 +50,15 @@ export class OfflineNoticeComponent {
|
|||||||
this.offlineProvider.error$.pipe(takeUntilDestroyed()).subscribe(hasError => {
|
this.offlineProvider.error$.pipe(takeUntilDestroyed()).subscribe(hasError => {
|
||||||
this.hasError = hasError;
|
this.hasError = hasError;
|
||||||
});
|
});
|
||||||
|
race(
|
||||||
|
settingsProvider.needsReload$.pipe(filter(it => it)),
|
||||||
|
configProvider.needsUpdate$.pipe(filter(it => it)),
|
||||||
|
)
|
||||||
|
.pipe(takeUntilDestroyed())
|
||||||
|
.subscribe(() => {
|
||||||
|
console.log('aha!');
|
||||||
|
this.needsReload = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
retry() {
|
retry() {
|
||||||
@@ -49,4 +67,15 @@ export class OfflineNoticeComponent {
|
|||||||
this.spinIcon.nativeElement.classList.add('spin');
|
this.spinIcon.nativeElement.classList.add('spin');
|
||||||
this.offlineProvider.retry();
|
this.offlineProvider.retry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reloadPage() {
|
||||||
|
await this.animationCtl
|
||||||
|
.create()
|
||||||
|
.duration(100)
|
||||||
|
.fromTo('opacity', 1, 0)
|
||||||
|
.addElement(document.querySelector('ion-app')!)
|
||||||
|
.play();
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
<ion-icon #spinIcon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
|
<ion-icon #spinIcon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
|
||||||
<ion-label>{{ 'app.errors.CONNECTION_ERROR' | translate }}</ion-label>
|
<ion-label>{{ 'app.errors.CONNECTION_ERROR' | translate }}</ion-label>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
<ion-button class="reload" color="warning" (click)="reloadPage()">
|
||||||
|
<ion-icon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
|
||||||
|
<ion-label>{{ 'settings.reloadPage' | translate }}</ion-label>
|
||||||
|
</ion-button>
|
||||||
<ion-button class="close" fill="clear" color="light" (click)="offlineProvider.dismissError()"
|
<ion-button class="close" fill="clear" color="light" (click)="offlineProvider.dismissError()"
|
||||||
><ion-icon [size]="16" [weight]="800" name="close" slot="icon-only"></ion-icon
|
><ion-icon [size]="16" [weight]="800" name="close" slot="icon-only"></ion-icon
|
||||||
></ion-button>
|
></ion-button>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
transition: all 150ms ease;
|
transition: all 150ms ease;
|
||||||
|
|
||||||
|
&.needs-reload,
|
||||||
&.is-offline,
|
&.is-offline,
|
||||||
&.has-error {
|
&.has-error {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.needs-reload > .reload,
|
||||||
&.is-offline > .offline-button,
|
&.is-offline > .offline-button,
|
||||||
&.has-error > .close,
|
&.has-error > .close,
|
||||||
&.has-error > .error-button {
|
&.has-error > .error-button {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import type {AnimationBuilder} from '@ionic/angular';
|
import type {AnimationBuilder} from '@ionic/angular';
|
||||||
import {AnimationController} from '@ionic/angular';
|
import {AnimationController} from '@ionic/angular';
|
||||||
import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
|
import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
|
||||||
|
import {iosDuration, iosEasing, mdDuration, mdEasing} from '../../../animation/easings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -23,10 +24,11 @@ import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
|
|||||||
export function tabsTransition(animationController: AnimationController): AnimationBuilder {
|
export function tabsTransition(animationController: AnimationController): AnimationBuilder {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
|
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
|
||||||
const duration = options.duration || 350;
|
const isMd = document.querySelector('ion-app.md') !== null;
|
||||||
const contentExitDuration = options.contentExitDuration || 100;
|
const duration = isMd ? mdDuration : iosDuration;
|
||||||
|
const easing = isMd ? mdEasing : iosEasing;
|
||||||
|
|
||||||
const rootTransition = animationController.create().duration(duration);
|
const rootTransition = animationController.create().duration(duration).easing(easing);
|
||||||
|
|
||||||
const enterTransition = animationController
|
const enterTransition = animationController
|
||||||
.create()
|
.create()
|
||||||
@@ -39,23 +41,15 @@ export function tabsTransition(animationController: AnimationController): Animat
|
|||||||
.addElement(options.leavingEl);
|
.addElement(options.leavingEl);
|
||||||
const exitTransition = animationController
|
const exitTransition = animationController
|
||||||
.create()
|
.create()
|
||||||
.duration(contentExitDuration * 2)
|
|
||||||
.easing('cubic-bezier(0.87, 0, 0.13, 1)')
|
|
||||||
.fromTo('opacity', '1', '0')
|
.fromTo('opacity', '1', '0')
|
||||||
.addElement(options.leavingEl.querySelector('ion-header'));
|
.addElement(options.leavingEl.querySelector('ion-header'));
|
||||||
const contentExit = animationController
|
const contentExit = animationController
|
||||||
.create()
|
.create()
|
||||||
.easing('linear')
|
|
||||||
.duration(contentExitDuration)
|
|
||||||
.fromTo('opacity', '1', '0')
|
.fromTo('opacity', '1', '0')
|
||||||
.addElement(options.leavingEl.querySelectorAll(':scope > *:not(ion-header)'));
|
.addElement(options.leavingEl.querySelectorAll(':scope > *:not(ion-header)'));
|
||||||
const contentEnter = animationController
|
const contentEnter = animationController
|
||||||
.create()
|
.create()
|
||||||
.delay(contentExitDuration)
|
.fromTo('transform', 'scale(1.05)', 'scale(1)')
|
||||||
.duration(duration - contentExitDuration)
|
|
||||||
.easing('cubic-bezier(0.16, 1, 0.3, 1)')
|
|
||||||
.fromTo('transform', 'scale(1.025)', 'scale(1)')
|
|
||||||
.fromTo('opacity', '0', '1')
|
|
||||||
.addElement(options.enteringEl.querySelectorAll(':scope > *:not(ion-header)'));
|
.addElement(options.enteringEl.querySelectorAll(':scope > *:not(ion-header)'));
|
||||||
|
|
||||||
rootTransition.addAnimation([enterTransition, contentExit, contentEnter, exitTransition, exitZIndex]);
|
rootTransition.addAnimation([enterTransition, contentExit, contentEnter, exitTransition, exitZIndex]);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user