refactor: move app to monorepo

This commit is contained in:
2023-05-24 14:26:54 +02:00
parent 20a336d3c4
commit ac62e621d5
794 changed files with 0 additions and 67 deletions

View File

@@ -0,0 +1,22 @@
/*
* 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 {chunk} from './chunk';
describe('chunk', function () {
it('should chunk items in the correct sizes', function () {
expect(chunk([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3)).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]);
});
});

View File

@@ -0,0 +1,28 @@
/*
* 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/>.
*/
/**
* Chunk array into smaller arrays of a specified size.
*
* @param array The array to chunk.
* @param chunkSize The size of each chunk.
*/
export function chunk<T>(array: T[], chunkSize = 1): T[][] {
const arrayCopy = [...array];
const out: T[][] = [];
if (chunkSize <= 0) return out;
while (arrayCopy.length > 0) out.push(arrayCopy.splice(0, chunkSize));
return out;
}

View File

@@ -0,0 +1,25 @@
/*
* 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 {differenceBy} from './difference';
describe('differenceBy', function () {
it('should return the difference of two arrays', function () {
const a = [1, 2, 3, 4, 5];
const b = [1, 2, 3];
expect(differenceBy(a, b, it => it)).toEqual([4, 5]);
});
});

View File

@@ -0,0 +1,23 @@
/*
* 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/>.
*/
/**
* Returns the difference between two arrays.
*/
export function differenceBy<T>(a: T[], b: T[], transform: (item: T) => unknown) {
const disallowed = new Set(b.map(transform));
return a.filter(item => !disallowed.has(transform(item)));
}

View File

@@ -0,0 +1,39 @@
/*
* 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 {get} from './get';
describe('get', function () {
it('should get a simple path', function () {
const object = {
a: {
b: {
c: 'd',
},
},
};
expect(get(object, 'a.b.c')).toBe('d');
});
it('should return undefined for a non-existent path', function () {
const object = {
a: {
b: {
c: 'd',
},
},
};
expect(get(object, 'a.b.c.d')).toBeUndefined();
});
});

View File

@@ -0,0 +1,30 @@
/*
* 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/>.
*/
/**
* Gets a value from a nested object.
* The path must be key names separated by dots.
* If the path doesn't exist, undefined is returned.
*/
export function get<U = unknown>(object: object, path: string): U {
return path.split('.').reduce(
(accumulator, current) =>
accumulator?.hasOwnProperty(current)
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
(accumulator as any)[current]
: undefined,
object,
) as unknown as U;
}

View File

@@ -0,0 +1,123 @@
/*
* 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 {groupBy, groupByStable, groupByProperty} from './group-by';
describe('groupBy', () => {
it('should group an array by a key', () => {
const array = [
{id: 1, name: 'one'},
{id: 2, name: 'two'},
{id: 3, name: 'three'},
{id: 4, name: 'four'},
{id: 5, name: 'five'},
];
const result = groupBy(array, it => it.name);
expect(result).toEqual({
one: [{id: 1, name: 'one'}],
two: [{id: 2, name: 'two'}],
three: [{id: 3, name: 'three'}],
four: [{id: 4, name: 'four'}],
five: [{id: 5, name: 'five'}],
});
});
it('should handle multiple elements per group', () => {
const array = [
{id: 1, name: 'one'},
{id: 2, name: 'two'},
{id: 3, name: 'three'},
{id: 4, name: 'four'},
{id: 5, name: 'five'},
{id: 6, name: 'one'},
{id: 7, name: 'two'},
{id: 8, name: 'three'},
{id: 9, name: 'four'},
{id: 10, name: 'five'},
];
const result = groupBy(array, it => it.name);
expect(result).toEqual({
one: [
{id: 1, name: 'one'},
{id: 6, name: 'one'},
],
two: [
{id: 2, name: 'two'},
{id: 7, name: 'two'},
],
three: [
{id: 3, name: 'three'},
{id: 8, name: 'three'},
],
four: [
{id: 4, name: 'four'},
{id: 9, name: 'four'},
],
five: [
{id: 5, name: 'five'},
{id: 10, name: 'five'},
],
});
});
});
describe('groupByStable', () => {
const array = [
{id: 2, name: 'two'},
{id: 4, name: 'three'},
{id: 3, name: 'three'},
{id: 1, name: 'one'},
];
const result = groupByStable(array, it => it.name);
it('should group an array by keys', () => {
expect(result.get('one')).toEqual([{id: 1, name: 'one'}]);
expect(result.get('two')).toEqual([{id: 2, name: 'two'}]);
expect(result.get('three')).toEqual([
{id: 4, name: 'three'},
{id: 3, name: 'three'},
]);
});
it('should provide ordered keys', () => {
expect([...result.keys()]).toEqual(['two', 'three', 'one']);
});
});
describe('groupByProperty', function () {
it('should group by property', () => {
const array = [
{id: 1, name: 'one'},
{id: 2, name: 'two'},
{id: 3, name: 'three'},
{id: 4, name: 'four'},
{id: 5, name: 'five'},
];
const result = groupByProperty(array, 'name');
expect(result).toEqual({
one: [{id: 1, name: 'one'}],
two: [{id: 2, name: 'two'}],
three: [{id: 3, name: 'three'}],
four: [{id: 4, name: 'four'}],
five: [{id: 5, name: 'five'}],
});
});
});

View File

@@ -0,0 +1,45 @@
/*
* 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/>.
*/
/**
* Group an array by a function
*/
export function groupBy<T>(collection: T[], group: (item: T) => string | undefined): Record<string, T[]> {
return collection.reduce((accumulator: Record<string, T[]>, item) => {
const key = group(item) ?? '';
accumulator[key] = accumulator[key] ?? [];
accumulator[key].push(item);
return accumulator;
}, {});
}
/**
* Group an array by a function (returns a Map, whose keys keep order info of items entry)
*/
export function groupByStable<T>(collection: T[], group: (item: T) => string | undefined): Map<string, T[]> {
return collection.reduce((accumulator: Map<string, T[]>, item) => {
const key = group(item) ?? '';
accumulator.set(key, accumulator.get(key) ?? []);
accumulator.get(key)?.push(item);
return accumulator;
}, new Map<string, T[]>());
}
/**
*
*/
export function groupByProperty<T extends object>(collection: T[], property: keyof T): Record<string, T[]> {
return groupBy(collection, item => item[property] as unknown as string);
}

View File

@@ -0,0 +1,41 @@
/*
* 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 {keyBy} from './key-by';
describe('keyBy', function () {
it('should key objects', function () {
const objects = [
{
id: 1,
name: 'foo',
},
{
id: 2,
name: 'bar',
},
];
const result = keyBy(objects, it => it.id);
expect(result).toEqual({
1: {
id: 1,
name: 'foo',
},
2: {
id: 2,
name: 'bar',
},
});
});
});

View File

@@ -0,0 +1,27 @@
/*
* 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/>.
*/
/**
* Create an object composed of keys generated from the results of running
* each element of collection thru iteratee. The corresponding value of
* each key is the last element responsible for generating the key. The
* iteratee is invoked with one argument: (value).
*/
export function keyBy<T>(collection: T[], key: (item: T) => string | number): Record<string, T> {
return collection.reduce((accumulator, item) => {
accumulator[key(item)] = item;
return accumulator;
}, {} as Record<string | number, T>);
}

View File

@@ -0,0 +1,50 @@
/*
* 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 {mapValues} from './map-values';
describe('map-values', () => {
it('should map values', () => {
const object = {
a: 1,
b: 2,
c: 3,
};
const result = mapValues(object, value => value * 2);
expect(result).toEqual({
a: 2,
b: 4,
c: 6,
});
});
it('should not modify the original object', () => {
const object = {
a: 1,
b: 2,
c: 3,
};
mapValues(object, value => value * 2);
expect(object).toEqual({
a: 1,
b: 2,
c: 3,
});
});
});

View File

@@ -0,0 +1,32 @@
/*
* 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/>.
*/
/**
* Maps the values of an object to a new object
*/
export function mapValues<T extends object, U>(
object: T,
transform: (value: T[keyof T], key: keyof T) => U,
): {[key in keyof T]: U} {
const result = {} as {[key in keyof T]: U};
for (const key in object) {
if (object.hasOwnProperty(key)) {
result[key] = transform(object[key], key);
}
}
return result;
}

View File

@@ -0,0 +1,44 @@
/*
* 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 {minBy} from './min';
describe('minBy', function () {
it('should pick the minimum value based on transform', function () {
expect(
minBy(
[
{id: 1, name: 'A'},
{id: 2, name: 'B'},
{id: 3, name: 'C'},
],
it => it.id,
),
).toEqual({id: 1, name: 'A'});
});
it('should not return undefined if there are other choices', function () {
expect(
minBy(
[
{id: undefined, name: 'B'},
{id: 1, name: 'A'},
{id: undefined, name: 'C'},
],
it => it.id,
),
).toEqual({id: 1, name: 'A'});
});
});

View File

@@ -0,0 +1,23 @@
/*
* 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/>.
*/
/**
* Returns the minimum value of a collection.
*/
export function minBy<T>(array: T[], transform: (item: T) => number | undefined): T {
const transforms = array.map(transform);
const min = Math.min(...(transforms.filter(it => !!it) as number[]));
return array.find((_, i) => transforms[i] === min) as T;
}

View File

@@ -0,0 +1,23 @@
/*
* 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 {omit} from './omit';
describe('omit', function () {
it('should omit keys', function () {
const object = {a: 1, b: 2, c: 3};
const result = omit(object, 'a', 'c');
expect(result).toEqual({b: 2});
});
});

View File

@@ -0,0 +1,23 @@
/*
* 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/>.
*/
/**
* Returns a new object without the specified keys.
*/
export function omit<T extends object, U extends keyof T>(object: T, ...keys: U[]): Omit<T, U> {
const out = {...object};
for (const key of keys) delete out[key];
return out as Exclude<T, U>;
}

View File

@@ -0,0 +1,25 @@
/*
* 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 {partition} from './partition';
describe('partition', function () {
it('should partition an array', function () {
expect(partition([1, 2, 3, 4], it => it % 2 === 0)).toEqual([
[2, 4],
[1, 3],
]);
});
});

View File

@@ -0,0 +1,28 @@
/*
* 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/>.
*/
/**
* Partitions a list into two lists. One with the elements that satisfy a predicate,
* and one with the elements that don't satisfy the predicate.
*/
export function partition<T>(array: T[], transform: (item: T) => boolean): [T[], T[]] {
return array.reduce<[T[], T[]]>(
(accumulator, item) => {
accumulator[transform(item) ? 0 : 1].push(item);
return accumulator;
},
[[], []],
);
}

View File

@@ -0,0 +1,23 @@
/*
* 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 {pick} from './pick';
describe('pick', function () {
it('should pick properties', function () {
const object = {a: 1, b: 2, c: 3};
const result = pick(object, ['a', 'c']);
expect(result).toEqual({a: 1, c: 3});
});
});

View File

@@ -0,0 +1,41 @@
/*
* 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/>.
*/
/**
* Pick a set of properties from an object
*/
export function pick<T extends object, U extends keyof T>(object: T, keys: U[]): Pick<T, U> {
return keys.reduce((accumulator, key) => {
if (object.hasOwnProperty(key)) {
accumulator[key] = object[key];
}
return accumulator;
}, {} as Pick<T, U>);
}
/**
* Pick a set of properties from an object using a predicate function
*/
export function pickBy<T extends object, U extends keyof T>(
object: T,
predicate: (value: T[U], key: U) => boolean,
): Pick<T, U> {
return (Object.keys(object) as U[]).reduce((accumulator, key) => {
if (predicate(object[key], key)) {
accumulator[key] = object[key];
}
return accumulator;
}, {} as Pick<T, U>);
}

View File

@@ -0,0 +1,30 @@
/*
* 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 {shuffle} from './shuffle';
describe('shuffle', function () {
it('should shuffle an array', function () {
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const shuffled = shuffle(array);
expect(shuffled).not.toEqual(array);
expect(shuffled).toEqual(jasmine.arrayContaining(array));
});
it('should not modify the original array', function () {
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
shuffle(array);
expect(array).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
});

View File

@@ -0,0 +1,28 @@
/*
* 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/>.
*/
/**
* Shuffles an array
*/
export function shuffle<T>(array: T[]): T[] {
const copy = [...array];
const out = [];
while (copy.length > 0) {
out.push(copy.splice(Math.floor(Math.random() * copy.length), 1)[0]);
}
return out;
}

View File

@@ -0,0 +1,33 @@
/*
* 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 {stringSort, stringSortBy} from './string-sort';
describe('stringSort', () => {
it('should sort an array of strings', () => {
expect(['a', 'c', 'b', 'd'].sort(stringSort)).toEqual(['a', 'b', 'c', 'd']);
});
});
describe('stringSortBy', () => {
it('should sort an array of strings', () => {
expect([{item: 'a'}, {item: 'c'}, {item: 'b'}, {item: 'd'}].sort(stringSortBy(it => it.item))).toEqual([
{item: 'a'},
{item: 'b'},
{item: 'c'},
{item: 'd'},
]);
});
});

View File

@@ -0,0 +1,36 @@
/*
* 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/>.
*/
/**
* sort function for two strings
*/
export function stringSort(a = '', b = ''): number {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
/**
* sort function for two strings that allows for a custom transform
*/
export function stringSortBy<T>(map: (item: T) => string | undefined): (a: T, b: T) => number {
return (a: T, b: T): number => {
const aValue = map(a) || '';
const bValue = map(b) || '';
if (aValue < bValue) return -1;
if (aValue > bValue) return 1;
return 0;
};
}

View File

@@ -0,0 +1,31 @@
/*
* 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 {sum, sumBy} from './sum';
describe('sum', () => {
it('should return the sum of all elements in the collection', () => {
const collection = [1, 2, 3, 4, 5];
const result = sum(collection);
expect(result).toBe(15);
});
});
describe('sumBy', function () {
it('should return the sum of all elements in the collection', () => {
const collection = [{a: 1}, {a: 2}, {a: 3}, {a: 4}, {a: 5}];
const result = sumBy(collection, it => it.a);
expect(result).toBe(15);
});
});

View File

@@ -0,0 +1,31 @@
/*
* 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/>.
*/
/**
* Sum an an array
*/
export function sumBy<T extends object>(
collection: T[],
transform: (value: T) => number | undefined,
): number {
return collection.reduce((accumulator, item) => accumulator + (transform(item) || 0), 0);
}
/**
* Sum an array of numbers
*/
export function sum(collection: Array<number | undefined>): number {
return collection.reduce<number>((accumulator, item) => accumulator + (item || 0), 0);
}

View File

@@ -0,0 +1,74 @@
/*
* 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 {Tree, treeGroupBy} from './tree-group';
interface TestItem {
id: number;
path?: string[];
}
describe('tree-group', function () {
it('should create a tree', function () {
const items: Array<TestItem> = [
{
id: 1,
path: ['a', 'b', 'c'],
},
{
id: 2,
path: ['a', 'b', 'd'],
},
];
const tree = treeGroupBy(items, item => item.path ?? []);
const expectedTree: Tree<TestItem> = {
a: {
b: {
c: {_: [items[0]]},
d: {_: [items[1]]},
} as Tree<TestItem>,
} as Tree<TestItem>,
} as Tree<TestItem>;
expect(tree).toEqual(expectedTree);
});
it('should also sort empty paths', () => {
const items: Array<TestItem> = [
{
id: 1,
path: ['a', 'b', 'c'],
},
{
id: 2,
},
];
const tree = treeGroupBy(items, item => item.path ?? []);
const expectedTree: Tree<TestItem> = {
a: {
b: {
c: {_: [items[0]]},
} as Tree<TestItem>,
} as Tree<TestItem>,
_: [items[1]],
} as Tree<TestItem>;
expect(tree).toEqual(expectedTree);
});
});

View File

@@ -0,0 +1,46 @@
/*
* 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/>.
*/
export type Tree<T> = {
[key: string]: Tree<T>;
} & {
_?: T[] | undefined;
};
/**
*
*/
export function treeGroupBy<T>(items: T[], transform: (item: T) => string[]): Tree<T> {
const tree: Tree<T> = {};
for (const item of items) {
let currentTree = tree;
const keys = transform(item);
if (keys.length === 0) {
currentTree._ = currentTree._ || [];
currentTree._.push(item);
}
for (const [i, key] of keys.entries()) {
currentTree = currentTree[key] = (currentTree[key] ?? {}) as Tree<T>;
if (i === keys.length - 1) {
currentTree._ = currentTree._ ?? [];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
currentTree._.push(item);
}
}
}
return tree;
}

View File

@@ -0,0 +1,24 @@
/*
* 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 {uniqBy} from './uniq';
describe('uniq', function () {
it('should return an array with unique values', function () {
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = uniqBy(array, it => it);
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
});

View File

@@ -0,0 +1,26 @@
/*
* 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/>.
*/
/**
* Filter out duplicates from an array.
*/
export function uniqBy<T>(array: T[], transform: (item: T) => string | number): T[] {
return Object.values(
array.reduce((accumulator, current) => {
accumulator[transform(current)] = current;
return accumulator;
}, {} as Record<string | number, T>),
);
}

View File

@@ -0,0 +1,25 @@
/*
* 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 {zip} from './zip';
describe('zip', function () {
it('should zip arrays together', function () {
expect(zip([1, 2, 3], [4, 5, 6])).toEqual([
[1, 4],
[2, 5],
[3, 6],
]);
});
});

View File

@@ -0,0 +1,21 @@
/*
* 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/>.
*/
/**
* Zip two arrays together.
*/
export function zip<T, U>(a: T[], b: U[]): [T, U][] {
return a.map((_, i) => [a[i], b[i]]);
}

View File

@@ -0,0 +1,178 @@
/*
* 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 {SCSearchBooleanFilter, SCSearchFilter, SCSearchValueFilter, SCThing} from '@openstapps/core';
import {logger} from '../ts-logger';
/**
* Checks if any filter applies to an SCThing
*/
export function checkFilter(thing: SCThing, filter: SCSearchFilter): boolean {
switch (filter.type) {
case 'availability' /*TODO*/:
break;
case 'boolean':
return applyBooleanFilter(thing, filter);
case 'distance' /*TODO*/:
break;
case 'value':
return applyValueFilter(thing, filter);
}
void logger.error(`Not implemented filter method "${filter.type}" in fake backend!`);
return false;
}
/**
* Checks if a value filter applies to an SCThing
*/
function applyValueFilter(thing: SCThing, filter: SCSearchValueFilter): boolean {
const path = filter.arguments.field.split('.');
const thingFieldValue = traverseToFieldPath(thing, path, filter.arguments.value);
if (!thingFieldValue.found) {
return false;
}
return thingFieldValue.result;
}
/**
* Object that can be accessed using foo[bar]
*/
interface IndexableObject {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
/**
* Result of a search for a field and comparison to a desired value
*/
type FieldSearchResult =
| {
/**
* Weather the field was found
*/
found: true;
/**
* The result of the comparison
*/
result: boolean;
}
| {
/**
* Weather the field was found
*/
found: false;
};
/**
* TODO
*/
function traverseToFieldPath(
value: IndexableObject,
path: string[],
desiredFieldValue: unknown,
): FieldSearchResult {
if (path.length === 0) {
void logger.error(`Value filter provided with zero length path`);
return {found: false};
}
if (value.hasOwnProperty(path[0])) {
const nestedProperty = value[path[0]];
if (path.length === 1) {
return esStyleFieldHandler(nestedProperty, nestedValue => {
return {
found: true,
result: nestedValue === desiredFieldValue,
};
});
}
return esStyleFieldHandler(nestedProperty, nestedValue => {
if (typeof nestedValue === 'object') {
return traverseToFieldPath(
nestedValue as IndexableObject,
// eslint-disable-next-line no-magic-numbers
path.slice(1),
desiredFieldValue,
);
}
return {found: false};
});
}
return {found: false};
}
/**
* ES treats arrays like normal fields
*/
function esStyleFieldHandler<T>(field: T | T[], handler: (value: T) => FieldSearchResult): FieldSearchResult {
if (Array.isArray(field)) {
for (const nestedField of field) {
const result = handler(nestedField);
if (result.found && result.result) {
return result;
}
}
// TODO: found is not accurate
return {found: false};
}
return handler(field);
}
/**
* Checks if a boolean filter applies to an SCThing
*/
function applyBooleanFilter(thing: SCThing, filter: SCSearchBooleanFilter): boolean {
let out = false;
switch (filter.arguments.operation) {
case 'and':
out = true;
for (const nesterFilter of filter.arguments.filters) {
out = out && checkFilter(thing, nesterFilter);
}
return out;
case 'or':
for (const nesterFilter of filter.arguments.filters) {
out = out || checkFilter(thing, nesterFilter);
}
return out;
case 'not':
if (filter.arguments.filters.length === 1) {
return !checkFilter(thing, filter.arguments.filters[0]);
}
void logger.error(`Too many filters for "not" boolean operation`);
return false;
}
void logger.error(`Not implemented boolean filter "${filter.arguments.operation}"`);
return false;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,259 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
SCAboutPageContentType,
SCAuthorizationProvider,
SCBackendAggregationConfiguration,
SCIndexResponse,
SCSettingInputType,
SCThingOriginType,
SCThingType,
} from '@openstapps/core';
import {Polygon} from 'geojson';
import packageJson from '../../../../package.json';
// provides sample aggregations to be used in tests or backendless development
export const sampleAggregations: SCBackendAggregationConfiguration[] = [
{
fieldName: 'categories',
onlyOnTypes: [
SCThingType.AcademicEvent,
SCThingType.Article,
SCThingType.Building,
SCThingType.Catalog,
SCThingType.Dish,
SCThingType.PointOfInterest,
SCThingType.Room,
],
},
{
fieldName: 'inPlace.name',
onlyOnTypes: [
SCThingType.DateSeries,
SCThingType.Dish,
SCThingType.Floor,
SCThingType.Organization,
SCThingType.PointOfInterest,
SCThingType.Room,
SCThingType.Ticket,
],
},
{
fieldName: 'academicTerms.acronym',
onlyOnTypes: [SCThingType.AcademicEvent, SCThingType.SportCourse],
},
{
fieldName: 'academicTerm.acronym',
onlyOnTypes: [SCThingType.Catalog],
},
{
fieldName: 'majors',
onlyOnTypes: [SCThingType.AcademicEvent],
},
{
fieldName: 'keywords',
onlyOnTypes: [SCThingType.Article, SCThingType.Book, SCThingType.Message, SCThingType.Video],
},
{
fieldName: 'type',
},
];
export const sampleAuthConfiguration: {
default: SCAuthorizationProvider;
paia: SCAuthorizationProvider;
} = {
default: {
client: {clientId: '', scopes: '', url: ''},
endpoints: {
authorization: '',
mapping: {id: '', name: ''},
token: '',
userinfo: '',
},
},
paia: {
client: {clientId: '', scopes: '', url: ''},
endpoints: {
authorization: '',
mapping: {id: '', name: ''},
token: '',
userinfo: '',
},
},
};
export const sampleDefaultPolygon: Polygon = {
coordinates: [
[
[8.660432999690723, 50.123027017044436],
[8.675496285518358, 50.123027017044436],
[8.675496285518358, 50.13066176448642],
[8.660432999690723, 50.13066176448642],
[8.660432999690723, 50.123027017044436],
],
],
type: 'Polygon',
};
const scVersion = packageJson.dependencies['@openstapps/core'];
export const sampleIndexResponse: SCIndexResponse = {
app: {
aboutPages: {
about: {
title: 'About',
content: [
{
value: 'This is the about page',
type: SCAboutPageContentType.MARKDOWN,
translations: {
en: {
value: 'This is the about page',
},
},
},
],
translations: {
en: {
title: 'About',
},
},
},
},
campusPolygon: {
coordinates: [[[1, 2]], [[1, 2]]],
type: 'Polygon',
},
features: {},
menus: [
{
icon: 'icon',
items: [
{
icon: 'icon',
route: '/index',
title: 'start',
translations: {
de: {
title: 'Start',
},
en: {
title: 'start',
},
},
},
],
title: 'main',
route: '/main',
translations: {
de: {
title: 'Haupt',
},
en: {
title: 'main',
},
},
},
],
name: 'StApps',
privacyPolicyUrl: 'foo.bar',
settings: [
{
categories: ['credentials'],
defaultValue: '',
inputType: SCSettingInputType.Text,
name: 'username',
order: 0,
origin: {
indexed: '2018-09-11T12:30:00Z',
name: 'Dummy',
type: SCThingOriginType.Remote,
},
translations: {
de: {
name: 'Benutzername',
},
en: {
name: 'Username',
},
},
type: SCThingType.Setting,
uid: '',
},
],
},
auth: {},
backend: {
SCVersion: scVersion,
externalRequestTimeout: 5000,
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
mappingIgnoredTags: [],
maxMultiSearchRouteQueries: 5,
maxRequestBodySize: 512 * 1024,
name: 'Technische Universität Berlin',
namespace: '909a8cbc-8520-456c-b474-ef1525f14209',
sortableFields: [
{
fieldName: 'name',
sortTypes: ['ducet'],
},
{
fieldName: 'type',
sortTypes: ['ducet'],
},
{
fieldName: 'categories',
onlyOnTypes: [
SCThingType.AcademicEvent,
SCThingType.Building,
SCThingType.Catalog,
SCThingType.Dish,
SCThingType.PointOfInterest,
SCThingType.Room,
],
sortTypes: ['ducet'],
},
{
fieldName: 'geo',
onlyOnTypes: [SCThingType.Building, SCThingType.PointOfInterest, SCThingType.Room],
sortTypes: ['distance'],
},
{
fieldName: 'geo',
onlyOnTypes: [SCThingType.Building, SCThingType.PointOfInterest, SCThingType.Room],
sortTypes: ['distance'],
},
{
fieldName: 'inPlace.geo',
onlyOnTypes: [
SCThingType.DateSeries,
SCThingType.Dish,
SCThingType.Floor,
SCThingType.Organization,
SCThingType.PointOfInterest,
SCThingType.Room,
SCThingType.Ticket,
],
sortTypes: ['distance'],
},
{
fieldName: 'offers',
onlyOnTypes: [SCThingType.Dish],
sortTypes: ['price'],
},
],
},
};

View File

@@ -0,0 +1,113 @@
/*
* Copyright (C) 2019, 2020 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 {SCFacet, SCThingType} from '@openstapps/core';
export const facetsMock: SCFacet[] = [
{
buckets: [
{
count: 60,
key: 'academic event',
},
{
count: 160,
key: 'message',
},
{
count: 151,
key: 'date series',
},
{
count: 106,
key: 'dish',
},
{
count: 20,
key: 'building',
},
{
count: 20,
key: 'semester',
},
],
field: 'type',
},
{
buckets: [
{
count: 12,
key: 'Max Mustermann',
},
{
count: 2,
key: 'Foo Bar',
},
],
field: 'performers',
onlyOnType: SCThingType.AcademicEvent,
},
{
buckets: [
{
count: 5,
key: 'colloquium',
},
{
count: 15,
key: 'course',
},
],
field: 'categories',
onlyOnType: SCThingType.AcademicEvent,
},
{
buckets: [
{
count: 5,
key: 'unipedia',
},
],
field: 'categories',
onlyOnType: SCThingType.Article,
},
{
buckets: [
{
count: 5,
key: 'employees',
},
{
count: 15,
key: 'students',
},
],
field: 'audiences',
onlyOnType: SCThingType.Message,
},
{
buckets: [
{
count: 5,
key: 'main dish',
},
{
count: 15,
key: 'salad',
},
],
field: 'categories',
onlyOnType: SCThingType.Dish,
},
];

View File

@@ -0,0 +1,438 @@
/*
* 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 {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {
SCAcademicEvent,
SCArticle,
SCBook,
SCBuilding,
SCCatalog,
SCDateSeries,
SCDish,
SCFavorite,
SCMessage,
SCPerson,
SCRoom,
SCSearchFilter,
SCThing,
SCThingOriginType,
SCThingType,
SCToDo,
SCToDoPriority,
} from '@openstapps/core';
import {Observable, of} from 'rxjs';
import {checkFilter} from './filters';
import {sampleResources} from './resources/test-resources';
/* eslint-disable */
const sampleMessages: SCMessage[] = [
{
audiences: ['students'],
categories: ['news'],
messageBody: 'Foo Message Text',
name: 'Foo Message',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Message,
uid: 'message-123',
},
{
audiences: ['employees'],
categories: ['news'],
messageBody: 'Bar Message Text',
name: 'Bar Message',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Message,
uid: 'message-456',
},
];
const sampleDishes: SCDish[] = [
{
categories: ['main dish', 'salad'],
name: 'Foo Dish',
// offers: [
// {
// 'availability': 'in stock',
// 'availabilityStarts': '2017-01-30T00:00:00.000Z',
// 'availabilityEnds': '2017-01-30T23:59:59.999Z',
// 'prices': {
// 'default': 4.85,
// 'student': 2.85,
// 'employee': 3.85,
// 'guest': 4.85,
// },
// ],
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Dish,
uid: 'dish-123',
},
{
categories: ['side dish', 'salad'],
name: 'Bar Dish',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Dish,
uid: 'dish-456',
},
];
const sampleBuildings: SCBuilding[] = [
{
categories: ['education'],
geo: {
point: {type: 'Point', coordinates: [12, 12]},
},
name: 'Foo Building',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Building,
uid: 'building-123',
},
];
const sampleRooms: SCRoom[] = [
{
categories: ['library'],
geo: {
point: {type: 'Point', coordinates: [12, 12]},
},
name: 'Foo Room',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Room,
uid: 'room-123',
},
];
const sampleArticles: SCArticle[] = [
{
articleBody: 'Foo Text',
categories: ['unipedia'],
name: 'Foo Article',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Article,
uid: 'article-123',
},
{
articleBody: 'Bar Text',
categories: ['unipedia'],
name: 'Bar Article',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Article,
uid: 'article-456',
},
];
const samplePersons: SCPerson[] = [
{
familyName: 'Person',
givenName: 'Foo',
name: 'Foo Person',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Person,
uid: 'person-123',
},
{
familyName: 'Person',
givenName: 'Bar',
name: 'Bar Person',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Person,
uid: 'person-456',
},
];
const sampleBooks: SCBook[] = [
{
authors: samplePersons,
ISBNs: ['123456'],
categories: ['ebook'],
name: 'Foo Book',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Book,
uid: 'HEB290615194',
},
{
authors: [],
ISBNs: ['123456'],
categories: ['book'],
name: 'Bar Book',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Book,
uid: 'book-234',
},
];
const sampleCatalogs: SCCatalog[] = [
{
categories: ['university events'],
level: 1,
name: 'Foo Catalog',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Catalog,
uid: 'catalog-123',
},
{
categories: ['university events'],
level: 1,
name: 'Bar Catalog',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.Catalog,
uid: 'catalog-456',
},
];
const sampleTodos: SCToDo[] = [
{
categories: ['foo category'],
done: false,
name: 'Foo Todo',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
priority: SCToDoPriority.LOW,
type: SCThingType.ToDo,
uid: 'todo-123',
},
{
categories: ['bar category'],
done: true,
name: 'Bar Todo',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
priority: SCToDoPriority.HIGH,
type: SCThingType.ToDo,
uid: 'todo-456',
},
];
const sampleFavorites: SCFavorite[] = [
{
data: sampleBuildings[0],
name: 'Foo Favorite',
origin: {
created: 'SOME-DATE',
type: SCThingOriginType.User,
},
type: SCThingType.Favorite,
uid: 'favorite-123',
},
{
data: samplePersons[1],
name: 'Bar Favorite',
origin: {
created: 'SOME-DATE',
type: SCThingOriginType.User,
},
type: SCThingType.Favorite,
uid: 'favorite-456',
},
];
const sampleAcademicEvents: SCAcademicEvent[] = [
{
categories: ['course'],
majors: ['Major One', 'Major Two'],
name: 'Foo Academic Event',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
performers: samplePersons,
type: SCThingType.AcademicEvent,
uid: 'academic-event-123',
},
{
categories: ['practicum'],
majors: ['Major Two', 'Major Three'],
name: 'Bar Academic Event',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
performers: samplePersons,
type: SCThingType.AcademicEvent,
uid: 'academic-event-456',
},
];
const sampleDateSeries: SCDateSeries[] = [
{
dates: ['2019-03-01T17:00:00+00:00', '2019-03-08T17:00:00+00:00'],
duration: 'PT2H',
event: sampleAcademicEvents[0],
repeatFrequency: 'P1W',
name: 'Foo Date Event - Date Series',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.DateSeries,
uid: 'date-series-123',
},
{
dates: ['2019-03-03T10:00:00+00:00', '2019-03-11T10:00:00+00:00'],
duration: 'PT2H',
event: sampleAcademicEvents[1],
name: 'Bar Date Event - Date Series',
origin: {
indexed: 'SOME-DATE',
name: 'some name',
type: SCThingOriginType.Remote,
},
type: SCThingType.DateSeries,
uid: 'date-series-456',
},
];
export const sampleThingsMap: {[key in SCThingType | string]: SCThing[]} = {
'academic event': sampleAcademicEvents,
'article': sampleArticles,
'book': sampleBooks,
'building': sampleBuildings,
'catalog': sampleCatalogs,
'course of studies': [],
'date series': sampleDateSeries,
'diff': [],
'dish': sampleDishes,
'favorite': sampleFavorites,
'floor': [],
'message': sampleMessages,
'organization': [],
'person': samplePersons,
'point of interest': [],
'room': sampleRooms,
'semester': [],
'setting': [],
'sport course': [],
'ticket': [],
'todo': sampleTodos,
'tour': [],
'video': [],
};
/**
* TODO
*/
@Injectable()
export class SampleThings {
/**
* TODO
*/
http: HttpClient;
constructor(http: HttpClient) {
this.http = http;
}
/**
* TODO
*/
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-explicit-any
getSampleThing(uid: string): Observable<any[]> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sampleThings: any[] = [];
for (const resource of sampleResources) {
if ((resource.instance.uid as SCThingType) === uid) {
sampleThings.push(resource.instance);
return of(sampleThings);
}
}
return of(sampleThings);
}
/**
* TODO
*/
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-explicit-any
getSampleThings(filter?: SCSearchFilter): Observable<any[]> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sampleThings: any[] = [];
for (const resource of sampleResources) {
// eslint-disable-next-line max-len
// if ([SCThingType.Video].includes(resource.instance.type as SCThingType)) {
if (typeof filter === 'undefined' || checkFilter(resource.instance as SCThing, filter)) {
sampleThings.push(resource.instance);
}
// }
}
return of(sampleThings);
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2019 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/>.
*/
/**
* An error that can occur in the StApps app
*/
export class AppError extends Error {
/**
* TODO
*
* @param name Name of the error
* @param message Message of the error
*/
constructor(name: string, message: string) {
super(message);
this.name = name;
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Observable} from 'rxjs';
/**
*
*/
export function fromMutationObserver(
target: Node,
options?: MutationObserverInit,
): Observable<MutationRecord[]> {
return new Observable(subscriber => {
const observer = new MutationObserver(mutations => {
subscriber.next(mutations);
});
observer.observe(target, options);
return () => {
observer.disconnect();
};
});
}

View File

@@ -0,0 +1,42 @@
/*
* 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 {HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse} from '@angular/common/http';
import {Observable, throwError} from 'rxjs';
import {NGXLogger} from 'ngx-logger';
import {catchError} from 'rxjs/operators';
@Injectable()
export class ServiceHandlerInterceptor implements HttpInterceptor {
constructor(private readonly logger: NGXLogger) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
// Fixes the issue of errors dropping into "toPromise()"
// and being not able to catch it in the "caller methods"
catchError((error: HttpErrorResponse) => {
const errorMessage =
error.error instanceof ErrorEvent
? `Error: ${error.error.message}`
: `Error Code: ${error.status}, Message: ${error.message}`;
this.logger.error(errorMessage);
return throwError(error);
}),
);
}
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (C) 2020 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 {NGXLogger} from 'ngx-logger';
export let logger: NGXLogger;
export const initLogger = (newLogger: NGXLogger) => (logger = newLogger);

View File

@@ -0,0 +1,90 @@
/*
* 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 {SHARED_AXIS_DIRECTIONS} from './material-motion';
/**
* /**
* Choreograph a shared axis animation based on a row of values so that changing state
* results in the correct, expected behavior of reverting the previous animation etc.
*
* The Choreographer manages motion of an element that changes value. This can be used in a variety of ways,
* for example multi-view choreographing can be achieved as such
*
* ```html
* <div [ngSwitch]='choreographer.state'
* [@animation]='choreographer.animationState'
* [@animation.done]='choreographer.done()'>
* <div *ngSwitchCase='"a"'/>
* <div *ngSwitchCase='"b"'/>
* </div>
* ```
*
* @see {@link https://material.io/design/motion/the-motion-system.html#shared-axis}
*/
export class SharedAxisChoreographer<T> {
/**
* Expected next value
*/
private expectedValue: T;
/**
* Animation State
*/
animationState: string;
/**
* Current value to read from
*/
currentValue: T;
constructor(initialValue: T, readonly pages?: T[]) {
this.currentValue = initialValue;
this.expectedValue = initialValue;
}
/**
* Must be linked to the animation callback
*/
animationDone() {
this.animationState = 'in';
this.currentValue = this.expectedValue;
}
/**
* Change view for a new state that the current active view should receive
*/
changeViewForState(newValue: T, direction?: -1 | 0 | 1) {
if (direction === 0) {
this.currentValue = this.expectedValue = newValue;
return;
}
this.expectedValue = newValue;
// pre-place animation state
// new element comes in from the right and pushes the old one to the left
this.animationState = SHARED_AXIS_DIRECTIONS[direction ?? this.getDirection(this.currentValue, newValue)];
}
/**
* Get direction from to
*/
getDirection(from: T, to: T) {
const element = this.pages?.find(it => it === from || it === to);
return element === from ? 1 : element === to ? -1 : 0;
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
// these are the ionic values
export const iosEasing = 'cubic-bezier(0.32,0.72,0,1)';
export const iosDuration = 540;
export const mdEasing = 'cubic-bezier(0.36,0.66,0.04,1)';
export const mdDuration = 280;

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {AnimationBuilder, AnimationController} from '@ionic/angular';
import {AnimationOptions} from '@ionic/angular/providers/nav-controller';
import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
/**
*
*/
export function fabExpand(animationController: AnimationController): AnimationBuilder {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
const rootTransition = animationController
.create()
.duration(options.duration ?? (options.mode === 'ios' ? iosDuration : mdDuration * 1.4))
.easing(options.mode === 'ios' ? iosEasing : mdEasing);
const back = options.direction === 'back';
const fabView = back ? options.enteringEl! : options.leavingEl!;
const otherView = back ? options.leavingEl! : options.enteringEl!;
const fab = fabView.querySelector('ion-fab-button').shadowRoot.querySelector('.button-native');
const fabBounds = fab.getBoundingClientRect();
const viewBounds = otherView.getBoundingClientRect();
const useReducedMotion = viewBounds.width > 500;
const reducedMotionTransform = `${Math.min(viewBounds.width * 0.3, 200)}px`;
const reducedMotionViewBorderRadius = '128px';
const reducedMotionFabGrow = '2';
const reducedMotionViewShrink = '0.9';
const viewCenterX = (viewBounds.width - viewBounds.x) / 2;
const viewCenterY = (viewBounds.height - viewBounds.y) / 2;
const viewOnFab = useReducedMotion
? `translate(${reducedMotionTransform}, ${reducedMotionTransform}) scale(${reducedMotionViewShrink})`
: `translate(${(fabBounds.x - viewBounds.x) / 2}px, ${(fabBounds.y - viewBounds.y) / 2}px) scale(${
fabBounds.width / viewBounds.width
}, ${fabBounds.height / viewBounds.height})`;
const fabOnView = useReducedMotion
? `translate(-${reducedMotionTransform}, -${reducedMotionTransform}) scale(${reducedMotionFabGrow})`
: `translate(${viewCenterX - fabBounds.x}px, ${viewCenterY - fabBounds.y}px) scale(${
viewBounds.width / fabBounds.width
}, ${viewBounds.height / fabBounds.height})`;
const transformNormal = `translate(0px, 0px) scale(1, 1)`;
const viewBorderRadius = useReducedMotion ? reducedMotionViewBorderRadius : '50%';
const fabViewFade = animationController
.create()
.beforeStyles({zIndex: -1})
.fromTo('opacity', '1', '1')
.addElement(fabView);
const fabGrow = animationController
.create()
.beforeStyles({transformOrigin: 'center'})
.fromTo('transform', back ? fabOnView : transformNormal, back ? transformNormal : fabOnView)
.fromTo('opacity', back ? '0' : '1', back ? '1' : '0')
.fromTo('borderRadius', back ? '0' : '50%', back ? '50%' : '0')
.addElement(fab);
const viewGrow = animationController
.create()
.beforeStyles({zIndex: 200, overflow: 'hidden', transformOrigin: 'center'})
.fromTo('transform', back ? transformNormal : viewOnFab, back ? viewOnFab : transformNormal)
.fromTo('opacity', back ? '1' : '0', back ? '0' : '1')
.fromTo('borderRadius', back ? '0' : viewBorderRadius, back ? viewBorderRadius : '0')
.addElement(otherView);
return rootTransition.addAnimation(fabGrow).addAnimation(viewGrow).addAnimation(fabViewFade);
};
}

View File

@@ -0,0 +1,78 @@
/*
* 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 {animate, sequence, state, style, transition, trigger} from '@angular/animations';
/**
* Fade transition
*
* @see {@link https://material.io/design/motion/the-motion-system.html#fade}
*/
export const materialFade = trigger('materialFade', [
state('in', style({opacity: 1})),
transition(':enter', [style({opacity: 0}), animate('250ms ease')]),
transition(':leave', [animate('200ms ease', style({opacity: 0}))]),
]);
/**
* Fade transition
*
* @see {@link https://material.io/design/motion/the-motion-system.html#fade}
*/
export const materialManualFade = trigger('materialManualFade', [
state('in', style({opacity: 1})),
state('out', style({opacity: 0})),
transition('in => out', animate('200ms ease')),
transition('out => in', animate('250ms ease')),
]);
/**
* Fade through transition
*
* @see {@link https://material.io/design/motion/the-motion-system.html#fade-through}
*/
export const materialFadeThrough = trigger('materialFadeThrough', [
state('in', style({transform: 'scale(100%)', opacity: 1})),
transition(':enter', [style({transform: 'scale(80%)', opacity: 0}), animate('250ms ease')]),
transition(':leave', [animate('200ms ease', style({opacity: 0}))]),
]);
export const SHARED_AXIS_DIRECTIONS = {
[-1]: 'go-backward',
[0]: 'in',
[1]: 'go-forward',
};
/**
* Shared axis transition along the X-Axis
*
* Needs to be manually choreographed
*
* @see {@link https://material.io/design/motion/the-motion-system.html#shared-axis}
* @see {SharedAxisChoreographer}
*/
export const materialSharedAxisX = trigger('materialSharedAxisX', [
state(SHARED_AXIS_DIRECTIONS[-1], style({opacity: 0, transform: 'translateX(30px)'})),
state(SHARED_AXIS_DIRECTIONS[0], style({opacity: 1, transform: 'translateX(0px)'})),
state(SHARED_AXIS_DIRECTIONS[1], style({opacity: 0, transform: 'translateX(-30px)'})),
transition(
`${SHARED_AXIS_DIRECTIONS[-1]} => ${SHARED_AXIS_DIRECTIONS[0]}`,
sequence([style({opacity: 0, transform: 'translateX(-30px)'}), animate('100ms ease-out')]),
),
transition(`${SHARED_AXIS_DIRECTIONS[0]} => *`, animate('100ms ease-out')),
transition(
`${SHARED_AXIS_DIRECTIONS[1]} => ${SHARED_AXIS_DIRECTIONS[0]}`,
sequence([style({opacity: 0, transform: 'translateX(30px)'}), animate('100ms ease-out')]),
),
]);

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2021 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 {animate, style, transition, trigger} from '@angular/animations';
export const chipTransition = trigger('chipTransition', [
transition(':enter', [
style({
'opacity': 0,
'transform': 'scaleX(80%)',
'transform-origin': 'left',
}),
animate('200ms ease', style({opacity: 1, transform: 'scaleX(100%)'})),
]),
]);
export const chipSkeletonTransition = trigger('chipSkeletonTransition', [
transition(':leave', [
style({
'opacity': 1,
'transform': 'scaleX(100%)',
'transform-origin': 'left',
}),
animate('200ms ease', style({opacity: 0, transform: 'scaleX(120%)'})),
]),
]);

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2018, 2019 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 {NgModule} from '@angular/core';
import {PreloadAllModules, RouterModule, Routes} from '@angular/router';
const routes: Routes = [{path: '', redirectTo: '/overview', pathMatch: 'full'}];
/**
* TODO
*/
@NgModule({
exports: [RouterModule],
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules,
errorHandler: error => {
// Handle unknown routes, at the moment this can only be done via window.location
if (error.message.includes('Cannot match any routes')) {
window.location.href = '/overview';
}
},
}),
],
})
export class AppRoutingModule {}

View File

@@ -0,0 +1,18 @@
<!--
~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-app style="margin-top: env(safe-area-inset-top)">
<stapps-navigation></stapps-navigation>
</ion-app>

View File

@@ -0,0 +1,109 @@
/*
* 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/>.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {Platform} from '@ionic/angular';
import {TranslateService} from '@ngx-translate/core';
import {ThingTranslateService} from './translation/thing-translate.service';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {AppComponent} from './app.component';
import {AuthModule} from './modules/auth/auth.module';
import {ConfigProvider} from './modules/config/config.provider';
import {SettingsProvider} from './modules/settings/settings.provider';
import {NGXLogger} from 'ngx-logger';
import {RouterTestingModule} from '@angular/router/testing';
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
import {sampleAuthConfiguration} from './_helpers/data/sample-configuration';
import {StorageProvider} from './modules/storage/storage.provider';
import {SimpleBrowser} from './util/browser.factory';
describe('AppComponent', () => {
let platformReadySpy: any;
let platformSpy: jasmine.SpyObj<Platform>;
let translateServiceSpy: jasmine.SpyObj<TranslateService>;
let thingTranslateServiceSpy: jasmine.SpyObj<ThingTranslateService>;
let settingsProvider: jasmine.SpyObj<SettingsProvider>;
let configProvider: jasmine.SpyObj<ConfigProvider>;
let ngxLogger: jasmine.SpyObj<NGXLogger>;
let scheduleSyncServiceSpy: jasmine.SpyObj<ScheduleSyncService>;
let platformIsSpy;
let storageProvider: jasmine.SpyObj<StorageProvider>;
let simpleBrowser: jasmine.SpyObj<SimpleBrowser>;
beforeEach(() => {
platformReadySpy = Promise.resolve();
platformIsSpy = Promise.resolve();
platformSpy = jasmine.createSpyObj('Platform', {
ready: platformReadySpy,
is: platformIsSpy,
});
translateServiceSpy = jasmine.createSpyObj('TranslateService', ['setDefaultLang', 'use']);
thingTranslateServiceSpy = jasmine.createSpyObj('ThingTranslateService', ['init']);
settingsProvider = jasmine.createSpyObj('SettingsProvider', [
'getSettingValue',
'provideSetting',
'setCategoriesOrder',
]);
scheduleSyncServiceSpy = jasmine.createSpyObj('ScheduleSyncService', [
'getDifferences',
'postDifferencesNotification',
]);
configProvider = jasmine.createSpyObj('ConfigProvider', ['init', 'getAnyValue']);
configProvider.getAnyValue = jasmine.createSpy().and.callFake(function () {
return sampleAuthConfiguration;
});
ngxLogger = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']);
storageProvider = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']);
simpleBrowser = jasmine.createSpyObj('SimpleBrowser', ['open']);
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([]), HttpClientTestingModule, AuthModule],
declarations: [AppComponent],
providers: [
{provide: Platform, useValue: platformSpy},
{provide: TranslateService, useValue: translateServiceSpy},
{provide: ThingTranslateService, useValue: thingTranslateServiceSpy},
{provide: ScheduleSyncService, useValue: scheduleSyncServiceSpy},
{provide: SettingsProvider, useValue: settingsProvider},
{provide: ConfigProvider, useValue: configProvider},
{provide: NGXLogger, useValue: ngxLogger},
{provide: StorageProvider, useValue: storageProvider},
{provide: SimpleBrowser, useValue: simpleBrowser},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it('should initialize the app', async () => {
TestBed.createComponent(AppComponent);
expect(platformSpy.ready).toHaveBeenCalled();
// await platformReadySpy;
// TODO: https://capacitorjs.com/docs/guides/mocking-plugins
// expect(splashScreenSpy.hide).toHaveBeenCalled();
});
// TODO: add more tests!
});

View File

@@ -0,0 +1,167 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {AfterContentInit, Component, NgZone} from '@angular/core';
import {Router} from '@angular/router';
import {App, URLOpenListenerEvent} from '@capacitor/app';
import {Platform, ToastController} from '@ionic/angular';
import {SettingsProvider} from './modules/settings/settings.provider';
import {AuthHelperService} from './modules/auth/auth-helper.service';
import {environment} from '../environments/environment';
import {StatusBar, Style} from '@capacitor/status-bar';
import {Capacitor} from '@capacitor/core';
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
import {NavigationBar} from '@hugotomazi/capacitor-navigation-bar';
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
/**
* TODO
*/
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
})
export class AppComponent implements AfterContentInit {
/**
* TODO
*/
pages: Array<{
/**
* TODO
*/
component: unknown;
/**
* TODO
*/
title: string;
}>;
/**
* Angular component selectors that should not infulence keyboard state
*/
ommitedEventSources = ['ion-input', 'ion-searchbar'];
/**
*
* @param platform TODO
* @param settingsProvider TODO
* @param router The angular router
* @param zone The angular zone
* @param authHelper Helper service for OAuth providers
* @param toastController Toast controller
*/
constructor(
private readonly platform: Platform,
private readonly settingsProvider: SettingsProvider,
private readonly router: Router,
private readonly zone: NgZone,
private readonly authHelper: AuthHelperService,
private readonly toastController: ToastController,
private readonly scheduleSyncService: ScheduleSyncService,
) {
void this.initializeApp();
}
ngAfterContentInit(): void {
this.scheduleSyncService.init();
void this.scheduleSyncService.enable();
}
/**
* TODO
*/
async initializeApp() {
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
this.zone.run(() => {
const slug = event.url.split(environment.app_host).pop();
if (slug) {
this.router.navigateByUrl(slug);
}
// If no match, do nothing - let regular routing
// logic take over
});
});
this.platform.ready().then(async () => {
if (Capacitor.isNativePlatform()) {
await StatusBar.setStyle({style: Style.Dark});
if (Capacitor.getPlatform() === 'android') {
await StatusBar.setBackgroundColor({
color: getComputedStyle(document.documentElement).getPropertyValue('--ion-color-primary').trim(),
});
await StatusBar.setOverlaysWebView({overlay: false});
await NavigationBar.setColor({
color: getComputedStyle(document.documentElement)
.getPropertyValue('--ion-background-color')
.trim(),
darkButtons: true,
});
}
}
await this.authNotificationsInit();
// set order of categories in settings
this.settingsProvider.setCategoriesOrder(['profile', 'privacy', 'credentials', 'others']);
});
window.addEventListener('touchmove', this.touchMoveEvent, true);
if (Capacitor.getPlatform() === 'ios') {
Keyboard.setResizeMode({mode: KeyboardResize.None});
}
}
private async authNotificationsInit() {
this.authHelper
.getProvider('default')
.events$.subscribe(action => this.showMessage(this.authHelper.getAuthMessage('default', action)));
this.authHelper
.getProvider('paia')
.events$.subscribe(action => this.showMessage(this.authHelper.getAuthMessage('paia', action)));
}
private async showMessage(message?: string) {
if (typeof message === 'undefined') {
return;
}
const toast = await this.toastController.create({
message: message,
duration: 2000,
color: 'success',
});
await toast.present();
}
/**
* Checks if keyboard should be dissmissed
*/
touchMoveEvent = (event: Event): void => {
if (
this.ommitedEventSources.includes(
(event?.target as unknown as Record<string, string>)?.['s-hn']?.toLowerCase(),
)
) {
return;
}
this.unfocusActiveElement();
};
/**
* Loses focus on the currently active element (meant for input fields).
* Results in virtual keyboard being dissmissed on native and web plattforms.
*/
unfocusActiveElement() {
const activeElement = document.activeElement;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(activeElement as any)?.blur();
}
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {CommonModule, LocationStrategy, PathLocationStrategy, registerLocaleData} from '@angular/common';
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from '@angular/common/http';
import localeDe from '@angular/common/locales/de';
import {APP_INITIALIZER, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {RouteReuseStrategy} from '@angular/router';
import {IonicModule, IonicRouteStrategy, Platform} from '@ionic/angular';
import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core';
import {TranslateHttpLoader} from '@ngx-translate/http-loader';
import moment from 'moment';
import 'moment/min/locales';
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
import SwiperCore, {FreeMode, Navigation} from 'swiper';
import {environment} from '../environments/environment';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {CatalogModule} from './modules/catalog/catalog.module';
import {ConfigModule} from './modules/config/config.module';
import {ConfigProvider} from './modules/config/config.provider';
import {DashboardModule} from './modules/dashboard/dashboard.module';
import {DataModule} from './modules/data/data.module';
import {HebisModule} from './modules/hebis/hebis.module';
import {MapModule} from './modules/map/map.module';
import {MenuModule} from './modules/menu/menu.module';
import {NewsModule} from './modules/news/news.module';
import {ScheduleModule} from './modules/schedule/schedule.module';
import {SettingsModule} from './modules/settings/settings.module';
import {SettingsProvider} from './modules/settings/settings.provider';
import {StorageModule} from './modules/storage/storage.module';
import {ThingTranslateModule} from './translation/thing-translate.module';
import {UtilModule} from './util/util.module';
import {initLogger} from './_helpers/ts-logger';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AboutModule} from './modules/about/about.module';
import {FavoritesModule} from './modules/favorites/favorites.module';
import {ProfilePageModule} from './modules/profile/profile.module';
import {FeedbackModule} from './modules/feedback/feedback.module';
import {DebugDataCollectorService} from './modules/data/debug-data-collector.service';
import {AuthModule} from './modules/auth/auth.module';
import {BackgroundModule} from './modules/background/background.module';
import {LibraryModule} from './modules/library/library.module';
import {StorageProvider} from './modules/storage/storage.provider';
import {AssessmentsModule} from './modules/assessments/assessments.module';
import {ServiceHandlerInterceptor} from './_helpers/service-handler.interceptor';
import {RoutingStackService} from './util/routing-stack.service';
import {SCSettingValue} from '@openstapps/core';
import {DefaultAuthService} from './modules/auth/default-auth.service';
import {PAIAAuthService} from './modules/auth/paia/paia-auth.service';
import {IonIconModule} from './util/ion-icon/ion-icon.module';
import {NavigationModule} from './modules/menu/navigation/navigation.module';
import {browserFactory, SimpleBrowser} from './util/browser.factory';
registerLocaleData(localeDe);
SwiperCore.use([FreeMode, Navigation]);
/**
* Initializes data needed on startup
*
* @param storageProvider provider of the saved data (using framework's storage)
* @param logger TODO
* @param settingsProvider provider of settings (e.g. language that has been set)
* @param configProvider TODO
* @param translateService TODO
* @param _routingStackService Just for init and to track the stack from the get go
*/
export function initializerFactory(
storageProvider: StorageProvider,
logger: NGXLogger,
settingsProvider: SettingsProvider,
configProvider: ConfigProvider,
translateService: TranslateService,
_routingStackService: RoutingStackService,
defaultAuthService: DefaultAuthService,
paiaAuthService: PAIAAuthService,
) {
return async () => {
initLogger(logger);
await storageProvider.init();
await configProvider.init();
await settingsProvider.init();
try {
if (configProvider.firstSession) {
// set language from browser
await settingsProvider.setSettingValue(
'profile',
'language',
translateService.getBrowserLang() as SCSettingValue,
);
}
const languageCode = (await settingsProvider.getValue('profile', 'language')) as string;
// this language will be used as a fallback when a translation isn't found in the current language
translateService.setDefaultLang('en');
translateService.use(languageCode);
moment.locale(languageCode);
await defaultAuthService.init();
await paiaAuthService.init();
} catch (error) {
logger.warn(error);
}
};
}
/**
* TODO
*
* @param http TODO
*/
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
/**
* TODO
*/
@NgModule({
bootstrap: [AppComponent],
declarations: [AppComponent],
imports: [
AboutModule,
AppRoutingModule,
AuthModule,
AssessmentsModule,
BackgroundModule,
BrowserModule,
BrowserAnimationsModule,
CatalogModule,
CommonModule,
ConfigModule,
DashboardModule,
DataModule,
HebisModule,
IonicModule.forRoot(),
IonIconModule,
FavoritesModule,
LibraryModule,
HttpClientModule,
ProfilePageModule,
FeedbackModule,
MapModule,
MenuModule,
NavigationModule,
NewsModule,
ScheduleModule,
SettingsModule,
StorageModule,
ThingTranslateModule.forRoot(),
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
deps: [HttpClient],
provide: TranslateLoader,
useFactory: createTranslateLoader,
},
}),
UtilModule,
// use maximal logging level when not in production, minimal (log only fatal errors) in production
LoggerModule.forRoot({
level: environment.production ? NgxLoggerLevel.FATAL : NgxLoggerLevel.TRACE,
}),
],
providers: [
{
provide: RouteReuseStrategy,
useClass: IonicRouteStrategy,
},
{
provide: LocationStrategy,
useClass: PathLocationStrategy,
},
{
provide: SimpleBrowser,
useFactory: browserFactory,
deps: [Platform],
},
{
provide: APP_INITIALIZER,
multi: true,
deps: [
StorageProvider,
NGXLogger,
SettingsProvider,
ConfigProvider,
TranslateService,
RoutingStackService,
DefaultAuthService,
PAIAAuthService,
],
useFactory: initializerFactory,
},
{
provide: HTTP_INTERCEPTORS,
useClass: ServiceHandlerInterceptor,
multi: true,
},
],
})
export class AppModule {
constructor(public debugDataCollectorService: DebugDataCollectorService) {}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component} from '@angular/core';
@Component({
selector: 'about-changelog',
templateUrl: 'about-changelog.html',
styleUrls: ['about-changelog.scss', './about-page/about-page.scss'],
})
export class AboutChangelogComponent {}

View File

@@ -0,0 +1,29 @@
<!--
~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-header>
<ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Changelog</ion-title>
<!-- TODO: translation -->
</ion-toolbar>
</ion-header>
<ion-content parallax>
<div class="about-changelog">
<markdown src="assets/about/CHANGELOG.md"></markdown>
</div>
</ion-content>

View File

@@ -0,0 +1,18 @@
/*!
* Copyright (C) 2021 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/>.
*/
ion-content {
--padding-start: 16px;
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {License} from './about-licenses.component';
@Component({
selector: 'about-license-modal',
templateUrl: 'about-license-modal.html',
styleUrls: ['about-license-modal.scss'],
})
export class AboutLicenseModalComponent {
@Input() license: License;
/**
* Action when close is pressed
*/
@Input() dismissAction: () => void;
}

View File

@@ -0,0 +1,30 @@
<!--
~ Copyright (C) 2021 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/>.
-->
<ion-card-header>
<ion-card-title>
<ion-label>{{ license.licenses }}</ion-label>
</ion-card-title>
<ion-button fill="clear" (click)="dismissAction()">
<ion-label>{{ 'modal.DISMISS' | translate }}</ion-label>
</ion-button>
</ion-card-header>
<ion-card-content>
<ion-content>
<ion-list>
<pre>{{ license.licenseText }}</pre>
</ion-list>
</ion-content>
</ion-card-content>

View File

@@ -0,0 +1,34 @@
/*!
* Copyright (C) 2021 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/>.
*/
ion-card-header {
ion-button {
position: absolute;
right: 0;
top: 0;
}
}
ion-card-content {
height: 100%;
ion-content {
ion-list {
pre {
white-space: pre-wrap;
}
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, OnInit} from '@angular/core';
import {ModalController} from '@ionic/angular';
import {AboutLicenseModalComponent} from './about-license-modal.component';
import licensesFile from 'src/assets/about/licenses.json';
export interface License {
name: string;
licenses: string;
repository: string;
authors?: string;
publisher?: string;
email?: string;
url?: string;
licenseText?: string;
}
@Component({
selector: 'about-changelog',
templateUrl: 'about-licenses.html',
styleUrls: ['about-licenses.scss', './about-page/about-page.scss'],
})
export class AboutLicensesComponent implements OnInit {
licenses: License[];
constructor(private modalController: ModalController) {}
ngOnInit() {
this.licenses = this.loadLicenses();
}
async viewLicense(license: License) {
const modal = await this.modalController.create({
component: AboutLicenseModalComponent,
componentProps: {
license: license,
dismissAction: () => {
modal.dismiss();
},
},
canDismiss: true,
});
return await modal.present();
}
loadLicenses(): License[] {
return licensesFile as License[];
}
}

View File

@@ -0,0 +1,51 @@
<!--
~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-header>
<ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Licenses</ion-title>
<!-- TODO: translation -->
</ion-toolbar>
</ion-header>
<ion-content parallax>
<div class="licenses-content">
<ion-card
*ngFor="let license of licenses"
[href]="license.url || license.repository"
rel="external"
target="_blank"
>
<ion-card-header>
<ion-card-title>
{{ license.name }}
<ion-icon size="16" weight="300" class="supertext-icon" name="open_in_browser"></ion-icon>
</ion-card-title>
<ion-card-subtitle *ngIf="license.authors || license.publisher">
{{ license.authors || license.publisher }}
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-chip (click)="$event.preventDefault(); viewLicense(license)">
<ion-icon name="copyright"></ion-icon>
<ion-label>{{ license.licenses }} License</ion-label>
</ion-chip>
</ion-card-content>
</ion-card>
</div>
</ion-content>

View File

@@ -0,0 +1,38 @@
/*!
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
ion-content > div {
height: 100%;
}
cdk-virtual-scroll-viewport {
height: 100%;
width: 100%;
}
:host ::ng-deep {
.cdk-virtual-scroll-content-wrapper {
width: 100%;
}
}
.virtual-scroll-expander {
clear: both;
}
.supertext-icon {
vertical-align: text-top;
height: 14px;
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCAboutPageContent} from '@openstapps/core';
@Component({
selector: 'about-page-content',
templateUrl: 'about-page-content.html',
styleUrls: ['about-page-content.scss'],
})
export class AboutPageContentComponent {
@Input() content: SCAboutPageContent;
isSimpleTextContent(content: unknown | string): content is string {
return typeof content === 'string';
}
}

View File

@@ -0,0 +1,43 @@
<!--
~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<div [ngSwitch]="content.type">
<markdown [data]="'value' | translateSimple: content" *ngSwitchCase="'markdown'"></markdown>
<div *ngSwitchCase="'section'">
<ion-card *ngIf="content.card; else noCard">
<ion-card-header>
<ion-card-title>{{ 'title' | translateSimple: content }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<about-page-content [content]="content.content"></about-page-content>
</ion-card-content>
</ion-card>
<ng-template #noCard>
<h2>{{ 'title' | translateSimple: content }}</h2>
<about-page-content [content]="content.content"></about-page-content>
</ng-template>
</div>
<ion-grid *ngSwitchCase="'table'">
<ion-row *ngFor="let row of content.rows">
<ion-col *ngFor="let col of row">
<about-page-content [content]="col"></about-page-content>
</ion-col>
</ion-row>
</ion-grid>
<ion-item *ngSwitchCase="'router link'" [routerLink]="content.link" fill="clear">
<ion-icon *ngIf="content.icon" [name]="content.icon" slot="start"></ion-icon>
<ion-label>{{ 'title' | translateSimple: content }}</ion-label>
</ion-item>
</div>

View File

@@ -0,0 +1,14 @@
/*!
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider';
import packageJson from '../../../../../package.json';
import config from 'capacitor.config';
@Component({
selector: 'about-page',
templateUrl: 'about-page.html',
styleUrls: ['about-page.scss'],
})
export class AboutPageComponent implements OnInit {
content: SCAboutPage;
appName = config.appName;
version = packageJson.version;
constructor(private readonly route: ActivatedRoute, private readonly configProvider: ConfigProvider) {}
async ngOnInit() {
const route = this.route.snapshot.url.map(it => it.path).join('/');
this.content =
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
}
}

View File

@@ -0,0 +1,32 @@
<!--
~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-header>
<ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title *ngIf="content; else titleLoading">{{ 'title' | translateSimple: content }}</ion-title>
<ng-template #titleLoading>
<ion-title><ion-skeleton-text animated="true"></ion-skeleton-text></ion-title>
</ng-template>
</ion-toolbar>
</ion-header>
<ion-content parallax *ngIf="content">
<ion-text>{{ appName }} v{{ version }}</ion-text>
<div class="page-content">
<about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content>
</div>
</ion-content>

View File

@@ -0,0 +1,91 @@
/*!
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
@import 'src/theme/util/_mixins.scss';
ion-text {
margin-inline: var(--spacing-md);
}
:host ::ng-deep {
ion-card {
margin: 0;
box-shadow: none;
ion-card-content {
h1 {
margin: 0;
}
padding-bottom: 8px;
}
ion-card-header {
color: var(--ion-color-dark);
padding-top: 8px;
padding-bottom: 4px;
font-weight: bold;
}
ion-grid,
ion-col {
padding-inline-start: 0;
padding-top: 0;
padding-bottom: 0;
}
}
ion-grid,
ion-col {
padding-inline-start: 0;
padding-top: 0;
padding-bottom: 0;
}
p,
h3,
h2,
h1 {
margin-inline: var(--spacing-md);
}
.about-changelog,
.licenses-content,
.page-content {
margin: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
background: var(--ion-item-background);
padding-block-end: var(--spacing-md);
@include border-radius-in-parallax(var(--border-radius-default));
& > * {
ion-card-subtitle {
font-size: var(--font-size-lg);
color: var(--ion-color-light-contrast);
}
display: block;
@include border-radius-in-parallax(var(--border-radius-default));
overflow: hidden;
position: relative;
margin: 0;
& > ion-thumbnail {
background: var(--ion-color-primary);
}
}
}
}
ion-text {
color: var(--ion-color-primary-contrast);
}

View File

@@ -0,0 +1,69 @@
/*
* 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 {RouterModule, Routes} from '@angular/router';
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {ConfigProvider} from '../config/config.provider';
import {AboutPageComponent} from './about-page/about-page.component';
import {MarkdownModule} from 'ngx-markdown';
import {AboutPageContentComponent} from './about-page/about-page-content.component';
import {AboutLicensesComponent} from './about-licenses.component';
import {DataModule} from '../data/data.module';
import {ScrollingModule} from '@angular/cdk/scrolling';
import {AboutLicenseModalComponent} from './about-license-modal.component';
import {AboutChangelogComponent} from './about-changelog.component';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
const settingsRoutes: Routes = [
{path: 'about', component: AboutPageComponent},
{path: 'about/changelog', component: AboutChangelogComponent},
{path: 'about/imprint', component: AboutPageComponent},
{path: 'about/privacy', component: AboutPageComponent},
{path: 'about/terms', component: AboutPageComponent},
{path: 'about/licenses', component: AboutLicensesComponent},
];
/**
* Settings Module
*/
@NgModule({
declarations: [
AboutPageComponent,
AboutPageContentComponent,
AboutLicensesComponent,
AboutLicenseModalComponent,
AboutChangelogComponent,
],
imports: [
CommonModule,
IonIconModule,
FormsModule,
IonicModule.forRoot(),
TranslateModule.forChild(),
ThingTranslateModule.forChild(),
RouterModule.forChild(settingsRoutes),
MarkdownModule,
DataModule,
ScrollingModule,
UtilModule,
],
providers: [ConfigProvider],
})
export class AboutModule {}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
/*
* 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 {NgModule} from '@angular/core';
import {AssessmentListItemComponent} from './types/assessment/assessment-list-item.component';
import {AssessmentBaseInfoComponent} from './types/assessment/assessment-base-info.component';
import {AssessmentDetailComponent} from './types/assessment/assessment-detail.component';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {DataModule} from '../data/data.module';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {CourseOfStudyAssessmentComponent} from './types/course-of-study/course-of-study-assessment.component';
import {AssessmentsPageComponent} from './page/assessments-page.component';
import {RouterModule} from '@angular/router';
import {AuthGuardService} from '../auth/auth-guard.service';
import {MomentModule} from 'ngx-moment';
import {AssessmentsListItemComponent} from './list/assessments-list-item.component';
import {AssessmentsDataListComponent} from './list/assessments-data-list.component';
import {AssessmentsDetailComponent} from './detail/assessments-detail.component';
import {AssessmentsProvider} from './assessments.provider';
import {AssessmentsSimpleDataListComponent} from './list/assessments-simple-data-list.component';
import {ProtectedRoutes} from '../auth/protected.routes';
import {AssessmentsTreeListComponent} from './list/assessments-tree-list.component';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {UtilModule} from '../../util/util.module';
const routes: ProtectedRoutes = [
{
path: 'assessments',
component: AssessmentsPageComponent,
data: {authProvider: 'default'},
canActivate: [AuthGuardService],
},
{
path: 'assessments/detail/:uid',
component: AssessmentsDetailComponent,
data: {authProvider: 'default'},
canActivate: [AuthGuardService],
},
];
@NgModule({
declarations: [
AssessmentListItemComponent,
AssessmentBaseInfoComponent,
AssessmentDetailComponent,
AssessmentsListItemComponent,
AssessmentsTreeListComponent,
CourseOfStudyAssessmentComponent,
AssessmentsPageComponent,
AssessmentsDataListComponent,
AssessmentsDetailComponent,
AssessmentsSimpleDataListComponent,
],
imports: [
CommonModule,
FormsModule,
IonIconModule,
IonicModule,
RouterModule.forChild(routes),
TranslateModule,
DataModule,
ThingTranslateModule,
MomentModule,
UtilModule,
],
providers: [AssessmentsProvider],
exports: [],
})
export class AssessmentsModule {}

View File

@@ -0,0 +1,115 @@
/*
* 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 {ConfigProvider} from '../config/config.provider';
import {SCAssessment, SCUuid} from '@openstapps/core';
import {DefaultAuthService} from '../auth/default-auth.service';
import {HttpClient} from '@angular/common/http';
import {uniqBy} from '../../_helpers/collections/uniq';
import {keyBy} from '../../_helpers/collections/key-by';
/**
*
*/
export function toAssessmentMap(data: SCAssessment[]): Record<SCUuid, SCAssessment> {
return keyBy(
uniqBy(
[
...data,
...data.flatMap<SCAssessment>(
assessment =>
[...(assessment.superAssessments ?? [])]
.reverse()
.map<SCAssessment>((superAssessment, index, array) => {
const superAssessmentCopy = {
...superAssessment,
} as SCAssessment;
superAssessmentCopy.origin = assessment.origin;
superAssessmentCopy.superAssessments = array.slice(index + 1).reverse();
return superAssessmentCopy;
}) ?? [],
),
] as SCAssessment[],
it => it.uid,
),
it => it.uid,
);
}
@Injectable({
providedIn: 'root',
})
export class AssessmentsProvider {
assessmentPath = 'assessments';
// usually this wouldn't be necessary, but the assessment service
// is very aggressive about too many requests being made to the server
cache?: Promise<SCAssessment[]>;
assessments: Promise<Record<SCUuid, SCAssessment>>;
cacheTimestamp = 0;
// 15 minutes
cacheMaxAge = 15 * 60 * 1000;
constructor(
readonly configProvider: ConfigProvider,
readonly defaultAuth: DefaultAuthService,
readonly http: HttpClient,
) {}
async getAssessment(uid: SCUuid, accessToken?: string | null, forceFetch = false): Promise<SCAssessment> {
await this.getAssessments(accessToken, forceFetch);
return (await this.assessments)[uid];
}
async getAssessments(accessToken?: string | null, forceFetch = false): Promise<SCAssessment[]> {
// again, this is a hack to get around the fact that the assessment service
// is very aggressive how many requests you can make, so it can happen
// during development that simply by reloading pages over and over again
// the assessment service will block you
if (accessToken === 'mock' && !this.cache) {
this.cacheTimestamp = Date.now();
this.cache = import('./assessment-mock-data.json').then(it => it.data as SCAssessment[]);
this.assessments = this.cache.then(toAssessmentMap);
}
if (this.cache && !forceFetch && Date.now() - this.cacheTimestamp < this.cacheMaxAge) {
return this.cache;
}
const url = this.configProvider.config.app.features.extern?.hisometry.url;
if (!url) throw new Error('Config lacks url for hisometry');
this.cache = this.http
.get<{data: SCAssessment[]}>(`${url}/${this.assessmentPath}`, {
headers: {
Authorization: `Bearer ${accessToken ?? (await this.defaultAuth.getValidToken()).accessToken}`,
},
})
.toPromise()
.then(it => {
this.cacheTimestamp = Date.now();
return it?.data ?? [];
});
this.assessments = this.cache.then(toAssessmentMap);
return this.cache;
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {AssessmentsProvider} from '../assessments.provider';
import {DataDetailComponent, ExternalDataLoadEvent} from '../../data/detail/data-detail.component';
import {NavController, ViewWillEnter} from '@ionic/angular';
import {Subscription} from 'rxjs';
import {DataRoutingService} from '../../data/data-routing.service';
import {SCAssessment} from '@openstapps/core';
@Component({
selector: 'assessments-detail',
templateUrl: 'assessments-detail.html',
styleUrls: ['assessments-detail.scss'],
})
export class AssessmentsDetailComponent implements ViewWillEnter, OnInit, OnDestroy {
constructor(
readonly route: ActivatedRoute,
readonly assessmentsProvider: AssessmentsProvider,
readonly dataRoutingService: DataRoutingService,
readonly navController: NavController,
readonly activatedRoute: ActivatedRoute,
) {}
subscriptions: Subscription[] = [];
@Input() dataPathAutoRouting = true;
@ViewChild(DataDetailComponent)
detailComponent: DataDetailComponent;
item: SCAssessment;
ngOnInit() {
if (!this.dataPathAutoRouting) return;
this.subscriptions.push(
this.dataRoutingService.pathSelectListener().subscribe(item => {
void this.navController.navigateBack(['assessments', 'detail', item.uid], {
queryParams: {
token: this.activatedRoute.snapshot.queryParamMap.get('token'),
},
});
}),
);
}
ngOnDestroy() {
for (const sub of this.subscriptions) sub.unsubscribe();
}
getItem(event: ExternalDataLoadEvent) {
this.assessmentsProvider
.getAssessment(event.uid, this.route.snapshot.queryParamMap.get('token'), event.forceReload)
.then(assessment => {
this.item = assessment;
event.resolve(this.item);
});
}
async ionViewWillEnter() {
await this.detailComponent.ionViewWillEnter();
}
}

View File

@@ -0,0 +1,32 @@
<!--
~ 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/>.
-->
<ion-header>
<ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>{{ 'data.detail.TITLE' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<stapps-data-detail
[autoRouteDataPath]="false"
[externalData]="true"
(loadItem)="getItem($event)"
[defaultHeader]="false"
>
<ng-template let-item>
<assessment-detail [item]="item"></assessment-detail>
</ng-template>
</stapps-data-detail>

View File

@@ -0,0 +1,18 @@
/*!
* 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/>.
*/
stapps-data-detail {
height: 100%;
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {Observable} from 'rxjs';
@Component({
selector: 'assessments-data-list',
templateUrl: './assessments-data-list.html',
styleUrls: ['./assessments-data-list.scss'],
})
export class AssessmentsDataListComponent {
/**
* All SCThings to display
*/
@Input() items?: SCThings[];
/**
* Output binding to trigger pagination fetch
*/
// eslint-disable-next-line @angular-eslint/no-output-rename
@Output('loadmore') loadMore = new EventEmitter<void>();
/**
* Emits when scroll view should reset to top
*/
@Input() resetToTop?: Observable<void>;
/**
* Indicates whether the list is to display SCThings of a single type
*/
@Input() singleType = false;
/**
* Signalizes that the data is being loaded
*/
@Input() loading = true;
}

View File

@@ -0,0 +1,29 @@
<!--
~ 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/>.
-->
<stapps-data-list
[items]="items"
[loading]="loading"
[singleType]="singleType"
[resetToTop]="resetToTop"
(loadmore)="loadMore.emit($event)"
>
<ng-template let-item>
<assessments-list-item [item]="item" [hideThumbnail]="singleType"></assessments-list-item>
</ng-template>
<ng-container header>
<ng-content select="[header]"></ng-content>
</ng-container>
</stapps-data-list>

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCThings} from '@openstapps/core';
@Component({
selector: 'assessments-list-item',
templateUrl: 'assessments-list-item.html',
styleUrls: ['assessments-list-item.scss'],
})
export class AssessmentsListItemComponent {
/**
* Whether the list item should show a thumbnail
*/
@Input() hideThumbnail = false;
/**
* An item to show
*/
@Input() item: SCThings;
}

View File

@@ -0,0 +1,25 @@
<!--
~ 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/>.
-->
<stapps-data-list-item
[item]="item"
[hideThumbnail]="hideThumbnail"
[lines]="'none'"
[favoriteButton]="false"
>
<ng-template let-data>
<stapps-assessment-list-item [item]="data"></stapps-assessment-list-item>
</ng-template>
</stapps-data-list-item>

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {Subscription} from 'rxjs';
import {DataRoutingService} from '../../data/data-routing.service';
import {ActivatedRoute, Router} from '@angular/router';
@Component({
selector: 'assessments-simple-data-list',
templateUrl: 'assessments-simple-data-list.html',
styleUrls: ['assessments-simple-data-list.scss'],
})
export class AssessmentsSimpleDataListComponent implements OnInit, OnDestroy {
/**
* All SCThings to display
*/
_items?: Promise<SCThings[] | undefined>;
/**
* Indicates whether or not the list is to display SCThings of a single type
*/
@Input() singleType = false;
/**
* List header
*/
@Input() listHeader?: string;
@Input() set items(items: SCThings[] | undefined) {
this._items = new Promise(resolve => resolve(items));
}
subscriptions: Subscription[] = [];
constructor(
readonly dataRoutingService: DataRoutingService,
readonly router: Router,
readonly activatedRoute: ActivatedRoute,
) {}
ngOnInit() {
this.subscriptions.push(
this.dataRoutingService.itemSelectListener().subscribe(thing => {
void this.router.navigate(['assessments', 'detail', thing.uid], {
queryParams: {
token: this.activatedRoute.snapshot.queryParamMap.get('token'),
},
});
}),
);
}
ngOnDestroy() {
for (const subscription of this.subscriptions) subscription.unsubscribe();
}
}

View File

@@ -0,0 +1,25 @@
<!--
~ 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/>.
-->
<stapps-simple-data-list
[singleType]="singleType"
[items]="_items"
[listHeader]="listHeader"
[autoRouting]="false"
>
<ng-template let-item>
<assessments-list-item [item]="item" [hideThumbnail]="singleType"></assessments-list-item>
</ng-template>
</stapps-simple-data-list>

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCThings} from '@openstapps/core';
@Component({
selector: 'assessments-tree-list',
templateUrl: 'assessments-tree-list.html',
styleUrls: ['assessments-tree-list.scss'],
})
export class AssessmentsTreeListComponent {
@Input() items?: Promise<SCThings[] | undefined>;
@Input() singleType = false;
@Input() groupingKey: string;
}

View File

@@ -0,0 +1,20 @@
<!--
~ 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/>.
-->
<tree-list [items]="items" [singleType]="singleType" [groupingKey]="groupingKey">
<ng-template let-item>
<assessments-list-item [item]="item" [hideThumbnail]="singleType"></assessments-list-item>
</ng-template>
</tree-list>

View File

@@ -0,0 +1,14 @@
/*!
* 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/>.
*/

View File

@@ -0,0 +1,110 @@
/*
* 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 {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {AssessmentsProvider} from '../assessments.provider';
import {SCAssessment, SCCourseOfStudy} from '@openstapps/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Subscription} from 'rxjs';
import {NGXLogger} from 'ngx-logger';
import {materialSharedAxisX} from '../../../animation/material-motion';
import {SharedAxisChoreographer} from '../../../animation/animation-choreographer';
import {DataProvider, DataScope} from '../../data/data.provider';
import {DataRoutingService} from '../../data/data-routing.service';
import {groupBy} from '../../../_helpers/collections/group-by';
import {mapValues} from '../../../_helpers/collections/map-values';
@Component({
selector: 'app-assessments-page',
templateUrl: 'assessments-page.html',
styleUrls: ['assessments-page.scss'],
animations: [materialSharedAxisX],
})
export class AssessmentsPageComponent implements OnInit, AfterViewInit, OnDestroy {
assessments: Promise<
Record<
string,
{
assessments: SCAssessment[];
courseOfStudy: Promise<SCCourseOfStudy | undefined>;
}
>
>;
assessmentKeys: string[] = [];
routingSubscription: Subscription;
@ViewChild('segment') segmentView!: HTMLIonSegmentElement;
sharedAxisChoreographer: SharedAxisChoreographer<string> = new SharedAxisChoreographer<string>('', []);
constructor(
readonly logger: NGXLogger,
readonly assessmentsProvider: AssessmentsProvider,
readonly dataProvider: DataProvider,
readonly activatedRoute: ActivatedRoute,
readonly dataRoutingService: DataRoutingService,
readonly router: Router,
) {}
ngAfterViewInit() {
this.segmentView.value = this.sharedAxisChoreographer.currentValue;
}
ngOnDestroy() {
this.routingSubscription.unsubscribe();
}
ngOnInit() {
this.routingSubscription = this.dataRoutingService.itemSelectListener().subscribe(thing => {
void this.router.navigate(['assessments', 'detail', thing.uid], {
queryParams: {
token: this.activatedRoute.snapshot.queryParamMap.get('token'),
},
});
});
this.activatedRoute.queryParams.subscribe(parameters => {
try {
this.assessments = this.assessmentsProvider
.getAssessments(parameters.token)
.then(assessments => groupBy(assessments, it => it.courseOfStudy?.uid ?? 'unknown'))
.then(it => {
this.assessmentKeys = Object.keys(it);
this.sharedAxisChoreographer = new SharedAxisChoreographer(
this.assessmentKeys[0],
this.assessmentKeys,
);
if (this.segmentView) {
this.segmentView.value = this.sharedAxisChoreographer.currentValue;
}
return it;
})
.then(groups =>
mapValues(groups, (group, uid) => ({
assessments: group,
courseOfStudy: this.dataProvider
.get(uid, DataScope.Remote)
.catch(() => group[0].courseOfStudy) as Promise<SCCourseOfStudy>,
})),
);
} catch (error) {
this.logger.error(error);
this.assessments = Promise.resolve({});
}
});
}
}

View File

@@ -0,0 +1,59 @@
<!--
~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-header>
<ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>{{ 'assessments.TITLE' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-segment
#segment
[scrollable]="true"
mode="md"
(ionChange)="sharedAxisChoreographer.changeViewForState(segment.value)"
value=""
>
<ion-segment-button *ngFor="let key of assessmentKeys" [value]="key">
<div *ngIf="assessments | async as assessments">
<ion-label
class="ion-text-wrap"
*ngIf="assessments[key].courseOfStudy | async as course; else defaultLabel"
>
{{ 'name' | thingTranslate: course }} ({{ 'academicDegree' | thingTranslate: course }})
</ion-label>
</div>
<ng-template #defaultLabel>
<ion-label>{{ key }}</ion-label>
</ng-template>
</ion-segment-button>
</ion-segment>
<div
[ngSwitch]="sharedAxisChoreographer.currentValue"
[@materialSharedAxisX]="sharedAxisChoreographer.animationState"
(@materialSharedAxisX.done)="sharedAxisChoreographer.animationDone()"
*ngIf="assessments | async as items"
class="content"
>
<course-of-study-assessment
[assessments]="items[sharedAxisChoreographer.currentValue].assessments"
[courseOfStudy]="items[sharedAxisChoreographer.currentValue].courseOfStudy | async"
></course-of-study-assessment>
</div>
</ion-content>

View File

@@ -0,0 +1,20 @@
/*!
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
.content {
height: 100%;
padding-inline: 8px;
--ion-item-background: var(--ion-background-color);
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCAssessment} from '@openstapps/core';
@Component({
selector: 'assessment-base-info',
templateUrl: 'assessment-base-info.html',
styleUrls: ['assessment-base-info.scss'],
})
export class AssessmentBaseInfoComponent {
_item: SCAssessment;
passed = false;
@Input() set item(item: SCAssessment) {
this._item = item;
this.passed = !/^(5[,.]0)|FX?$/i.test(item.grade);
}
}

View File

@@ -0,0 +1,29 @@
<!--
~ 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/>.
-->
<ion-label [color]="passed ? undefined : 'danger'"
>{{
(_item.grade | isNumeric)
? (_item.grade | numberLocalized: 'minimumFractionDigits:1,maximumFractionDigits:1')
: ''
}}
{{ 'status' | thingTranslate: _item | titlecase }},
{{ 'attempt' | propertyNameTranslate: _item }}
{{ _item.attempt }}
</ion-label>
<ion-note>
{{ _item.ects }}
{{ 'ects' | propertyNameTranslate: _item }}</ion-note
>

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCAssessment} from '@openstapps/core';
@Component({
selector: 'assessment-detail',
templateUrl: 'assessment-detail.html',
styleUrls: ['assessment-detail.scss'],
})
export class AssessmentDetailComponent {
@Input() item: SCAssessment;
}

View File

@@ -0,0 +1,29 @@
<!--
~ 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/>.
-->
<ion-card>
<ion-card-content>
<ion-note *ngIf="item.courseOfStudy as courseOfStudy">
{{ $any('courseOfStudy' | propertyNameTranslate: item) | titlecase }}:
{{ 'name' | thingTranslate: $any(courseOfStudy) }}
({{ 'academicDegree' | thingTranslate: $any(courseOfStudy) }})
</ion-note>
</ion-card-content>
</ion-card>
<ion-list class="container">
<ion-item lines="none">
<assessment-base-info [item]="item"></assessment-base-info>
</ion-item>
</ion-list>

View File

@@ -0,0 +1,4 @@
stapps-data-list {
height: 100px;
width: 100%;
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCAssessment} from '@openstapps/core';
@Component({
selector: 'stapps-assessment-list-item',
templateUrl: './assessment-list-item.html',
styleUrls: ['./assessment-list-item.scss'],
})
export class AssessmentListItemComponent {
@Input() item: SCAssessment;
}

View File

@@ -0,0 +1,22 @@
<!--
~ 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/>.
-->
<div class="container">
<h2 class="name">
{{ 'name' | thingTranslate: item }}
{{ item.date ? (item.date | amDateFormat) : '' }}
</h2>
<assessment-base-info [item]="item"></assessment-base-info>
</div>

View File

@@ -0,0 +1,40 @@
/*!
* 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/>.
*/
.column {
display: flex;
flex-direction: column;
}
.item {
height: 72px;
}
.tree-indicator {
width: 16px;
margin-right: 4px;
padding-left: 1px;
}
.super-assessments-list {
// prevent the list from hijacking hover overlays
z-index: -1;
:last-child {
.tree-indicator-after {
display: none;
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCAssessment, SCCourseOfStudyWithoutReferences} from '@openstapps/core';
@Component({
selector: 'course-of-study-assessment',
templateUrl: 'course-of-study-assessment.html',
styleUrls: ['course-of-study-assessment.scss'],
})
export class CourseOfStudyAssessmentComponent {
@Input() courseOfStudy: SCCourseOfStudyWithoutReferences | null;
_assessments: Promise<SCAssessment[]>;
@Input() set assessments(value: SCAssessment[]) {
this._assessments = Promise.resolve(value);
}
}

View File

@@ -0,0 +1,23 @@
<!--
~ 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/>.
-->
<section>
<h3>
{{ 'assessments.courseOfStudyAssessments.ASSESSMENTS' | translate }}
</h3>
<assessments-tree-list [items]="_assessments" [singleType]="true" [groupingKey]="'superAssessments'">
</assessments-tree-list>
</section>

View File

@@ -0,0 +1,14 @@
/*!
* 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/>.
*/

Some files were not shown because too many files have changed in this diff Show More